From 938c1e331d971a8045707fe79634f3a2b34f076d Mon Sep 17 00:00:00 2001 From: onspeedhp Date: Sat, 9 Aug 2025 04:24:07 +0700 Subject: [PATCH 1/6] Enhance LazorKit program with new commit and execute functionality for CPI transactions. Update Anchor.toml to specify anchor version and adjust cluster configuration. Introduce validation for CPI data and add new methods for committing and executing CPIs. Update tests to validate new functionality and improve overall code clarity. --- Anchor.toml | 9 +- .../lazorkit/src/instructions/commit_cpi.rs | 171 ++++++++++++++++ programs/lazorkit/src/instructions/execute.rs | 2 + .../src/instructions/execute_committed.rs | 188 ++++++++++++++++++ .../src/instructions/handlers/execute_tx.rs | 11 +- programs/lazorkit/src/instructions/mod.rs | 6 +- programs/lazorkit/src/lib.rs | 10 + programs/lazorkit/src/security.rs | 16 ++ programs/lazorkit/src/state/cpi_commit.rs | 29 +++ programs/lazorkit/src/state/message.rs | 2 +- programs/lazorkit/src/state/mod.rs | 2 + sdk/constants.ts | 1 + sdk/lazor-kit.ts | 172 ++++++++++++++-- tests/smart_wallet_with_default_rule.test.ts | 72 ++++++- tests/utils.ts | 10 +- 15 files changed, 670 insertions(+), 31 deletions(-) create mode 100644 programs/lazorkit/src/instructions/commit_cpi.rs create mode 100644 programs/lazorkit/src/instructions/execute_committed.rs create mode 100644 programs/lazorkit/src/state/cpi_commit.rs 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/programs/lazorkit/src/instructions/commit_cpi.rs b/programs/lazorkit/src/instructions/commit_cpi.rs new file mode 100644 index 0000000..1ca3180 --- /dev/null +++ b/programs/lazorkit/src/instructions/commit_cpi.rs @@ -0,0 +1,171 @@ +use anchor_lang::prelude::*; + +use crate::security::validation; +use crate::state::{ + Config, CpiCommit, SmartWalletAuthenticator, SmartWalletConfig, WhitelistRulePrograms, +}; +use crate::utils::{execute_cpi, get_pda_signer, sighash, verify_authorization, PasskeyExt}; +use crate::{constants::SMART_WALLET_SEED, error::LazorKitError, ID}; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct CommitArgs { + 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 split_index: u16, + pub rule_data: Option>, + pub cpi_program: Pubkey, + pub cpi_accounts_hash: [u8; 32], + pub cpi_data_hash: [u8; 32], + pub expires_at: i64, +} + +pub fn commit_cpi(ctx: Context, args: CommitArgs) -> Result<()> { + // Validate + validation::validate_remaining_accounts(&ctx.remaining_accounts)?; + if let Some(ref rule_data) = args.rule_data { + validation::validate_rule_data(rule_data)?; + } + // No CPI bytes stored in commit mode + + // Program not paused + require!(!ctx.accounts.config.is_paused, LazorKitError::ProgramPaused); + + // Authorization + 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, + )?; + + // Optionally rule-check now (binds policy at commit time) + if let Some(ref rule_data) = args.rule_data { + // First part of remaining accounts are for the rule program + let split = msg.split_index as usize; + require!( + split <= ctx.remaining_accounts.len(), + LazorKitError::InvalidSplitIndex + ); + let rule_accounts = &ctx.remaining_accounts[..split]; + // 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(), + )?; + + let rule_signer = get_pda_signer( + &args.passkey_pubkey, + ctx.accounts.smart_wallet.key(), + ctx.accounts.smart_wallet_authenticator.bump, + ); + // Ensure discriminator is check_rule + require!( + rule_data.get(0..8) == Some(&sighash("global", "check_rule")), + LazorKitError::InvalidCheckRuleDiscriminator + ); + execute_cpi( + rule_accounts, + rule_data, + &ctx.accounts.authenticator_program, + Some(rule_signer), + )?; + } + + // Write commit + let commit = &mut ctx.accounts.cpi_commit; + commit.owner_wallet = ctx.accounts.smart_wallet.key(); + commit.target_program = args.cpi_program; + commit.data_hash = args.cpi_data_hash; + commit.accounts_hash = args.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 + 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(), &args.cpi_data_hash], + 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.rs b/programs/lazorkit/src/instructions/execute.rs index 647e96d..7dd3477 100644 --- a/programs/lazorkit/src/instructions/execute.rs +++ b/programs/lazorkit/src/instructions/execute.rs @@ -268,4 +268,6 @@ pub struct Execute<'info> { /// by the `CallRuleProgram` action. It is passed as an UncheckedAccount /// and created via CPI if needed. pub new_smart_wallet_authenticator: Option>, + + // No blob in this path } diff --git a/programs/lazorkit/src/instructions/execute_committed.rs b/programs/lazorkit/src/instructions/execute_committed.rs new file mode 100644 index 0000000..36d0ae8 --- /dev/null +++ b/programs/lazorkit/src/instructions/execute_committed.rs @@ -0,0 +1,188 @@ +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}; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct ExecuteCommittedArgs { + /// Full CPI instruction data submitted at execution time + pub cpi_data: Vec, +} + +pub fn execute_committed(ctx: Context, args: ExecuteCommittedArgs) -> 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() + || commit.target_program != ctx.accounts.cpi_program.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(&args.cpi_data).to_bytes(); + if data_hash != commit.data_hash { + return Ok(()); + } + + 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!( + ctx.remaining_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)?, + ); + + // 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, + &args.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/handlers/execute_tx.rs b/programs/lazorkit/src/instructions/handlers/execute_tx.rs index c5aae01..4f4a694 100644 --- a/programs/lazorkit/src/instructions/handlers/execute_tx.rs +++ b/programs/lazorkit/src/instructions/handlers/execute_tx.rs @@ -73,7 +73,7 @@ pub fn handle<'c: 'info, 'info>( msg!("Rule check passed"); - // 6. Execute main CPI or transfer lamports + // 6. Execute main CPI or transfer lamports (inline data) if msg.cpi_data.get(0..4) == Some(&SOL_TRANSFER_DISCRIMINATOR) && ctx.accounts.cpi_program.key() == anchor_lang::solana_program::system_program::ID { @@ -150,14 +150,9 @@ pub fn handle<'c: 'info, 'info>( msg!("Executing CPI to program: {}", ctx.accounts.cpi_program.key()); - execute_cpi( - cpi_accounts, - &msg.cpi_data, - &ctx.accounts.cpi_program, - Some(wallet_signer), - )?; + 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/mod.rs b/programs/lazorkit/src/instructions/mod.rs index aae657b..deffa77 100644 --- a/programs/lazorkit/src/instructions/mod.rs +++ b/programs/lazorkit/src/instructions/mod.rs @@ -3,9 +3,13 @@ mod execute; mod handlers; mod initialize; mod admin; +mod commit_cpi; +mod execute_committed; pub use create_smart_wallet::*; pub use execute::*; pub use initialize::*; pub use admin::*; -pub use handlers::*; \ No newline at end of file +pub use handlers::*; +pub use commit_cpi::*; +pub use execute_committed::*; \ No newline at end of file diff --git a/programs/lazorkit/src/lib.rs b/programs/lazorkit/src/lib.rs index 645dac4..3f18082 100644 --- a/programs/lazorkit/src/lib.rs +++ b/programs/lazorkit/src/lib.rs @@ -64,4 +64,14 @@ pub mod lazorkit { pub fn add_whitelist_rule_program(ctx: Context) -> Result<()> { instructions::add_whitelist_rule_program(ctx) } + + /// Commit a CPI after verifying auth and rule. Stores data and constraints. + pub fn commit_cpi(ctx: Context, args: CommitArgs) -> Result<()> { + instructions::commit_cpi(ctx, args) + } + + /// Execute a previously committed CPI (no passkey verification here). + pub fn execute_committed(ctx: Context, args: ExecuteCommittedArgs) -> Result<()> { + instructions::execute_committed(ctx, args) + } } 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..b4b85a1 --- /dev/null +++ b/programs/lazorkit/src/state/cpi_commit.rs @@ -0,0 +1,29 @@ +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, + /// Target program id for the CPI + pub target_program: 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..322faa8 100644 --- a/programs/lazorkit/src/state/message.rs +++ b/programs/lazorkit/src/state/message.rs @@ -1,10 +1,10 @@ use anchor_lang::prelude::*; - #[derive(Default, AnchorSerialize, AnchorDeserialize, Debug)] pub struct Message { pub nonce: u64, pub current_timestamp: i64, pub split_index: u16, pub rule_data: Option>, + /// Direct CPI data fallback when no blob is used. pub cpi_data: Vec, } diff --git a/programs/lazorkit/src/state/mod.rs b/programs/lazorkit/src/state/mod.rs index 5b1d7a5..2ea21eb 100644 --- a/programs/lazorkit/src/state/mod.rs +++ b/programs/lazorkit/src/state/mod.rs @@ -1,5 +1,6 @@ mod config; 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/sdk/constants.ts b/sdk/constants.ts index 6ac1eec..a1409cc 100644 --- a/sdk/constants.ts +++ b/sdk/constants.ts @@ -11,6 +11,7 @@ export const WHITELIST_RULE_PROGRAMS_SEED = Buffer.from( ); 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/sdk/lazor-kit.ts b/sdk/lazor-kit.ts index 80e5164..e7fc413 100644 --- a/sdk/lazor-kit.ts +++ b/sdk/lazor-kit.ts @@ -1,5 +1,4 @@ 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'; @@ -31,7 +30,7 @@ export class LazorKitProgram { // Caches for PDAs private _config?: anchor.web3.PublicKey; private _whitelistRulePrograms?: anchor.web3.PublicKey; - private _lookupTableAccount?: web3.AddressLookupTableAccount; + private _lookupTableAccount?: anchor.web3.AddressLookupTableAccount; readonly defaultRuleProgram: DefaultRuleProgram; @@ -69,7 +68,7 @@ export class LazorKitProgram { /** * Get or fetch the address lookup table account */ - async getLookupTableAccount(): Promise { + async getLookupTableAccount(): Promise { if (!this._lookupTableAccount) { try { const response = await this.connection.getAddressLookupTable( @@ -213,21 +212,21 @@ export class LazorKitProgram { // Helper method to create versioned transactions private async createVersionedTransaction( - instructions: web3.TransactionInstruction[], + instructions: anchor.web3.TransactionInstruction[], payer: anchor.web3.PublicKey - ): Promise { + ): Promise { const lookupTableAccount = await this.getLookupTableAccount(); const { blockhash } = await this.connection.getLatestBlockhash(); // Create v0 compatible transaction message - const messageV0 = new web3.TransactionMessage({ + const messageV0 = new anchor.web3.TransactionMessage({ payerKey: payer, recentBlockhash: blockhash, instructions, }).compileToV0Message(lookupTableAccount ? [lookupTableAccount] : []); // Create v0 transaction from the v0 message - return new web3.VersionedTransaction(messageV0); + return new anchor.web3.VersionedTransaction(messageV0); } // txn methods @@ -342,7 +341,7 @@ export class LazorKitProgram { ruleIns: anchor.web3.TransactionInstruction | null = null, maxRetries: number = 3 ): Promise<{ - transaction: web3.Transaction; + transaction: anchor.web3.Transaction; walletId: bigint; smartWallet: anchor.web3.PublicKey; }> { @@ -394,7 +393,7 @@ export class LazorKitProgram { action: types.ExecuteActionType = types.ExecuteAction.ExecuteTx, newPasskey: number[] | null = null, verifyInstructionIndex: number = 0 - ): Promise { + ): Promise { const [smartWalletAuthenticator] = this.smartWalletAuthenticator( passkeyPubkey, smartWallet @@ -480,6 +479,8 @@ export class LazorKitProgram { ); } + // Old blob helpers removed; using commit/executeCommitted below + /** * Query the chain for the smart-wallet associated with a passkey. */ @@ -521,7 +522,6 @@ export class LazorKitProgram { async getMessage( smartWalletString: string, ruleIns: anchor.web3.TransactionInstruction | null = null, - smartWalletAuthenticatorString: string, cpiInstruction: anchor.web3.TransactionInstruction, executeAction: types.ExecuteActionType ): Promise { @@ -550,6 +550,7 @@ export class LazorKitProgram { // - 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_hash (Option<[u8;32]>): 1 byte tag (0=None,1=Some + 32 bytes) // - cpi_data (Vec): 4 bytes length + data bytes const currentTimestamp = Math.floor(Date.now() / 1000); @@ -557,8 +558,9 @@ export class LazorKitProgram { // 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) + // None(cpi_data_hash): only 1 byte tag const buffer = Buffer.alloc( - 18 + ruleDataSize + 4 + cpiInstruction.data.length + 18 + ruleDataSize + 1 + 4 + cpiInstruction.data.length ); // Write nonce as little-endian u64 (bytes 0-7) @@ -584,8 +586,12 @@ export class LazorKitProgram { buffer.writeUInt8(0, 18); } + // Write cpi_data_hash as None (no blob) -> 1 byte 0 + const afterHashOffset = 18 + ruleDataSize; + buffer.writeUInt8(0, afterHashOffset); // None + // Write cpi_data length as little-endian u32 - const cpiDataOffset = 18 + ruleDataSize; + const cpiDataOffset = afterHashOffset + 1; buffer.writeUInt32LE(cpiInstruction.data.length, cpiDataOffset); // Write cpi_data bytes @@ -593,4 +599,146 @@ export class LazorKitProgram { return buffer; } + + /** Build Message buffer with a blob hash instead of inline cpi_data */ + async getMessageWithBlob(): Promise { + throw new Error('Deprecated: use commitCpiTxn/executeCommittedTxn instead'); + } + + // === New commit/executeCommitted API === + + private computeAccountsHash( + cpiProgram: anchor.web3.PublicKey, + accountMetas: anchor.web3.AccountMeta[] + ): Uint8Array { + const h = sha256.create(); + h.update(cpiProgram.toBytes()); + for (const m of accountMetas) { + h.update(m.pubkey.toBytes()); + h.update(Uint8Array.from([m.isWritable ? 1 : 0, m.isSigner ? 1 : 0])); + } + return new Uint8Array(h.arrayBuffer()); + } + + commitPda(ownerWallet: anchor.web3.PublicKey, dataHash: Uint8Array) { + return anchor.web3.PublicKey.findProgramAddressSync( + [ + constants.CPI_COMMIT_SEED, + ownerWallet.toBuffer(), + Buffer.from(dataHash), + ], + this.programId + )[0]; + } + + async commitCpiTxn( + passkeyPubkey: number[], + clientDataJsonRaw: Buffer, + authenticatorDataRaw: Buffer, + signature: Buffer, + payer: anchor.web3.PublicKey, + smartWallet: anchor.web3.PublicKey, + cpiProgram: anchor.web3.PublicKey, + cpiIns: anchor.web3.TransactionInstruction, + ruleIns: anchor.web3.TransactionInstruction | undefined, + expiresAt: number, + verifyInstructionIndex: number = 0 + ): Promise { + const [smartWalletAuthenticator] = this.smartWalletAuthenticator( + passkeyPubkey, + smartWallet + ); + const smartWalletConfig = this.smartWalletConfig(smartWallet); + + let ruleInstruction: anchor.web3.TransactionInstruction | null = null; + + if (!ruleIns) { + ruleInstruction = await this.defaultRuleProgram.checkRuleIns( + smartWalletAuthenticator + ); + } else { + ruleInstruction = ruleIns; + } + + const ruleMetas = instructionToAccountMetas(ruleInstruction, payer); + const cpiMetas = instructionToAccountMetas(cpiIns, payer); + const remainingAccounts = [...ruleMetas, ...cpiMetas]; + const accountsHash = this.computeAccountsHash(cpiProgram, cpiMetas); + const dataHash = new Uint8Array(sha256.arrayBuffer(cpiIns.data)); + const cpiCommit = this.commitPda(smartWallet, dataHash); + + const message = Buffer.concat([ + authenticatorDataRaw, + Buffer.from(sha256.arrayBuffer(clientDataJsonRaw)), + ]); + + const verifySignatureIx = createSecp256r1Instruction( + message, + Buffer.from(passkeyPubkey), + signature + ); + + const ix = await this.program.methods + .commitCpi({ + passkeyPubkey, + signature, + clientDataJsonRaw, + authenticatorDataRaw, + verifyInstructionIndex, + splitIndex: ruleMetas.length, + ruleData: ruleIns ? ruleIns.data : null, + cpiProgram, + cpiAccountsHash: Array.from(accountsHash), + cpiDataHash: Array.from(dataHash), + expiresAt: new anchor.BN(expiresAt), + } as any) + .accountsPartial({ + payer, + config: this.config, + smartWallet, + smartWalletConfig, + smartWalletAuthenticator, + whitelistRulePrograms: this.whitelistRulePrograms, + authenticatorProgram: ( + await this.getSmartWalletConfigData(smartWallet) + ).ruleProgram, + cpiCommit, + ixSysvar: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, + systemProgram: anchor.web3.SystemProgram.programId, + }) + .remainingAccounts(remainingAccounts) + .instruction(); + + const tx = new anchor.web3.Transaction().add(verifySignatureIx).add(ix); + tx.feePayer = payer; + tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash; + return tx; + } + + async executeCommittedTxn( + payer: anchor.web3.PublicKey, + smartWallet: anchor.web3.PublicKey, + cpiProgram: anchor.web3.PublicKey, + cpiIns: anchor.web3.TransactionInstruction + ): Promise { + const dataHash = new Uint8Array(sha256.arrayBuffer(cpiIns.data)); + const cpiCommit = this.commitPda(smartWallet, dataHash); + const metas = instructionToAccountMetas(cpiIns, payer); + + const ix = await this.program.methods + .executeCommitted({ cpiData: cpiIns.data } as any) + .accountsPartial({ + payer, + config: this.config, + smartWallet, + smartWalletConfig: this.smartWalletConfig(smartWallet), + cpiProgram, + cpiCommit, + commitRefund: payer, + }) + .remainingAccounts(metas) + .instruction(); + + return this.createVersionedTransaction([ix], payer); + } } diff --git a/tests/smart_wallet_with_default_rule.test.ts b/tests/smart_wallet_with_default_rule.test.ts index 74c682e..9297746 100644 --- a/tests/smart_wallet_with_default_rule.test.ts +++ b/tests/smart_wallet_with_default_rule.test.ts @@ -19,7 +19,7 @@ describe('Test smart wallet with default rule', () => { const defaultRuleProgram = new DefaultRuleProgram(connection); const payer = anchor.web3.Keypair.fromSecretKey( - bs58.decode(process.env.MAINNET_DEPLOYER_PRIVATE_KEY!) + bs58.decode(process.env.PRIVATE_KEY!) ); before(async () => { @@ -41,7 +41,7 @@ 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(); @@ -105,6 +105,74 @@ describe('Test smart wallet with default rule', () => { ); }); + it('Store blob successfully', async () => { + const privateKey = ECDSA.generateKey(); + + const publicKeyBase64 = privateKey.toCompressedPublicKey(); + + const pubkey = Array.from(Buffer.from(publicKeyBase64, 'base64')); + + const smartWalletId = lazorkitProgram.generateWalletId(); + const smartWallet = lazorkitProgram.smartWallet(smartWalletId); + + const [smartWalletAuthenticator] = lazorkitProgram.smartWalletAuthenticator( + pubkey, + smartWallet + ); + + const initRuleIns = await defaultRuleProgram.initRuleIns( + payer.publicKey, + smartWallet, + smartWalletAuthenticator + ); + + const credentialId = base64.encode(Buffer.from('testing something')); // random string + + const { transaction: createSmartWalletTxn } = + await lazorkitProgram.createSmartWalletTxn( + pubkey, + payer.publicKey, + credentialId, + initRuleIns, + smartWalletId, + true + ); + + const sig = await sendAndConfirmTransaction( + connection, + createSmartWalletTxn, + [payer], + { + commitment: 'confirmed', + } + ); + + console.log('Create smart-wallet: ', sig); + + // store blob + + const data = Buffer.from('testing something'); + + const { transaction: storeBlobTxn } = await lazorkitProgram.storeCpiBlobTxn( + payer.publicKey, + smartWallet, + lazorkitProgram.programId, + data, + 0 + ); + + const sig2 = await sendAndConfirmTransaction( + connection, + storeBlobTxn, + [payer], + { + commitment: 'confirmed', + } + ); + + console.log('Store blob: ', sig2); + }); + xit('Create address lookup table', async () => { const slot = await connection.getSlot(); diff --git a/tests/utils.ts b/tests/utils.ts index 4e9df39..bceea5e 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -2,8 +2,8 @@ import { createMint, getOrCreateAssociatedTokenAccount, mintTo, -} from "@solana/spl-token"; -import { Connection, Keypair, PublicKey, Signer } from "@solana/web3.js"; +} from '@solana/spl-token'; +import { Connection, Keypair, PublicKey, Signer } from '@solana/web3.js'; export const fundAccountSOL = async ( connection: Connection, @@ -16,7 +16,7 @@ export const fundAccountSOL = async ( }; export const getTxDetails = async (connection: Connection, sig) => { - const latestBlockHash = await connection.getLatestBlockhash("processed"); + const latestBlockHash = await connection.getLatestBlockhash('processed'); await connection.confirmTransaction( { @@ -24,12 +24,12 @@ export const getTxDetails = async (connection: Connection, sig) => { lastValidBlockHeight: latestBlockHash.lastValidBlockHeight, signature: sig, }, - "confirmed" + 'confirmed' ); return await connection.getTransaction(sig, { maxSupportedTransactionVersion: 0, - commitment: "confirmed", + commitment: 'confirmed', }); }; From d74e45c2c1ebdaf22e2e96fadc6c2d7a472fe2b5 Mon Sep 17 00:00:00 2001 From: onspeedhp Date: Sat, 9 Aug 2025 23:53:36 +0700 Subject: [PATCH 2/6] Refactor LazorKit program by removing deprecated instructions and consolidating execution logic. Introduce new message structures for ExecuteMessage, CallRuleMessage, and ChangeRuleMessage to enhance clarity and maintainability. Update instruction handlers and SDK to reflect changes in execution flow and improve overall code organization. --- programs/lazorkit/src/error.rs | 4 - programs/lazorkit/src/events.rs | 3 - programs/lazorkit/src/instructions/execute.rs | 273 --------- .../lazorkit/src/instructions/execute/args.rs | 125 ++++ .../instructions/execute/call_rule_direct.rs | 158 +++++ .../execute/change_rule_direct.rs | 193 ++++++ .../{ => execute/chunk}/commit_cpi.rs | 135 +++-- .../{ => execute/chunk}/execute_committed.rs | 0 .../src/instructions/execute/chunk/mod.rs | 5 + .../execute/execute_txn_direct.rs | 258 ++++++++ .../lazorkit/src/instructions/execute/mod.rs | 11 + .../src/instructions/handlers/call_rule.rs | 90 --- .../src/instructions/handlers/change_rule.rs | 127 ---- .../src/instructions/handlers/execute_tx.rs | 159 ----- .../lazorkit/src/instructions/handlers/mod.rs | 3 - .../lazorkit/src/instructions/initialize.rs | 8 +- programs/lazorkit/src/instructions/mod.rs | 10 +- programs/lazorkit/src/lib.rs | 36 +- programs/lazorkit/src/state/message.rs | 65 +- programs/lazorkit/src/state/mod.rs | 2 +- programs/lazorkit/src/utils.rs | 78 ++- sdk/index.ts | 15 +- sdk/lazor-kit.ts | 566 ++++++++++++------ sdk/messages.ts | 150 +++++ sdk/types.ts | 11 +- 25 files changed, 1475 insertions(+), 1010 deletions(-) delete mode 100644 programs/lazorkit/src/instructions/execute.rs create mode 100644 programs/lazorkit/src/instructions/execute/args.rs create mode 100644 programs/lazorkit/src/instructions/execute/call_rule_direct.rs create mode 100644 programs/lazorkit/src/instructions/execute/change_rule_direct.rs rename programs/lazorkit/src/instructions/{ => execute/chunk}/commit_cpi.rs (56%) rename programs/lazorkit/src/instructions/{ => execute/chunk}/execute_committed.rs (100%) create mode 100644 programs/lazorkit/src/instructions/execute/chunk/mod.rs create mode 100644 programs/lazorkit/src/instructions/execute/execute_txn_direct.rs create mode 100644 programs/lazorkit/src/instructions/execute/mod.rs delete mode 100644 programs/lazorkit/src/instructions/handlers/call_rule.rs delete mode 100644 programs/lazorkit/src/instructions/handlers/change_rule.rs delete mode 100644 programs/lazorkit/src/instructions/handlers/execute_tx.rs delete mode 100644 programs/lazorkit/src/instructions/handlers/mod.rs create mode 100644 sdk/messages.ts 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..acb492a 100644 --- a/programs/lazorkit/src/events.rs +++ b/programs/lazorkit/src/events.rs @@ -16,7 +16,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, @@ -147,7 +146,6 @@ impl TransactionExecuted { pub fn emit_event( smart_wallet: Pubkey, authenticator: Pubkey, - action: &str, nonce: u64, rule_program: Pubkey, cpi_program: Pubkey, @@ -156,7 +154,6 @@ impl TransactionExecuted { emit!(Self { smart_wallet, authenticator, - action: action.to_string(), nonce, rule_program, cpi_program, diff --git a/programs/lazorkit/src/instructions/execute.rs b/programs/lazorkit/src/instructions/execute.rs deleted file mode 100644 index 7dd3477..0000000 --- a/programs/lazorkit/src/instructions/execute.rs +++ /dev/null @@ -1,273 +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>, - - // No blob in this path -} diff --git a/programs/lazorkit/src/instructions/execute/args.rs b/programs/lazorkit/src/instructions/execute/args.rs new file mode 100644 index 0000000..bfe8377 --- /dev/null +++ b/programs/lazorkit/src/instructions/execute/args.rs @@ -0,0 +1,125 @@ +use crate::error::LazorKitError; +use anchor_lang::prelude::*; + +pub trait Args { + fn validate(&self) -> Result<()>; +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct ExecuteTxnArgs { + 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 split_index: u16, + pub rule_data: Vec, + pub cpi_data: Vec, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct ChangeRuleArgs { + 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 split_index: u16, + pub old_rule_program: Pubkey, + pub destroy_rule_data: Vec, + pub new_rule_program: Pubkey, + pub init_rule_data: Vec, + pub create_new_authenticator: Option<[u8; 33]>, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct CallRuleArgs { + 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 rule_program: Pubkey, + pub rule_data: Vec, + pub create_new_authenticator: Option<[u8; 33]>, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct CommitArgs { + 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 rule_data: Vec, + pub cpi_program: Pubkey, + pub expires_at: i64, +} + +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/execute/call_rule_direct.rs b/programs/lazorkit/src/instructions/execute/call_rule_direct.rs new file mode 100644 index 0000000..e777e92 --- /dev/null +++ b/programs/lazorkit/src/instructions/execute/call_rule_direct.rs @@ -0,0 +1,158 @@ +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 msg.new_passkey.is_some() { 1 } else { 0 }; + let rule_accs = &ctx.remaining_accounts[start_idx..]; + let mut hasher = Hasher::default(); + hasher.hash(args.rule_program.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_pk) = msg.new_passkey { + require!( + new_pk[0] == 0x02 || new_pk[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_pk, + Vec::new(), + )?; + } + + // 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 + pub rule_program: UncheckedAccount<'info>, + + 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..4175fbb --- /dev/null +++ b/programs/lazorkit/src/instructions/execute/change_rule_direct.rs @@ -0,0 +1,193 @@ +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(ctx: Context, 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 provided args program ids match the passed accounts + require!( + args.old_rule_program == ctx.accounts.old_rule_program.key(), + LazorKitError::InvalidProgramAddress + ); + require!( + args.new_rule_program == ctx.accounts.new_rule_program.key(), + LazorKitError::InvalidProgramAddress + ); + // Ensure different programs + require!( + args.old_rule_program != args.new_rule_program, + 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(args.old_rule_program.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(args.new_rule_program.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!( + args.old_rule_program == default_rule || args.new_rule_program == default_rule, + LazorKitError::NoDefaultRuleProgram + ); + + // update wallet config + ctx.accounts.smart_wallet_config.rule_program = args.new_rule_program; + + // 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>, + + 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/commit_cpi.rs b/programs/lazorkit/src/instructions/execute/chunk/commit_cpi.rs similarity index 56% rename from programs/lazorkit/src/instructions/commit_cpi.rs rename to programs/lazorkit/src/instructions/execute/chunk/commit_cpi.rs index 1ca3180..4b0afef 100644 --- a/programs/lazorkit/src/instructions/commit_cpi.rs +++ b/programs/lazorkit/src/instructions/execute/chunk/commit_cpi.rs @@ -1,40 +1,23 @@ use anchor_lang::prelude::*; +use crate::instructions::CommitArgs; use crate::security::validation; use crate::state::{ - Config, CpiCommit, SmartWalletAuthenticator, SmartWalletConfig, WhitelistRulePrograms, + 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}; - -#[derive(AnchorSerialize, AnchorDeserialize, Clone)] -pub struct CommitArgs { - 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 split_index: u16, - pub rule_data: Option>, - pub cpi_program: Pubkey, - pub cpi_accounts_hash: [u8; 32], - pub cpi_data_hash: [u8; 32], - pub expires_at: i64, -} +use anchor_lang::solana_program::hash::{hash, Hasher}; pub fn commit_cpi(ctx: Context, args: CommitArgs) -> Result<()> { - // Validate + // 0. Validate validation::validate_remaining_accounts(&ctx.remaining_accounts)?; - if let Some(ref rule_data) = args.rule_data { - validation::validate_rule_data(rule_data)?; - } - // No CPI bytes stored in commit mode - - // Program not paused + validation::validate_rule_data(&args.rule_data)?; require!(!ctx.accounts.config.is_paused, LazorKitError::ProgramPaused); - // Authorization - let msg = verify_authorization( + // 1. Authorization -> typed ExecuteMessage + let msg: ExecuteMessage = verify_authorization::( &ctx.accounts.ix_sysvar, &ctx.accounts.smart_wallet_authenticator, ctx.accounts.smart_wallet.key(), @@ -46,51 +29,71 @@ pub fn commit_cpi(ctx: Context, args: CommitArgs) -> Result<()> { ctx.accounts.smart_wallet_config.last_nonce, )?; - // Optionally rule-check now (binds policy at commit time) - if let Some(ref rule_data) = args.rule_data { - // First part of remaining accounts are for the rule program - let split = msg.split_index as usize; - require!( - split <= ctx.remaining_accounts.len(), - LazorKitError::InvalidSplitIndex - ); - let rule_accounts = &ctx.remaining_accounts[..split]; - // 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(), - )?; - - let rule_signer = get_pda_signer( - &args.passkey_pubkey, - ctx.accounts.smart_wallet.key(), - ctx.accounts.smart_wallet_authenticator.bump, - ); - // Ensure discriminator is check_rule - require!( - rule_data.get(0..8) == Some(&sighash("global", "check_rule")), - LazorKitError::InvalidCheckRuleDiscriminator - ); - execute_cpi( - rule_accounts, - rule_data, - &ctx.accounts.authenticator_program, - Some(rule_signer), - )?; + // 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), + )?; - // Write commit + // 4. Validate CPI accounts hash using provided cpi program pubkey and message + let mut ch = Hasher::default(); + ch.hash(args.cpi_program.as_ref()); + // no CPI accounts are supplied during commit + require!( + ch.result().to_bytes() == msg.cpi_accounts_hash, + LazorKitError::InvalidAccountData + ); + // 4.1 No inline cpi bytes in commit path; rely on signed message hash only + + // 5. Write commit using hashes from message let commit = &mut ctx.accounts.cpi_commit; commit.owner_wallet = ctx.accounts.smart_wallet.key(); commit.target_program = args.cpi_program; - commit.data_hash = args.cpi_data_hash; - commit.accounts_hash = args.cpi_accounts_hash; + 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(); @@ -157,7 +160,7 @@ pub struct CommitCpi<'info> { init, payer = payer, space = 8 + CpiCommit::INIT_SPACE, - seeds = [CpiCommit::PREFIX_SEED, smart_wallet.key().as_ref(), &args.cpi_data_hash], + seeds = [CpiCommit::PREFIX_SEED, smart_wallet.key().as_ref(), &smart_wallet_config.last_nonce.to_le_bytes()], bump, owner = ID, )] diff --git a/programs/lazorkit/src/instructions/execute_committed.rs b/programs/lazorkit/src/instructions/execute/chunk/execute_committed.rs similarity index 100% rename from programs/lazorkit/src/instructions/execute_committed.rs rename to programs/lazorkit/src/instructions/execute/chunk/execute_committed.rs 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..3d0057b --- /dev/null +++ b/programs/lazorkit/src/instructions/execute/execute_txn_direct.rs @@ -0,0 +1,258 @@ +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>, + pub whitelist_rule_programs: Box>, + /// CHECK + pub authenticator_program: UncheckedAccount<'info>, + /// CHECK + pub cpi_program: UncheckedAccount<'info>, + 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..3a5fd08 --- /dev/null +++ b/programs/lazorkit/src/instructions/execute/mod.rs @@ -0,0 +1,11 @@ +mod args; +mod call_rule_direct; +mod change_rule_direct; +mod chunk; +mod execute_txn_direct; + +pub use args::*; +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 4f4a694..0000000 --- a/programs/lazorkit/src/instructions/handlers/execute_tx.rs +++ /dev/null @@ -1,159 +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 (inline data) - 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..654ae52 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(()) } @@ -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 deffa77..8560945 100644 --- a/programs/lazorkit/src/instructions/mod.rs +++ b/programs/lazorkit/src/instructions/mod.rs @@ -1,15 +1,9 @@ +mod admin; mod create_smart_wallet; mod execute; -mod handlers; mod initialize; -mod admin; -mod commit_cpi; -mod execute_committed; +pub use admin::*; pub use create_smart_wallet::*; pub use execute::*; pub use initialize::*; -pub use admin::*; -pub use handlers::*; -pub use commit_cpi::*; -pub use execute_committed::*; \ No newline at end of file diff --git a/programs/lazorkit/src/lib.rs b/programs/lazorkit/src/lib.rs index 3f18082..99d5730 100644 --- a/programs/lazorkit/src/lib.rs +++ b/programs/lazorkit/src/lib.rs @@ -52,26 +52,42 @@ pub mod lazorkit { ) } - /// 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) - } - /// 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(ctx: Context, 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) + } + /// Commit a CPI after verifying auth and rule. Stores data and constraints. - pub fn commit_cpi(ctx: Context, args: CommitArgs) -> Result<()> { + pub fn commit_cpi<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, CommitCpi<'info>>, + args: CommitArgs, + ) -> Result<()> { instructions::commit_cpi(ctx, args) } /// Execute a previously committed CPI (no passkey verification here). - pub fn execute_committed(ctx: Context, args: ExecuteCommittedArgs) -> Result<()> { + pub fn execute_committed( + ctx: Context, + args: ExecuteCommittedArgs, + ) -> Result<()> { instructions::execute_committed(ctx, args) } } diff --git a/programs/lazorkit/src/state/message.rs b/programs/lazorkit/src/state/message.rs index 322faa8..d3f9024 100644 --- a/programs/lazorkit/src/state/message.rs +++ b/programs/lazorkit/src/state/message.rs @@ -1,10 +1,65 @@ 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>, - /// Direct CPI data fallback when no blob is used. - pub cpi_data: Vec, + pub rule_data_hash: [u8; 32], + pub rule_accounts_hash: [u8; 32], + pub new_passkey: Option<[u8; 33]>, +} + +#[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 2ea21eb..316d0bf 100644 --- a/programs/lazorkit/src/state/mod.rs +++ b/programs/lazorkit/src/state/mod.rs @@ -1,5 +1,5 @@ mod config; -mod message; +pub mod message; mod cpi_commit; mod smart_wallet_authenticator; mod smart_wallet_config; diff --git a/programs/lazorkit/src/utils.rs b/programs/lazorkit/src/utils.rs index 6b5b7b8..81c64c7 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::state::{CallRuleMessage, ChangeRuleMessage, ExecuteMessage}; use crate::{error::LazorKitError, ID}; use anchor_lang::solana_program::{ instruction::Instruction, @@ -260,16 +260,9 @@ 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, @@ -279,12 +272,11 @@ pub fn verify_authorization( 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/index.ts b/sdk/index.ts index 37b40cd..11bbcb9 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -8,17 +8,4 @@ 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 +export * from './messages'; \ No newline at end of file diff --git a/sdk/lazor-kit.ts b/sdk/lazor-kit.ts index e7fc413..a731464 100644 --- a/sdk/lazor-kit.ts +++ b/sdk/lazor-kit.ts @@ -12,7 +12,7 @@ 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'; +import { sha256 as jsSha256 } from 'js-sha256'; // Polyfill for structuredClone (e.g. React Native/Expo) if (typeof globalThis.structuredClone !== 'function') { @@ -33,6 +33,7 @@ export class LazorKitProgram { private _lookupTableAccount?: anchor.web3.AddressLookupTableAccount; readonly defaultRuleProgram: DefaultRuleProgram; + private _executeMsgCoder?: anchor.BorshCoder; constructor(connection: anchor.web3.Connection) { this.connection = connection; @@ -104,7 +105,7 @@ export class LazorKitProgram { /** * Check if a wallet ID already exists on-chain */ - async isWalletIdTaken(walletId: bigint): Promise { + private async isWalletIdTaken(walletId: bigint): Promise { try { const smartWalletPda = this.smartWallet(walletId); const accountInfo = await this.connection.getAccountInfo(smartWalletPda); @@ -148,13 +149,6 @@ export class LazorKitProgram { ); } - async generateUniqueWalletIdWithRetry( - maxAttempts: number = 10 - ): Promise { - const walletId = await this.generateUniqueWalletId(maxAttempts); - return this.smartWallet(walletId); - } - /** * Find smart wallet PDA with given ID */ @@ -331,56 +325,6 @@ export class LazorKitProgram { }; } - /** - * 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: anchor.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, @@ -390,8 +334,6 @@ export class LazorKitProgram { 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( @@ -407,18 +349,11 @@ export class LazorKitProgram { 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'); - } + if (!ruleIns) { + ruleInstruction = await this.defaultRuleProgram.checkRuleIns( + smartWalletAuthenticator + ); + } else { ruleInstruction = ruleIns; } @@ -442,33 +377,26 @@ export class LazorKitProgram { ); const executeInstructionIx = await this.program.methods - .execute({ + .executeTxnDirect({ passkeyPubkey, signature, clientDataJsonRaw, authenticatorDataRaw, verifyInstructionIndex, - action, - createNewAuthenticator: newPasskey, + splitIndex: ruleInstruction.keys.length, + ruleData: ruleInstruction.data, + cpiData: cpiIns.data, }) .accountsPartial({ payer, - config: this.config, smartWallet, - smartWalletConfig: smartWalletConfig, + smartWalletConfig, + config: this.config, 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, + cpiProgram: cpiIns.programId, }) .remainingAccounts(remainingAccounts) .instruction(); @@ -479,11 +407,150 @@ export class LazorKitProgram { ); } - // Old blob helpers removed; using commit/executeCommitted below + async callRuleDirectTxn( + passkeyPubkey: number[], + clientDataJsonRaw: Buffer, + authenticatorDataRaw: Buffer, + signature: Buffer, + payer: anchor.web3.PublicKey, + smartWallet: anchor.web3.PublicKey, + ruleProgram: anchor.web3.PublicKey, + ruleIns: anchor.web3.TransactionInstruction, + verifyInstructionIndex: number = 0, + newPasskey?: number[] | Buffer, + newAuthenticator?: anchor.web3.PublicKey + ): Promise { + const [smartWalletAuthenticator] = this.smartWalletAuthenticator( + passkeyPubkey, + smartWallet + ); + const smartWalletConfig = this.smartWalletConfig(smartWallet); + + // Prepare remaining accounts: optional new authenticator first, then rule accounts + const remainingAccounts: anchor.web3.AccountMeta[] = []; + if (newAuthenticator) { + remainingAccounts.push({ + pubkey: newAuthenticator, + isWritable: true, + isSigner: false, + }); + } + remainingAccounts.push(...instructionToAccountMetas(ruleIns, payer)); + + const message = Buffer.concat([ + authenticatorDataRaw, + Buffer.from(sha256.arrayBuffer(clientDataJsonRaw)), + ]); + + const verifySignatureIx = createSecp256r1Instruction( + message, + Buffer.from(passkeyPubkey), + signature + ); + + const ix = await (this.program.methods as any) + .callRuleDirect({ + passkeyPubkey, + signature, + clientDataJsonRaw, + authenticatorDataRaw, + verifyInstructionIndex, + ruleProgram, + ruleData: ruleIns.data, + createNewAuthenticator: newPasskey + ? (Array.from(new Uint8Array(newPasskey as any)) as any) + : null, + } as any) + .accountsPartial({ + payer, + config: this.config, + smartWallet, + smartWalletConfig, + smartWalletAuthenticator, + ruleProgram, + whitelistRulePrograms: this.whitelistRulePrograms, + ixSysvar: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, + systemProgram: anchor.web3.SystemProgram.programId, + }) + .remainingAccounts(remainingAccounts) + .instruction(); + + return this.createVersionedTransaction( + [verifySignatureIx, ix], + payer + ); + } + + async changeRuleDirectTxn( + passkeyPubkey: number[], + clientDataJsonRaw: Buffer, + authenticatorDataRaw: Buffer, + signature: Buffer, + payer: anchor.web3.PublicKey, + smartWallet: anchor.web3.PublicKey, + oldRuleProgram: anchor.web3.PublicKey, + destroyRuleIns: anchor.web3.TransactionInstruction, + newRuleProgram: anchor.web3.PublicKey, + initRuleIns: anchor.web3.TransactionInstruction, + verifyInstructionIndex: number = 0 + ): Promise { + const [smartWalletAuthenticator] = this.smartWalletAuthenticator( + passkeyPubkey, + smartWallet + ); + const smartWalletConfig = this.smartWalletConfig(smartWallet); + + // Build remaining accounts: destroy accounts then init accounts + const destroyMetas = instructionToAccountMetas(destroyRuleIns, payer); + const initMetas = instructionToAccountMetas(initRuleIns, payer); + const remainingAccounts = [...destroyMetas, ...initMetas]; + const splitIndex = destroyMetas.length; + + const message = Buffer.concat([ + authenticatorDataRaw, + Buffer.from(sha256.arrayBuffer(clientDataJsonRaw)), + ]); + const verifySignatureIx = createSecp256r1Instruction( + message, + Buffer.from(passkeyPubkey), + signature + ); + + const ix = await (this.program.methods as any) + .changeRuleDirect({ + passkeyPubkey, + signature, + clientDataJsonRaw, + authenticatorDataRaw, + verifyInstructionIndex, + splitIndex, + oldRuleProgram, + destroyRuleData: destroyRuleIns.data, + newRuleProgram, + initRuleData: initRuleIns.data, + createNewAuthenticator: null, + } as any) + .accountsPartial({ + payer, + config: this.config, + smartWallet, + smartWalletConfig, + smartWalletAuthenticator, + oldRuleProgram, + newRuleProgram, + whitelistRulePrograms: this.whitelistRulePrograms, + ixSysvar: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, + systemProgram: anchor.web3.SystemProgram.programId, + }) + .remainingAccounts(remainingAccounts) + .instruction(); + + return this.createVersionedTransaction( + [verifySignatureIx, ix], + 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; @@ -516,96 +583,207 @@ export class LazorKitProgram { }; } - /** - * Build the serialized Message struct used for signing requests. - */ - async getMessage( - smartWalletString: string, - ruleIns: anchor.web3.TransactionInstruction | null = null, - cpiInstruction: anchor.web3.TransactionInstruction, - executeAction: types.ExecuteActionType + async buildExecuteMessage( + smartWallet: anchor.web3.PublicKey, + payer: anchor.web3.PublicKey, + ruleIns: anchor.web3.TransactionInstruction | null, + cpiIns: anchor.web3.TransactionInstruction ): 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; - } + const cfg = await this.getSmartWalletConfigData(smartWallet); + const nonce = BigInt(cfg.lastNonce.toString()); + const now = Math.floor(Date.now() / 1000); + + // Rule instruction and metas + let ruleInstruction = ruleIns; + if (!ruleInstruction) { + ruleInstruction = await this.defaultRuleProgram.checkRuleIns(smartWallet); } - - // 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_hash (Option<[u8;32]>): 1 byte tag (0=None,1=Some + 32 bytes) - // - 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) - // None(cpi_data_hash): only 1 byte tag - const buffer = Buffer.alloc( - 18 + ruleDataSize + 1 + 4 + cpiInstruction.data.length + const ruleMetas = instructionToAccountMetas(ruleInstruction, payer); + const ruleAccountsHash = this.computeAccountsHash( + ruleInstruction.programId, + ruleMetas + ); + const ruleDataHash = new Uint8Array( + jsSha256.arrayBuffer(ruleInstruction.data) ); - // 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); + // CPI hashes + const cpiMetas = instructionToAccountMetas(cpiIns, payer); + const cpiAccountsHash = this.computeAccountsHash( + cpiIns.programId, + cpiMetas + ); + const cpiDataHash = new Uint8Array(jsSha256.arrayBuffer(cpiIns.data)); + + const buf = Buffer.alloc(8 + 8 + 32 * 4); + buf.writeBigUInt64LE(nonce, 0); + buf.writeBigInt64LE(BigInt(now), 8); + Buffer.from(ruleDataHash).copy(buf, 16); + Buffer.from(ruleAccountsHash).copy(buf, 48); + Buffer.from(cpiDataHash).copy(buf, 80); + Buffer.from(cpiAccountsHash).copy(buf, 112); + return buf; + } - // 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); + async buildExecuteMessageWithCoder( + smartWallet: anchor.web3.PublicKey, + payer: anchor.web3.PublicKey, + ruleIns: anchor.web3.TransactionInstruction | null, + cpiIns: anchor.web3.TransactionInstruction + ): Promise { + const cfg = await this.getSmartWalletConfigData(smartWallet); + const nonceBn = new anchor.BN(cfg.lastNonce.toString()); + const nowBn = new anchor.BN(Math.floor(Date.now() / 1000)); + + // Resolve rule instruction and compute hashes + let ruleInstruction = ruleIns; + if (!ruleInstruction) { + ruleInstruction = await this.defaultRuleProgram.checkRuleIns(smartWallet); } + const ruleMetas = instructionToAccountMetas(ruleInstruction, payer); + const ruleAccountsHash = this.computeAccountsHash( + ruleInstruction.programId, + ruleMetas + ); + const ruleDataHash = new Uint8Array( + jsSha256.arrayBuffer(ruleInstruction.data) + ); - // Write cpi_data_hash as None (no blob) -> 1 byte 0 - const afterHashOffset = 18 + ruleDataSize; - buffer.writeUInt8(0, afterHashOffset); // None - - // Write cpi_data length as little-endian u32 - const cpiDataOffset = afterHashOffset + 1; - buffer.writeUInt32LE(cpiInstruction.data.length, cpiDataOffset); - - // Write cpi_data bytes - cpiInstruction.data.copy(buffer, cpiDataOffset + 4); + // CPI hashes + const cpiMetas = instructionToAccountMetas(cpiIns, payer); + const cpiAccountsHash = this.computeAccountsHash( + cpiIns.programId, + cpiMetas + ); + const cpiDataHash = new Uint8Array(jsSha256.arrayBuffer(cpiIns.data)); + + // Encode via BorshCoder using a minimal IDL type definition + const coder = this.getExecuteMessageCoder(); + const encoded = coder.types.encode('ExecuteMessage', { + nonce: nonceBn, + currentTimestamp: nowBn, + ruleDataHash: Array.from(ruleDataHash), + ruleAccountsHash: Array.from( + cpiAccountsHash.length === 32 ? ruleAccountsHash : ruleAccountsHash + ), + cpiDataHash: Array.from(cpiDataHash), + cpiAccountsHash: Array.from(cpiAccountsHash), + }); + return Buffer.from(encoded); + } - return buffer; + private getExecuteMessageCoder(): anchor.BorshCoder { + if ((this as any)._executeMsgCoder) return (this as any)._executeMsgCoder; + 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: 'newPasskey', type: { option: { array: ['u8', 33] } } }, + ], + }, + }, + { + 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] } }, + ], + }, + }, + ], + }; + (this as any)._executeMsgCoder = new anchor.BorshCoder(idl); + return (this as any)._executeMsgCoder; } - /** Build Message buffer with a blob hash instead of inline cpi_data */ - async getMessageWithBlob(): Promise { - throw new Error('Deprecated: use commitCpiTxn/executeCommittedTxn instead'); + async buildCallRuleMessageWithCoder( + smartWallet: anchor.web3.PublicKey, + payer: anchor.web3.PublicKey, + ruleProgram: anchor.web3.PublicKey, + ruleIns: anchor.web3.TransactionInstruction, + newPasskey?: Uint8Array | number[] | Buffer + ): Promise { + const cfg = await this.getSmartWalletConfigData(smartWallet); + const nonceBn = new anchor.BN(cfg.lastNonce.toString()); + const nowBn = new anchor.BN(Math.floor(Date.now() / 1000)); + const ruleMetas = instructionToAccountMetas(ruleIns, payer); + const ruleAccountsHash = this.computeAccountsHash(ruleProgram, ruleMetas); + const ruleDataHash = new Uint8Array(jsSha256.arrayBuffer(ruleIns.data)); + const coder = this.getExecuteMessageCoder(); + const encoded = coder.types.encode('CallRuleMessage', { + nonce: nonceBn, + currentTimestamp: nowBn, + ruleDataHash: Array.from(ruleDataHash), + ruleAccountsHash: Array.from(ruleAccountsHash), + newPasskey: + newPasskey && (newPasskey as any).length + ? Array.from(new Uint8Array(newPasskey as any)) + : null, + }); + return Buffer.from(encoded); } - // === New commit/executeCommitted API === + async buildChangeRuleMessageWithCoder( + smartWallet: anchor.web3.PublicKey, + payer: anchor.web3.PublicKey, + oldRuleProgram: anchor.web3.PublicKey, + destroyRuleIns: anchor.web3.TransactionInstruction, + newRuleProgram: anchor.web3.PublicKey, + initRuleIns: anchor.web3.TransactionInstruction + ): Promise { + const cfg = await this.getSmartWalletConfigData(smartWallet); + const nonceBn = new anchor.BN(cfg.lastNonce.toString()); + const nowBn = new anchor.BN(Math.floor(Date.now() / 1000)); + const oldMetas = instructionToAccountMetas(destroyRuleIns, payer); + const oldAccountsHash = this.computeAccountsHash(oldRuleProgram, oldMetas); + const oldDataHash = new Uint8Array( + jsSha256.arrayBuffer(destroyRuleIns.data) + ); + const newMetas = instructionToAccountMetas(initRuleIns, payer); + const newAccountsHash = this.computeAccountsHash(newRuleProgram, newMetas); + const newDataHash = new Uint8Array(jsSha256.arrayBuffer(initRuleIns.data)); + const coder = this.getExecuteMessageCoder(); + const encoded = coder.types.encode('ChangeRuleMessage', { + nonce: nonceBn, + currentTimestamp: nowBn, + oldRuleDataHash: Array.from(oldDataHash), + oldRuleAccountsHash: Array.from(oldAccountsHash), + newRuleDataHash: Array.from(newDataHash), + newRuleAccountsHash: Array.from(newAccountsHash), + }); + return Buffer.from(encoded); + } private computeAccountsHash( cpiProgram: anchor.web3.PublicKey, @@ -620,17 +798,6 @@ export class LazorKitProgram { return new Uint8Array(h.arrayBuffer()); } - commitPda(ownerWallet: anchor.web3.PublicKey, dataHash: Uint8Array) { - return anchor.web3.PublicKey.findProgramAddressSync( - [ - constants.CPI_COMMIT_SEED, - ownerWallet.toBuffer(), - Buffer.from(dataHash), - ], - this.programId - )[0]; - } - async commitCpiTxn( passkeyPubkey: number[], clientDataJsonRaw: Buffer, @@ -639,7 +806,6 @@ export class LazorKitProgram { payer: anchor.web3.PublicKey, smartWallet: anchor.web3.PublicKey, cpiProgram: anchor.web3.PublicKey, - cpiIns: anchor.web3.TransactionInstruction, ruleIns: anchor.web3.TransactionInstruction | undefined, expiresAt: number, verifyInstructionIndex: number = 0 @@ -649,6 +815,9 @@ export class LazorKitProgram { smartWallet ); const smartWalletConfig = this.smartWalletConfig(smartWallet); + const smartWalletConfigData = await this.getSmartWalletConfigData( + smartWallet + ); let ruleInstruction: anchor.web3.TransactionInstruction | null = null; @@ -660,12 +829,9 @@ export class LazorKitProgram { ruleInstruction = ruleIns; } + // In commit mode, only rule accounts are passed for hashing and CPI verification on-chain const ruleMetas = instructionToAccountMetas(ruleInstruction, payer); - const cpiMetas = instructionToAccountMetas(cpiIns, payer); - const remainingAccounts = [...ruleMetas, ...cpiMetas]; - const accountsHash = this.computeAccountsHash(cpiProgram, cpiMetas); - const dataHash = new Uint8Array(sha256.arrayBuffer(cpiIns.data)); - const cpiCommit = this.commitPda(smartWallet, dataHash); + const remainingAccounts = [...ruleMetas]; const message = Buffer.concat([ authenticatorDataRaw, @@ -685,11 +851,8 @@ export class LazorKitProgram { clientDataJsonRaw, authenticatorDataRaw, verifyInstructionIndex, - splitIndex: ruleMetas.length, - ruleData: ruleIns ? ruleIns.data : null, + ruleData: ruleInstruction!.data, cpiProgram, - cpiAccountsHash: Array.from(accountsHash), - cpiDataHash: Array.from(dataHash), expiresAt: new anchor.BN(expiresAt), } as any) .accountsPartial({ @@ -699,10 +862,7 @@ export class LazorKitProgram { smartWalletConfig, smartWalletAuthenticator, whitelistRulePrograms: this.whitelistRulePrograms, - authenticatorProgram: ( - await this.getSmartWalletConfigData(smartWallet) - ).ruleProgram, - cpiCommit, + authenticatorProgram: smartWalletConfigData.ruleProgram, ixSysvar: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, }) @@ -718,12 +878,20 @@ export class LazorKitProgram { async executeCommittedTxn( payer: anchor.web3.PublicKey, smartWallet: anchor.web3.PublicKey, - cpiProgram: anchor.web3.PublicKey, cpiIns: anchor.web3.TransactionInstruction ): Promise { - const dataHash = new Uint8Array(sha256.arrayBuffer(cpiIns.data)); - const cpiCommit = this.commitPda(smartWallet, dataHash); const metas = instructionToAccountMetas(cpiIns, payer); + const smartWalletConfigData = await this.getSmartWalletConfigData( + smartWallet + ); + const commitPda = anchor.web3.PublicKey.findProgramAddressSync( + [ + constants.CPI_COMMIT_SEED, + smartWallet.toBuffer(), + smartWalletConfigData.lastNonce.toArrayLike(Buffer, 'le', 8), + ], + this.programId + )[0]; const ix = await this.program.methods .executeCommitted({ cpiData: cpiIns.data } as any) @@ -732,8 +900,8 @@ export class LazorKitProgram { config: this.config, smartWallet, smartWalletConfig: this.smartWalletConfig(smartWallet), - cpiProgram, - cpiCommit, + cpiProgram: cpiIns.programId, + cpiCommit: commitPda, commitRefund: payer, }) .remainingAccounts(metas) diff --git a/sdk/messages.ts b/sdk/messages.ts new file mode 100644 index 0000000..e91f9c5 --- /dev/null +++ b/sdk/messages.ts @@ -0,0 +1,150 @@ +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: 'newPasskey', type: { option: { array: ['u8', 33] } } }, + ], + }, + }, + { + 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, + ruleProgram: anchor.web3.PublicKey, + ruleIns: anchor.web3.TransactionInstruction, + newPasskey?: Uint8Array | number[] | Buffer | null +): Buffer { + const ruleMetas = instructionToAccountMetas(ruleIns, payer); + const ruleAccountsHash = computeAccountsHash(ruleProgram, 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), + newPasskey: + newPasskey && (newPasskey as any).length + ? Array.from(new Uint8Array(newPasskey as any)) + : null, + }); + return Buffer.from(encoded); +} + +export function buildChangeRuleMessage( + payer: anchor.web3.PublicKey, + nonce: anchor.BN, + now: anchor.BN, + oldRuleProgram: anchor.web3.PublicKey, + destroyRuleIns: anchor.web3.TransactionInstruction, + newRuleProgram: anchor.web3.PublicKey, + initRuleIns: anchor.web3.TransactionInstruction +): Buffer { + const oldMetas = instructionToAccountMetas(destroyRuleIns, payer); + const oldAccountsHash = computeAccountsHash(oldRuleProgram, oldMetas); + const oldDataHash = new Uint8Array(sha256.arrayBuffer(destroyRuleIns.data)); + + const newMetas = instructionToAccountMetas(initRuleIns, payer); + const newAccountsHash = computeAccountsHash(newRuleProgram, 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/sdk/types.ts b/sdk/types.ts index b337d20..c37e697 100644 --- a/sdk/types.ts +++ b/sdk/types.ts @@ -7,15 +7,8 @@ export type SmartWalletConfig = anchor.IdlTypes['smartWalletConfig']; export type SmartWalletAuthenticator = anchor.IdlTypes['smartWalletAuthenticator']; export type Config = anchor.IdlTypes['config']; -export type WhitelistRulePrograms = anchor.IdlTypes['whitelistRulePrograms']; +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: {} }, -}; From 33bc7f234b7e307db04475702df19ca1d91c1cdd Mon Sep 17 00:00:00 2001 From: onspeedhp Date: Sun, 10 Aug 2025 10:28:43 +0700 Subject: [PATCH 3/6] Implement program pause functionality in create_smart_wallet and update account initialization in initialize.rs. Refactor account attributes to ensure proper execution and validation in transaction handling. Enhance SDK structure by removing deprecated files and updating import paths for improved clarity and maintainability. --- .../src/instructions/create_smart_wallet.rs | 2 + .../instructions/execute/call_rule_direct.rs | 6 + .../execute/change_rule_direct.rs | 5 + .../instructions/execute/chunk/commit_cpi.rs | 7 +- .../execute/execute_txn_direct.rs | 16 +- .../lazorkit/src/instructions/initialize.rs | 4 +- sdk/client/defaultRule.ts | 98 ++ sdk/client/lazorkit.ts | 706 ++++++++++++++ sdk/constants.ts | 24 +- sdk/default-rule-program.ts | 75 -- sdk/errors.ts | 28 + sdk/index.ts | 16 +- sdk/lazor-kit.ts | 912 ------------------ sdk/messages.ts | 64 +- sdk/pda/defaultRule.ts | 13 + sdk/pda/lazorkit.ts | 83 ++ sdk/types.ts | 14 +- sdk/utils.ts | 8 +- sdk/webauthn/secp256r1.ts | 119 +++ tests/smart_wallet_with_default_rule.test.ts | 106 +- tests/utils.ts | 10 +- 21 files changed, 1203 insertions(+), 1113 deletions(-) create mode 100644 sdk/client/defaultRule.ts create mode 100644 sdk/client/lazorkit.ts delete mode 100644 sdk/default-rule-program.ts create mode 100644 sdk/errors.ts delete mode 100644 sdk/lazor-kit.ts create mode 100644 sdk/pda/defaultRule.ts create mode 100644 sdk/pda/lazorkit.ts create mode 100644 sdk/webauthn/secp256r1.ts diff --git a/programs/lazorkit/src/instructions/create_smart_wallet.rs b/programs/lazorkit/src/instructions/create_smart_wallet.rs index 71f1585..a023951 100644 --- a/programs/lazorkit/src/instructions/create_smart_wallet.rs +++ b/programs/lazorkit/src/instructions/create_smart_wallet.rs @@ -18,6 +18,8 @@ pub fn create_smart_wallet( wallet_id: u64, // Random ID provided by client, is_pay_for_user: bool, ) -> 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)?; diff --git a/programs/lazorkit/src/instructions/execute/call_rule_direct.rs b/programs/lazorkit/src/instructions/execute/call_rule_direct.rs index e777e92..e32ad5a 100644 --- a/programs/lazorkit/src/instructions/execute/call_rule_direct.rs +++ b/programs/lazorkit/src/instructions/execute/call_rule_direct.rs @@ -143,8 +143,14 @@ pub struct CallRuleDirect<'info> { 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 diff --git a/programs/lazorkit/src/instructions/execute/change_rule_direct.rs b/programs/lazorkit/src/instructions/execute/change_rule_direct.rs index 4175fbb..5708119 100644 --- a/programs/lazorkit/src/instructions/execute/change_rule_direct.rs +++ b/programs/lazorkit/src/instructions/execute/change_rule_direct.rs @@ -184,6 +184,11 @@ pub struct ChangeRuleDirect<'info> { /// CHECK pub new_rule_program: UncheckedAccount<'info>, + #[account( + seeds = [WhitelistRulePrograms::PREFIX_SEED], + bump, + owner = ID + )] pub whitelist_rule_programs: Box>, /// CHECK diff --git a/programs/lazorkit/src/instructions/execute/chunk/commit_cpi.rs b/programs/lazorkit/src/instructions/execute/chunk/commit_cpi.rs index 4b0afef..df3b269 100644 --- a/programs/lazorkit/src/instructions/execute/chunk/commit_cpi.rs +++ b/programs/lazorkit/src/instructions/execute/chunk/commit_cpi.rs @@ -148,11 +148,16 @@ pub struct CommitCpi<'info> { )] pub smart_wallet_authenticator: Box>, - #[account(seeds = [WhitelistRulePrograms::PREFIX_SEED], bump, owner = ID)] + #[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) diff --git a/programs/lazorkit/src/instructions/execute/execute_txn_direct.rs b/programs/lazorkit/src/instructions/execute/execute_txn_direct.rs index 3d0057b..a7ba1d2 100644 --- a/programs/lazorkit/src/instructions/execute/execute_txn_direct.rs +++ b/programs/lazorkit/src/instructions/execute/execute_txn_direct.rs @@ -246,11 +246,23 @@ pub struct ExecuteTxn<'info> { #[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 + /// CHECK: must be executable (rule program) + #[account(executable)] pub authenticator_program: UncheckedAccount<'info>, - /// CHECK + /// 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)] diff --git a/programs/lazorkit/src/instructions/initialize.rs b/programs/lazorkit/src/instructions/initialize.rs index 654ae52..f551d8c 100644 --- a/programs/lazorkit/src/instructions/initialize.rs +++ b/programs/lazorkit/src/instructions/initialize.rs @@ -32,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], @@ -42,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], diff --git a/sdk/client/defaultRule.ts b/sdk/client/defaultRule.ts new file mode 100644 index 0000000..da799d0 --- /dev/null +++ b/sdk/client/defaultRule.ts @@ -0,0 +1,98 @@ +import * as anchor from "@coral-xyz/anchor"; +import { + Connection, + PublicKey, + SystemProgram, + TransactionInstruction, +} from "@solana/web3.js"; +import DefaultRuleIdl from "../../target/idl/default_rule.json"; +import { DefaultRule } from "../../target/types/default_rule"; +import { deriveRulePda } from "../pda/defaultRule"; +import { decodeAnchorError } from "../errors"; + +export type DefaultRuleClientOptions = { + connection: Connection; + programId?: PublicKey; +}; + +export class DefaultRuleClient { + readonly connection: Connection; + readonly program: anchor.Program; + readonly programId: PublicKey; + + constructor(opts: DefaultRuleClientOptions) { + this.connection = opts.connection; + const programDefault = new anchor.Program(DefaultRuleIdl as anchor.Idl, { + connection: opts.connection, + }) as unknown as anchor.Program; + this.programId = opts.programId ?? programDefault.programId; + this.program = new (anchor as any).Program( + DefaultRuleIdl as anchor.Idl, + this.programId, + { connection: opts.connection } + ) as anchor.Program; + } + + rulePda(smartWalletAuthenticator: PublicKey): PublicKey { + return deriveRulePda(this.programId, smartWalletAuthenticator); + } + + async buildInitRuleIx( + payer: PublicKey, + smartWallet: PublicKey, + smartWalletAuthenticator: PublicKey + ): Promise { + try { + return await this.program.methods + .initRule() + .accountsPartial({ + payer, + smartWallet, + smartWalletAuthenticator, + rule: this.rulePda(smartWalletAuthenticator), + systemProgram: SystemProgram.programId, + }) + .instruction(); + } catch (e) { + throw decodeAnchorError(e); + } + } + + async buildCheckRuleIx( + smartWalletAuthenticator: PublicKey + ): Promise { + try { + return await this.program.methods + .checkRule() + .accountsPartial({ + rule: this.rulePda(smartWalletAuthenticator), + smartWalletAuthenticator, + }) + .instruction(); + } catch (e) { + throw decodeAnchorError(e); + } + } + + async buildAddDeviceIx( + payer: PublicKey, + smartWalletAuthenticator: PublicKey, + newSmartWalletAuthenticator: PublicKey + ): Promise { + try { + return await this.program.methods + .addDevice() + .accountsPartial({ + payer, + smartWalletAuthenticator, + newSmartWalletAuthenticator, + rule: this.rulePda(smartWalletAuthenticator), + newRule: this.rulePda(newSmartWalletAuthenticator), + systemProgram: SystemProgram.programId, + }) + .instruction(); + } catch (e) { + throw decodeAnchorError(e); + } + } +} diff --git a/sdk/client/lazorkit.ts b/sdk/client/lazorkit.ts new file mode 100644 index 0000000..4f2cd1b --- /dev/null +++ b/sdk/client/lazorkit.ts @@ -0,0 +1,706 @@ +import * as anchor from '@coral-xyz/anchor'; +import { + Connection, + PublicKey, + SystemProgram, + TransactionInstruction, + VersionedTransaction, + TransactionMessage, +} from '@solana/web3.js'; +import LazorkitIdl from '../../target/idl/lazorkit.json'; +import { Lazorkit } from '../../target/types/lazorkit'; +import { + deriveConfigPda, + deriveWhitelistRuleProgramsPda, + deriveSmartWalletPda, + deriveSmartWalletConfigPda, + deriveSmartWalletAuthenticatorPda, + deriveCpiCommitPda, +} from '../pda/lazorkit'; +import { buildSecp256r1VerifyIx } from '../webauthn/secp256r1'; +import { decodeAnchorError, SDKError } from '../errors'; + +export type LazorkitClientOptions = { + connection: Connection; + programId?: PublicKey; +}; + +export class LazorkitClient { + readonly connection: Connection; + readonly program: anchor.Program; + readonly programId: PublicKey; + + constructor(opts: LazorkitClientOptions) { + this.connection = opts.connection; + const programDefault = new anchor.Program(LazorkitIdl as anchor.Idl, { + connection: opts.connection, + }) as unknown as anchor.Program; + this.programId = opts.programId ?? programDefault.programId; + // Bind program with explicit programId (cast to any to satisfy TS constructor overloads) + this.program = new (anchor as any).Program( + LazorkitIdl as anchor.Idl, + this.programId, + { connection: opts.connection } + ) as anchor.Program; + } + + // PDAs + configPda(): PublicKey { + return deriveConfigPda(this.programId); + } + whitelistRuleProgramsPda(): PublicKey { + return deriveWhitelistRuleProgramsPda(this.programId); + } + smartWalletPda(walletId: bigint): PublicKey { + return deriveSmartWalletPda(this.programId, walletId); + } + smartWalletConfigPda(smartWallet: PublicKey): PublicKey { + return deriveSmartWalletConfigPda(this.programId, smartWallet); + } + smartWalletAuthenticatorPda( + smartWallet: PublicKey, + passkey: Uint8Array + ): PublicKey { + return deriveSmartWalletAuthenticatorPda( + this.programId, + smartWallet, + passkey + )[0]; + } + cpiCommitPda(smartWallet: PublicKey, nonceLe8: Buffer): PublicKey { + return deriveCpiCommitPda(this.programId, smartWallet, nonceLe8); + } + + // Convenience helpers + generateWalletId(): bigint { + const timestamp = BigInt(Date.now()) & BigInt('0xFFFFFFFFFFFF'); + const randomPart = BigInt(Math.floor(Math.random() * 0xffff)); + const id = (timestamp << BigInt(16)) | randomPart; + return id === BigInt(0) ? BigInt(1) : id; + } + + 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 + ); + } + + // Builders (TransactionInstruction) + async buildInitializeIx( + payer: PublicKey, + defaultRuleProgram: PublicKey + ): Promise { + try { + return await this.program.methods + .initialize() + .accountsPartial({ + signer: payer, + config: this.configPda(), + whitelistRulePrograms: this.whitelistRuleProgramsPda(), + defaultRuleProgram, + systemProgram: SystemProgram.programId, + }) + .instruction(); + } catch (e) { + throw decodeAnchorError(e); + } + } + + async buildCreateSmartWalletIx(params: { + payer: PublicKey; + smartWalletId: bigint; + passkey33: Uint8Array; + credentialIdBase64: string; + ruleInstruction: TransactionInstruction; + isPayForUser?: boolean; + defaultRuleProgram: PublicKey; + }): Promise { + const { + payer, + smartWalletId, + passkey33, + credentialIdBase64, + ruleInstruction, + isPayForUser = false, + defaultRuleProgram, + } = params; + const smartWallet = this.smartWalletPda(smartWalletId); + const smartWalletConfig = this.smartWalletConfigPda(smartWallet); + const smartWalletAuthenticator = this.smartWalletAuthenticatorPda( + smartWallet, + passkey33 + ); + return await this.program.methods + .createSmartWallet( + Array.from(passkey33), + Buffer.from(credentialIdBase64, 'base64'), + ruleInstruction.data, + new anchor.BN(smartWalletId.toString()), + isPayForUser + ) + .accountsPartial({ + signer: payer, + smartWallet, + smartWalletConfig, + smartWalletAuthenticator, + config: this.configPda(), + defaultRuleProgram, + systemProgram: SystemProgram.programId, + }) + .remainingAccounts( + ruleInstruction.keys.map((k) => ({ + pubkey: k.pubkey, + isWritable: k.isWritable, + isSigner: k.isSigner, + })) + ) + .instruction(); + } + + async buildExecuteTxnDirectIx(params: { + payer: PublicKey; + smartWallet: PublicKey; + passkey33: Uint8Array; + signature64: Uint8Array; + clientDataJsonRaw: Uint8Array; + authenticatorDataRaw: Uint8Array; + ruleInstruction: TransactionInstruction; + cpiInstruction: TransactionInstruction; + verifyInstructionIndex?: number; // default 0 + ruleProgram?: PublicKey; // if omitted, fetched from smartWalletConfig + }): Promise { + const { + payer, + smartWallet, + passkey33, + signature64, + clientDataJsonRaw, + authenticatorDataRaw, + ruleInstruction, + cpiInstruction, + verifyInstructionIndex = 0, + ruleProgram, + } = params; + + const smartWalletConfig = this.smartWalletConfigPda(smartWallet); + const cfg = await this.getSmartWalletConfigData(smartWallet); + const smartWalletAuthenticator = this.smartWalletAuthenticatorPda( + smartWallet, + passkey33 + ); + + const remaining = [ + ...ruleInstruction.keys.map((k) => ({ + pubkey: k.pubkey, + isWritable: k.isWritable, + isSigner: k.isSigner, + })), + ...cpiInstruction.keys.map((k) => ({ + pubkey: k.pubkey, + isWritable: k.isWritable, + isSigner: k.isSigner, + })), + ]; + + const splitIndex = ruleInstruction.keys.length; + const actualRuleProgram = ruleProgram ?? (cfg.ruleProgram as PublicKey); + + return await (this.program.methods as any) + .executeTxnDirect({ + passkeyPubkey: Array.from(passkey33), + signature: Buffer.from(signature64), + clientDataJsonRaw: Buffer.from(clientDataJsonRaw), + authenticatorDataRaw: Buffer.from(authenticatorDataRaw), + verifyInstructionIndex, + splitIndex, + ruleData: ruleInstruction.data, + cpiData: cpiInstruction.data, + }) + .accountsPartial({ + payer, + smartWallet, + smartWalletConfig, + smartWalletAuthenticator, + whitelistRulePrograms: this.whitelistRuleProgramsPda(), + authenticatorProgram: actualRuleProgram, + cpiProgram: cpiInstruction.programId, + config: this.configPda(), + ixSysvar: (anchor.web3 as any).SYSVAR_INSTRUCTIONS_PUBKEY, + }) + .remainingAccounts(remaining) + .instruction(); + } + + async buildCallRuleDirectIx(params: { + payer: PublicKey; + smartWallet: PublicKey; + passkey33: Uint8Array; + signature64: Uint8Array; + clientDataJsonRaw: Uint8Array; + authenticatorDataRaw: Uint8Array; + ruleProgram: PublicKey; + ruleInstruction: TransactionInstruction; + verifyInstructionIndex?: number; // default 0 + newPasskey33?: Uint8Array; // optional + newAuthenticatorPda?: PublicKey; // optional + }): Promise { + const { + payer, + smartWallet, + passkey33, + signature64, + clientDataJsonRaw, + authenticatorDataRaw, + ruleProgram, + ruleInstruction, + verifyInstructionIndex = 0, + newPasskey33, + newAuthenticatorPda, + } = params; + + const smartWalletConfig = this.smartWalletConfigPda(smartWallet); + const smartWalletAuthenticator = this.smartWalletAuthenticatorPda( + smartWallet, + passkey33 + ); + + const remaining: { + pubkey: PublicKey; + isWritable: boolean; + isSigner: boolean; + }[] = []; + if (newAuthenticatorPda) { + remaining.push({ + pubkey: newAuthenticatorPda, + isWritable: true, + isSigner: false, + }); + } + remaining.push( + ...ruleInstruction.keys.map((k) => ({ + pubkey: k.pubkey, + isWritable: k.isWritable, + isSigner: k.isSigner, + })) + ); + + return await (this.program.methods as any) + .callRuleDirect({ + passkeyPubkey: Array.from(passkey33), + signature: Buffer.from(signature64), + clientDataJsonRaw: Buffer.from(clientDataJsonRaw), + authenticatorDataRaw: Buffer.from(authenticatorDataRaw), + verifyInstructionIndex, + ruleProgram, + ruleData: ruleInstruction.data, + createNewAuthenticator: newPasskey33 ? Array.from(newPasskey33) : null, + }) + .accountsPartial({ + payer, + config: this.configPda(), + smartWallet, + smartWalletConfig, + smartWalletAuthenticator, + ruleProgram, + whitelistRulePrograms: this.whitelistRuleProgramsPda(), + ixSysvar: (anchor.web3 as any).SYSVAR_INSTRUCTIONS_PUBKEY, + systemProgram: SystemProgram.programId, + }) + .remainingAccounts(remaining) + .instruction(); + } + + async buildChangeRuleDirectIx(params: { + payer: PublicKey; + smartWallet: PublicKey; + passkey33: Uint8Array; + signature64: Uint8Array; + clientDataJsonRaw: Uint8Array; + authenticatorDataRaw: Uint8Array; + oldRuleProgram: PublicKey; + destroyRuleInstruction: TransactionInstruction; + newRuleProgram: PublicKey; + initRuleInstruction: TransactionInstruction; + verifyInstructionIndex?: number; // default 0 + }): Promise { + const { + payer, + smartWallet, + passkey33, + signature64, + clientDataJsonRaw, + authenticatorDataRaw, + oldRuleProgram, + destroyRuleInstruction, + newRuleProgram, + initRuleInstruction, + verifyInstructionIndex = 0, + } = params; + + const smartWalletConfig = this.smartWalletConfigPda(smartWallet); + const smartWalletAuthenticator = this.smartWalletAuthenticatorPda( + smartWallet, + passkey33 + ); + + const destroyMetas = destroyRuleInstruction.keys.map((k) => ({ + pubkey: k.pubkey, + isWritable: k.isWritable, + isSigner: k.isSigner, + })); + const initMetas = initRuleInstruction.keys.map((k) => ({ + pubkey: k.pubkey, + isWritable: k.isWritable, + isSigner: k.isSigner, + })); + const remaining = [...destroyMetas, ...initMetas]; + const splitIndex = destroyMetas.length; + + return await (this.program.methods as any) + .changeRuleDirect({ + passkeyPubkey: Array.from(passkey33), + signature: Buffer.from(signature64), + clientDataJsonRaw: Buffer.from(clientDataJsonRaw), + authenticatorDataRaw: Buffer.from(authenticatorDataRaw), + verifyInstructionIndex, + splitIndex, + oldRuleProgram, + destroyRuleData: destroyRuleInstruction.data, + newRuleProgram, + initRuleData: initRuleInstruction.data, + createNewAuthenticator: null, + }) + .accountsPartial({ + payer, + config: this.configPda(), + smartWallet, + smartWalletConfig, + smartWalletAuthenticator, + oldRuleProgram, + newRuleProgram, + whitelistRulePrograms: this.whitelistRuleProgramsPda(), + ixSysvar: (anchor.web3 as any).SYSVAR_INSTRUCTIONS_PUBKEY, + systemProgram: SystemProgram.programId, + }) + .remainingAccounts(remaining) + .instruction(); + } + + async buildCommitCpiIx(params: { + payer: PublicKey; + smartWallet: PublicKey; + passkey33: Uint8Array; + signature64: Uint8Array; + clientDataJsonRaw: Uint8Array; + authenticatorDataRaw: Uint8Array; + ruleInstruction: TransactionInstruction; + cpiProgram: PublicKey; + expiresAt: number; + verifyInstructionIndex?: number; + }): Promise { + const { + payer, + smartWallet, + passkey33, + signature64, + clientDataJsonRaw, + authenticatorDataRaw, + ruleInstruction, + cpiProgram, + expiresAt, + verifyInstructionIndex = 0, + } = params; + + const smartWalletConfig = this.smartWalletConfigPda(smartWallet); + const smartWalletAuthenticator = this.smartWalletAuthenticatorPda( + smartWallet, + passkey33 + ); + + const cfg = await this.getSmartWalletConfigData(smartWallet); + const whitelist = this.whitelistRuleProgramsPda(); + const ruleProgram = cfg.ruleProgram as PublicKey; + + const remaining = ruleInstruction.keys.map((k) => ({ + pubkey: k.pubkey, + isWritable: k.isWritable, + isSigner: k.isSigner, + })); + + return await (this.program.methods as any) + .commitCpi({ + passkeyPubkey: Array.from(passkey33), + signature: Buffer.from(signature64), + clientDataJsonRaw: Buffer.from(clientDataJsonRaw), + authenticatorDataRaw: Buffer.from(authenticatorDataRaw), + verifyInstructionIndex, + ruleData: ruleInstruction.data, + cpiProgram, + expiresAt: new anchor.BN(expiresAt), + }) + .accountsPartial({ + payer, + config: this.configPda(), + smartWallet, + smartWalletConfig, + smartWalletAuthenticator, + whitelistRulePrograms: whitelist, + authenticatorProgram: ruleProgram, + ixSysvar: (anchor.web3 as any).SYSVAR_INSTRUCTIONS_PUBKEY, + systemProgram: SystemProgram.programId, + }) + .remainingAccounts(remaining) + .instruction(); + } + + async buildExecuteCommittedIx(params: { + payer: PublicKey; + smartWallet: PublicKey; + cpiInstruction: TransactionInstruction; + }): Promise { + const { payer, smartWallet, cpiInstruction } = params; + const cfg = await this.getSmartWalletConfigData(smartWallet); + const nonceLe8 = Buffer.alloc(8); + nonceLe8.writeBigUInt64LE(BigInt(cfg.lastNonce.toString())); + const cpiCommit = this.cpiCommitPda(smartWallet, nonceLe8); + return await (this.program.methods as any) + .executeCommitted({ cpiData: cpiInstruction.data }) + .accountsPartial({ + payer, + config: this.configPda(), + smartWallet, + smartWalletConfig: this.smartWalletConfigPda(smartWallet), + cpiProgram: cpiInstruction.programId, + cpiCommit, + commitRefund: payer, + }) + .remainingAccounts( + cpiInstruction.keys.map((k) => ({ + pubkey: k.pubkey, + isWritable: k.isWritable, + isSigner: k.isSigner, + })) + ) + .instruction(); + } + + // High-level: build transactions with Secp256r1 verify ix at index 0 + async executeTxnDirectTx(params: { + payer: PublicKey; + smartWallet: PublicKey; + passkey33: Uint8Array; + signature64: Uint8Array; + clientDataJsonRaw: Uint8Array; + authenticatorDataRaw: Uint8Array; + ruleInstruction: TransactionInstruction; + cpiInstruction: TransactionInstruction; + ruleProgram?: PublicKey; + }): Promise { + const verifyIx = buildSecp256r1VerifyIx( + Buffer.concat([ + Buffer.from(params.authenticatorDataRaw), + Buffer.from( + (anchor as any).utils.sha256.hash(params.clientDataJsonRaw), + 'hex' + ), + ]), + Buffer.from(params.passkey33), + Buffer.from(params.signature64) + ); + const execIx = await this.buildExecuteTxnDirectIx({ + payer: params.payer, + smartWallet: params.smartWallet, + passkey33: params.passkey33, + signature64: params.signature64, + clientDataJsonRaw: params.clientDataJsonRaw, + authenticatorDataRaw: params.authenticatorDataRaw, + ruleInstruction: params.ruleInstruction, + cpiInstruction: params.cpiInstruction, + verifyInstructionIndex: 0, + ruleProgram: params.ruleProgram, + }); + return this.buildV0Tx(params.payer, [verifyIx, execIx]); + } + + async callRuleDirectTx(params: { + payer: PublicKey; + smartWallet: PublicKey; + passkey33: Uint8Array; + signature64: Uint8Array; + clientDataJsonRaw: Uint8Array; + authenticatorDataRaw: Uint8Array; + ruleProgram: PublicKey; + ruleInstruction: TransactionInstruction; + newPasskey33?: Uint8Array; + newAuthenticatorPda?: PublicKey; + }): Promise { + const verifyIx = buildSecp256r1VerifyIx( + Buffer.concat([ + Buffer.from(params.authenticatorDataRaw), + Buffer.from( + (anchor as any).utils.sha256.hash(params.clientDataJsonRaw), + 'hex' + ), + ]), + Buffer.from(params.passkey33), + Buffer.from(params.signature64) + ); + const ix = await this.buildCallRuleDirectIx({ + payer: params.payer, + smartWallet: params.smartWallet, + passkey33: params.passkey33, + signature64: params.signature64, + clientDataJsonRaw: params.clientDataJsonRaw, + authenticatorDataRaw: params.authenticatorDataRaw, + ruleProgram: params.ruleProgram, + ruleInstruction: params.ruleInstruction, + verifyInstructionIndex: 0, + newPasskey33: params.newPasskey33, + newAuthenticatorPda: params.newAuthenticatorPda, + }); + return this.buildV0Tx(params.payer, [verifyIx, ix]); + } + + async changeRuleDirectTx(params: { + payer: PublicKey; + smartWallet: PublicKey; + passkey33: Uint8Array; + signature64: Uint8Array; + clientDataJsonRaw: Uint8Array; + authenticatorDataRaw: Uint8Array; + oldRuleProgram: PublicKey; + destroyRuleInstruction: TransactionInstruction; + newRuleProgram: PublicKey; + initRuleInstruction: TransactionInstruction; + }): Promise { + const verifyIx = buildSecp256r1VerifyIx( + Buffer.concat([ + Buffer.from(params.authenticatorDataRaw), + Buffer.from( + (anchor as any).utils.sha256.hash(params.clientDataJsonRaw), + 'hex' + ), + ]), + Buffer.from(params.passkey33), + Buffer.from(params.signature64) + ); + const ix = await this.buildChangeRuleDirectIx({ + payer: params.payer, + smartWallet: params.smartWallet, + passkey33: params.passkey33, + signature64: params.signature64, + clientDataJsonRaw: params.clientDataJsonRaw, + authenticatorDataRaw: params.authenticatorDataRaw, + oldRuleProgram: params.oldRuleProgram, + destroyRuleInstruction: params.destroyRuleInstruction, + newRuleProgram: params.newRuleProgram, + initRuleInstruction: params.initRuleInstruction, + verifyInstructionIndex: 0, + }); + return this.buildV0Tx(params.payer, [verifyIx, ix]); + } + + async commitCpiTx(params: { + payer: PublicKey; + smartWallet: PublicKey; + passkey33: Uint8Array; + signature64: Uint8Array; + clientDataJsonRaw: Uint8Array; + authenticatorDataRaw: Uint8Array; + ruleInstruction: TransactionInstruction; + cpiProgram: PublicKey; + expiresAt: number; + }) { + const verifyIx = buildSecp256r1VerifyIx( + Buffer.concat([ + Buffer.from(params.authenticatorDataRaw), + Buffer.from( + (anchor as any).utils.sha256.hash(params.clientDataJsonRaw), + 'hex' + ), + ]), + Buffer.from(params.passkey33), + Buffer.from(params.signature64) + ); + const ix = await this.buildCommitCpiIx({ + payer: params.payer, + smartWallet: params.smartWallet, + passkey33: params.passkey33, + signature64: params.signature64, + clientDataJsonRaw: params.clientDataJsonRaw, + authenticatorDataRaw: params.authenticatorDataRaw, + ruleInstruction: params.ruleInstruction, + cpiProgram: params.cpiProgram, + expiresAt: params.expiresAt, + verifyInstructionIndex: 0, + }); + const tx = new (anchor.web3 as any).Transaction().add(verifyIx).add(ix); + tx.feePayer = params.payer; + tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash; + return tx; + } + + // Convenience: VersionedTransaction v0 + async buildV0Tx( + payer: PublicKey, + ixs: TransactionInstruction[] + ): Promise { + try { + const { blockhash } = await this.connection.getLatestBlockhash(); + const msg = new TransactionMessage({ + payerKey: payer, + recentBlockhash: blockhash, + instructions: ixs, + }).compileToV0Message(); + return new VersionedTransaction(msg); + } catch (e) { + throw new SDKError('Failed to build v0 transaction', e as any); + } + } + + // Legacy-compat APIs for simpler DX + async initializeTxn(payer: PublicKey, defaultRuleProgram: PublicKey) { + const ix = await this.buildInitializeIx(payer, defaultRuleProgram); + return new anchor.web3.Transaction().add(ix); + } + + async createSmartWalletTx(params: { + payer: PublicKey; + smartWalletId: bigint; + passkey33: Uint8Array; + credentialIdBase64: string; + ruleInstruction: TransactionInstruction; + isPayForUser?: boolean; + defaultRuleProgram: PublicKey; + }) { + const ix = await this.buildCreateSmartWalletIx(params); + const tx = new anchor.web3.Transaction().add(ix); + tx.feePayer = params.payer; + tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash; + const smartWallet = this.smartWalletPda(params.smartWalletId); + return { + transaction: tx, + smartWalletId: params.smartWalletId, + smartWallet, + }; + } + + async simulate(ixs: TransactionInstruction[], payer: PublicKey) { + try { + const v0 = await this.buildV0Tx(payer, ixs); + // Empty signatures for simulate + return await this.connection.simulateTransaction(v0, { + sigVerify: false, + }); + } catch (e) { + throw decodeAnchorError(e); + } + } +} diff --git a/sdk/constants.ts b/sdk/constants.ts index a1409cc..cd4f637 100644 --- a/sdk/constants.ts +++ b/sdk/constants.ts @@ -1,26 +1,26 @@ -import * as anchor from '@coral-xyz/anchor'; +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_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' + "smart_wallet_authenticator" ); export const WHITELIST_RULE_PROGRAMS_SEED = Buffer.from( - 'whitelist_rule_programs' + "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'); +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'); -export const MEMBER_SEED = Buffer.from('member'); -export const RULE_SEED = Buffer.from('rule'); +export const RULE_DATA_SEED = Buffer.from("rule_data"); +export const MEMBER_SEED = Buffer.from("member"); +export const RULE_SEED = Buffer.from("rule"); // ADDRESS LOOKUP TABLE for Versioned Transactions (v0) // This lookup table contains frequently used program IDs and accounts // to reduce transaction size and enable more complex operations export const ADDRESS_LOOKUP_TABLE = new anchor.web3.PublicKey( - '7Pr3DG7tRPAjVb44gqbxTj1KstikAuVZY7YmXdotVjLA' + "7Pr3DG7tRPAjVb44gqbxTj1KstikAuVZY7YmXdotVjLA" ); 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/errors.ts b/sdk/errors.ts new file mode 100644 index 0000000..a926bbc --- /dev/null +++ b/sdk/errors.ts @@ -0,0 +1,28 @@ +import { AnchorError } from "@coral-xyz/anchor"; + +export class SDKError extends Error { + constructor( + message: string, + readonly cause?: unknown, + readonly logs?: string[] + ) { + super(message); + this.name = "SDKError"; + } +} + +export function decodeAnchorError(e: any): SDKError { + if (e instanceof SDKError) return e; + const logs: string[] | undefined = + e?.logs || e?.transactionMessage || undefined; + // AnchorError handling (works with provider.simulate or sendAndConfirm + if (e instanceof AnchorError) { + const code = e.error.errorCode.number; + const msg = e.error.errorMessage || e.message; + return new SDKError(`AnchorError ${code}: ${msg}`, e, logs); + } + // Fallback + const message = + typeof e?.message === "string" ? e.message : "Unknown SDK error"; + return new SDKError(message, e, logs); +} diff --git a/sdk/index.ts b/sdk/index.ts index 11bbcb9..70d2355 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -1,11 +1,15 @@ // Main SDK exports -export { LazorKitProgram } from './lazor-kit'; -export { DefaultRuleProgram } from './default-rule-program'; +export { LazorkitClient } from "./client/lazorkit"; +export { DefaultRuleClient } from "./client/defaultRule"; // Type exports -export * from './types'; +export * from "./types"; // Utility exports -export * from './utils'; -export * from './constants'; -export * from './messages'; \ No newline at end of file +export * from "./utils"; +export * from "./constants"; +export * from "./messages"; +export * as pda from "./pda/lazorkit"; +export * as rulePda from "./pda/defaultRule"; +export * as webauthn from "./webauthn/secp256r1"; +export * from "./errors"; diff --git a/sdk/lazor-kit.ts b/sdk/lazor-kit.ts deleted file mode 100644 index a731464..0000000 --- a/sdk/lazor-kit.ts +++ /dev/null @@ -1,912 +0,0 @@ -import * as anchor from '@coral-xyz/anchor'; -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 { sha256 as jsSha256 } from 'js-sha256'; - -// 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?: anchor.web3.AddressLookupTableAccount; - - readonly defaultRuleProgram: DefaultRuleProgram; - private _executeMsgCoder?: anchor.BorshCoder; - - 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 - */ - private 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` - ); - } - - /** - * 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: anchor.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 anchor.web3.TransactionMessage({ - payerKey: payer, - recentBlockhash: blockhash, - instructions, - }).compileToV0Message(lookupTableAccount ? [lookupTableAccount] : []); - - // Create v0 transaction from the v0 message - return new anchor.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, - }; - } - - 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, - 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 (!ruleIns) { - ruleInstruction = await this.defaultRuleProgram.checkRuleIns( - smartWalletAuthenticator - ); - } else { - 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 - .executeTxnDirect({ - passkeyPubkey, - signature, - clientDataJsonRaw, - authenticatorDataRaw, - verifyInstructionIndex, - splitIndex: ruleInstruction.keys.length, - ruleData: ruleInstruction.data, - cpiData: cpiIns.data, - }) - .accountsPartial({ - payer, - smartWallet, - smartWalletConfig, - config: this.config, - smartWalletAuthenticator, - whitelistRulePrograms: this.whitelistRulePrograms, - authenticatorProgram: smartWalletConfigData.ruleProgram, - ixSysvar: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, - cpiProgram: cpiIns.programId, - }) - .remainingAccounts(remainingAccounts) - .instruction(); - - return this.createVersionedTransaction( - [verifySignatureIx, executeInstructionIx], - payer - ); - } - - async callRuleDirectTxn( - passkeyPubkey: number[], - clientDataJsonRaw: Buffer, - authenticatorDataRaw: Buffer, - signature: Buffer, - payer: anchor.web3.PublicKey, - smartWallet: anchor.web3.PublicKey, - ruleProgram: anchor.web3.PublicKey, - ruleIns: anchor.web3.TransactionInstruction, - verifyInstructionIndex: number = 0, - newPasskey?: number[] | Buffer, - newAuthenticator?: anchor.web3.PublicKey - ): Promise { - const [smartWalletAuthenticator] = this.smartWalletAuthenticator( - passkeyPubkey, - smartWallet - ); - const smartWalletConfig = this.smartWalletConfig(smartWallet); - - // Prepare remaining accounts: optional new authenticator first, then rule accounts - const remainingAccounts: anchor.web3.AccountMeta[] = []; - if (newAuthenticator) { - remainingAccounts.push({ - pubkey: newAuthenticator, - isWritable: true, - isSigner: false, - }); - } - remainingAccounts.push(...instructionToAccountMetas(ruleIns, payer)); - - const message = Buffer.concat([ - authenticatorDataRaw, - Buffer.from(sha256.arrayBuffer(clientDataJsonRaw)), - ]); - - const verifySignatureIx = createSecp256r1Instruction( - message, - Buffer.from(passkeyPubkey), - signature - ); - - const ix = await (this.program.methods as any) - .callRuleDirect({ - passkeyPubkey, - signature, - clientDataJsonRaw, - authenticatorDataRaw, - verifyInstructionIndex, - ruleProgram, - ruleData: ruleIns.data, - createNewAuthenticator: newPasskey - ? (Array.from(new Uint8Array(newPasskey as any)) as any) - : null, - } as any) - .accountsPartial({ - payer, - config: this.config, - smartWallet, - smartWalletConfig, - smartWalletAuthenticator, - ruleProgram, - whitelistRulePrograms: this.whitelistRulePrograms, - ixSysvar: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, - systemProgram: anchor.web3.SystemProgram.programId, - }) - .remainingAccounts(remainingAccounts) - .instruction(); - - return this.createVersionedTransaction( - [verifySignatureIx, ix], - payer - ); - } - - async changeRuleDirectTxn( - passkeyPubkey: number[], - clientDataJsonRaw: Buffer, - authenticatorDataRaw: Buffer, - signature: Buffer, - payer: anchor.web3.PublicKey, - smartWallet: anchor.web3.PublicKey, - oldRuleProgram: anchor.web3.PublicKey, - destroyRuleIns: anchor.web3.TransactionInstruction, - newRuleProgram: anchor.web3.PublicKey, - initRuleIns: anchor.web3.TransactionInstruction, - verifyInstructionIndex: number = 0 - ): Promise { - const [smartWalletAuthenticator] = this.smartWalletAuthenticator( - passkeyPubkey, - smartWallet - ); - const smartWalletConfig = this.smartWalletConfig(smartWallet); - - // Build remaining accounts: destroy accounts then init accounts - const destroyMetas = instructionToAccountMetas(destroyRuleIns, payer); - const initMetas = instructionToAccountMetas(initRuleIns, payer); - const remainingAccounts = [...destroyMetas, ...initMetas]; - const splitIndex = destroyMetas.length; - - const message = Buffer.concat([ - authenticatorDataRaw, - Buffer.from(sha256.arrayBuffer(clientDataJsonRaw)), - ]); - const verifySignatureIx = createSecp256r1Instruction( - message, - Buffer.from(passkeyPubkey), - signature - ); - - const ix = await (this.program.methods as any) - .changeRuleDirect({ - passkeyPubkey, - signature, - clientDataJsonRaw, - authenticatorDataRaw, - verifyInstructionIndex, - splitIndex, - oldRuleProgram, - destroyRuleData: destroyRuleIns.data, - newRuleProgram, - initRuleData: initRuleIns.data, - createNewAuthenticator: null, - } as any) - .accountsPartial({ - payer, - config: this.config, - smartWallet, - smartWalletConfig, - smartWalletAuthenticator, - oldRuleProgram, - newRuleProgram, - whitelistRulePrograms: this.whitelistRulePrograms, - ixSysvar: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, - systemProgram: anchor.web3.SystemProgram.programId, - }) - .remainingAccounts(remainingAccounts) - .instruction(); - - return this.createVersionedTransaction( - [verifySignatureIx, ix], - payer - ); - } - - 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, - }; - } - - async buildExecuteMessage( - smartWallet: anchor.web3.PublicKey, - payer: anchor.web3.PublicKey, - ruleIns: anchor.web3.TransactionInstruction | null, - cpiIns: anchor.web3.TransactionInstruction - ): Promise { - const cfg = await this.getSmartWalletConfigData(smartWallet); - const nonce = BigInt(cfg.lastNonce.toString()); - const now = Math.floor(Date.now() / 1000); - - // Rule instruction and metas - let ruleInstruction = ruleIns; - if (!ruleInstruction) { - ruleInstruction = await this.defaultRuleProgram.checkRuleIns(smartWallet); - } - const ruleMetas = instructionToAccountMetas(ruleInstruction, payer); - const ruleAccountsHash = this.computeAccountsHash( - ruleInstruction.programId, - ruleMetas - ); - const ruleDataHash = new Uint8Array( - jsSha256.arrayBuffer(ruleInstruction.data) - ); - - // CPI hashes - const cpiMetas = instructionToAccountMetas(cpiIns, payer); - const cpiAccountsHash = this.computeAccountsHash( - cpiIns.programId, - cpiMetas - ); - const cpiDataHash = new Uint8Array(jsSha256.arrayBuffer(cpiIns.data)); - - const buf = Buffer.alloc(8 + 8 + 32 * 4); - buf.writeBigUInt64LE(nonce, 0); - buf.writeBigInt64LE(BigInt(now), 8); - Buffer.from(ruleDataHash).copy(buf, 16); - Buffer.from(ruleAccountsHash).copy(buf, 48); - Buffer.from(cpiDataHash).copy(buf, 80); - Buffer.from(cpiAccountsHash).copy(buf, 112); - return buf; - } - - async buildExecuteMessageWithCoder( - smartWallet: anchor.web3.PublicKey, - payer: anchor.web3.PublicKey, - ruleIns: anchor.web3.TransactionInstruction | null, - cpiIns: anchor.web3.TransactionInstruction - ): Promise { - const cfg = await this.getSmartWalletConfigData(smartWallet); - const nonceBn = new anchor.BN(cfg.lastNonce.toString()); - const nowBn = new anchor.BN(Math.floor(Date.now() / 1000)); - - // Resolve rule instruction and compute hashes - let ruleInstruction = ruleIns; - if (!ruleInstruction) { - ruleInstruction = await this.defaultRuleProgram.checkRuleIns(smartWallet); - } - const ruleMetas = instructionToAccountMetas(ruleInstruction, payer); - const ruleAccountsHash = this.computeAccountsHash( - ruleInstruction.programId, - ruleMetas - ); - const ruleDataHash = new Uint8Array( - jsSha256.arrayBuffer(ruleInstruction.data) - ); - - // CPI hashes - const cpiMetas = instructionToAccountMetas(cpiIns, payer); - const cpiAccountsHash = this.computeAccountsHash( - cpiIns.programId, - cpiMetas - ); - const cpiDataHash = new Uint8Array(jsSha256.arrayBuffer(cpiIns.data)); - - // Encode via BorshCoder using a minimal IDL type definition - const coder = this.getExecuteMessageCoder(); - const encoded = coder.types.encode('ExecuteMessage', { - nonce: nonceBn, - currentTimestamp: nowBn, - ruleDataHash: Array.from(ruleDataHash), - ruleAccountsHash: Array.from( - cpiAccountsHash.length === 32 ? ruleAccountsHash : ruleAccountsHash - ), - cpiDataHash: Array.from(cpiDataHash), - cpiAccountsHash: Array.from(cpiAccountsHash), - }); - return Buffer.from(encoded); - } - - private getExecuteMessageCoder(): anchor.BorshCoder { - if ((this as any)._executeMsgCoder) return (this as any)._executeMsgCoder; - 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: 'newPasskey', type: { option: { array: ['u8', 33] } } }, - ], - }, - }, - { - 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] } }, - ], - }, - }, - ], - }; - (this as any)._executeMsgCoder = new anchor.BorshCoder(idl); - return (this as any)._executeMsgCoder; - } - - async buildCallRuleMessageWithCoder( - smartWallet: anchor.web3.PublicKey, - payer: anchor.web3.PublicKey, - ruleProgram: anchor.web3.PublicKey, - ruleIns: anchor.web3.TransactionInstruction, - newPasskey?: Uint8Array | number[] | Buffer - ): Promise { - const cfg = await this.getSmartWalletConfigData(smartWallet); - const nonceBn = new anchor.BN(cfg.lastNonce.toString()); - const nowBn = new anchor.BN(Math.floor(Date.now() / 1000)); - const ruleMetas = instructionToAccountMetas(ruleIns, payer); - const ruleAccountsHash = this.computeAccountsHash(ruleProgram, ruleMetas); - const ruleDataHash = new Uint8Array(jsSha256.arrayBuffer(ruleIns.data)); - const coder = this.getExecuteMessageCoder(); - const encoded = coder.types.encode('CallRuleMessage', { - nonce: nonceBn, - currentTimestamp: nowBn, - ruleDataHash: Array.from(ruleDataHash), - ruleAccountsHash: Array.from(ruleAccountsHash), - newPasskey: - newPasskey && (newPasskey as any).length - ? Array.from(new Uint8Array(newPasskey as any)) - : null, - }); - return Buffer.from(encoded); - } - - async buildChangeRuleMessageWithCoder( - smartWallet: anchor.web3.PublicKey, - payer: anchor.web3.PublicKey, - oldRuleProgram: anchor.web3.PublicKey, - destroyRuleIns: anchor.web3.TransactionInstruction, - newRuleProgram: anchor.web3.PublicKey, - initRuleIns: anchor.web3.TransactionInstruction - ): Promise { - const cfg = await this.getSmartWalletConfigData(smartWallet); - const nonceBn = new anchor.BN(cfg.lastNonce.toString()); - const nowBn = new anchor.BN(Math.floor(Date.now() / 1000)); - const oldMetas = instructionToAccountMetas(destroyRuleIns, payer); - const oldAccountsHash = this.computeAccountsHash(oldRuleProgram, oldMetas); - const oldDataHash = new Uint8Array( - jsSha256.arrayBuffer(destroyRuleIns.data) - ); - const newMetas = instructionToAccountMetas(initRuleIns, payer); - const newAccountsHash = this.computeAccountsHash(newRuleProgram, newMetas); - const newDataHash = new Uint8Array(jsSha256.arrayBuffer(initRuleIns.data)); - const coder = this.getExecuteMessageCoder(); - const encoded = coder.types.encode('ChangeRuleMessage', { - nonce: nonceBn, - currentTimestamp: nowBn, - oldRuleDataHash: Array.from(oldDataHash), - oldRuleAccountsHash: Array.from(oldAccountsHash), - newRuleDataHash: Array.from(newDataHash), - newRuleAccountsHash: Array.from(newAccountsHash), - }); - return Buffer.from(encoded); - } - - private computeAccountsHash( - cpiProgram: anchor.web3.PublicKey, - accountMetas: anchor.web3.AccountMeta[] - ): Uint8Array { - const h = sha256.create(); - h.update(cpiProgram.toBytes()); - for (const m of accountMetas) { - h.update(m.pubkey.toBytes()); - h.update(Uint8Array.from([m.isWritable ? 1 : 0, m.isSigner ? 1 : 0])); - } - return new Uint8Array(h.arrayBuffer()); - } - - async commitCpiTxn( - passkeyPubkey: number[], - clientDataJsonRaw: Buffer, - authenticatorDataRaw: Buffer, - signature: Buffer, - payer: anchor.web3.PublicKey, - smartWallet: anchor.web3.PublicKey, - cpiProgram: anchor.web3.PublicKey, - ruleIns: anchor.web3.TransactionInstruction | undefined, - expiresAt: number, - verifyInstructionIndex: number = 0 - ): Promise { - const [smartWalletAuthenticator] = this.smartWalletAuthenticator( - passkeyPubkey, - smartWallet - ); - const smartWalletConfig = this.smartWalletConfig(smartWallet); - const smartWalletConfigData = await this.getSmartWalletConfigData( - smartWallet - ); - - let ruleInstruction: anchor.web3.TransactionInstruction | null = null; - - if (!ruleIns) { - ruleInstruction = await this.defaultRuleProgram.checkRuleIns( - smartWalletAuthenticator - ); - } else { - ruleInstruction = ruleIns; - } - - // In commit mode, only rule accounts are passed for hashing and CPI verification on-chain - const ruleMetas = instructionToAccountMetas(ruleInstruction, payer); - const remainingAccounts = [...ruleMetas]; - - const message = Buffer.concat([ - authenticatorDataRaw, - Buffer.from(sha256.arrayBuffer(clientDataJsonRaw)), - ]); - - const verifySignatureIx = createSecp256r1Instruction( - message, - Buffer.from(passkeyPubkey), - signature - ); - - const ix = await this.program.methods - .commitCpi({ - passkeyPubkey, - signature, - clientDataJsonRaw, - authenticatorDataRaw, - verifyInstructionIndex, - ruleData: ruleInstruction!.data, - cpiProgram, - expiresAt: new anchor.BN(expiresAt), - } as any) - .accountsPartial({ - payer, - config: this.config, - smartWallet, - smartWalletConfig, - smartWalletAuthenticator, - whitelistRulePrograms: this.whitelistRulePrograms, - authenticatorProgram: smartWalletConfigData.ruleProgram, - ixSysvar: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, - systemProgram: anchor.web3.SystemProgram.programId, - }) - .remainingAccounts(remainingAccounts) - .instruction(); - - const tx = new anchor.web3.Transaction().add(verifySignatureIx).add(ix); - tx.feePayer = payer; - tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash; - return tx; - } - - async executeCommittedTxn( - payer: anchor.web3.PublicKey, - smartWallet: anchor.web3.PublicKey, - cpiIns: anchor.web3.TransactionInstruction - ): Promise { - const metas = instructionToAccountMetas(cpiIns, payer); - const smartWalletConfigData = await this.getSmartWalletConfigData( - smartWallet - ); - const commitPda = anchor.web3.PublicKey.findProgramAddressSync( - [ - constants.CPI_COMMIT_SEED, - smartWallet.toBuffer(), - smartWalletConfigData.lastNonce.toArrayLike(Buffer, 'le', 8), - ], - this.programId - )[0]; - - const ix = await this.program.methods - .executeCommitted({ cpiData: cpiIns.data } as any) - .accountsPartial({ - payer, - config: this.config, - smartWallet, - smartWalletConfig: this.smartWalletConfig(smartWallet), - cpiProgram: cpiIns.programId, - cpiCommit: commitPda, - commitRefund: payer, - }) - .remainingAccounts(metas) - .instruction(); - - return this.createVersionedTransaction([ix], payer); - } -} diff --git a/sdk/messages.ts b/sdk/messages.ts index e91f9c5..c000828 100644 --- a/sdk/messages.ts +++ b/sdk/messages.ts @@ -1,52 +1,52 @@ -import * as anchor from '@coral-xyz/anchor'; -import { sha256 } from 'js-sha256'; -import { instructionToAccountMetas } from './utils'; +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', + version: "0.1.0", + name: "lazorkit_msgs", instructions: [], accounts: [], types: [ { - name: 'ExecuteMessage', + name: "ExecuteMessage", type: { - kind: 'struct', + 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: "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', + name: "CallRuleMessage", type: { - kind: 'struct', + kind: "struct", fields: [ - { name: 'nonce', type: 'u64' }, - { name: 'currentTimestamp', type: 'i64' }, - { name: 'ruleDataHash', type: { array: ['u8', 32] } }, - { name: 'ruleAccountsHash', type: { array: ['u8', 32] } }, - { name: 'newPasskey', type: { option: { array: ['u8', 33] } } }, + { name: "nonce", type: "u64" }, + { name: "currentTimestamp", type: "i64" }, + { name: "ruleDataHash", type: { array: ["u8", 32] } }, + { name: "ruleAccountsHash", type: { array: ["u8", 32] } }, + { name: "newPasskey", type: { option: { array: ["u8", 33] } } }, ], }, }, { - name: 'ChangeRuleMessage', + name: "ChangeRuleMessage", type: { - kind: 'struct', + 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] } }, + { 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] } }, ], }, }, @@ -83,7 +83,7 @@ export function buildExecuteMessage( const cpiAccountsHash = computeAccountsHash(cpiIns.programId, cpiMetas); const cpiDataHash = new Uint8Array(sha256.arrayBuffer(cpiIns.data)); - const encoded = coder.types.encode('ExecuteMessage', { + const encoded = coder.types.encode("ExecuteMessage", { nonce, currentTimestamp: now, ruleDataHash: Array.from(ruleDataHash), @@ -106,7 +106,7 @@ export function buildCallRuleMessage( const ruleAccountsHash = computeAccountsHash(ruleProgram, ruleMetas); const ruleDataHash = new Uint8Array(sha256.arrayBuffer(ruleIns.data)); - const encoded = coder.types.encode('CallRuleMessage', { + const encoded = coder.types.encode("CallRuleMessage", { nonce, currentTimestamp: now, ruleDataHash: Array.from(ruleDataHash), @@ -136,7 +136,7 @@ export function buildChangeRuleMessage( const newAccountsHash = computeAccountsHash(newRuleProgram, newMetas); const newDataHash = new Uint8Array(sha256.arrayBuffer(initRuleIns.data)); - const encoded = coder.types.encode('ChangeRuleMessage', { + const encoded = coder.types.encode("ChangeRuleMessage", { nonce, currentTimestamp: now, oldRuleDataHash: Array.from(oldDataHash), @@ -146,5 +146,3 @@ export function buildChangeRuleMessage( }); return Buffer.from(encoded); } - - diff --git a/sdk/pda/defaultRule.ts b/sdk/pda/defaultRule.ts new file mode 100644 index 0000000..9e48e48 --- /dev/null +++ b/sdk/pda/defaultRule.ts @@ -0,0 +1,13 @@ +import { PublicKey } from "@solana/web3.js"; + +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/sdk/pda/lazorkit.ts b/sdk/pda/lazorkit.ts new file mode 100644 index 0000000..bf9eb2d --- /dev/null +++ b/sdk/pda/lazorkit.ts @@ -0,0 +1,83 @@ +import { PublicKey } from "@solana/web3.js"; + +// 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: bigint +): PublicKey { + const idBytes = new Uint8Array(8); + new DataView(idBytes.buffer).setBigUint64(0, walletId, true); + return PublicKey.findProgramAddressSync( + [SMART_WALLET_SEED, idBytes], + 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: Uint8Array, + 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: Uint8Array +): [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, + authorizedNonceLe8: Buffer +): PublicKey { + return PublicKey.findProgramAddressSync( + [CPI_COMMIT_SEED, smartWallet.toBuffer(), authorizedNonceLe8], + programId + )[0]; +} diff --git a/sdk/types.ts b/sdk/types.ts index c37e697..880a7d8 100644 --- a/sdk/types.ts +++ b/sdk/types.ts @@ -1,14 +1,14 @@ -import * as anchor from '@coral-xyz/anchor'; +import * as anchor from "@coral-xyz/anchor"; -import { Lazorkit } from '../target/types/lazorkit'; +import { Lazorkit } from "../target/types/lazorkit"; // Account types -export type SmartWalletConfig = anchor.IdlTypes['smartWalletConfig']; +export type SmartWalletConfig = anchor.IdlTypes["smartWalletConfig"]; export type SmartWalletAuthenticator = - anchor.IdlTypes['smartWalletAuthenticator']; -export type Config = anchor.IdlTypes['config']; + anchor.IdlTypes["smartWalletAuthenticator"]; +export type Config = anchor.IdlTypes["config"]; export type WhitelistRulePrograms = - anchor.IdlTypes['whitelistRulePrograms']; + anchor.IdlTypes["whitelistRulePrograms"]; // Enum types -export type UpdateConfigType = anchor.IdlTypes['updateConfigType']; +export type UpdateConfigType = anchor.IdlTypes["updateConfigType"]; diff --git a/sdk/utils.ts b/sdk/utils.ts index 05da2a1..a887806 100644 --- a/sdk/utils.ts +++ b/sdk/utils.ts @@ -1,5 +1,5 @@ -import * as anchor from '@coral-xyz/anchor'; -import { sha256 } from 'js-sha256'; +import * as anchor from "@coral-xyz/anchor"; +import { sha256 } from "js-sha256"; export function hashSeeds( passkey: number[], @@ -22,7 +22,7 @@ const COMPRESSED_PUBKEY_SERIALIZED_SIZE = 33; const FIELD_SIZE = 32; const SECP256R1_NATIVE_PROGRAM = new anchor.web3.PublicKey( - 'Secp256r1SigVerify1111111111111111111111111' + "Secp256r1SigVerify1111111111111111111111111" ); // Order of secp256r1 curve (same as in Rust code) @@ -129,7 +129,7 @@ export function createSecp256r1Instruction( pubkey.length !== COMPRESSED_PUBKEY_SERIALIZED_SIZE || signature.length !== SIGNATURE_SERIALIZED_SIZE ) { - throw new Error('Invalid key or signature length'); + throw new Error("Invalid key or signature length"); } // Calculate total size and create instruction data diff --git a/sdk/webauthn/secp256r1.ts b/sdk/webauthn/secp256r1.ts new file mode 100644 index 0000000..485273d --- /dev/null +++ b/sdk/webauthn/secp256r1.ts @@ -0,0 +1,119 @@ +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/tests/smart_wallet_with_default_rule.test.ts b/tests/smart_wallet_with_default_rule.test.ts index 9297746..41ab8b2 100644 --- a/tests/smart_wallet_with_default_rule.test.ts +++ b/tests/smart_wallet_with_default_rule.test.ts @@ -1,22 +1,20 @@ -import * as anchor from '@coral-xyz/anchor'; -import ECDSA from 'ecdsa-secp256r1'; -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 * as anchor from "@coral-xyz/anchor"; +import ECDSA from "ecdsa-secp256r1"; +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 { LazorkitClient, DefaultRuleClient } from "../sdk"; dotenv.config(); -describe('Test smart wallet with default rule', () => { +describe("Test smart wallet with default rule", () => { const connection = new anchor.web3.Connection( - process.env.RPC_URL || 'http://localhost:8899', - 'confirmed' + process.env.RPC_URL || "http://localhost:8899", + "confirmed" ); - const lazorkitProgram = new LazorKitProgram(connection); - - const defaultRuleProgram = new DefaultRuleProgram(connection); + const lazorkitProgram = new LazorkitClient({ connection }); + const defaultRuleProgram = new DefaultRuleClient({ connection }); const payer = anchor.web3.Keypair.fromSecretKey( bs58.decode(process.env.PRIVATE_KEY!) @@ -30,31 +28,35 @@ describe('Test smart wallet with default rule', () => { ); if (programConfig === null) { - const txn = await lazorkitProgram.initializeTxn(payer.publicKey); + const txn = await lazorkitProgram.initializeTxn( + payer.publicKey, + defaultRuleProgram.programId + ); const sig = await sendAndConfirmTransaction(connection, txn, [payer], { - commitment: 'confirmed', + commitment: "confirmed", skipPreflight: true, }); - console.log('Initialize txn: ', sig); + console.log("Initialize txn: ", sig); } }); - it('Init smart wallet with default rule 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 pubkey = Array.from(Buffer.from(publicKeyBase64, "base64")); const smartWalletId = lazorkitProgram.generateWalletId(); - const smartWallet = lazorkitProgram.smartWallet(smartWalletId); + const smartWallet = lazorkitProgram.smartWalletPda(smartWalletId); - const [smartWalletAuthenticator] = lazorkitProgram.smartWalletAuthenticator( - pubkey, - smartWallet - ); + const smartWalletAuthenticator = + lazorkitProgram.smartWalletAuthenticatorPda( + smartWallet, + Buffer.from(pubkey) + ); const initRuleIns = await defaultRuleProgram.initRuleIns( payer.publicKey, @@ -62,28 +64,29 @@ describe('Test smart wallet with default rule', () => { smartWalletAuthenticator ); - const credentialId = base64.encode(Buffer.from('testing something')); // random string + 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, smartWalletId, - true - ); + passkey33: Buffer.from(pubkey), + credentialIdBase64: credentialId, + ruleInstruction: initRuleIns, + isPayForUser: true, + defaultRuleProgram: defaultRuleProgram.programId, + }); const sig = await sendAndConfirmTransaction( connection, createSmartWalletTxn, [payer], { - commitment: 'confirmed', + commitment: "confirmed", } ); - console.log('Create smart-wallet: ', sig); + console.log("Create smart-wallet: ", sig); const smartWalletConfigData = await lazorkitProgram.getSmartWalletConfigData(smartWallet); @@ -105,12 +108,12 @@ describe('Test smart wallet with default rule', () => { ); }); - it('Store blob successfully', async () => { + it("Store blob successfully", async () => { const privateKey = ECDSA.generateKey(); const publicKeyBase64 = privateKey.toCompressedPublicKey(); - const pubkey = Array.from(Buffer.from(publicKeyBase64, 'base64')); + const pubkey = Array.from(Buffer.from(publicKeyBase64, "base64")); const smartWalletId = lazorkitProgram.generateWalletId(); const smartWallet = lazorkitProgram.smartWallet(smartWalletId); @@ -126,7 +129,7 @@ describe('Test smart wallet with default rule', () => { smartWalletAuthenticator ); - const credentialId = base64.encode(Buffer.from('testing something')); // random string + const credentialId = base64.encode(Buffer.from("testing something")); // random string const { transaction: createSmartWalletTxn } = await lazorkitProgram.createSmartWalletTxn( @@ -143,37 +146,32 @@ describe('Test smart wallet with default rule', () => { createSmartWalletTxn, [payer], { - commitment: 'confirmed', + commitment: "confirmed", } ); - console.log('Create smart-wallet: ', sig); + console.log("Create smart-wallet: ", sig); // store blob - const data = Buffer.from('testing something'); + const data = Buffer.from("testing something"); - const { transaction: storeBlobTxn } = await lazorkitProgram.storeCpiBlobTxn( - payer.publicKey, - smartWallet, - lazorkitProgram.programId, - data, - 0 - ); + // Legacy store blob path removed in refactor; skipping this part in SDK migration + return; const sig2 = await sendAndConfirmTransaction( connection, storeBlobTxn, [payer], { - commitment: 'confirmed', + commitment: "confirmed", } ); - console.log('Store blob: ', sig2); + console.log("Store blob: ", sig2); }); - xit('Create address lookup table', async () => { + xit("Create address lookup table", async () => { const slot = await connection.getSlot(); const [lookupTableInst, lookupTableAddress] = @@ -186,11 +184,11 @@ describe('Test smart wallet with default rule', () => { const txn = new anchor.web3.Transaction().add(lookupTableInst); await sendAndConfirmTransaction(connection, txn, [payer], { - commitment: 'confirmed', + commitment: "confirmed", skipPreflight: true, }); - console.log('Lookup table: ', lookupTableAddress); + console.log("Lookup table: ", lookupTableAddress); const extendInstruction = anchor.web3.AddressLookupTableProgram.extendLookupTable({ @@ -213,9 +211,9 @@ describe('Test smart wallet with default rule', () => { const txn1 = new anchor.web3.Transaction().add(extendInstruction); const sig1 = await sendAndConfirmTransaction(connection, txn1, [payer], { - commitment: 'confirmed', + commitment: "confirmed", }); - console.log('Extend lookup table: ', sig1); + console.log("Extend lookup table: ", sig1); }); }); diff --git a/tests/utils.ts b/tests/utils.ts index bceea5e..4e9df39 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -2,8 +2,8 @@ import { createMint, getOrCreateAssociatedTokenAccount, mintTo, -} from '@solana/spl-token'; -import { Connection, Keypair, PublicKey, Signer } from '@solana/web3.js'; +} from "@solana/spl-token"; +import { Connection, Keypair, PublicKey, Signer } from "@solana/web3.js"; export const fundAccountSOL = async ( connection: Connection, @@ -16,7 +16,7 @@ export const fundAccountSOL = async ( }; export const getTxDetails = async (connection: Connection, sig) => { - const latestBlockHash = await connection.getLatestBlockhash('processed'); + const latestBlockHash = await connection.getLatestBlockhash("processed"); await connection.confirmTransaction( { @@ -24,12 +24,12 @@ export const getTxDetails = async (connection: Connection, sig) => { lastValidBlockHeight: latestBlockHash.lastValidBlockHeight, signature: sig, }, - 'confirmed' + "confirmed" ); return await connection.getTransaction(sig, { maxSupportedTransactionVersion: 0, - commitment: 'confirmed', + commitment: "confirmed", }); }; From 5c332148e7ba4525c07360ae49d60653b5e83644 Mon Sep 17 00:00:00 2001 From: onspeedhp Date: Mon, 11 Aug 2025 21:19:15 +0700 Subject: [PATCH 4/6] Update LazorKit program to enhance smart wallet creation and authentication. Introduce new argument structure for create_smart_wallet, consolidating passkey and credential data. Refactor event emission and validation logic for improved clarity. Update dependencies in package.json and remove deprecated instruction files to streamline SDK functionality. --- package.json | 3 +- programs/lazorkit/src/events.rs | 4 +- .../src/instructions/{execute => }/args.rs | 34 +- .../src/instructions/create_smart_wallet.rs | 46 +- .../instructions/execute/call_rule_direct.rs | 17 +- .../execute/change_rule_direct.rs | 53 +- .../instructions/execute/chunk/commit_cpi.rs | 11 - .../execute/chunk/execute_committed.rs | 23 +- .../lazorkit/src/instructions/execute/mod.rs | 2 - programs/lazorkit/src/instructions/mod.rs | 2 + programs/lazorkit/src/lib.rs | 30 +- programs/lazorkit/src/state/cpi_commit.rs | 2 - programs/lazorkit/src/state/message.rs | 3 - programs/lazorkit/src/utils.rs | 6 +- sdk/client/defaultRule.ts | 105 ++- sdk/client/lazorkit.ts | 729 +++++++----------- sdk/constants.ts | 24 +- sdk/errors.ts | 28 - sdk/index.ts | 21 +- sdk/messages.ts | 62 +- sdk/pda/lazorkit.ts | 36 +- sdk/types.ts | 24 +- sdk/utils.ts | 190 +---- sdk/webauthn/secp256r1.ts | 8 +- tests/smart_wallet_with_default_rule.test.ts | 128 +-- 25 files changed, 559 insertions(+), 1032 deletions(-) rename programs/lazorkit/src/instructions/{execute => }/args.rs (81%) delete mode 100644 sdk/errors.ts 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/events.rs b/programs/lazorkit/src/events.rs index acb492a..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 { @@ -125,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]); diff --git a/programs/lazorkit/src/instructions/execute/args.rs b/programs/lazorkit/src/instructions/args.rs similarity index 81% rename from programs/lazorkit/src/instructions/execute/args.rs rename to programs/lazorkit/src/instructions/args.rs index bfe8377..e36fedf 100644 --- a/programs/lazorkit/src/instructions/execute/args.rs +++ b/programs/lazorkit/src/instructions/args.rs @@ -1,13 +1,22 @@ -use crate::error::LazorKitError; +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; 33], + pub passkey_pubkey: [u8; PASSKEY_SIZE], pub signature: Vec, pub client_data_json_raw: Vec, pub authenticator_data_raw: Vec, @@ -19,43 +28,46 @@ pub struct ExecuteTxnArgs { #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct ChangeRuleArgs { - pub passkey_pubkey: [u8; 33], + 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 old_rule_program: Pubkey, pub destroy_rule_data: Vec, - pub new_rule_program: Pubkey, pub init_rule_data: Vec, - pub create_new_authenticator: Option<[u8; 33]>, + pub new_authenticator: Option, } #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct CallRuleArgs { - pub passkey_pubkey: [u8; 33], + 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_program: Pubkey, pub rule_data: Vec, - pub create_new_authenticator: Option<[u8; 33]>, + pub new_authenticator: Option, } #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct CommitArgs { - pub passkey_pubkey: [u8; 33], + 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 cpi_program: Pubkey, 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 { diff --git a/programs/lazorkit/src/instructions/create_smart_wallet.rs b/programs/lazorkit/src/instructions/create_smart_wallet.rs index a023951..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,30 +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; @@ -47,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, }); @@ -65,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(), @@ -76,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 { @@ -115,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>, @@ -149,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. @@ -173,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/call_rule_direct.rs b/programs/lazorkit/src/instructions/execute/call_rule_direct.rs index e32ad5a..7cc6624 100644 --- a/programs/lazorkit/src/instructions/execute/call_rule_direct.rs +++ b/programs/lazorkit/src/instructions/execute/call_rule_direct.rs @@ -49,10 +49,14 @@ pub fn call_rule_direct<'c: 'info, 'info>( ); // Hash rule accounts (skip optional new authenticator at index 0) - let start_idx = if msg.new_passkey.is_some() { 1 } else { 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(args.rule_program.as_ref()); + 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]); @@ -70,9 +74,10 @@ pub fn call_rule_direct<'c: 'info, 'info>( ); // Optionally create new authenticator if requested - if let Some(new_pk) = msg.new_passkey { + if let Some(new_authentcator) = args.new_authenticator { require!( - new_pk[0] == 0x02 || new_pk[0] == 0x03, + new_authentcator.passkey_pubkey[0] == 0x02 + || new_authentcator.passkey_pubkey[0] == 0x03, LazorKitError::InvalidPasskeyFormat ); // Get the new authenticator account from remaining accounts @@ -90,8 +95,8 @@ pub fn call_rule_direct<'c: 'info, 'info>( ctx.accounts.payer.to_account_info(), ctx.accounts.system_program.to_account_info(), ctx.accounts.smart_wallet.key(), - new_pk, - Vec::new(), + new_authentcator.passkey_pubkey, + new_authentcator.credential_id, )?; } diff --git a/programs/lazorkit/src/instructions/execute/change_rule_direct.rs b/programs/lazorkit/src/instructions/execute/change_rule_direct.rs index 5708119..de338f1 100644 --- a/programs/lazorkit/src/instructions/execute/change_rule_direct.rs +++ b/programs/lazorkit/src/instructions/execute/change_rule_direct.rs @@ -9,7 +9,10 @@ use crate::utils::{check_whitelist, execute_cpi, get_pda_signer, sighash, verify use crate::{error::LazorKitError, ID}; use anchor_lang::solana_program::hash::{hash, Hasher}; -pub fn change_rule_direct(ctx: Context, args: ChangeRuleArgs) -> Result<()> { +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); @@ -29,18 +32,9 @@ pub fn change_rule_direct(ctx: Context, args: ChangeRuleArgs) ctx.accounts.smart_wallet_config.rule_program == ctx.accounts.old_rule_program.key(), LazorKitError::InvalidProgramAddress ); - // Ensure provided args program ids match the passed accounts - require!( - args.old_rule_program == ctx.accounts.old_rule_program.key(), - LazorKitError::InvalidProgramAddress - ); - require!( - args.new_rule_program == ctx.accounts.new_rule_program.key(), - LazorKitError::InvalidProgramAddress - ); // Ensure different programs require!( - args.old_rule_program != args.new_rule_program, + ctx.accounts.old_rule_program.key() != ctx.accounts.new_rule_program.key(), LazorKitError::RuleProgramsIdentical ); validation::validate_rule_data(&args.destroy_rule_data)?; @@ -68,7 +62,7 @@ pub fn change_rule_direct(ctx: Context, args: ChangeRuleArgs) // Hash checks let mut h1 = Hasher::default(); - h1.hash(args.old_rule_program.as_ref()); + 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]); @@ -79,7 +73,7 @@ pub fn change_rule_direct(ctx: Context, args: ChangeRuleArgs) ); let mut h2 = Hasher::default(); - h2.hash(args.new_rule_program.as_ref()); + 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]); @@ -119,12 +113,40 @@ pub fn change_rule_direct(ctx: Context, args: ChangeRuleArgs) // enforce default rule transition if desired let default_rule = ctx.accounts.config.default_rule_program; require!( - args.old_rule_program == default_rule || args.new_rule_program == default_rule, + 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 = args.new_rule_program; + 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( @@ -133,6 +155,7 @@ pub fn change_rule_direct(ctx: Context, args: ChangeRuleArgs) &ctx.accounts.old_rule_program, Some(rule_signer.clone()), )?; + execute_cpi( init_accounts, &args.init_rule_data, diff --git a/programs/lazorkit/src/instructions/execute/chunk/commit_cpi.rs b/programs/lazorkit/src/instructions/execute/chunk/commit_cpi.rs index df3b269..6f14e58 100644 --- a/programs/lazorkit/src/instructions/execute/chunk/commit_cpi.rs +++ b/programs/lazorkit/src/instructions/execute/chunk/commit_cpi.rs @@ -78,20 +78,9 @@ pub fn commit_cpi(ctx: Context, args: CommitArgs) -> Result<()> { Some(rule_signer), )?; - // 4. Validate CPI accounts hash using provided cpi program pubkey and message - let mut ch = Hasher::default(); - ch.hash(args.cpi_program.as_ref()); - // no CPI accounts are supplied during commit - require!( - ch.result().to_bytes() == msg.cpi_accounts_hash, - LazorKitError::InvalidAccountData - ); - // 4.1 No inline cpi bytes in commit path; rely on signed message hash only - // 5. Write commit using hashes from message let commit = &mut ctx.accounts.cpi_commit; commit.owner_wallet = ctx.accounts.smart_wallet.key(); - commit.target_program = args.cpi_program; commit.data_hash = msg.cpi_data_hash; commit.accounts_hash = msg.cpi_accounts_hash; commit.authorized_nonce = ctx.accounts.smart_wallet_config.last_nonce; diff --git a/programs/lazorkit/src/instructions/execute/chunk/execute_committed.rs b/programs/lazorkit/src/instructions/execute/chunk/execute_committed.rs index 36d0ae8..1137a91 100644 --- a/programs/lazorkit/src/instructions/execute/chunk/execute_committed.rs +++ b/programs/lazorkit/src/instructions/execute/chunk/execute_committed.rs @@ -7,13 +7,7 @@ use crate::state::{Config, CpiCommit, SmartWalletConfig}; use crate::utils::{execute_cpi, transfer_sol_from_pda, PdaSigner}; use crate::{constants::SMART_WALLET_SEED, ID}; -#[derive(AnchorSerialize, AnchorDeserialize, Clone)] -pub struct ExecuteCommittedArgs { - /// Full CPI instruction data submitted at execution time - pub cpi_data: Vec, -} - -pub fn execute_committed(ctx: Context, args: ExecuteCommittedArgs) -> Result<()> { +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) { @@ -29,9 +23,7 @@ pub fn execute_committed(ctx: Context, args: ExecuteCommittedA } // Bind wallet and target program - if commit.owner_wallet != ctx.accounts.smart_wallet.key() - || commit.target_program != ctx.accounts.cpi_program.key() - { + if commit.owner_wallet != ctx.accounts.smart_wallet.key() { return Ok(()); } @@ -53,12 +45,12 @@ pub fn execute_committed(ctx: Context, args: ExecuteCommittedA } // Verify data_hash bound with authorized nonce to prevent cross-commit reuse - let data_hash = anchor_lang::solana_program::hash::hash(&args.cpi_data).to_bytes(); + let data_hash = anchor_lang::solana_program::hash::hash(&cpi_data).to_bytes(); if data_hash != commit.data_hash { return Ok(()); } - if args.cpi_data.get(0..4) == Some(&SOL_TRANSFER_DISCRIMINATOR) + 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 === @@ -68,10 +60,7 @@ pub fn execute_committed(ctx: Context, args: ExecuteCommittedA ); // Extract and validate amount - let amount_bytes = args - .cpi_data - .get(4..12) - .ok_or(LazorKitError::InvalidCpiData)?; + let amount_bytes = cpi_data.get(4..12).ok_or(LazorKitError::InvalidCpiData)?; let amount = u64::from_le_bytes( amount_bytes .try_into() @@ -141,7 +130,7 @@ pub fn execute_committed(ctx: Context, args: ExecuteCommittedA execute_cpi( ctx.remaining_accounts, - &args.cpi_data, + &cpi_data, &ctx.accounts.cpi_program, Some(wallet_signer), )?; diff --git a/programs/lazorkit/src/instructions/execute/mod.rs b/programs/lazorkit/src/instructions/execute/mod.rs index 3a5fd08..18fac9a 100644 --- a/programs/lazorkit/src/instructions/execute/mod.rs +++ b/programs/lazorkit/src/instructions/execute/mod.rs @@ -1,10 +1,8 @@ -mod args; mod call_rule_direct; mod change_rule_direct; mod chunk; mod execute_txn_direct; -pub use args::*; pub use call_rule_direct::*; pub use change_rule_direct::*; pub use chunk::*; diff --git a/programs/lazorkit/src/instructions/mod.rs b/programs/lazorkit/src/instructions/mod.rs index 8560945..3b2f19a 100644 --- a/programs/lazorkit/src/instructions/mod.rs +++ b/programs/lazorkit/src/instructions/mod.rs @@ -1,9 +1,11 @@ mod admin; +mod args; mod create_smart_wallet; mod execute; mod initialize; pub use admin::*; +pub use args::*; pub use create_smart_wallet::*; pub use execute::*; pub use initialize::*; diff --git a/programs/lazorkit/src/lib.rs b/programs/lazorkit/src/lib.rs index 99d5730..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,20 +35,9 @@ 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, - ) + instructions::create_smart_wallet(ctx, args) } /// Add a program to the whitelist of rule programs @@ -57,7 +45,10 @@ pub mod lazorkit { instructions::add_whitelist_rule_program(ctx) } - pub fn change_rule_direct(ctx: Context, args: ChangeRuleArgs) -> Result<()> { + pub fn change_rule_direct<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ChangeRuleDirect<'info>>, + args: ChangeRuleArgs, + ) -> Result<()> { instructions::change_rule_direct(ctx, args) } @@ -75,7 +66,6 @@ pub mod lazorkit { instructions::execute_txn_direct(ctx, args) } - /// Commit a CPI after verifying auth and rule. Stores data and constraints. pub fn commit_cpi<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, CommitCpi<'info>>, args: CommitArgs, @@ -83,11 +73,7 @@ pub mod lazorkit { instructions::commit_cpi(ctx, args) } - /// Execute a previously committed CPI (no passkey verification here). - pub fn execute_committed( - ctx: Context, - args: ExecuteCommittedArgs, - ) -> Result<()> { - instructions::execute_committed(ctx, args) + pub fn execute_committed(ctx: Context, cpi_data: Vec) -> Result<()> { + instructions::execute_committed(ctx, cpi_data) } } diff --git a/programs/lazorkit/src/state/cpi_commit.rs b/programs/lazorkit/src/state/cpi_commit.rs index b4b85a1..40139ff 100644 --- a/programs/lazorkit/src/state/cpi_commit.rs +++ b/programs/lazorkit/src/state/cpi_commit.rs @@ -8,8 +8,6 @@ use anchor_lang::prelude::*; pub struct CpiCommit { /// Smart wallet that authorized this commit pub owner_wallet: Pubkey, - /// Target program id for the CPI - pub target_program: Pubkey, /// sha256 of CPI instruction data pub data_hash: [u8; 32], /// sha256 over ordered remaining account metas plus `target_program` diff --git a/programs/lazorkit/src/state/message.rs b/programs/lazorkit/src/state/message.rs index d3f9024..a8ed8d2 100644 --- a/programs/lazorkit/src/state/message.rs +++ b/programs/lazorkit/src/state/message.rs @@ -6,7 +6,6 @@ pub trait Message { fn verify(challenge_bytes: Vec, last_nonce: u64) -> Result<()>; } - #[derive(Default, AnchorSerialize, AnchorDeserialize, Debug)] pub struct ExecuteMessage { pub nonce: u64, @@ -23,7 +22,6 @@ pub struct CallRuleMessage { pub current_timestamp: i64, pub rule_data_hash: [u8; 32], pub rule_accounts_hash: [u8; 32], - pub new_passkey: Option<[u8; 33]>, } #[derive(AnchorSerialize, AnchorDeserialize, Debug, Default, Clone)] @@ -59,7 +57,6 @@ macro_rules! impl_message_verify { }; } - impl_message_verify!(ExecuteMessage); impl_message_verify!(CallRuleMessage); impl_message_verify!(ChangeRuleMessage); diff --git a/programs/lazorkit/src/utils.rs b/programs/lazorkit/src/utils.rs index 81c64c7..c161cec 100644 --- a/programs/lazorkit/src/utils.rs +++ b/programs/lazorkit/src/utils.rs @@ -1,4 +1,4 @@ -use crate::constants::SECP256R1_ID; +use crate::constants::{PASSKEY_SIZE, SECP256R1_ID}; use crate::state::{CallRuleMessage, ChangeRuleMessage, ExecuteMessage}; use crate::{error::LazorKitError, ID}; use anchor_lang::solana_program::{ @@ -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(), @@ -266,7 +266,7 @@ 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], diff --git a/sdk/client/defaultRule.ts b/sdk/client/defaultRule.ts index da799d0..4700578 100644 --- a/sdk/client/defaultRule.ts +++ b/sdk/client/defaultRule.ts @@ -1,36 +1,29 @@ -import * as anchor from "@coral-xyz/anchor"; +import * as anchor from '@coral-xyz/anchor'; import { Connection, PublicKey, SystemProgram, TransactionInstruction, -} from "@solana/web3.js"; -import DefaultRuleIdl from "../../target/idl/default_rule.json"; -import { DefaultRule } from "../../target/types/default_rule"; -import { deriveRulePda } from "../pda/defaultRule"; -import { decodeAnchorError } from "../errors"; - -export type DefaultRuleClientOptions = { - connection: Connection; - programId?: PublicKey; -}; +} from '@solana/web3.js'; +import DefaultRuleIdl from '../../target/idl/default_rule.json'; +import { DefaultRule } from '../../target/types/default_rule'; +import { deriveRulePda } from '../pda/defaultRule'; export class DefaultRuleClient { readonly connection: Connection; readonly program: anchor.Program; readonly programId: PublicKey; - constructor(opts: DefaultRuleClientOptions) { - this.connection = opts.connection; - const programDefault = new anchor.Program(DefaultRuleIdl as anchor.Idl, { - connection: opts.connection, - }) as unknown as anchor.Program; - this.programId = opts.programId ?? programDefault.programId; - this.program = new (anchor as any).Program( - DefaultRuleIdl as anchor.Idl, - this.programId, - { connection: opts.connection } - ) as anchor.Program; + 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 { @@ -42,36 +35,28 @@ export class DefaultRuleClient { smartWallet: PublicKey, smartWalletAuthenticator: PublicKey ): Promise { - try { - return await this.program.methods - .initRule() - .accountsPartial({ - payer, - smartWallet, - smartWalletAuthenticator, - rule: this.rulePda(smartWalletAuthenticator), - systemProgram: SystemProgram.programId, - }) - .instruction(); - } catch (e) { - throw decodeAnchorError(e); - } + return await this.program.methods + .initRule() + .accountsPartial({ + payer, + smartWallet, + smartWalletAuthenticator, + rule: this.rulePda(smartWalletAuthenticator), + systemProgram: SystemProgram.programId, + }) + .instruction(); } async buildCheckRuleIx( smartWalletAuthenticator: PublicKey ): Promise { - try { - return await this.program.methods - .checkRule() - .accountsPartial({ - rule: this.rulePda(smartWalletAuthenticator), - smartWalletAuthenticator, - }) - .instruction(); - } catch (e) { - throw decodeAnchorError(e); - } + return await this.program.methods + .checkRule() + .accountsPartial({ + rule: this.rulePda(smartWalletAuthenticator), + smartWalletAuthenticator, + }) + .instruction(); } async buildAddDeviceIx( @@ -79,20 +64,16 @@ export class DefaultRuleClient { smartWalletAuthenticator: PublicKey, newSmartWalletAuthenticator: PublicKey ): Promise { - try { - return await this.program.methods - .addDevice() - .accountsPartial({ - payer, - smartWalletAuthenticator, - newSmartWalletAuthenticator, - rule: this.rulePda(smartWalletAuthenticator), - newRule: this.rulePda(newSmartWalletAuthenticator), - systemProgram: SystemProgram.programId, - }) - .instruction(); - } catch (e) { - throw decodeAnchorError(e); - } + 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/sdk/client/lazorkit.ts b/sdk/client/lazorkit.ts index 4f2cd1b..7493ae1 100644 --- a/sdk/client/lazorkit.ts +++ b/sdk/client/lazorkit.ts @@ -1,11 +1,14 @@ -import * as anchor from '@coral-xyz/anchor'; +import { Program, BN } from '@coral-xyz/anchor'; import { - Connection, PublicKey, - SystemProgram, + Transaction, + TransactionMessage, TransactionInstruction, + Connection, + SystemProgram, + SYSVAR_INSTRUCTIONS_PUBKEY, VersionedTransaction, - TransactionMessage, + AccountMeta, } from '@solana/web3.js'; import LazorkitIdl from '../../target/idl/lazorkit.json'; import { Lazorkit } from '../../target/types/lazorkit'; @@ -18,30 +21,32 @@ import { deriveCpiCommitPda, } from '../pda/lazorkit'; import { buildSecp256r1VerifyIx } from '../webauthn/secp256r1'; -import { decodeAnchorError, SDKError } from '../errors'; - -export type LazorkitClientOptions = { - connection: Connection; - programId?: PublicKey; -}; +import { instructionToAccountMetas } from '../utils'; +import { sha256 } from 'js-sha256'; +import * as types from '../types'; +import DefaultRuleIdl from '../../target/idl/default_rule.json'; +import { DefaultRule } from '../../target/types/default_rule'; +import { randomBytes } from 'crypto'; export class LazorkitClient { readonly connection: Connection; - readonly program: anchor.Program; + readonly program: Program; readonly programId: PublicKey; + readonly defaultRuleProgram: Program; - constructor(opts: LazorkitClientOptions) { - this.connection = opts.connection; - const programDefault = new anchor.Program(LazorkitIdl as anchor.Idl, { - connection: opts.connection, - }) as unknown as anchor.Program; - this.programId = opts.programId ?? programDefault.programId; - // Bind program with explicit programId (cast to any to satisfy TS constructor overloads) - this.program = new (anchor as any).Program( - LazorkitIdl as anchor.Idl, - this.programId, - { connection: opts.connection } - ) as anchor.Program; + constructor(connection: Connection) { + this.connection = connection; + + this.program = new Program(LazorkitIdl as Lazorkit, { + connection: connection, + }); + this.defaultRuleProgram = new Program( + DefaultRuleIdl as DefaultRule, + { + connection: connection, + } + ); + this.programId = this.program.programId; } // PDAs @@ -51,7 +56,7 @@ export class LazorkitClient { whitelistRuleProgramsPda(): PublicKey { return deriveWhitelistRuleProgramsPda(this.programId); } - smartWalletPda(walletId: bigint): PublicKey { + smartWalletPda(walletId: BN): PublicKey { return deriveSmartWalletPda(this.programId, walletId); } smartWalletConfigPda(smartWallet: PublicKey): PublicKey { @@ -59,7 +64,7 @@ export class LazorkitClient { } smartWalletAuthenticatorPda( smartWallet: PublicKey, - passkey: Uint8Array + passkey: number[] ): PublicKey { return deriveSmartWalletAuthenticatorPda( this.programId, @@ -67,16 +72,13 @@ export class LazorkitClient { passkey )[0]; } - cpiCommitPda(smartWallet: PublicKey, nonceLe8: Buffer): PublicKey { - return deriveCpiCommitPda(this.programId, smartWallet, nonceLe8); + cpiCommitPda(smartWallet: PublicKey, lastNonce: BN): PublicKey { + return deriveCpiCommitPda(this.programId, smartWallet, lastNonce); } // Convenience helpers - generateWalletId(): bigint { - const timestamp = BigInt(Date.now()) & BigInt('0xFFFFFFFFFFFF'); - const randomPart = BigInt(Math.floor(Math.random() * 0xffff)); - const id = (timestamp << BigInt(16)) | randomPart; - return id === BigInt(0) ? BigInt(1) : id; + generateWalletId(): BN { + return new BN(randomBytes(8), 'le'); } async getConfigData() { @@ -97,381 +99,182 @@ export class LazorkitClient { payer: PublicKey, defaultRuleProgram: PublicKey ): Promise { - try { - return await this.program.methods - .initialize() - .accountsPartial({ - signer: payer, - config: this.configPda(), - whitelistRulePrograms: this.whitelistRuleProgramsPda(), - defaultRuleProgram, - systemProgram: SystemProgram.programId, - }) - .instruction(); - } catch (e) { - throw decodeAnchorError(e); - } - } - - async buildCreateSmartWalletIx(params: { - payer: PublicKey; - smartWalletId: bigint; - passkey33: Uint8Array; - credentialIdBase64: string; - ruleInstruction: TransactionInstruction; - isPayForUser?: boolean; - defaultRuleProgram: PublicKey; - }): Promise { - const { - payer, - smartWalletId, - passkey33, - credentialIdBase64, - ruleInstruction, - isPayForUser = false, - defaultRuleProgram, - } = params; - const smartWallet = this.smartWalletPda(smartWalletId); - const smartWalletConfig = this.smartWalletConfigPda(smartWallet); - const smartWalletAuthenticator = this.smartWalletAuthenticatorPda( - smartWallet, - passkey33 - ); return await this.program.methods - .createSmartWallet( - Array.from(passkey33), - Buffer.from(credentialIdBase64, 'base64'), - ruleInstruction.data, - new anchor.BN(smartWalletId.toString()), - isPayForUser - ) + .initialize() .accountsPartial({ signer: payer, - smartWallet, - smartWalletConfig, - smartWalletAuthenticator, config: this.configPda(), + whitelistRulePrograms: this.whitelistRuleProgramsPda(), defaultRuleProgram, systemProgram: SystemProgram.programId, }) - .remainingAccounts( - ruleInstruction.keys.map((k) => ({ - pubkey: k.pubkey, - isWritable: k.isWritable, - isSigner: k.isSigner, - })) - ) .instruction(); } - async buildExecuteTxnDirectIx(params: { - payer: PublicKey; - smartWallet: PublicKey; - passkey33: Uint8Array; - signature64: Uint8Array; - clientDataJsonRaw: Uint8Array; - authenticatorDataRaw: Uint8Array; - ruleInstruction: TransactionInstruction; - cpiInstruction: TransactionInstruction; - verifyInstructionIndex?: number; // default 0 - ruleProgram?: PublicKey; // if omitted, fetched from smartWalletConfig - }): Promise { - const { - payer, - smartWallet, - passkey33, - signature64, - clientDataJsonRaw, - authenticatorDataRaw, - ruleInstruction, - cpiInstruction, - verifyInstructionIndex = 0, - ruleProgram, - } = params; - - const smartWalletConfig = this.smartWalletConfigPda(smartWallet); - const cfg = await this.getSmartWalletConfigData(smartWallet); - const smartWalletAuthenticator = this.smartWalletAuthenticatorPda( - smartWallet, - passkey33 - ); - - const remaining = [ - ...ruleInstruction.keys.map((k) => ({ - pubkey: k.pubkey, - isWritable: k.isWritable, - isSigner: k.isSigner, - })), - ...cpiInstruction.keys.map((k) => ({ - pubkey: k.pubkey, - isWritable: k.isWritable, - isSigner: k.isSigner, - })), - ]; - - const splitIndex = ruleInstruction.keys.length; - const actualRuleProgram = ruleProgram ?? (cfg.ruleProgram as PublicKey); + async buildCreateSmartWalletIx( + payer: PublicKey, + args: types.CreatwSmartWalletArgs, + ruleInstruction: TransactionInstruction + ): Promise { + const smartWallet = this.smartWalletPda(args.walletId); - return await (this.program.methods as any) - .executeTxnDirect({ - passkeyPubkey: Array.from(passkey33), - signature: Buffer.from(signature64), - clientDataJsonRaw: Buffer.from(clientDataJsonRaw), - authenticatorDataRaw: Buffer.from(authenticatorDataRaw), - verifyInstructionIndex, - splitIndex, - ruleData: ruleInstruction.data, - cpiData: cpiInstruction.data, + return await this.program.methods + .createSmartWallet(args) + .accountsPartial({ + signer: payer, + smartWallet, + smartWalletConfig: this.smartWalletConfigPda(smartWallet), + smartWalletAuthenticator: this.smartWalletAuthenticatorPda( + smartWallet, + args.passkeyPubkey + ), + 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, - smartWalletAuthenticator, + smartWalletConfig: this.smartWalletConfigPda(smartWallet), + smartWalletAuthenticator: this.smartWalletAuthenticatorPda( + smartWallet, + args.passkeyPubkey + ), whitelistRulePrograms: this.whitelistRuleProgramsPda(), - authenticatorProgram: actualRuleProgram, + authenticatorProgram: ruleInstruction.programId, cpiProgram: cpiInstruction.programId, config: this.configPda(), - ixSysvar: (anchor.web3 as any).SYSVAR_INSTRUCTIONS_PUBKEY, + ixSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, }) - .remainingAccounts(remaining) + .remainingAccounts([ + ...instructionToAccountMetas(ruleInstruction, payer), + ...instructionToAccountMetas(cpiInstruction, payer), + ]) .instruction(); } - async buildCallRuleDirectIx(params: { - payer: PublicKey; - smartWallet: PublicKey; - passkey33: Uint8Array; - signature64: Uint8Array; - clientDataJsonRaw: Uint8Array; - authenticatorDataRaw: Uint8Array; - ruleProgram: PublicKey; - ruleInstruction: TransactionInstruction; - verifyInstructionIndex?: number; // default 0 - newPasskey33?: Uint8Array; // optional - newAuthenticatorPda?: PublicKey; // optional - }): Promise { - const { - payer, - smartWallet, - passkey33, - signature64, - clientDataJsonRaw, - authenticatorDataRaw, - ruleProgram, - ruleInstruction, - verifyInstructionIndex = 0, - newPasskey33, - newAuthenticatorPda, - } = params; - - const smartWalletConfig = this.smartWalletConfigPda(smartWallet); - const smartWalletAuthenticator = this.smartWalletAuthenticatorPda( - smartWallet, - passkey33 - ); + async buildCallRuleDirectIx( + payer: PublicKey, + smartWallet: PublicKey, + args: types.CallRuleArgs, + ruleInstruction: TransactionInstruction + ): Promise { + const remaining: AccountMeta[] = []; - const remaining: { - pubkey: PublicKey; - isWritable: boolean; - isSigner: boolean; - }[] = []; - if (newAuthenticatorPda) { + if (args.newAuthenticator) { + const newSmartWalletAuthenticator = this.smartWalletAuthenticatorPda( + smartWallet, + args.newAuthenticator.passkeyPubkey + ); remaining.push({ - pubkey: newAuthenticatorPda, + pubkey: newSmartWalletAuthenticator, isWritable: true, isSigner: false, }); } - remaining.push( - ...ruleInstruction.keys.map((k) => ({ - pubkey: k.pubkey, - isWritable: k.isWritable, - isSigner: k.isSigner, - })) - ); - return await (this.program.methods as any) - .callRuleDirect({ - passkeyPubkey: Array.from(passkey33), - signature: Buffer.from(signature64), - clientDataJsonRaw: Buffer.from(clientDataJsonRaw), - authenticatorDataRaw: Buffer.from(authenticatorDataRaw), - verifyInstructionIndex, - ruleProgram, - ruleData: ruleInstruction.data, - createNewAuthenticator: newPasskey33 ? Array.from(newPasskey33) : null, - }) + remaining.push(...instructionToAccountMetas(ruleInstruction, payer)); + + return await this.program.methods + .callRuleDirect(args) .accountsPartial({ payer, config: this.configPda(), smartWallet, - smartWalletConfig, - smartWalletAuthenticator, - ruleProgram, + smartWalletConfig: this.smartWalletConfigPda(smartWallet), + smartWalletAuthenticator: this.smartWalletAuthenticatorPda( + smartWallet, + args.passkeyPubkey + ), + ruleProgram: ruleInstruction.programId, whitelistRulePrograms: this.whitelistRuleProgramsPda(), - ixSysvar: (anchor.web3 as any).SYSVAR_INSTRUCTIONS_PUBKEY, + ixSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, systemProgram: SystemProgram.programId, }) .remainingAccounts(remaining) .instruction(); } - async buildChangeRuleDirectIx(params: { - payer: PublicKey; - smartWallet: PublicKey; - passkey33: Uint8Array; - signature64: Uint8Array; - clientDataJsonRaw: Uint8Array; - authenticatorDataRaw: Uint8Array; - oldRuleProgram: PublicKey; - destroyRuleInstruction: TransactionInstruction; - newRuleProgram: PublicKey; - initRuleInstruction: TransactionInstruction; - verifyInstructionIndex?: number; // default 0 - }): Promise { - const { - payer, - smartWallet, - passkey33, - signature64, - clientDataJsonRaw, - authenticatorDataRaw, - oldRuleProgram, - destroyRuleInstruction, - newRuleProgram, - initRuleInstruction, - verifyInstructionIndex = 0, - } = params; - - const smartWalletConfig = this.smartWalletConfigPda(smartWallet); - const smartWalletAuthenticator = this.smartWalletAuthenticatorPda( - smartWallet, - passkey33 - ); - - const destroyMetas = destroyRuleInstruction.keys.map((k) => ({ - pubkey: k.pubkey, - isWritable: k.isWritable, - isSigner: k.isSigner, - })); - const initMetas = initRuleInstruction.keys.map((k) => ({ - pubkey: k.pubkey, - isWritable: k.isWritable, - isSigner: k.isSigner, - })); - const remaining = [...destroyMetas, ...initMetas]; - const splitIndex = destroyMetas.length; - - return await (this.program.methods as any) - .changeRuleDirect({ - passkeyPubkey: Array.from(passkey33), - signature: Buffer.from(signature64), - clientDataJsonRaw: Buffer.from(clientDataJsonRaw), - authenticatorDataRaw: Buffer.from(authenticatorDataRaw), - verifyInstructionIndex, - splitIndex, - oldRuleProgram, - destroyRuleData: destroyRuleInstruction.data, - newRuleProgram, - initRuleData: initRuleInstruction.data, - createNewAuthenticator: null, - }) + async buildChangeRuleDirectIx( + payer: PublicKey, + smartWallet: PublicKey, + args: types.ChangeRuleArgs, + destroyRuleInstruction: TransactionInstruction, + initRuleInstruction: TransactionInstruction + ): Promise { + return await this.program.methods + .changeRuleDirect(args) .accountsPartial({ payer, config: this.configPda(), smartWallet, - smartWalletConfig, - smartWalletAuthenticator, - oldRuleProgram, - newRuleProgram, + smartWalletConfig: this.smartWalletConfigPda(smartWallet), + smartWalletAuthenticator: this.smartWalletAuthenticatorPda( + smartWallet, + args.passkeyPubkey + ), + oldRuleProgram: destroyRuleInstruction.programId, + newRuleProgram: initRuleInstruction.programId, whitelistRulePrograms: this.whitelistRuleProgramsPda(), - ixSysvar: (anchor.web3 as any).SYSVAR_INSTRUCTIONS_PUBKEY, + ixSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, systemProgram: SystemProgram.programId, }) - .remainingAccounts(remaining) + .remainingAccounts([ + ...instructionToAccountMetas(destroyRuleInstruction, payer), + ...instructionToAccountMetas(initRuleInstruction, payer), + ]) .instruction(); } - async buildCommitCpiIx(params: { - payer: PublicKey; - smartWallet: PublicKey; - passkey33: Uint8Array; - signature64: Uint8Array; - clientDataJsonRaw: Uint8Array; - authenticatorDataRaw: Uint8Array; - ruleInstruction: TransactionInstruction; - cpiProgram: PublicKey; - expiresAt: number; - verifyInstructionIndex?: number; - }): Promise { - const { - payer, - smartWallet, - passkey33, - signature64, - clientDataJsonRaw, - authenticatorDataRaw, - ruleInstruction, - cpiProgram, - expiresAt, - verifyInstructionIndex = 0, - } = params; - - const smartWalletConfig = this.smartWalletConfigPda(smartWallet); - const smartWalletAuthenticator = this.smartWalletAuthenticatorPda( - smartWallet, - passkey33 - ); - - const cfg = await this.getSmartWalletConfigData(smartWallet); - const whitelist = this.whitelistRuleProgramsPda(); - const ruleProgram = cfg.ruleProgram as PublicKey; - - const remaining = ruleInstruction.keys.map((k) => ({ - pubkey: k.pubkey, - isWritable: k.isWritable, - isSigner: k.isSigner, - })); - - return await (this.program.methods as any) - .commitCpi({ - passkeyPubkey: Array.from(passkey33), - signature: Buffer.from(signature64), - clientDataJsonRaw: Buffer.from(clientDataJsonRaw), - authenticatorDataRaw: Buffer.from(authenticatorDataRaw), - verifyInstructionIndex, - ruleData: ruleInstruction.data, - cpiProgram, - expiresAt: new anchor.BN(expiresAt), - }) + 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, - smartWalletAuthenticator, - whitelistRulePrograms: whitelist, - authenticatorProgram: ruleProgram, - ixSysvar: (anchor.web3 as any).SYSVAR_INSTRUCTIONS_PUBKEY, + smartWalletConfig: this.smartWalletConfigPda(smartWallet), + smartWalletAuthenticator: this.smartWalletAuthenticatorPda( + smartWallet, + args.passkeyPubkey + ), + whitelistRulePrograms: this.whitelistRuleProgramsPda(), + authenticatorProgram: ruleInstruction.programId, + ixSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, systemProgram: SystemProgram.programId, }) - .remainingAccounts(remaining) + .remainingAccounts([...instructionToAccountMetas(ruleInstruction, payer)]) .instruction(); } - async buildExecuteCommittedIx(params: { - payer: PublicKey; - smartWallet: PublicKey; - cpiInstruction: TransactionInstruction; - }): Promise { - const { payer, smartWallet, cpiInstruction } = params; + async buildExecuteCommittedIx( + payer: PublicKey, + smartWallet: PublicKey, + cpiInstruction: TransactionInstruction + ): Promise { const cfg = await this.getSmartWalletConfigData(smartWallet); - const nonceLe8 = Buffer.alloc(8); - nonceLe8.writeBigUInt64LE(BigInt(cfg.lastNonce.toString())); - const cpiCommit = this.cpiCommitPda(smartWallet, nonceLe8); - return await (this.program.methods as any) - .executeCommitted({ cpiData: cpiInstruction.data }) + const cpiCommit = this.cpiCommitPda(smartWallet, cfg.lastNonce); + + return await this.program.methods + .executeCommitted(cpiInstruction.data) .accountsPartial({ payer, config: this.configPda(), @@ -481,13 +284,7 @@ export class LazorkitClient { cpiCommit, commitRefund: payer, }) - .remainingAccounts( - cpiInstruction.keys.map((k) => ({ - pubkey: k.pubkey, - isWritable: k.isWritable, - isSigner: k.isSigner, - })) - ) + .remainingAccounts([...instructionToAccountMetas(cpiInstruction, payer)]) .instruction(); } @@ -506,26 +303,28 @@ export class LazorkitClient { const verifyIx = buildSecp256r1VerifyIx( Buffer.concat([ Buffer.from(params.authenticatorDataRaw), - Buffer.from( - (anchor as any).utils.sha256.hash(params.clientDataJsonRaw), - 'hex' - ), + Buffer.from(sha256.hex(params.clientDataJsonRaw), 'hex'), ]), Buffer.from(params.passkey33), Buffer.from(params.signature64) ); - const execIx = await this.buildExecuteTxnDirectIx({ - payer: params.payer, - smartWallet: params.smartWallet, - passkey33: params.passkey33, - signature64: params.signature64, - clientDataJsonRaw: params.clientDataJsonRaw, - authenticatorDataRaw: params.authenticatorDataRaw, - ruleInstruction: params.ruleInstruction, - cpiInstruction: params.cpiInstruction, - verifyInstructionIndex: 0, - ruleProgram: params.ruleProgram, - }); + + const execIx = await this.buildExecuteTxnDirectIx( + params.payer, + params.smartWallet, + { + passkeyPubkey: Array.from(params.passkey33), + signature: Buffer.from(params.signature64), + clientDataJsonRaw: Buffer.from(params.clientDataJsonRaw), + authenticatorDataRaw: Buffer.from(params.authenticatorDataRaw), + verifyInstructionIndex: 0, + ruleData: params.ruleInstruction.data, + cpiData: params.cpiInstruction.data, + splitIndex: params.ruleInstruction.keys.length, + }, + params.ruleInstruction, + params.cpiInstruction + ); return this.buildV0Tx(params.payer, [verifyIx, execIx]); } @@ -538,33 +337,43 @@ export class LazorkitClient { authenticatorDataRaw: Uint8Array; ruleProgram: PublicKey; ruleInstruction: TransactionInstruction; - newPasskey33?: Uint8Array; - newAuthenticatorPda?: PublicKey; + newAuthenticator?: { + passkey33: Uint8Array; + credentialIdBase64: string; + }; // optional }): Promise { const verifyIx = buildSecp256r1VerifyIx( Buffer.concat([ Buffer.from(params.authenticatorDataRaw), - Buffer.from( - (anchor as any).utils.sha256.hash(params.clientDataJsonRaw), - 'hex' - ), + Buffer.from(sha256.hex(params.clientDataJsonRaw), 'hex'), ]), Buffer.from(params.passkey33), Buffer.from(params.signature64) ); - const ix = await this.buildCallRuleDirectIx({ - payer: params.payer, - smartWallet: params.smartWallet, - passkey33: params.passkey33, - signature64: params.signature64, - clientDataJsonRaw: params.clientDataJsonRaw, - authenticatorDataRaw: params.authenticatorDataRaw, - ruleProgram: params.ruleProgram, - ruleInstruction: params.ruleInstruction, - verifyInstructionIndex: 0, - newPasskey33: params.newPasskey33, - newAuthenticatorPda: params.newAuthenticatorPda, - }); + const ix = await this.buildCallRuleDirectIx( + params.payer, + params.smartWallet, + { + passkeyPubkey: Array.from(params.passkey33), + signature: Buffer.from(params.signature64), + clientDataJsonRaw: Buffer.from(params.clientDataJsonRaw), + authenticatorDataRaw: Buffer.from(params.authenticatorDataRaw), + newAuthenticator: params.newAuthenticator + ? { + passkeyPubkey: Array.from(params.newAuthenticator.passkey33), + 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]); } @@ -575,35 +384,49 @@ export class LazorkitClient { signature64: Uint8Array; clientDataJsonRaw: Uint8Array; authenticatorDataRaw: Uint8Array; - oldRuleProgram: PublicKey; destroyRuleInstruction: TransactionInstruction; - newRuleProgram: PublicKey; initRuleInstruction: TransactionInstruction; + newAuthenticator?: { + passkey33: Uint8Array; + credentialIdBase64: string; + }; // optional }): Promise { const verifyIx = buildSecp256r1VerifyIx( Buffer.concat([ Buffer.from(params.authenticatorDataRaw), - Buffer.from( - (anchor as any).utils.sha256.hash(params.clientDataJsonRaw), - 'hex' - ), + Buffer.from(sha256.hex(params.clientDataJsonRaw), 'hex'), ]), Buffer.from(params.passkey33), Buffer.from(params.signature64) ); - const ix = await this.buildChangeRuleDirectIx({ - payer: params.payer, - smartWallet: params.smartWallet, - passkey33: params.passkey33, - signature64: params.signature64, - clientDataJsonRaw: params.clientDataJsonRaw, - authenticatorDataRaw: params.authenticatorDataRaw, - oldRuleProgram: params.oldRuleProgram, - destroyRuleInstruction: params.destroyRuleInstruction, - newRuleProgram: params.newRuleProgram, - initRuleInstruction: params.initRuleInstruction, - verifyInstructionIndex: 0, - }); + + const ix = await this.buildChangeRuleDirectIx( + params.payer, + params.smartWallet, + { + passkeyPubkey: Array.from(params.passkey33), + signature: Buffer.from(params.signature64), + clientDataJsonRaw: Buffer.from(params.clientDataJsonRaw), + authenticatorDataRaw: Buffer.from(params.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.passkey33), + credentialId: Buffer.from( + params.newAuthenticator.credentialIdBase64, + 'base64' + ), + } + : null, + }, + params.destroyRuleInstruction, + params.initRuleInstruction + ); return this.buildV0Tx(params.payer, [verifyIx, ix]); } @@ -621,27 +444,26 @@ export class LazorkitClient { const verifyIx = buildSecp256r1VerifyIx( Buffer.concat([ Buffer.from(params.authenticatorDataRaw), - Buffer.from( - (anchor as any).utils.sha256.hash(params.clientDataJsonRaw), - 'hex' - ), + Buffer.from(sha256.hex(params.clientDataJsonRaw), 'hex'), ]), Buffer.from(params.passkey33), Buffer.from(params.signature64) ); - const ix = await this.buildCommitCpiIx({ - payer: params.payer, - smartWallet: params.smartWallet, - passkey33: params.passkey33, - signature64: params.signature64, - clientDataJsonRaw: params.clientDataJsonRaw, - authenticatorDataRaw: params.authenticatorDataRaw, - ruleInstruction: params.ruleInstruction, - cpiProgram: params.cpiProgram, - expiresAt: params.expiresAt, - verifyInstructionIndex: 0, - }); - const tx = new (anchor.web3 as any).Transaction().add(verifyIx).add(ix); + const ix = await this.buildCommitCpiIx( + params.payer, + params.smartWallet, + { + passkeyPubkey: Array.from(params.passkey33), + signature: Buffer.from(params.signature64), + clientDataJsonRaw: Buffer.from(params.clientDataJsonRaw), + authenticatorDataRaw: Buffer.from(params.authenticatorDataRaw), + expiresAt: new BN(params.expiresAt), + ruleData: params.ruleInstruction.data, + verifyInstructionIndex: 0, + }, + params.ruleInstruction + ); + const tx = new Transaction().add(verifyIx).add(ix); tx.feePayer = params.payer; tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash; return tx; @@ -652,55 +474,54 @@ export class LazorkitClient { payer: PublicKey, ixs: TransactionInstruction[] ): Promise { - try { - const { blockhash } = await this.connection.getLatestBlockhash(); - const msg = new TransactionMessage({ - payerKey: payer, - recentBlockhash: blockhash, - instructions: ixs, - }).compileToV0Message(); - return new VersionedTransaction(msg); - } catch (e) { - throw new SDKError('Failed to build v0 transaction', e as any); - } + 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, defaultRuleProgram: PublicKey) { const ix = await this.buildInitializeIx(payer, defaultRuleProgram); - return new anchor.web3.Transaction().add(ix); + return new Transaction().add(ix); } async createSmartWalletTx(params: { payer: PublicKey; - smartWalletId: bigint; passkey33: Uint8Array; credentialIdBase64: string; ruleInstruction: TransactionInstruction; isPayForUser?: boolean; defaultRuleProgram: PublicKey; + smartWalletId?: BN; }) { - const ix = await this.buildCreateSmartWalletIx(params); - const tx = new anchor.web3.Transaction().add(ix); + let smartWalletId: BN = this.generateWalletId(); + if (params.smartWalletId) { + smartWalletId = params.smartWalletId; + } + const args = { + passkeyPubkey: Array.from(params.passkey33), + credentialId: Buffer.from(params.credentialIdBase64, 'base64'), + ruleData: params.ruleInstruction.data, + walletId: smartWalletId, + isPayForUser: params.isPayForUser, + }; + const ix = await this.buildCreateSmartWalletIx( + params.payer, + args, + params.ruleInstruction + ); + const tx = new Transaction().add(ix); tx.feePayer = params.payer; tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash; - const smartWallet = this.smartWalletPda(params.smartWalletId); + const smartWallet = this.smartWalletPda(smartWalletId); return { transaction: tx, - smartWalletId: params.smartWalletId, + smartWalletId: smartWalletId, smartWallet, }; } - - async simulate(ixs: TransactionInstruction[], payer: PublicKey) { - try { - const v0 = await this.buildV0Tx(payer, ixs); - // Empty signatures for simulate - return await this.connection.simulateTransaction(v0, { - sigVerify: false, - }); - } catch (e) { - throw decodeAnchorError(e); - } - } } diff --git a/sdk/constants.ts b/sdk/constants.ts index cd4f637..a1409cc 100644 --- a/sdk/constants.ts +++ b/sdk/constants.ts @@ -1,26 +1,26 @@ -import * as anchor from "@coral-xyz/anchor"; +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_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" + 'smart_wallet_authenticator' ); export const WHITELIST_RULE_PROGRAMS_SEED = Buffer.from( - "whitelist_rule_programs" + '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"); +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"); -export const MEMBER_SEED = Buffer.from("member"); -export const RULE_SEED = Buffer.from("rule"); +export const RULE_DATA_SEED = Buffer.from('rule_data'); +export const MEMBER_SEED = Buffer.from('member'); +export const RULE_SEED = Buffer.from('rule'); // ADDRESS LOOKUP TABLE for Versioned Transactions (v0) // This lookup table contains frequently used program IDs and accounts // to reduce transaction size and enable more complex operations export const ADDRESS_LOOKUP_TABLE = new anchor.web3.PublicKey( - "7Pr3DG7tRPAjVb44gqbxTj1KstikAuVZY7YmXdotVjLA" + '7Pr3DG7tRPAjVb44gqbxTj1KstikAuVZY7YmXdotVjLA' ); diff --git a/sdk/errors.ts b/sdk/errors.ts deleted file mode 100644 index a926bbc..0000000 --- a/sdk/errors.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { AnchorError } from "@coral-xyz/anchor"; - -export class SDKError extends Error { - constructor( - message: string, - readonly cause?: unknown, - readonly logs?: string[] - ) { - super(message); - this.name = "SDKError"; - } -} - -export function decodeAnchorError(e: any): SDKError { - if (e instanceof SDKError) return e; - const logs: string[] | undefined = - e?.logs || e?.transactionMessage || undefined; - // AnchorError handling (works with provider.simulate or sendAndConfirm - if (e instanceof AnchorError) { - const code = e.error.errorCode.number; - const msg = e.error.errorMessage || e.message; - return new SDKError(`AnchorError ${code}: ${msg}`, e, logs); - } - // Fallback - const message = - typeof e?.message === "string" ? e.message : "Unknown SDK error"; - return new SDKError(message, e, logs); -} diff --git a/sdk/index.ts b/sdk/index.ts index 70d2355..9599bb0 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -1,15 +1,8 @@ -// Main SDK exports -export { LazorkitClient } from "./client/lazorkit"; -export { DefaultRuleClient } from "./client/defaultRule"; - -// Type exports -export * from "./types"; +// Polyfill for structuredClone if not available (for React Native/Expo) +if (typeof globalThis.structuredClone !== 'function') { + globalThis.structuredClone = (obj: any) => JSON.parse(JSON.stringify(obj)); +} -// Utility exports -export * from "./utils"; -export * from "./constants"; -export * from "./messages"; -export * as pda from "./pda/lazorkit"; -export * as rulePda from "./pda/defaultRule"; -export * as webauthn from "./webauthn/secp256r1"; -export * from "./errors"; +// Main SDK exports +export { LazorkitClient } from './client/lazorkit'; +export { DefaultRuleClient } from './client/defaultRule'; diff --git a/sdk/messages.ts b/sdk/messages.ts index c000828..7365b2f 100644 --- a/sdk/messages.ts +++ b/sdk/messages.ts @@ -1,52 +1,52 @@ -import * as anchor from "@coral-xyz/anchor"; -import { sha256 } from "js-sha256"; -import { instructionToAccountMetas } from "./utils"; +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", + version: '0.1.0', + name: 'lazorkit_msgs', instructions: [], accounts: [], types: [ { - name: "ExecuteMessage", + name: 'ExecuteMessage', type: { - kind: "struct", + 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: '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", + name: 'CallRuleMessage', type: { - kind: "struct", + kind: 'struct', fields: [ - { name: "nonce", type: "u64" }, - { name: "currentTimestamp", type: "i64" }, - { name: "ruleDataHash", type: { array: ["u8", 32] } }, - { name: "ruleAccountsHash", type: { array: ["u8", 32] } }, - { name: "newPasskey", type: { option: { array: ["u8", 33] } } }, + { name: 'nonce', type: 'u64' }, + { name: 'currentTimestamp', type: 'i64' }, + { name: 'ruleDataHash', type: { array: ['u8', 32] } }, + { name: 'ruleAccountsHash', type: { array: ['u8', 32] } }, + { name: 'newPasskey', type: { option: { array: ['u8', 33] } } }, ], }, }, { - name: "ChangeRuleMessage", + name: 'ChangeRuleMessage', type: { - kind: "struct", + 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] } }, + { 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] } }, ], }, }, @@ -83,7 +83,7 @@ export function buildExecuteMessage( const cpiAccountsHash = computeAccountsHash(cpiIns.programId, cpiMetas); const cpiDataHash = new Uint8Array(sha256.arrayBuffer(cpiIns.data)); - const encoded = coder.types.encode("ExecuteMessage", { + const encoded = coder.types.encode('ExecuteMessage', { nonce, currentTimestamp: now, ruleDataHash: Array.from(ruleDataHash), @@ -106,7 +106,7 @@ export function buildCallRuleMessage( const ruleAccountsHash = computeAccountsHash(ruleProgram, ruleMetas); const ruleDataHash = new Uint8Array(sha256.arrayBuffer(ruleIns.data)); - const encoded = coder.types.encode("CallRuleMessage", { + const encoded = coder.types.encode('CallRuleMessage', { nonce, currentTimestamp: now, ruleDataHash: Array.from(ruleDataHash), @@ -136,7 +136,7 @@ export function buildChangeRuleMessage( const newAccountsHash = computeAccountsHash(newRuleProgram, newMetas); const newDataHash = new Uint8Array(sha256.arrayBuffer(initRuleIns.data)); - const encoded = coder.types.encode("ChangeRuleMessage", { + const encoded = coder.types.encode('ChangeRuleMessage', { nonce, currentTimestamp: now, oldRuleDataHash: Array.from(oldDataHash), diff --git a/sdk/pda/lazorkit.ts b/sdk/pda/lazorkit.ts index bf9eb2d..4bba9df 100644 --- a/sdk/pda/lazorkit.ts +++ b/sdk/pda/lazorkit.ts @@ -1,16 +1,16 @@ -import { PublicKey } from "@solana/web3.js"; - +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 CONFIG_SEED = Buffer.from('config'); export const WHITELIST_RULE_PROGRAMS_SEED = Buffer.from( - "whitelist_rule_programs" + '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_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" + 'smart_wallet_authenticator' ); -export const CPI_COMMIT_SEED = Buffer.from("cpi_commit"); +export const CPI_COMMIT_SEED = Buffer.from('cpi_commit'); export function deriveConfigPda(programId: PublicKey): PublicKey { return PublicKey.findProgramAddressSync([CONFIG_SEED], programId)[0]; @@ -27,12 +27,10 @@ export function deriveWhitelistRuleProgramsPda( export function deriveSmartWalletPda( programId: PublicKey, - walletId: bigint + walletId: BN ): PublicKey { - const idBytes = new Uint8Array(8); - new DataView(idBytes.buffer).setBigUint64(0, walletId, true); return PublicKey.findProgramAddressSync( - [SMART_WALLET_SEED, idBytes], + [SMART_WALLET_SEED, walletId.toArrayLike(Buffer, 'le', 8)], programId )[0]; } @@ -49,10 +47,10 @@ export function deriveSmartWalletConfigPda( // Must match on-chain: sha256(passkey(33) || wallet(32)) export function hashPasskeyWithWallet( - passkeyCompressed33: Uint8Array, + passkeyCompressed33: number[], wallet: PublicKey ): Buffer { - const { sha256 } = require("js-sha256"); + const { sha256 } = require('js-sha256'); const buf = Buffer.alloc(65); Buffer.from(passkeyCompressed33).copy(buf, 0); wallet.toBuffer().copy(buf, 33); @@ -62,7 +60,7 @@ export function hashPasskeyWithWallet( export function deriveSmartWalletAuthenticatorPda( programId: PublicKey, smartWallet: PublicKey, - passkeyCompressed33: Uint8Array + passkeyCompressed33: number[] ): [PublicKey, number] { const hashed = hashPasskeyWithWallet(passkeyCompressed33, smartWallet); return PublicKey.findProgramAddressSync( @@ -74,10 +72,14 @@ export function deriveSmartWalletAuthenticatorPda( export function deriveCpiCommitPda( programId: PublicKey, smartWallet: PublicKey, - authorizedNonceLe8: Buffer + lastNonce: BN ): PublicKey { return PublicKey.findProgramAddressSync( - [CPI_COMMIT_SEED, smartWallet.toBuffer(), authorizedNonceLe8], + [ + CPI_COMMIT_SEED, + smartWallet.toBuffer(), + lastNonce.toArrayLike(Buffer, 'le', 8), + ], programId )[0]; } diff --git a/sdk/types.ts b/sdk/types.ts index 880a7d8..4fb6388 100644 --- a/sdk/types.ts +++ b/sdk/types.ts @@ -1,14 +1,24 @@ -import * as anchor from "@coral-xyz/anchor"; +import * as anchor from '@coral-xyz/anchor'; -import { Lazorkit } from "../target/types/lazorkit"; +import { Lazorkit } from '../target/types/lazorkit'; // Account types -export type SmartWalletConfig = anchor.IdlTypes["smartWalletConfig"]; +export type SmartWalletConfig = anchor.IdlTypes['smartWalletConfig']; export type SmartWalletAuthenticator = - anchor.IdlTypes["smartWalletAuthenticator"]; -export type Config = anchor.IdlTypes["config"]; + anchor.IdlTypes['smartWalletAuthenticator']; +export type Config = anchor.IdlTypes['config']; export type WhitelistRulePrograms = - anchor.IdlTypes["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 type UpdateConfigType = anchor.IdlTypes['updateConfigType']; diff --git a/sdk/utils.ts b/sdk/utils.ts index a887806..dcac4bd 100644 --- a/sdk/utils.ts +++ b/sdk/utils.ts @@ -1,193 +1,5 @@ -import * as anchor from "@coral-xyz/anchor"; -import { sha256 } from "js-sha256"; +import * as anchor from '@coral-xyz/anchor'; -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 diff --git a/sdk/webauthn/secp256r1.ts b/sdk/webauthn/secp256r1.ts index 485273d..00df704 100644 --- a/sdk/webauthn/secp256r1.ts +++ b/sdk/webauthn/secp256r1.ts @@ -1,5 +1,5 @@ -import * as anchor from "@coral-xyz/anchor"; -import { PublicKey, TransactionInstruction } from "@solana/web3.js"; +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; @@ -9,7 +9,7 @@ const COMPRESSED_PUBKEY_SERIALIZED_SIZE = 33; const FIELD_SIZE = 32; export const SECP256R1_PROGRAM_ID = new PublicKey( - "Secp256r1SigVerify1111111111111111111111111" + 'Secp256r1SigVerify1111111111111111111111111' ); const ORDER = new Uint8Array([ @@ -81,7 +81,7 @@ export function buildSecp256r1VerifyIx( compressedPubkey33.length !== COMPRESSED_PUBKEY_SERIALIZED_SIZE || sig.length !== SIGNATURE_SERIALIZED_SIZE ) { - throw new Error("Invalid secp256r1 key/signature length"); + throw new Error('Invalid secp256r1 key/signature length'); } const totalSize = diff --git a/tests/smart_wallet_with_default_rule.test.ts b/tests/smart_wallet_with_default_rule.test.ts index 41ab8b2..b0b4af4 100644 --- a/tests/smart_wallet_with_default_rule.test.ts +++ b/tests/smart_wallet_with_default_rule.test.ts @@ -1,20 +1,20 @@ -import * as anchor from "@coral-xyz/anchor"; -import ECDSA from "ecdsa-secp256r1"; -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 { LazorkitClient, DefaultRuleClient } from "../sdk"; +import * as anchor from '@coral-xyz/anchor'; +import ECDSA from 'ecdsa-secp256r1'; +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 { LazorkitClient, DefaultRuleClient } from '../sdk'; dotenv.config(); -describe("Test smart wallet with default rule", () => { +describe('Test smart wallet with default rule', () => { const connection = new anchor.web3.Connection( - process.env.RPC_URL || "http://localhost:8899", - "confirmed" + process.env.RPC_URL || 'http://localhost:8899', + 'confirmed' ); - const lazorkitProgram = new LazorkitClient({ connection }); - const defaultRuleProgram = new DefaultRuleClient({ connection }); + const lazorkitProgram = new LazorkitClient(connection); + const defaultRuleProgram = new DefaultRuleClient(connection); const payer = anchor.web3.Keypair.fromSecretKey( bs58.decode(process.env.PRIVATE_KEY!) @@ -24,7 +24,7 @@ describe("Test smart wallet with default rule", () => { // airdrop some SOL to the payer const programConfig = await connection.getAccountInfo( - lazorkitProgram.config + lazorkitProgram.configPda() ); if (programConfig === null) { @@ -34,47 +34,44 @@ describe("Test smart wallet with default rule", () => { ); const sig = await sendAndConfirmTransaction(connection, txn, [payer], { - commitment: "confirmed", + commitment: 'confirmed', skipPreflight: true, }); - console.log("Initialize txn: ", sig); + console.log('Initialize txn: ', sig); } }); - it("Init smart wallet with default rule 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 pubkey = Array.from(Buffer.from(publicKeyBase64, 'base64')); const smartWalletId = lazorkitProgram.generateWalletId(); const smartWallet = lazorkitProgram.smartWalletPda(smartWalletId); const smartWalletAuthenticator = - lazorkitProgram.smartWalletAuthenticatorPda( - smartWallet, - Buffer.from(pubkey) - ); + lazorkitProgram.smartWalletAuthenticatorPda(smartWallet, pubkey); - const initRuleIns = await defaultRuleProgram.initRuleIns( + const initRuleIns = await defaultRuleProgram.buildInitRuleIx( payer.publicKey, smartWallet, smartWalletAuthenticator ); - const credentialId = base64.encode(Buffer.from("testing something")); // random string + const credentialId = base64.encode(Buffer.from('testing something')); // random string const { transaction: createSmartWalletTxn } = await lazorkitProgram.createSmartWalletTx({ payer: payer.publicKey, - smartWalletId, passkey33: Buffer.from(pubkey), credentialIdBase64: credentialId, ruleInstruction: initRuleIns, isPayForUser: true, defaultRuleProgram: defaultRuleProgram.programId, + smartWalletId, }); const sig = await sendAndConfirmTransaction( @@ -82,11 +79,11 @@ describe("Test smart wallet with default rule", () => { createSmartWalletTxn, [payer], { - commitment: "confirmed", + commitment: 'confirmed', } ); - console.log("Create smart-wallet: ", sig); + console.log('Create smart-wallet: ', sig); const smartWalletConfigData = await lazorkitProgram.getSmartWalletConfigData(smartWallet); @@ -108,70 +105,7 @@ describe("Test smart wallet with default rule", () => { ); }); - it("Store blob successfully", async () => { - const privateKey = ECDSA.generateKey(); - - const publicKeyBase64 = privateKey.toCompressedPublicKey(); - - const pubkey = Array.from(Buffer.from(publicKeyBase64, "base64")); - - const smartWalletId = lazorkitProgram.generateWalletId(); - const smartWallet = lazorkitProgram.smartWallet(smartWalletId); - - const [smartWalletAuthenticator] = lazorkitProgram.smartWalletAuthenticator( - pubkey, - smartWallet - ); - - const initRuleIns = await defaultRuleProgram.initRuleIns( - payer.publicKey, - smartWallet, - smartWalletAuthenticator - ); - - const credentialId = base64.encode(Buffer.from("testing something")); // random string - - const { transaction: createSmartWalletTxn } = - await lazorkitProgram.createSmartWalletTxn( - pubkey, - payer.publicKey, - credentialId, - initRuleIns, - smartWalletId, - true - ); - - const sig = await sendAndConfirmTransaction( - connection, - createSmartWalletTxn, - [payer], - { - commitment: "confirmed", - } - ); - - console.log("Create smart-wallet: ", sig); - - // store blob - - const data = Buffer.from("testing something"); - - // Legacy store blob path removed in refactor; skipping this part in SDK migration - return; - - const sig2 = await sendAndConfirmTransaction( - connection, - storeBlobTxn, - [payer], - { - commitment: "confirmed", - } - ); - - console.log("Store blob: ", sig2); - }); - - xit("Create address lookup table", async () => { + xit('Create address lookup table', async () => { const slot = await connection.getSlot(); const [lookupTableInst, lookupTableAddress] = @@ -184,11 +118,11 @@ describe("Test smart wallet with default rule", () => { const txn = new anchor.web3.Transaction().add(lookupTableInst); await sendAndConfirmTransaction(connection, txn, [payer], { - commitment: "confirmed", + commitment: 'confirmed', skipPreflight: true, }); - console.log("Lookup table: ", lookupTableAddress); + console.log('Lookup table: ', lookupTableAddress); const extendInstruction = anchor.web3.AddressLookupTableProgram.extendLookupTable({ @@ -196,9 +130,9 @@ describe("Test smart wallet with default rule", () => { authority: payer.publicKey, lookupTable: lookupTableAddress, addresses: [ - lazorkitProgram.config, - lazorkitProgram.whitelistRulePrograms, - lazorkitProgram.defaultRuleProgram.programId, + lazorkitProgram.configPda(), + lazorkitProgram.whitelistRuleProgramsPda(), + defaultRuleProgram.programId, anchor.web3.SystemProgram.programId, anchor.web3.SYSVAR_RENT_PUBKEY, anchor.web3.SYSVAR_CLOCK_PUBKEY, @@ -211,9 +145,9 @@ describe("Test smart wallet with default rule", () => { const txn1 = new anchor.web3.Transaction().add(extendInstruction); const sig1 = await sendAndConfirmTransaction(connection, txn1, [payer], { - commitment: "confirmed", + commitment: 'confirmed', }); - console.log("Extend lookup table: ", sig1); + console.log('Extend lookup table: ', sig1); }); }); From 4fb0613480be1e5cd546bbcf53f8da331b6d3225 Mon Sep 17 00:00:00 2001 From: onspeedhp Date: Mon, 11 Aug 2025 23:51:59 +0700 Subject: [PATCH 5/6] Remove deprecated SDK files and refactor import paths to streamline the LazorKit program. This includes the removal of constants, types, utility functions, and client classes that are no longer in use, enhancing overall code clarity and maintainability. --- .../client/defaultRule.ts | 0 .../client/lazorkit.ts | 276 ++++++++++++------ {sdk => contract-integration}/constants.ts | 0 {sdk => contract-integration}/index.ts | 0 {sdk => contract-integration}/messages.ts | 0 .../pda/defaultRule.ts | 0 {sdk => contract-integration}/pda/lazorkit.ts | 0 {sdk => contract-integration}/types.ts | 1 - {sdk => contract-integration}/utils.ts | 0 .../webauthn/secp256r1.ts | 0 tests/smart_wallet_with_default_rule.test.ts | 27 +- 11 files changed, 190 insertions(+), 114 deletions(-) rename {sdk => contract-integration}/client/defaultRule.ts (100%) rename {sdk => contract-integration}/client/lazorkit.ts (68%) rename {sdk => contract-integration}/constants.ts (100%) rename {sdk => contract-integration}/index.ts (100%) rename {sdk => contract-integration}/messages.ts (100%) rename {sdk => contract-integration}/pda/defaultRule.ts (100%) rename {sdk => contract-integration}/pda/lazorkit.ts (100%) rename {sdk => contract-integration}/types.ts (99%) rename {sdk => contract-integration}/utils.ts (100%) rename {sdk => contract-integration}/webauthn/secp256r1.ts (100%) diff --git a/sdk/client/defaultRule.ts b/contract-integration/client/defaultRule.ts similarity index 100% rename from sdk/client/defaultRule.ts rename to contract-integration/client/defaultRule.ts diff --git a/sdk/client/lazorkit.ts b/contract-integration/client/lazorkit.ts similarity index 68% rename from sdk/client/lazorkit.ts rename to contract-integration/client/lazorkit.ts index 7493ae1..7eb5d99 100644 --- a/sdk/client/lazorkit.ts +++ b/contract-integration/client/lazorkit.ts @@ -24,15 +24,15 @@ import { buildSecp256r1VerifyIx } from '../webauthn/secp256r1'; import { instructionToAccountMetas } from '../utils'; import { sha256 } from 'js-sha256'; import * as types from '../types'; -import DefaultRuleIdl from '../../target/idl/default_rule.json'; -import { DefaultRule } from '../../target/types/default_rule'; import { randomBytes } from 'crypto'; +import { DefaultRuleClient } from './defaultRule'; +import * as bs58 from 'bs58'; export class LazorkitClient { readonly connection: Connection; readonly program: Program; readonly programId: PublicKey; - readonly defaultRuleProgram: Program; + readonly defaultRuleProgram: DefaultRuleClient; constructor(connection: Connection) { this.connection = connection; @@ -40,12 +40,7 @@ export class LazorkitClient { this.program = new Program(LazorkitIdl as Lazorkit, { connection: connection, }); - this.defaultRuleProgram = new Program( - DefaultRuleIdl as DefaultRule, - { - connection: connection, - } - ); + this.defaultRuleProgram = new DefaultRuleClient(connection); this.programId = this.program.programId; } @@ -93,19 +88,47 @@ export class LazorkitClient { 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, - defaultRuleProgram: PublicKey - ): Promise { + async buildInitializeIx(payer: PublicKey): Promise { return await this.program.methods .initialize() .accountsPartial({ signer: payer, config: this.configPda(), whitelistRulePrograms: this.whitelistRuleProgramsPda(), - defaultRuleProgram, + defaultRuleProgram: this.defaultRuleProgram.programId, systemProgram: SystemProgram.programId, }) .instruction(); @@ -113,21 +136,18 @@ export class LazorkitClient { async buildCreateSmartWalletIx( payer: PublicKey, - args: types.CreatwSmartWalletArgs, - ruleInstruction: TransactionInstruction + smartWallet: PublicKey, + smartWalletAuthenticator: PublicKey, + ruleInstruction: TransactionInstruction, + args: types.CreatwSmartWalletArgs ): Promise { - const smartWallet = this.smartWalletPda(args.walletId); - return await this.program.methods .createSmartWallet(args) .accountsPartial({ signer: payer, smartWallet, smartWalletConfig: this.smartWalletConfigPda(smartWallet), - smartWalletAuthenticator: this.smartWalletAuthenticatorPda( - smartWallet, - args.passkeyPubkey - ), + smartWalletAuthenticator, config: this.configPda(), defaultRuleProgram: this.defaultRuleProgram.programId, systemProgram: SystemProgram.programId, @@ -215,6 +235,23 @@ export class LazorkitClient { 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({ @@ -232,10 +269,7 @@ export class LazorkitClient { ixSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, systemProgram: SystemProgram.programId, }) - .remainingAccounts([ - ...instructionToAccountMetas(destroyRuleInstruction, payer), - ...instructionToAccountMetas(initRuleInstruction, payer), - ]) + .remainingAccounts(remaining) .instruction(); } @@ -292,37 +326,48 @@ export class LazorkitClient { async executeTxnDirectTx(params: { payer: PublicKey; smartWallet: PublicKey; - passkey33: Uint8Array; - signature64: Uint8Array; - clientDataJsonRaw: Uint8Array; - authenticatorDataRaw: Uint8Array; - ruleInstruction: TransactionInstruction; + passkeyPubkey: number[]; + signature64: String; + clientDataJsonRaw64: String; + authenticatorDataRaw64: String; + ruleInstruction: TransactionInstruction | null; cpiInstruction: TransactionInstruction; - ruleProgram?: PublicKey; }): Promise { + const authenticatorDataRaw = Buffer.from( + params.authenticatorDataRaw64, + 'base64' + ); + const clientDataJsonRaw = Buffer.from(params.clientDataJsonRaw64, 'base64'); const verifyIx = buildSecp256r1VerifyIx( Buffer.concat([ - Buffer.from(params.authenticatorDataRaw), - Buffer.from(sha256.hex(params.clientDataJsonRaw), 'hex'), + authenticatorDataRaw, + Buffer.from(sha256.hex(clientDataJsonRaw), 'hex'), ]), - Buffer.from(params.passkey33), + 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: Array.from(params.passkey33), - signature: Buffer.from(params.signature64), - clientDataJsonRaw: Buffer.from(params.clientDataJsonRaw), - authenticatorDataRaw: Buffer.from(params.authenticatorDataRaw), + passkeyPubkey: params.passkeyPubkey, + signature: Buffer.from(params.signature64, 'base64'), + clientDataJsonRaw, + authenticatorDataRaw, verifyInstructionIndex: 0, - ruleData: params.ruleInstruction.data, + ruleData: ruleInstruction.data, cpiData: params.cpiInstruction.data, - splitIndex: params.ruleInstruction.keys.length, + splitIndex: ruleInstruction.keys.length, }, - params.ruleInstruction, + ruleInstruction, params.cpiInstruction ); return this.buildV0Tx(params.payer, [verifyIx, execIx]); @@ -331,36 +376,43 @@ export class LazorkitClient { async callRuleDirectTx(params: { payer: PublicKey; smartWallet: PublicKey; - passkey33: Uint8Array; - signature64: Uint8Array; - clientDataJsonRaw: Uint8Array; - authenticatorDataRaw: Uint8Array; + passkeyPubkey: number[]; + signature64: String; + clientDataJsonRaw64: String; + authenticatorDataRaw64: String; ruleProgram: PublicKey; ruleInstruction: TransactionInstruction; newAuthenticator?: { - passkey33: Uint8Array; + 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([ - Buffer.from(params.authenticatorDataRaw), - Buffer.from(sha256.hex(params.clientDataJsonRaw), 'hex'), + authenticatorDataRaw, + Buffer.from(sha256.hex(clientDataJsonRaw), 'hex'), ]), - Buffer.from(params.passkey33), + Buffer.from(params.passkeyPubkey), Buffer.from(params.signature64) ); + const ix = await this.buildCallRuleDirectIx( params.payer, params.smartWallet, { - passkeyPubkey: Array.from(params.passkey33), - signature: Buffer.from(params.signature64), - clientDataJsonRaw: Buffer.from(params.clientDataJsonRaw), - authenticatorDataRaw: Buffer.from(params.authenticatorDataRaw), + passkeyPubkey: params.passkeyPubkey, + signature: Buffer.from(params.signature64, 'base64'), + clientDataJsonRaw: clientDataJsonRaw, + authenticatorDataRaw: authenticatorDataRaw, newAuthenticator: params.newAuthenticator ? { - passkeyPubkey: Array.from(params.newAuthenticator.passkey33), + passkeyPubkey: Array.from(params.newAuthenticator.passkeyPubkey), credentialId: Buffer.from( params.newAuthenticator.credentialIdBase64, 'base64' @@ -380,23 +432,29 @@ export class LazorkitClient { async changeRuleDirectTx(params: { payer: PublicKey; smartWallet: PublicKey; - passkey33: Uint8Array; - signature64: Uint8Array; - clientDataJsonRaw: Uint8Array; - authenticatorDataRaw: Uint8Array; + passkeyPubkey: number[]; + signature64: String; + clientDataJsonRaw64: String; + authenticatorDataRaw64: String; destroyRuleInstruction: TransactionInstruction; initRuleInstruction: TransactionInstruction; newAuthenticator?: { - passkey33: Uint8Array; + 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([ - Buffer.from(params.authenticatorDataRaw), - Buffer.from(sha256.hex(params.clientDataJsonRaw), 'hex'), + authenticatorDataRaw, + Buffer.from(sha256.hex(clientDataJsonRaw), 'hex'), ]), - Buffer.from(params.passkey33), + Buffer.from(params.passkeyPubkey), Buffer.from(params.signature64) ); @@ -404,10 +462,10 @@ export class LazorkitClient { params.payer, params.smartWallet, { - passkeyPubkey: Array.from(params.passkey33), - signature: Buffer.from(params.signature64), - clientDataJsonRaw: Buffer.from(params.clientDataJsonRaw), - authenticatorDataRaw: Buffer.from(params.authenticatorDataRaw), + passkeyPubkey: params.passkeyPubkey, + signature: Buffer.from(params.signature64, 'base64'), + clientDataJsonRaw, + authenticatorDataRaw, verifyInstructionIndex: 0, destroyRuleData: params.destroyRuleInstruction.data, initRuleData: params.initRuleInstruction.data, @@ -416,7 +474,7 @@ export class LazorkitClient { params.destroyRuleInstruction.keys.length, newAuthenticator: params.newAuthenticator ? { - passkeyPubkey: Array.from(params.newAuthenticator.passkey33), + passkeyPubkey: Array.from(params.newAuthenticator.passkeyPubkey), credentialId: Buffer.from( params.newAuthenticator.credentialIdBase64, 'base64' @@ -433,35 +491,48 @@ export class LazorkitClient { async commitCpiTx(params: { payer: PublicKey; smartWallet: PublicKey; - passkey33: Uint8Array; - signature64: Uint8Array; - clientDataJsonRaw: Uint8Array; - authenticatorDataRaw: Uint8Array; - ruleInstruction: TransactionInstruction; - cpiProgram: 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([ - Buffer.from(params.authenticatorDataRaw), - Buffer.from(sha256.hex(params.clientDataJsonRaw), 'hex'), + authenticatorDataRaw, + Buffer.from(sha256.hex(clientDataJsonRaw), 'hex'), ]), - Buffer.from(params.passkey33), + 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: Array.from(params.passkey33), - signature: Buffer.from(params.signature64), - clientDataJsonRaw: Buffer.from(params.clientDataJsonRaw), - authenticatorDataRaw: Buffer.from(params.authenticatorDataRaw), + passkeyPubkey: params.passkeyPubkey, + signature: Buffer.from(params.signature64, 'base64'), + clientDataJsonRaw: clientDataJsonRaw, + authenticatorDataRaw: authenticatorDataRaw, expiresAt: new BN(params.expiresAt), - ruleData: params.ruleInstruction.data, + ruleData: ruleInstruction.data, verifyInstructionIndex: 0, }, - params.ruleInstruction + ruleInstruction ); const tx = new Transaction().add(verifyIx).add(ix); tx.feePayer = params.payer; @@ -484,40 +555,57 @@ export class LazorkitClient { } // Legacy-compat APIs for simpler DX - async initializeTxn(payer: PublicKey, defaultRuleProgram: PublicKey) { - const ix = await this.buildInitializeIx(payer, defaultRuleProgram); + async initializeTxn(payer: PublicKey) { + const ix = await this.buildInitializeIx(payer); return new Transaction().add(ix); } async createSmartWalletTx(params: { payer: PublicKey; - passkey33: Uint8Array; + passkeyPubkey: number[]; credentialIdBase64: string; - ruleInstruction: TransactionInstruction; - isPayForUser?: boolean; - defaultRuleProgram: PublicKey; + 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: Array.from(params.passkey33), + passkeyPubkey: params.passkeyPubkey, credentialId: Buffer.from(params.credentialIdBase64, 'base64'), - ruleData: params.ruleInstruction.data, + ruleData: ruleInstruction.data, walletId: smartWalletId, isPayForUser: params.isPayForUser, }; + const ix = await this.buildCreateSmartWalletIx( params.payer, - args, - params.ruleInstruction + smartWallet, + smartWalletAuthenticator, + ruleInstruction, + args ); const tx = new Transaction().add(ix); tx.feePayer = params.payer; tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash; - const smartWallet = this.smartWalletPda(smartWalletId); return { transaction: tx, smartWalletId: smartWalletId, diff --git a/sdk/constants.ts b/contract-integration/constants.ts similarity index 100% rename from sdk/constants.ts rename to contract-integration/constants.ts diff --git a/sdk/index.ts b/contract-integration/index.ts similarity index 100% rename from sdk/index.ts rename to contract-integration/index.ts diff --git a/sdk/messages.ts b/contract-integration/messages.ts similarity index 100% rename from sdk/messages.ts rename to contract-integration/messages.ts diff --git a/sdk/pda/defaultRule.ts b/contract-integration/pda/defaultRule.ts similarity index 100% rename from sdk/pda/defaultRule.ts rename to contract-integration/pda/defaultRule.ts diff --git a/sdk/pda/lazorkit.ts b/contract-integration/pda/lazorkit.ts similarity index 100% rename from sdk/pda/lazorkit.ts rename to contract-integration/pda/lazorkit.ts diff --git a/sdk/types.ts b/contract-integration/types.ts similarity index 99% rename from sdk/types.ts rename to contract-integration/types.ts index 4fb6388..97c63f4 100644 --- a/sdk/types.ts +++ b/contract-integration/types.ts @@ -1,5 +1,4 @@ import * as anchor from '@coral-xyz/anchor'; - import { Lazorkit } from '../target/types/lazorkit'; // Account types diff --git a/sdk/utils.ts b/contract-integration/utils.ts similarity index 100% rename from sdk/utils.ts rename to contract-integration/utils.ts diff --git a/sdk/webauthn/secp256r1.ts b/contract-integration/webauthn/secp256r1.ts similarity index 100% rename from sdk/webauthn/secp256r1.ts rename to contract-integration/webauthn/secp256r1.ts diff --git a/tests/smart_wallet_with_default_rule.test.ts b/tests/smart_wallet_with_default_rule.test.ts index b0b4af4..d59281a 100644 --- a/tests/smart_wallet_with_default_rule.test.ts +++ b/tests/smart_wallet_with_default_rule.test.ts @@ -4,7 +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 { LazorkitClient, DefaultRuleClient } from '../sdk'; +import { LazorkitClient, DefaultRuleClient } from '../contract-integration'; dotenv.config(); describe('Test smart wallet with default rule', () => { @@ -14,7 +14,6 @@ describe('Test smart wallet with default rule', () => { ); const lazorkitProgram = new LazorkitClient(connection); - const defaultRuleProgram = new DefaultRuleClient(connection); const payer = anchor.web3.Keypair.fromSecretKey( bs58.decode(process.env.PRIVATE_KEY!) @@ -28,10 +27,7 @@ describe('Test smart wallet with default rule', () => { ); if (programConfig === null) { - const txn = await lazorkitProgram.initializeTxn( - payer.publicKey, - defaultRuleProgram.programId - ); + const txn = await lazorkitProgram.initializeTxn(payer.publicKey); const sig = await sendAndConfirmTransaction(connection, txn, [payer], { commitment: 'confirmed', @@ -47,30 +43,23 @@ describe('Test smart wallet with default rule', () => { 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.smartWalletPda(smartWalletId); const smartWalletAuthenticator = - lazorkitProgram.smartWalletAuthenticatorPda(smartWallet, pubkey); - - const initRuleIns = await defaultRuleProgram.buildInitRuleIx( - payer.publicKey, - smartWallet, - smartWalletAuthenticator - ); + lazorkitProgram.smartWalletAuthenticatorPda(smartWallet, passkeyPubkey); const credentialId = base64.encode(Buffer.from('testing something')); // random string const { transaction: createSmartWalletTxn } = await lazorkitProgram.createSmartWalletTx({ payer: payer.publicKey, - passkey33: Buffer.from(pubkey), + passkeyPubkey, credentialIdBase64: credentialId, - ruleInstruction: initRuleIns, + ruleInstruction: null, isPayForUser: true, - defaultRuleProgram: defaultRuleProgram.programId, smartWalletId, }); @@ -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() @@ -132,7 +121,7 @@ describe('Test smart wallet with default rule', () => { addresses: [ lazorkitProgram.configPda(), lazorkitProgram.whitelistRuleProgramsPda(), - defaultRuleProgram.programId, + lazorkitProgram.defaultRuleProgram.programId, anchor.web3.SystemProgram.programId, anchor.web3.SYSVAR_RENT_PUBKEY, anchor.web3.SYSVAR_CLOCK_PUBKEY, From 985d35a933db6b98b0579e95273f35557f326529 Mon Sep 17 00:00:00 2001 From: onspeedhp Date: Tue, 12 Aug 2025 01:02:19 +0700 Subject: [PATCH 6/6] Refactor constants and types in the LazorKit program for improved clarity and maintainability. Consolidate buffer initialization for seeds in constants.ts, update import paths in client files, and enhance message argument structures in types.ts. Additionally, streamline transaction instruction handling in client classes and ensure consistent formatting across the codebase. --- .../anchor/idl/default_rule.json | 272 ++ contract-integration/anchor/idl/lazorkit.json | 2895 ++++++++++++++++ .../anchor/types/default_rule.ts | 278 ++ contract-integration/anchor/types/lazorkit.ts | 2901 +++++++++++++++++ contract-integration/client/defaultRule.ts | 24 +- contract-integration/client/lazorkit.ts | 206 +- contract-integration/constants.ts | 8 +- contract-integration/index.ts | 1 + contract-integration/messages.ts | 17 +- contract-integration/pda/defaultRule.ts | 5 +- contract-integration/pda/lazorkit.ts | 33 +- contract-integration/types.ts | 41 +- contract-integration/webauthn/secp256r1.ts | 19 +- 13 files changed, 6525 insertions(+), 175 deletions(-) create mode 100644 contract-integration/anchor/idl/default_rule.json create mode 100644 contract-integration/anchor/idl/lazorkit.json create mode 100644 contract-integration/anchor/types/default_rule.ts create mode 100644 contract-integration/anchor/types/lazorkit.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 index 4700578..f4e3077 100644 --- a/contract-integration/client/defaultRule.ts +++ b/contract-integration/client/defaultRule.ts @@ -1,12 +1,7 @@ import * as anchor from '@coral-xyz/anchor'; -import { - Connection, - PublicKey, - SystemProgram, - TransactionInstruction, -} from '@solana/web3.js'; -import DefaultRuleIdl from '../../target/idl/default_rule.json'; -import { DefaultRule } from '../../target/types/default_rule'; +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 { @@ -17,12 +12,9 @@ export class DefaultRuleClient { constructor(connection: Connection) { this.connection = connection; - this.program = new anchor.Program( - DefaultRuleIdl as DefaultRule, - { - connection: connection, - } - ); + this.program = new anchor.Program(DefaultRuleIdl as DefaultRule, { + connection: connection, + }); this.programId = this.program.programId; } @@ -47,9 +39,7 @@ export class DefaultRuleClient { .instruction(); } - async buildCheckRuleIx( - smartWalletAuthenticator: PublicKey - ): Promise { + async buildCheckRuleIx(smartWalletAuthenticator: PublicKey): Promise { return await this.program.methods .checkRule() .accountsPartial({ diff --git a/contract-integration/client/lazorkit.ts b/contract-integration/client/lazorkit.ts index 7eb5d99..b26d9d3 100644 --- a/contract-integration/client/lazorkit.ts +++ b/contract-integration/client/lazorkit.ts @@ -10,8 +10,8 @@ import { VersionedTransaction, AccountMeta, } from '@solana/web3.js'; -import LazorkitIdl from '../../target/idl/lazorkit.json'; -import { Lazorkit } from '../../target/types/lazorkit'; +import LazorkitIdl from '../anchor/idl/lazorkit.json'; +import { Lazorkit } from '../anchor/types/lazorkit'; import { deriveConfigPda, deriveWhitelistRuleProgramsPda, @@ -27,6 +27,8 @@ 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; @@ -57,15 +59,8 @@ export class LazorkitClient { smartWalletConfigPda(smartWallet: PublicKey): PublicKey { return deriveSmartWalletConfigPda(this.programId, smartWallet); } - smartWalletAuthenticatorPda( - smartWallet: PublicKey, - passkey: number[] - ): PublicKey { - return deriveSmartWalletAuthenticatorPda( - this.programId, - smartWallet, - passkey - )[0]; + 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); @@ -84,9 +79,7 @@ export class LazorkitClient { return await this.program.account.smartWalletConfig.fetch(pda); } async getSmartWalletAuthenticatorData(smartWalletAuthenticator: PublicKey) { - return await this.program.account.smartWalletAuthenticator.fetch( - smartWalletAuthenticator - ); + return await this.program.account.smartWalletAuthenticator.fetch(smartWalletAuthenticator); } async getSmartWalletByPasskey(passkeyPubkey: number[]): Promise<{ smartWallet: PublicKey | null; @@ -111,8 +104,9 @@ export class LazorkitClient { return { smartWalletAuthenticator: null, smartWallet: null }; } - const smartWalletAuthenticatorData = - await this.getSmartWalletAuthenticatorData(accounts[0].pubkey); + const smartWalletAuthenticatorData = await this.getSmartWalletAuthenticatorData( + accounts[0].pubkey + ); return { smartWalletAuthenticator: accounts[0].pubkey, @@ -169,10 +163,7 @@ export class LazorkitClient { payer, smartWallet, smartWalletConfig: this.smartWalletConfigPda(smartWallet), - smartWalletAuthenticator: this.smartWalletAuthenticatorPda( - smartWallet, - args.passkeyPubkey - ), + smartWalletAuthenticator: this.smartWalletAuthenticatorPda(smartWallet, args.passkeyPubkey), whitelistRulePrograms: this.whitelistRuleProgramsPda(), authenticatorProgram: ruleInstruction.programId, cpiProgram: cpiInstruction.programId, @@ -215,10 +206,7 @@ export class LazorkitClient { config: this.configPda(), smartWallet, smartWalletConfig: this.smartWalletConfigPda(smartWallet), - smartWalletAuthenticator: this.smartWalletAuthenticatorPda( - smartWallet, - args.passkeyPubkey - ), + smartWalletAuthenticator: this.smartWalletAuthenticatorPda(smartWallet, args.passkeyPubkey), ruleProgram: ruleInstruction.programId, whitelistRulePrograms: this.whitelistRuleProgramsPda(), ixSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, @@ -259,10 +247,7 @@ export class LazorkitClient { config: this.configPda(), smartWallet, smartWalletConfig: this.smartWalletConfigPda(smartWallet), - smartWalletAuthenticator: this.smartWalletAuthenticatorPda( - smartWallet, - args.passkeyPubkey - ), + smartWalletAuthenticator: this.smartWalletAuthenticatorPda(smartWallet, args.passkeyPubkey), oldRuleProgram: destroyRuleInstruction.programId, newRuleProgram: initRuleInstruction.programId, whitelistRulePrograms: this.whitelistRuleProgramsPda(), @@ -286,10 +271,7 @@ export class LazorkitClient { config: this.configPda(), smartWallet, smartWalletConfig: this.smartWalletConfigPda(smartWallet), - smartWalletAuthenticator: this.smartWalletAuthenticatorPda( - smartWallet, - args.passkeyPubkey - ), + smartWalletAuthenticator: this.smartWalletAuthenticatorPda(smartWallet, args.passkeyPubkey), whitelistRulePrograms: this.whitelistRuleProgramsPda(), authenticatorProgram: ruleInstruction.programId, ixSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, @@ -330,19 +312,13 @@ export class LazorkitClient { signature64: String; clientDataJsonRaw64: String; authenticatorDataRaw64: String; - ruleInstruction: TransactionInstruction | null; + ruleInstruction?: TransactionInstruction; cpiInstruction: TransactionInstruction; }): Promise { - const authenticatorDataRaw = Buffer.from( - params.authenticatorDataRaw64, - 'base64' - ); + 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.concat([authenticatorDataRaw, Buffer.from(sha256.hex(clientDataJsonRaw), 'hex')]), Buffer.from(params.passkeyPubkey), Buffer.from(params.signature64) ); @@ -387,17 +363,11 @@ export class LazorkitClient { credentialIdBase64: string; }; // optional }): Promise { - const authenticatorDataRaw = Buffer.from( - params.authenticatorDataRaw64, - 'base64' - ); + 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.concat([authenticatorDataRaw, Buffer.from(sha256.hex(clientDataJsonRaw), 'hex')]), Buffer.from(params.passkeyPubkey), Buffer.from(params.signature64) ); @@ -413,16 +383,12 @@ export class LazorkitClient { newAuthenticator: params.newAuthenticator ? { passkeyPubkey: Array.from(params.newAuthenticator.passkeyPubkey), - credentialId: Buffer.from( - params.newAuthenticator.credentialIdBase64, - 'base64' - ), + credentialId: Buffer.from(params.newAuthenticator.credentialIdBase64, 'base64'), } : null, ruleData: params.ruleInstruction.data, verifyInstructionIndex: - (params.newAuthenticator ? 1 : 0) + - params.ruleInstruction.keys.length, + (params.newAuthenticator ? 1 : 0) + params.ruleInstruction.keys.length, }, params.ruleInstruction ); @@ -443,17 +409,11 @@ export class LazorkitClient { credentialIdBase64: string; }; // optional }): Promise { - const authenticatorDataRaw = Buffer.from( - params.authenticatorDataRaw64, - 'base64' - ); + 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.concat([authenticatorDataRaw, Buffer.from(sha256.hex(clientDataJsonRaw), 'hex')]), Buffer.from(params.passkeyPubkey), Buffer.from(params.signature64) ); @@ -469,16 +429,11 @@ export class LazorkitClient { verifyInstructionIndex: 0, destroyRuleData: params.destroyRuleInstruction.data, initRuleData: params.initRuleInstruction.data, - splitIndex: - (params.newAuthenticator ? 1 : 0) + - params.destroyRuleInstruction.keys.length, + 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' - ), + credentialId: Buffer.from(params.newAuthenticator.credentialIdBase64, 'base64'), } : null, }, @@ -498,17 +453,11 @@ export class LazorkitClient { ruleInstruction?: TransactionInstruction; expiresAt: number; }) { - const authenticatorDataRaw = Buffer.from( - params.authenticatorDataRaw64, - 'base64' - ); + 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.concat([authenticatorDataRaw, Buffer.from(sha256.hex(clientDataJsonRaw), 'hex')]), Buffer.from(params.passkeyPubkey), Buffer.from(params.signature64) ); @@ -534,17 +483,25 @@ export class LazorkitClient { }, ruleInstruction ); - const tx = new Transaction().add(verifyIx).add(ix); - tx.feePayer = params.payer; - tx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash; - return tx; + 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 { + async buildV0Tx(payer: PublicKey, ixs: TransactionInstruction[]): Promise { const { blockhash } = await this.connection.getLatestBlockhash(); const msg = new TransactionMessage({ payerKey: payer, @@ -564,8 +521,8 @@ export class LazorkitClient { payer: PublicKey; passkeyPubkey: number[]; credentialIdBase64: string; - ruleInstruction: TransactionInstruction | null; - isPayForUser: boolean; + ruleInstruction?: TransactionInstruction | null; + isPayForUser?: boolean; smartWalletId?: BN; }) { let smartWalletId: BN = this.generateWalletId(); @@ -593,7 +550,7 @@ export class LazorkitClient { credentialId: Buffer.from(params.credentialIdBase64, 'base64'), ruleData: ruleInstruction.data, walletId: smartWalletId, - isPayForUser: params.isPayForUser, + isPayForUser: params.isPayForUser === true, }; const ix = await this.buildCreateSmartWalletIx( @@ -612,4 +569,75 @@ export class LazorkitClient { 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/contract-integration/constants.ts b/contract-integration/constants.ts index a1409cc..690c7b7 100644 --- a/contract-integration/constants.ts +++ b/contract-integration/constants.ts @@ -3,12 +3,8 @@ 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'); diff --git a/contract-integration/index.ts b/contract-integration/index.ts index 9599bb0..88e4ddb 100644 --- a/contract-integration/index.ts +++ b/contract-integration/index.ts @@ -6,3 +6,4 @@ if (typeof globalThis.structuredClone !== 'function') { // 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 index 7365b2f..d387b18 100644 --- a/contract-integration/messages.ts +++ b/contract-integration/messages.ts @@ -32,7 +32,6 @@ const coder: anchor.BorshCoder = (() => { { name: 'currentTimestamp', type: 'i64' }, { name: 'ruleDataHash', type: { array: ['u8', 32] } }, { name: 'ruleAccountsHash', type: { array: ['u8', 32] } }, - { name: 'newPasskey', type: { option: { array: ['u8', 33] } } }, ], }, }, @@ -98,12 +97,10 @@ export function buildCallRuleMessage( payer: anchor.web3.PublicKey, nonce: anchor.BN, now: anchor.BN, - ruleProgram: anchor.web3.PublicKey, - ruleIns: anchor.web3.TransactionInstruction, - newPasskey?: Uint8Array | number[] | Buffer | null + ruleIns: anchor.web3.TransactionInstruction ): Buffer { const ruleMetas = instructionToAccountMetas(ruleIns, payer); - const ruleAccountsHash = computeAccountsHash(ruleProgram, ruleMetas); + const ruleAccountsHash = computeAccountsHash(ruleIns.programId, ruleMetas); const ruleDataHash = new Uint8Array(sha256.arrayBuffer(ruleIns.data)); const encoded = coder.types.encode('CallRuleMessage', { @@ -111,10 +108,6 @@ export function buildCallRuleMessage( currentTimestamp: now, ruleDataHash: Array.from(ruleDataHash), ruleAccountsHash: Array.from(ruleAccountsHash), - newPasskey: - newPasskey && (newPasskey as any).length - ? Array.from(new Uint8Array(newPasskey as any)) - : null, }); return Buffer.from(encoded); } @@ -123,17 +116,15 @@ export function buildChangeRuleMessage( payer: anchor.web3.PublicKey, nonce: anchor.BN, now: anchor.BN, - oldRuleProgram: anchor.web3.PublicKey, destroyRuleIns: anchor.web3.TransactionInstruction, - newRuleProgram: anchor.web3.PublicKey, initRuleIns: anchor.web3.TransactionInstruction ): Buffer { const oldMetas = instructionToAccountMetas(destroyRuleIns, payer); - const oldAccountsHash = computeAccountsHash(oldRuleProgram, oldMetas); + const oldAccountsHash = computeAccountsHash(destroyRuleIns.programId, oldMetas); const oldDataHash = new Uint8Array(sha256.arrayBuffer(destroyRuleIns.data)); const newMetas = instructionToAccountMetas(initRuleIns, payer); - const newAccountsHash = computeAccountsHash(newRuleProgram, newMetas); + const newAccountsHash = computeAccountsHash(initRuleIns.programId, newMetas); const newDataHash = new Uint8Array(sha256.arrayBuffer(initRuleIns.data)); const encoded = coder.types.encode('ChangeRuleMessage', { diff --git a/contract-integration/pda/defaultRule.ts b/contract-integration/pda/defaultRule.ts index 9e48e48..4b10442 100644 --- a/contract-integration/pda/defaultRule.ts +++ b/contract-integration/pda/defaultRule.ts @@ -1,6 +1,7 @@ -import { PublicKey } from "@solana/web3.js"; +import { PublicKey } from '@solana/web3.js'; +import { Buffer } from 'buffer'; -export const RULE_SEED = Buffer.from("rule"); +export const RULE_SEED = Buffer.from('rule'); export function deriveRulePda( programId: PublicKey, diff --git a/contract-integration/pda/lazorkit.ts b/contract-integration/pda/lazorkit.ts index 4bba9df..5ca35a0 100644 --- a/contract-integration/pda/lazorkit.ts +++ b/contract-integration/pda/lazorkit.ts @@ -2,33 +2,21 @@ 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 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 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 deriveWhitelistRuleProgramsPda(programId: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync([WHITELIST_RULE_PROGRAMS_SEED], programId)[0]; } -export function deriveSmartWalletPda( - programId: PublicKey, - walletId: BN -): PublicKey { +export function deriveSmartWalletPda(programId: PublicKey, walletId: BN): PublicKey { return PublicKey.findProgramAddressSync( [SMART_WALLET_SEED, walletId.toArrayLike(Buffer, 'le', 8)], programId @@ -46,10 +34,7 @@ export function deriveSmartWalletConfigPda( } // Must match on-chain: sha256(passkey(33) || wallet(32)) -export function hashPasskeyWithWallet( - passkeyCompressed33: number[], - wallet: PublicKey -): Buffer { +export function hashPasskeyWithWallet(passkeyCompressed33: number[], wallet: PublicKey): Buffer { const { sha256 } = require('js-sha256'); const buf = Buffer.alloc(65); Buffer.from(passkeyCompressed33).copy(buf, 0); @@ -75,11 +60,7 @@ export function deriveCpiCommitPda( lastNonce: BN ): PublicKey { return PublicKey.findProgramAddressSync( - [ - CPI_COMMIT_SEED, - smartWallet.toBuffer(), - lastNonce.toArrayLike(Buffer, 'le', 8), - ], + [CPI_COMMIT_SEED, smartWallet.toBuffer(), lastNonce.toArrayLike(Buffer, 'le', 8)], programId )[0]; } diff --git a/contract-integration/types.ts b/contract-integration/types.ts index 97c63f4..82a49a1 100644 --- a/contract-integration/types.ts +++ b/contract-integration/types.ts @@ -1,23 +1,46 @@ import * as anchor from '@coral-xyz/anchor'; -import { Lazorkit } from '../target/types/lazorkit'; +import { Lazorkit } from './anchor/types/lazorkit'; // Account types export type SmartWalletConfig = anchor.IdlTypes['smartWalletConfig']; -export type SmartWalletAuthenticator = - anchor.IdlTypes['smartWalletAuthenticator']; +export type SmartWalletAuthenticator = anchor.IdlTypes['smartWalletAuthenticator']; export type Config = anchor.IdlTypes['config']; -export type WhitelistRulePrograms = - anchor.IdlTypes['whitelistRulePrograms']; +export type WhitelistRulePrograms = anchor.IdlTypes['whitelistRulePrograms']; // argument type -export type CreatwSmartWalletArgs = - anchor.IdlTypes['creatwSmartWalletArgs']; +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']; +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/webauthn/secp256r1.ts b/contract-integration/webauthn/secp256r1.ts index 00df704..e8b68f3 100644 --- a/contract-integration/webauthn/secp256r1.ts +++ b/contract-integration/webauthn/secp256r1.ts @@ -8,19 +8,15 @@ const SIGNATURE_SERIALIZED_SIZE = 64; const COMPRESSED_PUBKEY_SERIALIZED_SIZE = 33; const FIELD_SIZE = 32; -export const SECP256R1_PROGRAM_ID = new PublicKey( - 'Secp256r1SigVerify1111111111111111111111111' -); +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, + 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, + 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 { @@ -85,10 +81,7 @@ export function buildSecp256r1VerifyIx( } const totalSize = - DATA_START + - SIGNATURE_SERIALIZED_SIZE + - COMPRESSED_PUBKEY_SERIALIZED_SIZE + - message.length; + DATA_START + SIGNATURE_SERIALIZED_SIZE + COMPRESSED_PUBKEY_SERIALIZED_SIZE + message.length; const data = new Uint8Array(totalSize); const numSignatures = 1;