diff --git a/packages/wasm-solana/Cargo.lock b/packages/wasm-solana/Cargo.lock index 51a3ab0..38fba10 100644 --- a/packages/wasm-solana/Cargo.lock +++ b/packages/wasm-solana/Cargo.lock @@ -81,12 +81,48 @@ dependencies = [ "syn", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bumpalo" version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +[[package]] +name = "bv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8834bb1d8ee5dc048ee3124f2c7c1afcc6bc9aed03f11e9dfd8c69470a5db340" +dependencies = [ + "feature-probe", + "serde", +] + +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "byteorder" version = "1.5.0" @@ -230,6 +266,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "feature-probe" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -392,6 +434,12 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + [[package]] name = "memchr" version = "2.7.6" @@ -699,6 +747,17 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "solana-account-info" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc3397241392f5756925029acaa8515dc70fcbe3d8059d4885d7d6533baf64fd" +dependencies = [ + "solana-address 2.0.0", + "solana-program-error", + "solana-program-memory", +] + [[package]] name = "solana-address" version = "1.1.0" @@ -743,6 +802,44 @@ dependencies = [ "parking_lot", ] +[[package]] +name = "solana-clock" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb62e9381182459a4520b5fe7fb22d423cae736239a6427fc398a88743d0ed59" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids 3.1.0", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-compute-budget-interface" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8432d2c4c22d0499aa06d62e4f7e333f81777b3d7c96050ae9e5cb71a8c3aee4" +dependencies = [ + "borsh", + "solana-instruction 2.3.3", + "solana-sdk-ids 2.2.1", +] + +[[package]] +name = "solana-cpi" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dea26709d867aada85d0d3617db0944215c8bb28d3745b912de7db13a23280c" +dependencies = [ + "solana-account-info", + "solana-define-syscall 4.0.1", + "solana-instruction 3.1.0", + "solana-program-error", + "solana-pubkey 4.0.0", + "solana-stable-layout", +] + [[package]] name = "solana-decode-error" version = "2.3.0" @@ -758,12 +855,56 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ae3e2abcf541c8122eafe9a625d4d194b4023c20adde1e251f94e056bb1aee2" +[[package]] +name = "solana-define-syscall" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9697086a4e102d28a156b8d6b521730335d6951bd39a5e766512bbe09007cee" + [[package]] name = "solana-define-syscall" version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57e5b1c0bc1d4a4d10c88a4100499d954c09d3fecfae4912c1a074dff68b1738" +[[package]] +name = "solana-epoch-rewards" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b319a4ed70390af911090c020571f0ff1f4ec432522d05ab89f5c08080381995" +dependencies = [ + "serde", + "serde_derive", + "solana-hash 3.1.0", + "solana-sdk-ids 3.1.0", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-epoch-schedule" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5481e72cc4d52c169db73e4c0cd16de8bc943078aac587ec4817a75cc6388f" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids 3.1.0", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-fee-calculator" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a73cc03ca4bed871ca174558108835f8323e85917bb38b9c81c7af2ab853efe" +dependencies = [ + "log", + "serde", + "serde_derive", +] + [[package]] name = "solana-hash" version = "2.3.0" @@ -792,6 +933,8 @@ version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a5d48a6ee7b91fc7b998944ab026ed7b3e2fc8ee3bc58452644a86c2648152f" dependencies = [ + "bytemuck", + "bytemuck_derive", "five8 1.0.0", "serde", "serde_derive", @@ -819,8 +962,10 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee1b699a2c1518028a9982e255e0eca10c44d90006542d9d7f9f40dbce3f7c78" dependencies = [ + "bincode", "borsh", "serde", + "serde_derive", "solana-define-syscall 4.0.1", "solana-instruction-error", "solana-pubkey 4.0.0", @@ -854,6 +999,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "solana-last-restart-slot" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcda154ec827f5fc1e4da0af3417951b7e9b8157540f81f936c4a8b1156134d0" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids 3.1.0", + "solana-sdk-macro", + "solana-sysvar-id", +] + [[package]] name = "solana-message" version = "3.0.1" @@ -868,17 +1026,47 @@ dependencies = [ "solana-hash 3.1.0", "solana-instruction 3.1.0", "solana-sanitize 3.0.1", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-short-vec", "solana-transaction-error 3.0.0", ] +[[package]] +name = "solana-msg" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "264275c556ea7e22b9d3f87d56305546a38d4eee8ec884f3b126236cb7dcbbb4" +dependencies = [ + "solana-define-syscall 3.0.0", +] + +[[package]] +name = "solana-program-entrypoint" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c9b0a1ff494e05f503a08b3d51150b73aa639544631e510279d6375f290997" +dependencies = [ + "solana-account-info", + "solana-define-syscall 4.0.1", + "solana-program-error", + "solana-pubkey 4.0.0", +] + [[package]] name = "solana-program-error" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1af32c995a7b692a915bb7414d5f8e838450cf7c70414e763d8abcae7b51f28" +[[package]] +name = "solana-program-memory" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4068648649653c2c50546e9a7fb761791b5ab0cda054c771bb5808d3a4b9eb52" +dependencies = [ + "solana-define-syscall 4.0.1", +] + [[package]] name = "solana-pubkey" version = "2.4.0" @@ -917,6 +1105,19 @@ dependencies = [ "solana-address 2.0.0", ] +[[package]] +name = "solana-rent" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e860d5499a705369778647e97d760f7670adfb6fc8419dd3d568deccd46d5487" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids 3.1.0", + "solana-sdk-macro", + "solana-sysvar-id", +] + [[package]] name = "solana-sanitize" version = "2.2.1" @@ -929,6 +1130,15 @@ version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf09694a0fc14e5ffb18f9b7b7c0f15ecb6eac5b5610bf76a1853459d19daf9" +[[package]] +name = "solana-sdk-ids" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5d8b9cc68d5c88b062a33e23a6466722467dde0035152d8fb1afbcdf350a5f" +dependencies = [ + "solana-pubkey 2.4.0", +] + [[package]] name = "solana-sdk-ids" version = "3.1.0" @@ -938,6 +1148,18 @@ dependencies = [ "solana-address 2.0.0", ] +[[package]] +name = "solana-sdk-macro" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6430000e97083460b71d9fbadc52a2ab2f88f53b3a4c5e58c5ae3640a0e8c00" +dependencies = [ + "bs58", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "solana-seed-phrase" version = "2.2.1" @@ -1026,6 +1248,117 @@ dependencies = [ "solana-transaction-error 3.0.0", ] +[[package]] +name = "solana-slot-hashes" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80a293f952293281443c04f4d96afd9d547721923d596e92b4377ed2360f1746" +dependencies = [ + "serde", + "serde_derive", + "solana-hash 3.1.0", + "solana-sdk-ids 3.1.0", + "solana-sysvar-id", +] + +[[package]] +name = "solana-slot-history" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f914f6b108f5bba14a280b458d023e3621c9973f27f015a4d755b50e88d89e97" +dependencies = [ + "bv", + "serde", + "serde_derive", + "solana-sdk-ids 3.1.0", + "solana-sysvar-id", +] + +[[package]] +name = "solana-stable-layout" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1da74507795b6e8fb60b7c7306c0c36e2c315805d16eaaf479452661234685ac" +dependencies = [ + "solana-instruction 3.1.0", + "solana-pubkey 3.0.0", +] + +[[package]] +name = "solana-stake-interface" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9bc26191b533f9a6e5a14cca05174119819ced680a80febff2f5051a713f0db" +dependencies = [ + "num-traits", + "serde", + "serde_derive", + "solana-clock", + "solana-cpi", + "solana-instruction 3.1.0", + "solana-program-error", + "solana-pubkey 3.0.0", + "solana-system-interface", + "solana-sysvar", +] + +[[package]] +name = "solana-system-interface" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e1790547bfc3061f1ee68ea9d8dc6c973c02a163697b24263a8e9f2e6d4afa2" +dependencies = [ + "num-traits", + "serde", + "serde_derive", + "solana-instruction 3.1.0", + "solana-msg", + "solana-program-error", + "solana-pubkey 3.0.0", +] + +[[package]] +name = "solana-sysvar" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6690d3dd88f15c21edff68eb391ef8800df7a1f5cec84ee3e8d1abf05affdf74" +dependencies = [ + "base64", + "bincode", + "lazy_static", + "serde", + "serde_derive", + "solana-account-info", + "solana-clock", + "solana-define-syscall 4.0.1", + "solana-epoch-rewards", + "solana-epoch-schedule", + "solana-fee-calculator", + "solana-hash 4.0.1", + "solana-instruction 3.1.0", + "solana-last-restart-slot", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-pubkey 4.0.0", + "solana-rent", + "solana-sdk-ids 3.1.0", + "solana-sdk-macro", + "solana-slot-hashes", + "solana-slot-history", + "solana-sysvar-id", +] + +[[package]] +name = "solana-sysvar-id" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17358d1e9a13e5b9c2264d301102126cf11a47fd394cdf3dec174fe7bc96e1de" +dependencies = [ + "solana-address 2.0.0", + "solana-sdk-ids 3.1.0", +] + [[package]] name = "solana-transaction" version = "3.0.2" @@ -1041,7 +1374,7 @@ dependencies = [ "solana-instruction-error", "solana-message", "solana-sanitize 3.0.1", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-short-vec", "solana-signature 3.1.0", "solana-signer 3.0.0", @@ -1087,6 +1420,21 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "toml_datetime" version = "0.7.5+spec-1.1.0" @@ -1253,13 +1601,17 @@ version = "0.1.0" dependencies = [ "base64", "bincode", + "borsh", "hex", "js-sys", "serde", "serde_json", + "solana-compute-budget-interface", "solana-keypair", "solana-pubkey 2.4.0", "solana-signer 2.2.1", + "solana-stake-interface", + "solana-system-interface", "solana-transaction", "wasm-bindgen", "wasm-bindgen-test", diff --git a/packages/wasm-solana/Cargo.toml b/packages/wasm-solana/Cargo.toml index 8db6bc8..cd2c609 100644 --- a/packages/wasm-solana/Cargo.toml +++ b/packages/wasm-solana/Cargo.toml @@ -17,8 +17,13 @@ solana-pubkey = { version = "2.0", features = ["curve25519"] } solana-keypair = "2.0" solana-signer = "2.0" solana-transaction = { version = "3.0", features = ["serde", "bincode"] } -# Serialization for transaction deserialization +# Instruction decoder interfaces (official Solana crates) +solana-system-interface = { version = "2.0", features = ["bincode"] } +solana-stake-interface = { version = "2.0", features = ["bincode"] } +solana-compute-budget-interface = { version = "2.0", features = ["borsh"] } +# Serialization for transaction and instruction deserialization bincode = "1.3" +borsh = "1.5" base64 = "0.22" [dev-dependencies] diff --git a/packages/wasm-solana/js/index.ts b/packages/wasm-solana/js/index.ts index 70c4634..d131301 100644 --- a/packages/wasm-solana/js/index.ts +++ b/packages/wasm-solana/js/index.ts @@ -7,11 +7,49 @@ void wasm; export * as keypair from "./keypair.js"; export * as pubkey from "./pubkey.js"; export * as transaction from "./transaction.js"; +export * as instructions from "./instructions.js"; // Top-level class exports for convenience export { Keypair } from "./keypair.js"; export { Pubkey } from "./pubkey.js"; export { Transaction } from "./transaction.js"; +// Instruction decoder exports +export { + // Functions + isSystemProgram, + decodeSystemInstruction, + isStakeProgram, + decodeStakeInstruction, + isComputeBudgetProgram, + decodeComputeBudgetInstruction, + // Constants + SYSTEM_PROGRAM_ID, + STAKE_PROGRAM_ID, + COMPUTE_BUDGET_PROGRAM_ID, +} from "./instructions.js"; + // Type exports export type { AccountMeta, Instruction } from "./transaction.js"; +export type { + // System instruction types (commonly used) + SystemInstruction, + SystemCreateAccount, + SystemTransfer, + SystemAdvanceNonceAccount, + SystemInitializeNonceAccount, + // Stake instruction types (commonly used) + StakeInstruction, + StakeLockup, + StakeInitialize, + StakeAuthorize, + StakeDelegateStake, + StakeSplit, + StakeWithdraw, + StakeDeactivate, + StakeMerge, + // ComputeBudget instruction types + ComputeBudgetInstruction, + ComputeBudgetSetComputeUnitLimit, + ComputeBudgetSetComputeUnitPrice, +} from "./instructions.js"; diff --git a/packages/wasm-solana/js/instructions.ts b/packages/wasm-solana/js/instructions.ts new file mode 100644 index 0000000..6182101 --- /dev/null +++ b/packages/wasm-solana/js/instructions.ts @@ -0,0 +1,207 @@ +/** + * Instruction decoders for Solana native programs. + * + * Provides decoders for: + * - System Program (transfers, account creation, nonce operations) + * - Stake Program (staking operations) + * - ComputeBudget Program (fee and compute limit settings) + * + * Note: The underlying WASM decoder supports all instruction types from official + * Solana crates. TypeScript types are provided for commonly used instructions. + */ + +import { + SystemInstructionDecoder as WasmSystemDecoder, + StakeInstructionDecoder as WasmStakeDecoder, + ComputeBudgetInstructionDecoder as WasmComputeBudgetDecoder, +} from "./wasm/wasm_solana.js"; + +// ============================================================================= +// System Instruction Types (commonly used in BitGoJS) +// ============================================================================= + +export interface SystemCreateAccount { + type: "CreateAccount"; + lamports: bigint; + space: bigint; + owner: string; +} + +export interface SystemTransfer { + type: "Transfer"; + lamports: bigint; +} + +export interface SystemAdvanceNonceAccount { + type: "AdvanceNonceAccount"; +} + +export interface SystemInitializeNonceAccount { + type: "InitializeNonceAccount"; + authorized: string; +} + +/** Union of commonly used System instruction types */ +export type SystemInstruction = + | SystemCreateAccount + | SystemTransfer + | SystemAdvanceNonceAccount + | SystemInitializeNonceAccount + | { type: string; [key: string]: unknown }; // Other instruction types + +// ============================================================================= +// Stake Instruction Types (commonly used in BitGoJS) +// ============================================================================= + +export interface StakeLockup { + unixTimestamp: bigint; + epoch: bigint; + custodian: string; +} + +export interface StakeInitialize { + type: "Initialize"; + staker: string; + withdrawer: string; + lockup: StakeLockup; +} + +export interface StakeAuthorize { + type: "Authorize"; + newAuthority: string; + stakeAuthorize: "Staker" | "Withdrawer"; +} + +export interface StakeDelegateStake { + type: "DelegateStake"; +} + +export interface StakeSplit { + type: "Split"; + lamports: bigint; +} + +export interface StakeWithdraw { + type: "Withdraw"; + lamports: bigint; +} + +export interface StakeDeactivate { + type: "Deactivate"; +} + +export interface StakeMerge { + type: "Merge"; +} + +/** Union of commonly used Stake instruction types */ +export type StakeInstruction = + | StakeInitialize + | StakeAuthorize + | StakeDelegateStake + | StakeSplit + | StakeWithdraw + | StakeDeactivate + | StakeMerge + | { type: string; [key: string]: unknown }; // Other instruction types + +// ============================================================================= +// ComputeBudget Instruction Types +// ============================================================================= + +export interface ComputeBudgetSetComputeUnitLimit { + type: "SetComputeUnitLimit"; + units: number; +} + +export interface ComputeBudgetSetComputeUnitPrice { + type: "SetComputeUnitPrice"; + microLamports: bigint; +} + +/** Union of commonly used ComputeBudget instruction types */ +export type ComputeBudgetInstruction = + | ComputeBudgetSetComputeUnitLimit + | ComputeBudgetSetComputeUnitPrice + | { type: string; [key: string]: unknown }; // Other instruction types + +// ============================================================================= +// System Instruction Decoder +// ============================================================================= + +/** System Program ID */ +export const SYSTEM_PROGRAM_ID = "11111111111111111111111111111111"; + +/** + * Check if a program ID is the System Program + * @param programId - The program ID to check (base58 string) + */ +export function isSystemProgram(programId: string): boolean { + return WasmSystemDecoder.is_system_program(programId); +} + +/** + * Decode a System program instruction from raw bytes. + * Supports all System instruction types via official Solana crates. + * @param data - The instruction data bytes + * @returns The decoded instruction with type discriminant + * @throws Error if the instruction cannot be decoded + */ +export function decodeSystemInstruction(data: Uint8Array): SystemInstruction { + return WasmSystemDecoder.decode(data) as SystemInstruction; +} + +// ============================================================================= +// Stake Instruction Decoder +// ============================================================================= + +/** Stake Program ID */ +export const STAKE_PROGRAM_ID = "Stake11111111111111111111111111111111111111"; + +/** + * Check if a program ID is the Stake Program + * @param programId - The program ID to check (base58 string) + */ +export function isStakeProgram(programId: string): boolean { + return WasmStakeDecoder.is_stake_program(programId); +} + +/** + * Decode a Stake program instruction from raw bytes. + * Supports all Stake instruction types via official Solana crates. + * @param data - The instruction data bytes + * @returns The decoded instruction with type discriminant + * @throws Error if the instruction cannot be decoded + */ +export function decodeStakeInstruction(data: Uint8Array): StakeInstruction { + return WasmStakeDecoder.decode(data) as StakeInstruction; +} + +// ============================================================================= +// ComputeBudget Instruction Decoder +// ============================================================================= + +/** ComputeBudget Program ID */ +export const COMPUTE_BUDGET_PROGRAM_ID = + "ComputeBudget111111111111111111111111111111"; + +/** + * Check if a program ID is the ComputeBudget Program + * @param programId - The program ID to check (base58 string) + */ +export function isComputeBudgetProgram(programId: string): boolean { + return WasmComputeBudgetDecoder.is_compute_budget_program(programId); +} + +/** + * Decode a ComputeBudget program instruction from raw bytes. + * Supports all ComputeBudget instruction types via official Solana crates. + * @param data - The instruction data bytes + * @returns The decoded instruction with type discriminant + * @throws Error if the instruction cannot be decoded + */ +export function decodeComputeBudgetInstruction( + data: Uint8Array +): ComputeBudgetInstruction { + return WasmComputeBudgetDecoder.decode(data) as ComputeBudgetInstruction; +} diff --git a/packages/wasm-solana/src/instructions.rs b/packages/wasm-solana/src/instructions.rs new file mode 100644 index 0000000..4db17e0 --- /dev/null +++ b/packages/wasm-solana/src/instructions.rs @@ -0,0 +1,131 @@ +//! Instruction decoders using official Solana interface crates. +//! +//! This module wraps official Solana instruction types for WASM compatibility: +//! - `solana-system-interface` for System program +//! - `solana-stake-interface` for Stake program +//! - `solana-compute-budget-interface` for ComputeBudget program + +use crate::error::WasmSolanaError; + +// Re-export official instruction types +pub use solana_compute_budget_interface::ComputeBudgetInstruction; +pub use solana_stake_interface::instruction::StakeInstruction; +pub use solana_system_interface::instruction::SystemInstruction; + +/// Program IDs as base58 strings +pub const SYSTEM_PROGRAM_ID: &str = "11111111111111111111111111111111"; +pub const STAKE_PROGRAM_ID: &str = "Stake11111111111111111111111111111111111111"; +pub const COMPUTE_BUDGET_PROGRAM_ID: &str = "ComputeBudget111111111111111111111111111111"; + +/// Decode a System program instruction from raw bytes. +pub fn decode_system_instruction(data: &[u8]) -> Result { + bincode::deserialize(data) + .map_err(|e| WasmSolanaError::new(&format!("Failed to decode System instruction: {}", e))) +} + +/// Decode a Stake program instruction from raw bytes. +pub fn decode_stake_instruction(data: &[u8]) -> Result { + bincode::deserialize(data) + .map_err(|e| WasmSolanaError::new(&format!("Failed to decode Stake instruction: {}", e))) +} + +/// Decode a ComputeBudget program instruction from raw bytes. +pub fn decode_compute_budget_instruction( + data: &[u8], +) -> Result { + use borsh::BorshDeserialize; + ComputeBudgetInstruction::try_from_slice(data) + .map_err(|e| WasmSolanaError::new(&format!("Failed to decode ComputeBudget instruction: {}", e))) +} + +/// Check if a program ID is the System program. +pub fn is_system_program(program_id: &str) -> bool { + program_id == SYSTEM_PROGRAM_ID +} + +/// Check if a program ID is the Stake program. +pub fn is_stake_program(program_id: &str) -> bool { + program_id == STAKE_PROGRAM_ID +} + +/// Check if a program ID is the ComputeBudget program. +pub fn is_compute_budget_program(program_id: &str) -> bool { + program_id == COMPUTE_BUDGET_PROGRAM_ID +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_decode_system_transfer() { + // Transfer 100000 lamports (discriminator 2 + u64 lamports) + let data = [ + 2, 0, 0, 0, // discriminator = 2 (Transfer) + 160, 134, 1, 0, 0, 0, 0, 0, // lamports = 100000 + ]; + + let instr = decode_system_instruction(&data).unwrap(); + match instr { + SystemInstruction::Transfer { lamports } => { + assert_eq!(lamports, 100000); + } + _ => panic!("Expected Transfer instruction"), + } + } + + #[test] + fn test_decode_system_advance_nonce() { + let data = [4, 0, 0, 0]; // discriminator = 4 (AdvanceNonceAccount) + let instr = decode_system_instruction(&data).unwrap(); + assert!(matches!(instr, SystemInstruction::AdvanceNonceAccount)); + } + + #[test] + fn test_decode_stake_delegate() { + let data = [2, 0, 0, 0]; // discriminator = 2 (DelegateStake) + let instr = decode_stake_instruction(&data).unwrap(); + assert!(matches!(instr, StakeInstruction::DelegateStake)); + } + + #[test] + fn test_decode_stake_deactivate() { + let data = [5, 0, 0, 0]; // discriminator = 5 (Deactivate) + let instr = decode_stake_instruction(&data).unwrap(); + assert!(matches!(instr, StakeInstruction::Deactivate)); + } + + #[test] + fn test_decode_compute_budget_set_limit() { + let data = [ + 2, // discriminator = 2 (SetComputeUnitLimit) + 64, 66, 15, 0, // units = 1000000 + ]; + let instr = decode_compute_budget_instruction(&data).unwrap(); + assert!(matches!( + instr, + ComputeBudgetInstruction::SetComputeUnitLimit(1000000) + )); + } + + #[test] + fn test_decode_compute_budget_set_price() { + let data = [ + 3, // discriminator = 3 (SetComputeUnitPrice) + 232, 3, 0, 0, 0, 0, 0, 0, // micro_lamports = 1000 + ]; + let instr = decode_compute_budget_instruction(&data).unwrap(); + assert!(matches!( + instr, + ComputeBudgetInstruction::SetComputeUnitPrice(1000) + )); + } + + #[test] + fn test_program_id_checks() { + assert!(is_system_program(SYSTEM_PROGRAM_ID)); + assert!(!is_system_program(STAKE_PROGRAM_ID)); + assert!(is_stake_program(STAKE_PROGRAM_ID)); + assert!(is_compute_budget_program(COMPUTE_BUDGET_PROGRAM_ID)); + } +} diff --git a/packages/wasm-solana/src/lib.rs b/packages/wasm-solana/src/lib.rs index 3822c77..6d3a57b 100644 --- a/packages/wasm-solana/src/lib.rs +++ b/packages/wasm-solana/src/lib.rs @@ -24,6 +24,7 @@ //! ``` mod error; +pub mod instructions; pub mod keypair; pub mod pubkey; pub mod transaction; @@ -31,6 +32,12 @@ pub mod wasm; // Re-export core types at crate root pub use error::WasmSolanaError; +pub use instructions::{ + decode_compute_budget_instruction, decode_stake_instruction, decode_system_instruction, + is_compute_budget_program, is_stake_program, is_system_program, ComputeBudgetInstruction, + StakeInstruction, SystemInstruction, COMPUTE_BUDGET_PROGRAM_ID, STAKE_PROGRAM_ID, + SYSTEM_PROGRAM_ID, +}; pub use keypair::{Keypair, KeypairExt}; pub use pubkey::{Pubkey, PubkeyExt}; pub use transaction::{Transaction, TransactionExt}; diff --git a/packages/wasm-solana/src/wasm/instructions.rs b/packages/wasm-solana/src/wasm/instructions.rs new file mode 100644 index 0000000..c139f4b --- /dev/null +++ b/packages/wasm-solana/src/wasm/instructions.rs @@ -0,0 +1,362 @@ +//! WASM bindings for instruction decoders. +//! +//! Provides JavaScript-friendly interfaces for decoding Solana program instructions +//! using official Solana interface crates. + +use crate::error::WasmSolanaError; +use crate::instructions::{ + decode_compute_budget_instruction, decode_stake_instruction, decode_system_instruction, + is_compute_budget_program, is_stake_program, is_system_program, ComputeBudgetInstruction, + StakeInstruction, SystemInstruction, COMPUTE_BUDGET_PROGRAM_ID, STAKE_PROGRAM_ID, + SYSTEM_PROGRAM_ID, +}; +use wasm_bindgen::prelude::*; + +// ============================================================================= +// System Instruction WASM Bindings +// ============================================================================= + +/// WASM namespace for System Program instruction decoding. +#[wasm_bindgen] +pub struct SystemInstructionDecoder; + +#[wasm_bindgen] +impl SystemInstructionDecoder { + /// The System Program ID as a base58 string. + #[wasm_bindgen(getter)] + pub fn program_id() -> String { + SYSTEM_PROGRAM_ID.to_string() + } + + /// Check if the given program ID is the System Program. + #[wasm_bindgen] + pub fn is_system_program(program_id: &str) -> bool { + is_system_program(program_id) + } + + /// Decode a System instruction from raw bytes. + /// + /// Returns a JS object with: + /// - `type`: string (e.g., "Transfer", "CreateAccount") + /// - Additional fields depending on the instruction type + #[wasm_bindgen] + pub fn decode(data: &[u8]) -> Result { + let instr = decode_system_instruction(data)?; + let obj = js_sys::Object::new(); + + // Set the instruction type and fields based on variant + match instr { + SystemInstruction::CreateAccount { + lamports, + space, + owner, + } => { + set_string(&obj, "type", "CreateAccount")?; + set_u64(&obj, "lamports", lamports)?; + set_u64(&obj, "space", space)?; + set_string(&obj, "owner", &owner.to_string())?; + } + SystemInstruction::Assign { owner } => { + set_string(&obj, "type", "Assign")?; + set_string(&obj, "owner", &owner.to_string())?; + } + SystemInstruction::Transfer { lamports } => { + set_string(&obj, "type", "Transfer")?; + set_u64(&obj, "lamports", lamports)?; + } + SystemInstruction::CreateAccountWithSeed { + base, + seed, + lamports, + space, + owner, + } => { + set_string(&obj, "type", "CreateAccountWithSeed")?; + set_string(&obj, "base", &base.to_string())?; + set_string(&obj, "seed", &seed)?; + set_u64(&obj, "lamports", lamports)?; + set_u64(&obj, "space", space)?; + set_string(&obj, "owner", &owner.to_string())?; + } + SystemInstruction::AdvanceNonceAccount => { + set_string(&obj, "type", "AdvanceNonceAccount")?; + } + SystemInstruction::WithdrawNonceAccount(lamports) => { + set_string(&obj, "type", "WithdrawNonceAccount")?; + set_u64(&obj, "lamports", lamports)?; + } + SystemInstruction::InitializeNonceAccount(authority) => { + set_string(&obj, "type", "InitializeNonceAccount")?; + set_string(&obj, "authorized", &authority.to_string())?; + } + SystemInstruction::AuthorizeNonceAccount(authority) => { + set_string(&obj, "type", "AuthorizeNonceAccount")?; + set_string(&obj, "authorized", &authority.to_string())?; + } + SystemInstruction::Allocate { space } => { + set_string(&obj, "type", "Allocate")?; + set_u64(&obj, "space", space)?; + } + SystemInstruction::AllocateWithSeed { + base, + seed, + space, + owner, + } => { + set_string(&obj, "type", "AllocateWithSeed")?; + set_string(&obj, "base", &base.to_string())?; + set_string(&obj, "seed", &seed)?; + set_u64(&obj, "space", space)?; + set_string(&obj, "owner", &owner.to_string())?; + } + SystemInstruction::AssignWithSeed { base, seed, owner } => { + set_string(&obj, "type", "AssignWithSeed")?; + set_string(&obj, "base", &base.to_string())?; + set_string(&obj, "seed", &seed)?; + set_string(&obj, "owner", &owner.to_string())?; + } + SystemInstruction::TransferWithSeed { + lamports, + from_seed, + from_owner, + } => { + set_string(&obj, "type", "TransferWithSeed")?; + set_u64(&obj, "lamports", lamports)?; + set_string(&obj, "fromSeed", &from_seed)?; + set_string(&obj, "fromOwner", &from_owner.to_string())?; + } + SystemInstruction::UpgradeNonceAccount => { + set_string(&obj, "type", "UpgradeNonceAccount")?; + } + } + + Ok(obj) + } +} + +// ============================================================================= +// Stake Instruction WASM Bindings +// ============================================================================= + +/// WASM namespace for Stake Program instruction decoding. +#[wasm_bindgen] +pub struct StakeInstructionDecoder; + +#[wasm_bindgen] +impl StakeInstructionDecoder { + /// The Stake Program ID as a base58 string. + #[wasm_bindgen(getter)] + pub fn program_id() -> String { + STAKE_PROGRAM_ID.to_string() + } + + /// Check if the given program ID is the Stake Program. + #[wasm_bindgen] + pub fn is_stake_program(program_id: &str) -> bool { + is_stake_program(program_id) + } + + /// Decode a Stake instruction from raw bytes. + #[wasm_bindgen] + pub fn decode(data: &[u8]) -> Result { + let instr = decode_stake_instruction(data)?; + let obj = js_sys::Object::new(); + + match instr { + StakeInstruction::Initialize(authorized, lockup) => { + set_string(&obj, "type", "Initialize")?; + set_string(&obj, "staker", &authorized.staker.to_string())?; + set_string(&obj, "withdrawer", &authorized.withdrawer.to_string())?; + // Add lockup info + let lockup_obj = js_sys::Object::new(); + set_i64(&lockup_obj, "unixTimestamp", lockup.unix_timestamp)?; + set_u64(&lockup_obj, "epoch", lockup.epoch)?; + set_string(&lockup_obj, "custodian", &lockup.custodian.to_string())?; + js_sys::Reflect::set(&obj, &"lockup".into(), &lockup_obj) + .map_err(|_| WasmSolanaError::new("Failed to set lockup"))?; + } + StakeInstruction::Authorize(new_authority, stake_authorize) => { + set_string(&obj, "type", "Authorize")?; + set_string(&obj, "newAuthority", &new_authority.to_string())?; + let auth_type = match stake_authorize { + solana_stake_interface::state::StakeAuthorize::Staker => "Staker", + solana_stake_interface::state::StakeAuthorize::Withdrawer => "Withdrawer", + }; + set_string(&obj, "stakeAuthorize", auth_type)?; + } + StakeInstruction::DelegateStake => { + set_string(&obj, "type", "DelegateStake")?; + } + StakeInstruction::Split(lamports) => { + set_string(&obj, "type", "Split")?; + set_u64(&obj, "lamports", lamports)?; + } + StakeInstruction::Withdraw(lamports) => { + set_string(&obj, "type", "Withdraw")?; + set_u64(&obj, "lamports", lamports)?; + } + StakeInstruction::Deactivate => { + set_string(&obj, "type", "Deactivate")?; + } + StakeInstruction::SetLockup(lockup_args) => { + set_string(&obj, "type", "SetLockup")?; + if let Some(ts) = lockup_args.unix_timestamp { + set_i64(&obj, "unixTimestamp", ts)?; + } + if let Some(e) = lockup_args.epoch { + set_u64(&obj, "epoch", e)?; + } + if let Some(c) = lockup_args.custodian { + set_string(&obj, "custodian", &c.to_string())?; + } + } + StakeInstruction::Merge => { + set_string(&obj, "type", "Merge")?; + } + StakeInstruction::AuthorizeWithSeed(args) => { + set_string(&obj, "type", "AuthorizeWithSeed")?; + set_string(&obj, "newAuthority", &args.new_authorized_pubkey.to_string())?; + let auth_type = match args.stake_authorize { + solana_stake_interface::state::StakeAuthorize::Staker => "Staker", + solana_stake_interface::state::StakeAuthorize::Withdrawer => "Withdrawer", + }; + set_string(&obj, "stakeAuthorize", auth_type)?; + set_string(&obj, "authoritySeed", &args.authority_seed)?; + set_string(&obj, "authorityOwner", &args.authority_owner.to_string())?; + } + StakeInstruction::InitializeChecked => { + set_string(&obj, "type", "InitializeChecked")?; + } + StakeInstruction::AuthorizeChecked(stake_authorize) => { + set_string(&obj, "type", "AuthorizeChecked")?; + let auth_type = match stake_authorize { + solana_stake_interface::state::StakeAuthorize::Staker => "Staker", + solana_stake_interface::state::StakeAuthorize::Withdrawer => "Withdrawer", + }; + set_string(&obj, "stakeAuthorize", auth_type)?; + } + StakeInstruction::AuthorizeCheckedWithSeed(args) => { + set_string(&obj, "type", "AuthorizeCheckedWithSeed")?; + let auth_type = match args.stake_authorize { + solana_stake_interface::state::StakeAuthorize::Staker => "Staker", + solana_stake_interface::state::StakeAuthorize::Withdrawer => "Withdrawer", + }; + set_string(&obj, "stakeAuthorize", auth_type)?; + set_string(&obj, "authoritySeed", &args.authority_seed)?; + set_string(&obj, "authorityOwner", &args.authority_owner.to_string())?; + } + StakeInstruction::SetLockupChecked(lockup_args) => { + set_string(&obj, "type", "SetLockupChecked")?; + if let Some(ts) = lockup_args.unix_timestamp { + set_i64(&obj, "unixTimestamp", ts)?; + } + if let Some(e) = lockup_args.epoch { + set_u64(&obj, "epoch", e)?; + } + } + StakeInstruction::GetMinimumDelegation => { + set_string(&obj, "type", "GetMinimumDelegation")?; + } + StakeInstruction::DeactivateDelinquent => { + set_string(&obj, "type", "DeactivateDelinquent")?; + } + StakeInstruction::Redelegate => { + set_string(&obj, "type", "Redelegate")?; + } + StakeInstruction::MoveStake(lamports) => { + set_string(&obj, "type", "MoveStake")?; + set_u64(&obj, "lamports", lamports)?; + } + StakeInstruction::MoveLamports(lamports) => { + set_string(&obj, "type", "MoveLamports")?; + set_u64(&obj, "lamports", lamports)?; + } + } + + Ok(obj) + } +} + +// ============================================================================= +// ComputeBudget Instruction WASM Bindings +// ============================================================================= + +/// WASM namespace for ComputeBudget Program instruction decoding. +#[wasm_bindgen] +pub struct ComputeBudgetInstructionDecoder; + +#[wasm_bindgen] +impl ComputeBudgetInstructionDecoder { + /// The ComputeBudget Program ID as a base58 string. + #[wasm_bindgen(getter)] + pub fn program_id() -> String { + COMPUTE_BUDGET_PROGRAM_ID.to_string() + } + + /// Check if the given program ID is the ComputeBudget Program. + #[wasm_bindgen] + pub fn is_compute_budget_program(program_id: &str) -> bool { + is_compute_budget_program(program_id) + } + + /// Decode a ComputeBudget instruction from raw bytes. + #[wasm_bindgen] + pub fn decode(data: &[u8]) -> Result { + let instr = decode_compute_budget_instruction(data)?; + let obj = js_sys::Object::new(); + + match instr { + ComputeBudgetInstruction::Unused => { + set_string(&obj, "type", "Unused")?; + } + ComputeBudgetInstruction::RequestHeapFrame(bytes) => { + set_string(&obj, "type", "RequestHeapFrame")?; + set_u32(&obj, "bytes", bytes)?; + } + ComputeBudgetInstruction::SetComputeUnitLimit(units) => { + set_string(&obj, "type", "SetComputeUnitLimit")?; + set_u32(&obj, "units", units)?; + } + ComputeBudgetInstruction::SetComputeUnitPrice(micro_lamports) => { + set_string(&obj, "type", "SetComputeUnitPrice")?; + set_u64(&obj, "microLamports", micro_lamports)?; + } + ComputeBudgetInstruction::SetLoadedAccountsDataSizeLimit(bytes) => { + set_string(&obj, "type", "SetLoadedAccountsDataSizeLimit")?; + set_u32(&obj, "bytes", bytes)?; + } + } + + Ok(obj) + } +} + +// ============================================================================= +// Helper Functions +// ============================================================================= + +fn set_string(obj: &js_sys::Object, key: &str, value: &str) -> Result<(), WasmSolanaError> { + js_sys::Reflect::set(obj, &key.into(), &value.into()) + .map_err(|_| WasmSolanaError::new(&format!("Failed to set {}", key)))?; + Ok(()) +} + +fn set_u32(obj: &js_sys::Object, key: &str, value: u32) -> Result<(), WasmSolanaError> { + js_sys::Reflect::set(obj, &key.into(), &JsValue::from(value)) + .map_err(|_| WasmSolanaError::new(&format!("Failed to set {}", key)))?; + Ok(()) +} + +fn set_u64(obj: &js_sys::Object, key: &str, value: u64) -> Result<(), WasmSolanaError> { + // Use BigInt for u64 to preserve precision + js_sys::Reflect::set(obj, &key.into(), &js_sys::BigInt::from(value).into()) + .map_err(|_| WasmSolanaError::new(&format!("Failed to set {}", key)))?; + Ok(()) +} + +fn set_i64(obj: &js_sys::Object, key: &str, value: i64) -> Result<(), WasmSolanaError> { + // Use BigInt for i64 to preserve precision + js_sys::Reflect::set(obj, &key.into(), &js_sys::BigInt::from(value).into()) + .map_err(|_| WasmSolanaError::new(&format!("Failed to set {}", key)))?; + Ok(()) +} diff --git a/packages/wasm-solana/src/wasm/mod.rs b/packages/wasm-solana/src/wasm/mod.rs index 99aa9d4..8db8f92 100644 --- a/packages/wasm-solana/src/wasm/mod.rs +++ b/packages/wasm-solana/src/wasm/mod.rs @@ -1,7 +1,11 @@ +mod instructions; mod keypair; mod pubkey; mod transaction; +pub use instructions::{ + ComputeBudgetInstructionDecoder, StakeInstructionDecoder, SystemInstructionDecoder, +}; pub use keypair::WasmKeypair; pub use pubkey::WasmPubkey; pub use transaction::WasmTransaction; diff --git a/packages/wasm-solana/test/instructions.ts b/packages/wasm-solana/test/instructions.ts new file mode 100644 index 0000000..e697838 --- /dev/null +++ b/packages/wasm-solana/test/instructions.ts @@ -0,0 +1,206 @@ +import * as assert from "assert"; +import { + decodeSystemInstruction, + decodeStakeInstruction, + decodeComputeBudgetInstruction, + isSystemProgram, + isStakeProgram, + isComputeBudgetProgram, + SYSTEM_PROGRAM_ID, + STAKE_PROGRAM_ID, + COMPUTE_BUDGET_PROGRAM_ID, +} from "../js/instructions.js"; +import { Transaction } from "../js/transaction.js"; + +describe("Instruction Decoders", () => { + describe("Program ID Constants", () => { + it("should have correct System Program ID", () => { + assert.strictEqual(SYSTEM_PROGRAM_ID, "11111111111111111111111111111111"); + }); + + it("should have correct Stake Program ID", () => { + assert.strictEqual( + STAKE_PROGRAM_ID, + "Stake11111111111111111111111111111111111111" + ); + }); + + it("should have correct ComputeBudget Program ID", () => { + assert.strictEqual( + COMPUTE_BUDGET_PROGRAM_ID, + "ComputeBudget111111111111111111111111111111" + ); + }); + }); + + describe("Program ID Checks", () => { + it("should identify System Program", () => { + assert.ok(isSystemProgram(SYSTEM_PROGRAM_ID)); + assert.ok(!isSystemProgram(STAKE_PROGRAM_ID)); + assert.ok(!isSystemProgram(COMPUTE_BUDGET_PROGRAM_ID)); + assert.ok(!isSystemProgram("SomeOtherProgram")); + }); + + it("should identify Stake Program", () => { + assert.ok(isStakeProgram(STAKE_PROGRAM_ID)); + assert.ok(!isStakeProgram(SYSTEM_PROGRAM_ID)); + assert.ok(!isStakeProgram(COMPUTE_BUDGET_PROGRAM_ID)); + }); + + it("should identify ComputeBudget Program", () => { + assert.ok(isComputeBudgetProgram(COMPUTE_BUDGET_PROGRAM_ID)); + assert.ok(!isComputeBudgetProgram(SYSTEM_PROGRAM_ID)); + assert.ok(!isComputeBudgetProgram(STAKE_PROGRAM_ID)); + }); + }); + + describe("System Instruction Decoder", () => { + it("should decode Transfer instruction", () => { + // Transfer 100000 lamports + // discriminator 2 (u32 little-endian) + lamports (u64 little-endian) + const data = new Uint8Array([ + 2, 0, 0, 0, // discriminator = 2 (Transfer) + 160, 134, 1, 0, 0, 0, 0, 0, // lamports = 100000 + ]); + + const instr = decodeSystemInstruction(data); + + assert.strictEqual(instr.type, "Transfer"); + if (instr.type === "Transfer") { + assert.strictEqual(instr.lamports, BigInt(100000)); + } + }); + + it("should decode AdvanceNonceAccount instruction", () => { + const data = new Uint8Array([4, 0, 0, 0]); // discriminator = 4 + + const instr = decodeSystemInstruction(data); + assert.strictEqual(instr.type, "AdvanceNonceAccount"); + }); + + it("should throw on invalid System instruction data", () => { + const invalidData = new Uint8Array([255, 255, 255, 255]); + assert.throws(() => decodeSystemInstruction(invalidData), /Failed to decode/); + }); + }); + + describe("Stake Instruction Decoder", () => { + it("should decode DelegateStake instruction", () => { + const data = new Uint8Array([2, 0, 0, 0]); // discriminator = 2 + + const instr = decodeStakeInstruction(data); + assert.strictEqual(instr.type, "DelegateStake"); + }); + + it("should decode Deactivate instruction", () => { + const data = new Uint8Array([5, 0, 0, 0]); // discriminator = 5 + + const instr = decodeStakeInstruction(data); + assert.strictEqual(instr.type, "Deactivate"); + }); + + it("should decode Split instruction", () => { + // Split with 500000 lamports + const data = new Uint8Array([ + 3, 0, 0, 0, // discriminator = 3 (Split) + 32, 161, 7, 0, 0, 0, 0, 0, // lamports = 500000 + ]); + + const instr = decodeStakeInstruction(data); + + assert.strictEqual(instr.type, "Split"); + if (instr.type === "Split") { + assert.strictEqual(instr.lamports, BigInt(500000)); + } + }); + + it("should decode Withdraw instruction", () => { + // Withdraw with 200000 lamports + const data = new Uint8Array([ + 4, 0, 0, 0, // discriminator = 4 (Withdraw) + 64, 13, 3, 0, 0, 0, 0, 0, // lamports = 200000 + ]); + + const instr = decodeStakeInstruction(data); + + assert.strictEqual(instr.type, "Withdraw"); + if (instr.type === "Withdraw") { + assert.strictEqual(instr.lamports, BigInt(200000)); + } + }); + + it("should decode Merge instruction", () => { + const data = new Uint8Array([7, 0, 0, 0]); // discriminator = 7 + + const instr = decodeStakeInstruction(data); + assert.strictEqual(instr.type, "Merge"); + }); + + it("should throw on invalid Stake instruction data", () => { + const invalidData = new Uint8Array([255, 255, 255, 255]); + assert.throws(() => decodeStakeInstruction(invalidData), /Failed to decode/); + }); + }); + + describe("ComputeBudget Instruction Decoder", () => { + it("should decode SetComputeUnitLimit instruction", () => { + // SetComputeUnitLimit with 1000000 units + const data = new Uint8Array([ + 2, // discriminator = 2 + 64, 66, 15, 0, // units = 1000000 (u32) + ]); + + const instr = decodeComputeBudgetInstruction(data); + + assert.strictEqual(instr.type, "SetComputeUnitLimit"); + if (instr.type === "SetComputeUnitLimit") { + assert.strictEqual(instr.units, 1000000); + } + }); + + it("should decode SetComputeUnitPrice instruction", () => { + // SetComputeUnitPrice with 1000 micro-lamports + const data = new Uint8Array([ + 3, // discriminator = 3 + 232, 3, 0, 0, 0, 0, 0, 0, // microLamports = 1000 (u64) + ]); + + const instr = decodeComputeBudgetInstruction(data); + + assert.strictEqual(instr.type, "SetComputeUnitPrice"); + if (instr.type === "SetComputeUnitPrice") { + assert.strictEqual(instr.microLamports, BigInt(1000)); + } + }); + + it("should throw on invalid ComputeBudget instruction data", () => { + const invalidData = new Uint8Array([255, 255, 255, 255]); + assert.throws( + () => decodeComputeBudgetInstruction(invalidData), + /Failed to decode/ + ); + }); + }); + + describe("Integration with Transaction", () => { + it("should decode System Transfer from real transaction", () => { + // This is a real SOL transfer transaction + const TEST_TX_BASE64 = + "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDFVMqpim7tqEi2XL8R6KKkP0DYJvY3eiRXLlL1P9EjYgXKQC+k0FKnqyC4AZGJR7OhJXfpPP3NHOhS8t/6G7bLAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/1c7Oaj3RbyLIjU0/ZPpsmVfVUWAzc8g36fK5g6A0JoBAgIAAQwCAAAAoIYBAAAAAAA="; + + const tx = Transaction.fromBase64(TEST_TX_BASE64); + const instr = tx.instructionAt(0); + + assert.ok(instr); + assert.strictEqual(instr.programId, SYSTEM_PROGRAM_ID); + assert.ok(isSystemProgram(instr.programId)); + + // Decode the instruction data + const decoded = decodeSystemInstruction(instr.data); + assert.strictEqual(decoded.type, "Transfer"); + if (decoded.type === "Transfer") { + assert.strictEqual(decoded.lamports, BigInt(100000)); + } + }); + }); +});