From e2de5cc9dc8010b64e93f1cd6122aec6fad5dbf2 Mon Sep 17 00:00:00 2001 From: Luis Covarrubias Date: Thu, 5 Feb 2026 19:02:09 -0800 Subject: [PATCH] refactor(wasm-solana): remove unused instruction-level builder Remove the instruction-level `buildTransaction` API that was replaced by `buildFromIntent`. The intent-based API is now used in production by wallet-platform for all transaction building. Removed: - src/builder/ module (build.rs, types.rs, versioned.rs) - src/wasm/builder.rs WASM bindings - js/builder.ts TypeScript wrapper - test/builder.ts tests - All builder type exports from index.ts The `buildFromIntent` API remains as the primary transaction building interface. --- packages/wasm-solana/js/builder.ts | 610 ------- packages/wasm-solana/js/index.ts | 44 - packages/wasm-solana/js/versioned.ts | 42 +- packages/wasm-solana/src/builder/build.rs | 1588 ----------------- packages/wasm-solana/src/builder/mod.rs | 23 - packages/wasm-solana/src/builder/types.rs | 456 ----- packages/wasm-solana/src/builder/versioned.rs | 258 --- packages/wasm-solana/src/lib.rs | 5 +- packages/wasm-solana/src/wasm/builder.rs | 118 -- packages/wasm-solana/src/wasm/mod.rs | 2 - packages/wasm-solana/test/builder.ts | 694 ------- packages/wasm-solana/test/transaction.ts | 144 -- 12 files changed, 3 insertions(+), 3981 deletions(-) delete mode 100644 packages/wasm-solana/js/builder.ts delete mode 100644 packages/wasm-solana/src/builder/build.rs delete mode 100644 packages/wasm-solana/src/builder/mod.rs delete mode 100644 packages/wasm-solana/src/builder/types.rs delete mode 100644 packages/wasm-solana/src/builder/versioned.rs delete mode 100644 packages/wasm-solana/src/wasm/builder.rs delete mode 100644 packages/wasm-solana/test/builder.ts diff --git a/packages/wasm-solana/js/builder.ts b/packages/wasm-solana/js/builder.ts deleted file mode 100644 index 9ff1608..0000000 --- a/packages/wasm-solana/js/builder.ts +++ /dev/null @@ -1,610 +0,0 @@ -/** - * Transaction building from high-level intents. - * - * Provides types and functions for building Solana transactions from a - * declarative intent structure, without requiring the full @solana/web3.js dependency. - */ - -import { BuilderNamespace } from "./wasm/wasm_solana.js"; -import { Transaction } from "./transaction.js"; -import { VersionedTransaction } from "./versioned.js"; - -// ============================================================================= -// Nonce Types -// ============================================================================= - -/** Use a recent blockhash for the transaction */ -export interface BlockhashNonceSource { - type: "blockhash"; - /** The recent blockhash value (base58) */ - value: string; -} - -/** Use a durable nonce account for the transaction */ -export interface DurableNonceSource { - type: "durable"; - /** The nonce account address (base58) */ - address: string; - /** The nonce authority address (base58) */ - authority: string; - /** The nonce value stored in the account (base58) - this becomes the blockhash */ - value: string; -} - -/** Nonce source for the transaction */ -export type NonceSource = BlockhashNonceSource | DurableNonceSource; - -// ============================================================================= -// Address Lookup Table Types (Versioned Transactions) -// ============================================================================= - -/** - * Address Lookup Table data for versioned transactions. - * - * ALTs allow transactions to reference more accounts than the legacy format - * by storing account addresses in on-chain lookup tables. - */ -export interface AddressLookupTable { - /** The lookup table account address (base58) */ - accountKey: string; - /** Indices of writable accounts in the lookup table */ - writableIndexes: number[]; - /** Indices of readonly accounts in the lookup table */ - readonlyIndexes: number[]; -} - -// ============================================================================= -// Instruction Types -// ============================================================================= - -/** SOL transfer instruction */ -export interface TransferInstruction { - type: "transfer"; - /** Source account (base58) */ - from: string; - /** Destination account (base58) */ - to: string; - /** Amount in lamports */ - lamports: bigint; -} - -/** Create new account instruction */ -export interface CreateAccountInstruction { - type: "createAccount"; - /** Funding account (base58) */ - from: string; - /** New account address (base58) */ - newAccount: string; - /** Lamports to transfer */ - lamports: bigint; - /** Space to allocate in bytes */ - space: number; - /** Owner program (base58) */ - owner: string; -} - -/** Advance durable nonce instruction */ -export interface NonceAdvanceInstruction { - type: "nonceAdvance"; - /** Nonce account address (base58) */ - nonce: string; - /** Nonce authority (base58) */ - authority: string; -} - -/** Initialize nonce account instruction */ -export interface NonceInitializeInstruction { - type: "nonceInitialize"; - /** Nonce account address (base58) */ - nonce: string; - /** Nonce authority (base58) */ - authority: string; -} - -/** Allocate space instruction */ -export interface AllocateInstruction { - type: "allocate"; - /** Account to allocate (base58) */ - account: string; - /** Space to allocate in bytes */ - space: number; -} - -/** Assign account to program instruction */ -export interface AssignInstruction { - type: "assign"; - /** Account to assign (base58) */ - account: string; - /** New owner program (base58) */ - owner: string; -} - -/** Memo instruction */ -export interface MemoInstruction { - type: "memo"; - /** The memo message */ - message: string; -} - -/** Compute budget instruction */ -export interface ComputeBudgetInstruction { - type: "computeBudget"; - /** Compute unit limit (optional) */ - unitLimit?: number; - /** Compute unit price in micro-lamports (optional) */ - unitPrice?: number; -} - -// ============================================================================= -// Stake Program Instructions -// ============================================================================= - -/** Initialize a stake account instruction */ -export interface StakeInitializeInstruction { - type: "stakeInitialize"; - /** Stake account address (base58) */ - stake: string; - /** Authorized staker pubkey (base58) */ - staker: string; - /** Authorized withdrawer pubkey (base58) */ - withdrawer: string; -} - -/** Delegate stake to a validator instruction */ -export interface StakeDelegateInstruction { - type: "stakeDelegate"; - /** Stake account address (base58) */ - stake: string; - /** Vote account (validator) to delegate to (base58) */ - vote: string; - /** Stake authority (base58) */ - authority: string; -} - -/** Deactivate a stake account instruction */ -export interface StakeDeactivateInstruction { - type: "stakeDeactivate"; - /** Stake account address (base58) */ - stake: string; - /** Stake authority (base58) */ - authority: string; -} - -/** Withdraw from a stake account instruction */ -export interface StakeWithdrawInstruction { - type: "stakeWithdraw"; - /** Stake account address (base58) */ - stake: string; - /** Recipient address (base58) */ - recipient: string; - /** Amount in lamports to withdraw */ - lamports: bigint; - /** Withdraw authority (base58) */ - authority: string; -} - -/** Change stake account authorization instruction */ -export interface StakeAuthorizeInstruction { - type: "stakeAuthorize"; - /** Stake account address (base58) */ - stake: string; - /** New authority pubkey (base58) */ - newAuthority: string; - /** Authorization type: "staker" or "withdrawer" */ - authorizeType: "staker" | "withdrawer"; - /** Current authority (base58) */ - authority: string; -} - -/** Split stake account instruction (for partial deactivation) */ -export interface StakeSplitInstruction { - type: "stakeSplit"; - /** Source stake account address (base58) */ - stake: string; - /** Destination stake account (must be uninitialized/created first) (base58) */ - splitStake: string; - /** Stake authority (base58) */ - authority: string; - /** Amount in lamports to split */ - lamports: bigint; -} - -// ============================================================================= -// SPL Token Instructions -// ============================================================================= - -/** Transfer tokens instruction (uses TransferChecked) */ -export interface TokenTransferInstruction { - type: "tokenTransfer"; - /** Source token account (base58) */ - source: string; - /** Destination token account (base58) */ - destination: string; - /** Token mint address (base58) */ - mint: string; - /** Amount of tokens (in smallest units) */ - amount: bigint; - /** Number of decimals for the token */ - decimals: number; - /** Owner/authority of the source account (base58) */ - authority: string; - /** Token program ID (optional, defaults to SPL Token) */ - programId?: string; -} - -/** Create an Associated Token Account instruction */ -export interface CreateAssociatedTokenAccountInstruction { - type: "createAssociatedTokenAccount"; - /** Payer for account creation (base58) */ - payer: string; - /** Owner of the new ATA (base58) */ - owner: string; - /** Token mint address (base58) */ - mint: string; - /** Token program ID (optional, defaults to SPL Token) */ - tokenProgramId?: string; -} - -/** Close an Associated Token Account instruction */ -export interface CloseAssociatedTokenAccountInstruction { - type: "closeAssociatedTokenAccount"; - /** Token account to close (base58) */ - account: string; - /** Destination for remaining lamports (base58) */ - destination: string; - /** Authority of the account (base58) */ - authority: string; - /** Token program ID (optional, defaults to SPL Token) */ - programId?: string; -} - -/** Mint tokens to an account instruction */ -export interface MintToInstruction { - type: "mintTo"; - /** Token mint address (base58) */ - mint: string; - /** Destination token account (base58) */ - destination: string; - /** Mint authority (base58) */ - authority: string; - /** Amount of tokens to mint (in smallest units) */ - amount: bigint; - /** Token program ID (optional, defaults to SPL Token) */ - programId?: string; -} - -/** Burn tokens from an account instruction */ -export interface BurnInstruction { - type: "burn"; - /** Token mint address (base58) */ - mint: string; - /** Source token account to burn from (base58) */ - account: string; - /** Token account authority (base58) */ - authority: string; - /** Amount of tokens to burn (in smallest units) */ - amount: bigint; - /** Token program ID (optional, defaults to SPL Token) */ - programId?: string; -} - -/** Approve a delegate to transfer tokens instruction */ -export interface ApproveInstruction { - type: "approve"; - /** Token account to approve delegation for (base58) */ - account: string; - /** Delegate address (who can transfer) (base58) */ - delegate: string; - /** Token account owner (base58) */ - owner: string; - /** Amount of tokens to approve (in smallest units) */ - amount: bigint; - /** Token program ID (optional, defaults to SPL Token) */ - programId?: string; -} - -// ============================================================================= -// Jito Stake Pool Instructions -// ============================================================================= - -/** Deposit SOL into a stake pool (Jito liquid staking) */ -export interface StakePoolDepositSolInstruction { - type: "stakePoolDepositSol"; - /** Stake pool address (base58) */ - stakePool: string; - /** Withdraw authority PDA (base58) */ - withdrawAuthority: string; - /** Reserve stake account (base58) */ - reserveStake: string; - /** Funding account (SOL source, signer) (base58) */ - fundingAccount: string; - /** Destination for pool tokens (base58) */ - destinationPoolAccount: string; - /** Manager fee account (base58) */ - managerFeeAccount: string; - /** Referral pool account (base58) */ - referralPoolAccount: string; - /** Pool mint address (base58) */ - poolMint: string; - /** Amount in lamports to deposit */ - lamports: bigint; -} - -/** Withdraw stake from a stake pool (Jito liquid staking) */ -export interface StakePoolWithdrawStakeInstruction { - type: "stakePoolWithdrawStake"; - /** Stake pool address (base58) */ - stakePool: string; - /** Validator list account (base58) */ - validatorList: string; - /** Withdraw authority PDA (base58) */ - withdrawAuthority: string; - /** Validator stake account to split from (base58) */ - validatorStake: string; - /** Destination stake account (uninitialized) (base58) */ - destinationStake: string; - /** Authority for the destination stake account (base58) */ - destinationStakeAuthority: string; - /** Source pool token account authority (signer) (base58) */ - sourceTransferAuthority: string; - /** Source pool token account (base58) */ - sourcePoolAccount: string; - /** Manager fee account (base58) */ - managerFeeAccount: string; - /** Pool mint address (base58) */ - poolMint: string; - /** Amount of pool tokens to burn */ - poolTokens: bigint; -} - -// ============================================================================= -// Custom Instruction -// ============================================================================= - -/** Account metadata for custom instructions */ -export interface CustomAccountMeta { - /** Account public key (base58) */ - pubkey: string; - /** Whether the account is a signer */ - isSigner: boolean; - /** Whether the account is writable */ - isWritable: boolean; -} - -/** - * Custom instruction for invoking any program. - * Enables passthrough of arbitrary instructions for extensibility. - */ -export interface CustomInstruction { - type: "custom"; - /** The program ID to invoke (base58) */ - programId: string; - /** Account metas for the instruction */ - accounts: CustomAccountMeta[]; - /** Instruction data (base64 or hex encoded) */ - data: string; - /** Encoding of the data field: "base64" (default) or "hex" */ - encoding?: "base64" | "hex"; -} - -/** Union of all instruction types */ -export type Instruction = - | TransferInstruction - | CreateAccountInstruction - | NonceAdvanceInstruction - | NonceInitializeInstruction - | AllocateInstruction - | AssignInstruction - | MemoInstruction - | ComputeBudgetInstruction - | StakeInitializeInstruction - | StakeDelegateInstruction - | StakeDeactivateInstruction - | StakeWithdrawInstruction - | StakeAuthorizeInstruction - | StakeSplitInstruction - | TokenTransferInstruction - | CreateAssociatedTokenAccountInstruction - | CloseAssociatedTokenAccountInstruction - | MintToInstruction - | BurnInstruction - | ApproveInstruction - | StakePoolDepositSolInstruction - | StakePoolWithdrawStakeInstruction - | CustomInstruction; - -// ============================================================================= -// TransactionIntent -// ============================================================================= - -/** - * A declarative intent to build a Solana transaction. - * - * @example - * ```typescript - * const intent: TransactionIntent = { - * feePayer: 'DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB', - * nonce: { - * type: 'blockhash', - * value: 'GWaQEymC3Z9SHM2gkh8u12xL1zJPMHPCSVR3pSDpEXE4' - * }, - * instructions: [ - * { type: 'transfer', from: '...', to: '...', lamports: '1000000' } - * ] - * }; - * ``` - */ -export interface TransactionIntent { - /** The fee payer's public key (base58) */ - feePayer: string; - /** The nonce source (blockhash or durable nonce) */ - nonce: NonceSource; - /** List of instructions to include */ - instructions: Instruction[]; - - // ===== Versioned Transaction Fields (MessageV0) ===== - // If addressLookupTables is provided, a versioned transaction is built. - - /** - * Address Lookup Tables for versioned transactions. - * If provided, builds a MessageV0 transaction instead of legacy. - */ - addressLookupTables?: AddressLookupTable[]; - - /** - * Static account keys (for versioned transaction round-trip). - * These are the accounts stored directly in the message. - */ - staticAccountKeys?: string[]; -} - -// ============================================================================= -// buildTransaction function -// ============================================================================= - -/** - * Build a Solana transaction from a high-level intent. - * - * This function takes a declarative TransactionIntent and produces a Transaction - * object that can be inspected, signed, and serialized. - * - * The returned transaction is unsigned - signatures should be added via - * `addSignature()` before serializing with `toBytes()` and broadcasting. - * - * @param intent - The transaction intent describing what to build - * @returns A Transaction object that can be inspected, signed, and serialized - * @throws Error if the intent cannot be built (e.g., invalid addresses) - * - * @example - * ```typescript - * import { buildTransaction } from '@bitgo/wasm-solana'; - * - * // Build a simple SOL transfer - * const tx = buildTransaction({ - * feePayer: sender, - * nonce: { type: 'blockhash', value: blockhash }, - * instructions: [ - * { type: 'transfer', from: sender, to: recipient, lamports: 1000000n } - * ] - * }); - * - * // Inspect the transaction - * console.log(tx.feePayer); - * console.log(tx.recentBlockhash); - * - * // Get the signable payload for signing - * const payload = tx.signablePayload(); - * - * // Add signature and serialize - * tx.addSignature(signerPubkey, signature); - * const txBytes = tx.toBytes(); - * ``` - * - * @example - * ```typescript - * // Build with durable nonce and priority fee - * const tx = buildTransaction({ - * feePayer: sender, - * nonce: { type: 'durable', address: nonceAccount, authority: sender, value: nonceValue }, - * instructions: [ - * { type: 'computeBudget', unitLimit: 200000, unitPrice: 5000 }, - * { type: 'transfer', from: sender, to: recipient, lamports: 1000000n }, - * { type: 'memo', message: 'BitGo transfer' } - * ] - * }); - * ``` - */ -export function buildTransaction(intent: TransactionIntent): Transaction { - const wasm = BuilderNamespace.build_transaction(intent); - return Transaction.fromWasm(wasm); -} - -// ============================================================================= -// Raw Versioned Transaction Data Types (for fromVersionedTransactionData path) -// ============================================================================= - -/** - * A pre-compiled versioned instruction (uses indexes, not pubkeys). - * This is the format used in MessageV0 transactions. - */ -export interface VersionedInstruction { - /** Index into the account keys array for the program ID */ - programIdIndex: number; - /** Indexes into the account keys array for instruction accounts */ - accountKeyIndexes: number[]; - /** Instruction data (base58 encoded) */ - data: string; -} - -/** - * Message header for versioned transactions. - * Describes the structure of the account keys array. - */ -export interface MessageHeader { - /** Number of required signatures */ - numRequiredSignatures: number; - /** Number of readonly signed accounts */ - numReadonlySignedAccounts: number; - /** Number of readonly unsigned accounts */ - numReadonlyUnsignedAccounts: number; -} - -/** - * Raw versioned transaction data for direct serialization. - * This is used when we have pre-formed MessageV0 data that just needs to be serialized. - * No instruction compilation is needed - just serialize the raw structure. - */ -export interface RawVersionedTransactionData { - /** Static account keys (base58 encoded public keys) */ - staticAccountKeys: string[]; - /** Address lookup tables */ - addressLookupTables: AddressLookupTable[]; - /** Pre-compiled instructions with index-based account references */ - versionedInstructions: VersionedInstruction[]; - /** Message header */ - messageHeader: MessageHeader; - /** Recent blockhash (base58) */ - recentBlockhash: string; -} - -/** - * Build a versioned transaction directly from raw MessageV0 data. - * - * This function is used for the `fromVersionedTransactionData()` path where we already - * have pre-compiled versioned data (indexes + ALT refs). No instruction compilation - * is needed - we just serialize the raw structure. - * - * @param data - Raw versioned transaction data - * @returns A VersionedTransaction object that can be inspected, signed, and serialized - * @throws Error if the data is invalid - * - * @example - * ```typescript - * import { buildFromVersionedData } from '@bitgo/wasm-solana'; - * - * const tx = buildFromVersionedData({ - * staticAccountKeys: ['pubkey1', 'pubkey2', ...], - * addressLookupTables: [ - * { accountKey: 'altPubkey', writableIndexes: [0, 1], readonlyIndexes: [2] } - * ], - * versionedInstructions: [ - * { programIdIndex: 0, accountKeyIndexes: [1, 2], data: 'base58EncodedData' } - * ], - * messageHeader: { - * numRequiredSignatures: 1, - * numReadonlySignedAccounts: 0, - * numReadonlyUnsignedAccounts: 3 - * }, - * recentBlockhash: 'blockhash' - * }); - * - * // Inspect, sign, and serialize - * console.log(tx.feePayer); - * tx.addSignature(signerPubkey, signature); - * const txBytes = tx.toBytes(); - * ``` - */ -export function buildFromVersionedData(data: RawVersionedTransactionData): VersionedTransaction { - const wasm = BuilderNamespace.build_from_versioned_data(data); - return VersionedTransaction.fromWasm(wasm); -} diff --git a/packages/wasm-solana/js/index.ts b/packages/wasm-solana/js/index.ts index 13eec3d..d3d4bd6 100644 --- a/packages/wasm-solana/js/index.ts +++ b/packages/wasm-solana/js/index.ts @@ -8,7 +8,6 @@ export * as keypair from "./keypair.js"; export * as pubkey from "./pubkey.js"; export * as transaction from "./transaction.js"; export * as parser from "./parser.js"; -export * as builder from "./builder.js"; // Top-level class exports for convenience export { Keypair } from "./keypair.js"; @@ -21,7 +20,6 @@ export type { AddressLookupTableData } from "./versioned.js"; // Top-level function exports export { parseTransaction } from "./parser.js"; -export { buildTransaction, buildFromVersionedData } from "./builder.js"; export { buildFromIntent } from "./intentBuilder.js"; // Intent builder type exports @@ -93,45 +91,3 @@ export type { StakePoolWithdrawStakeParams, UnknownInstructionParams, } from "./parser.js"; - -// Builder type exports (prefixed to avoid conflict with parser/transaction types) -export type { - TransactionIntent, - NonceSource as BuilderNonceSource, - BlockhashNonceSource, - DurableNonceSource, - AddressLookupTable as BuilderAddressLookupTable, - Instruction as BuilderInstruction, - TransferInstruction, - CreateAccountInstruction, - NonceAdvanceInstruction, - NonceInitializeInstruction, - AllocateInstruction, - AssignInstruction, - MemoInstruction, - ComputeBudgetInstruction, - // Stake Program - StakeInitializeInstruction, - StakeDelegateInstruction, - StakeDeactivateInstruction, - StakeWithdrawInstruction, - StakeAuthorizeInstruction, - StakeSplitInstruction, - // SPL Token - TokenTransferInstruction, - CreateAssociatedTokenAccountInstruction, - CloseAssociatedTokenAccountInstruction, - MintToInstruction, - BurnInstruction, - ApproveInstruction, - // Jito Stake Pool - StakePoolDepositSolInstruction, - StakePoolWithdrawStakeInstruction, - // Custom Instruction - CustomInstruction as BuilderCustomInstruction, - CustomAccountMeta, - // Raw Versioned Transaction Data (for fromVersionedTransactionData path) - RawVersionedTransactionData, - VersionedInstruction as BuilderVersionedInstruction, - MessageHeader, -} from "./builder.js"; diff --git a/packages/wasm-solana/js/versioned.ts b/packages/wasm-solana/js/versioned.ts index 1df22a2..a09a14b 100644 --- a/packages/wasm-solana/js/versioned.ts +++ b/packages/wasm-solana/js/versioned.ts @@ -6,12 +6,7 @@ * transaction size by referencing accounts via lookup table indices. */ -import { - WasmVersionedTransaction, - is_versioned_transaction, - BuilderNamespace, -} from "./wasm/wasm_solana.js"; -import type { RawVersionedTransactionData } from "./builder.js"; +import { WasmVersionedTransaction, is_versioned_transaction } from "./wasm/wasm_solana.js"; /** * Address Lookup Table data extracted from versioned transactions. @@ -98,41 +93,6 @@ export class VersionedTransaction { return new VersionedTransaction(wasm); } - /** - * Create a versioned transaction from raw MessageV0 data. - * - * This is used for the `fromVersionedTransactionData()` path where we have - * pre-compiled versioned data (indexes + ALT refs). No instruction compilation - * is needed - this just constructs the transaction from the raw structure. - * - * @param data - Raw versioned transaction data - * @returns A VersionedTransaction instance - * - * @example - * ```typescript - * const tx = VersionedTransaction.fromVersionedData({ - * staticAccountKeys: ['pubkey1', 'pubkey2', ...], - * addressLookupTables: [ - * { accountKey: 'altPubkey', writableIndexes: [0, 1], readonlyIndexes: [2] } - * ], - * versionedInstructions: [ - * { programIdIndex: 0, accountKeyIndexes: [1, 2], data: 'base58EncodedData' } - * ], - * messageHeader: { - * numRequiredSignatures: 1, - * numReadonlySignedAccounts: 0, - * numReadonlyUnsignedAccounts: 3 - * }, - * recentBlockhash: 'blockhash' - * }); - * ``` - */ - static fromVersionedData(data: RawVersionedTransactionData): VersionedTransaction { - // Build the transaction using WASM and wrap in TypeScript class - const wasm = BuilderNamespace.build_from_versioned_data(data); - return VersionedTransaction.fromWasm(wasm); - } - /** * Check if this is a versioned transaction (MessageV0). */ diff --git a/packages/wasm-solana/src/builder/build.rs b/packages/wasm-solana/src/builder/build.rs deleted file mode 100644 index 9bc8c70..0000000 --- a/packages/wasm-solana/src/builder/build.rs +++ /dev/null @@ -1,1588 +0,0 @@ -//! Transaction building implementation. -//! -//! Uses the Solana SDK for transaction construction and serialization. - -use crate::error::WasmSolanaError; - -use super::types::{Instruction as IntentInstruction, Nonce, TransactionIntent}; - -// Use SDK types for building (3.x ecosystem) -use solana_compute_budget_interface::ComputeBudgetInstruction; -use solana_sdk::hash::Hash; -use solana_sdk::instruction::{AccountMeta, Instruction}; -use solana_sdk::message::Message; -use solana_sdk::pubkey::Pubkey; -use solana_sdk::sysvar::clock as clock_sysvar; -use solana_sdk::transaction::Transaction; -// Use stake instruction helpers from the crate (handles sysvars internally) -use solana_stake_interface::instruction as stake_ix; -use solana_stake_interface::state::{Authorized, Lockup, StakeAuthorize}; -use solana_system_interface::instruction::{self as system_ix, SystemInstruction}; -use spl_stake_pool::instruction::StakePoolInstruction; -// SPL Token instruction encoding - use the crate for data packing to avoid manual byte construction -use spl_token::instruction::TokenInstruction; - -/// Well-known program IDs. -/// -/// Note: Solana ecosystem is split between SDK 2.x (solana_program) and SDK 3.x (solana_sdk): -/// - SDK 3.x compatible crates export IDs we can use directly (e.g., solana_stake_interface::program::ID) -/// - SPL crates (spl-token, spl-memo, spl-associated-token-account) use solana_program (2.x) types -/// which are incompatible with our solana_sdk (3.x) types at compile time. -/// -/// These program IDs are string-parsed because the SPL crates' ID constants return -/// `solana_program::pubkey::Pubkey`, not `solana_sdk::pubkey::Pubkey`. While the bytes are -/// identical, Rust's type system prevents direct usage across the SDK version boundary. -/// -/// The values here match the SPL crate declare_id! macros: -/// - spl_memo: "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr" -/// - spl_associated_token_account: "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" -/// - spl_token: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" -/// - spl_stake_pool: "SPoo1Ku8WFXoNDMHPsrGSTSG1Y47rzgn41SLUNakuHy" -mod program_ids { - use super::Pubkey; - - /// SPL Memo Program v2. - /// https://github.com/solana-program/memo/blob/main/interface/src/lib.rs#L15 - pub fn memo_program() -> Pubkey { - "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr" - .parse() - .unwrap() - } - - /// Associated Token Account Program. - /// https://github.com/solana-program/associated-token-account/blob/main/interface/src/lib.rs#L10 - pub fn ata_program() -> Pubkey { - "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" - .parse() - .unwrap() - } - - /// Native System Program. - /// https://docs.solanalabs.com/runtime/programs#system-program - /// Used for ATA creation which requires system program in accounts. - pub fn system_program() -> Pubkey { - "11111111111111111111111111111111".parse().unwrap() - } - - /// SPL Token Program. - /// https://github.com/solana-program/token/blob/main/interface/src/lib.rs#L17 - /// Used for stake pool operations that need token program in accounts. - pub fn token_program() -> Pubkey { - "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" - .parse() - .unwrap() - } - - /// SPL Stake Pool Program. - /// https://github.com/solana-program/stake-pool/blob/main/program/src/lib.rs#L11 - /// Note: spl_stake_pool::id() exists but returns solana_program::pubkey::Pubkey (2.x types), - /// which is incompatible with solana_sdk::pubkey::Pubkey (3.x types). - pub fn stake_pool_program() -> Pubkey { - "SPoo1Ku8WFXoNDMHPsrGSTSG1Y47rzgn41SLUNakuHy" - .parse() - .unwrap() - } -} - -/// Build a transaction from an intent structure. -/// -/// Returns the serialized unsigned transaction (wire format). -/// -/// # Transaction Types -/// -/// - If `intent.address_lookup_tables` is set, builds a versioned transaction (MessageV0) -/// - Otherwise, builds a legacy transaction -pub fn build_transaction(intent: TransactionIntent) -> Result, WasmSolanaError> { - // Check if this should be a versioned transaction - if super::versioned::should_build_versioned(&intent) { - return build_versioned_transaction(intent); - } - - // Legacy transaction building - build_legacy_transaction(intent) -} - -/// Build a versioned transaction (MessageV0) with Address Lookup Tables. -fn build_versioned_transaction(intent: TransactionIntent) -> Result, WasmSolanaError> { - // Build instructions first (same as legacy) - let mut instructions: Vec = Vec::new(); - - // Handle nonce - if let Nonce::Durable { - address, authority, .. - } = &intent.nonce - { - let nonce_pubkey: Pubkey = address - .parse() - .map_err(|_| WasmSolanaError::new(&format!("Invalid nonce.address: {}", address)))?; - let authority_pubkey: Pubkey = authority.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid nonce.authority: {}", authority)) - })?; - instructions.push(solana_system_interface::instruction::advance_nonce_account( - &nonce_pubkey, - &authority_pubkey, - )); - } - - // Build each instruction - for ix in intent.instructions.clone() { - instructions.push(build_instruction(ix)?); - } - - // Delegate to versioned module - super::versioned::build_versioned_transaction(&intent, instructions) -} - -/// Build a legacy transaction (original format). -fn build_legacy_transaction(intent: TransactionIntent) -> Result, WasmSolanaError> { - // Parse fee payer - let fee_payer: Pubkey = intent - .fee_payer - .parse() - .map_err(|_| WasmSolanaError::new(&format!("Invalid fee_payer: {}", intent.fee_payer)))?; - - // Build all instructions - let mut instructions: Vec = Vec::new(); - - // Handle nonce - either blockhash or durable nonce - let blockhash_str = match &intent.nonce { - Nonce::Blockhash { value } => value.clone(), - Nonce::Durable { - address, - authority, - value, - } => { - // For durable nonce, prepend the nonce advance instruction - let nonce_pubkey: Pubkey = address.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid nonce.address: {}", address)) - })?; - let authority_pubkey: Pubkey = authority.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid nonce.authority: {}", authority)) - })?; - instructions.push(system_ix::advance_nonce_account( - &nonce_pubkey, - &authority_pubkey, - )); - - // The blockhash is the nonce value stored in the nonce account - value.clone() - } - }; - - // Parse blockhash - let blockhash: Hash = blockhash_str - .parse() - .map_err(|_| WasmSolanaError::new(&format!("Invalid blockhash: {}", blockhash_str)))?; - - // Build each instruction - for ix in intent.instructions { - instructions.push(build_instruction(ix)?); - } - - // Create message using SDK (handles account ordering correctly) - let message = Message::new_with_blockhash(&instructions, Some(&fee_payer), &blockhash); - - // Create unsigned transaction - let mut tx = Transaction::new_unsigned(message); - tx.message.recent_blockhash = blockhash; - - // Serialize using bincode (standard Solana serialization) - let tx_bytes = - bincode::serialize(&tx).map_err(|e| WasmSolanaError::new(&format!("Serialize: {}", e)))?; - - Ok(tx_bytes) -} - -/// Build a single instruction from the IntentInstruction enum. -fn build_instruction(ix: IntentInstruction) -> Result { - match ix { - // ===== System Program ===== - IntentInstruction::Transfer { from, to, lamports } => { - let from_pubkey: Pubkey = from - .parse() - .map_err(|_| WasmSolanaError::new(&format!("Invalid transfer.from: {}", from)))?; - let to_pubkey: Pubkey = to - .parse() - .map_err(|_| WasmSolanaError::new(&format!("Invalid transfer.to: {}", to)))?; - Ok(system_ix::transfer(&from_pubkey, &to_pubkey, lamports)) - } - - IntentInstruction::CreateAccount { - from, - new_account, - lamports, - space, - owner, - } => { - let from_pubkey: Pubkey = from.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid createAccount.from: {}", from)) - })?; - let new_pubkey: Pubkey = new_account.parse().map_err(|_| { - WasmSolanaError::new(&format!( - "Invalid createAccount.newAccount: {}", - new_account - )) - })?; - let owner_pubkey: Pubkey = owner.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid createAccount.owner: {}", owner)) - })?; - Ok(system_ix::create_account( - &from_pubkey, - &new_pubkey, - lamports, - space, - &owner_pubkey, - )) - } - - IntentInstruction::NonceAdvance { nonce, authority } => { - let nonce_pubkey: Pubkey = nonce.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid nonceAdvance.nonce: {}", nonce)) - })?; - let authority_pubkey: Pubkey = authority.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid nonceAdvance.authority: {}", authority)) - })?; - Ok(system_ix::advance_nonce_account( - &nonce_pubkey, - &authority_pubkey, - )) - } - - IntentInstruction::NonceInitialize { nonce, authority } => { - // Note: In SDK 3.x, nonce initialization is combined with creation. - // This creates an InitializeNonceAccount instruction manually. - let nonce_pubkey: Pubkey = nonce.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid nonceInitialize.nonce: {}", nonce)) - })?; - let authority_pubkey: Pubkey = authority.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid nonceInitialize.authority: {}", authority)) - })?; - Ok(build_nonce_initialize(&nonce_pubkey, &authority_pubkey)) - } - - IntentInstruction::Allocate { account, space } => { - let account_pubkey: Pubkey = account.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid allocate.account: {}", account)) - })?; - Ok(system_ix::allocate(&account_pubkey, space)) - } - - IntentInstruction::Assign { account, owner } => { - let account_pubkey: Pubkey = account.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid assign.account: {}", account)) - })?; - let owner_pubkey: Pubkey = owner - .parse() - .map_err(|_| WasmSolanaError::new(&format!("Invalid assign.owner: {}", owner)))?; - Ok(system_ix::assign(&account_pubkey, &owner_pubkey)) - } - - // ===== Memo Program ===== - IntentInstruction::Memo { message } => Ok(build_memo(&message)), - - // ===== Compute Budget Program ===== - IntentInstruction::ComputeBudget { - unit_limit, - unit_price, - } => { - // Return a single instruction - prefer unit_price if both specified - // Use SDK's ComputeBudgetInstruction 3.x methods (compatible with solana-sdk 3.x) - if let Some(price) = unit_price { - Ok(ComputeBudgetInstruction::set_compute_unit_price(price)) - } else if let Some(limit) = unit_limit { - Ok(ComputeBudgetInstruction::set_compute_unit_limit(limit)) - } else { - Err(WasmSolanaError::new( - "ComputeBudget instruction requires either unitLimit or unitPrice", - )) - } - } - - // ===== Stake Program ===== - IntentInstruction::StakeInitialize { - stake, - staker, - withdrawer, - } => { - let stake_pubkey: Pubkey = stake.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid stakeInitialize.stake: {}", stake)) - })?; - let staker_pubkey: Pubkey = staker.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid stakeInitialize.staker: {}", staker)) - })?; - let withdrawer_pubkey: Pubkey = withdrawer.parse().map_err(|_| { - WasmSolanaError::new(&format!( - "Invalid stakeInitialize.withdrawer: {}", - withdrawer - )) - })?; - Ok(build_stake_initialize( - &stake_pubkey, - &Authorized { - staker: staker_pubkey, - withdrawer: withdrawer_pubkey, - }, - )) - } - - IntentInstruction::StakeDelegate { - stake, - vote, - authority, - } => { - let stake_pubkey: Pubkey = stake.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid stakeDelegate.stake: {}", stake)) - })?; - let vote_pubkey: Pubkey = vote.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid stakeDelegate.vote: {}", vote)) - })?; - let authority_pubkey: Pubkey = authority.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid stakeDelegate.authority: {}", authority)) - })?; - Ok(build_stake_delegate( - &stake_pubkey, - &vote_pubkey, - &authority_pubkey, - )) - } - - IntentInstruction::StakeDeactivate { stake, authority } => { - let stake_pubkey: Pubkey = stake.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid stakeDeactivate.stake: {}", stake)) - })?; - let authority_pubkey: Pubkey = authority.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid stakeDeactivate.authority: {}", authority)) - })?; - Ok(build_stake_deactivate(&stake_pubkey, &authority_pubkey)) - } - - IntentInstruction::StakeWithdraw { - stake, - recipient, - lamports, - authority, - } => { - let stake_pubkey: Pubkey = stake.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid stakeWithdraw.stake: {}", stake)) - })?; - let recipient_pubkey: Pubkey = recipient.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid stakeWithdraw.recipient: {}", recipient)) - })?; - let authority_pubkey: Pubkey = authority.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid stakeWithdraw.authority: {}", authority)) - })?; - Ok(build_stake_withdraw( - &stake_pubkey, - &recipient_pubkey, - lamports, - &authority_pubkey, - )) - } - - IntentInstruction::StakeAuthorize { - stake, - new_authority, - authorize_type, - authority, - } => { - let stake_pubkey: Pubkey = stake.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid stakeAuthorize.stake: {}", stake)) - })?; - let new_authority_pubkey: Pubkey = new_authority.parse().map_err(|_| { - WasmSolanaError::new(&format!( - "Invalid stakeAuthorize.newAuthority: {}", - new_authority - )) - })?; - let authority_pubkey: Pubkey = authority.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid stakeAuthorize.authority: {}", authority)) - })?; - let stake_authorize = match authorize_type.to_lowercase().as_str() { - "staker" => StakeAuthorize::Staker, - "withdrawer" => StakeAuthorize::Withdrawer, - _ => { - return Err(WasmSolanaError::new(&format!( - "Invalid stakeAuthorize.authorizeType: {} (expected 'staker' or 'withdrawer')", - authorize_type - ))) - } - }; - Ok(build_stake_authorize( - &stake_pubkey, - &authority_pubkey, - &new_authority_pubkey, - stake_authorize, - )) - } - - IntentInstruction::StakeSplit { - stake, - split_stake, - authority, - lamports, - } => { - let stake_pubkey: Pubkey = stake.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid stakeSplit.stake: {}", stake)) - })?; - let split_stake_pubkey: Pubkey = split_stake.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid stakeSplit.splitStake: {}", split_stake)) - })?; - let authority_pubkey: Pubkey = authority.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid stakeSplit.authority: {}", authority)) - })?; - Ok(build_stake_split( - &stake_pubkey, - &split_stake_pubkey, - &authority_pubkey, - lamports, - )) - } - - // ===== SPL Token Program ===== - IntentInstruction::TokenTransfer { - source, - destination, - mint, - amount, - decimals, - authority, - program_id, - } => { - let source_pubkey: Pubkey = source.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid tokenTransfer.source: {}", source)) - })?; - let destination_pubkey: Pubkey = destination.parse().map_err(|_| { - WasmSolanaError::new(&format!( - "Invalid tokenTransfer.destination: {}", - destination - )) - })?; - let mint_pubkey: Pubkey = mint.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid tokenTransfer.mint: {}", mint)) - })?; - let authority_pubkey: Pubkey = authority.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid tokenTransfer.authority: {}", authority)) - })?; - let token_program: Pubkey = program_id.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid tokenTransfer.programId: {}", program_id)) - })?; - Ok(build_token_transfer_checked( - &source_pubkey, - &mint_pubkey, - &destination_pubkey, - &authority_pubkey, - amount, - decimals, - &token_program, - )) - } - - IntentInstruction::CreateAssociatedTokenAccount { - payer, - owner, - mint, - token_program_id, - } => { - let payer_pubkey: Pubkey = payer.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid createAta.payer: {}", payer)) - })?; - let owner_pubkey: Pubkey = owner.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid createAta.owner: {}", owner)) - })?; - let mint_pubkey: Pubkey = mint - .parse() - .map_err(|_| WasmSolanaError::new(&format!("Invalid createAta.mint: {}", mint)))?; - let token_program: Pubkey = token_program_id.parse().map_err(|_| { - WasmSolanaError::new(&format!( - "Invalid createAta.tokenProgramId: {}", - token_program_id - )) - })?; - Ok(build_create_ata( - &payer_pubkey, - &owner_pubkey, - &mint_pubkey, - &token_program, - )) - } - - IntentInstruction::CloseAssociatedTokenAccount { - account, - destination, - authority, - program_id, - } => { - let account_pubkey: Pubkey = account.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid closeAta.account: {}", account)) - })?; - let destination_pubkey: Pubkey = destination.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid closeAta.destination: {}", destination)) - })?; - let authority_pubkey: Pubkey = authority.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid closeAta.authority: {}", authority)) - })?; - let token_program: Pubkey = program_id.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid closeAta.programId: {}", program_id)) - })?; - Ok(build_close_account( - &account_pubkey, - &destination_pubkey, - &authority_pubkey, - &token_program, - )) - } - - IntentInstruction::MintTo { - mint, - destination, - authority, - amount, - program_id, - } => { - let mint_pubkey: Pubkey = mint - .parse() - .map_err(|_| WasmSolanaError::new(&format!("Invalid mintTo.mint: {}", mint)))?; - let destination_pubkey: Pubkey = destination.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid mintTo.destination: {}", destination)) - })?; - let authority_pubkey: Pubkey = authority.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid mintTo.authority: {}", authority)) - })?; - let token_program: Pubkey = program_id.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid mintTo.programId: {}", program_id)) - })?; - Ok(build_mint_to( - &mint_pubkey, - &destination_pubkey, - &authority_pubkey, - amount, - &token_program, - )) - } - - IntentInstruction::Burn { - mint, - account, - authority, - amount, - program_id, - } => { - let mint_pubkey: Pubkey = mint - .parse() - .map_err(|_| WasmSolanaError::new(&format!("Invalid burn.mint: {}", mint)))?; - let account_pubkey: Pubkey = account - .parse() - .map_err(|_| WasmSolanaError::new(&format!("Invalid burn.account: {}", account)))?; - let authority_pubkey: Pubkey = authority.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid burn.authority: {}", authority)) - })?; - let token_program: Pubkey = program_id.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid burn.programId: {}", program_id)) - })?; - Ok(build_burn( - &account_pubkey, - &mint_pubkey, - &authority_pubkey, - amount, - &token_program, - )) - } - - IntentInstruction::Approve { - account, - delegate, - owner, - amount, - program_id, - } => { - let account_pubkey: Pubkey = account.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid approve.account: {}", account)) - })?; - let delegate_pubkey: Pubkey = delegate.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid approve.delegate: {}", delegate)) - })?; - let owner_pubkey: Pubkey = owner - .parse() - .map_err(|_| WasmSolanaError::new(&format!("Invalid approve.owner: {}", owner)))?; - let token_program: Pubkey = program_id.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid approve.programId: {}", program_id)) - })?; - Ok(build_approve( - &account_pubkey, - &delegate_pubkey, - &owner_pubkey, - amount, - &token_program, - )) - } - - // ===== Jito Stake Pool ===== - IntentInstruction::StakePoolDepositSol { - stake_pool, - withdraw_authority, - reserve_stake, - funding_account, - destination_pool_account, - manager_fee_account, - referral_pool_account, - pool_mint, - lamports, - } => { - let stake_pool_pubkey: Pubkey = stake_pool.parse().map_err(|_| { - WasmSolanaError::new(&format!( - "Invalid stakePoolDepositSol.stakePool: {}", - stake_pool - )) - })?; - let withdraw_authority_pubkey: Pubkey = withdraw_authority.parse().map_err(|_| { - WasmSolanaError::new(&format!( - "Invalid stakePoolDepositSol.withdrawAuthority: {}", - withdraw_authority - )) - })?; - let reserve_stake_pubkey: Pubkey = reserve_stake.parse().map_err(|_| { - WasmSolanaError::new(&format!( - "Invalid stakePoolDepositSol.reserveStake: {}", - reserve_stake - )) - })?; - let funding_account_pubkey: Pubkey = funding_account.parse().map_err(|_| { - WasmSolanaError::new(&format!( - "Invalid stakePoolDepositSol.fundingAccount: {}", - funding_account - )) - })?; - let destination_pool_account_pubkey: Pubkey = - destination_pool_account.parse().map_err(|_| { - WasmSolanaError::new(&format!( - "Invalid stakePoolDepositSol.destinationPoolAccount: {}", - destination_pool_account - )) - })?; - let manager_fee_account_pubkey: Pubkey = manager_fee_account.parse().map_err(|_| { - WasmSolanaError::new(&format!( - "Invalid stakePoolDepositSol.managerFeeAccount: {}", - manager_fee_account - )) - })?; - let referral_pool_account_pubkey: Pubkey = - referral_pool_account.parse().map_err(|_| { - WasmSolanaError::new(&format!( - "Invalid stakePoolDepositSol.referralPoolAccount: {}", - referral_pool_account - )) - })?; - let pool_mint_pubkey: Pubkey = pool_mint.parse().map_err(|_| { - WasmSolanaError::new(&format!( - "Invalid stakePoolDepositSol.poolMint: {}", - pool_mint - )) - })?; - Ok(build_stake_pool_deposit_sol( - &stake_pool_pubkey, - &withdraw_authority_pubkey, - &reserve_stake_pubkey, - &funding_account_pubkey, - &destination_pool_account_pubkey, - &manager_fee_account_pubkey, - &referral_pool_account_pubkey, - &pool_mint_pubkey, - lamports, - )) - } - - IntentInstruction::StakePoolWithdrawStake { - stake_pool, - validator_list, - withdraw_authority, - validator_stake, - destination_stake, - destination_stake_authority, - source_transfer_authority, - source_pool_account, - manager_fee_account, - pool_mint, - pool_tokens, - } => { - let stake_pool_pubkey: Pubkey = stake_pool.parse().map_err(|_| { - WasmSolanaError::new(&format!( - "Invalid stakePoolWithdrawStake.stakePool: {}", - stake_pool - )) - })?; - let validator_list_pubkey: Pubkey = validator_list.parse().map_err(|_| { - WasmSolanaError::new(&format!( - "Invalid stakePoolWithdrawStake.validatorList: {}", - validator_list - )) - })?; - let withdraw_authority_pubkey: Pubkey = withdraw_authority.parse().map_err(|_| { - WasmSolanaError::new(&format!( - "Invalid stakePoolWithdrawStake.withdrawAuthority: {}", - withdraw_authority - )) - })?; - let validator_stake_pubkey: Pubkey = validator_stake.parse().map_err(|_| { - WasmSolanaError::new(&format!( - "Invalid stakePoolWithdrawStake.validatorStake: {}", - validator_stake - )) - })?; - let destination_stake_pubkey: Pubkey = destination_stake.parse().map_err(|_| { - WasmSolanaError::new(&format!( - "Invalid stakePoolWithdrawStake.destinationStake: {}", - destination_stake - )) - })?; - let destination_stake_authority_pubkey: Pubkey = - destination_stake_authority.parse().map_err(|_| { - WasmSolanaError::new(&format!( - "Invalid stakePoolWithdrawStake.destinationStakeAuthority: {}", - destination_stake_authority - )) - })?; - let source_transfer_authority_pubkey: Pubkey = - source_transfer_authority.parse().map_err(|_| { - WasmSolanaError::new(&format!( - "Invalid stakePoolWithdrawStake.sourceTransferAuthority: {}", - source_transfer_authority - )) - })?; - let source_pool_account_pubkey: Pubkey = source_pool_account.parse().map_err(|_| { - WasmSolanaError::new(&format!( - "Invalid stakePoolWithdrawStake.sourcePoolAccount: {}", - source_pool_account - )) - })?; - let manager_fee_account_pubkey: Pubkey = manager_fee_account.parse().map_err(|_| { - WasmSolanaError::new(&format!( - "Invalid stakePoolWithdrawStake.managerFeeAccount: {}", - manager_fee_account - )) - })?; - let pool_mint_pubkey: Pubkey = pool_mint.parse().map_err(|_| { - WasmSolanaError::new(&format!( - "Invalid stakePoolWithdrawStake.poolMint: {}", - pool_mint - )) - })?; - - Ok(build_stake_pool_withdraw_stake( - &stake_pool_pubkey, - &validator_list_pubkey, - &withdraw_authority_pubkey, - &validator_stake_pubkey, - &destination_stake_pubkey, - &destination_stake_authority_pubkey, - &source_transfer_authority_pubkey, - &source_pool_account_pubkey, - &manager_fee_account_pubkey, - &pool_mint_pubkey, - pool_tokens, - )) - } - - // ===== Custom/Raw Instruction ===== - IntentInstruction::Custom { - program_id, - accounts, - data, - encoding, - } => { - let program_pubkey: Pubkey = program_id.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid custom.programId: {}", program_id)) - })?; - - // Decode the data based on encoding - let data_bytes = match encoding.as_str() { - "hex" => hex::decode(&data).map_err(|e| { - WasmSolanaError::new(&format!("Invalid hex data in custom instruction: {}", e)) - })?, - _ => { - use base64::Engine; - base64::engine::general_purpose::STANDARD - .decode(&data) - .map_err(|e| { - WasmSolanaError::new(&format!( - "Invalid base64 data in custom instruction: {}", - e - )) - })? - } - }; - - // Parse account metas - let account_metas: Vec = accounts - .into_iter() - .map(|acc| { - let pubkey: Pubkey = acc.pubkey.parse().map_err(|_| { - WasmSolanaError::new(&format!("Invalid account pubkey: {}", acc.pubkey)) - })?; - Ok(if acc.is_writable { - AccountMeta::new(pubkey, acc.is_signer) - } else { - AccountMeta::new_readonly(pubkey, acc.is_signer) - }) - }) - .collect::, WasmSolanaError>>()?; - - Ok(Instruction::new_with_bytes( - program_pubkey, - &data_bytes, - account_metas, - )) - } - } -} - -// ===== Nonce Instruction Builders ===== - -/// Build an InitializeNonceAccount instruction using the SDK's SystemInstruction enum. -/// SDK 3.x `create_nonce_account` combines create + initialize; we extract just initialize. -fn build_nonce_initialize(nonce: &Pubkey, authority: &Pubkey) -> Instruction { - // System program ID - let system_program_id: Pubkey = "11111111111111111111111111111111".parse().unwrap(); - - // Sysvars (same addresses as used by SDK) - let recent_blockhashes_sysvar: Pubkey = "SysvarRecentB1ockHashes11111111111111111111" - .parse() - .unwrap(); - let rent_sysvar: Pubkey = "SysvarRent111111111111111111111111111111111" - .parse() - .unwrap(); - - // Use SDK's SystemInstruction enum with bincode serialization (same as SDK does) - Instruction::new_with_bincode( - system_program_id, - &SystemInstruction::InitializeNonceAccount(*authority), - vec![ - AccountMeta::new(*nonce, false), // nonce account: writable - AccountMeta::new_readonly(recent_blockhashes_sysvar, false), // RecentBlockhashes sysvar - AccountMeta::new_readonly(rent_sysvar, false), // Rent sysvar - ], - ) -} - -// ===== Other Instruction Builders ===== - -/// Build a memo instruction. -fn build_memo(message: &str) -> Instruction { - Instruction::new_with_bytes(program_ids::memo_program(), message.as_bytes(), vec![]) -} - -// ===== Stake Instruction Builders ===== -// These use solana_stake_interface helpers which handle sysvars internally. - -/// Build a stake initialize instruction. -/// Uses solana_stake_interface::instruction::initialize which handles rent sysvar. -fn build_stake_initialize(stake: &Pubkey, authorized: &Authorized) -> Instruction { - stake_ix::initialize(stake, authorized, &Lockup::default()) -} - -/// Build a stake delegate instruction. -/// Uses solana_stake_interface::instruction::delegate_stake which handles -/// clock, stake_history, and stake_config sysvars internally. -fn build_stake_delegate(stake: &Pubkey, vote: &Pubkey, authority: &Pubkey) -> Instruction { - stake_ix::delegate_stake(stake, authority, vote) -} - -/// Build a stake deactivate instruction. -/// Uses solana_stake_interface::instruction::deactivate_stake which handles clock sysvar. -fn build_stake_deactivate(stake: &Pubkey, authority: &Pubkey) -> Instruction { - stake_ix::deactivate_stake(stake, authority) -} - -/// Build a stake withdraw instruction. -/// Uses solana_stake_interface::instruction::withdraw which handles -/// clock and stake_history sysvars internally. -fn build_stake_withdraw( - stake: &Pubkey, - recipient: &Pubkey, - lamports: u64, - authority: &Pubkey, -) -> Instruction { - stake_ix::withdraw(stake, authority, recipient, lamports, None) -} - -/// Build a stake authorize instruction. -/// Uses solana_stake_interface::instruction::authorize which handles clock sysvar. -fn build_stake_authorize( - stake: &Pubkey, - authority: &Pubkey, - new_authority: &Pubkey, - stake_authorize: StakeAuthorize, -) -> Instruction { - stake_ix::authorize(stake, authority, new_authority, stake_authorize, None) -} - -/// Build a stake split instruction. -/// Note: We build this manually because stake_ix::split returns Vec -/// (including account creation), but our interface expects a single instruction. -/// Callers should ensure the split_stake account is already created. -fn build_stake_split( - stake: &Pubkey, - split_stake: &Pubkey, - authority: &Pubkey, - lamports: u64, -) -> Instruction { - use solana_stake_interface::instruction::StakeInstruction; - - Instruction::new_with_bincode( - solana_stake_interface::program::ID, - &StakeInstruction::Split(lamports), - vec![ - AccountMeta::new(*stake, false), // source stake account - AccountMeta::new(*split_stake, false), // destination stake account - AccountMeta::new_readonly(*authority, true), // stake authority (signer) - ], - ) -} - -// ===== SPL Token Instruction Builders ===== -// These use spl_token::instruction::TokenInstruction for data encoding to avoid manual byte construction. -// This ensures we stay in sync with any changes to the SPL Token program instruction format. - -/// Build a TransferChecked instruction for SPL Token. -/// TransferChecked is safer than Transfer as it verifies decimals. -fn build_token_transfer_checked( - source: &Pubkey, - mint: &Pubkey, - destination: &Pubkey, - authority: &Pubkey, - amount: u64, - decimals: u8, - token_program: &Pubkey, -) -> Instruction { - // Use SPL Token crate for instruction data encoding - let data = TokenInstruction::TransferChecked { amount, decimals }.pack(); - - Instruction::new_with_bytes( - *token_program, - &data, - vec![ - AccountMeta::new(*source, false), // source token account - AccountMeta::new_readonly(*mint, false), // mint - AccountMeta::new(*destination, false), // destination token account - AccountMeta::new_readonly(*authority, true), // owner/authority (signer) - ], - ) -} - -/// Build a CreateAssociatedTokenAccount instruction. -fn build_create_ata( - payer: &Pubkey, - owner: &Pubkey, - mint: &Pubkey, - token_program: &Pubkey, -) -> Instruction { - // Derive the ATA address - let ata = get_associated_token_address(owner, mint, token_program); - - // ATA program create instruction has no data (or discriminator 0) - Instruction::new_with_bytes( - program_ids::ata_program(), - &[], - vec![ - AccountMeta::new(*payer, true), // payer (signer) - AccountMeta::new(ata, false), // associated token account - AccountMeta::new_readonly(*owner, false), // wallet owner - AccountMeta::new_readonly(*mint, false), // token mint - AccountMeta::new_readonly(program_ids::system_program(), false), // system program - AccountMeta::new_readonly(*token_program, false), // token program - ], - ) -} - -/// Build a CloseAccount instruction for SPL Token. -fn build_close_account( - account: &Pubkey, - destination: &Pubkey, - authority: &Pubkey, - token_program: &Pubkey, -) -> Instruction { - // Use SPL Token crate for instruction data encoding - let data = TokenInstruction::CloseAccount.pack(); - - Instruction::new_with_bytes( - *token_program, - &data, - vec![ - AccountMeta::new(*account, false), // account to close - AccountMeta::new(*destination, false), // destination for lamports - AccountMeta::new_readonly(*authority, true), // owner/authority (signer) - ], - ) -} - -/// Derive the Associated Token Account address. -fn get_associated_token_address(owner: &Pubkey, mint: &Pubkey, token_program: &Pubkey) -> Pubkey { - // ATA is a PDA with seeds: [owner, token_program, mint] - let seeds = &[owner.as_ref(), token_program.as_ref(), mint.as_ref()]; - let (ata, _bump) = Pubkey::find_program_address(seeds, &program_ids::ata_program()); - ata -} - -/// Build a MintTo instruction for SPL Token. -fn build_mint_to( - mint: &Pubkey, - destination: &Pubkey, - authority: &Pubkey, - amount: u64, - token_program: &Pubkey, -) -> Instruction { - // Use SPL Token crate for instruction data encoding - let data = TokenInstruction::MintTo { amount }.pack(); - - Instruction::new_with_bytes( - *token_program, - &data, - vec![ - AccountMeta::new(*mint, false), // mint - AccountMeta::new(*destination, false), // destination token account - AccountMeta::new_readonly(*authority, true), // mint authority (signer) - ], - ) -} - -/// Build a Burn instruction for SPL Token. -fn build_burn( - account: &Pubkey, - mint: &Pubkey, - authority: &Pubkey, - amount: u64, - token_program: &Pubkey, -) -> Instruction { - // Use SPL Token crate for instruction data encoding - let data = TokenInstruction::Burn { amount }.pack(); - - Instruction::new_with_bytes( - *token_program, - &data, - vec![ - AccountMeta::new(*account, false), // source token account - AccountMeta::new(*mint, false), // mint - AccountMeta::new_readonly(*authority, true), // owner/authority (signer) - ], - ) -} - -/// Build an Approve instruction for SPL Token. -fn build_approve( - account: &Pubkey, - delegate: &Pubkey, - owner: &Pubkey, - amount: u64, - token_program: &Pubkey, -) -> Instruction { - // Use SPL Token crate for instruction data encoding - let data = TokenInstruction::Approve { amount }.pack(); - - Instruction::new_with_bytes( - *token_program, - &data, - vec![ - AccountMeta::new(*account, false), // token account - AccountMeta::new_readonly(*delegate, false), // delegate - AccountMeta::new_readonly(*owner, true), // owner (signer) - ], - ) -} - -// ===== Jito Stake Pool Instruction Builders ===== - -/// Build a DepositSol instruction for SPL Stake Pool (Jito). -#[allow(clippy::too_many_arguments)] -fn build_stake_pool_deposit_sol( - stake_pool: &Pubkey, - withdraw_authority: &Pubkey, - reserve_stake: &Pubkey, - funding_account: &Pubkey, - destination_pool_account: &Pubkey, - manager_fee_account: &Pubkey, - referral_pool_account: &Pubkey, - pool_mint: &Pubkey, - lamports: u64, -) -> Instruction { - use borsh::BorshSerialize; - - // DepositSol instruction data using spl-stake-pool - let instruction_data = StakePoolInstruction::DepositSol(lamports); - let mut data = Vec::new(); - instruction_data.serialize(&mut data).unwrap(); - - Instruction::new_with_bytes( - program_ids::stake_pool_program(), - &data, - vec![ - AccountMeta::new(*stake_pool, false), - AccountMeta::new_readonly(*withdraw_authority, false), - AccountMeta::new(*reserve_stake, false), - AccountMeta::new(*funding_account, true), // signer - AccountMeta::new(*destination_pool_account, false), - AccountMeta::new(*manager_fee_account, false), - AccountMeta::new(*referral_pool_account, false), - AccountMeta::new(*pool_mint, false), - AccountMeta::new_readonly(program_ids::system_program(), false), - AccountMeta::new_readonly(program_ids::token_program(), false), - ], - ) -} - -/// Build a WithdrawStake instruction for SPL Stake Pool (Jito). -/// Uses solana_stake_interface::program::ID for the stake program. -#[allow(clippy::too_many_arguments)] -fn build_stake_pool_withdraw_stake( - stake_pool: &Pubkey, - validator_list: &Pubkey, - withdraw_authority: &Pubkey, - validator_stake: &Pubkey, - destination_stake: &Pubkey, - destination_stake_authority: &Pubkey, - source_transfer_authority: &Pubkey, - source_pool_account: &Pubkey, - manager_fee_account: &Pubkey, - pool_mint: &Pubkey, - pool_tokens: u64, -) -> Instruction { - use borsh::BorshSerialize; - - // WithdrawStake instruction data using spl-stake-pool - let instruction_data = StakePoolInstruction::WithdrawStake(pool_tokens); - let mut data = Vec::new(); - instruction_data.serialize(&mut data).unwrap(); - - Instruction::new_with_bytes( - program_ids::stake_pool_program(), - &data, - vec![ - AccountMeta::new(*stake_pool, false), - AccountMeta::new(*validator_list, false), - AccountMeta::new_readonly(*withdraw_authority, false), - AccountMeta::new(*validator_stake, false), - AccountMeta::new(*destination_stake, false), - AccountMeta::new_readonly(*destination_stake_authority, false), - AccountMeta::new_readonly(*source_transfer_authority, true), // signer - AccountMeta::new(*source_pool_account, false), - AccountMeta::new(*manager_fee_account, false), - AccountMeta::new(*pool_mint, false), - AccountMeta::new_readonly(clock_sysvar::ID, false), - AccountMeta::new_readonly(program_ids::token_program(), false), - AccountMeta::new_readonly(solana_stake_interface::program::ID, false), - ], - ) -} - -#[cfg(test)] -mod tests { - use super::*; - - // Use our 2.x parsing Transaction for verification (different type than SDK Transaction) - fn verify_tx_structure(tx_bytes: &[u8], expected_instructions: usize) { - use crate::transaction::TransactionExt; - let tx = crate::Transaction::from_bytes(tx_bytes).unwrap(); - assert_eq!(tx.num_instructions(), expected_instructions); - } - - #[test] - fn test_build_simple_transfer() { - let intent = TransactionIntent { - fee_payer: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(), - nonce: Nonce::Blockhash { - value: "GWaQEymC3Z9SHM2gkh8u12xL1zJPMHPCSVR3pSDpEXE4".to_string(), - }, - instructions: vec![IntentInstruction::Transfer { - from: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(), - to: "FKjSjCqByQRwSzZoMXA7bKnDbJe41YgJTHFFzBeC42bH".to_string(), - lamports: 1000000, - }], - address_lookup_tables: None, - static_account_keys: None, - }; - - let result = build_transaction(intent); - assert!(result.is_ok(), "Failed to build transaction: {:?}", result); - - let tx_bytes = result.unwrap(); - assert!(!tx_bytes.is_empty()); - verify_tx_structure(&tx_bytes, 1); - } - - #[test] - fn test_build_with_memo() { - let intent = TransactionIntent { - fee_payer: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(), - nonce: Nonce::Blockhash { - value: "GWaQEymC3Z9SHM2gkh8u12xL1zJPMHPCSVR3pSDpEXE4".to_string(), - }, - instructions: vec![ - IntentInstruction::Transfer { - from: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(), - to: "FKjSjCqByQRwSzZoMXA7bKnDbJe41YgJTHFFzBeC42bH".to_string(), - lamports: 1000000, - }, - IntentInstruction::Memo { - message: "BitGo transfer".to_string(), - }, - ], - address_lookup_tables: None, - static_account_keys: None, - }; - - let result = build_transaction(intent); - assert!(result.is_ok()); - - let tx_bytes = result.unwrap(); - verify_tx_structure(&tx_bytes, 2); - } - - #[test] - fn test_build_with_compute_budget() { - let intent = TransactionIntent { - fee_payer: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(), - nonce: Nonce::Blockhash { - value: "GWaQEymC3Z9SHM2gkh8u12xL1zJPMHPCSVR3pSDpEXE4".to_string(), - }, - instructions: vec![ - IntentInstruction::ComputeBudget { - unit_limit: Some(200000), - unit_price: None, - }, - IntentInstruction::Transfer { - from: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(), - to: "FKjSjCqByQRwSzZoMXA7bKnDbJe41YgJTHFFzBeC42bH".to_string(), - lamports: 1000000, - }, - ], - address_lookup_tables: None, - static_account_keys: None, - }; - - let result = build_transaction(intent); - assert!(result.is_ok()); - } - - #[test] - fn test_invalid_pubkey() { - let intent = TransactionIntent { - fee_payer: "invalid".to_string(), - nonce: Nonce::Blockhash { - value: "GWaQEymC3Z9SHM2gkh8u12xL1zJPMHPCSVR3pSDpEXE4".to_string(), - }, - instructions: vec![], - address_lookup_tables: None, - static_account_keys: None, - }; - - let result = build_transaction(intent); - assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("Invalid")); - } - - #[test] - fn test_build_stake_delegate() { - let intent = TransactionIntent { - fee_payer: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(), - nonce: Nonce::Blockhash { - value: "GWaQEymC3Z9SHM2gkh8u12xL1zJPMHPCSVR3pSDpEXE4".to_string(), - }, - instructions: vec![IntentInstruction::StakeDelegate { - stake: "FKjSjCqByQRwSzZoMXA7bKnDbJe41YgJTHFFzBeC42bH".to_string(), - vote: "5ZWgXcyqrrNpQHCme5SdC5hCeYb2o3fEJhF7Gok3bTVN".to_string(), - authority: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(), - }], - address_lookup_tables: None, - static_account_keys: None, - }; - - let result = build_transaction(intent); - assert!( - result.is_ok(), - "Failed to build stake delegate: {:?}", - result - ); - verify_tx_structure(&result.unwrap(), 1); - } - - #[test] - fn test_build_stake_deactivate() { - let intent = TransactionIntent { - fee_payer: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(), - nonce: Nonce::Blockhash { - value: "GWaQEymC3Z9SHM2gkh8u12xL1zJPMHPCSVR3pSDpEXE4".to_string(), - }, - instructions: vec![IntentInstruction::StakeDeactivate { - stake: "FKjSjCqByQRwSzZoMXA7bKnDbJe41YgJTHFFzBeC42bH".to_string(), - authority: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(), - }], - address_lookup_tables: None, - static_account_keys: None, - }; - - let result = build_transaction(intent); - assert!( - result.is_ok(), - "Failed to build stake deactivate: {:?}", - result - ); - verify_tx_structure(&result.unwrap(), 1); - } - - #[test] - fn test_build_stake_withdraw() { - let intent = TransactionIntent { - fee_payer: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(), - nonce: Nonce::Blockhash { - value: "GWaQEymC3Z9SHM2gkh8u12xL1zJPMHPCSVR3pSDpEXE4".to_string(), - }, - instructions: vec![IntentInstruction::StakeWithdraw { - stake: "FKjSjCqByQRwSzZoMXA7bKnDbJe41YgJTHFFzBeC42bH".to_string(), - recipient: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(), - lamports: 1000000, - authority: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(), - }], - address_lookup_tables: None, - static_account_keys: None, - }; - - let result = build_transaction(intent); - assert!( - result.is_ok(), - "Failed to build stake withdraw: {:?}", - result - ); - verify_tx_structure(&result.unwrap(), 1); - } - - #[test] - fn test_build_token_transfer() { - let intent = TransactionIntent { - fee_payer: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(), - nonce: Nonce::Blockhash { - value: "GWaQEymC3Z9SHM2gkh8u12xL1zJPMHPCSVR3pSDpEXE4".to_string(), - }, - instructions: vec![IntentInstruction::TokenTransfer { - source: "FKjSjCqByQRwSzZoMXA7bKnDbJe41YgJTHFFzBeC42bH".to_string(), - destination: "5ZWgXcyqrrNpQHCme5SdC5hCeYb2o3fEJhF7Gok3bTVN".to_string(), - mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(), // USDC mint - amount: 1000000, - decimals: 6, - authority: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(), - program_id: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA".to_string(), - }], - address_lookup_tables: None, - static_account_keys: None, - }; - - let result = build_transaction(intent); - assert!( - result.is_ok(), - "Failed to build token transfer: {:?}", - result - ); - verify_tx_structure(&result.unwrap(), 1); - } - - #[test] - fn test_build_create_ata() { - let intent = TransactionIntent { - fee_payer: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(), - nonce: Nonce::Blockhash { - value: "GWaQEymC3Z9SHM2gkh8u12xL1zJPMHPCSVR3pSDpEXE4".to_string(), - }, - instructions: vec![IntentInstruction::CreateAssociatedTokenAccount { - payer: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(), - owner: "FKjSjCqByQRwSzZoMXA7bKnDbJe41YgJTHFFzBeC42bH".to_string(), - mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(), // USDC mint - token_program_id: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA".to_string(), - }], - address_lookup_tables: None, - static_account_keys: None, - }; - - let result = build_transaction(intent); - assert!(result.is_ok(), "Failed to build create ATA: {:?}", result); - verify_tx_structure(&result.unwrap(), 1); - } - - #[test] - fn test_build_close_ata() { - let intent = TransactionIntent { - fee_payer: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(), - nonce: Nonce::Blockhash { - value: "GWaQEymC3Z9SHM2gkh8u12xL1zJPMHPCSVR3pSDpEXE4".to_string(), - }, - instructions: vec![IntentInstruction::CloseAssociatedTokenAccount { - account: "FKjSjCqByQRwSzZoMXA7bKnDbJe41YgJTHFFzBeC42bH".to_string(), - destination: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(), - authority: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(), - program_id: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA".to_string(), - }], - address_lookup_tables: None, - static_account_keys: None, - }; - - let result = build_transaction(intent); - assert!(result.is_ok(), "Failed to build close ATA: {:?}", result); - verify_tx_structure(&result.unwrap(), 1); - } - - #[test] - fn test_build_mint_to() { - let intent = TransactionIntent { - fee_payer: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(), - nonce: Nonce::Blockhash { - value: "GWaQEymC3Z9SHM2gkh8u12xL1zJPMHPCSVR3pSDpEXE4".to_string(), - }, - instructions: vec![IntentInstruction::MintTo { - mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(), - destination: "FKjSjCqByQRwSzZoMXA7bKnDbJe41YgJTHFFzBeC42bH".to_string(), - authority: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(), - amount: 1000000, - program_id: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA".to_string(), - }], - address_lookup_tables: None, - static_account_keys: None, - }; - - let result = build_transaction(intent); - assert!(result.is_ok(), "Failed to build mint to: {:?}", result); - verify_tx_structure(&result.unwrap(), 1); - } - - #[test] - fn test_build_burn() { - let intent = TransactionIntent { - fee_payer: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(), - nonce: Nonce::Blockhash { - value: "GWaQEymC3Z9SHM2gkh8u12xL1zJPMHPCSVR3pSDpEXE4".to_string(), - }, - instructions: vec![IntentInstruction::Burn { - mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(), - account: "FKjSjCqByQRwSzZoMXA7bKnDbJe41YgJTHFFzBeC42bH".to_string(), - authority: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(), - amount: 1000000, - program_id: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA".to_string(), - }], - address_lookup_tables: None, - static_account_keys: None, - }; - - let result = build_transaction(intent); - assert!(result.is_ok(), "Failed to build burn: {:?}", result); - verify_tx_structure(&result.unwrap(), 1); - } - - #[test] - fn test_build_approve() { - let intent = TransactionIntent { - fee_payer: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(), - nonce: Nonce::Blockhash { - value: "GWaQEymC3Z9SHM2gkh8u12xL1zJPMHPCSVR3pSDpEXE4".to_string(), - }, - instructions: vec![IntentInstruction::Approve { - account: "FKjSjCqByQRwSzZoMXA7bKnDbJe41YgJTHFFzBeC42bH".to_string(), - delegate: "5ZWgXcyqrrNpQHCme5SdC5hCeYb2o3fEJhF7Gok3bTVN".to_string(), - owner: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(), - amount: 1000000, - program_id: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA".to_string(), - }], - address_lookup_tables: None, - static_account_keys: None, - }; - - let result = build_transaction(intent); - assert!(result.is_ok(), "Failed to build approve: {:?}", result); - verify_tx_structure(&result.unwrap(), 1); - } - - #[test] - fn test_build_stake_pool_deposit_sol() { - // Jito stake pool addresses (testnet-like) - let intent = TransactionIntent { - fee_payer: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(), - nonce: Nonce::Blockhash { - value: "GWaQEymC3Z9SHM2gkh8u12xL1zJPMHPCSVR3pSDpEXE4".to_string(), - }, - instructions: vec![IntentInstruction::StakePoolDepositSol { - stake_pool: "Jito4APyf642JPZPx3hGc6WWJ8zPKtRbRs4P815Awbb".to_string(), - withdraw_authority: "6iQKfEyhr3bZMotVkW6beNZz5CPAkiwvgV2CTje9pVSS".to_string(), - reserve_stake: "BgKUXdS4Wy6Vdgp1jwT2dz5ZgxPG94aPL77dQscSPGmc".to_string(), - funding_account: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(), - destination_pool_account: "FKjSjCqByQRwSzZoMXA7bKnDbJe41YgJTHFFzBeC42bH" - .to_string(), - manager_fee_account: "5ZWgXcyqrrNpQHCme5SdC5hCeYb2o3fEJhF7Gok3bTVN".to_string(), - referral_pool_account: "5ZWgXcyqrrNpQHCme5SdC5hCeYb2o3fEJhF7Gok3bTVN".to_string(), - pool_mint: "J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn".to_string(), - lamports: 1000000000, // 1 SOL - }], - address_lookup_tables: None, - static_account_keys: None, - }; - - let result = build_transaction(intent); - assert!( - result.is_ok(), - "Failed to build stake pool deposit sol: {:?}", - result - ); - verify_tx_structure(&result.unwrap(), 1); - } - - #[test] - fn test_build_stake_pool_withdraw_stake() { - // Jito stake pool addresses (testnet-like) - let intent = TransactionIntent { - fee_payer: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(), - nonce: Nonce::Blockhash { - value: "GWaQEymC3Z9SHM2gkh8u12xL1zJPMHPCSVR3pSDpEXE4".to_string(), - }, - instructions: vec![IntentInstruction::StakePoolWithdrawStake { - stake_pool: "Jito4APyf642JPZPx3hGc6WWJ8zPKtRbRs4P815Awbb".to_string(), - validator_list: "3R3nGZpQs2aZo5FDQvd2MUQ5R5E9g7NvHQaxpLPYA8r2".to_string(), - withdraw_authority: "6iQKfEyhr3bZMotVkW6beNZz5CPAkiwvgV2CTje9pVSS".to_string(), - validator_stake: "BgKUXdS4Wy6Vdgp1jwT2dz5ZgxPG94aPL77dQscSPGmc".to_string(), - destination_stake: "FKjSjCqByQRwSzZoMXA7bKnDbJe41YgJTHFFzBeC42bH".to_string(), - destination_stake_authority: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB" - .to_string(), - source_transfer_authority: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB" - .to_string(), - source_pool_account: "5ZWgXcyqrrNpQHCme5SdC5hCeYb2o3fEJhF7Gok3bTVN".to_string(), - manager_fee_account: "5ZWgXcyqrrNpQHCme5SdC5hCeYb2o3fEJhF7Gok3bTVN".to_string(), - pool_mint: "J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn".to_string(), - pool_tokens: 1000000000, // 1 JitoSOL - }], - address_lookup_tables: None, - static_account_keys: None, - }; - - let result = build_transaction(intent); - assert!( - result.is_ok(), - "Failed to build stake pool withdraw stake: {:?}", - result - ); - verify_tx_structure(&result.unwrap(), 1); - } - - #[test] - fn test_build_stake_split() { - let intent = TransactionIntent { - fee_payer: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(), - nonce: Nonce::Blockhash { - value: "GWaQEymC3Z9SHM2gkh8u12xL1zJPMHPCSVR3pSDpEXE4".to_string(), - }, - instructions: vec![IntentInstruction::StakeSplit { - stake: "FKjSjCqByQRwSzZoMXA7bKnDbJe41YgJTHFFzBeC42bH".to_string(), - split_stake: "5ZWgXcyqrrNpQHCme5SdC5hCeYb2o3fEJhF7Gok3bTVN".to_string(), - authority: "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB".to_string(), - lamports: 500000000, // 0.5 SOL - }], - address_lookup_tables: None, - static_account_keys: None, - }; - - let result = build_transaction(intent); - assert!(result.is_ok(), "Failed to build stake split: {:?}", result); - verify_tx_structure(&result.unwrap(), 1); - } -} diff --git a/packages/wasm-solana/src/builder/mod.rs b/packages/wasm-solana/src/builder/mod.rs deleted file mode 100644 index f90c82a..0000000 --- a/packages/wasm-solana/src/builder/mod.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Transaction building module. -//! -//! This module provides the `buildTransaction()` function which creates Solana -//! transactions from a high-level `TransactionIntent` structure. -//! -//! # Transaction Types -//! -//! - **Legacy transactions**: Standard format, all accounts inline -//! - **Versioned transactions (MessageV0)**: Supports Address Lookup Tables -//! -//! The builder automatically selects the format based on whether -//! `address_lookup_tables` is provided in the intent. - -mod build; -mod types; -mod versioned; - -pub use build::build_transaction; -pub use types::{ - AddressLookupTable, Instruction, MessageHeader, Nonce, RawVersionedTransactionData, - TransactionIntent, VersionedInstruction, -}; -pub use versioned::{build_from_raw_versioned_data, should_build_versioned}; diff --git a/packages/wasm-solana/src/builder/types.rs b/packages/wasm-solana/src/builder/types.rs deleted file mode 100644 index 8ff8d34..0000000 --- a/packages/wasm-solana/src/builder/types.rs +++ /dev/null @@ -1,456 +0,0 @@ -//! Types for transaction building. -//! -//! These types are designed to be serialized from JavaScript via serde. -//! Public keys use string (base58) representations. -//! Amounts use u64 which maps to JavaScript BigInt via wasm-bindgen. - -use serde::Deserialize; - -/// Nonce source for transaction - either a recent blockhash or durable nonce account. -#[derive(Debug, Clone, Deserialize)] -#[serde(tag = "type", rename_all = "camelCase")] -pub enum Nonce { - /// Use a recent blockhash (standard transactions) - Blockhash { value: String }, - /// Use a durable nonce account (offline signing) - Durable { - address: String, - authority: String, - /// Nonce value stored in the account (this becomes the blockhash) - value: String, - }, -} - -/// Intent to build a transaction. -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct TransactionIntent { - /// The fee payer's public key (base58) - pub fee_payer: String, - /// Nonce source - pub nonce: Nonce, - /// List of instructions to include - pub instructions: Vec, - - // ===== Versioned Transaction Fields (MessageV0) ===== - // If these fields are provided, a versioned transaction is built. - /// Address Lookup Tables for versioned transactions. - /// If provided, builds a MessageV0 transaction instead of legacy. - #[serde(rename = "addressLookupTables", default)] - pub address_lookup_tables: Option>, - - /// Static account keys (for versioned transaction round-trip). - /// These are the accounts stored directly in the message. - #[serde(rename = "staticAccountKeys", default)] - pub static_account_keys: Option>, -} - -/// Address Lookup Table data for versioned transactions. -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AddressLookupTable { - /// The lookup table account address (base58) - #[serde(rename = "accountKey")] - pub account_key: String, - /// Indices of writable accounts in the lookup table - #[serde(rename = "writableIndexes")] - pub writable_indexes: Vec, - /// Indices of readonly accounts in the lookup table - #[serde(rename = "readonlyIndexes")] - pub readonly_indexes: Vec, -} - -/// An instruction to include in the transaction. -/// -/// This is a discriminated union (tagged enum) that supports all instruction types. -/// Use the `type` field to determine which variant is being used. -#[derive(Debug, Clone, Deserialize)] -#[serde(tag = "type", rename_all = "camelCase")] -pub enum Instruction { - // ===== System Program Instructions ===== - /// Transfer SOL from one account to another - Transfer { - from: String, - to: String, - /// Amount in lamports - lamports: u64, - }, - - /// Create a new account - CreateAccount { - from: String, - #[serde(rename = "newAccount")] - new_account: String, - /// Lamports to transfer to new account - lamports: u64, - /// Space to allocate in bytes - space: u64, - /// Program owner of the new account - owner: String, - }, - - /// Advance a nonce account - NonceAdvance { - /// Nonce account address - nonce: String, - /// Nonce authority - authority: String, - }, - - /// Initialize a nonce account - NonceInitialize { - /// Nonce account address - nonce: String, - /// Nonce authority - authority: String, - }, - - /// Allocate space in an account - Allocate { account: String, space: u64 }, - - /// Assign account to a program - Assign { account: String, owner: String }, - - // ===== Memo Program ===== - /// Add a memo to the transaction - Memo { message: String }, - - // ===== Compute Budget Program ===== - /// Set compute budget (priority fees) - ComputeBudget { - /// Compute unit limit (optional) - #[serde(rename = "unitLimit")] - unit_limit: Option, - /// Compute unit price in micro-lamports (optional) - #[serde(rename = "unitPrice")] - unit_price: Option, - }, - // ===== Stake Program Instructions ===== - /// Initialize a stake account with authorized staker and withdrawer - StakeInitialize { - /// Stake account address - stake: String, - /// Authorized staker pubkey - staker: String, - /// Authorized withdrawer pubkey - withdrawer: String, - }, - - /// Delegate stake to a validator - StakeDelegate { - /// Stake account address - stake: String, - /// Vote account (validator) to delegate to - vote: String, - /// Stake authority - authority: String, - }, - - /// Deactivate a stake account - StakeDeactivate { - /// Stake account address - stake: String, - /// Stake authority - authority: String, - }, - - /// Withdraw from a stake account - StakeWithdraw { - /// Stake account address - stake: String, - /// Recipient address for withdrawn lamports - recipient: String, - /// Amount in lamports to withdraw - lamports: u64, - /// Withdraw authority - authority: String, - }, - - /// Change stake account authorization - StakeAuthorize { - /// Stake account address - stake: String, - /// New authority pubkey - #[serde(rename = "newAuthority")] - new_authority: String, - /// Authorization type: "staker" or "withdrawer" - #[serde(rename = "authorizeType")] - authorize_type: String, - /// Current authority - authority: String, - }, - - /// Split stake account (used for partial deactivation) - StakeSplit { - /// Source stake account address - stake: String, - /// Destination stake account (must be uninitialized/created first) - #[serde(rename = "splitStake")] - split_stake: String, - /// Stake authority - authority: String, - /// Amount in lamports to split - lamports: u64, - }, - - // ===== SPL Token Instructions ===== - /// Transfer tokens (uses TransferChecked for safety) - TokenTransfer { - /// Source token account - source: String, - /// Destination token account - destination: String, - /// Token mint address - mint: String, - /// Amount of tokens to transfer (in smallest units) - amount: u64, - /// Number of decimals for the token - decimals: u8, - /// Owner/authority of the source account - authority: String, - /// Token program ID (TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA or Token-2022) - #[serde(rename = "programId", default = "default_token_program")] - program_id: String, - }, - - /// Create an Associated Token Account - CreateAssociatedTokenAccount { - /// Payer for account creation - payer: String, - /// Owner of the new ATA - owner: String, - /// Token mint address - mint: String, - /// Token program ID (optional, defaults to Token Program) - #[serde(rename = "tokenProgramId", default = "default_token_program")] - token_program_id: String, - }, - - /// Close an Associated Token Account - CloseAssociatedTokenAccount { - /// Token account to close - account: String, - /// Destination for remaining lamports - destination: String, - /// Authority of the account - authority: String, - /// Token program ID (optional, defaults to Token Program) - #[serde(rename = "programId", default = "default_token_program")] - program_id: String, - }, - - /// Mint tokens to an account (requires mint authority) - MintTo { - /// Token mint address - mint: String, - /// Destination token account - destination: String, - /// Mint authority - authority: String, - /// Amount of tokens to mint (in smallest units) - amount: u64, - /// Token program ID (optional, defaults to Token Program) - #[serde(rename = "programId", default = "default_token_program")] - program_id: String, - }, - - /// Burn tokens from an account - Burn { - /// Token mint address - mint: String, - /// Source token account to burn from - account: String, - /// Token account authority - authority: String, - /// Amount of tokens to burn (in smallest units) - amount: u64, - /// Token program ID (optional, defaults to Token Program) - #[serde(rename = "programId", default = "default_token_program")] - program_id: String, - }, - - /// Approve a delegate to transfer tokens - Approve { - /// Token account to approve delegation for - account: String, - /// Delegate address (who can transfer) - delegate: String, - /// Token account owner - owner: String, - /// Amount of tokens to approve (in smallest units) - amount: u64, - /// Token program ID (optional, defaults to Token Program) - #[serde(rename = "programId", default = "default_token_program")] - program_id: String, - }, - - // ===== Jito Stake Pool Instructions ===== - /// Deposit SOL into a stake pool (Jito liquid staking) - StakePoolDepositSol { - /// Stake pool address - #[serde(rename = "stakePool")] - stake_pool: String, - /// Withdraw authority PDA - #[serde(rename = "withdrawAuthority")] - withdraw_authority: String, - /// Reserve stake account - #[serde(rename = "reserveStake")] - reserve_stake: String, - /// Funding account (SOL source, signer) - #[serde(rename = "fundingAccount")] - funding_account: String, - /// Destination for pool tokens - #[serde(rename = "destinationPoolAccount")] - destination_pool_account: String, - /// Manager fee account - #[serde(rename = "managerFeeAccount")] - manager_fee_account: String, - /// Referral pool account - #[serde(rename = "referralPoolAccount")] - referral_pool_account: String, - /// Pool mint address - #[serde(rename = "poolMint")] - pool_mint: String, - /// Amount in lamports to deposit - lamports: u64, - }, - - /// Withdraw stake from a stake pool (Jito liquid staking) - StakePoolWithdrawStake { - /// Stake pool address - #[serde(rename = "stakePool")] - stake_pool: String, - /// Validator list account - #[serde(rename = "validatorList")] - validator_list: String, - /// Withdraw authority PDA - #[serde(rename = "withdrawAuthority")] - withdraw_authority: String, - /// Validator stake account to split from - #[serde(rename = "validatorStake")] - validator_stake: String, - /// Destination stake account (uninitialized) - #[serde(rename = "destinationStake")] - destination_stake: String, - /// Authority for the destination stake account - #[serde(rename = "destinationStakeAuthority")] - destination_stake_authority: String, - /// Source pool token account authority (signer) - #[serde(rename = "sourceTransferAuthority")] - source_transfer_authority: String, - /// Source pool token account - #[serde(rename = "sourcePoolAccount")] - source_pool_account: String, - /// Manager fee account - #[serde(rename = "managerFeeAccount")] - manager_fee_account: String, - /// Pool mint address - #[serde(rename = "poolMint")] - pool_mint: String, - /// Amount of pool tokens to burn - #[serde(rename = "poolTokens")] - pool_tokens: u64, - }, - - // ===== Custom/Raw Instruction ===== - /// A custom instruction that can invoke any program. - /// This enables passthrough of arbitrary instructions for extensibility. - Custom { - /// The program ID to invoke (base58) - #[serde(rename = "programId")] - program_id: String, - /// Account metas for the instruction - accounts: Vec, - /// Instruction data (base64 or hex encoded) - data: String, - /// Encoding of the data field: "base64" (default) or "hex" - #[serde(default = "default_encoding")] - encoding: String, - }, -} - -/// Account meta for custom instructions -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CustomAccountMeta { - /// Account public key (base58) - pub pubkey: String, - /// Whether the account is a signer - #[serde(default)] - pub is_signer: bool, - /// Whether the account is writable - #[serde(default)] - pub is_writable: bool, -} - -fn default_token_program() -> String { - "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA".to_string() -} - -fn default_encoding() -> String { - "base64".to_string() -} - -// ============================================================================= -// Raw Versioned Transaction Data (for fromVersionedTransactionData path) -// ============================================================================= - -/// Raw versioned transaction data for direct serialization. -/// This is used when we have pre-formed MessageV0 data that just needs to be serialized. -/// No instruction compilation is needed - just serialize the raw structure. -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct RawVersionedTransactionData { - /// Static account keys (base58 encoded public keys) - #[serde(rename = "staticAccountKeys")] - pub static_account_keys: Vec, - - /// Address lookup tables - #[serde(rename = "addressLookupTables")] - pub address_lookup_tables: Vec, - - /// Pre-compiled instructions with index-based account references - #[serde(rename = "versionedInstructions")] - pub versioned_instructions: Vec, - - /// Message header - #[serde(rename = "messageHeader")] - pub message_header: MessageHeader, - - /// Recent blockhash (base58) - #[serde(rename = "recentBlockhash")] - pub recent_blockhash: String, -} - -/// A pre-compiled versioned instruction (uses indexes, not pubkeys) -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct VersionedInstruction { - /// Index into the account keys array for the program ID - #[serde(rename = "programIdIndex")] - pub program_id_index: u8, - - /// Indexes into the account keys array for instruction accounts - #[serde(rename = "accountKeyIndexes")] - pub account_key_indexes: Vec, - - /// Instruction data (base58 encoded) - pub data: String, -} - -/// Message header for versioned transactions -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct MessageHeader { - /// Number of required signatures - #[serde(rename = "numRequiredSignatures")] - pub num_required_signatures: u8, - - /// Number of readonly signed accounts - #[serde(rename = "numReadonlySignedAccounts")] - pub num_readonly_signed_accounts: u8, - - /// Number of readonly unsigned accounts - #[serde(rename = "numReadonlyUnsignedAccounts")] - pub num_readonly_unsigned_accounts: u8, -} diff --git a/packages/wasm-solana/src/builder/versioned.rs b/packages/wasm-solana/src/builder/versioned.rs deleted file mode 100644 index 7e7f149..0000000 --- a/packages/wasm-solana/src/builder/versioned.rs +++ /dev/null @@ -1,258 +0,0 @@ -//! Versioned transaction building (MessageV0). -//! -//! This module handles building versioned transactions with Address Lookup Tables (ALTs). -//! Versioned transactions use MessageV0 which supports referencing accounts from ALTs, -//! allowing transactions with more accounts than the legacy format. -//! -//! # When to Use -//! -//! Build a versioned transaction when: -//! - The `TransactionIntent` has `address_lookup_tables` field set -//! - Round-tripping a parsed versioned transaction -//! -//! # Wire Format -//! -//! MessageV0 transactions have a version byte (0x80) followed by the message. -//! The ALT references allow account indices beyond 255. - -use crate::builder::types::{ - AddressLookupTable, Nonce, RawVersionedTransactionData, TransactionIntent, -}; -use crate::error::WasmSolanaError; -use solana_message::v0::Message as MessageV0; -use solana_message::AddressLookupTableAccount; -use solana_sdk::bs58; -use solana_sdk::hash::Hash; -use solana_sdk::instruction::Instruction; -use solana_sdk::pubkey::Pubkey; -use solana_transaction::versioned::VersionedTransaction; -use std::str::FromStr; - -/// Build a versioned transaction (MessageV0) from an intent. -/// -/// This is called when the intent has `address_lookup_tables` set. -/// The ALTs must include the actual account keys for proper compilation. -/// -/// # Arguments -/// -/// * `intent` - The transaction intent with ALT data -/// * `instructions` - Pre-built instructions (built by build.rs) -/// -/// # Returns -/// -/// Serialized versioned transaction bytes -pub fn build_versioned_transaction( - intent: &TransactionIntent, - instructions: Vec, -) -> Result, WasmSolanaError> { - // Parse fee payer - let fee_payer: Pubkey = intent - .fee_payer - .parse() - .map_err(|e| WasmSolanaError::new(&format!("Invalid fee payer: {}", e)))?; - - // Parse blockhash - let blockhash_str = match &intent.nonce { - Nonce::Blockhash { value } => value.clone(), - Nonce::Durable { value, .. } => value.clone(), - }; - let blockhash = Hash::from_str(&blockhash_str) - .map_err(|e| WasmSolanaError::new(&format!("Invalid blockhash: {}", e)))?; - - // Convert ALT data to AddressLookupTableAccount format - // Note: For compilation, we need the actual account keys from the ALT. - // Since we don't have them, we use a simplified approach that works for - // round-tripping pre-built versioned transactions. - let alt_accounts = convert_alts_for_compile(&intent.address_lookup_tables)?; - - // Try to compile the MessageV0 - let message = MessageV0::try_compile(&fee_payer, &instructions, &alt_accounts, blockhash) - .map_err(|e| WasmSolanaError::new(&format!("Failed to compile MessageV0: {:?}", e)))?; - - // Create versioned transaction with empty signatures - let versioned_tx = VersionedTransaction { - signatures: vec![], - message: solana_message::VersionedMessage::V0(message), - }; - - // Serialize to bytes - bincode::serialize(&versioned_tx).map_err(|e| { - WasmSolanaError::new(&format!("Failed to serialize versioned transaction: {}", e)) - }) -} - -/// Convert AddressLookupTable data to AddressLookupTableAccount for compilation. -/// -/// Note: This is a simplified conversion. For full ALT support, we'd need -/// the actual account keys stored in each ALT. For now, this supports -/// transactions where all accounts are in static_account_keys. -fn convert_alts_for_compile( - alts: &Option>, -) -> Result, WasmSolanaError> { - let Some(alts) = alts else { - return Ok(vec![]); - }; - - let mut accounts = Vec::with_capacity(alts.len()); - - for alt in alts { - let key: Pubkey = alt - .account_key - .parse() - .map_err(|e| WasmSolanaError::new(&format!("Invalid ALT account key: {}", e)))?; - - // For now, we create empty address lists. - // Full ALT support would require fetching ALT account data. - accounts.push(AddressLookupTableAccount { - key, - addresses: vec![], // Would need actual ALT data for new transactions - }); - } - - Ok(accounts) -} - -/// Check if an intent should be built as a versioned transaction. -pub fn should_build_versioned(intent: &TransactionIntent) -> bool { - intent.address_lookup_tables.is_some() -} - -/// Build a versioned transaction directly from raw MessageV0 data. -/// -/// This function is used for the `fromVersionedTransactionData()` path where we already -/// have pre-compiled versioned data (indexes + ALT refs). No instruction compilation -/// is needed - we just serialize the raw structure to bytes. -/// -/// # Arguments -/// -/// * `data` - Raw versioned transaction data with pre-compiled instructions -/// -/// # Returns -/// -/// Serialized versioned transaction bytes (unsigned) -pub fn build_from_raw_versioned_data( - data: &RawVersionedTransactionData, -) -> Result, WasmSolanaError> { - use solana_message::compiled_instruction::CompiledInstruction; - use solana_message::v0::MessageAddressTableLookup; - use solana_message::MessageHeader; - - // Parse static account keys - let static_account_keys: Vec = data - .static_account_keys - .iter() - .map(|key| { - key.parse().map_err(|e| { - WasmSolanaError::new(&format!("Invalid static account key '{}': {}", key, e)) - }) - }) - .collect::, _>>()?; - - // Parse blockhash - let recent_blockhash = Hash::from_str(&data.recent_blockhash) - .map_err(|e| WasmSolanaError::new(&format!("Invalid blockhash: {}", e)))?; - - // Convert instructions to compiled format - let compiled_instructions: Vec = data - .versioned_instructions - .iter() - .map(|ix| { - // Decode base58 instruction data - let instruction_data = bs58::decode(&ix.data) - .into_vec() - .map_err(|e| WasmSolanaError::new(&format!("Invalid instruction data: {}", e)))?; - - Ok(CompiledInstruction { - program_id_index: ix.program_id_index, - accounts: ix.account_key_indexes.clone(), - data: instruction_data, - }) - }) - .collect::, WasmSolanaError>>()?; - - // Convert address lookup tables - let address_table_lookups: Vec = - data.address_lookup_tables - .iter() - .map(|alt| { - let account_key: Pubkey = alt.account_key.parse().map_err(|e| { - WasmSolanaError::new(&format!("Invalid ALT account key: {}", e)) - })?; - - Ok(MessageAddressTableLookup { - account_key, - writable_indexes: alt.writable_indexes.clone(), - readonly_indexes: alt.readonly_indexes.clone(), - }) - }) - .collect::, WasmSolanaError>>()?; - - // Create MessageV0 directly (no compilation needed) - let message = MessageV0 { - header: MessageHeader { - num_required_signatures: data.message_header.num_required_signatures, - num_readonly_signed_accounts: data.message_header.num_readonly_signed_accounts, - num_readonly_unsigned_accounts: data.message_header.num_readonly_unsigned_accounts, - }, - account_keys: static_account_keys, - recent_blockhash, - instructions: compiled_instructions, - address_table_lookups, - }; - - // Create versioned transaction with empty signatures - // The number of signatures is determined by num_required_signatures - let signatures = vec![ - solana_sdk::signature::Signature::default(); - data.message_header.num_required_signatures as usize - ]; - - let versioned_tx = VersionedTransaction { - signatures, - message: solana_message::VersionedMessage::V0(message), - }; - - // Serialize to bytes - bincode::serialize(&versioned_tx).map_err(|e| { - WasmSolanaError::new(&format!("Failed to serialize versioned transaction: {}", e)) - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_should_build_versioned_with_alts() { - let intent = TransactionIntent { - fee_payer: "11111111111111111111111111111111".to_string(), - nonce: Nonce::Blockhash { - value: "11111111111111111111111111111111".to_string(), - }, - instructions: vec![], - address_lookup_tables: Some(vec![AddressLookupTable { - account_key: "11111111111111111111111111111111".to_string(), - writable_indexes: vec![0], - readonly_indexes: vec![1], - }]), - static_account_keys: None, - }; - - assert!(should_build_versioned(&intent)); - } - - #[test] - fn test_should_not_build_versioned_without_alts() { - let intent = TransactionIntent { - fee_payer: "11111111111111111111111111111111".to_string(), - nonce: Nonce::Blockhash { - value: "11111111111111111111111111111111".to_string(), - }, - instructions: vec![], - address_lookup_tables: None, - static_account_keys: None, - }; - - assert!(!should_build_versioned(&intent)); - } -} diff --git a/packages/wasm-solana/src/lib.rs b/packages/wasm-solana/src/lib.rs index 35827cd..a23e14f 100644 --- a/packages/wasm-solana/src/lib.rs +++ b/packages/wasm-solana/src/lib.rs @@ -23,7 +23,6 @@ //! let pubkey = Pubkey::from_base58("FKjSjCqByQRwSzZoMXA7bKnDbJe41YgJTHFFzBeC42bH").unwrap(); //! ``` -pub mod builder; mod error; mod instructions; pub mod intent; @@ -45,6 +44,6 @@ pub use versioned::{ // Re-export WASM types pub use wasm::{ - is_versioned_transaction, BuilderNamespace, IntentNamespace, ParserNamespace, WasmKeypair, - WasmPubkey, WasmTransaction, WasmVersionedTransaction, + is_versioned_transaction, IntentNamespace, ParserNamespace, WasmKeypair, WasmPubkey, + WasmTransaction, WasmVersionedTransaction, }; diff --git a/packages/wasm-solana/src/wasm/builder.rs b/packages/wasm-solana/src/wasm/builder.rs deleted file mode 100644 index d22a56c..0000000 --- a/packages/wasm-solana/src/wasm/builder.rs +++ /dev/null @@ -1,118 +0,0 @@ -//! WASM binding for transaction building. -//! -//! Exposes transaction building functions: -//! - `buildTransaction` - Creates a Transaction from a high-level intent structure -//! - `buildFromVersionedData` - Creates a VersionedTransaction from raw MessageV0 data - -use crate::builder; -use crate::wasm::transaction::{WasmTransaction, WasmVersionedTransaction}; -use wasm_bindgen::prelude::*; - -/// Namespace for transaction building operations. -#[wasm_bindgen] -pub struct BuilderNamespace; - -#[wasm_bindgen] -impl BuilderNamespace { - /// Build a Solana transaction from an intent structure. - /// - /// Takes a TransactionIntent JSON object and returns serialized transaction bytes. - /// - /// # Intent Structure - /// - /// ```json - /// { - /// "feePayer": "DgT9qyYwYKBRDyDw3EfR12LHQCQjtNrKu2qMsXHuosmB", - /// "nonce": { - /// "type": "blockhash", - /// "value": "GWaQEymC3Z9SHM2gkh8u12xL1zJPMHPCSVR3pSDpEXE4" - /// }, - /// "instructions": [ - /// { "type": "transfer", "from": "...", "to": "...", "lamports": "1000000" }, - /// { "type": "memo", "message": "BitGo tx" } - /// ] - /// } - /// ``` - /// - /// # Instruction Types - /// - /// - `transfer`: SOL transfer (from, to, lamports) - /// - `createAccount`: Create new account (from, newAccount, lamports, space, owner) - /// - `nonceAdvance`: Advance durable nonce (nonce, authority) - /// - `nonceInitialize`: Initialize nonce account (nonce, authority) - /// - `allocate`: Allocate space (account, space) - /// - `assign`: Assign to program (account, owner) - /// - `memo`: Add memo (message) - /// - `computeBudget`: Set compute units (unitLimit, unitPrice) - /// - /// # Returns - /// - /// A `Transaction` object that can be inspected, signed, and serialized. - /// The transaction will have empty signature placeholders that can be - /// filled in later by signing via `addSignature()`. - /// - /// @param intent - The transaction intent as a JSON object - /// @returns Transaction object - #[wasm_bindgen] - pub fn build_transaction(intent: JsValue) -> Result { - // Deserialize the intent from JavaScript - let intent: builder::TransactionIntent = - serde_wasm_bindgen::from_value(intent).map_err(|e| { - JsValue::from_str(&format!("Failed to parse transaction intent: {}", e)) - })?; - - // Build the transaction bytes - let bytes = - builder::build_transaction(intent).map_err(|e| JsValue::from_str(&e.to_string()))?; - - // Wrap in WasmTransaction for rich API access - WasmTransaction::from_bytes(&bytes).map_err(|e| JsValue::from_str(&e.to_string())) - } - - /// Build a versioned transaction directly from raw MessageV0 data. - /// - /// This function is used for the `fromVersionedTransactionData()` path where we already - /// have pre-compiled versioned data (indexes + ALT refs). No instruction compilation - /// is needed - we just serialize the raw structure to bytes. - /// - /// # Data Structure - /// - /// ```json - /// { - /// "staticAccountKeys": ["pubkey1", "pubkey2", ...], - /// "addressLookupTables": [ - /// { "accountKey": "altPubkey", "writableIndexes": [0, 1], "readonlyIndexes": [2] } - /// ], - /// "versionedInstructions": [ - /// { "programIdIndex": 0, "accountKeyIndexes": [1, 2], "data": "base58EncodedData" } - /// ], - /// "messageHeader": { - /// "numRequiredSignatures": 1, - /// "numReadonlySignedAccounts": 0, - /// "numReadonlyUnsignedAccounts": 3 - /// }, - /// "recentBlockhash": "blockhash" - /// } - /// ``` - /// - /// @param data - Raw versioned transaction data as a JSON object - /// @returns VersionedTransaction object - #[wasm_bindgen] - pub fn build_from_versioned_data(data: JsValue) -> Result { - // Deserialize the raw versioned data from JavaScript - let data: builder::RawVersionedTransactionData = serde_wasm_bindgen::from_value(data) - .map_err(|e| { - JsValue::from_str(&format!( - "Failed to parse versioned transaction data: {}", - e - )) - })?; - - // Build the versioned transaction bytes - let bytes = builder::build_from_raw_versioned_data(&data) - .map_err(|e| JsValue::from_str(&e.to_string()))?; - - // Wrap in WasmVersionedTransaction for rich API access - WasmVersionedTransaction::from_bytes(&bytes).map_err(|e| JsValue::from_str(&e.to_string())) - } -} diff --git a/packages/wasm-solana/src/wasm/mod.rs b/packages/wasm-solana/src/wasm/mod.rs index 8a1cad7..bf87600 100644 --- a/packages/wasm-solana/src/wasm/mod.rs +++ b/packages/wasm-solana/src/wasm/mod.rs @@ -1,4 +1,3 @@ -mod builder; mod constants; mod intent; mod keypair; @@ -7,7 +6,6 @@ mod pubkey; mod transaction; pub mod try_into_js_value; -pub use builder::BuilderNamespace; pub use intent::IntentNamespace; pub use keypair::WasmKeypair; pub use parser::ParserNamespace; diff --git a/packages/wasm-solana/test/builder.ts b/packages/wasm-solana/test/builder.ts deleted file mode 100644 index 85047dd..0000000 --- a/packages/wasm-solana/test/builder.ts +++ /dev/null @@ -1,694 +0,0 @@ -import * as assert from "assert"; -import { - buildTransaction, - parseTransaction, - type TransactionIntent, - type BuilderInstruction, -} from "../js/index.js"; - -describe("buildTransaction", () => { - // Test addresses from BitGoJS sdk-coin-sol/test/resources/sol.ts - const AUTH_ACCOUNT = "5hr5fisPi6DXNuuRpm5XUbzpiEnmdyxXuBDTwzwZj5Pe"; // authAccount.pub - const RECIPIENT = "FKjSjCqByQRwSzZoMXA7bKnDbJe41YgJTHFFzBeC42bH"; // accountWithSeed.publicKey - const NONCE_ACCOUNT = "8Y7RM6JfcX4ASSNBkrkrmSbRu431YVi9Y3oLFnzC2dCh"; // nonceAccount.pub - const BLOCKHASH = "5ne7phA48Jrvpn39AtupB8ZkCCAy8gLTfpGihZPuDqen"; // blockHashes.validBlockHashes[0] - const STAKE_ACCOUNT = "3c5emUWjViFqT72LxQYec8gkU8ZtmfKKXHvGgJNUBdYx"; // stakeAccount.pub - - // Aliases for clarity - const SENDER = AUTH_ACCOUNT; - - describe("simple transfer", () => { - it("should build a SOL transfer transaction", () => { - const intent: TransactionIntent = { - feePayer: SENDER, - nonce: { type: "blockhash", value: BLOCKHASH }, - instructions: [{ type: "transfer", from: SENDER, to: RECIPIENT, lamports: 1000000n }], - }; - - const tx = buildTransaction(intent); - const txBytes = tx.toBytes(); - assert.ok(txBytes instanceof Uint8Array); - assert.ok(txBytes.length > 0); - - const parsed = parseTransaction(tx); - assert.strictEqual(parsed.feePayer, SENDER); - assert.strictEqual(parsed.nonce, BLOCKHASH); - assert.strictEqual(parsed.instructionsData.length, 1); - assert.strictEqual(parsed.instructionsData[0].type, "Transfer"); - }); - - it("should parse the transfer instruction correctly", () => { - const intent: TransactionIntent = { - feePayer: SENDER, - nonce: { type: "blockhash", value: BLOCKHASH }, - instructions: [ - { - type: "transfer", - from: SENDER, - to: RECIPIENT, - lamports: 1000000n, - }, - ], - }; - - const tx = buildTransaction(intent); - const parsed = parseTransaction(tx); - - const transfer = parsed.instructionsData[0]; - assert.strictEqual(transfer.type, "Transfer"); - if (transfer.type === "Transfer") { - // Parser uses fromAddress/toAddress/amount - assert.strictEqual(transfer.fromAddress, SENDER); - assert.strictEqual(transfer.toAddress, RECIPIENT); - assert.strictEqual(transfer.amount, 1000000n); - } - }); - }); - - describe("transfer with memo", () => { - it("should build a transfer with memo", () => { - const intent: TransactionIntent = { - feePayer: SENDER, - nonce: { type: "blockhash", value: BLOCKHASH }, - instructions: [ - { type: "transfer", from: SENDER, to: RECIPIENT, lamports: 1000000n }, - { type: "memo", message: "BitGo transfer" }, - ], - }; - - const tx = buildTransaction(intent); - const parsed = parseTransaction(tx); - - assert.strictEqual(parsed.instructionsData.length, 2); - assert.strictEqual(parsed.instructionsData[0].type, "Transfer"); - assert.strictEqual(parsed.instructionsData[1].type, "Memo"); - - const memo = parsed.instructionsData[1]; - if (memo.type === "Memo") { - // Parser uses 'memo' field - assert.strictEqual(memo.memo, "BitGo transfer"); - } - }); - }); - - describe("compute budget", () => { - it("should build with compute unit limit", () => { - const intent: TransactionIntent = { - feePayer: SENDER, - nonce: { type: "blockhash", value: BLOCKHASH }, - instructions: [ - { type: "computeBudget", unitLimit: 200000 }, - { type: "transfer", from: SENDER, to: RECIPIENT, lamports: 1000000n }, - ], - }; - - const tx = buildTransaction(intent); - const parsed = parseTransaction(tx); - - assert.strictEqual(parsed.instructionsData.length, 2); - assert.strictEqual(parsed.instructionsData[0].type, "SetComputeUnitLimit"); - assert.strictEqual(parsed.instructionsData[1].type, "Transfer"); - - const computeBudget = parsed.instructionsData[0]; - if (computeBudget.type === "SetComputeUnitLimit") { - assert.strictEqual(computeBudget.units, 200000); - } - }); - - it("should build with compute unit price (priority fee)", () => { - const intent: TransactionIntent = { - feePayer: SENDER, - nonce: { type: "blockhash", value: BLOCKHASH }, - instructions: [ - { type: "computeBudget", unitPrice: 5000 }, - { type: "transfer", from: SENDER, to: RECIPIENT, lamports: 1000000n }, - ], - }; - - const tx = buildTransaction(intent); - const parsed = parseTransaction(tx); - - assert.strictEqual(parsed.instructionsData.length, 2); - assert.strictEqual(parsed.instructionsData[0].type, "SetPriorityFee"); - assert.strictEqual(parsed.instructionsData[1].type, "Transfer"); - - const priorityFee = parsed.instructionsData[0]; - if (priorityFee.type === "SetPriorityFee") { - // Parser uses 'fee' as BigInt - assert.strictEqual(priorityFee.fee, BigInt(5000)); - } - }); - }); - - describe("durable nonce", () => { - it("should prepend nonce advance instruction for durable nonce", () => { - // Use BitGoJS nonceAccount.pub and a sample nonce value - const NONCE_AUTHORITY = SENDER; - // This is the nonce value stored in the nonce account (becomes the blockhash) - const NONCE_VALUE = "GHtXQBsoZHVnNFa9YevAzFr17DJjgHXk3ycTKD5xD3Zi"; - - const intent: TransactionIntent = { - feePayer: SENDER, - nonce: { - type: "durable", - address: NONCE_ACCOUNT, - authority: NONCE_AUTHORITY, - value: NONCE_VALUE, - }, - instructions: [{ type: "transfer", from: SENDER, to: RECIPIENT, lamports: 1000000n }], - }; - - const tx = buildTransaction(intent); - const parsed = parseTransaction(tx); - - // Should have 2 instructions: NonceAdvance + Transfer - assert.strictEqual(parsed.instructionsData.length, 2); - assert.strictEqual(parsed.instructionsData[0].type, "NonceAdvance"); - assert.strictEqual(parsed.instructionsData[1].type, "Transfer"); - - // Verify nonce advance params - const nonceAdvance = parsed.instructionsData[0]; - if (nonceAdvance.type === "NonceAdvance") { - // Parser uses walletNonceAddress/authWalletAddress - assert.strictEqual(nonceAdvance.walletNonceAddress, NONCE_ACCOUNT); - assert.strictEqual(nonceAdvance.authWalletAddress, NONCE_AUTHORITY); - } - }); - }); - - describe("create account", () => { - it("should build create account instruction", () => { - // Use BitGoJS stakeAccount.pub as the new account - const NEW_ACCOUNT = STAKE_ACCOUNT; - const SYSTEM_PROGRAM = "11111111111111111111111111111111"; - - const intent: TransactionIntent = { - feePayer: SENDER, - nonce: { type: "blockhash", value: BLOCKHASH }, - instructions: [ - { - type: "createAccount", - from: SENDER, - newAccount: NEW_ACCOUNT, - lamports: 1000000n, - space: 165, - owner: SYSTEM_PROGRAM, - }, - ], - }; - - const tx = buildTransaction(intent); - const parsed = parseTransaction(tx); - - assert.strictEqual(parsed.instructionsData.length, 1); - assert.strictEqual(parsed.instructionsData[0].type, "CreateAccount"); - - const createAccount = parsed.instructionsData[0]; - if (createAccount.type === "CreateAccount") { - // Parser uses fromAddress/newAddress/amount/space/owner - assert.strictEqual(createAccount.fromAddress, SENDER); - assert.strictEqual(createAccount.newAddress, NEW_ACCOUNT); - assert.strictEqual(createAccount.amount, 1000000n); - assert.strictEqual(createAccount.space, 165n); - assert.strictEqual(createAccount.owner, SYSTEM_PROGRAM); - } - }); - }); - - describe("error handling", () => { - it("should reject invalid public key", () => { - const intent: TransactionIntent = { - feePayer: "invalid", - nonce: { type: "blockhash", value: BLOCKHASH }, - instructions: [], - }; - - assert.throws(() => buildTransaction(intent), /Invalid fee_payer/); - }); - - it("should reject invalid blockhash", () => { - const intent: TransactionIntent = { - feePayer: SENDER, - nonce: { type: "blockhash", value: "invalid" }, - instructions: [], - }; - - assert.throws(() => buildTransaction(intent), /Invalid blockhash/); - }); - - it("should reject computeBudget without unitLimit or unitPrice", () => { - const intent: TransactionIntent = { - feePayer: SENDER, - nonce: { type: "blockhash", value: BLOCKHASH }, - instructions: [{ type: "computeBudget" } as BuilderInstruction], - }; - - assert.throws(() => buildTransaction(intent), /ComputeBudget.*unitLimit.*unitPrice/); - }); - }); - - describe("roundtrip", () => { - it("should produce consistent bytes on rebuild", () => { - const intent: TransactionIntent = { - feePayer: SENDER, - nonce: { type: "blockhash", value: BLOCKHASH }, - instructions: [ - { type: "transfer", from: SENDER, to: RECIPIENT, lamports: 1000000n }, - { type: "memo", message: "Test" }, - ], - }; - - const tx1 = buildTransaction(intent); - const tx2 = buildTransaction(intent); - - // Compare serialized bytes since Transaction objects have different wasm pointers - assert.deepStrictEqual(tx1.toBytes(), tx2.toBytes()); - }); - }); - - // ===== Stake Program Tests ===== - describe("stake program", () => { - // From BitGoJS test/resources/sol.ts - const VALIDATOR = "CyjoLt3kjqB57K7ewCBHmnHq3UgEj3ak6A7m6EsBsuhA"; // validator.pub - - it("should build stake initialize instruction", () => { - const intent: TransactionIntent = { - feePayer: SENDER, - nonce: { type: "blockhash", value: BLOCKHASH }, - instructions: [ - { - type: "stakeInitialize", - stake: STAKE_ACCOUNT, - staker: SENDER, - withdrawer: SENDER, - }, - ], - }; - - const tx = buildTransaction(intent); - const parsed = parseTransaction(tx); - - assert.strictEqual(parsed.instructionsData.length, 1); - assert.strictEqual(parsed.instructionsData[0].type, "StakeInitialize"); - - const stakeInit = parsed.instructionsData[0]; - if (stakeInit.type === "StakeInitialize") { - assert.strictEqual(stakeInit.stakingAddress, STAKE_ACCOUNT); - assert.strictEqual(stakeInit.staker, SENDER); - assert.strictEqual(stakeInit.withdrawer, SENDER); - } - }); - - it("should build stake delegate instruction", () => { - const intent: TransactionIntent = { - feePayer: SENDER, - nonce: { type: "blockhash", value: BLOCKHASH }, - instructions: [ - { - type: "stakeDelegate", - stake: STAKE_ACCOUNT, - vote: VALIDATOR, - authority: SENDER, - }, - ], - }; - - const tx = buildTransaction(intent); - const parsed = parseTransaction(tx); - - assert.strictEqual(parsed.instructionsData.length, 1); - assert.strictEqual(parsed.instructionsData[0].type, "StakingDelegate"); - - const stakeDelegate = parsed.instructionsData[0]; - if (stakeDelegate.type === "StakingDelegate") { - assert.strictEqual(stakeDelegate.stakingAddress, STAKE_ACCOUNT); - assert.strictEqual(stakeDelegate.validator, VALIDATOR); - assert.strictEqual(stakeDelegate.fromAddress, SENDER); - } - }); - - it("should build stake deactivate instruction", () => { - const intent: TransactionIntent = { - feePayer: SENDER, - nonce: { type: "blockhash", value: BLOCKHASH }, - instructions: [ - { - type: "stakeDeactivate", - stake: STAKE_ACCOUNT, - authority: SENDER, - }, - ], - }; - - const tx = buildTransaction(intent); - const parsed = parseTransaction(tx); - - assert.strictEqual(parsed.instructionsData.length, 1); - assert.strictEqual(parsed.instructionsData[0].type, "StakingDeactivate"); - - const stakeDeactivate = parsed.instructionsData[0]; - if (stakeDeactivate.type === "StakingDeactivate") { - assert.strictEqual(stakeDeactivate.stakingAddress, STAKE_ACCOUNT); - assert.strictEqual(stakeDeactivate.fromAddress, SENDER); - } - }); - - it("should build stake withdraw instruction", () => { - const intent: TransactionIntent = { - feePayer: SENDER, - nonce: { type: "blockhash", value: BLOCKHASH }, - instructions: [ - { - type: "stakeWithdraw", - stake: STAKE_ACCOUNT, - recipient: RECIPIENT, - lamports: 300000n, - authority: SENDER, - }, - ], - }; - - const tx = buildTransaction(intent); - const parsed = parseTransaction(tx); - - assert.strictEqual(parsed.instructionsData.length, 1); - assert.strictEqual(parsed.instructionsData[0].type, "StakingWithdraw"); - - const stakeWithdraw = parsed.instructionsData[0]; - if (stakeWithdraw.type === "StakingWithdraw") { - assert.strictEqual(stakeWithdraw.stakingAddress, STAKE_ACCOUNT); - assert.strictEqual(stakeWithdraw.fromAddress, SENDER); - assert.strictEqual(stakeWithdraw.amount, 300000n); - } - }); - - it("should build full staking activate flow", () => { - // Typical staking activate: CreateAccount + StakeInitialize + StakeDelegate - // The parser combines these into a single StakingActivate instruction - const STAKE_PROGRAM = "Stake11111111111111111111111111111111111111"; - - const intent: TransactionIntent = { - feePayer: SENDER, - nonce: { type: "blockhash", value: BLOCKHASH }, - instructions: [ - { - type: "createAccount", - from: SENDER, - newAccount: STAKE_ACCOUNT, - lamports: 300000n, - space: 200, // Stake account size - owner: STAKE_PROGRAM, - }, - { - type: "stakeInitialize", - stake: STAKE_ACCOUNT, - staker: SENDER, - withdrawer: SENDER, - }, - { - type: "stakeDelegate", - stake: STAKE_ACCOUNT, - vote: VALIDATOR, - authority: SENDER, - }, - ], - }; - - const tx = buildTransaction(intent); - const parsed = parseTransaction(tx); - - // Parser returns individual instructions; combining is done in BitGoJS wasmInstructionCombiner - assert.strictEqual(parsed.instructionsData.length, 3); - assert.strictEqual(parsed.instructionsData[0].type, "CreateAccount"); - assert.strictEqual(parsed.instructionsData[1].type, "StakeInitialize"); - assert.strictEqual(parsed.instructionsData[2].type, "StakingDelegate"); - - // Verify CreateAccount details - const createAccount = parsed.instructionsData[0]; - if (createAccount.type === "CreateAccount") { - assert.strictEqual(createAccount.fromAddress, SENDER); - assert.strictEqual(createAccount.newAddress, STAKE_ACCOUNT); - assert.strictEqual(createAccount.amount, 300000n); - } - - // Verify StakingDelegate details - const stakeDelegate = parsed.instructionsData[2]; - if (stakeDelegate.type === "StakingDelegate") { - assert.strictEqual(stakeDelegate.stakingAddress, STAKE_ACCOUNT); - assert.strictEqual(stakeDelegate.validator, VALIDATOR); - } - }); - }); - - // ===== SPL Token Tests ===== - describe("spl token", () => { - // From BitGoJS test/resources/sol.ts - const MINT_USDC = "F4uLeXJoFz3hw13MposuwaQbMcZbCjqvEGPPeRRB1Byf"; // tokenTransfers.mintUSDC - const SOURCE_ATA = "2fyhC1YbqaYszkUQw2YGNRVkr2abr69UwFXVCjz4Q5f5"; // tokenTransfers.sourceUSDC - const DEST_ATA = "FKjSjCqByQRwSzZoMXA7bKnDbJe41YgJTHFFzBeC42bH"; - - it("should build token transfer instruction", () => { - const intent: TransactionIntent = { - feePayer: SENDER, - nonce: { type: "blockhash", value: BLOCKHASH }, - instructions: [ - { - type: "tokenTransfer", - source: SOURCE_ATA, - destination: DEST_ATA, - mint: MINT_USDC, - amount: 300000n, - decimals: 9, - authority: SENDER, - }, - ], - }; - - const tx = buildTransaction(intent); - const parsed = parseTransaction(tx); - - assert.strictEqual(parsed.instructionsData.length, 1); - assert.strictEqual(parsed.instructionsData[0].type, "TokenTransfer"); - - const tokenTransfer = parsed.instructionsData[0]; - if (tokenTransfer.type === "TokenTransfer") { - assert.strictEqual(tokenTransfer.sourceAddress, SOURCE_ATA); - assert.strictEqual(tokenTransfer.toAddress, DEST_ATA); - assert.strictEqual(tokenTransfer.amount, 300000n); - assert.strictEqual(tokenTransfer.tokenAddress, MINT_USDC); - assert.strictEqual(tokenTransfer.decimalPlaces, 9); - } - }); - - it("should build create associated token account instruction", () => { - const intent: TransactionIntent = { - feePayer: SENDER, - nonce: { type: "blockhash", value: BLOCKHASH }, - instructions: [ - { - type: "createAssociatedTokenAccount", - payer: SENDER, - owner: RECIPIENT, - mint: MINT_USDC, - }, - ], - }; - - const tx = buildTransaction(intent); - const parsed = parseTransaction(tx); - - assert.strictEqual(parsed.instructionsData.length, 1); - assert.strictEqual(parsed.instructionsData[0].type, "CreateAssociatedTokenAccount"); - - const createAta = parsed.instructionsData[0]; - if (createAta.type === "CreateAssociatedTokenAccount") { - assert.strictEqual(createAta.payerAddress, SENDER); - assert.strictEqual(createAta.ownerAddress, RECIPIENT); - assert.strictEqual(createAta.mintAddress, MINT_USDC); - } - }); - - it("should build close associated token account instruction", () => { - const intent: TransactionIntent = { - feePayer: SENDER, - nonce: { type: "blockhash", value: BLOCKHASH }, - instructions: [ - { - type: "closeAssociatedTokenAccount", - account: SOURCE_ATA, - destination: SENDER, - authority: SENDER, - }, - ], - }; - - const tx = buildTransaction(intent); - const parsed = parseTransaction(tx); - - assert.strictEqual(parsed.instructionsData.length, 1); - assert.strictEqual(parsed.instructionsData[0].type, "CloseAssociatedTokenAccount"); - - const closeAta = parsed.instructionsData[0]; - if (closeAta.type === "CloseAssociatedTokenAccount") { - assert.strictEqual(closeAta.accountAddress, SOURCE_ATA); - assert.strictEqual(closeAta.destinationAddress, SENDER); - assert.strictEqual(closeAta.authorityAddress, SENDER); - } - }); - - it("should build token transfer with create ATA", () => { - const intent: TransactionIntent = { - feePayer: SENDER, - nonce: { type: "blockhash", value: BLOCKHASH }, - instructions: [ - { - type: "createAssociatedTokenAccount", - payer: SENDER, - owner: RECIPIENT, - mint: MINT_USDC, - }, - { - type: "tokenTransfer", - source: SOURCE_ATA, - destination: DEST_ATA, - mint: MINT_USDC, - amount: 300000n, - decimals: 9, - authority: SENDER, - }, - { type: "memo", message: "test memo" }, - ], - }; - - const tx = buildTransaction(intent); - const parsed = parseTransaction(tx); - - assert.strictEqual(parsed.instructionsData.length, 3); - assert.strictEqual(parsed.instructionsData[0].type, "CreateAssociatedTokenAccount"); - assert.strictEqual(parsed.instructionsData[1].type, "TokenTransfer"); - assert.strictEqual(parsed.instructionsData[2].type, "Memo"); - }); - }); - - // ===== Jito Stake Pool Tests ===== - describe("jito stake pool", () => { - // From BitGoJS Jito constants - const JITO_STAKE_POOL = "Jito4APyf642JPZPx3hGc6WWJ8zPKtRbRs4P815Awbb"; - const JITO_WITHDRAW_AUTHORITY = "6iQKfEyhr3bZMotVkW6beNZz5CPAkiwvgV2CTje9pVSS"; - const JITO_RESERVE_STAKE = "BgKUXdS4Wy6Vdgp1jwT2dz5ZgxPG94aPL77dQscSPGmc"; - const JITO_POOL_MINT = "J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn"; // JitoSOL - const MANAGER_FEE_ACCOUNT = "5ZWgXcyqrrNpQHCme5SdC5hCeYb2o3fEJhF7Gok3bTVN"; - const VALIDATOR_LIST = "3R3nGZpQs2aZo5FDQvd2MUQ5R5E9g7NvHQaxpLPYA8r2"; - const VALIDATOR_STAKE = "BgKUXdS4Wy6Vdgp1jwT2dz5ZgxPG94aPL77dQscSPGmc"; - const DEST_STAKE = "FKjSjCqByQRwSzZoMXA7bKnDbJe41YgJTHFFzBeC42bH"; - const SOURCE_POOL_ACCOUNT = "5ZWgXcyqrrNpQHCme5SdC5hCeYb2o3fEJhF7Gok3bTVN"; - - it("should build stake pool deposit sol instruction", () => { - const intent: TransactionIntent = { - feePayer: SENDER, - nonce: { type: "blockhash", value: BLOCKHASH }, - instructions: [ - { - type: "stakePoolDepositSol", - stakePool: JITO_STAKE_POOL, - withdrawAuthority: JITO_WITHDRAW_AUTHORITY, - reserveStake: JITO_RESERVE_STAKE, - fundingAccount: SENDER, - destinationPoolAccount: SOURCE_POOL_ACCOUNT, - managerFeeAccount: MANAGER_FEE_ACCOUNT, - referralPoolAccount: MANAGER_FEE_ACCOUNT, - poolMint: JITO_POOL_MINT, - lamports: 300000n, - }, - ], - }; - - const tx = buildTransaction(intent); - const parsed = parseTransaction(tx); - - assert.strictEqual(parsed.instructionsData.length, 1); - assert.strictEqual(parsed.instructionsData[0].type, "StakePoolDepositSol"); - - const depositSol = parsed.instructionsData[0]; - if (depositSol.type === "StakePoolDepositSol") { - assert.strictEqual(depositSol.stakePool, JITO_STAKE_POOL); - assert.strictEqual(depositSol.fundingAccount, SENDER); - assert.strictEqual(depositSol.poolMint, JITO_POOL_MINT); - assert.strictEqual(depositSol.lamports, 300000n); - } - }); - - it("should build stake pool withdraw stake instruction", () => { - const intent: TransactionIntent = { - feePayer: SENDER, - nonce: { type: "blockhash", value: BLOCKHASH }, - instructions: [ - { - type: "stakePoolWithdrawStake", - stakePool: JITO_STAKE_POOL, - validatorList: VALIDATOR_LIST, - withdrawAuthority: JITO_WITHDRAW_AUTHORITY, - validatorStake: VALIDATOR_STAKE, - destinationStake: DEST_STAKE, - destinationStakeAuthority: SENDER, - sourceTransferAuthority: SENDER, - sourcePoolAccount: SOURCE_POOL_ACCOUNT, - managerFeeAccount: MANAGER_FEE_ACCOUNT, - poolMint: JITO_POOL_MINT, - poolTokens: 300000n, - }, - ], - }; - - const tx = buildTransaction(intent); - const parsed = parseTransaction(tx); - - assert.strictEqual(parsed.instructionsData.length, 1); - assert.strictEqual(parsed.instructionsData[0].type, "StakePoolWithdrawStake"); - - const withdrawStake = parsed.instructionsData[0]; - if (withdrawStake.type === "StakePoolWithdrawStake") { - assert.strictEqual(withdrawStake.stakePool, JITO_STAKE_POOL); - assert.strictEqual(withdrawStake.destinationStake, DEST_STAKE); - assert.strictEqual(withdrawStake.sourceTransferAuthority, SENDER); - assert.strictEqual(withdrawStake.poolMint, JITO_POOL_MINT); - assert.strictEqual(withdrawStake.poolTokens, 300000n); - } - }); - - it("should build jito deposit with create ATA", () => { - // Typical Jito deposit flow: Create ATA for JitoSOL + DepositSol - const intent: TransactionIntent = { - feePayer: SENDER, - nonce: { type: "blockhash", value: BLOCKHASH }, - instructions: [ - { - type: "createAssociatedTokenAccount", - payer: SENDER, - owner: SENDER, - mint: JITO_POOL_MINT, - }, - { - type: "stakePoolDepositSol", - stakePool: JITO_STAKE_POOL, - withdrawAuthority: JITO_WITHDRAW_AUTHORITY, - reserveStake: JITO_RESERVE_STAKE, - fundingAccount: SENDER, - destinationPoolAccount: SOURCE_POOL_ACCOUNT, - managerFeeAccount: MANAGER_FEE_ACCOUNT, - referralPoolAccount: MANAGER_FEE_ACCOUNT, - poolMint: JITO_POOL_MINT, - lamports: 1000000000n, // 1 SOL - }, - ], - }; - - const tx = buildTransaction(intent); - const parsed = parseTransaction(tx); - - assert.strictEqual(parsed.instructionsData.length, 2); - assert.strictEqual(parsed.instructionsData[0].type, "CreateAssociatedTokenAccount"); - assert.strictEqual(parsed.instructionsData[1].type, "StakePoolDepositSol"); - }); - }); -}); diff --git a/packages/wasm-solana/test/transaction.ts b/packages/wasm-solana/test/transaction.ts index 4964ef6..57622f6 100644 --- a/packages/wasm-solana/test/transaction.ts +++ b/packages/wasm-solana/test/transaction.ts @@ -222,148 +222,4 @@ describe("Transaction", () => { assert.deepStrictEqual(sigs[0], signature); }); }); - - describe("VersionedTransaction.fromVersionedData", () => { - it("should build versioned transaction from raw MessageV0 data", () => { - // Create minimal versioned transaction data - // Fee payer is first account - const feePayer = "2gCzKgSETrQ74HZfisZUENTLyNhV6cAgV77xDMhxmHg2"; - const data = { - staticAccountKeys: [ - feePayer, - "11111111111111111111111111111111", // system program - ], - addressLookupTables: [], - versionedInstructions: [ - { - programIdIndex: 1, - accountKeyIndexes: [0], - data: "3Bxs4ThwQbE4vyj", // base58 encoded transfer instruction data - }, - ], - messageHeader: { - numRequiredSignatures: 1, - numReadonlySignedAccounts: 0, - numReadonlyUnsignedAccounts: 1, - }, - recentBlockhash: "GHtXQBsoZHVnNFa9YevAzFr17DJjgHXk3ycTKD5xD3Zi", - }; - - const tx = VersionedTransaction.fromVersionedData(data); - - // Verify basic properties - assert.ok(tx.feePayer); // Fee payer exists - assert.strictEqual(tx.recentBlockhash, data.recentBlockhash); - assert.strictEqual(tx.numSignatures, 1); - assert.ok(tx.numInstructions > 0); - // Fee payer should be the first static account key (index 0) - assert.strictEqual(tx.feePayer, feePayer); - }); - - it("should roundtrip versioned transaction", () => { - const data = { - staticAccountKeys: [ - "2gCzKgSETrQ74HZfisZUENTLyNhV6cAgV77xDMhxmHg2", - "11111111111111111111111111111111", - ], - addressLookupTables: [], - versionedInstructions: [ - { - programIdIndex: 1, - accountKeyIndexes: [0], - data: "3Bxs4ThwQbE4vyj", - }, - ], - messageHeader: { - numRequiredSignatures: 1, - numReadonlySignedAccounts: 0, - numReadonlyUnsignedAccounts: 1, - }, - recentBlockhash: "GHtXQBsoZHVnNFa9YevAzFr17DJjgHXk3ycTKD5xD3Zi", - }; - - const tx = VersionedTransaction.fromVersionedData(data); - const bytes = tx.toBytes(); - - // Deserialize and verify using VersionedTransaction - const tx2 = VersionedTransaction.fromBytes(bytes); - assert.strictEqual(tx2.feePayer, tx.feePayer); - assert.strictEqual(tx2.recentBlockhash, tx.recentBlockhash); - }); - - it("should build versioned transaction with ALTs (Jupiter-like)", () => { - // This is extracted from a real Jupiter swap versioned transaction - // which uses Address Lookup Tables - const data = { - staticAccountKeys: [ - "35aKHPPJqb7qVNAaUb8DQLRC3Njp5RJZJSQM3v2PZhM7", - "ESuE8KSzSHBRCtgDwauL7vCR2ohxrWXf8rw75vVbNFvL", - "DWkKDVpGEVeABT4xh4SoBJzzxhSZxBuK7fWAD5LiMBui", - "4fxWJ1umh7bWbMrhrPaJcdV3EYjwm2kqPVKWHq7JcNXb", - "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", - "11111111111111111111111111111111", - "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4", - "D8cy77BBepLMngZx6ZukaTff5hCt1HrWyKk3Hnd9oitf", - "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", - "ComputeBudget111111111111111111111111111111", - "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8", - "5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1", - "srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX", - ], - addressLookupTables: [ - { - accountKey: "2immgwYNHBbyVQKVGCEkgWpi53bLwWNRMB5G2nbgYV17", - writableIndexes: [0, 16, 21, 23, 34, 45], - readonlyIndexes: [1, 4, 22, 24, 37, 53, 61, 65], - }, - { - accountKey: "6i9zbbghVBpHm6A8DqqBDDnJZ9zRLcqZVTdNkQyTpGjC", - writableIndexes: [2, 3], - readonlyIndexes: [5, 6, 7], - }, - ], - versionedInstructions: [ - { - programIdIndex: 9, - accountKeyIndexes: [], - data: "3DdGGhkhJbjm", - }, - { - programIdIndex: 9, - accountKeyIndexes: [], - data: "Fj2Eoy", - }, - { - programIdIndex: 6, - accountKeyIndexes: [7, 0, 5, 4, 10, 11, 12, 1, 2, 3, 8], - data: "2gCNTm5Pp1JgJmCK3KqDm", - }, - ], - messageHeader: { - numRequiredSignatures: 1, - numReadonlySignedAccounts: 0, - numReadonlyUnsignedAccounts: 5, - }, - recentBlockhash: "GHtXQBsoZHVnNFa9YevAzFr17DJjgHXk3ycTKD5xD3Zi", - }; - - const tx = VersionedTransaction.fromVersionedData(data); - - // Verify basic properties - assert.ok(tx.feePayer); - assert.strictEqual(tx.feePayer, data.staticAccountKeys[0]); - assert.strictEqual(tx.recentBlockhash, data.recentBlockhash); - assert.strictEqual(tx.numSignatures, 1); - assert.strictEqual(tx.numInstructions, 3); - - // Verify we can serialize and it's a valid versioned transaction - const bytes = tx.toBytes(); - assert.ok(bytes.length > 0); - - // Verify we can parse it back - const tx2 = VersionedTransaction.fromBytes(bytes); - assert.strictEqual(tx2.feePayer, tx.feePayer); - assert.strictEqual(tx2.recentBlockhash, tx.recentBlockhash); - }); - }); });