diff --git a/Anchor.toml b/Anchor.toml index 8631130..0c02eec 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -1,10 +1,15 @@ [toolchain] package_manager = "yarn" +anchor_version = "0.31.0" [features] resolution = true skip-lint = false +[programs.mainnet] +lazorkit = "J6Big9w1VNeRZgDWH5qmNz2Nd6XFq5QeZbqC8caqSE5W" +default_rule = "CNT2aEgxucQjmt5SRsA6hSGrt241Bvc9zsgPvSuMjQTE" + [programs.devnet] lazorkit = "J6Big9w1VNeRZgDWH5qmNz2Nd6XFq5QeZbqC8caqSE5W" default_rule = "CNT2aEgxucQjmt5SRsA6hSGrt241Bvc9zsgPvSuMjQTE" @@ -17,8 +22,8 @@ default_rule = "CNT2aEgxucQjmt5SRsA6hSGrt241Bvc9zsgPvSuMjQTE" url = "https://api.apr.dev" [provider] -cluster = "https://rpc.shyft.to?api_key=gaxCgX8-zR24VN60" -wallet = "~/.config/solana/mainnet_deployer.json" +cluster = "devnet" +wallet = "~/.config/solana/id.json" [scripts] test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.test.ts" diff --git a/contract-integration/anchor/idl/default_rule.json b/contract-integration/anchor/idl/default_rule.json new file mode 100644 index 0000000..87ad792 --- /dev/null +++ b/contract-integration/anchor/idl/default_rule.json @@ -0,0 +1,272 @@ +{ + "address": "CNT2aEgxucQjmt5SRsA6hSGrt241Bvc9zsgPvSuMjQTE", + "metadata": { + "name": "default_rule", + "version": "0.1.0", + "spec": "0.1.0", + "description": "Created with Anchor" + }, + "instructions": [ + { + "name": "add_device", + "discriminator": [ + 21, + 27, + 66, + 42, + 18, + 30, + 14, + 18 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "smart_wallet_authenticator", + "signer": true + }, + { + "name": "new_smart_wallet_authenticator" + }, + { + "name": "rule", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 114, + 117, + 108, + 101 + ] + }, + { + "kind": "account", + "path": "smart_wallet_authenticator" + } + ] + } + }, + { + "name": "new_rule", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 114, + 117, + 108, + 101 + ] + }, + { + "kind": "account", + "path": "new_smart_wallet_authenticator" + } + ] + } + }, + { + "name": "lazorkit", + "address": "J6Big9w1VNeRZgDWH5qmNz2Nd6XFq5QeZbqC8caqSE5W" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "check_rule", + "discriminator": [ + 215, + 90, + 220, + 175, + 191, + 212, + 144, + 147 + ], + "accounts": [ + { + "name": "smart_wallet_authenticator", + "signer": true + }, + { + "name": "rule", + "writable": true + } + ], + "args": [] + }, + { + "name": "init_rule", + "discriminator": [ + 129, + 224, + 96, + 169, + 247, + 125, + 74, + 118 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "smart_wallet" + }, + { + "name": "smart_wallet_authenticator", + "docs": [ + "CHECK" + ], + "signer": true + }, + { + "name": "rule", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 114, + 117, + 108, + 101 + ] + }, + { + "kind": "account", + "path": "smart_wallet_authenticator" + } + ] + } + }, + { + "name": "lazorkit", + "address": "J6Big9w1VNeRZgDWH5qmNz2Nd6XFq5QeZbqC8caqSE5W" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [] + } + ], + "accounts": [ + { + "name": "Rule", + "discriminator": [ + 82, + 10, + 53, + 40, + 250, + 61, + 143, + 130 + ] + }, + { + "name": "SmartWalletAuthenticator", + "discriminator": [ + 126, + 36, + 85, + 166, + 77, + 139, + 221, + 129 + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "InvalidPasskey" + }, + { + "code": 6001, + "name": "UnAuthorize" + } + ], + "types": [ + { + "name": "Rule", + "type": { + "kind": "struct", + "fields": [ + { + "name": "smart_wallet", + "type": "pubkey" + }, + { + "name": "smart_wallet_authenticator", + "type": "pubkey" + } + ] + } + }, + { + "name": "SmartWalletAuthenticator", + "docs": [ + "Account that stores authentication data for a smart wallet" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "passkey_pubkey", + "docs": [ + "The public key of the passkey that can authorize transactions" + ], + "type": { + "array": [ + "u8", + 33 + ] + } + }, + { + "name": "smart_wallet", + "docs": [ + "The smart wallet this authenticator belongs to" + ], + "type": "pubkey" + }, + { + "name": "credential_id", + "docs": [ + "The credential ID this authenticator belongs to" + ], + "type": "bytes" + }, + { + "name": "bump", + "docs": [ + "Bump seed for PDA derivation" + ], + "type": "u8" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/contract-integration/anchor/idl/lazorkit.json b/contract-integration/anchor/idl/lazorkit.json new file mode 100644 index 0000000..186b715 --- /dev/null +++ b/contract-integration/anchor/idl/lazorkit.json @@ -0,0 +1,2895 @@ +{ + "address": "J6Big9w1VNeRZgDWH5qmNz2Nd6XFq5QeZbqC8caqSE5W", + "metadata": { + "name": "lazorkit", + "version": "0.1.0", + "spec": "0.1.0", + "description": "Created with Anchor" + }, + "docs": [ + "The Lazor Kit program provides smart wallet functionality with passkey authentication" + ], + "instructions": [ + { + "name": "add_whitelist_rule_program", + "docs": [ + "Add a program to the whitelist of rule programs" + ], + "discriminator": [ + 133, + 37, + 74, + 189, + 59, + 238, + 188, + 210 + ], + "accounts": [ + { + "name": "authority", + "writable": true, + "signer": true, + "relations": [ + "config" + ] + }, + { + "name": "config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + }, + { + "name": "whitelist_rule_programs", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 119, + 104, + 105, + 116, + 101, + 108, + 105, + 115, + 116, + 95, + 114, + 117, + 108, + 101, + 95, + 112, + 114, + 111, + 103, + 114, + 97, + 109, + 115 + ] + } + ] + } + } + ], + "args": [] + }, + { + "name": "call_rule_direct", + "discriminator": [ + 97, + 234, + 75, + 197, + 171, + 164, + 239, + 65 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + }, + { + "name": "smart_wallet", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 109, + 97, + 114, + 116, + 95, + 119, + 97, + 108, + 108, + 101, + 116 + ] + }, + { + "kind": "account", + "path": "smart_wallet_config.id", + "account": "SmartWalletConfig" + } + ] + } + }, + { + "name": "smart_wallet_config", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 109, + 97, + 114, + 116, + 95, + 119, + 97, + 108, + 108, + 101, + 116, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "smart_wallet" + } + ] + } + }, + { + "name": "smart_wallet_authenticator" + }, + { + "name": "rule_program" + }, + { + "name": "whitelist_rule_programs", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 119, + 104, + 105, + 116, + 101, + 108, + 105, + 115, + 116, + 95, + 114, + 117, + 108, + 101, + 95, + 112, + 114, + 111, + 103, + 114, + 97, + 109, + 115 + ] + } + ] + } + }, + { + "name": "new_smart_wallet_authenticator", + "docs": [ + "Optional new authenticator to initialize when requested in message" + ], + "optional": true + }, + { + "name": "ix_sysvar", + "address": "Sysvar1nstructions1111111111111111111111111" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": { + "name": "CallRuleArgs" + } + } + } + ] + }, + { + "name": "change_rule_direct", + "discriminator": [ + 117, + 33, + 70, + 46, + 48, + 232, + 110, + 70 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + }, + { + "name": "smart_wallet", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 109, + 97, + 114, + 116, + 95, + 119, + 97, + 108, + 108, + 101, + 116 + ] + }, + { + "kind": "account", + "path": "smart_wallet_config.id", + "account": "SmartWalletConfig" + } + ] + } + }, + { + "name": "smart_wallet_config", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 109, + 97, + 114, + 116, + 95, + 119, + 97, + 108, + 108, + 101, + 116, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "smart_wallet" + } + ] + } + }, + { + "name": "smart_wallet_authenticator" + }, + { + "name": "old_rule_program", + "docs": [ + "CHECK" + ] + }, + { + "name": "new_rule_program", + "docs": [ + "CHECK" + ] + }, + { + "name": "whitelist_rule_programs", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 119, + 104, + 105, + 116, + 101, + 108, + 105, + 115, + 116, + 95, + 114, + 117, + 108, + 101, + 95, + 112, + 114, + 111, + 103, + 114, + 97, + 109, + 115 + ] + } + ] + } + }, + { + "name": "ix_sysvar", + "docs": [ + "CHECK" + ], + "address": "Sysvar1nstructions1111111111111111111111111" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": { + "name": "ChangeRuleArgs" + } + } + } + ] + }, + { + "name": "commit_cpi", + "discriminator": [ + 74, + 89, + 187, + 45, + 241, + 147, + 133, + 62 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + }, + { + "name": "smart_wallet", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 109, + 97, + 114, + 116, + 95, + 119, + 97, + 108, + 108, + 101, + 116 + ] + }, + { + "kind": "account", + "path": "smart_wallet_config.id", + "account": "SmartWalletConfig" + } + ] + } + }, + { + "name": "smart_wallet_config", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 109, + 97, + 114, + 116, + 95, + 119, + 97, + 108, + 108, + 101, + 116, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "smart_wallet" + } + ] + } + }, + { + "name": "smart_wallet_authenticator", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 109, + 97, + 114, + 116, + 95, + 119, + 97, + 108, + 108, + 101, + 116, + 95, + 97, + 117, + 116, + 104, + 101, + 110, + 116, + 105, + 99, + 97, + 116, + 111, + 114 + ] + }, + { + "kind": "account", + "path": "smart_wallet" + }, + { + "kind": "arg", + "path": "args.passkey_pubkey.to_hashed_bytes(smart_wallet" + } + ] + } + }, + { + "name": "whitelist_rule_programs", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 119, + 104, + 105, + 116, + 101, + 108, + 105, + 115, + 116, + 95, + 114, + 117, + 108, + 101, + 95, + 112, + 114, + 111, + 103, + 114, + 97, + 109, + 115 + ] + } + ] + } + }, + { + "name": "authenticator_program", + "docs": [ + "Rule program for optional policy enforcement at commit time" + ] + }, + { + "name": "cpi_commit", + "docs": [ + "New commit account (rent payer: payer)" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 112, + 105, + 95, + 99, + 111, + 109, + 109, + 105, + 116 + ] + }, + { + "kind": "account", + "path": "smart_wallet" + }, + { + "kind": "account", + "path": "smart_wallet_config.last_nonce", + "account": "SmartWalletConfig" + } + ] + } + }, + { + "name": "ix_sysvar", + "address": "Sysvar1nstructions1111111111111111111111111" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": { + "name": "CommitArgs" + } + } + } + ] + }, + { + "name": "create_smart_wallet", + "docs": [ + "Create a new smart wallet with passkey authentication" + ], + "discriminator": [ + 129, + 39, + 235, + 18, + 132, + 68, + 203, + 19 + ], + "accounts": [ + { + "name": "signer", + "writable": true, + "signer": true + }, + { + "name": "whitelist_rule_programs", + "docs": [ + "Whitelist of allowed rule programs" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 119, + 104, + 105, + 116, + 101, + 108, + 105, + 115, + 116, + 95, + 114, + 117, + 108, + 101, + 95, + 112, + 114, + 111, + 103, + 114, + 97, + 109, + 115 + ] + } + ] + } + }, + { + "name": "smart_wallet", + "docs": [ + "The smart wallet PDA being created with random ID" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 109, + 97, + 114, + 116, + 95, + 119, + 97, + 108, + 108, + 101, + 116 + ] + }, + { + "kind": "arg", + "path": "args.wallet_id" + } + ] + } + }, + { + "name": "smart_wallet_config", + "docs": [ + "Smart wallet configuration data" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 109, + 97, + 114, + 116, + 95, + 119, + 97, + 108, + 108, + 101, + 116, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "smart_wallet" + } + ] + } + }, + { + "name": "smart_wallet_authenticator", + "docs": [ + "Smart wallet authenticator for the passkey" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 109, + 97, + 114, + 116, + 95, + 119, + 97, + 108, + 108, + 101, + 116, + 95, + 97, + 117, + 116, + 104, + 101, + 110, + 116, + 105, + 99, + 97, + 116, + 111, + 114 + ] + }, + { + "kind": "account", + "path": "smart_wallet" + }, + { + "kind": "arg", + "path": "args.passkey_pubkey.to_hashed_bytes(smart_wallet" + } + ] + } + }, + { + "name": "config", + "docs": [ + "Program configuration" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + }, + { + "name": "default_rule_program", + "docs": [ + "Default rule program for the smart wallet" + ] + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": { + "name": "CreatwSmartWalletArgs" + } + } + } + ] + }, + { + "name": "execute_committed", + "discriminator": [ + 183, + 133, + 244, + 196, + 134, + 40, + 191, + 126 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + }, + { + "name": "smart_wallet", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 109, + 97, + 114, + 116, + 95, + 119, + 97, + 108, + 108, + 101, + 116 + ] + }, + { + "kind": "account", + "path": "smart_wallet_config.id", + "account": "SmartWalletConfig" + } + ] + } + }, + { + "name": "smart_wallet_config", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 109, + 97, + 114, + 116, + 95, + 119, + 97, + 108, + 108, + 101, + 116, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "smart_wallet" + } + ] + } + }, + { + "name": "cpi_program" + }, + { + "name": "cpi_commit", + "docs": [ + "Commit to execute. Closed on success to refund rent." + ], + "writable": true + }, + { + "name": "commit_refund", + "writable": true + } + ], + "args": [ + { + "name": "cpi_data", + "type": "bytes" + } + ] + }, + { + "name": "execute_txn_direct", + "discriminator": [ + 121, + 40, + 165, + 106, + 50, + 95, + 121, + 118 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "smart_wallet", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 109, + 97, + 114, + 116, + 95, + 119, + 97, + 108, + 108, + 101, + 116 + ] + }, + { + "kind": "account", + "path": "smart_wallet_config.id", + "account": "SmartWalletConfig" + } + ] + } + }, + { + "name": "smart_wallet_config", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 109, + 97, + 114, + 116, + 95, + 119, + 97, + 108, + 108, + 101, + 116, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "smart_wallet" + } + ] + } + }, + { + "name": "smart_wallet_authenticator" + }, + { + "name": "whitelist_rule_programs", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 119, + 104, + 105, + 116, + 101, + 108, + 105, + 115, + 116, + 95, + 114, + 117, + 108, + 101, + 95, + 112, + 114, + 111, + 103, + 114, + 97, + 109, + 115 + ] + } + ] + } + }, + { + "name": "authenticator_program" + }, + { + "name": "cpi_program" + }, + { + "name": "config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + }, + { + "name": "ix_sysvar", + "address": "Sysvar1nstructions1111111111111111111111111" + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": { + "name": "ExecuteTxnArgs" + } + } + } + ] + }, + { + "name": "initialize", + "docs": [ + "Initialize the program by creating the sequence tracker" + ], + "discriminator": [ + 175, + 175, + 109, + 31, + 13, + 152, + 155, + 237 + ], + "accounts": [ + { + "name": "signer", + "docs": [ + "The signer of the transaction, who will be the initial authority." + ], + "writable": true, + "signer": true + }, + { + "name": "config", + "docs": [ + "The program's configuration account." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + }, + { + "name": "whitelist_rule_programs", + "docs": [ + "The list of whitelisted rule programs that can be used with smart wallets." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 119, + 104, + 105, + 116, + 101, + 108, + 105, + 115, + 116, + 95, + 114, + 117, + 108, + 101, + 95, + 112, + 114, + 111, + 103, + 114, + 97, + 109, + 115 + ] + } + ] + } + }, + { + "name": "default_rule_program", + "docs": [ + "The default rule program to be used for new smart wallets." + ] + }, + { + "name": "system_program", + "docs": [ + "The system program." + ], + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "update_config", + "docs": [ + "Update the program configuration" + ], + "discriminator": [ + 29, + 158, + 252, + 191, + 10, + 83, + 219, + 99 + ], + "accounts": [ + { + "name": "authority", + "docs": [ + "The current authority of the program." + ], + "writable": true, + "signer": true, + "relations": [ + "config" + ] + }, + { + "name": "config", + "docs": [ + "The program's configuration account." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + } + ], + "args": [ + { + "name": "param", + "type": { + "defined": { + "name": "UpdateConfigType" + } + } + }, + { + "name": "value", + "type": "u64" + } + ] + } + ], + "accounts": [ + { + "name": "Config", + "discriminator": [ + 155, + 12, + 170, + 224, + 30, + 250, + 204, + 130 + ] + }, + { + "name": "CpiCommit", + "discriminator": [ + 50, + 161, + 109, + 178, + 148, + 116, + 95, + 160 + ] + }, + { + "name": "SmartWalletAuthenticator", + "discriminator": [ + 126, + 36, + 85, + 166, + 77, + 139, + 221, + 129 + ] + }, + { + "name": "SmartWalletConfig", + "discriminator": [ + 138, + 211, + 3, + 80, + 65, + 100, + 207, + 142 + ] + }, + { + "name": "WhitelistRulePrograms", + "discriminator": [ + 234, + 147, + 45, + 188, + 65, + 212, + 154, + 241 + ] + } + ], + "events": [ + { + "name": "AuthenticatorAdded", + "discriminator": [ + 213, + 87, + 171, + 174, + 101, + 129, + 32, + 44 + ] + }, + { + "name": "ConfigUpdated", + "discriminator": [ + 40, + 241, + 230, + 122, + 11, + 19, + 198, + 194 + ] + }, + { + "name": "ErrorEvent", + "discriminator": [ + 163, + 35, + 212, + 206, + 66, + 104, + 234, + 251 + ] + }, + { + "name": "FeeCollected", + "discriminator": [ + 12, + 28, + 17, + 248, + 244, + 36, + 8, + 73 + ] + }, + { + "name": "ProgramInitialized", + "discriminator": [ + 43, + 70, + 110, + 241, + 199, + 218, + 221, + 245 + ] + }, + { + "name": "ProgramPausedStateChanged", + "discriminator": [ + 148, + 9, + 117, + 157, + 18, + 25, + 122, + 32 + ] + }, + { + "name": "RuleProgramChanged", + "discriminator": [ + 116, + 110, + 184, + 140, + 118, + 243, + 237, + 111 + ] + }, + { + "name": "SecurityEvent", + "discriminator": [ + 16, + 175, + 241, + 170, + 85, + 9, + 201, + 100 + ] + }, + { + "name": "SmartWalletCreated", + "discriminator": [ + 145, + 37, + 118, + 21, + 58, + 251, + 56, + 128 + ] + }, + { + "name": "SolTransfer", + "discriminator": [ + 0, + 186, + 79, + 129, + 194, + 76, + 94, + 9 + ] + }, + { + "name": "TransactionExecuted", + "discriminator": [ + 211, + 227, + 168, + 14, + 32, + 111, + 189, + 210 + ] + }, + { + "name": "WhitelistRuleProgramAdded", + "discriminator": [ + 219, + 72, + 34, + 198, + 65, + 224, + 225, + 103 + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "PasskeyMismatch", + "msg": "Passkey public key mismatch with stored authenticator" + }, + { + "code": 6001, + "name": "SmartWalletMismatch", + "msg": "Smart wallet address mismatch with authenticator" + }, + { + "code": 6002, + "name": "AuthenticatorNotFound", + "msg": "Smart wallet authenticator account not found or invalid" + }, + { + "code": 6003, + "name": "Secp256r1InvalidLength", + "msg": "Secp256r1 instruction has invalid data length" + }, + { + "code": 6004, + "name": "Secp256r1HeaderMismatch", + "msg": "Secp256r1 instruction header validation failed" + }, + { + "code": 6005, + "name": "Secp256r1DataMismatch", + "msg": "Secp256r1 signature data validation failed" + }, + { + "code": 6006, + "name": "Secp256r1InstructionNotFound", + "msg": "Secp256r1 instruction not found at specified index" + }, + { + "code": 6007, + "name": "InvalidSignature", + "msg": "Invalid signature provided for passkey verification" + }, + { + "code": 6008, + "name": "ClientDataInvalidUtf8", + "msg": "Client data JSON is not valid UTF-8" + }, + { + "code": 6009, + "name": "ClientDataJsonParseError", + "msg": "Client data JSON parsing failed" + }, + { + "code": 6010, + "name": "ChallengeMissing", + "msg": "Challenge field missing from client data JSON" + }, + { + "code": 6011, + "name": "ChallengeBase64DecodeError", + "msg": "Challenge base64 decoding failed" + }, + { + "code": 6012, + "name": "ChallengeDeserializationError", + "msg": "Challenge message deserialization failed" + }, + { + "code": 6013, + "name": "TimestampTooOld", + "msg": "Message timestamp is too far in the past" + }, + { + "code": 6014, + "name": "TimestampTooNew", + "msg": "Message timestamp is too far in the future" + }, + { + "code": 6015, + "name": "NonceMismatch", + "msg": "Nonce mismatch: expected different value" + }, + { + "code": 6016, + "name": "NonceOverflow", + "msg": "Nonce overflow: cannot increment further" + }, + { + "code": 6017, + "name": "RuleProgramNotWhitelisted", + "msg": "Rule program not found in whitelist" + }, + { + "code": 6018, + "name": "WhitelistFull", + "msg": "The whitelist of rule programs is full." + }, + { + "code": 6019, + "name": "RuleDataRequired", + "msg": "Rule data is required but not provided" + }, + { + "code": 6020, + "name": "InvalidCheckRuleDiscriminator", + "msg": "Invalid instruction discriminator for check_rule" + }, + { + "code": 6021, + "name": "InvalidDestroyDiscriminator", + "msg": "Invalid instruction discriminator for destroy" + }, + { + "code": 6022, + "name": "InvalidInitRuleDiscriminator", + "msg": "Invalid instruction discriminator for init_rule" + }, + { + "code": 6023, + "name": "RuleProgramsIdentical", + "msg": "Old and new rule programs are identical" + }, + { + "code": 6024, + "name": "NoDefaultRuleProgram", + "msg": "Neither old nor new rule program is the default" + }, + { + "code": 6025, + "name": "InvalidRemainingAccounts", + "msg": "Invalid remaining accounts" + }, + { + "code": 6026, + "name": "CpiDataMissing", + "msg": "CPI data is required but not provided" + }, + { + "code": 6027, + "name": "InvalidCpiData", + "msg": "CPI data is invalid or malformed" + }, + { + "code": 6028, + "name": "InsufficientRuleAccounts", + "msg": "Insufficient remaining accounts for rule instruction" + }, + { + "code": 6029, + "name": "InsufficientCpiAccounts", + "msg": "Insufficient remaining accounts for CPI instruction" + }, + { + "code": 6030, + "name": "AccountSliceOutOfBounds", + "msg": "Account slice index out of bounds" + }, + { + "code": 6031, + "name": "SolTransferInsufficientAccounts", + "msg": "SOL transfer requires at least 2 remaining accounts" + }, + { + "code": 6032, + "name": "NewAuthenticatorMissing", + "msg": "New authenticator account is required but not provided" + }, + { + "code": 6033, + "name": "NewAuthenticatorPasskeyMissing", + "msg": "New authenticator passkey is required but not provided" + }, + { + "code": 6034, + "name": "InsufficientLamports", + "msg": "Insufficient lamports for requested transfer" + }, + { + "code": 6035, + "name": "TransferAmountOverflow", + "msg": "Transfer amount would cause arithmetic overflow" + }, + { + "code": 6036, + "name": "InvalidBumpSeed", + "msg": "Invalid bump seed for PDA derivation" + }, + { + "code": 6037, + "name": "InvalidAccountOwner", + "msg": "Account owner verification failed" + }, + { + "code": 6038, + "name": "InvalidAccountDiscriminator", + "msg": "Account discriminator mismatch" + }, + { + "code": 6039, + "name": "InvalidProgramId", + "msg": "Invalid program ID" + }, + { + "code": 6040, + "name": "ProgramNotExecutable", + "msg": "Program not executable" + }, + { + "code": 6041, + "name": "SmartWalletAuthenticatorAlreadyInitialized", + "msg": "Smart wallet authenticator already initialized" + }, + { + "code": 6042, + "name": "CredentialIdTooLarge", + "msg": "Credential ID exceeds maximum allowed size" + }, + { + "code": 6043, + "name": "CredentialIdEmpty", + "msg": "Credential ID cannot be empty" + }, + { + "code": 6044, + "name": "RuleDataTooLarge", + "msg": "Rule data exceeds maximum allowed size" + }, + { + "code": 6045, + "name": "CpiDataTooLarge", + "msg": "CPI data exceeds maximum allowed size" + }, + { + "code": 6046, + "name": "TooManyRemainingAccounts", + "msg": "Too many remaining accounts provided" + }, + { + "code": 6047, + "name": "InvalidPDADerivation", + "msg": "Invalid PDA derivation" + }, + { + "code": 6048, + "name": "TransactionTooOld", + "msg": "Transaction is too old" + }, + { + "code": 6049, + "name": "RateLimitExceeded", + "msg": "Rate limit exceeded" + }, + { + "code": 6050, + "name": "InvalidAccountData", + "msg": "Invalid account data" + }, + { + "code": 6051, + "name": "Unauthorized", + "msg": "Unauthorized access attempt" + }, + { + "code": 6052, + "name": "ProgramPaused", + "msg": "Program is paused" + }, + { + "code": 6053, + "name": "InvalidInstructionData", + "msg": "Invalid instruction data" + }, + { + "code": 6054, + "name": "AccountAlreadyInitialized", + "msg": "Account already initialized" + }, + { + "code": 6055, + "name": "AccountNotInitialized", + "msg": "Account not initialized" + }, + { + "code": 6056, + "name": "InvalidAccountState", + "msg": "Invalid account state" + }, + { + "code": 6057, + "name": "IntegerOverflow", + "msg": "Operation would cause integer overflow" + }, + { + "code": 6058, + "name": "IntegerUnderflow", + "msg": "Operation would cause integer underflow" + }, + { + "code": 6059, + "name": "InvalidFeeAmount", + "msg": "Invalid fee amount" + }, + { + "code": 6060, + "name": "InsufficientBalanceForFee", + "msg": "Insufficient balance for fee" + }, + { + "code": 6061, + "name": "InvalidAuthority", + "msg": "Invalid authority" + }, + { + "code": 6062, + "name": "AuthorityMismatch", + "msg": "Authority mismatch" + }, + { + "code": 6063, + "name": "InvalidSequenceNumber", + "msg": "Invalid sequence number" + }, + { + "code": 6064, + "name": "DuplicateTransaction", + "msg": "Duplicate transaction detected" + }, + { + "code": 6065, + "name": "InvalidTransactionOrdering", + "msg": "Invalid transaction ordering" + }, + { + "code": 6066, + "name": "MaxWalletLimitReached", + "msg": "Maximum wallet limit reached" + }, + { + "code": 6067, + "name": "InvalidWalletConfiguration", + "msg": "Invalid wallet configuration" + }, + { + "code": 6068, + "name": "WalletNotFound", + "msg": "Wallet not found" + }, + { + "code": 6069, + "name": "InvalidPasskeyFormat", + "msg": "Invalid passkey format" + }, + { + "code": 6070, + "name": "PasskeyAlreadyRegistered", + "msg": "Passkey already registered" + }, + { + "code": 6071, + "name": "InvalidMessageFormat", + "msg": "Invalid message format" + }, + { + "code": 6072, + "name": "MessageSizeExceedsLimit", + "msg": "Message size exceeds limit" + }, + { + "code": 6073, + "name": "InvalidSplitIndex", + "msg": "Invalid split index" + }, + { + "code": 6074, + "name": "CpiExecutionFailed", + "msg": "CPI execution failed" + }, + { + "code": 6075, + "name": "InvalidProgramAddress", + "msg": "Invalid program address" + }, + { + "code": 6076, + "name": "WhitelistOperationFailed", + "msg": "Whitelist operation failed" + }, + { + "code": 6077, + "name": "InvalidWhitelistState", + "msg": "Invalid whitelist state" + }, + { + "code": 6078, + "name": "EmergencyShutdown", + "msg": "Emergency shutdown activated" + }, + { + "code": 6079, + "name": "RecoveryModeRequired", + "msg": "Recovery mode required" + }, + { + "code": 6080, + "name": "InvalidRecoveryAttempt", + "msg": "Invalid recovery attempt" + }, + { + "code": 6081, + "name": "AuditLogFull", + "msg": "Audit log full" + }, + { + "code": 6082, + "name": "InvalidAuditEntry", + "msg": "Invalid audit entry" + }, + { + "code": 6083, + "name": "ReentrancyDetected", + "msg": "Reentrancy detected" + }, + { + "code": 6084, + "name": "InvalidCallDepth", + "msg": "Invalid call depth" + }, + { + "code": 6085, + "name": "StackOverflowProtection", + "msg": "Stack overflow protection triggered" + }, + { + "code": 6086, + "name": "MemoryLimitExceeded", + "msg": "Memory limit exceeded" + }, + { + "code": 6087, + "name": "ComputationLimitExceeded", + "msg": "Computation limit exceeded" + }, + { + "code": 6088, + "name": "InvalidRentExemption", + "msg": "Invalid rent exemption" + }, + { + "code": 6089, + "name": "AccountClosureFailed", + "msg": "Account closure failed" + }, + { + "code": 6090, + "name": "InvalidAccountClosure", + "msg": "Invalid account closure" + }, + { + "code": 6091, + "name": "RefundFailed", + "msg": "Refund failed" + }, + { + "code": 6092, + "name": "InvalidRefundAmount", + "msg": "Invalid refund amount" + } + ], + "types": [ + { + "name": "AuthenticatorAdded", + "docs": [ + "Event emitted when a new authenticator is added" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "smart_wallet", + "type": "pubkey" + }, + { + "name": "new_authenticator", + "type": "pubkey" + }, + { + "name": "passkey_hash", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "added_by", + "type": "pubkey" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "CallRuleArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "passkey_pubkey", + "type": { + "array": [ + "u8", + 33 + ] + } + }, + { + "name": "signature", + "type": "bytes" + }, + { + "name": "client_data_json_raw", + "type": "bytes" + }, + { + "name": "authenticator_data_raw", + "type": "bytes" + }, + { + "name": "verify_instruction_index", + "type": "u8" + }, + { + "name": "rule_data", + "type": "bytes" + }, + { + "name": "new_authenticator", + "type": { + "option": { + "defined": { + "name": "NewAuthenticatorArgs" + } + } + } + } + ] + } + }, + { + "name": "ChangeRuleArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "passkey_pubkey", + "type": { + "array": [ + "u8", + 33 + ] + } + }, + { + "name": "signature", + "type": "bytes" + }, + { + "name": "client_data_json_raw", + "type": "bytes" + }, + { + "name": "authenticator_data_raw", + "type": "bytes" + }, + { + "name": "verify_instruction_index", + "type": "u8" + }, + { + "name": "split_index", + "type": "u16" + }, + { + "name": "destroy_rule_data", + "type": "bytes" + }, + { + "name": "init_rule_data", + "type": "bytes" + }, + { + "name": "new_authenticator", + "type": { + "option": { + "defined": { + "name": "NewAuthenticatorArgs" + } + } + } + } + ] + } + }, + { + "name": "CommitArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "passkey_pubkey", + "type": { + "array": [ + "u8", + 33 + ] + } + }, + { + "name": "signature", + "type": "bytes" + }, + { + "name": "client_data_json_raw", + "type": "bytes" + }, + { + "name": "authenticator_data_raw", + "type": "bytes" + }, + { + "name": "verify_instruction_index", + "type": "u8" + }, + { + "name": "rule_data", + "type": "bytes" + }, + { + "name": "expires_at", + "type": "i64" + } + ] + } + }, + { + "name": "Config", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "create_smart_wallet_fee", + "type": "u64" + }, + { + "name": "execute_fee", + "type": "u64" + }, + { + "name": "default_rule_program", + "type": "pubkey" + }, + { + "name": "is_paused", + "type": "bool" + } + ] + } + }, + { + "name": "ConfigUpdated", + "docs": [ + "Event emitted when program configuration is updated" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "update_type", + "type": "string" + }, + { + "name": "old_value", + "type": "string" + }, + { + "name": "new_value", + "type": "string" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "CpiCommit", + "docs": [ + "Commit record for a future CPI execution.", + "Created after full passkey + rule verification. Contains all bindings", + "necessary to perform the CPI later without re-verification." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "owner_wallet", + "docs": [ + "Smart wallet that authorized this commit" + ], + "type": "pubkey" + }, + { + "name": "data_hash", + "docs": [ + "sha256 of CPI instruction data" + ], + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "accounts_hash", + "docs": [ + "sha256 over ordered remaining account metas plus `target_program`" + ], + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "authorized_nonce", + "docs": [ + "The nonce that was authorized at commit time (bound into data hash)" + ], + "type": "u64" + }, + { + "name": "expires_at", + "docs": [ + "Unix expiration timestamp" + ], + "type": "i64" + }, + { + "name": "rent_refund_to", + "docs": [ + "Where to refund rent when closing the commit" + ], + "type": "pubkey" + } + ] + } + }, + { + "name": "CreatwSmartWalletArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "passkey_pubkey", + "type": { + "array": [ + "u8", + 33 + ] + } + }, + { + "name": "credential_id", + "type": "bytes" + }, + { + "name": "rule_data", + "type": "bytes" + }, + { + "name": "wallet_id", + "type": "u64" + }, + { + "name": "is_pay_for_user", + "type": "bool" + } + ] + } + }, + { + "name": "ErrorEvent", + "docs": [ + "Event emitted for errors that are caught and handled" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "smart_wallet", + "type": { + "option": "pubkey" + } + }, + { + "name": "error_code", + "type": "string" + }, + { + "name": "error_message", + "type": "string" + }, + { + "name": "action_attempted", + "type": "string" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "ExecuteTxnArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "passkey_pubkey", + "type": { + "array": [ + "u8", + 33 + ] + } + }, + { + "name": "signature", + "type": "bytes" + }, + { + "name": "client_data_json_raw", + "type": "bytes" + }, + { + "name": "authenticator_data_raw", + "type": "bytes" + }, + { + "name": "verify_instruction_index", + "type": "u8" + }, + { + "name": "split_index", + "type": "u16" + }, + { + "name": "rule_data", + "type": "bytes" + }, + { + "name": "cpi_data", + "type": "bytes" + } + ] + } + }, + { + "name": "FeeCollected", + "docs": [ + "Event emitted when a fee is collected" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "smart_wallet", + "type": "pubkey" + }, + { + "name": "fee_type", + "type": "string" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "recipient", + "type": "pubkey" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "NewAuthenticatorArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "passkey_pubkey", + "type": { + "array": [ + "u8", + 33 + ] + } + }, + { + "name": "credential_id", + "type": "bytes" + } + ] + } + }, + { + "name": "ProgramInitialized", + "docs": [ + "Event emitted when program is initialized" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "default_rule_program", + "type": "pubkey" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "ProgramPausedStateChanged", + "docs": [ + "Event emitted when program is paused/unpaused" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "is_paused", + "type": "bool" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "RuleProgramChanged", + "docs": [ + "Event emitted when a rule program is changed" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "smart_wallet", + "type": "pubkey" + }, + { + "name": "old_rule_program", + "type": "pubkey" + }, + { + "name": "new_rule_program", + "type": "pubkey" + }, + { + "name": "nonce", + "type": "u64" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "SecurityEvent", + "docs": [ + "Event emitted for security-related events" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "event_type", + "type": "string" + }, + { + "name": "smart_wallet", + "type": { + "option": "pubkey" + } + }, + { + "name": "details", + "type": "string" + }, + { + "name": "severity", + "type": "string" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "SmartWalletAuthenticator", + "docs": [ + "Account that stores authentication data for a smart wallet" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "passkey_pubkey", + "docs": [ + "The public key of the passkey that can authorize transactions" + ], + "type": { + "array": [ + "u8", + 33 + ] + } + }, + { + "name": "smart_wallet", + "docs": [ + "The smart wallet this authenticator belongs to" + ], + "type": "pubkey" + }, + { + "name": "credential_id", + "docs": [ + "The credential ID this authenticator belongs to" + ], + "type": "bytes" + }, + { + "name": "bump", + "docs": [ + "Bump seed for PDA derivation" + ], + "type": "u8" + } + ] + } + }, + { + "name": "SmartWalletConfig", + "docs": [ + "Data account for a smart wallet" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "id", + "docs": [ + "Unique identifier for this smart wallet" + ], + "type": "u64" + }, + { + "name": "rule_program", + "docs": [ + "Optional rule program that governs this wallet's operations" + ], + "type": "pubkey" + }, + { + "name": "last_nonce", + "type": "u64" + }, + { + "name": "bump", + "docs": [ + "Bump seed for PDA derivation" + ], + "type": "u8" + } + ] + } + }, + { + "name": "SmartWalletCreated", + "docs": [ + "Event emitted when a new smart wallet is created" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "smart_wallet", + "type": "pubkey" + }, + { + "name": "authenticator", + "type": "pubkey" + }, + { + "name": "sequence_id", + "type": "u64" + }, + { + "name": "rule_program", + "type": "pubkey" + }, + { + "name": "passkey_hash", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "SolTransfer", + "docs": [ + "Event emitted when a SOL transfer occurs" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "smart_wallet", + "type": "pubkey" + }, + { + "name": "destination", + "type": "pubkey" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "nonce", + "type": "u64" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "TransactionExecuted", + "docs": [ + "Event emitted when a transaction is executed" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "smart_wallet", + "type": "pubkey" + }, + { + "name": "authenticator", + "type": "pubkey" + }, + { + "name": "nonce", + "type": "u64" + }, + { + "name": "rule_program", + "type": "pubkey" + }, + { + "name": "cpi_program", + "type": "pubkey" + }, + { + "name": "success", + "type": "bool" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "UpdateConfigType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "CreateWalletFee" + }, + { + "name": "ExecuteFee" + }, + { + "name": "DefaultRuleProgram" + }, + { + "name": "Admin" + }, + { + "name": "PauseProgram" + }, + { + "name": "UnpauseProgram" + } + ] + } + }, + { + "name": "WhitelistRuleProgramAdded", + "docs": [ + "Event emitted when a whitelist rule program is added" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "rule_program", + "type": "pubkey" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "WhitelistRulePrograms", + "docs": [ + "Account that stores whitelisted rule program addresses" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "list", + "docs": [ + "List of whitelisted program addresses" + ], + "type": { + "vec": "pubkey" + } + }, + { + "name": "bump", + "docs": [ + "Bump seed for PDA derivation" + ], + "type": "u8" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/contract-integration/anchor/types/default_rule.ts b/contract-integration/anchor/types/default_rule.ts new file mode 100644 index 0000000..06c5d1c --- /dev/null +++ b/contract-integration/anchor/types/default_rule.ts @@ -0,0 +1,278 @@ +/** + * Program IDL in camelCase format in order to be used in JS/TS. + * + * Note that this is only a type helper and is not the actual IDL. The original + * IDL can be found at `target/idl/default_rule.json`. + */ +export type DefaultRule = { + "address": "CNT2aEgxucQjmt5SRsA6hSGrt241Bvc9zsgPvSuMjQTE", + "metadata": { + "name": "defaultRule", + "version": "0.1.0", + "spec": "0.1.0", + "description": "Created with Anchor" + }, + "instructions": [ + { + "name": "addDevice", + "discriminator": [ + 21, + 27, + 66, + 42, + 18, + 30, + 14, + 18 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "smartWalletAuthenticator", + "signer": true + }, + { + "name": "newSmartWalletAuthenticator" + }, + { + "name": "rule", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 114, + 117, + 108, + 101 + ] + }, + { + "kind": "account", + "path": "smartWalletAuthenticator" + } + ] + } + }, + { + "name": "newRule", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 114, + 117, + 108, + 101 + ] + }, + { + "kind": "account", + "path": "newSmartWalletAuthenticator" + } + ] + } + }, + { + "name": "lazorkit", + "address": "J6Big9w1VNeRZgDWH5qmNz2Nd6XFq5QeZbqC8caqSE5W" + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "checkRule", + "discriminator": [ + 215, + 90, + 220, + 175, + 191, + 212, + 144, + 147 + ], + "accounts": [ + { + "name": "smartWalletAuthenticator", + "signer": true + }, + { + "name": "rule", + "writable": true + } + ], + "args": [] + }, + { + "name": "initRule", + "discriminator": [ + 129, + 224, + 96, + 169, + 247, + 125, + 74, + 118 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "smartWallet" + }, + { + "name": "smartWalletAuthenticator", + "docs": [ + "CHECK" + ], + "signer": true + }, + { + "name": "rule", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 114, + 117, + 108, + 101 + ] + }, + { + "kind": "account", + "path": "smartWalletAuthenticator" + } + ] + } + }, + { + "name": "lazorkit", + "address": "J6Big9w1VNeRZgDWH5qmNz2Nd6XFq5QeZbqC8caqSE5W" + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + } + ], + "args": [] + } + ], + "accounts": [ + { + "name": "rule", + "discriminator": [ + 82, + 10, + 53, + 40, + 250, + 61, + 143, + 130 + ] + }, + { + "name": "smartWalletAuthenticator", + "discriminator": [ + 126, + 36, + 85, + 166, + 77, + 139, + 221, + 129 + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "invalidPasskey" + }, + { + "code": 6001, + "name": "unAuthorize" + } + ], + "types": [ + { + "name": "rule", + "type": { + "kind": "struct", + "fields": [ + { + "name": "smartWallet", + "type": "pubkey" + }, + { + "name": "smartWalletAuthenticator", + "type": "pubkey" + } + ] + } + }, + { + "name": "smartWalletAuthenticator", + "docs": [ + "Account that stores authentication data for a smart wallet" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "passkeyPubkey", + "docs": [ + "The public key of the passkey that can authorize transactions" + ], + "type": { + "array": [ + "u8", + 33 + ] + } + }, + { + "name": "smartWallet", + "docs": [ + "The smart wallet this authenticator belongs to" + ], + "type": "pubkey" + }, + { + "name": "credentialId", + "docs": [ + "The credential ID this authenticator belongs to" + ], + "type": "bytes" + }, + { + "name": "bump", + "docs": [ + "Bump seed for PDA derivation" + ], + "type": "u8" + } + ] + } + } + ] +}; diff --git a/contract-integration/anchor/types/lazorkit.ts b/contract-integration/anchor/types/lazorkit.ts new file mode 100644 index 0000000..a62a95c --- /dev/null +++ b/contract-integration/anchor/types/lazorkit.ts @@ -0,0 +1,2901 @@ +/** + * Program IDL in camelCase format in order to be used in JS/TS. + * + * Note that this is only a type helper and is not the actual IDL. The original + * IDL can be found at `target/idl/lazorkit.json`. + */ +export type Lazorkit = { + "address": "J6Big9w1VNeRZgDWH5qmNz2Nd6XFq5QeZbqC8caqSE5W", + "metadata": { + "name": "lazorkit", + "version": "0.1.0", + "spec": "0.1.0", + "description": "Created with Anchor" + }, + "docs": [ + "The Lazor Kit program provides smart wallet functionality with passkey authentication" + ], + "instructions": [ + { + "name": "addWhitelistRuleProgram", + "docs": [ + "Add a program to the whitelist of rule programs" + ], + "discriminator": [ + 133, + 37, + 74, + 189, + 59, + 238, + 188, + 210 + ], + "accounts": [ + { + "name": "authority", + "writable": true, + "signer": true, + "relations": [ + "config" + ] + }, + { + "name": "config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + }, + { + "name": "whitelistRulePrograms", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 119, + 104, + 105, + 116, + 101, + 108, + 105, + 115, + 116, + 95, + 114, + 117, + 108, + 101, + 95, + 112, + 114, + 111, + 103, + 114, + 97, + 109, + 115 + ] + } + ] + } + } + ], + "args": [] + }, + { + "name": "callRuleDirect", + "discriminator": [ + 97, + 234, + 75, + 197, + 171, + 164, + 239, + 65 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + }, + { + "name": "smartWallet", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 109, + 97, + 114, + 116, + 95, + 119, + 97, + 108, + 108, + 101, + 116 + ] + }, + { + "kind": "account", + "path": "smart_wallet_config.id", + "account": "smartWalletConfig" + } + ] + } + }, + { + "name": "smartWalletConfig", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 109, + 97, + 114, + 116, + 95, + 119, + 97, + 108, + 108, + 101, + 116, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "smartWallet" + } + ] + } + }, + { + "name": "smartWalletAuthenticator" + }, + { + "name": "ruleProgram" + }, + { + "name": "whitelistRulePrograms", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 119, + 104, + 105, + 116, + 101, + 108, + 105, + 115, + 116, + 95, + 114, + 117, + 108, + 101, + 95, + 112, + 114, + 111, + 103, + 114, + 97, + 109, + 115 + ] + } + ] + } + }, + { + "name": "newSmartWalletAuthenticator", + "docs": [ + "Optional new authenticator to initialize when requested in message" + ], + "optional": true + }, + { + "name": "ixSysvar", + "address": "Sysvar1nstructions1111111111111111111111111" + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": { + "name": "callRuleArgs" + } + } + } + ] + }, + { + "name": "changeRuleDirect", + "discriminator": [ + 117, + 33, + 70, + 46, + 48, + 232, + 110, + 70 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + }, + { + "name": "smartWallet", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 109, + 97, + 114, + 116, + 95, + 119, + 97, + 108, + 108, + 101, + 116 + ] + }, + { + "kind": "account", + "path": "smart_wallet_config.id", + "account": "smartWalletConfig" + } + ] + } + }, + { + "name": "smartWalletConfig", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 109, + 97, + 114, + 116, + 95, + 119, + 97, + 108, + 108, + 101, + 116, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "smartWallet" + } + ] + } + }, + { + "name": "smartWalletAuthenticator" + }, + { + "name": "oldRuleProgram", + "docs": [ + "CHECK" + ] + }, + { + "name": "newRuleProgram", + "docs": [ + "CHECK" + ] + }, + { + "name": "whitelistRulePrograms", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 119, + 104, + 105, + 116, + 101, + 108, + 105, + 115, + 116, + 95, + 114, + 117, + 108, + 101, + 95, + 112, + 114, + 111, + 103, + 114, + 97, + 109, + 115 + ] + } + ] + } + }, + { + "name": "ixSysvar", + "docs": [ + "CHECK" + ], + "address": "Sysvar1nstructions1111111111111111111111111" + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": { + "name": "changeRuleArgs" + } + } + } + ] + }, + { + "name": "commitCpi", + "discriminator": [ + 74, + 89, + 187, + 45, + 241, + 147, + 133, + 62 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + }, + { + "name": "smartWallet", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 109, + 97, + 114, + 116, + 95, + 119, + 97, + 108, + 108, + 101, + 116 + ] + }, + { + "kind": "account", + "path": "smart_wallet_config.id", + "account": "smartWalletConfig" + } + ] + } + }, + { + "name": "smartWalletConfig", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 109, + 97, + 114, + 116, + 95, + 119, + 97, + 108, + 108, + 101, + 116, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "smartWallet" + } + ] + } + }, + { + "name": "smartWalletAuthenticator", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 109, + 97, + 114, + 116, + 95, + 119, + 97, + 108, + 108, + 101, + 116, + 95, + 97, + 117, + 116, + 104, + 101, + 110, + 116, + 105, + 99, + 97, + 116, + 111, + 114 + ] + }, + { + "kind": "account", + "path": "smartWallet" + }, + { + "kind": "arg", + "path": "args.passkey_pubkey.to_hashed_bytes(smart_wallet" + } + ] + } + }, + { + "name": "whitelistRulePrograms", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 119, + 104, + 105, + 116, + 101, + 108, + 105, + 115, + 116, + 95, + 114, + 117, + 108, + 101, + 95, + 112, + 114, + 111, + 103, + 114, + 97, + 109, + 115 + ] + } + ] + } + }, + { + "name": "authenticatorProgram", + "docs": [ + "Rule program for optional policy enforcement at commit time" + ] + }, + { + "name": "cpiCommit", + "docs": [ + "New commit account (rent payer: payer)" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 112, + 105, + 95, + 99, + 111, + 109, + 109, + 105, + 116 + ] + }, + { + "kind": "account", + "path": "smartWallet" + }, + { + "kind": "account", + "path": "smart_wallet_config.last_nonce", + "account": "smartWalletConfig" + } + ] + } + }, + { + "name": "ixSysvar", + "address": "Sysvar1nstructions1111111111111111111111111" + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": { + "name": "commitArgs" + } + } + } + ] + }, + { + "name": "createSmartWallet", + "docs": [ + "Create a new smart wallet with passkey authentication" + ], + "discriminator": [ + 129, + 39, + 235, + 18, + 132, + 68, + 203, + 19 + ], + "accounts": [ + { + "name": "signer", + "writable": true, + "signer": true + }, + { + "name": "whitelistRulePrograms", + "docs": [ + "Whitelist of allowed rule programs" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 119, + 104, + 105, + 116, + 101, + 108, + 105, + 115, + 116, + 95, + 114, + 117, + 108, + 101, + 95, + 112, + 114, + 111, + 103, + 114, + 97, + 109, + 115 + ] + } + ] + } + }, + { + "name": "smartWallet", + "docs": [ + "The smart wallet PDA being created with random ID" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 109, + 97, + 114, + 116, + 95, + 119, + 97, + 108, + 108, + 101, + 116 + ] + }, + { + "kind": "arg", + "path": "args.wallet_id" + } + ] + } + }, + { + "name": "smartWalletConfig", + "docs": [ + "Smart wallet configuration data" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 109, + 97, + 114, + 116, + 95, + 119, + 97, + 108, + 108, + 101, + 116, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "smartWallet" + } + ] + } + }, + { + "name": "smartWalletAuthenticator", + "docs": [ + "Smart wallet authenticator for the passkey" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 109, + 97, + 114, + 116, + 95, + 119, + 97, + 108, + 108, + 101, + 116, + 95, + 97, + 117, + 116, + 104, + 101, + 110, + 116, + 105, + 99, + 97, + 116, + 111, + 114 + ] + }, + { + "kind": "account", + "path": "smartWallet" + }, + { + "kind": "arg", + "path": "args.passkey_pubkey.to_hashed_bytes(smart_wallet" + } + ] + } + }, + { + "name": "config", + "docs": [ + "Program configuration" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + }, + { + "name": "defaultRuleProgram", + "docs": [ + "Default rule program for the smart wallet" + ] + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": { + "name": "creatwSmartWalletArgs" + } + } + } + ] + }, + { + "name": "executeCommitted", + "discriminator": [ + 183, + 133, + 244, + 196, + 134, + 40, + 191, + 126 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + }, + { + "name": "smartWallet", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 109, + 97, + 114, + 116, + 95, + 119, + 97, + 108, + 108, + 101, + 116 + ] + }, + { + "kind": "account", + "path": "smart_wallet_config.id", + "account": "smartWalletConfig" + } + ] + } + }, + { + "name": "smartWalletConfig", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 109, + 97, + 114, + 116, + 95, + 119, + 97, + 108, + 108, + 101, + 116, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "smartWallet" + } + ] + } + }, + { + "name": "cpiProgram" + }, + { + "name": "cpiCommit", + "docs": [ + "Commit to execute. Closed on success to refund rent." + ], + "writable": true + }, + { + "name": "commitRefund", + "writable": true + } + ], + "args": [ + { + "name": "cpiData", + "type": "bytes" + } + ] + }, + { + "name": "executeTxnDirect", + "discriminator": [ + 121, + 40, + 165, + 106, + 50, + 95, + 121, + 118 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "smartWallet", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 109, + 97, + 114, + 116, + 95, + 119, + 97, + 108, + 108, + 101, + 116 + ] + }, + { + "kind": "account", + "path": "smart_wallet_config.id", + "account": "smartWalletConfig" + } + ] + } + }, + { + "name": "smartWalletConfig", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 109, + 97, + 114, + 116, + 95, + 119, + 97, + 108, + 108, + 101, + 116, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "smartWallet" + } + ] + } + }, + { + "name": "smartWalletAuthenticator" + }, + { + "name": "whitelistRulePrograms", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 119, + 104, + 105, + 116, + 101, + 108, + 105, + 115, + 116, + 95, + 114, + 117, + 108, + 101, + 95, + 112, + 114, + 111, + 103, + 114, + 97, + 109, + 115 + ] + } + ] + } + }, + { + "name": "authenticatorProgram" + }, + { + "name": "cpiProgram" + }, + { + "name": "config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + }, + { + "name": "ixSysvar", + "address": "Sysvar1nstructions1111111111111111111111111" + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": { + "name": "executeTxnArgs" + } + } + } + ] + }, + { + "name": "initialize", + "docs": [ + "Initialize the program by creating the sequence tracker" + ], + "discriminator": [ + 175, + 175, + 109, + 31, + 13, + 152, + 155, + 237 + ], + "accounts": [ + { + "name": "signer", + "docs": [ + "The signer of the transaction, who will be the initial authority." + ], + "writable": true, + "signer": true + }, + { + "name": "config", + "docs": [ + "The program's configuration account." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + }, + { + "name": "whitelistRulePrograms", + "docs": [ + "The list of whitelisted rule programs that can be used with smart wallets." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 119, + 104, + 105, + 116, + 101, + 108, + 105, + 115, + 116, + 95, + 114, + 117, + 108, + 101, + 95, + 112, + 114, + 111, + 103, + 114, + 97, + 109, + 115 + ] + } + ] + } + }, + { + "name": "defaultRuleProgram", + "docs": [ + "The default rule program to be used for new smart wallets." + ] + }, + { + "name": "systemProgram", + "docs": [ + "The system program." + ], + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "updateConfig", + "docs": [ + "Update the program configuration" + ], + "discriminator": [ + 29, + 158, + 252, + 191, + 10, + 83, + 219, + 99 + ], + "accounts": [ + { + "name": "authority", + "docs": [ + "The current authority of the program." + ], + "writable": true, + "signer": true, + "relations": [ + "config" + ] + }, + { + "name": "config", + "docs": [ + "The program's configuration account." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + } + ], + "args": [ + { + "name": "param", + "type": { + "defined": { + "name": "updateConfigType" + } + } + }, + { + "name": "value", + "type": "u64" + } + ] + } + ], + "accounts": [ + { + "name": "config", + "discriminator": [ + 155, + 12, + 170, + 224, + 30, + 250, + 204, + 130 + ] + }, + { + "name": "cpiCommit", + "discriminator": [ + 50, + 161, + 109, + 178, + 148, + 116, + 95, + 160 + ] + }, + { + "name": "smartWalletAuthenticator", + "discriminator": [ + 126, + 36, + 85, + 166, + 77, + 139, + 221, + 129 + ] + }, + { + "name": "smartWalletConfig", + "discriminator": [ + 138, + 211, + 3, + 80, + 65, + 100, + 207, + 142 + ] + }, + { + "name": "whitelistRulePrograms", + "discriminator": [ + 234, + 147, + 45, + 188, + 65, + 212, + 154, + 241 + ] + } + ], + "events": [ + { + "name": "authenticatorAdded", + "discriminator": [ + 213, + 87, + 171, + 174, + 101, + 129, + 32, + 44 + ] + }, + { + "name": "configUpdated", + "discriminator": [ + 40, + 241, + 230, + 122, + 11, + 19, + 198, + 194 + ] + }, + { + "name": "errorEvent", + "discriminator": [ + 163, + 35, + 212, + 206, + 66, + 104, + 234, + 251 + ] + }, + { + "name": "feeCollected", + "discriminator": [ + 12, + 28, + 17, + 248, + 244, + 36, + 8, + 73 + ] + }, + { + "name": "programInitialized", + "discriminator": [ + 43, + 70, + 110, + 241, + 199, + 218, + 221, + 245 + ] + }, + { + "name": "programPausedStateChanged", + "discriminator": [ + 148, + 9, + 117, + 157, + 18, + 25, + 122, + 32 + ] + }, + { + "name": "ruleProgramChanged", + "discriminator": [ + 116, + 110, + 184, + 140, + 118, + 243, + 237, + 111 + ] + }, + { + "name": "securityEvent", + "discriminator": [ + 16, + 175, + 241, + 170, + 85, + 9, + 201, + 100 + ] + }, + { + "name": "smartWalletCreated", + "discriminator": [ + 145, + 37, + 118, + 21, + 58, + 251, + 56, + 128 + ] + }, + { + "name": "solTransfer", + "discriminator": [ + 0, + 186, + 79, + 129, + 194, + 76, + 94, + 9 + ] + }, + { + "name": "transactionExecuted", + "discriminator": [ + 211, + 227, + 168, + 14, + 32, + 111, + 189, + 210 + ] + }, + { + "name": "whitelistRuleProgramAdded", + "discriminator": [ + 219, + 72, + 34, + 198, + 65, + 224, + 225, + 103 + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "passkeyMismatch", + "msg": "Passkey public key mismatch with stored authenticator" + }, + { + "code": 6001, + "name": "smartWalletMismatch", + "msg": "Smart wallet address mismatch with authenticator" + }, + { + "code": 6002, + "name": "authenticatorNotFound", + "msg": "Smart wallet authenticator account not found or invalid" + }, + { + "code": 6003, + "name": "secp256r1InvalidLength", + "msg": "Secp256r1 instruction has invalid data length" + }, + { + "code": 6004, + "name": "secp256r1HeaderMismatch", + "msg": "Secp256r1 instruction header validation failed" + }, + { + "code": 6005, + "name": "secp256r1DataMismatch", + "msg": "Secp256r1 signature data validation failed" + }, + { + "code": 6006, + "name": "secp256r1InstructionNotFound", + "msg": "Secp256r1 instruction not found at specified index" + }, + { + "code": 6007, + "name": "invalidSignature", + "msg": "Invalid signature provided for passkey verification" + }, + { + "code": 6008, + "name": "clientDataInvalidUtf8", + "msg": "Client data JSON is not valid UTF-8" + }, + { + "code": 6009, + "name": "clientDataJsonParseError", + "msg": "Client data JSON parsing failed" + }, + { + "code": 6010, + "name": "challengeMissing", + "msg": "Challenge field missing from client data JSON" + }, + { + "code": 6011, + "name": "challengeBase64DecodeError", + "msg": "Challenge base64 decoding failed" + }, + { + "code": 6012, + "name": "challengeDeserializationError", + "msg": "Challenge message deserialization failed" + }, + { + "code": 6013, + "name": "timestampTooOld", + "msg": "Message timestamp is too far in the past" + }, + { + "code": 6014, + "name": "timestampTooNew", + "msg": "Message timestamp is too far in the future" + }, + { + "code": 6015, + "name": "nonceMismatch", + "msg": "Nonce mismatch: expected different value" + }, + { + "code": 6016, + "name": "nonceOverflow", + "msg": "Nonce overflow: cannot increment further" + }, + { + "code": 6017, + "name": "ruleProgramNotWhitelisted", + "msg": "Rule program not found in whitelist" + }, + { + "code": 6018, + "name": "whitelistFull", + "msg": "The whitelist of rule programs is full." + }, + { + "code": 6019, + "name": "ruleDataRequired", + "msg": "Rule data is required but not provided" + }, + { + "code": 6020, + "name": "invalidCheckRuleDiscriminator", + "msg": "Invalid instruction discriminator for check_rule" + }, + { + "code": 6021, + "name": "invalidDestroyDiscriminator", + "msg": "Invalid instruction discriminator for destroy" + }, + { + "code": 6022, + "name": "invalidInitRuleDiscriminator", + "msg": "Invalid instruction discriminator for init_rule" + }, + { + "code": 6023, + "name": "ruleProgramsIdentical", + "msg": "Old and new rule programs are identical" + }, + { + "code": 6024, + "name": "noDefaultRuleProgram", + "msg": "Neither old nor new rule program is the default" + }, + { + "code": 6025, + "name": "invalidRemainingAccounts", + "msg": "Invalid remaining accounts" + }, + { + "code": 6026, + "name": "cpiDataMissing", + "msg": "CPI data is required but not provided" + }, + { + "code": 6027, + "name": "invalidCpiData", + "msg": "CPI data is invalid or malformed" + }, + { + "code": 6028, + "name": "insufficientRuleAccounts", + "msg": "Insufficient remaining accounts for rule instruction" + }, + { + "code": 6029, + "name": "insufficientCpiAccounts", + "msg": "Insufficient remaining accounts for CPI instruction" + }, + { + "code": 6030, + "name": "accountSliceOutOfBounds", + "msg": "Account slice index out of bounds" + }, + { + "code": 6031, + "name": "solTransferInsufficientAccounts", + "msg": "SOL transfer requires at least 2 remaining accounts" + }, + { + "code": 6032, + "name": "newAuthenticatorMissing", + "msg": "New authenticator account is required but not provided" + }, + { + "code": 6033, + "name": "newAuthenticatorPasskeyMissing", + "msg": "New authenticator passkey is required but not provided" + }, + { + "code": 6034, + "name": "insufficientLamports", + "msg": "Insufficient lamports for requested transfer" + }, + { + "code": 6035, + "name": "transferAmountOverflow", + "msg": "Transfer amount would cause arithmetic overflow" + }, + { + "code": 6036, + "name": "invalidBumpSeed", + "msg": "Invalid bump seed for PDA derivation" + }, + { + "code": 6037, + "name": "invalidAccountOwner", + "msg": "Account owner verification failed" + }, + { + "code": 6038, + "name": "invalidAccountDiscriminator", + "msg": "Account discriminator mismatch" + }, + { + "code": 6039, + "name": "invalidProgramId", + "msg": "Invalid program ID" + }, + { + "code": 6040, + "name": "programNotExecutable", + "msg": "Program not executable" + }, + { + "code": 6041, + "name": "smartWalletAuthenticatorAlreadyInitialized", + "msg": "Smart wallet authenticator already initialized" + }, + { + "code": 6042, + "name": "credentialIdTooLarge", + "msg": "Credential ID exceeds maximum allowed size" + }, + { + "code": 6043, + "name": "credentialIdEmpty", + "msg": "Credential ID cannot be empty" + }, + { + "code": 6044, + "name": "ruleDataTooLarge", + "msg": "Rule data exceeds maximum allowed size" + }, + { + "code": 6045, + "name": "cpiDataTooLarge", + "msg": "CPI data exceeds maximum allowed size" + }, + { + "code": 6046, + "name": "tooManyRemainingAccounts", + "msg": "Too many remaining accounts provided" + }, + { + "code": 6047, + "name": "invalidPdaDerivation", + "msg": "Invalid PDA derivation" + }, + { + "code": 6048, + "name": "transactionTooOld", + "msg": "Transaction is too old" + }, + { + "code": 6049, + "name": "rateLimitExceeded", + "msg": "Rate limit exceeded" + }, + { + "code": 6050, + "name": "invalidAccountData", + "msg": "Invalid account data" + }, + { + "code": 6051, + "name": "unauthorized", + "msg": "Unauthorized access attempt" + }, + { + "code": 6052, + "name": "programPaused", + "msg": "Program is paused" + }, + { + "code": 6053, + "name": "invalidInstructionData", + "msg": "Invalid instruction data" + }, + { + "code": 6054, + "name": "accountAlreadyInitialized", + "msg": "Account already initialized" + }, + { + "code": 6055, + "name": "accountNotInitialized", + "msg": "Account not initialized" + }, + { + "code": 6056, + "name": "invalidAccountState", + "msg": "Invalid account state" + }, + { + "code": 6057, + "name": "integerOverflow", + "msg": "Operation would cause integer overflow" + }, + { + "code": 6058, + "name": "integerUnderflow", + "msg": "Operation would cause integer underflow" + }, + { + "code": 6059, + "name": "invalidFeeAmount", + "msg": "Invalid fee amount" + }, + { + "code": 6060, + "name": "insufficientBalanceForFee", + "msg": "Insufficient balance for fee" + }, + { + "code": 6061, + "name": "invalidAuthority", + "msg": "Invalid authority" + }, + { + "code": 6062, + "name": "authorityMismatch", + "msg": "Authority mismatch" + }, + { + "code": 6063, + "name": "invalidSequenceNumber", + "msg": "Invalid sequence number" + }, + { + "code": 6064, + "name": "duplicateTransaction", + "msg": "Duplicate transaction detected" + }, + { + "code": 6065, + "name": "invalidTransactionOrdering", + "msg": "Invalid transaction ordering" + }, + { + "code": 6066, + "name": "maxWalletLimitReached", + "msg": "Maximum wallet limit reached" + }, + { + "code": 6067, + "name": "invalidWalletConfiguration", + "msg": "Invalid wallet configuration" + }, + { + "code": 6068, + "name": "walletNotFound", + "msg": "Wallet not found" + }, + { + "code": 6069, + "name": "invalidPasskeyFormat", + "msg": "Invalid passkey format" + }, + { + "code": 6070, + "name": "passkeyAlreadyRegistered", + "msg": "Passkey already registered" + }, + { + "code": 6071, + "name": "invalidMessageFormat", + "msg": "Invalid message format" + }, + { + "code": 6072, + "name": "messageSizeExceedsLimit", + "msg": "Message size exceeds limit" + }, + { + "code": 6073, + "name": "invalidSplitIndex", + "msg": "Invalid split index" + }, + { + "code": 6074, + "name": "cpiExecutionFailed", + "msg": "CPI execution failed" + }, + { + "code": 6075, + "name": "invalidProgramAddress", + "msg": "Invalid program address" + }, + { + "code": 6076, + "name": "whitelistOperationFailed", + "msg": "Whitelist operation failed" + }, + { + "code": 6077, + "name": "invalidWhitelistState", + "msg": "Invalid whitelist state" + }, + { + "code": 6078, + "name": "emergencyShutdown", + "msg": "Emergency shutdown activated" + }, + { + "code": 6079, + "name": "recoveryModeRequired", + "msg": "Recovery mode required" + }, + { + "code": 6080, + "name": "invalidRecoveryAttempt", + "msg": "Invalid recovery attempt" + }, + { + "code": 6081, + "name": "auditLogFull", + "msg": "Audit log full" + }, + { + "code": 6082, + "name": "invalidAuditEntry", + "msg": "Invalid audit entry" + }, + { + "code": 6083, + "name": "reentrancyDetected", + "msg": "Reentrancy detected" + }, + { + "code": 6084, + "name": "invalidCallDepth", + "msg": "Invalid call depth" + }, + { + "code": 6085, + "name": "stackOverflowProtection", + "msg": "Stack overflow protection triggered" + }, + { + "code": 6086, + "name": "memoryLimitExceeded", + "msg": "Memory limit exceeded" + }, + { + "code": 6087, + "name": "computationLimitExceeded", + "msg": "Computation limit exceeded" + }, + { + "code": 6088, + "name": "invalidRentExemption", + "msg": "Invalid rent exemption" + }, + { + "code": 6089, + "name": "accountClosureFailed", + "msg": "Account closure failed" + }, + { + "code": 6090, + "name": "invalidAccountClosure", + "msg": "Invalid account closure" + }, + { + "code": 6091, + "name": "refundFailed", + "msg": "Refund failed" + }, + { + "code": 6092, + "name": "invalidRefundAmount", + "msg": "Invalid refund amount" + } + ], + "types": [ + { + "name": "authenticatorAdded", + "docs": [ + "Event emitted when a new authenticator is added" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "smartWallet", + "type": "pubkey" + }, + { + "name": "newAuthenticator", + "type": "pubkey" + }, + { + "name": "passkeyHash", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "addedBy", + "type": "pubkey" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "callRuleArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "passkeyPubkey", + "type": { + "array": [ + "u8", + 33 + ] + } + }, + { + "name": "signature", + "type": "bytes" + }, + { + "name": "clientDataJsonRaw", + "type": "bytes" + }, + { + "name": "authenticatorDataRaw", + "type": "bytes" + }, + { + "name": "verifyInstructionIndex", + "type": "u8" + }, + { + "name": "ruleData", + "type": "bytes" + }, + { + "name": "newAuthenticator", + "type": { + "option": { + "defined": { + "name": "newAuthenticatorArgs" + } + } + } + } + ] + } + }, + { + "name": "changeRuleArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "passkeyPubkey", + "type": { + "array": [ + "u8", + 33 + ] + } + }, + { + "name": "signature", + "type": "bytes" + }, + { + "name": "clientDataJsonRaw", + "type": "bytes" + }, + { + "name": "authenticatorDataRaw", + "type": "bytes" + }, + { + "name": "verifyInstructionIndex", + "type": "u8" + }, + { + "name": "splitIndex", + "type": "u16" + }, + { + "name": "destroyRuleData", + "type": "bytes" + }, + { + "name": "initRuleData", + "type": "bytes" + }, + { + "name": "newAuthenticator", + "type": { + "option": { + "defined": { + "name": "newAuthenticatorArgs" + } + } + } + } + ] + } + }, + { + "name": "commitArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "passkeyPubkey", + "type": { + "array": [ + "u8", + 33 + ] + } + }, + { + "name": "signature", + "type": "bytes" + }, + { + "name": "clientDataJsonRaw", + "type": "bytes" + }, + { + "name": "authenticatorDataRaw", + "type": "bytes" + }, + { + "name": "verifyInstructionIndex", + "type": "u8" + }, + { + "name": "ruleData", + "type": "bytes" + }, + { + "name": "expiresAt", + "type": "i64" + } + ] + } + }, + { + "name": "config", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "createSmartWalletFee", + "type": "u64" + }, + { + "name": "executeFee", + "type": "u64" + }, + { + "name": "defaultRuleProgram", + "type": "pubkey" + }, + { + "name": "isPaused", + "type": "bool" + } + ] + } + }, + { + "name": "configUpdated", + "docs": [ + "Event emitted when program configuration is updated" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "updateType", + "type": "string" + }, + { + "name": "oldValue", + "type": "string" + }, + { + "name": "newValue", + "type": "string" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "cpiCommit", + "docs": [ + "Commit record for a future CPI execution.", + "Created after full passkey + rule verification. Contains all bindings", + "necessary to perform the CPI later without re-verification." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "ownerWallet", + "docs": [ + "Smart wallet that authorized this commit" + ], + "type": "pubkey" + }, + { + "name": "dataHash", + "docs": [ + "sha256 of CPI instruction data" + ], + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "accountsHash", + "docs": [ + "sha256 over ordered remaining account metas plus `target_program`" + ], + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "authorizedNonce", + "docs": [ + "The nonce that was authorized at commit time (bound into data hash)" + ], + "type": "u64" + }, + { + "name": "expiresAt", + "docs": [ + "Unix expiration timestamp" + ], + "type": "i64" + }, + { + "name": "rentRefundTo", + "docs": [ + "Where to refund rent when closing the commit" + ], + "type": "pubkey" + } + ] + } + }, + { + "name": "creatwSmartWalletArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "passkeyPubkey", + "type": { + "array": [ + "u8", + 33 + ] + } + }, + { + "name": "credentialId", + "type": "bytes" + }, + { + "name": "ruleData", + "type": "bytes" + }, + { + "name": "walletId", + "type": "u64" + }, + { + "name": "isPayForUser", + "type": "bool" + } + ] + } + }, + { + "name": "errorEvent", + "docs": [ + "Event emitted for errors that are caught and handled" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "smartWallet", + "type": { + "option": "pubkey" + } + }, + { + "name": "errorCode", + "type": "string" + }, + { + "name": "errorMessage", + "type": "string" + }, + { + "name": "actionAttempted", + "type": "string" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "executeTxnArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "passkeyPubkey", + "type": { + "array": [ + "u8", + 33 + ] + } + }, + { + "name": "signature", + "type": "bytes" + }, + { + "name": "clientDataJsonRaw", + "type": "bytes" + }, + { + "name": "authenticatorDataRaw", + "type": "bytes" + }, + { + "name": "verifyInstructionIndex", + "type": "u8" + }, + { + "name": "splitIndex", + "type": "u16" + }, + { + "name": "ruleData", + "type": "bytes" + }, + { + "name": "cpiData", + "type": "bytes" + } + ] + } + }, + { + "name": "feeCollected", + "docs": [ + "Event emitted when a fee is collected" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "smartWallet", + "type": "pubkey" + }, + { + "name": "feeType", + "type": "string" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "recipient", + "type": "pubkey" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "newAuthenticatorArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "passkeyPubkey", + "type": { + "array": [ + "u8", + 33 + ] + } + }, + { + "name": "credentialId", + "type": "bytes" + } + ] + } + }, + { + "name": "programInitialized", + "docs": [ + "Event emitted when program is initialized" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "defaultRuleProgram", + "type": "pubkey" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "programPausedStateChanged", + "docs": [ + "Event emitted when program is paused/unpaused" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "isPaused", + "type": "bool" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "ruleProgramChanged", + "docs": [ + "Event emitted when a rule program is changed" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "smartWallet", + "type": "pubkey" + }, + { + "name": "oldRuleProgram", + "type": "pubkey" + }, + { + "name": "newRuleProgram", + "type": "pubkey" + }, + { + "name": "nonce", + "type": "u64" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "securityEvent", + "docs": [ + "Event emitted for security-related events" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "eventType", + "type": "string" + }, + { + "name": "smartWallet", + "type": { + "option": "pubkey" + } + }, + { + "name": "details", + "type": "string" + }, + { + "name": "severity", + "type": "string" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "smartWalletAuthenticator", + "docs": [ + "Account that stores authentication data for a smart wallet" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "passkeyPubkey", + "docs": [ + "The public key of the passkey that can authorize transactions" + ], + "type": { + "array": [ + "u8", + 33 + ] + } + }, + { + "name": "smartWallet", + "docs": [ + "The smart wallet this authenticator belongs to" + ], + "type": "pubkey" + }, + { + "name": "credentialId", + "docs": [ + "The credential ID this authenticator belongs to" + ], + "type": "bytes" + }, + { + "name": "bump", + "docs": [ + "Bump seed for PDA derivation" + ], + "type": "u8" + } + ] + } + }, + { + "name": "smartWalletConfig", + "docs": [ + "Data account for a smart wallet" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "id", + "docs": [ + "Unique identifier for this smart wallet" + ], + "type": "u64" + }, + { + "name": "ruleProgram", + "docs": [ + "Optional rule program that governs this wallet's operations" + ], + "type": "pubkey" + }, + { + "name": "lastNonce", + "type": "u64" + }, + { + "name": "bump", + "docs": [ + "Bump seed for PDA derivation" + ], + "type": "u8" + } + ] + } + }, + { + "name": "smartWalletCreated", + "docs": [ + "Event emitted when a new smart wallet is created" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "smartWallet", + "type": "pubkey" + }, + { + "name": "authenticator", + "type": "pubkey" + }, + { + "name": "sequenceId", + "type": "u64" + }, + { + "name": "ruleProgram", + "type": "pubkey" + }, + { + "name": "passkeyHash", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "solTransfer", + "docs": [ + "Event emitted when a SOL transfer occurs" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "smartWallet", + "type": "pubkey" + }, + { + "name": "destination", + "type": "pubkey" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "nonce", + "type": "u64" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "transactionExecuted", + "docs": [ + "Event emitted when a transaction is executed" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "smartWallet", + "type": "pubkey" + }, + { + "name": "authenticator", + "type": "pubkey" + }, + { + "name": "nonce", + "type": "u64" + }, + { + "name": "ruleProgram", + "type": "pubkey" + }, + { + "name": "cpiProgram", + "type": "pubkey" + }, + { + "name": "success", + "type": "bool" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "updateConfigType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "createWalletFee" + }, + { + "name": "executeFee" + }, + { + "name": "defaultRuleProgram" + }, + { + "name": "admin" + }, + { + "name": "pauseProgram" + }, + { + "name": "unpauseProgram" + } + ] + } + }, + { + "name": "whitelistRuleProgramAdded", + "docs": [ + "Event emitted when a whitelist rule program is added" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "ruleProgram", + "type": "pubkey" + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "whitelistRulePrograms", + "docs": [ + "Account that stores whitelisted rule program addresses" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "list", + "docs": [ + "List of whitelisted program addresses" + ], + "type": { + "vec": "pubkey" + } + }, + { + "name": "bump", + "docs": [ + "Bump seed for PDA derivation" + ], + "type": "u8" + } + ] + } + } + ] +}; diff --git a/contract-integration/client/defaultRule.ts b/contract-integration/client/defaultRule.ts new file mode 100644 index 0000000..f4e3077 --- /dev/null +++ b/contract-integration/client/defaultRule.ts @@ -0,0 +1,69 @@ +import * as anchor from '@coral-xyz/anchor'; +import { Connection, PublicKey, SystemProgram, TransactionInstruction } from '@solana/web3.js'; +import DefaultRuleIdl from '../anchor/idl/default_rule.json'; +import { DefaultRule } from '../anchor/types/default_rule'; +import { deriveRulePda } from '../pda/defaultRule'; + +export class DefaultRuleClient { + readonly connection: Connection; + readonly program: anchor.Program; + readonly programId: PublicKey; + + constructor(connection: Connection) { + this.connection = connection; + + this.program = new anchor.Program(DefaultRuleIdl as DefaultRule, { + connection: connection, + }); + this.programId = this.program.programId; + } + + rulePda(smartWalletAuthenticator: PublicKey): PublicKey { + return deriveRulePda(this.programId, smartWalletAuthenticator); + } + + async buildInitRuleIx( + payer: PublicKey, + smartWallet: PublicKey, + smartWalletAuthenticator: PublicKey + ): Promise { + return await this.program.methods + .initRule() + .accountsPartial({ + payer, + smartWallet, + smartWalletAuthenticator, + rule: this.rulePda(smartWalletAuthenticator), + systemProgram: SystemProgram.programId, + }) + .instruction(); + } + + async buildCheckRuleIx(smartWalletAuthenticator: PublicKey): Promise { + return await this.program.methods + .checkRule() + .accountsPartial({ + rule: this.rulePda(smartWalletAuthenticator), + smartWalletAuthenticator, + }) + .instruction(); + } + + async buildAddDeviceIx( + payer: PublicKey, + smartWalletAuthenticator: PublicKey, + newSmartWalletAuthenticator: PublicKey + ): Promise { + return await this.program.methods + .addDevice() + .accountsPartial({ + payer, + smartWalletAuthenticator, + newSmartWalletAuthenticator, + rule: this.rulePda(smartWalletAuthenticator), + newRule: this.rulePda(newSmartWalletAuthenticator), + systemProgram: SystemProgram.programId, + }) + .instruction(); + } +} diff --git a/contract-integration/client/lazorkit.ts b/contract-integration/client/lazorkit.ts new file mode 100644 index 0000000..b26d9d3 --- /dev/null +++ b/contract-integration/client/lazorkit.ts @@ -0,0 +1,643 @@ +import { Program, BN } from '@coral-xyz/anchor'; +import { + PublicKey, + Transaction, + TransactionMessage, + TransactionInstruction, + Connection, + SystemProgram, + SYSVAR_INSTRUCTIONS_PUBKEY, + VersionedTransaction, + AccountMeta, +} from '@solana/web3.js'; +import LazorkitIdl from '../anchor/idl/lazorkit.json'; +import { Lazorkit } from '../anchor/types/lazorkit'; +import { + deriveConfigPda, + deriveWhitelistRuleProgramsPda, + deriveSmartWalletPda, + deriveSmartWalletConfigPda, + deriveSmartWalletAuthenticatorPda, + deriveCpiCommitPda, +} from '../pda/lazorkit'; +import { buildSecp256r1VerifyIx } from '../webauthn/secp256r1'; +import { instructionToAccountMetas } from '../utils'; +import { sha256 } from 'js-sha256'; +import * as types from '../types'; +import { randomBytes } from 'crypto'; +import { DefaultRuleClient } from './defaultRule'; +import * as bs58 from 'bs58'; +import { Buffer } from 'buffer'; +import { buildCallRuleMessage, buildChangeRuleMessage, buildExecuteMessage } from '../messages'; + +export class LazorkitClient { + readonly connection: Connection; + readonly program: Program; + readonly programId: PublicKey; + readonly defaultRuleProgram: DefaultRuleClient; + + constructor(connection: Connection) { + this.connection = connection; + + this.program = new Program(LazorkitIdl as Lazorkit, { + connection: connection, + }); + this.defaultRuleProgram = new DefaultRuleClient(connection); + this.programId = this.program.programId; + } + + // PDAs + configPda(): PublicKey { + return deriveConfigPda(this.programId); + } + whitelistRuleProgramsPda(): PublicKey { + return deriveWhitelistRuleProgramsPda(this.programId); + } + smartWalletPda(walletId: BN): PublicKey { + return deriveSmartWalletPda(this.programId, walletId); + } + smartWalletConfigPda(smartWallet: PublicKey): PublicKey { + return deriveSmartWalletConfigPda(this.programId, smartWallet); + } + smartWalletAuthenticatorPda(smartWallet: PublicKey, passkey: number[]): PublicKey { + return deriveSmartWalletAuthenticatorPda(this.programId, smartWallet, passkey)[0]; + } + cpiCommitPda(smartWallet: PublicKey, lastNonce: BN): PublicKey { + return deriveCpiCommitPda(this.programId, smartWallet, lastNonce); + } + + // Convenience helpers + generateWalletId(): BN { + return new BN(randomBytes(8), 'le'); + } + + async getConfigData() { + return await this.program.account.config.fetch(this.configPda()); + } + async getSmartWalletConfigData(smartWallet: PublicKey) { + const pda = this.smartWalletConfigPda(smartWallet); + return await this.program.account.smartWalletConfig.fetch(pda); + } + async getSmartWalletAuthenticatorData(smartWalletAuthenticator: PublicKey) { + return await this.program.account.smartWalletAuthenticator.fetch(smartWalletAuthenticator); + } + async getSmartWalletByPasskey(passkeyPubkey: number[]): Promise<{ + smartWallet: PublicKey | null; + smartWalletAuthenticator: PublicKey | null; + }> { + const discriminator = LazorkitIdl.accounts.find( + (a: any) => a.name === 'SmartWalletAuthenticator' + )!.discriminator; + + const accounts = await this.connection.getProgramAccounts(this.programId, { + dataSlice: { + offset: 8, + length: 33, + }, + filters: [ + { memcmp: { offset: 0, bytes: bs58.encode(discriminator) } }, + { memcmp: { offset: 8, bytes: bs58.encode(passkeyPubkey) } }, + ], + }); + + if (accounts.length === 0) { + return { smartWalletAuthenticator: null, smartWallet: null }; + } + + const smartWalletAuthenticatorData = await this.getSmartWalletAuthenticatorData( + accounts[0].pubkey + ); + + return { + smartWalletAuthenticator: accounts[0].pubkey, + smartWallet: smartWalletAuthenticatorData.smartWallet, + }; + } + + // Builders (TransactionInstruction) + async buildInitializeIx(payer: PublicKey): Promise { + return await this.program.methods + .initialize() + .accountsPartial({ + signer: payer, + config: this.configPda(), + whitelistRulePrograms: this.whitelistRuleProgramsPda(), + defaultRuleProgram: this.defaultRuleProgram.programId, + systemProgram: SystemProgram.programId, + }) + .instruction(); + } + + async buildCreateSmartWalletIx( + payer: PublicKey, + smartWallet: PublicKey, + smartWalletAuthenticator: PublicKey, + ruleInstruction: TransactionInstruction, + args: types.CreatwSmartWalletArgs + ): Promise { + return await this.program.methods + .createSmartWallet(args) + .accountsPartial({ + signer: payer, + smartWallet, + smartWalletConfig: this.smartWalletConfigPda(smartWallet), + smartWalletAuthenticator, + config: this.configPda(), + defaultRuleProgram: this.defaultRuleProgram.programId, + systemProgram: SystemProgram.programId, + }) + .remainingAccounts([...instructionToAccountMetas(ruleInstruction, payer)]) + .instruction(); + } + + async buildExecuteTxnDirectIx( + payer: PublicKey, + smartWallet: PublicKey, + args: types.ExecuteTxnArgs, + ruleInstruction: TransactionInstruction, + cpiInstruction: TransactionInstruction + ): Promise { + return await this.program.methods + .executeTxnDirect(args) + .accountsPartial({ + payer, + smartWallet, + smartWalletConfig: this.smartWalletConfigPda(smartWallet), + smartWalletAuthenticator: this.smartWalletAuthenticatorPda(smartWallet, args.passkeyPubkey), + whitelistRulePrograms: this.whitelistRuleProgramsPda(), + authenticatorProgram: ruleInstruction.programId, + cpiProgram: cpiInstruction.programId, + config: this.configPda(), + ixSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, + }) + .remainingAccounts([ + ...instructionToAccountMetas(ruleInstruction, payer), + ...instructionToAccountMetas(cpiInstruction, payer), + ]) + .instruction(); + } + + async buildCallRuleDirectIx( + payer: PublicKey, + smartWallet: PublicKey, + args: types.CallRuleArgs, + ruleInstruction: TransactionInstruction + ): Promise { + const remaining: AccountMeta[] = []; + + if (args.newAuthenticator) { + const newSmartWalletAuthenticator = this.smartWalletAuthenticatorPda( + smartWallet, + args.newAuthenticator.passkeyPubkey + ); + remaining.push({ + pubkey: newSmartWalletAuthenticator, + isWritable: true, + isSigner: false, + }); + } + + remaining.push(...instructionToAccountMetas(ruleInstruction, payer)); + + return await this.program.methods + .callRuleDirect(args) + .accountsPartial({ + payer, + config: this.configPda(), + smartWallet, + smartWalletConfig: this.smartWalletConfigPda(smartWallet), + smartWalletAuthenticator: this.smartWalletAuthenticatorPda(smartWallet, args.passkeyPubkey), + ruleProgram: ruleInstruction.programId, + whitelistRulePrograms: this.whitelistRuleProgramsPda(), + ixSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, + systemProgram: SystemProgram.programId, + }) + .remainingAccounts(remaining) + .instruction(); + } + + async buildChangeRuleDirectIx( + payer: PublicKey, + smartWallet: PublicKey, + args: types.ChangeRuleArgs, + destroyRuleInstruction: TransactionInstruction, + initRuleInstruction: TransactionInstruction + ): Promise { + const remaining: AccountMeta[] = []; + + if (args.newAuthenticator) { + const newSmartWalletAuthenticator = this.smartWalletAuthenticatorPda( + smartWallet, + args.newAuthenticator.passkeyPubkey + ); + remaining.push({ + pubkey: newSmartWalletAuthenticator, + isWritable: true, + isSigner: false, + }); + } + + remaining.push(...instructionToAccountMetas(destroyRuleInstruction, payer)); + remaining.push(...instructionToAccountMetas(initRuleInstruction, payer)); + + return await this.program.methods + .changeRuleDirect(args) + .accountsPartial({ + payer, + config: this.configPda(), + smartWallet, + smartWalletConfig: this.smartWalletConfigPda(smartWallet), + smartWalletAuthenticator: this.smartWalletAuthenticatorPda(smartWallet, args.passkeyPubkey), + oldRuleProgram: destroyRuleInstruction.programId, + newRuleProgram: initRuleInstruction.programId, + whitelistRulePrograms: this.whitelistRuleProgramsPda(), + ixSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, + systemProgram: SystemProgram.programId, + }) + .remainingAccounts(remaining) + .instruction(); + } + + async buildCommitCpiIx( + payer: PublicKey, + smartWallet: PublicKey, + args: types.CommitArgs, + ruleInstruction: TransactionInstruction + ): Promise { + return await this.program.methods + .commitCpi(args) + .accountsPartial({ + payer, + config: this.configPda(), + smartWallet, + smartWalletConfig: this.smartWalletConfigPda(smartWallet), + smartWalletAuthenticator: this.smartWalletAuthenticatorPda(smartWallet, args.passkeyPubkey), + whitelistRulePrograms: this.whitelistRuleProgramsPda(), + authenticatorProgram: ruleInstruction.programId, + ixSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, + systemProgram: SystemProgram.programId, + }) + .remainingAccounts([...instructionToAccountMetas(ruleInstruction, payer)]) + .instruction(); + } + + async buildExecuteCommittedIx( + payer: PublicKey, + smartWallet: PublicKey, + cpiInstruction: TransactionInstruction + ): Promise { + const cfg = await this.getSmartWalletConfigData(smartWallet); + const cpiCommit = this.cpiCommitPda(smartWallet, cfg.lastNonce); + + return await this.program.methods + .executeCommitted(cpiInstruction.data) + .accountsPartial({ + payer, + config: this.configPda(), + smartWallet, + smartWalletConfig: this.smartWalletConfigPda(smartWallet), + cpiProgram: cpiInstruction.programId, + cpiCommit, + commitRefund: payer, + }) + .remainingAccounts([...instructionToAccountMetas(cpiInstruction, payer)]) + .instruction(); + } + + // High-level: build transactions with Secp256r1 verify ix at index 0 + async executeTxnDirectTx(params: { + payer: PublicKey; + smartWallet: PublicKey; + passkeyPubkey: number[]; + signature64: String; + clientDataJsonRaw64: String; + authenticatorDataRaw64: String; + ruleInstruction?: TransactionInstruction; + cpiInstruction: TransactionInstruction; + }): Promise { + const authenticatorDataRaw = Buffer.from(params.authenticatorDataRaw64, 'base64'); + const clientDataJsonRaw = Buffer.from(params.clientDataJsonRaw64, 'base64'); + const verifyIx = buildSecp256r1VerifyIx( + Buffer.concat([authenticatorDataRaw, Buffer.from(sha256.hex(clientDataJsonRaw), 'hex')]), + Buffer.from(params.passkeyPubkey), + Buffer.from(params.signature64) + ); + + let ruleInstruction = await this.defaultRuleProgram.buildCheckRuleIx( + this.smartWalletAuthenticatorPda(params.smartWallet, params.passkeyPubkey) + ); + if (params.ruleInstruction) { + ruleInstruction = ruleInstruction; + } + + const execIx = await this.buildExecuteTxnDirectIx( + params.payer, + params.smartWallet, + { + passkeyPubkey: params.passkeyPubkey, + signature: Buffer.from(params.signature64, 'base64'), + clientDataJsonRaw, + authenticatorDataRaw, + verifyInstructionIndex: 0, + ruleData: ruleInstruction.data, + cpiData: params.cpiInstruction.data, + splitIndex: ruleInstruction.keys.length, + }, + ruleInstruction, + params.cpiInstruction + ); + return this.buildV0Tx(params.payer, [verifyIx, execIx]); + } + + async callRuleDirectTx(params: { + payer: PublicKey; + smartWallet: PublicKey; + passkeyPubkey: number[]; + signature64: String; + clientDataJsonRaw64: String; + authenticatorDataRaw64: String; + ruleProgram: PublicKey; + ruleInstruction: TransactionInstruction; + newAuthenticator?: { + passkeyPubkey: number[]; + credentialIdBase64: string; + }; // optional + }): Promise { + const authenticatorDataRaw = Buffer.from(params.authenticatorDataRaw64, 'base64'); + const clientDataJsonRaw = Buffer.from(params.clientDataJsonRaw64, 'base64'); + + const verifyIx = buildSecp256r1VerifyIx( + Buffer.concat([authenticatorDataRaw, Buffer.from(sha256.hex(clientDataJsonRaw), 'hex')]), + Buffer.from(params.passkeyPubkey), + Buffer.from(params.signature64) + ); + + const ix = await this.buildCallRuleDirectIx( + params.payer, + params.smartWallet, + { + passkeyPubkey: params.passkeyPubkey, + signature: Buffer.from(params.signature64, 'base64'), + clientDataJsonRaw: clientDataJsonRaw, + authenticatorDataRaw: authenticatorDataRaw, + newAuthenticator: params.newAuthenticator + ? { + passkeyPubkey: Array.from(params.newAuthenticator.passkeyPubkey), + credentialId: Buffer.from(params.newAuthenticator.credentialIdBase64, 'base64'), + } + : null, + ruleData: params.ruleInstruction.data, + verifyInstructionIndex: + (params.newAuthenticator ? 1 : 0) + params.ruleInstruction.keys.length, + }, + params.ruleInstruction + ); + return this.buildV0Tx(params.payer, [verifyIx, ix]); + } + + async changeRuleDirectTx(params: { + payer: PublicKey; + smartWallet: PublicKey; + passkeyPubkey: number[]; + signature64: String; + clientDataJsonRaw64: String; + authenticatorDataRaw64: String; + destroyRuleInstruction: TransactionInstruction; + initRuleInstruction: TransactionInstruction; + newAuthenticator?: { + passkeyPubkey: number[]; + credentialIdBase64: string; + }; // optional + }): Promise { + const authenticatorDataRaw = Buffer.from(params.authenticatorDataRaw64, 'base64'); + const clientDataJsonRaw = Buffer.from(params.clientDataJsonRaw64, 'base64'); + + const verifyIx = buildSecp256r1VerifyIx( + Buffer.concat([authenticatorDataRaw, Buffer.from(sha256.hex(clientDataJsonRaw), 'hex')]), + Buffer.from(params.passkeyPubkey), + Buffer.from(params.signature64) + ); + + const ix = await this.buildChangeRuleDirectIx( + params.payer, + params.smartWallet, + { + passkeyPubkey: params.passkeyPubkey, + signature: Buffer.from(params.signature64, 'base64'), + clientDataJsonRaw, + authenticatorDataRaw, + verifyInstructionIndex: 0, + destroyRuleData: params.destroyRuleInstruction.data, + initRuleData: params.initRuleInstruction.data, + splitIndex: (params.newAuthenticator ? 1 : 0) + params.destroyRuleInstruction.keys.length, + newAuthenticator: params.newAuthenticator + ? { + passkeyPubkey: Array.from(params.newAuthenticator.passkeyPubkey), + credentialId: Buffer.from(params.newAuthenticator.credentialIdBase64, 'base64'), + } + : null, + }, + params.destroyRuleInstruction, + params.initRuleInstruction + ); + return this.buildV0Tx(params.payer, [verifyIx, ix]); + } + + async commitCpiTx(params: { + payer: PublicKey; + smartWallet: PublicKey; + passkeyPubkey: number[]; + signature64: String; + clientDataJsonRaw64: String; + authenticatorDataRaw64: String; + ruleInstruction?: TransactionInstruction; + expiresAt: number; + }) { + const authenticatorDataRaw = Buffer.from(params.authenticatorDataRaw64, 'base64'); + const clientDataJsonRaw = Buffer.from(params.clientDataJsonRaw64, 'base64'); + + const verifyIx = buildSecp256r1VerifyIx( + Buffer.concat([authenticatorDataRaw, Buffer.from(sha256.hex(clientDataJsonRaw), 'hex')]), + Buffer.from(params.passkeyPubkey), + Buffer.from(params.signature64) + ); + + let ruleInstruction = await this.defaultRuleProgram.buildCheckRuleIx( + this.smartWalletAuthenticatorPda(params.smartWallet, params.passkeyPubkey) + ); + if (params.ruleInstruction) { + ruleInstruction = ruleInstruction; + } + + const ix = await this.buildCommitCpiIx( + params.payer, + params.smartWallet, + { + passkeyPubkey: params.passkeyPubkey, + signature: Buffer.from(params.signature64, 'base64'), + clientDataJsonRaw: clientDataJsonRaw, + authenticatorDataRaw: authenticatorDataRaw, + expiresAt: new BN(params.expiresAt), + ruleData: ruleInstruction.data, + verifyInstructionIndex: 0, + }, + ruleInstruction + ); + return this.buildV0Tx(params.payer, [verifyIx, ix]); + } + + async executeCommitedTx(params: { + payer: PublicKey; + smartWallet: PublicKey; + cpiInstruction: TransactionInstruction; + }): Promise { + const ix = await this.buildExecuteCommittedIx( + params.payer, + params.smartWallet, + params.cpiInstruction + ); + + return this.buildV0Tx(params.payer, [ix]); + } + + // Convenience: VersionedTransaction v0 + async buildV0Tx(payer: PublicKey, ixs: TransactionInstruction[]): Promise { + const { blockhash } = await this.connection.getLatestBlockhash(); + const msg = new TransactionMessage({ + payerKey: payer, + recentBlockhash: blockhash, + instructions: ixs, + }).compileToV0Message(); + return new VersionedTransaction(msg); + } + + // Legacy-compat APIs for simpler DX + async initializeTxn(payer: PublicKey) { + const ix = await this.buildInitializeIx(payer); + return new Transaction().add(ix); + } + + async createSmartWalletTx(params: { + payer: PublicKey; + passkeyPubkey: number[]; + credentialIdBase64: string; + ruleInstruction?: TransactionInstruction | null; + isPayForUser?: boolean; + smartWalletId?: BN; + }) { + let smartWalletId: BN = this.generateWalletId(); + if (params.smartWalletId) { + smartWalletId = params.smartWalletId; + } + const smartWallet = this.smartWalletPda(smartWalletId); + const smartWalletAuthenticator = this.smartWalletAuthenticatorPda( + smartWallet, + params.passkeyPubkey + ); + + let ruleInstruction = await this.defaultRuleProgram.buildInitRuleIx( + params.payer, + smartWallet, + smartWalletAuthenticator + ); + + if (params.ruleInstruction) { + ruleInstruction = params.ruleInstruction; + } + + const args = { + passkeyPubkey: params.passkeyPubkey, + credentialId: Buffer.from(params.credentialIdBase64, 'base64'), + ruleData: ruleInstruction.data, + walletId: smartWalletId, + isPayForUser: params.isPayForUser === true, + }; + + const ix = await this.buildCreateSmartWalletIx( + params.payer, + smartWallet, + smartWalletAuthenticator, + ruleInstruction, + args + ); + const tx = new Transaction().add(ix); + tx.feePayer = params.payer; + tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash; + return { + transaction: tx, + smartWalletId: smartWalletId, + smartWallet, + }; + } + + async buildMessage(params: { + action: types.MessageArgs; + payer: PublicKey; + smartWallet: PublicKey; + passkeyPubkey: number[]; + }): Promise> { + let message: Buffer; + + const { action, payer, smartWallet, passkeyPubkey } = params; + + switch (action.type) { + case types.SmartWalletAction.ExecuteTx: { + const { ruleInstruction: ruleIns, cpiInstruction } = + action.args as types.ArgsByAction[types.SmartWalletAction.ExecuteTx]; + + let ruleInstruction = await this.defaultRuleProgram.buildCheckRuleIx( + this.smartWalletAuthenticatorPda(smartWallet, passkeyPubkey) + ); + + if (ruleIns) { + ruleInstruction = ruleIns; + } + + const smartWalletConfigData = await this.getSmartWalletConfigData(smartWallet); + + message = buildExecuteMessage( + payer, + smartWalletConfigData.lastNonce, + new BN(Math.floor(Date.now() / 1000)), + ruleInstruction, + cpiInstruction + ); + break; + } + case types.SmartWalletAction.CallRule: { + const { ruleInstruction } = + action.args as types.ArgsByAction[types.SmartWalletAction.CallRule]; + + const smartWalletConfigData = await this.getSmartWalletConfigData(smartWallet); + + message = buildCallRuleMessage( + payer, + smartWalletConfigData.lastNonce, + new BN(Math.floor(Date.now() / 1000)), + ruleInstruction + ); + break; + } + case types.SmartWalletAction.ChangeRule: { + const { initRuleIns, destroyRuleIns } = + action.args as types.ArgsByAction[types.SmartWalletAction.ChangeRule]; + + const smartWalletConfigData = await this.getSmartWalletConfigData(smartWallet); + + message = buildChangeRuleMessage( + payer, + smartWalletConfigData.lastNonce, + new BN(Math.floor(Date.now() / 1000)), + destroyRuleIns, + initRuleIns + ); + break; + } + + default: + throw new Error(`Unsupported SmartWalletAction: ${action.type}`); + } + + return message; + } +} diff --git a/sdk/constants.ts b/contract-integration/constants.ts similarity index 77% rename from sdk/constants.ts rename to contract-integration/constants.ts index 6ac1eec..690c7b7 100644 --- a/sdk/constants.ts +++ b/contract-integration/constants.ts @@ -3,14 +3,11 @@ import * as anchor from '@coral-xyz/anchor'; // LAZOR.KIT PROGRAM - PDA Seeds export const SMART_WALLET_SEED = Buffer.from('smart_wallet'); export const SMART_WALLET_CONFIG_SEED = Buffer.from('smart_wallet_config'); -export const SMART_WALLET_AUTHENTICATOR_SEED = Buffer.from( - 'smart_wallet_authenticator' -); -export const WHITELIST_RULE_PROGRAMS_SEED = Buffer.from( - 'whitelist_rule_programs' -); +export const SMART_WALLET_AUTHENTICATOR_SEED = Buffer.from('smart_wallet_authenticator'); +export const WHITELIST_RULE_PROGRAMS_SEED = Buffer.from('whitelist_rule_programs'); export const CONFIG_SEED = Buffer.from('config'); export const AUTHORITY_SEED = Buffer.from('authority'); +export const CPI_COMMIT_SEED = Buffer.from('cpi_commit'); // RULE PROGRAM SEEDS export const RULE_DATA_SEED = Buffer.from('rule_data'); diff --git a/contract-integration/index.ts b/contract-integration/index.ts new file mode 100644 index 0000000..88e4ddb --- /dev/null +++ b/contract-integration/index.ts @@ -0,0 +1,9 @@ +// Polyfill for structuredClone if not available (for React Native/Expo) +if (typeof globalThis.structuredClone !== 'function') { + globalThis.structuredClone = (obj: any) => JSON.parse(JSON.stringify(obj)); +} + +// Main SDK exports +export { LazorkitClient } from './client/lazorkit'; +export { DefaultRuleClient } from './client/defaultRule'; +export * from './types'; diff --git a/contract-integration/messages.ts b/contract-integration/messages.ts new file mode 100644 index 0000000..d387b18 --- /dev/null +++ b/contract-integration/messages.ts @@ -0,0 +1,139 @@ +import * as anchor from '@coral-xyz/anchor'; +import { sha256 } from 'js-sha256'; +import { instructionToAccountMetas } from './utils'; + +const coder: anchor.BorshCoder = (() => { + const idl: any = { + version: '0.1.0', + name: 'lazorkit_msgs', + instructions: [], + accounts: [], + types: [ + { + name: 'ExecuteMessage', + type: { + kind: 'struct', + fields: [ + { name: 'nonce', type: 'u64' }, + { name: 'currentTimestamp', type: 'i64' }, + { name: 'ruleDataHash', type: { array: ['u8', 32] } }, + { name: 'ruleAccountsHash', type: { array: ['u8', 32] } }, + { name: 'cpiDataHash', type: { array: ['u8', 32] } }, + { name: 'cpiAccountsHash', type: { array: ['u8', 32] } }, + ], + }, + }, + { + name: 'CallRuleMessage', + type: { + kind: 'struct', + fields: [ + { name: 'nonce', type: 'u64' }, + { name: 'currentTimestamp', type: 'i64' }, + { name: 'ruleDataHash', type: { array: ['u8', 32] } }, + { name: 'ruleAccountsHash', type: { array: ['u8', 32] } }, + ], + }, + }, + { + name: 'ChangeRuleMessage', + type: { + kind: 'struct', + fields: [ + { name: 'nonce', type: 'u64' }, + { name: 'currentTimestamp', type: 'i64' }, + { name: 'oldRuleDataHash', type: { array: ['u8', 32] } }, + { name: 'oldRuleAccountsHash', type: { array: ['u8', 32] } }, + { name: 'newRuleDataHash', type: { array: ['u8', 32] } }, + { name: 'newRuleAccountsHash', type: { array: ['u8', 32] } }, + ], + }, + }, + ], + }; + return new anchor.BorshCoder(idl); +})(); + +function computeAccountsHash( + programId: anchor.web3.PublicKey, + metas: anchor.web3.AccountMeta[] +): Uint8Array { + const h = sha256.create(); + h.update(programId.toBytes()); + for (const m of metas) { + h.update(m.pubkey.toBytes()); + h.update(Uint8Array.from([m.isWritable ? 1 : 0, m.isSigner ? 1 : 0])); + } + return new Uint8Array(h.arrayBuffer()); +} + +export function buildExecuteMessage( + payer: anchor.web3.PublicKey, + nonce: anchor.BN, + now: anchor.BN, + ruleIns: anchor.web3.TransactionInstruction, + cpiIns: anchor.web3.TransactionInstruction +): Buffer { + const ruleMetas = instructionToAccountMetas(ruleIns, payer); + const ruleAccountsHash = computeAccountsHash(ruleIns.programId, ruleMetas); + const ruleDataHash = new Uint8Array(sha256.arrayBuffer(ruleIns.data)); + + const cpiMetas = instructionToAccountMetas(cpiIns, payer); + const cpiAccountsHash = computeAccountsHash(cpiIns.programId, cpiMetas); + const cpiDataHash = new Uint8Array(sha256.arrayBuffer(cpiIns.data)); + + const encoded = coder.types.encode('ExecuteMessage', { + nonce, + currentTimestamp: now, + ruleDataHash: Array.from(ruleDataHash), + ruleAccountsHash: Array.from(ruleAccountsHash), + cpiDataHash: Array.from(cpiDataHash), + cpiAccountsHash: Array.from(cpiAccountsHash), + }); + return Buffer.from(encoded); +} + +export function buildCallRuleMessage( + payer: anchor.web3.PublicKey, + nonce: anchor.BN, + now: anchor.BN, + ruleIns: anchor.web3.TransactionInstruction +): Buffer { + const ruleMetas = instructionToAccountMetas(ruleIns, payer); + const ruleAccountsHash = computeAccountsHash(ruleIns.programId, ruleMetas); + const ruleDataHash = new Uint8Array(sha256.arrayBuffer(ruleIns.data)); + + const encoded = coder.types.encode('CallRuleMessage', { + nonce, + currentTimestamp: now, + ruleDataHash: Array.from(ruleDataHash), + ruleAccountsHash: Array.from(ruleAccountsHash), + }); + return Buffer.from(encoded); +} + +export function buildChangeRuleMessage( + payer: anchor.web3.PublicKey, + nonce: anchor.BN, + now: anchor.BN, + destroyRuleIns: anchor.web3.TransactionInstruction, + initRuleIns: anchor.web3.TransactionInstruction +): Buffer { + const oldMetas = instructionToAccountMetas(destroyRuleIns, payer); + const oldAccountsHash = computeAccountsHash(destroyRuleIns.programId, oldMetas); + const oldDataHash = new Uint8Array(sha256.arrayBuffer(destroyRuleIns.data)); + + const newMetas = instructionToAccountMetas(initRuleIns, payer); + const newAccountsHash = computeAccountsHash(initRuleIns.programId, newMetas); + const newDataHash = new Uint8Array(sha256.arrayBuffer(initRuleIns.data)); + + const encoded = coder.types.encode('ChangeRuleMessage', { + nonce, + currentTimestamp: now, + oldRuleDataHash: Array.from(oldDataHash), + oldRuleAccountsHash: Array.from(oldAccountsHash), + newRuleDataHash: Array.from(newDataHash), + newRuleAccountsHash: Array.from(newAccountsHash), + }); + return Buffer.from(encoded); +} diff --git a/contract-integration/pda/defaultRule.ts b/contract-integration/pda/defaultRule.ts new file mode 100644 index 0000000..4b10442 --- /dev/null +++ b/contract-integration/pda/defaultRule.ts @@ -0,0 +1,14 @@ +import { PublicKey } from '@solana/web3.js'; +import { Buffer } from 'buffer'; + +export const RULE_SEED = Buffer.from('rule'); + +export function deriveRulePda( + programId: PublicKey, + smartWalletAuthenticator: PublicKey +): PublicKey { + return PublicKey.findProgramAddressSync( + [RULE_SEED, smartWalletAuthenticator.toBuffer()], + programId + )[0]; +} diff --git a/contract-integration/pda/lazorkit.ts b/contract-integration/pda/lazorkit.ts new file mode 100644 index 0000000..5ca35a0 --- /dev/null +++ b/contract-integration/pda/lazorkit.ts @@ -0,0 +1,66 @@ +import { PublicKey } from '@solana/web3.js'; +import { BN } from '@coral-xyz/anchor'; +// Mirror on-chain seeds +export const CONFIG_SEED = Buffer.from('config'); +export const WHITELIST_RULE_PROGRAMS_SEED = Buffer.from('whitelist_rule_programs'); +export const SMART_WALLET_SEED = Buffer.from('smart_wallet'); +export const SMART_WALLET_CONFIG_SEED = Buffer.from('smart_wallet_config'); +export const SMART_WALLET_AUTHENTICATOR_SEED = Buffer.from('smart_wallet_authenticator'); +export const CPI_COMMIT_SEED = Buffer.from('cpi_commit'); + +export function deriveConfigPda(programId: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync([CONFIG_SEED], programId)[0]; +} + +export function deriveWhitelistRuleProgramsPda(programId: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync([WHITELIST_RULE_PROGRAMS_SEED], programId)[0]; +} + +export function deriveSmartWalletPda(programId: PublicKey, walletId: BN): PublicKey { + return PublicKey.findProgramAddressSync( + [SMART_WALLET_SEED, walletId.toArrayLike(Buffer, 'le', 8)], + programId + )[0]; +} + +export function deriveSmartWalletConfigPda( + programId: PublicKey, + smartWallet: PublicKey +): PublicKey { + return PublicKey.findProgramAddressSync( + [SMART_WALLET_CONFIG_SEED, smartWallet.toBuffer()], + programId + )[0]; +} + +// Must match on-chain: sha256(passkey(33) || wallet(32)) +export function hashPasskeyWithWallet(passkeyCompressed33: number[], wallet: PublicKey): Buffer { + const { sha256 } = require('js-sha256'); + const buf = Buffer.alloc(65); + Buffer.from(passkeyCompressed33).copy(buf, 0); + wallet.toBuffer().copy(buf, 33); + return Buffer.from(sha256.arrayBuffer(buf)).subarray(0, 32); +} + +export function deriveSmartWalletAuthenticatorPda( + programId: PublicKey, + smartWallet: PublicKey, + passkeyCompressed33: number[] +): [PublicKey, number] { + const hashed = hashPasskeyWithWallet(passkeyCompressed33, smartWallet); + return PublicKey.findProgramAddressSync( + [SMART_WALLET_AUTHENTICATOR_SEED, smartWallet.toBuffer(), hashed], + programId + ); +} + +export function deriveCpiCommitPda( + programId: PublicKey, + smartWallet: PublicKey, + lastNonce: BN +): PublicKey { + return PublicKey.findProgramAddressSync( + [CPI_COMMIT_SEED, smartWallet.toBuffer(), lastNonce.toArrayLike(Buffer, 'le', 8)], + programId + )[0]; +} diff --git a/contract-integration/types.ts b/contract-integration/types.ts new file mode 100644 index 0000000..82a49a1 --- /dev/null +++ b/contract-integration/types.ts @@ -0,0 +1,46 @@ +import * as anchor from '@coral-xyz/anchor'; +import { Lazorkit } from './anchor/types/lazorkit'; + +// Account types +export type SmartWalletConfig = anchor.IdlTypes['smartWalletConfig']; +export type SmartWalletAuthenticator = anchor.IdlTypes['smartWalletAuthenticator']; +export type Config = anchor.IdlTypes['config']; +export type WhitelistRulePrograms = anchor.IdlTypes['whitelistRulePrograms']; + +// argument type +export type CreatwSmartWalletArgs = anchor.IdlTypes['creatwSmartWalletArgs']; +export type ExecuteTxnArgs = anchor.IdlTypes['executeTxnArgs']; +export type ChangeRuleArgs = anchor.IdlTypes['changeRuleArgs']; +export type CallRuleArgs = anchor.IdlTypes['callRuleArgs']; +export type CommitArgs = anchor.IdlTypes['commitArgs']; +export type NewAuthenticatorArgs = anchor.IdlTypes['newAuthenticatorArgs']; + +// Enum types +export type UpdateConfigType = anchor.IdlTypes['updateConfigType']; + +export enum SmartWalletAction { + ChangeRule, + CallRule, + ExecuteTx, +} + +export type ArgsByAction = { + [SmartWalletAction.ExecuteTx]: { + ruleInstruction?: anchor.web3.TransactionInstruction; + cpiInstruction: anchor.web3.TransactionInstruction; + }; + [SmartWalletAction.CallRule]: { + ruleInstruction: anchor.web3.TransactionInstruction; + newPasskey: number[]; + }; + [SmartWalletAction.ChangeRule]: { + destroyRuleIns: anchor.web3.TransactionInstruction; + initRuleIns: anchor.web3.TransactionInstruction; + newPasskey: number[]; + }; +}; + +export type MessageArgs = { + type: K; + args: ArgsByAction[K]; +}; diff --git a/contract-integration/utils.ts b/contract-integration/utils.ts new file mode 100644 index 0000000..dcac4bd --- /dev/null +++ b/contract-integration/utils.ts @@ -0,0 +1,12 @@ +import * as anchor from '@coral-xyz/anchor'; + +export function instructionToAccountMetas( + ix: anchor.web3.TransactionInstruction, + payer: anchor.web3.PublicKey +): anchor.web3.AccountMeta[] { + return ix.keys.map((k) => ({ + pubkey: k.pubkey, + isWritable: k.isWritable, + isSigner: k.pubkey.equals(payer), + })); +} diff --git a/contract-integration/webauthn/secp256r1.ts b/contract-integration/webauthn/secp256r1.ts new file mode 100644 index 0000000..e8b68f3 --- /dev/null +++ b/contract-integration/webauthn/secp256r1.ts @@ -0,0 +1,112 @@ +import * as anchor from '@coral-xyz/anchor'; +import { PublicKey, TransactionInstruction } from '@solana/web3.js'; + +const SIGNATURE_OFFSETS_SERIALIZED_SIZE = 14; +const SIGNATURE_OFFSETS_START = 2; +const DATA_START = SIGNATURE_OFFSETS_SERIALIZED_SIZE + SIGNATURE_OFFSETS_START; +const SIGNATURE_SERIALIZED_SIZE = 64; +const COMPRESSED_PUBKEY_SERIALIZED_SIZE = 33; +const FIELD_SIZE = 32; + +export const SECP256R1_PROGRAM_ID = new PublicKey('Secp256r1SigVerify1111111111111111111111111'); + +const ORDER = new Uint8Array([ + 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xbc, 0xe6, 0xfa, 0xad, 0xa7, 0x17, 0x9e, 0x84, 0xf3, 0xb9, 0xca, 0xc2, 0xfc, 0x63, 0x25, 0x51, +]); +const HALF_ORDER = new Uint8Array([ + 0x7f, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xde, 0x73, 0x7d, 0x56, 0xd3, 0x8b, 0xcf, 0x42, 0x79, 0xdc, 0xe5, 0x61, 0x7e, 0x31, 0x92, 0xa8, +]); + +function isGreaterThan(a: Uint8Array, b: Uint8Array): boolean { + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return a[i] > b[i]; + } + return false; +} + +function sub(a: Uint8Array, b: Uint8Array): Uint8Array { + const out = new Uint8Array(a.length); + let borrow = 0; + for (let i = a.length - 1; i >= 0; i--) { + let d = a[i] - b[i] - borrow; + if (d < 0) { + d += 256; + borrow = 1; + } else { + borrow = 0; + } + out[i] = d; + } + return out; +} + +function bytesOf(obj: any): Uint8Array { + if (obj instanceof Uint8Array) return obj; + if (Array.isArray(obj)) return new Uint8Array(obj); + const keys = Object.keys(obj); + const buf = new ArrayBuffer(keys.length * 2); + const view = new DataView(buf); + keys.forEach((k, i) => view.setUint16(i * 2, obj[k] as number, true)); + return new Uint8Array(buf); +} + +export function buildSecp256r1VerifyIx( + message: Uint8Array, + compressedPubkey33: Uint8Array, + signature: Uint8Array +): TransactionInstruction { + let sig = Buffer.from(signature); + if (sig.length !== SIGNATURE_SERIALIZED_SIZE) { + const r = sig.subarray(0, FIELD_SIZE); + const s = sig.subarray(FIELD_SIZE, FIELD_SIZE * 2); + const R = Buffer.alloc(FIELD_SIZE); + const S = Buffer.alloc(FIELD_SIZE); + r.copy(R, FIELD_SIZE - r.length); + s.copy(S, FIELD_SIZE - s.length); + if (isGreaterThan(S, HALF_ORDER)) { + const newS = sub(ORDER, S); + sig = Buffer.concat([R, Buffer.from(newS)]); + } else { + sig = Buffer.concat([R, S]); + } + } + + if ( + compressedPubkey33.length !== COMPRESSED_PUBKEY_SERIALIZED_SIZE || + sig.length !== SIGNATURE_SERIALIZED_SIZE + ) { + throw new Error('Invalid secp256r1 key/signature length'); + } + + const totalSize = + DATA_START + SIGNATURE_SERIALIZED_SIZE + COMPRESSED_PUBKEY_SERIALIZED_SIZE + message.length; + const data = new Uint8Array(totalSize); + + const numSignatures = 1; + const publicKeyOffset = DATA_START; + const signatureOffset = publicKeyOffset + COMPRESSED_PUBKEY_SERIALIZED_SIZE; + const messageDataOffset = signatureOffset + SIGNATURE_SERIALIZED_SIZE; + + data.set(bytesOf([numSignatures, 0]), 0); + const offsets = { + signature_offset: signatureOffset, + signature_instruction_index: 0xffff, + public_key_offset: publicKeyOffset, + public_key_instruction_index: 0xffff, + message_data_offset: messageDataOffset, + message_data_size: message.length, + message_instruction_index: 0xffff, + } as const; + data.set(bytesOf(offsets), SIGNATURE_OFFSETS_START); + data.set(compressedPubkey33, publicKeyOffset); + data.set(sig, signatureOffset); + data.set(message, messageDataOffset); + + return new anchor.web3.TransactionInstruction({ + programId: SECP256R1_PROGRAM_ID, + keys: [], + data: Buffer.from(data), + }); +} diff --git a/package.json b/package.json index 2f92bed..bb4cdf4 100644 --- a/package.json +++ b/package.json @@ -8,13 +8,14 @@ "@coral-xyz/anchor": "^0.31.0", "@solana/spl-token": "^0.4.13", "@solana/web3.js": "^1.98.2", + "crypto": "^1.0.1", "dotenv": "^16.5.0", "ecdsa-secp256r1": "^1.3.3", "js-sha256": "^0.11.0" }, "devDependencies": { - "@types/bs58": "^5.0.0", "@types/bn.js": "^5.1.0", + "@types/bs58": "^5.0.0", "@types/chai": "^4.3.0", "@types/mocha": "^9.0.0", "chai": "^4.3.4", diff --git a/programs/lazorkit/src/error.rs b/programs/lazorkit/src/error.rs index 6ff747b..c813509 100644 --- a/programs/lazorkit/src/error.rs +++ b/programs/lazorkit/src/error.rs @@ -168,10 +168,6 @@ pub enum LazorKitError { InvalidMessageFormat, #[msg("Message size exceeds limit")] MessageSizeExceedsLimit, - #[msg("Invalid action type")] - InvalidActionType, - #[msg("Action not supported")] - ActionNotSupported, #[msg("Invalid split index")] InvalidSplitIndex, #[msg("CPI execution failed")] diff --git a/programs/lazorkit/src/events.rs b/programs/lazorkit/src/events.rs index 46ded09..8e2f386 100644 --- a/programs/lazorkit/src/events.rs +++ b/programs/lazorkit/src/events.rs @@ -1,5 +1,7 @@ use anchor_lang::prelude::*; +use crate::constants::PASSKEY_SIZE; + /// Event emitted when a new smart wallet is created #[event] pub struct SmartWalletCreated { @@ -16,7 +18,6 @@ pub struct SmartWalletCreated { pub struct TransactionExecuted { pub smart_wallet: Pubkey, pub authenticator: Pubkey, - pub action: String, pub nonce: u64, pub rule_program: Pubkey, pub cpi_program: Pubkey, @@ -126,7 +127,7 @@ impl SmartWalletCreated { authenticator: Pubkey, sequence_id: u64, rule_program: Pubkey, - passkey_pubkey: [u8; 33], + passkey_pubkey: [u8; PASSKEY_SIZE], ) -> Result<()> { let mut passkey_hash = [0u8; 32]; passkey_hash.copy_from_slice(&anchor_lang::solana_program::hash::hash(&passkey_pubkey).to_bytes()[..32]); @@ -147,7 +148,6 @@ impl TransactionExecuted { pub fn emit_event( smart_wallet: Pubkey, authenticator: Pubkey, - action: &str, nonce: u64, rule_program: Pubkey, cpi_program: Pubkey, @@ -156,7 +156,6 @@ impl TransactionExecuted { emit!(Self { smart_wallet, authenticator, - action: action.to_string(), nonce, rule_program, cpi_program, diff --git a/programs/lazorkit/src/instructions/args.rs b/programs/lazorkit/src/instructions/args.rs new file mode 100644 index 0000000..e36fedf --- /dev/null +++ b/programs/lazorkit/src/instructions/args.rs @@ -0,0 +1,137 @@ +use crate::{constants::PASSKEY_SIZE, error::LazorKitError}; +use anchor_lang::prelude::*; + +pub trait Args { + fn validate(&self) -> Result<()>; +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct CreatwSmartWalletArgs { + pub passkey_pubkey: [u8; PASSKEY_SIZE], + pub credential_id: Vec, + pub rule_data: Vec, + pub wallet_id: u64, // Random ID provided by client, + pub is_pay_for_user: bool, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct ExecuteTxnArgs { + pub passkey_pubkey: [u8; PASSKEY_SIZE], + pub signature: Vec, + pub client_data_json_raw: Vec, + pub authenticator_data_raw: Vec, + pub verify_instruction_index: u8, + pub split_index: u16, + pub rule_data: Vec, + pub cpi_data: Vec, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct ChangeRuleArgs { + pub passkey_pubkey: [u8; PASSKEY_SIZE], + pub signature: Vec, + pub client_data_json_raw: Vec, + pub authenticator_data_raw: Vec, + pub verify_instruction_index: u8, + pub split_index: u16, + pub destroy_rule_data: Vec, + pub init_rule_data: Vec, + pub new_authenticator: Option, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct CallRuleArgs { + pub passkey_pubkey: [u8; PASSKEY_SIZE], + pub signature: Vec, + pub client_data_json_raw: Vec, + pub authenticator_data_raw: Vec, + pub verify_instruction_index: u8, + pub rule_data: Vec, + pub new_authenticator: Option, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct CommitArgs { + pub passkey_pubkey: [u8; PASSKEY_SIZE], + pub signature: Vec, + pub client_data_json_raw: Vec, + pub authenticator_data_raw: Vec, + pub verify_instruction_index: u8, + pub rule_data: Vec, + pub expires_at: i64, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, InitSpace)] +pub struct NewAuthenticatorArgs { + pub passkey_pubkey: [u8; PASSKEY_SIZE], + #[max_len(256)] + pub credential_id: Vec, +} + +macro_rules! impl_args_validate { + ($t:ty) => { + impl Args for $t { + fn validate(&self) -> Result<()> { + // Validate passkey format + require!( + self.passkey_pubkey[0] == 0x02 || self.passkey_pubkey[0] == 0x03, + LazorKitError::InvalidPasskeyFormat + ); + + // Validate signature length (Secp256r1 signature should be 64 bytes) + require!(self.signature.len() == 64, LazorKitError::InvalidSignature); + + // Validate client data and authenticator data are not empty + require!( + !self.client_data_json_raw.is_empty(), + LazorKitError::InvalidInstructionData + ); + require!( + !self.authenticator_data_raw.is_empty(), + LazorKitError::InvalidInstructionData + ); + + // Validate verify instruction index + require!( + self.verify_instruction_index < 255, + LazorKitError::InvalidInstructionData + ); + + Ok(()) + } + } + }; +} + +impl Args for CommitArgs { + fn validate(&self) -> Result<()> { + // Common passkey/signature/client/auth checks + require!( + self.passkey_pubkey[0] == 0x02 || self.passkey_pubkey[0] == 0x03, + LazorKitError::InvalidPasskeyFormat + ); + require!(self.signature.len() == 64, LazorKitError::InvalidSignature); + require!( + !self.client_data_json_raw.is_empty(), + LazorKitError::InvalidInstructionData + ); + require!( + !self.authenticator_data_raw.is_empty(), + LazorKitError::InvalidInstructionData + ); + require!( + self.verify_instruction_index < 255, + LazorKitError::InvalidInstructionData + ); + // Split index bounds check left to runtime with account len; ensure rule_data present + require!( + !self.rule_data.is_empty(), + LazorKitError::InvalidInstructionData + ); + Ok(()) + } +} + +impl_args_validate!(ExecuteTxnArgs); +impl_args_validate!(ChangeRuleArgs); +impl_args_validate!(CallRuleArgs); diff --git a/programs/lazorkit/src/instructions/create_smart_wallet.rs b/programs/lazorkit/src/instructions/create_smart_wallet.rs index 71f1585..c7e6b8a 100644 --- a/programs/lazorkit/src/instructions/create_smart_wallet.rs +++ b/programs/lazorkit/src/instructions/create_smart_wallet.rs @@ -1,9 +1,10 @@ use anchor_lang::prelude::*; use crate::{ - constants::{PASSKEY_SIZE, SMART_WALLET_SEED}, + constants::SMART_WALLET_SEED, error::LazorKitError, events::{FeeCollected, SmartWalletCreated}, + instructions::CreatwSmartWalletArgs, security::validation, state::{Config, SmartWalletAuthenticator, SmartWalletConfig, WhitelistRulePrograms}, utils::{execute_cpi, transfer_sol_from_pda, PasskeyExt, PdaSigner}, @@ -12,28 +13,29 @@ use crate::{ pub fn create_smart_wallet( ctx: Context, - passkey_pubkey: [u8; PASSKEY_SIZE], - credential_id: Vec, - rule_data: Vec, - wallet_id: u64, // Random ID provided by client, - is_pay_for_user: bool, + args: CreatwSmartWalletArgs, ) -> Result<()> { + // Program must not be paused + require!(!ctx.accounts.config.is_paused, LazorKitError::ProgramPaused); // === Input Validation === - validation::validate_credential_id(&credential_id)?; - validation::validate_rule_data(&rule_data)?; + validation::validate_credential_id(&args.credential_id)?; + validation::validate_rule_data(&args.rule_data)?; validation::validate_remaining_accounts(&ctx.remaining_accounts)?; // Validate passkey format (ensure it's a valid compressed public key) require!( - passkey_pubkey[0] == 0x02 || passkey_pubkey[0] == 0x03, + args.passkey_pubkey[0] == 0x02 || args.passkey_pubkey[0] == 0x03, LazorKitError::InvalidPasskeyFormat ); // Validate wallet ID is not zero (reserved) - require!(wallet_id != 0, LazorKitError::InvalidSequenceNumber); + require!(args.wallet_id != 0, LazorKitError::InvalidSequenceNumber); // Additional validation: ensure wallet ID is within reasonable bounds - require!(wallet_id < u64::MAX, LazorKitError::InvalidSequenceNumber); + require!( + args.wallet_id < u64::MAX, + LazorKitError::InvalidSequenceNumber + ); // === Configuration === let wallet_data = &mut ctx.accounts.smart_wallet_config; @@ -45,16 +47,16 @@ pub fn create_smart_wallet( // === Initialize Smart Wallet Config === wallet_data.set_inner(SmartWalletConfig { rule_program: ctx.accounts.config.default_rule_program, - id: wallet_id, + id: args.wallet_id, last_nonce: 0, bump: ctx.bumps.smart_wallet, }); // === Initialize Smart Wallet Authenticator === smart_wallet_authenticator.set_inner(SmartWalletAuthenticator { - passkey_pubkey, + passkey_pubkey: args.passkey_pubkey, smart_wallet: ctx.accounts.smart_wallet.key(), - credential_id: credential_id.clone(), + credential_id: args.credential_id.clone(), bump: ctx.bumps.smart_wallet_authenticator, }); @@ -63,7 +65,7 @@ pub fn create_smart_wallet( seeds: vec![ SmartWalletAuthenticator::PREFIX_SEED.to_vec(), ctx.accounts.smart_wallet.key().as_ref().to_vec(), - passkey_pubkey + args.passkey_pubkey .to_hashed_bytes(ctx.accounts.smart_wallet.key()) .as_ref() .to_vec(), @@ -74,12 +76,12 @@ pub fn create_smart_wallet( // === Execute Rule Program CPI === execute_cpi( &ctx.remaining_accounts, - &rule_data, + &args.rule_data, &ctx.accounts.default_rule_program, Some(signer), )?; - if !is_pay_for_user { + if !args.is_pay_for_user { // === Collect Creation Fee === let fee = ctx.accounts.config.create_smart_wallet_fee; if fee > 0 { @@ -113,22 +115,22 @@ pub fn create_smart_wallet( "Authenticator: {}", ctx.accounts.smart_wallet_authenticator.key() ); - msg!("Wallet ID: {}", wallet_id); + msg!("Wallet ID: {}", args.wallet_id); // Emit wallet creation event SmartWalletCreated::emit_event( ctx.accounts.smart_wallet.key(), ctx.accounts.smart_wallet_authenticator.key(), - wallet_id, + args.wallet_id, ctx.accounts.config.default_rule_program, - passkey_pubkey, + args.passkey_pubkey, )?; Ok(()) } #[derive(Accounts)] -#[instruction(passkey_pubkey: [u8; PASSKEY_SIZE], credential_id: Vec, rule_data: Vec, wallet_id: u64)] +#[instruction(args: CreatwSmartWalletArgs,)] pub struct CreateSmartWallet<'info> { #[account(mut)] pub signer: Signer<'info>, @@ -147,7 +149,7 @@ pub struct CreateSmartWallet<'info> { init, payer = signer, space = 0, - seeds = [SMART_WALLET_SEED, wallet_id.to_le_bytes().as_ref()], + seeds = [SMART_WALLET_SEED, args.wallet_id.to_le_bytes().as_ref()], bump )] /// CHECK: This account is only used for its public key and seeds. @@ -171,7 +173,7 @@ pub struct CreateSmartWallet<'info> { seeds = [ SmartWalletAuthenticator::PREFIX_SEED, smart_wallet.key().as_ref(), - passkey_pubkey.to_hashed_bytes(smart_wallet.key()).as_ref() + args.passkey_pubkey.to_hashed_bytes(smart_wallet.key()).as_ref() ], bump )] diff --git a/programs/lazorkit/src/instructions/execute.rs b/programs/lazorkit/src/instructions/execute.rs deleted file mode 100644 index 647e96d..0000000 --- a/programs/lazorkit/src/instructions/execute.rs +++ /dev/null @@ -1,271 +0,0 @@ -//! Unified smart-wallet instruction dispatcher. -//! -//! External callers only need to invoke **one** instruction (`execute`) and -//! specify the desired `Action`. Internally we forward to specialised -//! handler functions located in `handlers/`. - -// ----------------------------------------------------------------------------- -// Imports -// ----------------------------------------------------------------------------- -use anchor_lang::prelude::*; -use anchor_lang::solana_program::sysvar::instructions::ID as IX_ID; - -use crate::security::validation; -use crate::state::{Config, SmartWalletAuthenticator, SmartWalletConfig, WhitelistRulePrograms}; -use crate::utils::{verify_authorization, PasskeyExt}; -use crate::{constants::SMART_WALLET_SEED, error::LazorKitError, ID}; - -use super::handlers::{call_rule, execute_tx, change_rule}; - -/// Supported wallet actions -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, PartialEq, Eq)] -pub enum Action { - ExecuteTx, - ChangeRuleProgram, - CallRuleProgram, -} - -/// Single args struct shared by all actions -#[derive(AnchorSerialize, AnchorDeserialize, Clone)] -pub struct ExecuteArgs { - pub passkey_pubkey: [u8; 33], - pub signature: Vec, - pub client_data_json_raw: Vec, - pub authenticator_data_raw: Vec, - pub verify_instruction_index: u8, - pub action: Action, - /// optional new authenticator passkey (only for `CallRuleProgram`) - pub create_new_authenticator: Option<[u8; 33]>, -} - -impl ExecuteArgs { - /// Validate execute arguments - pub fn validate(&self) -> Result<()> { - // Validate passkey format - require!( - self.passkey_pubkey[0] == 0x02 || self.passkey_pubkey[0] == 0x03, - LazorKitError::InvalidPasskeyFormat - ); - - // Validate signature length (Secp256r1 signature should be 64 bytes) - require!( - self.signature.len() == 64, - LazorKitError::InvalidSignature - ); - - // Validate client data and authenticator data are not empty - require!( - !self.client_data_json_raw.is_empty(), - LazorKitError::InvalidInstructionData - ); - require!( - !self.authenticator_data_raw.is_empty(), - LazorKitError::InvalidInstructionData - ); - - // Validate verify instruction index - require!( - self.verify_instruction_index < 255, - LazorKitError::InvalidInstructionData - ); - - // Validate new authenticator if provided - if let Some(new_auth) = self.create_new_authenticator { - require!( - new_auth[0] == 0x02 || new_auth[0] == 0x03, - LazorKitError::InvalidPasskeyFormat - ); - - // Only CallRuleProgram action can create new authenticator - require!( - self.action == Action::CallRuleProgram, - LazorKitError::InvalidActionType - ); - } - - Ok(()) - } -} - -/// Single entry-point for all smart-wallet interactions -pub fn execute<'c: 'info, 'info>( - mut ctx: Context<'_, '_, 'c, 'info, Execute<'info>>, - args: ExecuteArgs, -) -> Result<()> { - // ------------------------------------------------------------------ - // 1. Input Validation - // ------------------------------------------------------------------ - args.validate()?; - validation::validate_remaining_accounts(&ctx.remaining_accounts)?; - - // Check if program is paused (emergency shutdown) - require!( - !ctx.accounts.config.is_paused, - LazorKitError::ProgramPaused - ); - - // Validate smart wallet state - require!( - ctx.accounts.smart_wallet_config.id < u64::MAX, - LazorKitError::InvalidWalletConfiguration - ); - - // ------------------------------------------------------------------ - // 2. Authorization (shared) - // ------------------------------------------------------------------ - let msg = verify_authorization( - &ctx.accounts.ix_sysvar, - &ctx.accounts.smart_wallet_authenticator, - ctx.accounts.smart_wallet.key(), - args.passkey_pubkey, - args.signature.clone(), - &args.client_data_json_raw, - &args.authenticator_data_raw, - args.verify_instruction_index, - ctx.accounts.smart_wallet_config.last_nonce, - )?; - - // Additional validation on the message - if let Some(ref rule_data) = msg.rule_data { - validation::validate_rule_data(rule_data)?; - } - validation::validate_cpi_data(&msg.cpi_data)?; - - // Validate split index - let total_accounts = ctx.remaining_accounts.len(); - require!( - (msg.split_index as usize) <= total_accounts, - LazorKitError::InvalidSplitIndex - ); - - // ------------------------------------------------------------------ - // 3. Dispatch to specialised handler - // ------------------------------------------------------------------ - msg!("Executing action: {:?}", args.action); - msg!("Smart wallet: {}", ctx.accounts.smart_wallet.key()); - msg!("Nonce: {}", ctx.accounts.smart_wallet_config.last_nonce); - - match args.action { - Action::ExecuteTx => { - execute_tx::handle(&mut ctx, &args, &msg)?; - } - Action::ChangeRuleProgram => { - change_rule::handle(&mut ctx, &args, &msg)?; - } - Action::CallRuleProgram => { - call_rule::handle(&mut ctx, &args, &msg)?; - } - } - - // ------------------------------------------------------------------ - // 4. Post-execution updates - // ------------------------------------------------------------------ - - // Increment nonce with overflow protection - ctx.accounts.smart_wallet_config.last_nonce = ctx - .accounts - .smart_wallet_config - .last_nonce - .checked_add(1) - .ok_or(LazorKitError::NonceOverflow)?; - - // Collect execution fee if configured - let fee = ctx.accounts.config.execute_fee; - if fee > 0 { - // Check smart wallet has sufficient balance - let smart_wallet_balance = ctx.accounts.smart_wallet.lamports(); - let rent = Rent::get()?.minimum_balance(0); - - require!( - smart_wallet_balance >= fee + rent, - LazorKitError::InsufficientBalanceForFee - ); - - crate::utils::transfer_sol_from_pda( - &ctx.accounts.smart_wallet, - &ctx.accounts.payer, - fee, - )?; - } - - // Emit execution event - msg!("Action executed successfully"); - msg!("New nonce: {}", ctx.accounts.smart_wallet_config.last_nonce); - - Ok(()) -} - -// ----------------------------------------------------------------------------- -// Anchor account context – superset of all action requirements -// ----------------------------------------------------------------------------- -#[derive(Accounts)] -#[instruction(args: ExecuteArgs)] -pub struct Execute<'info> { - #[account(mut)] - pub payer: Signer<'info>, - - #[account( - seeds = [Config::PREFIX_SEED], - bump, - owner = ID - )] - pub config: Box>, - - #[account( - mut, - seeds = [SMART_WALLET_SEED, smart_wallet_config.id.to_le_bytes().as_ref()], - bump = smart_wallet_config.bump, - owner = ID, - )] - /// CHECK: Validated through seeds and bump - pub smart_wallet: UncheckedAccount<'info>, - - #[account( - mut, - seeds = [SmartWalletConfig::PREFIX_SEED, smart_wallet.key().as_ref()], - bump, - owner = ID, - constraint = smart_wallet_config.rule_program != Pubkey::default() @ LazorKitError::InvalidWalletConfiguration - )] - pub smart_wallet_config: Box>, - - #[account( - seeds = [ - SmartWalletAuthenticator::PREFIX_SEED, - smart_wallet.key().as_ref(), - args.passkey_pubkey.to_hashed_bytes(smart_wallet.key()).as_ref() - ], - bump = smart_wallet_authenticator.bump, - owner = ID, - constraint = smart_wallet_authenticator.smart_wallet == smart_wallet.key() @ LazorKitError::SmartWalletMismatch, - constraint = smart_wallet_authenticator.passkey_pubkey == args.passkey_pubkey @ LazorKitError::PasskeyMismatch - )] - pub smart_wallet_authenticator: Box>, - - #[account( - seeds = [WhitelistRulePrograms::PREFIX_SEED], - bump, - owner = ID - )] - pub whitelist_rule_programs: Box>, - - /// CHECK: Validated in handlers based on action type - pub authenticator_program: UncheckedAccount<'info>, - - #[account( - address = IX_ID, - constraint = ix_sysvar.key() == IX_ID @ LazorKitError::InvalidAccountData - )] - /// CHECK: instruction sysvar validated by address - pub ix_sysvar: UncheckedAccount<'info>, - - pub system_program: Program<'info, System>, - - /// CHECK: Validated in handlers based on action type - pub cpi_program: UncheckedAccount<'info>, - - /// The new authenticator is an optional account that is only initialized - /// by the `CallRuleProgram` action. It is passed as an UncheckedAccount - /// and created via CPI if needed. - pub new_smart_wallet_authenticator: Option>, -} diff --git a/programs/lazorkit/src/instructions/execute/call_rule_direct.rs b/programs/lazorkit/src/instructions/execute/call_rule_direct.rs new file mode 100644 index 0000000..7cc6624 --- /dev/null +++ b/programs/lazorkit/src/instructions/execute/call_rule_direct.rs @@ -0,0 +1,169 @@ +use anchor_lang::prelude::*; + +use crate::instructions::{Args as _, CallRuleArgs}; +use crate::security::validation; +use crate::state::{ + CallRuleMessage, Config, SmartWalletAuthenticator, SmartWalletConfig, WhitelistRulePrograms, +}; +use crate::utils::{check_whitelist, execute_cpi, get_pda_signer, verify_authorization}; +use crate::{error::LazorKitError, ID}; +use anchor_lang::solana_program::hash::{hash, Hasher}; + +pub fn call_rule_direct<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, CallRuleDirect<'info>>, + args: CallRuleArgs, +) -> Result<()> { + // 0. Validate args and global state + args.validate()?; + require!(!ctx.accounts.config.is_paused, LazorKitError::ProgramPaused); + validation::validate_remaining_accounts(&ctx.remaining_accounts)?; + validation::validate_program_executable(&ctx.accounts.rule_program)?; + // Rule program must be the configured one and whitelisted + require!( + ctx.accounts.rule_program.key() == ctx.accounts.smart_wallet_config.rule_program, + LazorKitError::InvalidProgramAddress + ); + check_whitelist( + &ctx.accounts.whitelist_rule_programs, + &ctx.accounts.rule_program.key(), + )?; + validation::validate_rule_data(&args.rule_data)?; + + // Verify and deserialize message purpose-built for call-rule + let msg: CallRuleMessage = verify_authorization( + &ctx.accounts.ix_sysvar, + &ctx.accounts.smart_wallet_authenticator, + ctx.accounts.smart_wallet.key(), + args.passkey_pubkey, + args.signature.clone(), + &args.client_data_json_raw, + &args.authenticator_data_raw, + args.verify_instruction_index, + ctx.accounts.smart_wallet_config.last_nonce, + )?; + + // Compare inline rule_data hash + require!( + hash(&args.rule_data).to_bytes() == msg.rule_data_hash, + LazorKitError::InvalidInstructionData + ); + + // Hash rule accounts (skip optional new authenticator at index 0) + let start_idx = if args.new_authenticator.is_some() { + 1 + } else { + 0 + }; + let rule_accs = &ctx.remaining_accounts[start_idx..]; + let mut hasher = Hasher::default(); + hasher.hash(ctx.accounts.rule_program.key().as_ref()); + for acc in rule_accs.iter() { + hasher.hash(acc.key.as_ref()); + hasher.hash(&[acc.is_writable as u8, acc.is_signer as u8]); + } + require!( + hasher.result().to_bytes() == msg.rule_accounts_hash, + LazorKitError::InvalidAccountData + ); + + // PDA signer for rule CPI + let rule_signer = get_pda_signer( + &args.passkey_pubkey, + ctx.accounts.smart_wallet.key(), + ctx.accounts.smart_wallet_authenticator.bump, + ); + + // Optionally create new authenticator if requested + if let Some(new_authentcator) = args.new_authenticator { + require!( + new_authentcator.passkey_pubkey[0] == 0x02 + || new_authentcator.passkey_pubkey[0] == 0x03, + LazorKitError::InvalidPasskeyFormat + ); + // Get the new authenticator account from remaining accounts + let new_auth = ctx + .remaining_accounts + .first() + .ok_or(LazorKitError::InvalidRemainingAccounts)?; + + require!( + new_auth.data_is_empty(), + LazorKitError::AccountAlreadyInitialized + ); + crate::state::SmartWalletAuthenticator::init( + new_auth, + ctx.accounts.payer.to_account_info(), + ctx.accounts.system_program.to_account_info(), + ctx.accounts.smart_wallet.key(), + new_authentcator.passkey_pubkey, + new_authentcator.credential_id, + )?; + } + + // Execute rule CPI + execute_cpi( + rule_accs, + &args.rule_data, + &ctx.accounts.rule_program, + Some(rule_signer), + )?; + + // increment nonce + ctx.accounts.smart_wallet_config.last_nonce = ctx + .accounts + .smart_wallet_config + .last_nonce + .checked_add(1) + .ok_or(LazorKitError::NonceOverflow)?; + + Ok(()) +} + +#[derive(Accounts)] +pub struct CallRuleDirect<'info> { + #[account(mut)] + pub payer: Signer<'info>, + + #[account(seeds = [Config::PREFIX_SEED], bump, owner = ID)] + pub config: Box>, + + #[account( + mut, + seeds = [crate::constants::SMART_WALLET_SEED, smart_wallet_config.id.to_le_bytes().as_ref()], + bump = smart_wallet_config.bump, + owner = ID, + )] + /// CHECK: smart wallet PDA verified by seeds + pub smart_wallet: UncheckedAccount<'info>, + + #[account( + mut, + seeds = [SmartWalletConfig::PREFIX_SEED, smart_wallet.key().as_ref()], + bump, + owner = ID, + )] + pub smart_wallet_config: Box>, + + #[account(owner = ID)] + pub smart_wallet_authenticator: Box>, + + /// CHECK: executable rule program + #[account(executable)] + pub rule_program: UncheckedAccount<'info>, + + #[account( + seeds = [WhitelistRulePrograms::PREFIX_SEED], + bump, + owner = ID + )] + pub whitelist_rule_programs: Box>, + + /// Optional new authenticator to initialize when requested in message + pub new_smart_wallet_authenticator: Option>, + + /// CHECK: instruction sysvar + #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)] + pub ix_sysvar: UncheckedAccount<'info>, + + pub system_program: Program<'info, System>, +} diff --git a/programs/lazorkit/src/instructions/execute/change_rule_direct.rs b/programs/lazorkit/src/instructions/execute/change_rule_direct.rs new file mode 100644 index 0000000..de338f1 --- /dev/null +++ b/programs/lazorkit/src/instructions/execute/change_rule_direct.rs @@ -0,0 +1,221 @@ +use anchor_lang::prelude::*; + +use crate::instructions::{Args as _, ChangeRuleArgs}; +use crate::security::validation; +use crate::state::{ + ChangeRuleMessage, Config, SmartWalletAuthenticator, SmartWalletConfig, WhitelistRulePrograms, +}; +use crate::utils::{check_whitelist, execute_cpi, get_pda_signer, sighash, verify_authorization}; +use crate::{error::LazorKitError, ID}; +use anchor_lang::solana_program::hash::{hash, Hasher}; + +pub fn change_rule_direct<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ChangeRuleDirect<'info>>, + args: ChangeRuleArgs, +) -> Result<()> { + // 0. Validate args and global state + args.validate()?; + require!(!ctx.accounts.config.is_paused, LazorKitError::ProgramPaused); + validation::validate_remaining_accounts(&ctx.remaining_accounts)?; + validation::validate_program_executable(&ctx.accounts.old_rule_program)?; + validation::validate_program_executable(&ctx.accounts.new_rule_program)?; + // Whitelist and config checks + check_whitelist( + &ctx.accounts.whitelist_rule_programs, + &ctx.accounts.old_rule_program.key(), + )?; + check_whitelist( + &ctx.accounts.whitelist_rule_programs, + &ctx.accounts.new_rule_program.key(), + )?; + require!( + ctx.accounts.smart_wallet_config.rule_program == ctx.accounts.old_rule_program.key(), + LazorKitError::InvalidProgramAddress + ); + // Ensure different programs + require!( + ctx.accounts.old_rule_program.key() != ctx.accounts.new_rule_program.key(), + LazorKitError::RuleProgramsIdentical + ); + validation::validate_rule_data(&args.destroy_rule_data)?; + validation::validate_rule_data(&args.init_rule_data)?; + + let msg: ChangeRuleMessage = verify_authorization( + &ctx.accounts.ix_sysvar, + &ctx.accounts.smart_wallet_authenticator, + ctx.accounts.smart_wallet.key(), + args.passkey_pubkey, + args.signature.clone(), + &args.client_data_json_raw, + &args.authenticator_data_raw, + args.verify_instruction_index, + ctx.accounts.smart_wallet_config.last_nonce, + )?; + + // accounts layout: Use split_index from args to separate destroy and init accounts + let split = args.split_index as usize; + require!( + split <= ctx.remaining_accounts.len(), + LazorKitError::AccountSliceOutOfBounds + ); + let (destroy_accounts, init_accounts) = ctx.remaining_accounts.split_at(split); + + // Hash checks + let mut h1 = Hasher::default(); + h1.hash(ctx.accounts.old_rule_program.key().as_ref()); + for a in destroy_accounts.iter() { + h1.hash(a.key.as_ref()); + h1.hash(&[a.is_writable as u8, a.is_signer as u8]); + } + require!( + h1.result().to_bytes() == msg.old_rule_accounts_hash, + LazorKitError::InvalidAccountData + ); + + let mut h2 = Hasher::default(); + h2.hash(ctx.accounts.new_rule_program.key().as_ref()); + for a in init_accounts.iter() { + h2.hash(a.key.as_ref()); + h2.hash(&[a.is_writable as u8, a.is_signer as u8]); + } + require!( + h2.result().to_bytes() == msg.new_rule_accounts_hash, + LazorKitError::InvalidAccountData + ); + + // discriminators + require!( + args.destroy_rule_data.get(0..8) == Some(&sighash("global", "destroy")), + LazorKitError::InvalidDestroyDiscriminator + ); + require!( + args.init_rule_data.get(0..8) == Some(&sighash("global", "init_rule")), + LazorKitError::InvalidInitRuleDiscriminator + ); + + // Compare rule data hashes from message + require!( + hash(&args.destroy_rule_data).to_bytes() == msg.old_rule_data_hash, + LazorKitError::InvalidInstructionData + ); + require!( + hash(&args.init_rule_data).to_bytes() == msg.new_rule_data_hash, + LazorKitError::InvalidInstructionData + ); + + // signer for CPI + let rule_signer = get_pda_signer( + &args.passkey_pubkey, + ctx.accounts.smart_wallet.key(), + ctx.accounts.smart_wallet_authenticator.bump, + ); + + // enforce default rule transition if desired + let default_rule = ctx.accounts.config.default_rule_program; + require!( + ctx.accounts.old_rule_program.key() == default_rule + || ctx.accounts.new_rule_program.key() == default_rule, + LazorKitError::NoDefaultRuleProgram + ); + + // update wallet config + ctx.accounts.smart_wallet_config.rule_program = ctx.accounts.new_rule_program.key(); + + // Optionally create new authenticator if requested + if let Some(new_authentcator) = args.new_authenticator { + require!( + new_authentcator.passkey_pubkey[0] == 0x02 + || new_authentcator.passkey_pubkey[0] == 0x03, + LazorKitError::InvalidPasskeyFormat + ); + // Get the new authenticator account from remaining accounts + let new_auth = ctx + .remaining_accounts + .first() + .ok_or(LazorKitError::InvalidRemainingAccounts)?; + + require!( + new_auth.data_is_empty(), + LazorKitError::AccountAlreadyInitialized + ); + crate::state::SmartWalletAuthenticator::init( + new_auth, + ctx.accounts.payer.to_account_info(), + ctx.accounts.system_program.to_account_info(), + ctx.accounts.smart_wallet.key(), + new_authentcator.passkey_pubkey, + new_authentcator.credential_id, + )?; + } + + // destroy and init + execute_cpi( + destroy_accounts, + &args.destroy_rule_data, + &ctx.accounts.old_rule_program, + Some(rule_signer.clone()), + )?; + + execute_cpi( + init_accounts, + &args.init_rule_data, + &ctx.accounts.new_rule_program, + Some(rule_signer), + )?; + + // bump nonce + ctx.accounts.smart_wallet_config.last_nonce = ctx + .accounts + .smart_wallet_config + .last_nonce + .checked_add(1) + .ok_or(LazorKitError::NonceOverflow)?; + + Ok(()) +} + +#[derive(Accounts)] +pub struct ChangeRuleDirect<'info> { + #[account(mut)] + pub payer: Signer<'info>, + + #[account(seeds = [Config::PREFIX_SEED], bump, owner = ID)] + pub config: Box>, + + #[account( + mut, + seeds = [crate::constants::SMART_WALLET_SEED, smart_wallet_config.id.to_le_bytes().as_ref()], + bump = smart_wallet_config.bump, + owner = ID, + )] + /// CHECK: PDA verified by seeds + pub smart_wallet: UncheckedAccount<'info>, + + #[account( + mut, + seeds = [SmartWalletConfig::PREFIX_SEED, smart_wallet.key().as_ref()], + bump, + owner = ID, + )] + pub smart_wallet_config: Box>, + + #[account(owner = ID)] + pub smart_wallet_authenticator: Box>, + + /// CHECK + pub old_rule_program: UncheckedAccount<'info>, + /// CHECK + pub new_rule_program: UncheckedAccount<'info>, + + #[account( + seeds = [WhitelistRulePrograms::PREFIX_SEED], + bump, + owner = ID + )] + pub whitelist_rule_programs: Box>, + + /// CHECK + #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)] + pub ix_sysvar: UncheckedAccount<'info>, + pub system_program: Program<'info, System>, +} diff --git a/programs/lazorkit/src/instructions/execute/chunk/commit_cpi.rs b/programs/lazorkit/src/instructions/execute/chunk/commit_cpi.rs new file mode 100644 index 0000000..6f14e58 --- /dev/null +++ b/programs/lazorkit/src/instructions/execute/chunk/commit_cpi.rs @@ -0,0 +1,168 @@ +use anchor_lang::prelude::*; + +use crate::instructions::CommitArgs; +use crate::security::validation; +use crate::state::{ + Config, CpiCommit, ExecuteMessage, SmartWalletAuthenticator, SmartWalletConfig, + WhitelistRulePrograms, +}; +use crate::utils::{execute_cpi, get_pda_signer, sighash, verify_authorization, PasskeyExt}; +use crate::{constants::SMART_WALLET_SEED, error::LazorKitError, ID}; +use anchor_lang::solana_program::hash::{hash, Hasher}; + +pub fn commit_cpi(ctx: Context, args: CommitArgs) -> Result<()> { + // 0. Validate + validation::validate_remaining_accounts(&ctx.remaining_accounts)?; + validation::validate_rule_data(&args.rule_data)?; + require!(!ctx.accounts.config.is_paused, LazorKitError::ProgramPaused); + + // 1. Authorization -> typed ExecuteMessage + let msg: ExecuteMessage = verify_authorization::( + &ctx.accounts.ix_sysvar, + &ctx.accounts.smart_wallet_authenticator, + ctx.accounts.smart_wallet.key(), + args.passkey_pubkey, + args.signature.clone(), + &args.client_data_json_raw, + &args.authenticator_data_raw, + args.verify_instruction_index, + ctx.accounts.smart_wallet_config.last_nonce, + )?; + + // 2. In commit mode, all remaining accounts are for rule checking + let rule_accounts = &ctx.remaining_accounts[..]; + + // 3. Optional rule-check now (bind policy & validate hashes) + // Ensure rule program matches config and whitelist + validation::validate_program_executable(&ctx.accounts.authenticator_program)?; + require!( + ctx.accounts.authenticator_program.key() == ctx.accounts.smart_wallet_config.rule_program, + LazorKitError::InvalidProgramAddress + ); + crate::utils::check_whitelist( + &ctx.accounts.whitelist_rule_programs, + &ctx.accounts.authenticator_program.key(), + )?; + + // Compare rule_data hash with message + require!( + hash(&args.rule_data).to_bytes() == msg.rule_data_hash, + LazorKitError::InvalidInstructionData + ); + // Compare rule_accounts hash with message + let mut rh = Hasher::default(); + rh.hash(ctx.accounts.authenticator_program.key.as_ref()); + for a in rule_accounts.iter() { + rh.hash(a.key.as_ref()); + rh.hash(&[a.is_writable as u8, a.is_signer as u8]); + } + require!( + rh.result().to_bytes() == msg.rule_accounts_hash, + LazorKitError::InvalidAccountData + ); + + // Execute rule check + let rule_signer = get_pda_signer( + &args.passkey_pubkey, + ctx.accounts.smart_wallet.key(), + ctx.accounts.smart_wallet_authenticator.bump, + ); + require!( + args.rule_data.get(0..8) == Some(&sighash("global", "check_rule")), + LazorKitError::InvalidCheckRuleDiscriminator + ); + execute_cpi( + rule_accounts, + &args.rule_data, + &ctx.accounts.authenticator_program, + Some(rule_signer), + )?; + + // 5. Write commit using hashes from message + let commit = &mut ctx.accounts.cpi_commit; + commit.owner_wallet = ctx.accounts.smart_wallet.key(); + commit.data_hash = msg.cpi_data_hash; + commit.accounts_hash = msg.cpi_accounts_hash; + commit.authorized_nonce = ctx.accounts.smart_wallet_config.last_nonce; + commit.expires_at = args.expires_at; + commit.rent_refund_to = ctx.accounts.payer.key(); + + // Advance nonce + ctx.accounts.smart_wallet_config.last_nonce = ctx + .accounts + .smart_wallet_config + .last_nonce + .checked_add(1) + .ok_or(LazorKitError::NonceOverflow)?; + + Ok(()) +} + +#[derive(Accounts)] +#[instruction(args: CommitArgs)] +pub struct CommitCpi<'info> { + #[account(mut)] + pub payer: Signer<'info>, + + #[account(seeds = [Config::PREFIX_SEED], bump, owner = ID)] + pub config: Box>, + + #[account( + mut, + seeds = [SMART_WALLET_SEED, smart_wallet_config.id.to_le_bytes().as_ref()], + bump = smart_wallet_config.bump, + owner = ID, + )] + /// CHECK: PDA verified + pub smart_wallet: UncheckedAccount<'info>, + + #[account( + mut, + seeds = [SmartWalletConfig::PREFIX_SEED, smart_wallet.key().as_ref()], + bump, + owner = ID, + )] + pub smart_wallet_config: Box>, + + #[account( + seeds = [ + SmartWalletAuthenticator::PREFIX_SEED, + smart_wallet.key().as_ref(), + args.passkey_pubkey.to_hashed_bytes(smart_wallet.key()).as_ref() + ], + bump = smart_wallet_authenticator.bump, + owner = ID, + constraint = smart_wallet_authenticator.smart_wallet == smart_wallet.key() @ LazorKitError::SmartWalletMismatch, + constraint = smart_wallet_authenticator.passkey_pubkey == args.passkey_pubkey @ LazorKitError::PasskeyMismatch + )] + pub smart_wallet_authenticator: Box>, + + #[account( + seeds = [WhitelistRulePrograms::PREFIX_SEED], + bump, + owner = ID + )] + pub whitelist_rule_programs: Box>, + + /// Rule program for optional policy enforcement at commit time + /// CHECK: validated via executable + whitelist + #[account(executable)] + pub authenticator_program: UncheckedAccount<'info>, + + /// New commit account (rent payer: payer) + #[account( + init, + payer = payer, + space = 8 + CpiCommit::INIT_SPACE, + seeds = [CpiCommit::PREFIX_SEED, smart_wallet.key().as_ref(), &smart_wallet_config.last_nonce.to_le_bytes()], + bump, + owner = ID, + )] + pub cpi_commit: Account<'info, CpiCommit>, + + /// CHECK: instructions sysvar + #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)] + pub ix_sysvar: UncheckedAccount<'info>, + + pub system_program: Program<'info, System>, +} diff --git a/programs/lazorkit/src/instructions/execute/chunk/execute_committed.rs b/programs/lazorkit/src/instructions/execute/chunk/execute_committed.rs new file mode 100644 index 0000000..1137a91 --- /dev/null +++ b/programs/lazorkit/src/instructions/execute/chunk/execute_committed.rs @@ -0,0 +1,177 @@ +use anchor_lang::prelude::*; + +use crate::constants::SOL_TRANSFER_DISCRIMINATOR; +use crate::error::LazorKitError; +use crate::security::validation; +use crate::state::{Config, CpiCommit, SmartWalletConfig}; +use crate::utils::{execute_cpi, transfer_sol_from_pda, PdaSigner}; +use crate::{constants::SMART_WALLET_SEED, ID}; + +pub fn execute_committed(ctx: Context, cpi_data: Vec) -> Result<()> { + // We'll gracefully abort (close the commit and return Ok) if any binding check fails. + // Only hard fail on obviously invalid input sizes. + if let Err(_) = validation::validate_remaining_accounts(&ctx.remaining_accounts) { + return Ok(()); // graceful no-op; account will still be closed below + } + + let commit = &mut ctx.accounts.cpi_commit; + + // Expiry and usage + let now = Clock::get()?.unix_timestamp; + if commit.expires_at < now { + return Ok(()); + } + + // Bind wallet and target program + if commit.owner_wallet != ctx.accounts.smart_wallet.key() { + return Ok(()); + } + + // Validate program is executable only (no whitelist/rule checks here) + if !ctx.accounts.cpi_program.executable { + return Ok(()); + } + + // Compute accounts hash from remaining accounts and compare + let mut hasher = anchor_lang::solana_program::hash::Hasher::default(); + hasher.hash(ctx.accounts.cpi_program.key.as_ref()); + for acc in ctx.remaining_accounts.iter() { + hasher.hash(acc.key.as_ref()); + hasher.hash(&[acc.is_writable as u8, acc.is_signer as u8]); + } + let computed = hasher.result().to_bytes(); + if computed != commit.accounts_hash { + return Ok(()); + } + + // Verify data_hash bound with authorized nonce to prevent cross-commit reuse + let data_hash = anchor_lang::solana_program::hash::hash(&cpi_data).to_bytes(); + if data_hash != commit.data_hash { + return Ok(()); + } + + if cpi_data.get(0..4) == Some(&SOL_TRANSFER_DISCRIMINATOR) + && ctx.accounts.cpi_program.key() == anchor_lang::solana_program::system_program::ID + { + // === Native SOL Transfer === + require!( + ctx.remaining_accounts.len() >= 2, + LazorKitError::SolTransferInsufficientAccounts + ); + + // Extract and validate amount + let amount_bytes = cpi_data.get(4..12).ok_or(LazorKitError::InvalidCpiData)?; + let amount = u64::from_le_bytes( + amount_bytes + .try_into() + .map_err(|_| LazorKitError::InvalidCpiData)?, + ); + + // Validate amount + validation::validate_lamport_amount(amount)?; + + // Ensure destination is valid + let destination_account = &ctx.remaining_accounts[1]; + require!( + destination_account.key() != ctx.accounts.smart_wallet.key(), + LazorKitError::InvalidAccountData + ); + + // Check wallet has sufficient balance + let wallet_balance = ctx.accounts.smart_wallet.lamports(); + let rent_exempt = Rent::get()?.minimum_balance(0); + let total_needed = amount + .checked_add(ctx.accounts.config.execute_fee) + .ok_or(LazorKitError::IntegerOverflow)? + .checked_add(rent_exempt) + .ok_or(LazorKitError::IntegerOverflow)?; + + require!( + wallet_balance >= total_needed, + LazorKitError::InsufficientLamports + ); + + msg!( + "Transferring {} lamports to {}", + amount, + destination_account.key() + ); + + transfer_sol_from_pda(&ctx.accounts.smart_wallet, destination_account, amount)?; + } else { + // Validate CPI program + validation::validate_program_executable(&ctx.accounts.cpi_program)?; + + // Ensure CPI program is not this program (prevent reentrancy) + require!( + ctx.accounts.cpi_program.key() != crate::ID, + LazorKitError::ReentrancyDetected + ); + + // Ensure sufficient accounts for CPI + require!( + !ctx.remaining_accounts.is_empty(), + LazorKitError::InsufficientCpiAccounts + ); + + // Create wallet signer + let wallet_signer = PdaSigner { + seeds: vec![ + SMART_WALLET_SEED.to_vec(), + ctx.accounts.smart_wallet_config.id.to_le_bytes().to_vec(), + ], + bump: ctx.accounts.smart_wallet_config.bump, + }; + + msg!( + "Executing CPI to program: {}", + ctx.accounts.cpi_program.key() + ); + + execute_cpi( + ctx.remaining_accounts, + &cpi_data, + &ctx.accounts.cpi_program, + Some(wallet_signer), + )?; + } + + Ok(()) +} + +#[derive(Accounts)] +pub struct ExecuteCommitted<'info> { + #[account(mut)] + pub payer: Signer<'info>, + + #[account(seeds = [Config::PREFIX_SEED], bump, owner = ID)] + pub config: Box>, + + #[account( + mut, + seeds = [SMART_WALLET_SEED, smart_wallet_config.id.to_le_bytes().as_ref()], + bump = smart_wallet_config.bump, + owner = ID, + )] + /// CHECK: PDA verified + pub smart_wallet: UncheckedAccount<'info>, + + #[account( + mut, + seeds = [SmartWalletConfig::PREFIX_SEED, smart_wallet.key().as_ref()], + bump, + owner = ID, + )] + pub smart_wallet_config: Box>, + + /// CHECK: target CPI program + pub cpi_program: UncheckedAccount<'info>, + + /// Commit to execute. Closed on success to refund rent. + #[account(mut, close = commit_refund)] + pub cpi_commit: Account<'info, CpiCommit>, + + /// CHECK: rent refund destination (stored in commit) + #[account(mut, address = cpi_commit.rent_refund_to)] + pub commit_refund: UncheckedAccount<'info>, +} diff --git a/programs/lazorkit/src/instructions/execute/chunk/mod.rs b/programs/lazorkit/src/instructions/execute/chunk/mod.rs new file mode 100644 index 0000000..d9cb4f9 --- /dev/null +++ b/programs/lazorkit/src/instructions/execute/chunk/mod.rs @@ -0,0 +1,5 @@ +mod commit_cpi; +mod execute_committed; + +pub use commit_cpi::*; +pub use execute_committed::*; diff --git a/programs/lazorkit/src/instructions/execute/execute_txn_direct.rs b/programs/lazorkit/src/instructions/execute/execute_txn_direct.rs new file mode 100644 index 0000000..a7ba1d2 --- /dev/null +++ b/programs/lazorkit/src/instructions/execute/execute_txn_direct.rs @@ -0,0 +1,270 @@ +use anchor_lang::prelude::*; + +use crate::instructions::{Args as _, ExecuteTxnArgs}; +use crate::security::validation; +use crate::state::ExecuteMessage; +use crate::utils::{ + check_whitelist, execute_cpi, get_pda_signer, sighash, split_remaining_accounts, + transfer_sol_from_pda, verify_authorization, PdaSigner, +}; +use crate::{ + constants::{SMART_WALLET_SEED, SOL_TRANSFER_DISCRIMINATOR}, + error::LazorKitError, +}; +use anchor_lang::solana_program::hash::{hash, Hasher}; + +pub fn execute_txn_direct<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ExecuteTxn<'info>>, + args: ExecuteTxnArgs, +) -> Result<()> { + // 0. Validate args and global state + args.validate()?; + require!(!ctx.accounts.config.is_paused, LazorKitError::ProgramPaused); + validation::validate_remaining_accounts(&ctx.remaining_accounts)?; + + // 0.1 Verify authorization and parse typed message + let msg: ExecuteMessage = verify_authorization( + &ctx.accounts.ix_sysvar, + &ctx.accounts.smart_wallet_authenticator, + ctx.accounts.smart_wallet.key(), + args.passkey_pubkey, + args.signature.clone(), + &args.client_data_json_raw, + &args.authenticator_data_raw, + args.verify_instruction_index, + ctx.accounts.smart_wallet_config.last_nonce, + )?; + + // 1. Validate and check rule program + let rule_program_info = &ctx.accounts.authenticator_program; + + // Ensure rule program is executable + validation::validate_program_executable(rule_program_info)?; + + // Verify rule program is whitelisted + check_whitelist( + &ctx.accounts.whitelist_rule_programs, + &rule_program_info.key(), + )?; + + // Ensure rule program matches wallet configuration + require!( + rule_program_info.key() == ctx.accounts.smart_wallet_config.rule_program, + LazorKitError::InvalidProgramAddress + ); + + // 2. Prepare PDA signer for rule CPI + let rule_signer = get_pda_signer( + &args.passkey_pubkey, + ctx.accounts.smart_wallet.key(), + ctx.accounts.smart_wallet_authenticator.bump, + ); + + // 3. Split remaining accounts + let (rule_accounts, cpi_accounts) = + split_remaining_accounts(&ctx.remaining_accounts, args.split_index)?; + + // Validate account counts + require!( + !rule_accounts.is_empty(), + LazorKitError::InsufficientRuleAccounts + ); + + // 4. Verify rule discriminator on provided rule_data + let rule_data = &args.rule_data; + require!( + rule_data.get(0..8) == Some(&sighash("global", "check_rule")), + LazorKitError::InvalidCheckRuleDiscriminator + ); + + // 4.1 Validate rule_data size and compare hash from message + validation::validate_rule_data(rule_data)?; + require!( + hash(rule_data).to_bytes() == msg.rule_data_hash, + LazorKitError::InvalidInstructionData + ); + + // 4.2 Compare rule accounts hash against message + let mut rh = Hasher::default(); + rh.hash(rule_program_info.key.as_ref()); + for acc in rule_accounts.iter() { + rh.hash(acc.key.as_ref()); + rh.hash(&[acc.is_writable as u8, acc.is_signer as u8]); + } + require!( + rh.result().to_bytes() == msg.rule_accounts_hash, + LazorKitError::InvalidAccountData + ); + + // 5. Execute rule CPI to check if the transaction is allowed + msg!( + "Executing rule check for smart wallet: {}", + ctx.accounts.smart_wallet.key() + ); + + execute_cpi( + rule_accounts, + rule_data, + rule_program_info, + Some(rule_signer), + )?; + + msg!("Rule check passed"); + + // 6. Validate CPI payload and compare hashes + validation::validate_cpi_data(&args.cpi_data)?; + require!( + hash(&args.cpi_data).to_bytes() == msg.cpi_data_hash, + LazorKitError::InvalidInstructionData + ); + let mut ch = Hasher::default(); + ch.hash(ctx.accounts.cpi_program.key.as_ref()); + for acc in cpi_accounts.iter() { + ch.hash(acc.key.as_ref()); + ch.hash(&[acc.is_writable as u8, acc.is_signer as u8]); + } + require!( + ch.result().to_bytes() == msg.cpi_accounts_hash, + LazorKitError::InvalidAccountData + ); + + // 7. Execute main CPI or transfer lamports + if args.cpi_data.get(0..4) == Some(&SOL_TRANSFER_DISCRIMINATOR) + && ctx.accounts.cpi_program.key() == anchor_lang::solana_program::system_program::ID + { + // === Native SOL Transfer === + require!( + cpi_accounts.len() >= 2, + LazorKitError::SolTransferInsufficientAccounts + ); + + // Extract and validate amount + let amount_bytes = args + .cpi_data + .get(4..12) + .ok_or(LazorKitError::InvalidCpiData)?; + let amount = u64::from_le_bytes( + amount_bytes + .try_into() + .map_err(|_| LazorKitError::InvalidCpiData)?, + ); + + validation::validate_lamport_amount(amount)?; + + // Ensure destination is valid + let destination_account = &cpi_accounts[1]; + require!( + destination_account.key() != ctx.accounts.smart_wallet.key(), + LazorKitError::InvalidAccountData + ); + + // Check wallet has sufficient balance + let wallet_balance = ctx.accounts.smart_wallet.lamports(); + let rent_exempt = Rent::get()?.minimum_balance(0); + let total_needed = amount + .checked_add(ctx.accounts.config.execute_fee) + .ok_or(LazorKitError::IntegerOverflow)? + .checked_add(rent_exempt) + .ok_or(LazorKitError::IntegerOverflow)?; + + require!( + wallet_balance >= total_needed, + LazorKitError::InsufficientLamports + ); + + msg!( + "Transferring {} lamports to {}", + amount, + destination_account.key() + ); + transfer_sol_from_pda(&ctx.accounts.smart_wallet, destination_account, amount)?; + } else { + // === General CPI === + validation::validate_program_executable(&ctx.accounts.cpi_program)?; + require!( + ctx.accounts.cpi_program.key() != crate::ID, + LazorKitError::ReentrancyDetected + ); + require!( + !cpi_accounts.is_empty(), + LazorKitError::InsufficientCpiAccounts + ); + + // Create wallet signer + let wallet_signer = PdaSigner { + seeds: vec![ + SMART_WALLET_SEED.to_vec(), + ctx.accounts.smart_wallet_config.id.to_le_bytes().to_vec(), + ], + bump: ctx.accounts.smart_wallet_config.bump, + }; + + msg!( + "Executing CPI to program: {}", + ctx.accounts.cpi_program.key() + ); + execute_cpi( + cpi_accounts, + &args.cpi_data, + &ctx.accounts.cpi_program, + Some(wallet_signer), + )?; + } + + msg!("Transaction executed successfully"); + // 8. Increment nonce + ctx.accounts.smart_wallet_config.last_nonce = ctx + .accounts + .smart_wallet_config + .last_nonce + .checked_add(1) + .ok_or(LazorKitError::NonceOverflow)?; + Ok(()) +} + +#[derive(Accounts)] +pub struct ExecuteTxn<'info> { + #[account(mut)] + pub payer: Signer<'info>, + + #[account( + mut, + seeds = [SMART_WALLET_SEED, smart_wallet_config.id.to_le_bytes().as_ref()], + bump = smart_wallet_config.bump, + owner = crate::ID, + )] + /// CHECK: PDA verified by seeds + pub smart_wallet: UncheckedAccount<'info>, + + #[account( + mut, + seeds = [crate::state::SmartWalletConfig::PREFIX_SEED, smart_wallet.key().as_ref()], + bump, + owner = crate::ID, + )] + pub smart_wallet_config: Box>, + + #[account(owner = crate::ID)] + pub smart_wallet_authenticator: Box>, + #[account( + seeds = [crate::state::WhitelistRulePrograms::PREFIX_SEED], + bump, + owner = crate::ID + )] + pub whitelist_rule_programs: Box>, + /// CHECK: must be executable (rule program) + #[account(executable)] + pub authenticator_program: UncheckedAccount<'info>, + /// CHECK: must be executable (target program) + #[account(executable)] + pub cpi_program: UncheckedAccount<'info>, + #[account( + seeds = [crate::state::Config::PREFIX_SEED], + bump, + owner = crate::ID + )] + pub config: Box>, + /// CHECK: instruction sysvar + #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)] + pub ix_sysvar: UncheckedAccount<'info>, +} diff --git a/programs/lazorkit/src/instructions/execute/mod.rs b/programs/lazorkit/src/instructions/execute/mod.rs new file mode 100644 index 0000000..18fac9a --- /dev/null +++ b/programs/lazorkit/src/instructions/execute/mod.rs @@ -0,0 +1,9 @@ +mod call_rule_direct; +mod change_rule_direct; +mod chunk; +mod execute_txn_direct; + +pub use call_rule_direct::*; +pub use change_rule_direct::*; +pub use chunk::*; +pub use execute_txn_direct::*; diff --git a/programs/lazorkit/src/instructions/handlers/call_rule.rs b/programs/lazorkit/src/instructions/handlers/call_rule.rs deleted file mode 100644 index 4dbcc8c..0000000 --- a/programs/lazorkit/src/instructions/handlers/call_rule.rs +++ /dev/null @@ -1,90 +0,0 @@ -use super::super::{Execute, ExecuteArgs}; -use crate::error::LazorKitError; -use crate::security::validation; -use crate::state::{Message, SmartWalletAuthenticator}; -use crate::utils::{check_whitelist, execute_cpi, get_pda_signer, split_remaining_accounts}; -use anchor_lang::prelude::*; - -/// Handle `Action::CallRuleProgram` – may optionally create a new authenticator. -pub fn handle<'c: 'info, 'info>( - ctx: &mut Context<'_, '_, 'c, 'info, Execute<'info>>, - args: &ExecuteArgs, - msg: &Message, -) -> Result<()> { - let rule_program = &ctx.accounts.authenticator_program; - - // === Validate rule program === - validation::validate_program_executable(rule_program)?; - check_whitelist(&ctx.accounts.whitelist_rule_programs, &rule_program.key())?; - - // Ensure rule program matches wallet configuration - require!( - rule_program.key() == ctx.accounts.smart_wallet_config.rule_program, - LazorKitError::InvalidProgramAddress - ); - - // === Optionally create a new authenticator === - if let Some(new_passkey) = args.create_new_authenticator { - msg!("Creating new authenticator for passkey"); - - // Validate new passkey format - require!( - new_passkey[0] == 0x02 || new_passkey[0] == 0x03, - LazorKitError::InvalidPasskeyFormat - ); - - // Get the new authenticator account from remaining accounts - let new_smart_wallet_authenticator = ctx - .remaining_accounts - .first() - .ok_or(LazorKitError::InvalidRemainingAccounts)?; - - // Ensure the account is not already initialized - require!( - new_smart_wallet_authenticator.data_is_empty(), - LazorKitError::AccountAlreadyInitialized - ); - - // Initialize the new authenticator - SmartWalletAuthenticator::init( - &new_smart_wallet_authenticator, - ctx.accounts.payer.to_account_info(), - ctx.accounts.system_program.to_account_info(), - ctx.accounts.smart_wallet.key(), - new_passkey, - Vec::new(), // Empty credential ID for secondary authenticators - )?; - - msg!("New authenticator created: {}", new_smart_wallet_authenticator.key()); - } - - // === Prepare for rule CPI === - let rule_signer = get_pda_signer( - &args.passkey_pubkey, - ctx.accounts.smart_wallet.key(), - ctx.accounts.smart_wallet_authenticator.bump, - ); - - // Split accounts (skip first if new authenticator was created) - let skip_count = if args.create_new_authenticator.is_some() { 1 } else { 0 }; - let remaining_for_split = &ctx.remaining_accounts[skip_count..]; - - let (_, cpi_accounts) = split_remaining_accounts(remaining_for_split, msg.split_index)?; - - // Validate we have accounts for CPI - require!( - !cpi_accounts.is_empty(), - LazorKitError::InsufficientCpiAccounts - ); - - // Validate CPI data - validation::validate_cpi_data(&msg.cpi_data)?; - - msg!("Executing rule program CPI"); - - execute_cpi(cpi_accounts, &msg.cpi_data, rule_program, Some(rule_signer))?; - - msg!("Rule program call completed successfully"); - - Ok(()) -} diff --git a/programs/lazorkit/src/instructions/handlers/change_rule.rs b/programs/lazorkit/src/instructions/handlers/change_rule.rs deleted file mode 100644 index 36213c2..0000000 --- a/programs/lazorkit/src/instructions/handlers/change_rule.rs +++ /dev/null @@ -1,127 +0,0 @@ -use super::super::{Execute, ExecuteArgs}; -use crate::error::LazorKitError; -use crate::security::validation; -use crate::state::Message; -use crate::utils::{ - check_whitelist, execute_cpi, get_pda_signer, sighash, split_remaining_accounts, -}; -use anchor_lang::prelude::*; - -/// Handle `Action::ChangeRuleProgram` -pub fn handle<'c: 'info, 'info>( - ctx: &mut Context<'_, '_, 'c, 'info, Execute<'info>>, - args: &ExecuteArgs, - msg: &Message, -) -> Result<()> { - let old_rule_program = &ctx.accounts.authenticator_program; - let new_rule_program = &ctx.accounts.cpi_program; - - // === Validate both programs are executable === - validation::validate_program_executable(old_rule_program)?; - validation::validate_program_executable(new_rule_program)?; - - // === Verify both programs are whitelisted === - check_whitelist( - &ctx.accounts.whitelist_rule_programs, - &old_rule_program.key(), - )?; - check_whitelist( - &ctx.accounts.whitelist_rule_programs, - &new_rule_program.key(), - )?; - - // === Validate current rule program matches wallet config === - require!( - old_rule_program.key() == ctx.accounts.smart_wallet_config.rule_program, - LazorKitError::InvalidProgramAddress - ); - - // === Check if rule_data is provided and verify destroy discriminator === - let rule_data = msg - .rule_data - .as_ref() - .ok_or(LazorKitError::RuleDataRequired)?; - - // Validate rule data size - validation::validate_rule_data(rule_data)?; - - require!( - rule_data.get(0..8) == Some(&sighash("global", "destroy")), - LazorKitError::InvalidDestroyDiscriminator - ); - - // === Validate init_rule discriminator === - require!( - msg.cpi_data.get(0..8) == Some(&sighash("global", "init_rule")), - LazorKitError::InvalidInitRuleDiscriminator - ); - - // === Ensure programs are different === - require!( - old_rule_program.key() != new_rule_program.key(), - LazorKitError::RuleProgramsIdentical - ); - - // === Default rule constraint === - // This constraint means that a user can only switch between the default rule - // and another rule. They cannot switch between two non-default rules. - let default_rule_program = ctx.accounts.config.default_rule_program; - require!( - old_rule_program.key() == default_rule_program - || new_rule_program.key() == default_rule_program, - LazorKitError::NoDefaultRuleProgram - ); - - // === Update wallet configuration === - msg!("Changing rule program from {} to {}", - old_rule_program.key(), - new_rule_program.key() - ); - - ctx.accounts.smart_wallet_config.rule_program = new_rule_program.key(); - - // === Create PDA signer === - let rule_signer = get_pda_signer( - &args.passkey_pubkey, - ctx.accounts.smart_wallet.key(), - ctx.accounts.smart_wallet_authenticator.bump, - ); - - // === Split and validate accounts === - let (rule_accounts, cpi_accounts) = - split_remaining_accounts(ctx.remaining_accounts, msg.split_index)?; - - // Ensure we have sufficient accounts for both operations - require!( - !rule_accounts.is_empty(), - LazorKitError::InsufficientRuleAccounts - ); - require!( - !cpi_accounts.is_empty(), - LazorKitError::InsufficientCpiAccounts - ); - - // === Destroy old rule instance === - msg!("Destroying old rule instance"); - - execute_cpi( - rule_accounts, - rule_data, - old_rule_program, - Some(rule_signer.clone()), - )?; - - // === Initialize new rule instance === - msg!("Initializing new rule instance"); - - execute_cpi( - cpi_accounts, - &msg.cpi_data, - new_rule_program, - Some(rule_signer), - )?; - - msg!("Rule program changed successfully"); - - Ok(()) -} diff --git a/programs/lazorkit/src/instructions/handlers/execute_tx.rs b/programs/lazorkit/src/instructions/handlers/execute_tx.rs deleted file mode 100644 index c5aae01..0000000 --- a/programs/lazorkit/src/instructions/handlers/execute_tx.rs +++ /dev/null @@ -1,164 +0,0 @@ -use anchor_lang::prelude::*; - -use crate::security::validation; -use crate::utils::{ - check_whitelist, execute_cpi, get_pda_signer, sighash, split_remaining_accounts, - transfer_sol_from_pda, PdaSigner, -}; -use crate::{ - constants::{SMART_WALLET_SEED, SOL_TRANSFER_DISCRIMINATOR}, - error::LazorKitError, -}; - -use super::super::{Execute, ExecuteArgs}; -use crate::state::Message; - -/// Handle `Action::ExecuteTx` -pub fn handle<'c: 'info, 'info>( - ctx: &mut Context<'_, '_, 'c, 'info, Execute<'info>>, - _args: &ExecuteArgs, - msg: &Message, -) -> Result<()> { - // 1. Validate and check rule program - let rule_program_info = &ctx.accounts.authenticator_program; - - // Ensure rule program is executable - validation::validate_program_executable(rule_program_info)?; - - // Verify rule program is whitelisted - check_whitelist( - &ctx.accounts.whitelist_rule_programs, - &rule_program_info.key(), - )?; - - // Ensure rule program matches wallet configuration - require!( - rule_program_info.key() == ctx.accounts.smart_wallet_config.rule_program, - LazorKitError::InvalidProgramAddress - ); - - // 2. Prepare PDA signer for rule CPI - let rule_signer = get_pda_signer( - &_args.passkey_pubkey, - ctx.accounts.smart_wallet.key(), - ctx.accounts.smart_wallet_authenticator.bump, - ); - - // 3. Split remaining accounts - let (rule_accounts, cpi_accounts) = - split_remaining_accounts(&ctx.remaining_accounts, msg.split_index)?; - - // Validate account counts - require!( - !rule_accounts.is_empty(), - LazorKitError::InsufficientRuleAccounts - ); - - // 4. Check if rule_data is provided and verify rule discriminator - let rule_data = msg.rule_data.as_ref().ok_or(LazorKitError::RuleDataRequired)?; - require!( - rule_data.get(0..8) == Some(&sighash("global", "check_rule")), - LazorKitError::InvalidCheckRuleDiscriminator - ); - - // 5. Execute rule CPI to check if the transaction is allowed - msg!("Executing rule check for smart wallet: {}", ctx.accounts.smart_wallet.key()); - - execute_cpi( - rule_accounts, - rule_data, - rule_program_info, - Some(rule_signer), - )?; - - msg!("Rule check passed"); - - // 6. Execute main CPI or transfer lamports - if msg.cpi_data.get(0..4) == Some(&SOL_TRANSFER_DISCRIMINATOR) - && ctx.accounts.cpi_program.key() == anchor_lang::solana_program::system_program::ID - { - // === Native SOL Transfer === - require!( - cpi_accounts.len() >= 2, - LazorKitError::SolTransferInsufficientAccounts - ); - - // Extract and validate amount - let amount_bytes = msg - .cpi_data - .get(4..12) - .ok_or(LazorKitError::InvalidCpiData)?; - let amount = u64::from_le_bytes( - amount_bytes - .try_into() - .map_err(|_| LazorKitError::InvalidCpiData)?, - ); - - // Validate amount - validation::validate_lamport_amount(amount)?; - - // Ensure destination is valid - let destination_account = &cpi_accounts[1]; - require!( - destination_account.key() != ctx.accounts.smart_wallet.key(), - LazorKitError::InvalidAccountData - ); - - // Check wallet has sufficient balance - let wallet_balance = ctx.accounts.smart_wallet.lamports(); - let rent_exempt = Rent::get()?.minimum_balance(0); - let total_needed = amount - .checked_add(ctx.accounts.config.execute_fee) - .ok_or(LazorKitError::IntegerOverflow)? - .checked_add(rent_exempt) - .ok_or(LazorKitError::IntegerOverflow)?; - - require!( - wallet_balance >= total_needed, - LazorKitError::InsufficientLamports - ); - - msg!("Transferring {} lamports to {}", amount, destination_account.key()); - - transfer_sol_from_pda(&ctx.accounts.smart_wallet, destination_account, amount)?; - } else { - // === General CPI === - - // Validate CPI program - validation::validate_program_executable(&ctx.accounts.cpi_program)?; - - // Ensure CPI program is not this program (prevent reentrancy) - require!( - ctx.accounts.cpi_program.key() != crate::ID, - LazorKitError::ReentrancyDetected - ); - - // Ensure sufficient accounts for CPI - require!( - !cpi_accounts.is_empty(), - LazorKitError::InsufficientCpiAccounts - ); - - // Create wallet signer - let wallet_signer = PdaSigner { - seeds: vec![ - SMART_WALLET_SEED.to_vec(), - ctx.accounts.smart_wallet_config.id.to_le_bytes().to_vec(), - ], - bump: ctx.accounts.smart_wallet_config.bump, - }; - - msg!("Executing CPI to program: {}", ctx.accounts.cpi_program.key()); - - execute_cpi( - cpi_accounts, - &msg.cpi_data, - &ctx.accounts.cpi_program, - Some(wallet_signer), - )?; - } - - msg!("Transaction executed successfully"); - - Ok(()) -} diff --git a/programs/lazorkit/src/instructions/handlers/mod.rs b/programs/lazorkit/src/instructions/handlers/mod.rs deleted file mode 100644 index 331ea5f..0000000 --- a/programs/lazorkit/src/instructions/handlers/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod call_rule; -pub mod execute_tx; -pub mod change_rule; diff --git a/programs/lazorkit/src/instructions/initialize.rs b/programs/lazorkit/src/instructions/initialize.rs index d8bf3dc..f551d8c 100644 --- a/programs/lazorkit/src/instructions/initialize.rs +++ b/programs/lazorkit/src/instructions/initialize.rs @@ -20,11 +20,7 @@ pub fn initialize(ctx: Context) -> Result<()> { config.execute_fee = 0; // LAMPORTS config.default_rule_program = ctx.accounts.default_rule_program.key(); config.is_paused = false; - - msg!("LazorKit initialized successfully"); - msg!("Authority: {}", config.authority); - msg!("Default rule program: {}", config.default_rule_program); - + Ok(()) } @@ -36,7 +32,7 @@ pub struct Initialize<'info> { /// The program's configuration account. #[account( - init_if_needed, + init, payer = signer, space = 8 + Config::INIT_SPACE, seeds = [Config::PREFIX_SEED], @@ -46,7 +42,7 @@ pub struct Initialize<'info> { /// The list of whitelisted rule programs that can be used with smart wallets. #[account( - init_if_needed, + init, payer = signer, space = 8 + WhitelistRulePrograms::INIT_SPACE, seeds = [WhitelistRulePrograms::PREFIX_SEED], @@ -56,7 +52,7 @@ pub struct Initialize<'info> { /// The default rule program to be used for new smart wallets. /// CHECK: This is checked to be executable. - pub default_rule_program: AccountInfo<'info>, + pub default_rule_program: UncheckedAccount<'info>, /// The system program. pub system_program: Program<'info, System>, diff --git a/programs/lazorkit/src/instructions/mod.rs b/programs/lazorkit/src/instructions/mod.rs index aae657b..3b2f19a 100644 --- a/programs/lazorkit/src/instructions/mod.rs +++ b/programs/lazorkit/src/instructions/mod.rs @@ -1,11 +1,11 @@ +mod admin; +mod args; mod create_smart_wallet; mod execute; -mod handlers; mod initialize; -mod admin; +pub use admin::*; +pub use args::*; pub use create_smart_wallet::*; pub use execute::*; pub use initialize::*; -pub use admin::*; -pub use handlers::*; \ No newline at end of file diff --git a/programs/lazorkit/src/lib.rs b/programs/lazorkit/src/lib.rs index 645dac4..5c7bd24 100644 --- a/programs/lazorkit/src/lib.rs +++ b/programs/lazorkit/src/lib.rs @@ -8,7 +8,6 @@ pub mod security; pub mod state; pub mod utils; -use constants::PASSKEY_SIZE; use instructions::*; use state::*; @@ -36,32 +35,45 @@ pub mod lazorkit { /// Create a new smart wallet with passkey authentication pub fn create_smart_wallet( ctx: Context, - passkey_pubkey: [u8; PASSKEY_SIZE], - credential_id: Vec, - rule_data: Vec, - wallet_id: u64, - is_pay_for_user: bool, + args: CreatwSmartWalletArgs, ) -> Result<()> { - instructions::create_smart_wallet( - ctx, - passkey_pubkey, - credential_id, - rule_data, - wallet_id, - is_pay_for_user, - ) - } - - /// Unified execute entrypoint covering all smart-wallet actions - pub fn execute<'c: 'info, 'info>( - ctx: Context<'_, '_, 'c, 'info, Execute<'info>>, - args: ExecuteArgs, - ) -> Result<()> { - instructions::execute(ctx, args) + instructions::create_smart_wallet(ctx, args) } /// Add a program to the whitelist of rule programs pub fn add_whitelist_rule_program(ctx: Context) -> Result<()> { instructions::add_whitelist_rule_program(ctx) } + + pub fn change_rule_direct<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ChangeRuleDirect<'info>>, + args: ChangeRuleArgs, + ) -> Result<()> { + instructions::change_rule_direct(ctx, args) + } + + pub fn call_rule_direct<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, CallRuleDirect<'info>>, + args: CallRuleArgs, + ) -> Result<()> { + instructions::call_rule_direct(ctx, args) + } + + pub fn execute_txn_direct<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ExecuteTxn<'info>>, + args: ExecuteTxnArgs, + ) -> Result<()> { + instructions::execute_txn_direct(ctx, args) + } + + pub fn commit_cpi<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, CommitCpi<'info>>, + args: CommitArgs, + ) -> Result<()> { + instructions::commit_cpi(ctx, args) + } + + pub fn execute_committed(ctx: Context, cpi_data: Vec) -> Result<()> { + instructions::execute_committed(ctx, cpi_data) + } } diff --git a/programs/lazorkit/src/security.rs b/programs/lazorkit/src/security.rs index f1775ad..dc6b679 100644 --- a/programs/lazorkit/src/security.rs +++ b/programs/lazorkit/src/security.rs @@ -66,6 +66,22 @@ pub mod validation { Ok(()) } + /// Validate CPI data when a blob hash may be present. If `has_hash` is true, + /// inline cpi_data can be empty; otherwise, it must be non-empty. + pub fn validate_cpi_data_or_hash(cpi_data: &[u8], has_hash: bool) -> Result<()> { + require!( + cpi_data.len() <= MAX_CPI_DATA_SIZE, + LazorKitError::CpiDataTooLarge + ); + if !has_hash { + require!( + !cpi_data.is_empty(), + LazorKitError::CpiDataMissing + ); + } + Ok(()) + } + /// Validate remaining accounts count pub fn validate_remaining_accounts(accounts: &[AccountInfo]) -> Result<()> { require!( diff --git a/programs/lazorkit/src/state/cpi_commit.rs b/programs/lazorkit/src/state/cpi_commit.rs new file mode 100644 index 0000000..40139ff --- /dev/null +++ b/programs/lazorkit/src/state/cpi_commit.rs @@ -0,0 +1,27 @@ +use anchor_lang::prelude::*; + +/// Commit record for a future CPI execution. +/// Created after full passkey + rule verification. Contains all bindings +/// necessary to perform the CPI later without re-verification. +#[account] +#[derive(InitSpace, Debug)] +pub struct CpiCommit { + /// Smart wallet that authorized this commit + pub owner_wallet: Pubkey, + /// sha256 of CPI instruction data + pub data_hash: [u8; 32], + /// sha256 over ordered remaining account metas plus `target_program` + pub accounts_hash: [u8; 32], + /// The nonce that was authorized at commit time (bound into data hash) + pub authorized_nonce: u64, + /// Unix expiration timestamp + pub expires_at: i64, + /// Where to refund rent when closing the commit + pub rent_refund_to: Pubkey, +} + +impl CpiCommit { + pub const PREFIX_SEED: &'static [u8] = b"cpi_commit"; +} + + diff --git a/programs/lazorkit/src/state/message.rs b/programs/lazorkit/src/state/message.rs index be49af4..a8ed8d2 100644 --- a/programs/lazorkit/src/state/message.rs +++ b/programs/lazorkit/src/state/message.rs @@ -1,10 +1,62 @@ use anchor_lang::prelude::*; +pub const MAX_TIMESTAMP_DRIFT_SECONDS: i64 = 30; + +pub trait Message { + fn verify(challenge_bytes: Vec, last_nonce: u64) -> Result<()>; +} + #[derive(Default, AnchorSerialize, AnchorDeserialize, Debug)] -pub struct Message { +pub struct ExecuteMessage { + pub nonce: u64, + pub current_timestamp: i64, + pub rule_data_hash: [u8; 32], + pub rule_accounts_hash: [u8; 32], + pub cpi_data_hash: [u8; 32], + pub cpi_accounts_hash: [u8; 32], +} + +#[derive(AnchorSerialize, AnchorDeserialize, Debug, Default, Clone)] +pub struct CallRuleMessage { pub nonce: u64, pub current_timestamp: i64, - pub split_index: u16, - pub rule_data: Option>, - pub cpi_data: Vec, + pub rule_data_hash: [u8; 32], + pub rule_accounts_hash: [u8; 32], } + +#[derive(AnchorSerialize, AnchorDeserialize, Debug, Default, Clone)] +pub struct ChangeRuleMessage { + pub nonce: u64, + pub current_timestamp: i64, + pub old_rule_data_hash: [u8; 32], + pub old_rule_accounts_hash: [u8; 32], + pub new_rule_data_hash: [u8; 32], + pub new_rule_accounts_hash: [u8; 32], +} + +macro_rules! impl_message_verify { + ($t:ty) => { + impl Message for $t { + fn verify(challenge_bytes: Vec, last_nonce: u64) -> Result<()> { + let hdr: $t = AnchorDeserialize::deserialize(&mut &challenge_bytes[..]) + .map_err(|_| crate::error::LazorKitError::ChallengeDeserializationError)?; + let now = Clock::get()?.unix_timestamp; + if hdr.current_timestamp < now.saturating_sub(MAX_TIMESTAMP_DRIFT_SECONDS) { + return Err(crate::error::LazorKitError::TimestampTooOld.into()); + } + if hdr.current_timestamp > now.saturating_add(MAX_TIMESTAMP_DRIFT_SECONDS) { + return Err(crate::error::LazorKitError::TimestampTooNew.into()); + } + require!( + hdr.nonce == last_nonce, + crate::error::LazorKitError::NonceMismatch + ); + Ok(()) + } + } + }; +} + +impl_message_verify!(ExecuteMessage); +impl_message_verify!(CallRuleMessage); +impl_message_verify!(ChangeRuleMessage); diff --git a/programs/lazorkit/src/state/mod.rs b/programs/lazorkit/src/state/mod.rs index 5b1d7a5..316d0bf 100644 --- a/programs/lazorkit/src/state/mod.rs +++ b/programs/lazorkit/src/state/mod.rs @@ -1,5 +1,6 @@ mod config; -mod message; +pub mod message; +mod cpi_commit; mod smart_wallet_authenticator; mod smart_wallet_config; // mod smart_wallet_seq; // No longer needed - using random IDs instead @@ -8,6 +9,7 @@ mod writer; pub use config::*; pub use message::*; +pub use cpi_commit::*; pub use smart_wallet_authenticator::*; pub use smart_wallet_config::*; // pub use smart_wallet_seq::*; // No longer needed - using random IDs instead diff --git a/programs/lazorkit/src/utils.rs b/programs/lazorkit/src/utils.rs index 6b5b7b8..c161cec 100644 --- a/programs/lazorkit/src/utils.rs +++ b/programs/lazorkit/src/utils.rs @@ -1,5 +1,5 @@ -use crate::constants::SECP256R1_ID; -use crate::state::Message; +use crate::constants::{PASSKEY_SIZE, SECP256R1_ID}; +use crate::state::{CallRuleMessage, ChangeRuleMessage, ExecuteMessage}; use crate::{error::LazorKitError, ID}; use anchor_lang::solana_program::{ instruction::Instruction, @@ -237,7 +237,7 @@ pub fn get_account_slice<'a>( } /// Helper: Create a PDA signer struct -pub fn get_pda_signer(passkey: &[u8; 33], wallet: Pubkey, bump: u8) -> PdaSigner { +pub fn get_pda_signer(passkey: &[u8; PASSKEY_SIZE], wallet: Pubkey, bump: u8) -> PdaSigner { PdaSigner { seeds: vec![ crate::state::SmartWalletAuthenticator::PREFIX_SEED.to_vec(), @@ -260,31 +260,23 @@ pub fn check_whitelist( Ok(()) } -/// Maximum allowed clock drift when validating the signed `Message`. -pub const MAX_TIMESTAMP_DRIFT_SECONDS: i64 = 30; - -/// Unified helper used by all instruction handlers to verify -/// 1. passkey matches the authenticator -/// 2. authenticator belongs to the smart-wallet -/// 3. secp256r1 signature & message integrity -/// 4. timestamp & nonce constraints -#[allow(clippy::too_many_arguments)] -pub fn verify_authorization( +/// Same as `verify_authorization` but deserializes the challenge payload into the +/// caller-provided type `T`. +pub fn verify_authorization( ix_sysvar: &AccountInfo, authenticator: &crate::state::SmartWalletAuthenticator, smart_wallet_key: Pubkey, - passkey_pubkey: [u8; 33], + passkey_pubkey: [u8; PASSKEY_SIZE], signature: Vec, client_data_json_raw: &[u8], authenticator_data_raw: &[u8], verify_instruction_index: u8, last_nonce: u64, -) -> Result { - use crate::state::Message; +) -> Result { use anchor_lang::solana_program::sysvar::instructions::load_instruction_at_checked; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; - // 1) passkey & wallet checks -------------------------------------------------------------- + // 1) passkey & wallet checks require!( authenticator.passkey_pubkey == passkey_pubkey, crate::error::LazorKitError::PasskeyMismatch @@ -294,7 +286,7 @@ pub fn verify_authorization( crate::error::LazorKitError::SmartWalletMismatch ); - // 2) locate the secp256r1 verify instruction ---------------------------------------------- + // 2) locate the secp256r1 verify instruction let secp_ix = load_instruction_at_checked(verify_instruction_index as usize, ix_sysvar)?; // 3) reconstruct signed message (authenticatorData || SHA256(clientDataJSON)) @@ -303,7 +295,7 @@ pub fn verify_authorization( message.extend_from_slice(authenticator_data_raw); message.extend_from_slice(client_hash.as_ref()); - // 4) parse the challenge from clientDataJSON --------------------------------------------- + // 4) parse the challenge from clientDataJSON let json_str = core::str::from_utf8(client_data_json_raw) .map_err(|_| crate::error::LazorKitError::ClientDataInvalidUtf8)?; let parsed: serde_json::Value = serde_json::from_str(json_str) @@ -312,32 +304,52 @@ pub fn verify_authorization( .as_str() .ok_or(crate::error::LazorKitError::ChallengeMissing)?; - // strip surrounding quotes, whitespace, slashes let challenge_clean = challenge.trim_matches(|c| c == '"' || c == '\'' || c == '/' || c == ' '); let challenge_bytes = URL_SAFE_NO_PAD .decode(challenge_clean) .map_err(|_| crate::error::LazorKitError::ChallengeBase64DecodeError)?; - let msg = Message::try_from_slice(&challenge_bytes) + verify_secp256r1_instruction(&secp_ix, authenticator.passkey_pubkey, message, signature)?; + // Verify header and return the typed message + M::verify(challenge_bytes.clone(), last_nonce)?; + let t: M = AnchorDeserialize::deserialize(&mut &challenge_bytes[..]) .map_err(|_| crate::error::LazorKitError::ChallengeDeserializationError)?; + Ok(t) +} - // 5) timestamp / nonce policy ------------------------------------------------------------- - let now = Clock::get()?.unix_timestamp; - if msg.current_timestamp < now.saturating_sub(MAX_TIMESTAMP_DRIFT_SECONDS) { - return Err(crate::error::LazorKitError::TimestampTooOld.into()); - } - if msg.current_timestamp > now.saturating_add(MAX_TIMESTAMP_DRIFT_SECONDS) { - return Err(crate::error::LazorKitError::TimestampTooNew.into()); - } - require!( - msg.nonce == last_nonce, - crate::error::LazorKitError::NonceMismatch - ); +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy)] +pub struct HeaderView { + pub nonce: u64, + pub current_timestamp: i64, +} - // 6) finally verify the secp256r1 signature ---------------------------------------------- - verify_secp256r1_instruction(&secp_ix, authenticator.passkey_pubkey, message, signature)?; +pub trait HasHeader { + fn header(&self) -> HeaderView; +} - Ok(msg) +impl HasHeader for ExecuteMessage { + fn header(&self) -> HeaderView { + HeaderView { + nonce: self.nonce, + current_timestamp: self.current_timestamp, + } + } +} +impl HasHeader for CallRuleMessage { + fn header(&self) -> HeaderView { + HeaderView { + nonce: self.nonce, + current_timestamp: self.current_timestamp, + } + } +} +impl HasHeader for ChangeRuleMessage { + fn header(&self) -> HeaderView { + HeaderView { + nonce: self.nonce, + current_timestamp: self.current_timestamp, + } + } } /// Helper: Split remaining accounts into `(rule_accounts, cpi_accounts)` using `split_index` coming from `Message`. diff --git a/sdk/default-rule-program.ts b/sdk/default-rule-program.ts deleted file mode 100644 index fba5873..0000000 --- a/sdk/default-rule-program.ts +++ /dev/null @@ -1,75 +0,0 @@ -import * as anchor from '@coral-xyz/anchor'; -import { DefaultRule } from '../target/types/default_rule'; -import IDL from '../target/idl/default_rule.json'; - -import * as constants from './constants'; - -export class DefaultRuleProgram { - private connection: anchor.web3.Connection; - private Idl: anchor.Idl = IDL as DefaultRule; - - constructor(connection: anchor.web3.Connection) { - this.connection = connection; - } - - get program(): anchor.Program { - return new anchor.Program(this.Idl, { - connection: this.connection, - }); - } - - get programId(): anchor.web3.PublicKey { - return this.program.programId; - } - - rule(smartWalletAuthenticator: anchor.web3.PublicKey): anchor.web3.PublicKey { - return anchor.web3.PublicKey.findProgramAddressSync( - [constants.RULE_SEED, smartWalletAuthenticator.toBuffer()], - this.programId - )[0]; - } - - async initRuleIns( - payer: anchor.web3.PublicKey, - smartWallet: anchor.web3.PublicKey, - smartWalletAuthenticator: anchor.web3.PublicKey - ) { - return await this.program.methods - .initRule() - .accountsPartial({ - payer, - smartWallet, - smartWalletAuthenticator, - rule: this.rule(smartWalletAuthenticator), - systemProgram: anchor.web3.SystemProgram.programId, - }) - .instruction(); - } - - async checkRuleIns(smartWalletAuthenticator: anchor.web3.PublicKey) { - return await this.program.methods - .checkRule() - .accountsPartial({ - rule: this.rule(smartWalletAuthenticator), - smartWalletAuthenticator, - }) - .instruction(); - } - - async addDeviceIns( - payer: anchor.web3.PublicKey, - smartWalletAuthenticator: anchor.web3.PublicKey, - newSmartWalletAuthenticator: anchor.web3.PublicKey - ) { - return await this.program.methods - .addDevice() - .accountsPartial({ - payer, - smartWalletAuthenticator, - newSmartWalletAuthenticator, - rule: this.rule(smartWalletAuthenticator), - newRule: this.rule(newSmartWalletAuthenticator), - }) - .instruction(); - } -} diff --git a/sdk/index.ts b/sdk/index.ts deleted file mode 100644 index 37b40cd..0000000 --- a/sdk/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -// Main SDK exports -export { LazorKitProgram } from './lazor-kit'; -export { DefaultRuleProgram } from './default-rule-program'; - -// Type exports -export * from './types'; - -// Utility exports -export * from './utils'; -export * from './constants'; - - -// Re-export commonly used Solana types for convenience -export { - Connection, - PublicKey, - Keypair, - Transaction, - VersionedTransaction, - TransactionInstruction, - TransactionMessage, - AddressLookupTableAccount, - AddressLookupTableProgram, -} from '@solana/web3.js'; \ No newline at end of file diff --git a/sdk/lazor-kit.ts b/sdk/lazor-kit.ts deleted file mode 100644 index 80e5164..0000000 --- a/sdk/lazor-kit.ts +++ /dev/null @@ -1,596 +0,0 @@ -import * as anchor from '@coral-xyz/anchor'; -import * as web3 from '@solana/web3.js'; -import IDL from '../target/idl/lazorkit.json'; -import * as bs58 from 'bs58'; -import { Lazorkit } from '../target/types/lazorkit'; -import * as constants from './constants'; -import { - createSecp256r1Instruction, - hashSeeds, - instructionToAccountMetas, -} from './utils'; -import * as types from './types'; -import { sha256 } from 'js-sha256'; -import { DefaultRuleProgram } from './default-rule-program'; -import { Buffer } from 'buffer'; -import { PublicKey } from '@solana/web3.js'; - -// Polyfill for structuredClone (e.g. React Native/Expo) -if (typeof globalThis.structuredClone !== 'function') { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore – minimal polyfill for non-circular data - globalThis.structuredClone = (obj: unknown) => - JSON.parse(JSON.stringify(obj)); -} - -export class LazorKitProgram { - readonly connection: anchor.web3.Connection; - readonly program: anchor.Program; - readonly programId: anchor.web3.PublicKey; - - // Caches for PDAs - private _config?: anchor.web3.PublicKey; - private _whitelistRulePrograms?: anchor.web3.PublicKey; - private _lookupTableAccount?: web3.AddressLookupTableAccount; - - readonly defaultRuleProgram: DefaultRuleProgram; - - constructor(connection: anchor.web3.Connection) { - this.connection = connection; - this.program = new anchor.Program(IDL as anchor.Idl, { - connection, - }) as unknown as anchor.Program; - this.programId = this.program.programId; - this.defaultRuleProgram = new DefaultRuleProgram(connection); - } - - // PDA getters - get config(): anchor.web3.PublicKey { - if (!this._config) { - this._config = anchor.web3.PublicKey.findProgramAddressSync( - [constants.CONFIG_SEED], - this.programId - )[0]; - } - return this._config; - } - - get whitelistRulePrograms(): anchor.web3.PublicKey { - if (!this._whitelistRulePrograms) { - this._whitelistRulePrograms = - anchor.web3.PublicKey.findProgramAddressSync( - [constants.WHITELIST_RULE_PROGRAMS_SEED], - this.programId - )[0]; - } - return this._whitelistRulePrograms; - } - - /** - * Get or fetch the address lookup table account - */ - async getLookupTableAccount(): Promise { - if (!this._lookupTableAccount) { - try { - const response = await this.connection.getAddressLookupTable( - constants.ADDRESS_LOOKUP_TABLE - ); - this._lookupTableAccount = response.value; - } catch (error) { - console.warn('Failed to fetch lookup table account:', error); - return null; - } - } - return this._lookupTableAccount; - } - - /** - * Generate a random wallet ID - * Uses timestamp + random number to minimize collision probability - */ - generateWalletId(): bigint { - // Use timestamp in milliseconds (lower 48 bits) - const timestamp = BigInt(Date.now()) & BigInt('0xFFFFFFFFFFFF'); - - // Generate random 16 bits - const randomPart = BigInt(Math.floor(Math.random() * 0xffff)); - - // Combine: timestamp (48 bits) + random (16 bits) = 64 bits - const walletId = (timestamp << BigInt(16)) | randomPart; - - // Ensure it's not zero (reserved) - return walletId === BigInt(0) ? BigInt(1) : walletId; - } - - /** - * Check if a wallet ID already exists on-chain - */ - async isWalletIdTaken(walletId: bigint): Promise { - try { - const smartWalletPda = this.smartWallet(walletId); - const accountInfo = await this.connection.getAccountInfo(smartWalletPda); - return accountInfo !== null; - } catch (error) { - // If there's an error checking, assume it's not taken - return false; - } - } - - /** - * Generate a unique wallet ID by checking for collisions - * Retries up to maxAttempts times if collisions are found - */ - async generateUniqueWalletId(maxAttempts: number = 10): Promise { - for (let attempt = 0; attempt < maxAttempts; attempt++) { - const walletId = this.generateWalletId(); - - // Check if this ID is already taken - const isTaken = await this.isWalletIdTaken(walletId); - - if (!isTaken) { - return walletId; - } - - // If taken, log and retry - console.warn( - `Wallet ID ${walletId} already exists, retrying... (attempt ${ - attempt + 1 - }/${maxAttempts})` - ); - - // Add small delay to avoid rapid retries - if (attempt < maxAttempts - 1) { - await new Promise((resolve) => setTimeout(resolve, 100)); - } - } - - throw new Error( - `Failed to generate unique wallet ID after ${maxAttempts} attempts` - ); - } - - async generateUniqueWalletIdWithRetry( - maxAttempts: number = 10 - ): Promise { - const walletId = await this.generateUniqueWalletId(maxAttempts); - return this.smartWallet(walletId); - } - - /** - * Find smart wallet PDA with given ID - */ - smartWallet(walletId: bigint): anchor.web3.PublicKey { - const idBytes = new Uint8Array(8); - const view = new DataView(idBytes.buffer); - view.setBigUint64(0, walletId, true); // little-endian - - return anchor.web3.PublicKey.findProgramAddressSync( - [constants.SMART_WALLET_SEED, idBytes], - this.programId - )[0]; - } - - smartWalletConfig(smartWallet: anchor.web3.PublicKey) { - return anchor.web3.PublicKey.findProgramAddressSync( - [constants.SMART_WALLET_CONFIG_SEED, smartWallet.toBuffer()], - this.programId - )[0]; - } - - smartWalletAuthenticator( - passkeyPubkey: number[], - smartWallet: anchor.web3.PublicKey - ) { - const hashedPasskey = hashSeeds(passkeyPubkey, smartWallet); - return anchor.web3.PublicKey.findProgramAddressSync( - [ - constants.SMART_WALLET_AUTHENTICATOR_SEED, - smartWallet.toBuffer(), - hashedPasskey, - ], - this.programId - ); - } - - // async methods - - async getConfigData(): Promise { - return await this.program.account.config.fetch(this.config); - } - - async getSmartWalletConfigData(smartWallet: anchor.web3.PublicKey) { - const config = this.smartWalletConfig(smartWallet); - return await this.program.account.smartWalletConfig.fetch(config); - } - - async getSmartWalletAuthenticatorData( - smartWalletAuthenticator: anchor.web3.PublicKey - ) { - return await this.program.account.smartWalletAuthenticator.fetch( - smartWalletAuthenticator - ); - } - - // Helper method to create versioned transactions - private async createVersionedTransaction( - instructions: web3.TransactionInstruction[], - payer: anchor.web3.PublicKey - ): Promise { - const lookupTableAccount = await this.getLookupTableAccount(); - const { blockhash } = await this.connection.getLatestBlockhash(); - - // Create v0 compatible transaction message - const messageV0 = new web3.TransactionMessage({ - payerKey: payer, - recentBlockhash: blockhash, - instructions, - }).compileToV0Message(lookupTableAccount ? [lookupTableAccount] : []); - - // Create v0 transaction from the v0 message - return new web3.VersionedTransaction(messageV0); - } - - // txn methods - - async initializeTxn( - payer: anchor.web3.PublicKey - ): Promise { - const ix = await this.program.methods - .initialize() - .accountsPartial({ - signer: payer, - config: this.config, - whitelistRulePrograms: this.whitelistRulePrograms, - defaultRuleProgram: this.defaultRuleProgram.programId, - systemProgram: anchor.web3.SystemProgram.programId, - }) - .instruction(); - return new anchor.web3.Transaction().add(ix); - } - - async updateConfigTxn( - authority: anchor.web3.PublicKey, - param: types.UpdateConfigType, - value: number, - remainingAccounts: anchor.web3.AccountMeta[] = [] - ): Promise { - const ix = await this.program.methods - .updateConfig(param, new anchor.BN(value)) - .accountsPartial({ - authority, - config: this._config ?? this.config, - }) - .remainingAccounts(remainingAccounts) - .instruction(); - return new anchor.web3.Transaction().add(ix); - } - - /** - * Create smart wallet with automatic collision detection - */ - async createSmartWalletTxn( - passkeyPubkey: number[], - payer: anchor.web3.PublicKey, - credentialId: string = '', - ruleIns: anchor.web3.TransactionInstruction | null = null, - walletId?: bigint, - isPayForUser: boolean = false - ): Promise<{ - transaction: anchor.web3.Transaction; - walletId: bigint; - smartWallet: anchor.web3.PublicKey; - }> { - // Generate unique ID if not provided - const id = walletId ?? (await this.generateUniqueWalletId()); - - const smartWallet = this.smartWallet(id); - const [smartWalletAuthenticator] = this.smartWalletAuthenticator( - passkeyPubkey, - smartWallet - ); - - // If caller does not provide a rule instruction, default to initRule of DefaultRuleProgram - const ruleInstruction = - ruleIns || - (await this.defaultRuleProgram.initRuleIns( - payer, - smartWallet, - smartWalletAuthenticator - )); - - const remainingAccounts = instructionToAccountMetas(ruleInstruction, payer); - - const createSmartWalletIx = await this.program.methods - .createSmartWallet( - passkeyPubkey, - Buffer.from(credentialId, 'base64'), - ruleInstruction.data, - new anchor.BN(id.toString()), - isPayForUser - ) - .accountsPartial({ - signer: payer, - whitelistRulePrograms: this.whitelistRulePrograms, - smartWallet, - smartWalletConfig: this.smartWalletConfig(smartWallet), - smartWalletAuthenticator, - config: this.config, - systemProgram: anchor.web3.SystemProgram.programId, - defaultRuleProgram: this.defaultRuleProgram.programId, - }) - .remainingAccounts(remainingAccounts) - .instruction(); - - const tx = new anchor.web3.Transaction().add(createSmartWalletIx); - tx.feePayer = payer; - tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash; - - return { - transaction: tx, - walletId: id, - smartWallet, - }; - } - - /** - * Create smart wallet with retry logic for collision handling - */ - async createSmartWalletWithRetry( - passkeyPubkey: number[], - payer: anchor.web3.PublicKey, - credentialId: string = '', - ruleIns: anchor.web3.TransactionInstruction | null = null, - maxRetries: number = 3 - ): Promise<{ - transaction: web3.Transaction; - walletId: bigint; - smartWallet: anchor.web3.PublicKey; - }> { - let lastError: Error | null = null; - - for (let attempt = 0; attempt < maxRetries; attempt++) { - try { - return await this.createSmartWalletTxn( - passkeyPubkey, - payer, - credentialId, - ruleIns - ); - } catch (error) { - lastError = error as Error; - - // Check if this is a collision error (account already exists) - if ( - error instanceof Error && - error.message.includes('already in use') - ) { - console.warn( - `Wallet creation failed due to collision, retrying... (attempt ${ - attempt + 1 - }/${maxRetries})` - ); - continue; - } - - // If it's not a collision error, don't retry - throw error; - } - } - - throw new Error( - `Failed to create smart wallet after ${maxRetries} attempts. Last error: ${lastError?.message}` - ); - } - - async executeInstructionTxn( - passkeyPubkey: number[], - clientDataJsonRaw: Buffer, - authenticatorDataRaw: Buffer, - signature: Buffer, - payer: anchor.web3.PublicKey, - smartWallet: anchor.web3.PublicKey, - cpiIns: anchor.web3.TransactionInstruction, - ruleIns: anchor.web3.TransactionInstruction | null = null, - action: types.ExecuteActionType = types.ExecuteAction.ExecuteTx, - newPasskey: number[] | null = null, - verifyInstructionIndex: number = 0 - ): Promise { - const [smartWalletAuthenticator] = this.smartWalletAuthenticator( - passkeyPubkey, - smartWallet - ); - const smartWalletConfig = this.smartWalletConfig(smartWallet); - const smartWalletConfigData = await this.getSmartWalletConfigData( - smartWallet - ); - - const remainingAccounts: anchor.web3.AccountMeta[] = []; - - let ruleInstruction: anchor.web3.TransactionInstruction | null = null; - - if (action == types.ExecuteAction.ExecuteTx) { - if (!ruleIns) { - ruleInstruction = await this.defaultRuleProgram.checkRuleIns( - smartWalletAuthenticator - ); - } else { - ruleInstruction = ruleIns; - } - } else if (action == types.ExecuteAction.ChangeRuleProgram) { - if (!ruleIns) { - throw new Error('Rule instruction is required'); - } - ruleInstruction = ruleIns; - } - - if (ruleInstruction) { - remainingAccounts.push( - ...instructionToAccountMetas(ruleInstruction, payer) - ); - } - - remainingAccounts.push(...instructionToAccountMetas(cpiIns, payer)); - - const message = Buffer.concat([ - authenticatorDataRaw, - Buffer.from(sha256.arrayBuffer(clientDataJsonRaw)), - ]); - - const verifySignatureIx = createSecp256r1Instruction( - message, - Buffer.from(passkeyPubkey), - signature - ); - - const executeInstructionIx = await this.program.methods - .execute({ - passkeyPubkey, - signature, - clientDataJsonRaw, - authenticatorDataRaw, - verifyInstructionIndex, - action, - createNewAuthenticator: newPasskey, - }) - .accountsPartial({ - payer, - config: this.config, - smartWallet, - smartWalletConfig: smartWalletConfig, - smartWalletAuthenticator, - whitelistRulePrograms: this.whitelistRulePrograms, - authenticatorProgram: smartWalletConfigData.ruleProgram, - ixSysvar: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, - systemProgram: anchor.web3.SystemProgram.programId, - cpiProgram: cpiIns - ? cpiIns.programId - : anchor.web3.SystemProgram.programId, - newSmartWalletAuthenticator: newPasskey - ? new anchor.web3.PublicKey( - this.smartWalletAuthenticator(newPasskey, smartWallet)[0] - ) - : anchor.web3.SystemProgram.programId, - }) - .remainingAccounts(remainingAccounts) - .instruction(); - - return this.createVersionedTransaction( - [verifySignatureIx, executeInstructionIx], - payer - ); - } - - /** - * Query the chain for the smart-wallet associated with a passkey. - */ - async getSmartWalletByPasskey(passkeyPubkey: number[]): Promise<{ - smartWallet: anchor.web3.PublicKey | null; - smartWalletAuthenticator: anchor.web3.PublicKey | null; - }> { - const discriminator = (IDL as any).accounts.find( - (a: any) => a.name === 'SmartWalletAuthenticator' - )!.discriminator; - - const accounts = await this.connection.getProgramAccounts(this.programId, { - dataSlice: { - offset: 8, - length: 33, - }, - filters: [ - { memcmp: { offset: 0, bytes: bs58.encode(discriminator) } }, - { memcmp: { offset: 8, bytes: bs58.encode(passkeyPubkey) } }, - ], - }); - - if (accounts.length === 0) { - return { smartWalletAuthenticator: null, smartWallet: null }; - } - - const smartWalletAuthenticatorData = - await this.getSmartWalletAuthenticatorData(accounts[0].pubkey); - - return { - smartWalletAuthenticator: accounts[0].pubkey, - smartWallet: smartWalletAuthenticatorData.smartWallet, - }; - } - - /** - * Build the serialized Message struct used for signing requests. - */ - async getMessage( - smartWalletString: string, - ruleIns: anchor.web3.TransactionInstruction | null = null, - smartWalletAuthenticatorString: string, - cpiInstruction: anchor.web3.TransactionInstruction, - executeAction: types.ExecuteActionType - ): Promise { - const smartWallet = new anchor.web3.PublicKey(smartWalletString); - const smartWalletData = await this.getSmartWalletConfigData(smartWallet); - - let ruleInstruction: anchor.web3.TransactionInstruction | null = null; - - if (executeAction == types.ExecuteAction.ChangeRuleProgram) { - if (!ruleIns) { - throw new Error('Rule instruction is required'); - } - ruleInstruction = ruleIns; - } else if (executeAction == types.ExecuteAction.ExecuteTx) { - if (!ruleIns) { - ruleInstruction = await this.defaultRuleProgram.checkRuleIns( - smartWallet - ); - } else { - ruleInstruction = ruleIns; - } - } - - // Manually serialize the message struct: - // - nonce (u64): 8 bytes - // - current_timestamp (i64): 8 bytes (unix seconds) - // - split_index (u16): 2 bytes - // - rule_data (Option>): 1 byte (Some/None) + 4 bytes length + data bytes (if Some) - // - cpi_data (Vec): 4 bytes length + data bytes - - const currentTimestamp = Math.floor(Date.now() / 1000); - - // Calculate buffer size based on whether rule_data is provided - const ruleDataLength = ruleInstruction ? ruleInstruction.data.length : 0; - const ruleDataSize = ruleInstruction ? 5 + ruleDataLength : 1; // 1 byte for Option + 4 bytes length + data (if Some) - const buffer = Buffer.alloc( - 18 + ruleDataSize + 4 + cpiInstruction.data.length - ); - - // Write nonce as little-endian u64 (bytes 0-7) - buffer.writeBigUInt64LE(BigInt(smartWalletData.lastNonce.toString()), 0); - - // Write current_timestamp as little-endian i64 (bytes 8-15) - buffer.writeBigInt64LE(BigInt(currentTimestamp), 8); - - // Write split_index as little-endian u16 (bytes 16-17) - const splitIndex = ruleInstruction ? ruleInstruction.keys.length : 0; - buffer.writeUInt16LE(splitIndex, 16); - - // Write rule_data (Option>) - if (ruleInstruction) { - // Write Some variant (1 byte) - buffer.writeUInt8(1, 18); - // Write rule_data length as little-endian u32 (bytes 19-22) - buffer.writeUInt32LE(ruleInstruction.data.length, 19); - // Write rule_data bytes (starting at byte 23) - ruleInstruction.data.copy(buffer, 23); - } else { - // Write None variant (1 byte) - buffer.writeUInt8(0, 18); - } - - // Write cpi_data length as little-endian u32 - const cpiDataOffset = 18 + ruleDataSize; - buffer.writeUInt32LE(cpiInstruction.data.length, cpiDataOffset); - - // Write cpi_data bytes - cpiInstruction.data.copy(buffer, cpiDataOffset + 4); - - return buffer; - } -} diff --git a/sdk/types.ts b/sdk/types.ts deleted file mode 100644 index b337d20..0000000 --- a/sdk/types.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as anchor from '@coral-xyz/anchor'; - -import { Lazorkit } from '../target/types/lazorkit'; - -// Account types -export type SmartWalletConfig = anchor.IdlTypes['smartWalletConfig']; -export type SmartWalletAuthenticator = - anchor.IdlTypes['smartWalletAuthenticator']; -export type Config = anchor.IdlTypes['config']; -export type WhitelistRulePrograms = anchor.IdlTypes['whitelistRulePrograms']; - -// Enum types -export type UpdateConfigType = anchor.IdlTypes['updateConfigType']; -export type ExecuteActionType = anchor.IdlTypes['action']; - -// Action constants -export const ExecuteAction = { - ExecuteTx: { executeTx: {} }, - ChangeRuleProgram: { changeRuleProgram: {} }, - CallRuleProgram: { callRuleProgram: {} }, -}; diff --git a/sdk/utils.ts b/sdk/utils.ts deleted file mode 100644 index 05da2a1..0000000 --- a/sdk/utils.ts +++ /dev/null @@ -1,200 +0,0 @@ -import * as anchor from '@coral-xyz/anchor'; -import { sha256 } from 'js-sha256'; - -export function hashSeeds( - passkey: number[], - smartWallet: anchor.web3.PublicKey -): Buffer { - const rawBuffer = Buffer.concat([ - Buffer.from(passkey), - smartWallet.toBuffer(), - ]); - const hash = sha256.arrayBuffer(rawBuffer); - return Buffer.from(hash).subarray(0, 32); -} - -// Constants from the Rust code -const SIGNATURE_OFFSETS_SERIALIZED_SIZE = 14; -const SIGNATURE_OFFSETS_START = 2; -const DATA_START = SIGNATURE_OFFSETS_SERIALIZED_SIZE + SIGNATURE_OFFSETS_START; -const SIGNATURE_SERIALIZED_SIZE: number = 64; -const COMPRESSED_PUBKEY_SERIALIZED_SIZE = 33; -const FIELD_SIZE = 32; - -const SECP256R1_NATIVE_PROGRAM = new anchor.web3.PublicKey( - 'Secp256r1SigVerify1111111111111111111111111' -); - -// Order of secp256r1 curve (same as in Rust code) -const SECP256R1_ORDER = new Uint8Array([ - 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, - 0xff, 0xff, 0xff, 0xbc, 0xe6, 0xfa, 0xad, 0xa7, 0x17, 0x9e, 0x84, 0xf3, 0xb9, - 0xca, 0xc2, 0xfc, 0x63, 0x25, 0x51, -]); - -// Half order of secp256r1 curve (same as in Rust code) -const SECP256R1_HALF_ORDER = new Uint8Array([ - 0x7f, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, - 0xff, 0xff, 0xff, 0xde, 0x73, 0x7d, 0x56, 0xd3, 0x8b, 0xcf, 0x42, 0x79, 0xdc, - 0xe5, 0x61, 0x7e, 0x31, 0x92, 0xa8, -]); - -interface Secp256r1SignatureOffsets { - signature_offset: number; - signature_instruction_index: number; - public_key_offset: number; - public_key_instruction_index: number; - message_data_offset: number; - message_data_size: number; - message_instruction_index: number; -} - -function bytesOf(data: any): Uint8Array { - if (data instanceof Uint8Array) { - return data; - } else if (Array.isArray(data)) { - return new Uint8Array(data); - } else { - // Convert object to buffer using DataView for consistent byte ordering - const buffer = new ArrayBuffer(Object.values(data).length * 2); - const view = new DataView(buffer); - Object.values(data).forEach((value, index) => { - view.setUint16(index * 2, value as number, true); - }); - return new Uint8Array(buffer); - } -} - -// Compare two big numbers represented as Uint8Arrays -function isGreaterThan(a: Uint8Array, b: Uint8Array): boolean { - if (a.length !== b.length) { - return a.length > b.length; - } - for (let i = 0; i < a.length; i++) { - if (a[i] !== b[i]) { - return a[i] > b[i]; - } - } - return false; -} - -// Subtract one big number from another (a - b), both represented as Uint8Arrays -function subtractBigNumbers(a: Uint8Array, b: Uint8Array): Uint8Array { - const result = new Uint8Array(a.length); - let borrow = 0; - - for (let i = a.length - 1; i >= 0; i--) { - let diff = a[i] - b[i] - borrow; - if (diff < 0) { - diff += 256; - borrow = 1; - } else { - borrow = 0; - } - result[i] = diff; - } - - return result; -} - -export function createSecp256r1Instruction( - message: Uint8Array, - pubkey: Buffer, - signature: Buffer -): anchor.web3.TransactionInstruction { - try { - // Ensure signature is the correct length - if (signature.length !== SIGNATURE_SERIALIZED_SIZE) { - // Extract r and s from the signature - const r = signature.slice(0, FIELD_SIZE); - const s = signature.slice(FIELD_SIZE, FIELD_SIZE * 2); - - // Pad r and s to correct length if needed - const paddedR = Buffer.alloc(FIELD_SIZE, 0); - const paddedS = Buffer.alloc(FIELD_SIZE, 0); - r.copy(paddedR, FIELD_SIZE - r.length); - s.copy(paddedS, FIELD_SIZE - s.length); - - // Check if s > half_order, if so, compute s = order - s - if (isGreaterThan(paddedS, SECP256R1_HALF_ORDER)) { - const newS = subtractBigNumbers(SECP256R1_ORDER, paddedS); - signature = Buffer.concat([paddedR, Buffer.from(newS)]); - } else { - signature = Buffer.concat([paddedR, paddedS]); - } - } - - // Verify lengths - if ( - pubkey.length !== COMPRESSED_PUBKEY_SERIALIZED_SIZE || - signature.length !== SIGNATURE_SERIALIZED_SIZE - ) { - throw new Error('Invalid key or signature length'); - } - - // Calculate total size and create instruction data - const totalSize = - DATA_START + - SIGNATURE_SERIALIZED_SIZE + - COMPRESSED_PUBKEY_SERIALIZED_SIZE + - message.length; - - const instructionData = new Uint8Array(totalSize); - - // Calculate offsets - const numSignatures: number = 1; - const publicKeyOffset = DATA_START; - const signatureOffset = publicKeyOffset + COMPRESSED_PUBKEY_SERIALIZED_SIZE; - const messageDataOffset = signatureOffset + SIGNATURE_SERIALIZED_SIZE; - - // Write number of signatures - instructionData.set(bytesOf([numSignatures, 0]), 0); - - // Create and write offsets - const offsets: Secp256r1SignatureOffsets = { - signature_offset: signatureOffset, - signature_instruction_index: 0xffff, // u16::MAX - public_key_offset: publicKeyOffset, - public_key_instruction_index: 0xffff, - message_data_offset: messageDataOffset, - message_data_size: message.length, - message_instruction_index: 0xffff, - }; - - // Write all components - instructionData.set(bytesOf(offsets), SIGNATURE_OFFSETS_START); - instructionData.set(pubkey, publicKeyOffset); - instructionData.set(signature, signatureOffset); - instructionData.set(message, messageDataOffset); - - return new anchor.web3.TransactionInstruction({ - keys: [], - programId: SECP256R1_NATIVE_PROGRAM, - data: Buffer.from(instructionData), - }); - } catch (error) { - throw new Error(`Failed to create secp256r1 instruction: ${error}`); - } -} - -/** - * Convenience helper: convert a {@link anchor.web3.TransactionInstruction}'s `keys` - * array into the `AccountMeta` objects Anchor expects for - * `remainingAccounts(...)`. - * - * The mapping uses the original `isWritable` flag from the instruction and - * marks the account as a signer if either: - * • the instruction already flagged it as signer, or - * • the account equals the provided {@link payer} (the wallet paying for the - * transaction). - */ -export function instructionToAccountMetas( - ix: anchor.web3.TransactionInstruction, - payer: anchor.web3.PublicKey -): anchor.web3.AccountMeta[] { - return ix.keys.map((k) => ({ - pubkey: k.pubkey, - isWritable: k.isWritable, - isSigner: k.pubkey.equals(payer), - })); -} diff --git a/tests/smart_wallet_with_default_rule.test.ts b/tests/smart_wallet_with_default_rule.test.ts index 74c682e..d59281a 100644 --- a/tests/smart_wallet_with_default_rule.test.ts +++ b/tests/smart_wallet_with_default_rule.test.ts @@ -4,8 +4,7 @@ import { expect } from 'chai'; import { LAMPORTS_PER_SOL, sendAndConfirmTransaction } from '@solana/web3.js'; import * as dotenv from 'dotenv'; import { base64, bs58 } from '@coral-xyz/anchor/dist/cjs/utils/bytes'; -import { LazorKitProgram } from '../sdk/lazor-kit'; -import { DefaultRuleProgram } from '../sdk/default-rule-program'; +import { LazorkitClient, DefaultRuleClient } from '../contract-integration'; dotenv.config(); describe('Test smart wallet with default rule', () => { @@ -14,19 +13,17 @@ describe('Test smart wallet with default rule', () => { 'confirmed' ); - const lazorkitProgram = new LazorKitProgram(connection); - - const defaultRuleProgram = new DefaultRuleProgram(connection); + const lazorkitProgram = new LazorkitClient(connection); const payer = anchor.web3.Keypair.fromSecretKey( - bs58.decode(process.env.MAINNET_DEPLOYER_PRIVATE_KEY!) + bs58.decode(process.env.PRIVATE_KEY!) ); before(async () => { // airdrop some SOL to the payer const programConfig = await connection.getAccountInfo( - lazorkitProgram.config + lazorkitProgram.configPda() ); if (programConfig === null) { @@ -41,38 +38,30 @@ describe('Test smart wallet with default rule', () => { } }); - it('Initialize successfully', async () => { + it('Init smart wallet with default rule successfully', async () => { const privateKey = ECDSA.generateKey(); const publicKeyBase64 = privateKey.toCompressedPublicKey(); - const pubkey = Array.from(Buffer.from(publicKeyBase64, 'base64')); + const passkeyPubkey = Array.from(Buffer.from(publicKeyBase64, 'base64')); const smartWalletId = lazorkitProgram.generateWalletId(); - const smartWallet = lazorkitProgram.smartWallet(smartWalletId); - - const [smartWalletAuthenticator] = lazorkitProgram.smartWalletAuthenticator( - pubkey, - smartWallet - ); + const smartWallet = lazorkitProgram.smartWalletPda(smartWalletId); - const initRuleIns = await defaultRuleProgram.initRuleIns( - payer.publicKey, - smartWallet, - smartWalletAuthenticator - ); + const smartWalletAuthenticator = + lazorkitProgram.smartWalletAuthenticatorPda(smartWallet, passkeyPubkey); const credentialId = base64.encode(Buffer.from('testing something')); // random string const { transaction: createSmartWalletTxn } = - await lazorkitProgram.createSmartWalletTxn( - pubkey, - payer.publicKey, - credentialId, - initRuleIns, + await lazorkitProgram.createSmartWalletTx({ + payer: payer.publicKey, + passkeyPubkey, + credentialIdBase64: credentialId, + ruleInstruction: null, + isPayForUser: true, smartWalletId, - true - ); + }); const sig = await sendAndConfirmTransaction( connection, @@ -98,7 +87,7 @@ describe('Test smart wallet with default rule', () => { ); expect(smartWalletAuthenticatorData.passkeyPubkey.toString()).to.be.equal( - pubkey.toString() + passkeyPubkey.toString() ); expect(smartWalletAuthenticatorData.smartWallet.toString()).to.be.equal( smartWallet.toString() @@ -130,8 +119,8 @@ describe('Test smart wallet with default rule', () => { authority: payer.publicKey, lookupTable: lookupTableAddress, addresses: [ - lazorkitProgram.config, - lazorkitProgram.whitelistRulePrograms, + lazorkitProgram.configPda(), + lazorkitProgram.whitelistRuleProgramsPda(), lazorkitProgram.defaultRuleProgram.programId, anchor.web3.SystemProgram.programId, anchor.web3.SYSVAR_RENT_PUBKEY,