Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions packages/wasm-solana/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@
},
"license": "MIT",
"files": [
"dist/esm/js/**/*",
"dist/cjs/js/**/*",
"dist/cjs/package.json"
"dist"
],
"exports": {
".": {
Expand Down
117 changes: 73 additions & 44 deletions packages/wasm-solana/src/intent/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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::<Result<Vec<_>, 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<Instruction> = 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(
Expand All @@ -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;
Expand Down
9 changes: 9 additions & 0 deletions packages/wasm-solana/src/intent/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
/// Single token address (legacy format)
#[serde(default)]
pub token_address: Option<String>,
/// Multiple token addresses (array format from wallet-platform)
#[serde(default)]
pub token_addresses: Option<Vec<String>>,
/// Single token program ID (legacy format)
#[serde(default)]
pub token_program_id: Option<String>,
/// Multiple token program IDs (array format, parallel to token_addresses)
#[serde(default)]
pub token_program_ids: Option<Vec<String>>,
#[serde(default)]
pub memo: Option<String>,
}
Expand Down
37 changes: 37 additions & 0 deletions packages/wasm-solana/test/intentBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down