From 769cfdaa87997655a69d614a5b205c3451d844ab Mon Sep 17 00:00:00 2001 From: Luis Covarrubias Date: Thu, 5 Feb 2026 20:40:25 -0800 Subject: [PATCH 1/2] feat: support enableToken intent with token address arrays - Update EnableTokenIntent to accept token_addresses and token_program_ids arrays - Update build_enable_token to iterate arrays and create one ATA instruction per token - Maintains backward compatibility with single tokenAddress format - Document well-known program IDs with links to official docs --- packages/wasm-solana/src/intent/build.rs | 117 ++++++++++++++--------- packages/wasm-solana/src/intent/types.rs | 9 ++ 2 files changed, 82 insertions(+), 44 deletions(-) diff --git a/packages/wasm-solana/src/intent/build.rs b/packages/wasm-solana/src/intent/build.rs index c15b578..a176e0c 100644 --- a/packages/wasm-solana/src/intent/build.rs +++ b/packages/wasm-solana/src/intent/build.rs @@ -20,6 +20,14 @@ use solana_stake_interface::instruction as stake_ix; use solana_stake_interface::state::{Authorized, Lockup}; use solana_system_interface::instruction as system_ix; +// Well-known Solana program IDs +// SPL Token Program: https://www.solana-program.com/docs/token +const SPL_TOKEN_PROGRAM_ID: &str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"; +// Associated Token Account Program: https://www.solana-program.com/docs/associated-token-account +const SPL_ATA_PROGRAM_ID: &str = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"; +// System Program +const SYSTEM_PROGRAM_ID: &str = "11111111111111111111111111111111"; + // Constants const STAKE_ACCOUNT_SPACE: u64 = 200; const STAKE_ACCOUNT_RENT: u64 = 2282880; // ~0.00228288 SOL @@ -615,49 +623,73 @@ fn build_enable_token( .map_err(|_| WasmSolanaError::new("Invalid recipientAddress"))? .unwrap_or(fee_payer); - let mint: Pubkey = intent - .token_address - .as_ref() - .ok_or_else(|| WasmSolanaError::new("Missing tokenAddress"))? - .parse() - .map_err(|_| WasmSolanaError::new("Invalid tokenAddress"))?; - - let token_program: Pubkey = intent - .token_program_id - .as_ref() - .map(|p| p.parse()) - .transpose() - .map_err(|_| WasmSolanaError::new("Invalid tokenProgramId"))? - .unwrap_or_else(|| { - "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" - .parse() - .unwrap() - }); - - // Derive ATA address - let ata_program: Pubkey = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" - .parse() - .unwrap(); - let seeds = &[owner.as_ref(), token_program.as_ref(), mint.as_ref()]; - let (ata, _bump) = Pubkey::find_program_address(seeds, &ata_program); + // Build list of (mint, token_program) pairs from either array or single format + let default_program: Pubkey = SPL_TOKEN_PROGRAM_ID.parse().unwrap(); + let token_pairs: Vec<(Pubkey, Pubkey)> = if let Some(ref addresses) = intent.token_addresses { + // Array format: tokenAddresses + tokenProgramIds + let program_ids = intent.token_program_ids.as_ref(); + + addresses + .iter() + .enumerate() + .map(|(i, addr)| { + let mint: Pubkey = addr.parse().map_err(|_| { + WasmSolanaError::new(&format!("Invalid tokenAddress at index {}", i)) + })?; + let program: Pubkey = program_ids + .and_then(|ids| ids.get(i)) + .map(|p| p.parse().unwrap_or(default_program)) + .unwrap_or(default_program); + Ok((mint, program)) + }) + .collect::, WasmSolanaError>>()? + } else if let Some(ref addr) = intent.token_address { + // Single format: tokenAddress + tokenProgramId + let mint: Pubkey = addr + .parse() + .map_err(|_| WasmSolanaError::new("Invalid tokenAddress"))?; + let token_program: Pubkey = intent + .token_program_id + .as_ref() + .map(|p| p.parse()) + .transpose() + .map_err(|_| WasmSolanaError::new("Invalid tokenProgramId"))? + .unwrap_or(default_program); + vec![(mint, token_program)] + } else { + return Err(WasmSolanaError::new( + "Missing tokenAddress or tokenAddresses", + )); + }; - let system_program: Pubkey = "11111111111111111111111111111111".parse().unwrap(); + let ata_program: Pubkey = SPL_ATA_PROGRAM_ID.parse().unwrap(); + let system_program: Pubkey = SYSTEM_PROGRAM_ID.parse().unwrap(); use solana_sdk::instruction::AccountMeta; - let instruction = Instruction::new_with_bytes( - ata_program, - &[], - vec![ - AccountMeta::new(fee_payer, true), - AccountMeta::new(ata, false), - AccountMeta::new_readonly(owner, false), - AccountMeta::new_readonly(mint, false), - AccountMeta::new_readonly(system_program, false), - AccountMeta::new_readonly(token_program, false), - ], - ); - Ok((vec![instruction], vec![])) + // Build one instruction per token + let instructions: Vec = token_pairs + .iter() + .map(|(mint, token_program)| { + let seeds = &[owner.as_ref(), token_program.as_ref(), mint.as_ref()]; + let (ata, _bump) = Pubkey::find_program_address(seeds, &ata_program); + + Instruction::new_with_bytes( + ata_program, + &[], + vec![ + AccountMeta::new(fee_payer, true), + AccountMeta::new(ata, false), + AccountMeta::new_readonly(owner, false), + AccountMeta::new_readonly(*mint, false), + AccountMeta::new_readonly(system_program, false), + AccountMeta::new_readonly(*token_program, false), + ], + ) + }) + .collect(); + + Ok((instructions, vec![])) } fn build_close_ata( @@ -683,17 +715,14 @@ fn build_close_ata( .parse() .map_err(|_| WasmSolanaError::new("Invalid tokenAccountAddress"))?; + let default_program: Pubkey = SPL_TOKEN_PROGRAM_ID.parse().unwrap(); let token_program: Pubkey = intent .token_program_id .as_ref() .map(|p| p.parse()) .transpose() .map_err(|_| WasmSolanaError::new("Invalid tokenProgramId"))? - .unwrap_or_else(|| { - "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" - .parse() - .unwrap() - }); + .unwrap_or(default_program); // CloseAccount instruction use spl_token::instruction::TokenInstruction; diff --git a/packages/wasm-solana/src/intent/types.rs b/packages/wasm-solana/src/intent/types.rs index f60a6e3..d4bc8f7 100644 --- a/packages/wasm-solana/src/intent/types.rs +++ b/packages/wasm-solana/src/intent/types.rs @@ -233,16 +233,25 @@ pub struct DelegateIntent { } /// Enable token intent (create ATA) +/// Supports both single token (tokenAddress) and multiple tokens (tokenAddresses array) #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct EnableTokenIntent { pub intent_type: String, #[serde(default)] pub recipient_address: Option, + /// Single token address (legacy format) #[serde(default)] pub token_address: Option, + /// Multiple token addresses (array format from wallet-platform) + #[serde(default)] + pub token_addresses: Option>, + /// Single token program ID (legacy format) #[serde(default)] pub token_program_id: Option, + /// Multiple token program IDs (array format, parallel to token_addresses) + #[serde(default)] + pub token_program_ids: Option>, #[serde(default)] pub memo: Option, } From 78c2a0393d704b589fdc3197dffeb26720850627 Mon Sep 17 00:00:00 2001 From: Luis Covarrubias Date: Thu, 5 Feb 2026 22:43:02 -0800 Subject: [PATCH 2/2] test(wasm-solana): add test for enableToken with tokenAddresses array - Add test case for multiple tokens in enableToken intent - Simplify package.json files field to include full dist directory --- packages/wasm-solana/package.json | 4 +-- packages/wasm-solana/test/intentBuilder.ts | 37 ++++++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/packages/wasm-solana/package.json b/packages/wasm-solana/package.json index ff9f91c..f20718f 100644 --- a/packages/wasm-solana/package.json +++ b/packages/wasm-solana/package.json @@ -9,9 +9,7 @@ }, "license": "MIT", "files": [ - "dist/esm/js/**/*", - "dist/cjs/js/**/*", - "dist/cjs/package.json" + "dist" ], "exports": { ".": { diff --git a/packages/wasm-solana/test/intentBuilder.ts b/packages/wasm-solana/test/intentBuilder.ts index fa687f9..519e3a6 100644 --- a/packages/wasm-solana/test/intentBuilder.ts +++ b/packages/wasm-solana/test/intentBuilder.ts @@ -259,6 +259,43 @@ describe("buildFromIntent", function () { ); assert(createAta, "Should have CreateAssociatedTokenAccount instruction"); }); + + it("should build enableToken with multiple tokens (tokenAddresses array)", function () { + const intent = { + intentType: "enableToken", + tokenAddresses: [ + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC + "SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt", // SRM + "orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE", // ORCA + ], + tokenProgramIds: [ + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + ], + recipientAddress: feePayer, + }; + + const result = buildFromIntent(intent, { + feePayer, + nonce: { type: "blockhash", value: blockhash }, + }); + + assert(result.transaction instanceof Transaction, "Should return Transaction object"); + assert.equal(result.generatedKeypairs.length, 0, "Should not generate keypairs"); + + // Verify instructions + const parsed = parseTransaction(result.transaction.toBytes()); + + const createAtaInstructions = parsed.instructionsData.filter( + (i: any) => i.type === "CreateAssociatedTokenAccount", + ); + assert.equal( + createAtaInstructions.length, + 3, + "Should have 3 CreateAssociatedTokenAccount instructions", + ); + }); }); describe("closeAssociatedTokenAccount intent", function () {