diff --git a/package-lock.json b/package-lock.json index b81c317..7649bc3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3021,7 +3021,6 @@ "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -3400,7 +3399,6 @@ "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", @@ -3631,7 +3629,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -4732,7 +4729,6 @@ "integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.47.0", "@typescript-eslint/types": "8.47.0", @@ -5208,7 +5204,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5271,7 +5266,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5923,7 +5917,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001646", "electron-to-chromium": "^1.5.4", @@ -8076,7 +8069,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8919,8 +8911,7 @@ "node_modules/fp-ts": { "version": "2.16.9", "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.9.tgz", - "integrity": "sha512-+I2+FnVB+tVaxcYyQkHUq7ZdKScaBlX53A41mxQtpIccsfyv8PzdzP7fzp2AY832T4aoK6UZ5WRX/ebGd8uZuQ==", - "peer": true + "integrity": "sha512-+I2+FnVB+tVaxcYyQkHUq7ZdKScaBlX53A41mxQtpIccsfyv8PzdzP7fzp2AY832T4aoK6UZ5WRX/ebGd8uZuQ==" }, "node_modules/fresh": { "version": "0.5.2", @@ -10362,7 +10353,6 @@ "version": "2.2.21", "resolved": "https://registry.npmjs.org/io-ts/-/io-ts-2.2.21.tgz", "integrity": "sha512-zz2Z69v9ZIC3mMLYWIeoUcwWD6f+O7yP92FMVVaXEOSZH1jnVBmET/urd/uoarD1WGBY4rCj8TAyMPzsGNzMFQ==", - "peer": true, "peerDependencies": { "fp-ts": "^2.5.0" } @@ -11861,7 +11851,6 @@ "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "dev": true, "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -12628,7 +12617,6 @@ "version": "2.3.13", "resolved": "https://registry.npmjs.org/monocle-ts/-/monocle-ts-2.3.13.tgz", "integrity": "sha512-D5Ygd3oulEoAm3KuGO0eeJIrhFf1jlQIoEVV2DYsZUMz42j4tGxgct97Aq68+F8w4w4geEnwFa8HayTS/7lpKQ==", - "peer": true, "peerDependencies": { "fp-ts": "^2.5.0" } @@ -12932,7 +12920,6 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/newtype-ts/-/newtype-ts-0.3.5.tgz", "integrity": "sha512-v83UEQMlVR75yf1OUdoSFssjitxzjZlqBAjiGQ4WJaML8Jdc68LJ+BaSAXUmKY4bNzp7hygkKLYTsDi14PxI2g==", - "peer": true, "peerDependencies": { "fp-ts": "^2.0.0", "monocle-ts": "^2.0.0" @@ -15357,7 +15344,6 @@ "dev": true, "inBundle": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -15509,7 +15495,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@napi-rs/wasm-runtime": "0.2.4", "@yarnpkg/lockfile": "^1.1.0", @@ -16816,7 +16801,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.1", @@ -17943,7 +17927,6 @@ "integrity": "sha512-6qGjWccl5yoyugHt3jTgztJ9Y0JVzyH8/Voc/D8PlLat9pwxQYXz7W1Dpnq5h0/G5GCYGUaDSlYcyk3AMh5A6g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/error": "^4.0.0", @@ -20063,7 +20046,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -20246,8 +20228,7 @@ "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/tsx": { "version": "4.20.6", @@ -20400,7 +20381,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -20769,7 +20749,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", "dev": true, - "peer": true, "dependencies": { "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", @@ -20816,7 +20795,6 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", @@ -20900,7 +20878,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -21013,7 +20990,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", diff --git a/packages/wasm-solana/Cargo.lock b/packages/wasm-solana/Cargo.lock index 6805a7b..04a2755 100644 --- a/packages/wasm-solana/Cargo.lock +++ b/packages/wasm-solana/Cargo.lock @@ -68,6 +68,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + [[package]] name = "async-trait" version = "0.1.89" @@ -85,6 +91,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.12.3" @@ -97,6 +109,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bincode" version = "1.3.3" @@ -322,6 +340,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "constant_time_eq" version = "0.4.2" @@ -343,6 +367,18 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -405,6 +441,16 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "derivation-path" version = "0.2.0" @@ -427,17 +473,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", + "const-oid", "crypto-common", "subtle", ] +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature 2.2.0", + "spki", +] + [[package]] name = "ed25519" version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" dependencies = [ - "signature", + "signature 1.6.4", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature 2.2.0", ] [[package]] @@ -447,19 +518,65 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" dependencies = [ "curve25519-dalek 3.2.0", - "ed25519", + "ed25519 1.5.3", "rand 0.7.3", "serde", "sha2 0.9.9", "zeroize", ] +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek 4.1.3", + "ed25519 2.2.3", + "rand_core 0.6.4", + "serde", + "sha2 0.10.9", + "subtle", + "zeroize", +] + +[[package]] +name = "ed25519-dalek-bip32" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b49a684b133c4980d7ee783936af771516011c8cd15f429dbda77245e282f03" +dependencies = [ + "derivation-path", + "ed25519-dalek 2.2.0", + "hmac", + "sha2 0.10.9", +] + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -472,6 +589,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -546,6 +673,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -572,6 +700,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "hashbrown" version = "0.13.2" @@ -657,6 +796,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2 0.10.9", + "signature 2.2.0", +] + [[package]] name = "keccak" version = "0.1.5" @@ -909,6 +1062,16 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "polyval" version = "0.6.2" @@ -1055,6 +1218,16 @@ dependencies = [ "bitflags", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "rustc_version" version = "0.4.1" @@ -1085,6 +1258,20 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "semver" version = "1.0.27" @@ -1210,6 +1397,22 @@ version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "smallvec" version = "1.15.1" @@ -1229,6 +1432,24 @@ dependencies = [ "solana-sdk-ids 2.2.1", ] +[[package]] +name = "solana-account" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60e0ac2a81ae17e1b3570deb50242ab4cfde50b848b898f57288b6271cc7b71f" +dependencies = [ + "bincode", + "serde", + "serde_bytes", + "serde_derive", + "solana-account-info 3.1.0", + "solana-clock 3.0.0", + "solana-instruction-error", + "solana-pubkey 4.0.0", + "solana-sdk-ids 3.1.0", + "solana-sysvar 3.1.1", +] + [[package]] name = "solana-account-info" version = "2.3.0" @@ -1248,6 +1469,8 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc3397241392f5756925029acaa8515dc70fcbe3d8059d4885d7d6533baf64fd" dependencies = [ + "bincode", + "serde_core", "solana-address 2.0.0", "solana-program-error 3.0.0", "solana-program-memory 3.1.0", @@ -1268,8 +1491,13 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e37320fd2945c5d654b2c6210624a52d66c3f1f73b653ed211ab91a703b35bdd" dependencies = [ + "borsh 1.6.0", + "bytemuck", + "bytemuck_derive", + "curve25519-dalek 4.1.3", "five8 1.0.0", "five8_const 1.0.0", + "rand 0.8.5", "serde", "serde_derive", "solana-atomic-u64 3.0.0", @@ -1296,6 +1524,18 @@ dependencies = [ "solana-slot-hashes 2.2.1", ] +[[package]] +name = "solana-address-lookup-table-interface" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e8df0b083c10ce32490410f3795016b1b5d9b4d094658c0a5e496753645b7cd" +dependencies = [ + "solana-clock 3.0.0", + "solana-pubkey 4.0.0", + "solana-sdk-ids 3.1.0", + "solana-slot-hashes 3.0.0", +] + [[package]] name = "solana-atomic-u64" version = "2.2.1" @@ -1325,6 +1565,17 @@ dependencies = [ "solana-define-syscall 2.3.0", ] +[[package]] +name = "solana-big-mod-exp" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30c80fb6d791b3925d5ec4bf23a7c169ef5090c013059ec3ed7d0b2c04efa085" +dependencies = [ + "num-bigint", + "num-traits", + "solana-define-syscall 3.0.0", +] + [[package]] name = "solana-bincode" version = "2.2.1" @@ -1348,6 +1599,17 @@ dependencies = [ "solana-sanitize 2.2.1", ] +[[package]] +name = "solana-blake3-hasher" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7116e1d942a2432ca3f514625104757ab8a56233787e95144c93950029e31176" +dependencies = [ + "blake3", + "solana-define-syscall 4.0.1", + "solana-hash 4.0.1", +] + [[package]] name = "solana-borsh" version = "2.2.1" @@ -1386,13 +1648,13 @@ dependencies = [ [[package]] name = "solana-compute-budget-interface" -version = "2.2.2" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8432d2c4c22d0499aa06d62e4f7e333f81777b3d7c96050ae9e5cb71a8c3aee4" +checksum = "8292c436b269ad23cecc8b24f7da3ab07ca111661e25e00ce0e1d22771951ab9" dependencies = [ "borsh 1.6.0", - "solana-instruction 2.3.3", - "solana-sdk-ids 2.2.1", + "solana-instruction 3.1.0", + "solana-sdk-ids 3.1.0", ] [[package]] @@ -1475,6 +1737,27 @@ dependencies = [ "uriparse", ] +[[package]] +name = "solana-derivation-path" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff71743072690fdbdfcdc37700ae1cb77485aaad49019473a81aee099b1e0b8c" +dependencies = [ + "derivation-path", + "qstring", + "uriparse", +] + +[[package]] +name = "solana-epoch-info" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e093c84f6ece620a6b10cd036574b0cd51944231ab32d81f80f76d54aba833e6" +dependencies = [ + "serde", + "serde_derive", +] + [[package]] name = "solana-epoch-rewards" version = "2.2.1" @@ -1503,6 +1786,17 @@ dependencies = [ "solana-sysvar-id 3.1.0", ] +[[package]] +name = "solana-epoch-rewards-hasher" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee8beac9bff4db9225e57d532d169b0be5e447f1e6601a2f50f27a01bf5518f" +dependencies = [ + "siphasher", + "solana-address 2.0.0", + "solana-hash 4.0.1", +] + [[package]] name = "solana-epoch-schedule" version = "2.2.1" @@ -1529,6 +1823,16 @@ dependencies = [ "solana-sysvar-id 3.1.0", ] +[[package]] +name = "solana-epoch-stake" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc6693d0ea833b880514b9b88d95afb80b42762dca98b0712465d1fcbbcb89e" +dependencies = [ + "solana-define-syscall 3.0.0", + "solana-pubkey 3.0.0", +] + [[package]] name = "solana-example-mocks" version = "2.2.1" @@ -1537,19 +1841,40 @@ checksum = "84461d56cbb8bb8d539347151e0525b53910102e4bced875d49d5139708e39d3" dependencies = [ "serde", "serde_derive", - "solana-address-lookup-table-interface", + "solana-address-lookup-table-interface 2.2.2", "solana-clock 2.2.2", "solana-hash 2.3.0", "solana-instruction 2.3.3", - "solana-keccak-hasher", + "solana-keccak-hasher 2.2.1", "solana-message 2.4.0", - "solana-nonce", + "solana-nonce 2.2.1", "solana-pubkey 2.4.0", "solana-sdk-ids 2.2.1", "solana-system-interface 1.0.0", "thiserror 2.0.18", ] +[[package]] +name = "solana-example-mocks" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978855d164845c1b0235d4b4d101cadc55373fffaf0b5b6cfa2194d25b2ed658" +dependencies = [ + "serde", + "serde_derive", + "solana-address-lookup-table-interface 3.0.1", + "solana-clock 3.0.0", + "solana-hash 3.1.0", + "solana-instruction 3.1.0", + "solana-keccak-hasher 3.1.0", + "solana-message 3.0.1", + "solana-nonce 3.0.0", + "solana-pubkey 3.0.0", + "solana-sdk-ids 3.1.0", + "solana-system-interface 2.0.0", + "thiserror 2.0.18", +] + [[package]] name = "solana-feature-gate-interface" version = "2.2.2" @@ -1559,7 +1884,7 @@ dependencies = [ "bincode", "serde", "serde_derive", - "solana-account", + "solana-account 2.2.1", "solana-account-info 2.3.0", "solana-instruction 2.3.3", "solana-program-error 2.2.2", @@ -1591,6 +1916,22 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "solana-fee-structure" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2abdb1223eea8ec64136f39cb1ffcf257e00f915c957c35c0dd9e3f4e700b0" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "solana-hard-forks" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abacc4b66ce471f135f48f22facf75cbbb0f8a252fbe2c1e0aa59d5b203f519" + [[package]] name = "solana-hash" version = "2.3.0" @@ -1624,6 +1965,7 @@ version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a5d48a6ee7b91fc7b998944ab026ed7b3e2fc8ee3bc58452644a86c2648152f" dependencies = [ + "borsh 1.6.0", "bytemuck", "bytemuck_derive", "five8 1.0.0", @@ -1634,8 +1976,18 @@ dependencies = [ ] [[package]] -name = "solana-instruction" -version = "2.3.3" +name = "solana-inflation" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e92f37a14e7c660628752833250dd3dcd8e95309876aee751d7f8769a27947c6" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "solana-instruction" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bab5682934bd1f65f8d2c16f21cb532526fcc1a09f796e2cacdb091eee5774ad" dependencies = [ @@ -1692,10 +2044,28 @@ dependencies = [ "solana-pubkey 2.4.0", "solana-sanitize 2.2.1", "solana-sdk-ids 2.2.1", - "solana-serialize-utils", + "solana-serialize-utils 2.2.1", "solana-sysvar-id 2.2.1", ] +[[package]] +name = "solana-instructions-sysvar" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ddf67876c541aa1e21ee1acae35c95c6fbc61119814bfef70579317a5e26955" +dependencies = [ + "bitflags", + "solana-account-info 3.1.0", + "solana-instruction 3.1.0", + "solana-instruction-error", + "solana-program-error 3.0.0", + "solana-pubkey 3.0.0", + "solana-sanitize 3.0.1", + "solana-sdk-ids 3.1.0", + "solana-serialize-utils 3.1.0", + "solana-sysvar-id 3.1.0", +] + [[package]] name = "solana-keccak-hasher" version = "2.2.1" @@ -1708,22 +2078,51 @@ dependencies = [ "solana-sanitize 2.2.1", ] +[[package]] +name = "solana-keccak-hasher" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed1c0d16d6fdeba12291a1f068cdf0d479d9bff1141bf44afd7aa9d485f65ef8" +dependencies = [ + "sha3", + "solana-define-syscall 4.0.1", + "solana-hash 4.0.1", +] + [[package]] name = "solana-keypair" version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd3f04aa1a05c535e93e121a95f66e7dcccf57e007282e8255535d24bf1e98bb" dependencies = [ - "ed25519-dalek", + "ed25519-dalek 1.0.1", "five8 0.2.1", "rand 0.7.3", "solana-pubkey 2.4.0", - "solana-seed-phrase", + "solana-seed-phrase 2.2.1", "solana-signature 2.3.0", "solana-signer 2.2.1", "wasm-bindgen", ] +[[package]] +name = "solana-keypair" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ac8be597c9e231b0cab2928ce3bc3e4ee77d9c0ad92977b9d901f3879f25a7a" +dependencies = [ + "ed25519-dalek 2.2.0", + "ed25519-dalek-bip32", + "five8 1.0.0", + "rand 0.8.5", + "solana-address 2.0.0", + "solana-derivation-path 3.0.0", + "solana-seed-derivable 3.0.0", + "solana-seed-phrase 3.0.0", + "solana-signature 3.1.0", + "solana-signer 3.0.0", +] + [[package]] name = "solana-last-restart-slot" version = "2.2.1" @@ -1824,6 +2223,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85666605c9fd727f865ed381665db0a8fc29f984a030ecc1e40f43bfb2541623" dependencies = [ "bincode", + "blake3", "lazy_static", "serde", "serde_derive", @@ -1860,6 +2260,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61515b880c36974053dd499c0510066783f0cc6ac17def0c7ef2a244874cf4a9" +[[package]] +name = "solana-native-token" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8dd4c280dca9d046139eb5b7a5ac9ad10403fbd64964c7d7571214950d758f" + [[package]] name = "solana-nonce" version = "2.2.1" @@ -1874,6 +2280,54 @@ dependencies = [ "solana-sha256-hasher 2.3.0", ] +[[package]] +name = "solana-nonce" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abbdc6c8caf1c08db9f36a50967539d0f72b9f1d4aea04fec5430f532e5afadc" +dependencies = [ + "solana-fee-calculator 3.0.0", + "solana-hash 3.1.0", + "solana-pubkey 3.0.0", + "solana-sha256-hasher 3.1.0", +] + +[[package]] +name = "solana-offchain-message" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e2a1141a673f72a05cf406b99e4b2b8a457792b7c01afa07b3f00d4e2de393" +dependencies = [ + "num_enum", + "solana-hash 3.1.0", + "solana-packet", + "solana-pubkey 3.0.0", + "solana-sanitize 3.0.1", + "solana-sha256-hasher 3.1.0", + "solana-signature 3.1.0", + "solana-signer 3.0.0", +] + +[[package]] +name = "solana-packet" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edf2f25743c95229ac0fdc32f8f5893ef738dbf332c669e9861d33ddb0f469d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "solana-presigner" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f704eaf825be3180832445b9e4983b875340696e8e7239bf2d535b0f86c14a2" +dependencies = [ + "solana-pubkey 3.0.0", + "solana-signature 3.1.0", + "solana-signer 3.0.0", +] + [[package]] name = "solana-program" version = "2.3.0" @@ -1900,11 +2354,11 @@ dependencies = [ "serde_bytes", "serde_derive", "solana-account-info 2.3.0", - "solana-address-lookup-table-interface", + "solana-address-lookup-table-interface 2.2.2", "solana-atomic-u64 2.2.1", - "solana-big-mod-exp", + "solana-big-mod-exp 2.2.1", "solana-bincode", - "solana-blake3-hasher", + "solana-blake3-hasher 2.2.1", "solana-borsh", "solana-clock 2.2.2", "solana-cpi 2.2.1", @@ -1912,34 +2366,34 @@ dependencies = [ "solana-define-syscall 2.3.0", "solana-epoch-rewards 2.2.1", "solana-epoch-schedule 2.2.1", - "solana-example-mocks", + "solana-example-mocks 2.2.1", "solana-feature-gate-interface", "solana-fee-calculator 2.2.1", "solana-hash 2.3.0", "solana-instruction 2.3.3", - "solana-instructions-sysvar", - "solana-keccak-hasher", + "solana-instructions-sysvar 2.2.2", + "solana-keccak-hasher 2.2.1", "solana-last-restart-slot 2.2.1", "solana-loader-v2-interface", "solana-loader-v3-interface", "solana-loader-v4-interface", "solana-message 2.4.0", "solana-msg 2.2.1", - "solana-native-token", - "solana-nonce", + "solana-native-token 2.3.0", + "solana-nonce 2.2.1", "solana-program-entrypoint 2.3.0", "solana-program-error 2.2.2", "solana-program-memory 2.3.1", - "solana-program-option", - "solana-program-pack", + "solana-program-option 2.2.1", + "solana-program-pack 2.2.1", "solana-pubkey 2.4.0", "solana-rent 2.2.1", "solana-sanitize 2.2.1", "solana-sdk-ids 2.2.1", "solana-sdk-macro 2.2.1", - "solana-secp256k1-recover", - "solana-serde-varint", - "solana-serialize-utils", + "solana-secp256k1-recover 2.2.1", + "solana-serde-varint 2.2.2", + "solana-serialize-utils 2.2.1", "solana-sha256-hasher 2.3.0", "solana-short-vec 2.2.1", "solana-slot-hashes 2.2.1", @@ -1954,6 +2408,52 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "solana-program" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91b12305dd81045d705f427acd0435a2e46444b65367d7179d7bdcfc3bc5f5eb" +dependencies = [ + "memoffset", + "solana-account-info 3.1.0", + "solana-big-mod-exp 3.0.0", + "solana-blake3-hasher 3.1.0", + "solana-clock 3.0.0", + "solana-cpi 3.1.0", + "solana-define-syscall 3.0.0", + "solana-epoch-rewards 3.0.0", + "solana-epoch-schedule 3.0.0", + "solana-epoch-stake", + "solana-example-mocks 3.0.0", + "solana-fee-calculator 3.0.0", + "solana-hash 3.1.0", + "solana-instruction 3.1.0", + "solana-instruction-error", + "solana-instructions-sysvar 3.0.0", + "solana-keccak-hasher 3.1.0", + "solana-last-restart-slot 3.0.0", + "solana-msg 3.0.0", + "solana-native-token 3.0.0", + "solana-program-entrypoint 3.1.1", + "solana-program-error 3.0.0", + "solana-program-memory 3.1.0", + "solana-program-option 3.0.0", + "solana-program-pack 3.0.0", + "solana-pubkey 3.0.0", + "solana-rent 3.1.0", + "solana-sdk-ids 3.1.0", + "solana-secp256k1-recover 3.1.0", + "solana-serde-varint 3.0.0", + "solana-serialize-utils 3.1.0", + "solana-sha256-hasher 3.1.0", + "solana-short-vec 3.1.0", + "solana-slot-hashes 3.0.0", + "solana-slot-history 3.0.0", + "solana-stable-layout 3.0.0", + "solana-sysvar 3.1.1", + "solana-sysvar-id 3.1.0", +] + [[package]] name = "solana-program-entrypoint" version = "2.3.0" @@ -1999,6 +2499,10 @@ name = "solana-program-error" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1af32c995a7b692a915bb7414d5f8e838450cf7c70414e763d8abcae7b51f28" +dependencies = [ + "serde", + "serde_derive", +] [[package]] name = "solana-program-memory" @@ -2024,6 +2528,12 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc677a2e9bc616eda6dbdab834d463372b92848b2bfe4a1ed4e4b4adba3397d0" +[[package]] +name = "solana-program-option" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e7b4ddb464f274deb4a497712664c3b612e3f5f82471d4e47710fc4ab1c3095" + [[package]] name = "solana-program-pack" version = "2.2.1" @@ -2033,6 +2543,15 @@ dependencies = [ "solana-program-error 2.2.2", ] +[[package]] +name = "solana-program-pack" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c169359de21f6034a63ebf96d6b380980307df17a8d371344ff04a883ec4e9d0" +dependencies = [ + "solana-program-error 3.0.0", +] + [[package]] name = "solana-pubkey" version = "2.4.0" @@ -2065,6 +2584,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8909d399deb0851aa524420beeb5646b115fd253ef446e35fe4504c904da3941" dependencies = [ + "rand 0.8.5", "solana-address 1.1.0", ] @@ -2115,6 +2635,44 @@ version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf09694a0fc14e5ffb18f9b7b7c0f15ecb6eac5b5610bf76a1853459d19daf9" +[[package]] +name = "solana-sdk" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f03df7969f5e723ad31b6c9eadccc209037ac4caa34d8dc259316b05c11e82b" +dependencies = [ + "bincode", + "bs58", + "serde", + "solana-account 3.3.0", + "solana-epoch-info", + "solana-epoch-rewards-hasher", + "solana-fee-structure", + "solana-inflation", + "solana-keypair 3.1.0", + "solana-message 3.0.1", + "solana-offchain-message", + "solana-presigner", + "solana-program 3.0.0", + "solana-program-memory 3.1.0", + "solana-pubkey 3.0.0", + "solana-sanitize 3.0.1", + "solana-sdk-ids 3.1.0", + "solana-sdk-macro 3.0.0", + "solana-seed-derivable 3.0.0", + "solana-seed-phrase 3.0.0", + "solana-serde", + "solana-serde-varint 3.0.0", + "solana-short-vec 3.1.0", + "solana-shred-version", + "solana-signature 3.1.0", + "solana-signer 3.0.0", + "solana-time-utils", + "solana-transaction", + "solana-transaction-error 3.0.0", + "thiserror 2.0.18", +] + [[package]] name = "solana-sdk-ids" version = "2.2.1" @@ -2168,6 +2726,17 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "solana-secp256k1-recover" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de18cfdab99eeb940fbedd8c981fa130c0d76252da75d05446f22fae8b51932" +dependencies = [ + "k256", + "solana-define-syscall 4.0.1", + "thiserror 2.0.18", +] + [[package]] name = "solana-security-txt" version = "1.1.2" @@ -2183,7 +2752,16 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3beb82b5adb266c6ea90e5cf3967235644848eac476c5a1f2f9283a143b7c97f" dependencies = [ - "solana-derivation-path", + "solana-derivation-path 2.2.1", +] + +[[package]] +name = "solana-seed-derivable" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff7bdb72758e3bec33ed0e2658a920f1f35dfb9ed576b951d20d63cb61ecd95c" +dependencies = [ + "solana-derivation-path 3.0.0", ] [[package]] @@ -2197,6 +2775,26 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "solana-seed-phrase" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc905b200a95f2ea9146e43f2a7181e3aeb55de6bc12afb36462d00a3c7310de" +dependencies = [ + "hmac", + "pbkdf2", + "sha2 0.10.9", +] + +[[package]] +name = "solana-serde" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709a93cab694c70f40b279d497639788fc2ccbcf9b4aa32273d4b361322c02dd" +dependencies = [ + "serde", +] + [[package]] name = "solana-serde-varint" version = "2.2.2" @@ -2206,6 +2804,15 @@ dependencies = [ "serde", ] +[[package]] +name = "solana-serde-varint" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e5174c57d5ff3c1995f274d17156964664566e2cde18a07bba1586d35a70d3b" +dependencies = [ + "serde", +] + [[package]] name = "solana-serialize-utils" version = "2.2.1" @@ -2217,6 +2824,17 @@ dependencies = [ "solana-sanitize 2.2.1", ] +[[package]] +name = "solana-serialize-utils" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e41dd8feea239516c623a02f0a81c2367f4b604d7965237fed0751aeec33ed" +dependencies = [ + "solana-instruction-error", + "solana-pubkey 3.0.0", + "solana-sanitize 3.0.1", +] + [[package]] name = "solana-sha256-hasher" version = "2.3.0" @@ -2257,13 +2875,24 @@ dependencies = [ "serde_core", ] +[[package]] +name = "solana-shred-version" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94953e22ca28fe4541a3447d6baeaf519cc4ddc063253bfa673b721f34c136bb" +dependencies = [ + "solana-hard-forks", + "solana-hash 3.1.0", + "solana-sha256-hasher 3.1.0", +] + [[package]] name = "solana-signature" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64c8ec8e657aecfc187522fc67495142c12f35e55ddeca8698edbb738b8dbd8c" dependencies = [ - "ed25519-dalek", + "ed25519-dalek 1.0.1", "five8 0.2.1", "solana-sanitize 2.2.1", ] @@ -2274,7 +2903,9 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bb8057cc0e9f7b5e89883d49de6f407df655bb6f3a71d0b7baf9986a2218fd9" dependencies = [ + "ed25519-dalek 2.2.0", "five8 0.2.1", + "rand 0.8.5", "serde", "serde-big-array", "serde_derive", @@ -2445,6 +3076,21 @@ dependencies = [ "solana-pubkey 3.0.0", ] +[[package]] +name = "solana-system-interface" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14591d6508042ebefb110305d3ba761615927146a26917ade45dc332d8e1ecde" +dependencies = [ + "num-traits", + "serde", + "serde_derive", + "solana-address 2.0.0", + "solana-instruction 3.1.0", + "solana-msg 3.0.0", + "solana-program-error 3.0.0", +] + [[package]] name = "solana-sysvar" version = "2.3.0" @@ -2466,7 +3112,7 @@ dependencies = [ "solana-fee-calculator 2.2.1", "solana-hash 2.3.0", "solana-instruction 2.3.3", - "solana-instructions-sysvar", + "solana-instructions-sysvar 2.2.2", "solana-last-restart-slot 2.2.1", "solana-program-entrypoint 2.3.0", "solana-program-error 2.2.2", @@ -2490,6 +3136,8 @@ checksum = "6690d3dd88f15c21edff68eb391ef8800df7a1f5cec84ee3e8d1abf05affdf74" dependencies = [ "base64 0.22.1", "bincode", + "bytemuck", + "bytemuck_derive", "lazy_static", "serde", "serde_derive", @@ -2534,6 +3182,12 @@ dependencies = [ "solana-sdk-ids 3.1.0", ] +[[package]] +name = "solana-time-utils" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced92c60aa76ec4780a9d93f3bd64dfa916e1b998eacc6f1c110f3f444f02c9" + [[package]] name = "solana-transaction" version = "3.0.2" @@ -2596,8 +3250,8 @@ dependencies = [ "solana-pubkey 2.4.0", "solana-rent 2.2.1", "solana-sdk-ids 2.2.1", - "solana-serde-varint", - "solana-serialize-utils", + "solana-serde-varint 2.2.2", + "solana-serialize-utils 2.2.1", "solana-short-vec 2.2.1", "solana-system-interface 1.0.0", ] @@ -2624,12 +3278,12 @@ dependencies = [ "serde_derive", "serde_json", "sha3", - "solana-derivation-path", + "solana-derivation-path 2.2.1", "solana-instruction 2.3.3", "solana-pubkey 2.4.0", "solana-sdk-ids 2.2.1", - "solana-seed-derivable", - "solana-seed-phrase", + "solana-seed-derivable 2.2.1", + "solana-seed-phrase 2.2.1", "solana-signature 2.3.0", "solana-signer 2.2.1", "subtle", @@ -2638,6 +3292,78 @@ dependencies = [ "zeroize", ] +[[package]] +name = "solana-zk-token-sdk" +version = "2.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5055e5df94abd5badf4f947681c893375bdb6f8f543c05d2a7ab9647a6a9d205" +dependencies = [ + "aes-gcm-siv", + "base64 0.22.1", + "bincode", + "bytemuck", + "bytemuck_derive", + "curve25519-dalek 4.1.3", + "itertools", + "merlin", + "num-derive", + "num-traits", + "rand 0.8.5", + "serde", + "serde_derive", + "serde_json", + "sha3", + "solana-curve25519", + "solana-derivation-path 2.2.1", + "solana-instruction 2.3.3", + "solana-pubkey 2.4.0", + "solana-sdk-ids 2.2.1", + "solana-seed-derivable 2.2.1", + "solana-seed-phrase 2.2.1", + "solana-signature 2.3.0", + "solana-signer 2.2.1", + "subtle", + "thiserror 2.0.18", + "zeroize", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "spl-associated-token-account" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68034596cf4804880d265f834af1ff2f821ad5293e41fa0f8f59086c181fc38e" +dependencies = [ + "assert_matches", + "borsh 1.6.0", + "num-derive", + "num-traits", + "solana-program 2.3.0", + "spl-token 6.0.0", + "spl-token-2022 4.0.1", + "thiserror 1.0.69", +] + +[[package]] +name = "spl-discriminator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a38ea8b6dedb7065887f12d62ed62c1743aa70749e8558f963609793f6fb12bc" +dependencies = [ + "bytemuck", + "solana-program 2.3.0", + "spl-discriminator-derive", +] + [[package]] name = "spl-discriminator" version = "0.4.1" @@ -2694,10 +3420,19 @@ dependencies = [ "solana-system-interface 1.0.0", "solana-sysvar 2.3.0", "solana-zk-sdk", - "spl-pod", + "spl-pod 0.5.1", "spl-token-confidential-transfer-proof-extraction", ] +[[package]] +name = "spl-memo" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0dba2f2bb6419523405d21c301a32c9f9568354d4742552e7972af801f4bdb3" +dependencies = [ + "solana-program 2.3.0", +] + [[package]] name = "spl-memo" version = "6.0.0" @@ -2712,6 +3447,20 @@ dependencies = [ "solana-pubkey 2.4.0", ] +[[package]] +name = "spl-pod" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c704c88fc457fa649ba3aabe195c79d885c3f26709efaddc453c8de352c90b87" +dependencies = [ + "borsh 1.6.0", + "bytemuck", + "bytemuck_derive", + "solana-program 2.3.0", + "solana-zk-token-sdk", + "spl-program-error 0.5.0", +] + [[package]] name = "spl-pod" version = "0.5.1" @@ -2726,12 +3475,25 @@ dependencies = [ "solana-decode-error", "solana-msg 2.2.1", "solana-program-error 2.2.2", - "solana-program-option", + "solana-program-option 2.2.1", "solana-pubkey 2.4.0", "solana-zk-sdk", "thiserror 2.0.18", ] +[[package]] +name = "spl-program-error" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7b28bed65356558133751cc32b48a7a5ddfc59ac4e941314630bbed1ac10532" +dependencies = [ + "num-derive", + "num-traits", + "solana-program 2.3.0", + "spl-program-error-derive 0.4.1", + "thiserror 1.0.69", +] + [[package]] name = "spl-program-error" version = "0.7.0" @@ -2743,10 +3505,22 @@ dependencies = [ "solana-decode-error", "solana-msg 2.2.1", "solana-program-error 2.2.2", - "spl-program-error-derive", + "spl-program-error-derive 0.5.0", "thiserror 2.0.18", ] +[[package]] +name = "spl-program-error-derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d375dd76c517836353e093c2dbb490938ff72821ab568b545fd30ab3256b3e" +dependencies = [ + "proc-macro2", + "quote", + "sha2 0.10.9", + "syn 2.0.114", +] + [[package]] name = "spl-program-error-derive" version = "0.5.0" @@ -2774,15 +3548,29 @@ dependencies = [ "num_enum", "serde", "serde_derive", - "solana-program", + "solana-program 2.3.0", "solana-security-txt", "solana-stake-interface 1.2.1", "solana-system-interface 1.0.0", - "spl-pod", - "spl-token-2022", + "spl-pod 0.5.1", + "spl-token-2022 9.0.0", "thiserror 2.0.18", ] +[[package]] +name = "spl-tlv-account-resolution" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37a75a5f0fcc58126693ed78a17042e9dc53f07e357d6be91789f7d62aff61a4" +dependencies = [ + "bytemuck", + "solana-program 2.3.0", + "spl-discriminator 0.3.0", + "spl-pod 0.3.1", + "spl-program-error 0.5.0", + "spl-type-length-value 0.5.0", +] + [[package]] name = "spl-tlv-account-resolution" version = "0.10.0" @@ -2798,13 +3586,28 @@ dependencies = [ "solana-msg 2.2.1", "solana-program-error 2.2.2", "solana-pubkey 2.4.0", - "spl-discriminator", - "spl-pod", - "spl-program-error", - "spl-type-length-value", + "spl-discriminator 0.4.1", + "spl-pod 0.5.1", + "spl-program-error 0.7.0", + "spl-type-length-value 0.8.0", "thiserror 2.0.18", ] +[[package]] +name = "spl-token" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a0f06ac7f23dc0984931b1fe309468f14ea58e32660439c1cef19456f5d0e3" +dependencies = [ + "arrayref", + "bytemuck", + "num-derive", + "num-traits", + "num_enum", + "solana-program 2.3.0", + "thiserror 1.0.69", +] + [[package]] name = "spl-token" version = "8.0.0" @@ -2824,8 +3627,8 @@ dependencies = [ "solana-program-entrypoint 2.3.0", "solana-program-error 2.2.2", "solana-program-memory 2.3.1", - "solana-program-option", - "solana-program-pack", + "solana-program-option 2.2.1", + "solana-program-pack 2.2.1", "solana-pubkey 2.4.0", "solana-rent 2.2.1", "solana-sdk-ids 2.2.1", @@ -2833,6 +3636,30 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "spl-token-2022" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33afcf7a47274725990c783a6e86330883cae26655ad6e2d910f765e530d2cef" +dependencies = [ + "arrayref", + "bytemuck", + "num-derive", + "num-traits", + "num_enum", + "solana-program 2.3.0", + "solana-security-txt", + "solana-zk-token-sdk", + "spl-memo 5.0.0", + "spl-pod 0.3.1", + "spl-token 6.0.0", + "spl-token-group-interface 0.3.0", + "spl-token-metadata-interface 0.4.0", + "spl-transfer-hook-interface 0.7.0", + "spl-type-length-value 0.5.0", + "thiserror 1.0.69", +] + [[package]] name = "spl-token-2022" version = "9.0.0" @@ -2850,12 +3677,12 @@ dependencies = [ "solana-decode-error", "solana-instruction 2.3.3", "solana-msg 2.2.1", - "solana-native-token", + "solana-native-token 2.3.0", "solana-program-entrypoint 2.3.0", "solana-program-error 2.2.2", "solana-program-memory 2.3.1", - "solana-program-option", - "solana-program-pack", + "solana-program-option 2.2.1", + "solana-program-pack 2.2.1", "solana-pubkey 2.4.0", "solana-rent 2.2.1", "solana-sdk-ids 2.2.1", @@ -2864,16 +3691,16 @@ dependencies = [ "solana-sysvar 2.3.0", "solana-zk-sdk", "spl-elgamal-registry", - "spl-memo", - "spl-pod", - "spl-token", + "spl-memo 6.0.0", + "spl-pod 0.5.1", + "spl-token 8.0.0", "spl-token-confidential-transfer-ciphertext-arithmetic", "spl-token-confidential-transfer-proof-extraction", "spl-token-confidential-transfer-proof-generation", - "spl-token-group-interface", - "spl-token-metadata-interface", - "spl-transfer-hook-interface", - "spl-type-length-value", + "spl-token-group-interface 0.6.0", + "spl-token-metadata-interface 0.7.0", + "spl-transfer-hook-interface 0.10.0", + "spl-type-length-value 0.8.0", "thiserror 2.0.18", ] @@ -2899,13 +3726,13 @@ dependencies = [ "solana-account-info 2.3.0", "solana-curve25519", "solana-instruction 2.3.3", - "solana-instructions-sysvar", + "solana-instructions-sysvar 2.2.2", "solana-msg 2.2.1", "solana-program-error 2.2.2", "solana-pubkey 2.4.0", "solana-sdk-ids 2.2.1", "solana-zk-sdk", - "spl-pod", + "spl-pod 0.5.1", "thiserror 2.0.18", ] @@ -2920,6 +3747,19 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "spl-token-group-interface" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8752b85a5ecc1d9f3a43bce3dd9a6a053673aacf5deb513d1cbb88d3534ffd" +dependencies = [ + "bytemuck", + "solana-program 2.3.0", + "spl-discriminator 0.3.0", + "spl-pod 0.3.1", + "spl-program-error 0.5.0", +] + [[package]] name = "spl-token-group-interface" version = "0.6.0" @@ -2934,11 +3774,25 @@ dependencies = [ "solana-msg 2.2.1", "solana-program-error 2.2.2", "solana-pubkey 2.4.0", - "spl-discriminator", - "spl-pod", + "spl-discriminator 0.4.1", + "spl-pod 0.5.1", "thiserror 2.0.18", ] +[[package]] +name = "spl-token-metadata-interface" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6c2318ddff97e006ed9b1291ebec0750a78547f870f62a69c56fe3b46a5d8fc" +dependencies = [ + "borsh 1.6.0", + "solana-program 2.3.0", + "spl-discriminator 0.3.0", + "spl-pod 0.3.1", + "spl-program-error 0.5.0", + "spl-type-length-value 0.5.0", +] + [[package]] name = "spl-token-metadata-interface" version = "0.7.0" @@ -2954,12 +3808,28 @@ dependencies = [ "solana-msg 2.2.1", "solana-program-error 2.2.2", "solana-pubkey 2.4.0", - "spl-discriminator", - "spl-pod", - "spl-type-length-value", + "spl-discriminator 0.4.1", + "spl-pod 0.5.1", + "spl-type-length-value 0.8.0", "thiserror 2.0.18", ] +[[package]] +name = "spl-transfer-hook-interface" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a110f33d941275d9f868b96daaa993f1e73b6806cc8836e43075b4d3ad8338a7" +dependencies = [ + "arrayref", + "bytemuck", + "solana-program 2.3.0", + "spl-discriminator 0.3.0", + "spl-pod 0.3.1", + "spl-program-error 0.5.0", + "spl-tlv-account-resolution 0.7.0", + "spl-type-length-value 0.5.0", +] + [[package]] name = "spl-transfer-hook-interface" version = "0.10.0" @@ -2977,14 +3847,27 @@ dependencies = [ "solana-msg 2.2.1", "solana-program-error 2.2.2", "solana-pubkey 2.4.0", - "spl-discriminator", - "spl-pod", - "spl-program-error", - "spl-tlv-account-resolution", - "spl-type-length-value", + "spl-discriminator 0.4.1", + "spl-pod 0.5.1", + "spl-program-error 0.7.0", + "spl-tlv-account-resolution 0.10.0", + "spl-type-length-value 0.8.0", "thiserror 2.0.18", ] +[[package]] +name = "spl-type-length-value" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdcd73ec187bc409464c60759232e309f83b52a18a9c5610bf281c9c6432918c" +dependencies = [ + "bytemuck", + "solana-program 2.3.0", + "spl-discriminator 0.3.0", + "spl-pod 0.3.1", + "spl-program-error 0.5.0", +] + [[package]] name = "spl-type-length-value" version = "0.8.0" @@ -2998,8 +3881,8 @@ dependencies = [ "solana-decode-error", "solana-msg 2.2.1", "solana-program-error 2.2.2", - "spl-discriminator", - "spl-pod", + "spl-discriminator 0.4.1", + "spl-pod 0.5.1", "thiserror 2.0.18", ] @@ -3287,14 +4170,20 @@ dependencies = [ "serde", "serde-wasm-bindgen", "serde_json", + "solana-address 1.1.0", "solana-compute-budget-interface", - "solana-keypair", + "solana-keypair 2.2.3", "solana-pubkey 2.4.0", + "solana-sdk", + "solana-signature 3.1.0", "solana-signer 2.2.1", "solana-stake-interface 2.0.2", - "solana-system-interface 2.0.0", + "solana-system-interface 3.0.0", "solana-transaction", + "spl-associated-token-account", + "spl-memo 5.0.0", "spl-stake-pool", + "spl-token 6.0.0", "wasm-bindgen", "wasm-bindgen-test", ] diff --git a/packages/wasm-solana/Cargo.toml b/packages/wasm-solana/Cargo.toml index f942172..b64ae13 100644 --- a/packages/wasm-solana/Cargo.toml +++ b/packages/wasm-solana/Cargo.toml @@ -17,21 +17,29 @@ wasm-bindgen = "0.2" js-sys = "0.3" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -# Solana SDK crates +# Solana 3.x crates (for transaction building) +solana-sdk = { version = "3.0", default-features = false, features = ["full"] } +solana-transaction = { version = "3.0", features = ["serde", "bincode"] } +solana-system-interface = { version = "3.0", features = ["bincode"] } +solana-compute-budget-interface = { version = "3.0", features = ["borsh"] } +# Solana 2.x crates (no 3.x available yet for these) +solana-stake-interface = { version = "2.0", features = ["bincode"] } solana-pubkey = { version = "2.0", features = ["curve25519"] } solana-keypair = "2.0" solana-signer = "2.0" -solana-transaction = { version = "3.0", features = ["serde", "bincode"] } -# 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"] } +solana-signature = "3.0" +solana-address = "1.0" # Serialization bincode = "1.3" borsh = "1.5" base64 = "0.22" +hex = "0.4" serde-wasm-bindgen = "0.6" +# SPL crates for token/ATA operations spl-stake-pool = { version = "2.0.3", features = ["no-entrypoint"] } +spl-token = { version = "6.0", features = ["no-entrypoint"] } +spl-associated-token-account = { version = "4.0", features = ["no-entrypoint"] } +spl-memo = { version = "5.0", features = ["no-entrypoint"] } [dev-dependencies] wasm-bindgen-test = "0.3" diff --git a/packages/wasm-solana/bitgo-wasm-solana-0.0.1.tgz b/packages/wasm-solana/bitgo-wasm-solana-0.0.1.tgz index cad565b..dbebc38 100644 Binary files a/packages/wasm-solana/bitgo-wasm-solana-0.0.1.tgz and b/packages/wasm-solana/bitgo-wasm-solana-0.0.1.tgz differ diff --git a/packages/wasm-solana/js/builder.ts b/packages/wasm-solana/js/builder.ts new file mode 100644 index 0000000..3fbc658 --- /dev/null +++ b/packages/wasm-solana/js/builder.ts @@ -0,0 +1,443 @@ +/** + * 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"; + +// ============================================================================= +// 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; + +// ============================================================================= +// 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; +} + +/** 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; + +// ============================================================================= +// 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[]; +} + +// ============================================================================= +// buildTransaction function +// ============================================================================= + +/** + * Build a Solana transaction from a high-level intent. + * + * This function takes a declarative TransactionIntent and produces serialized + * transaction bytes that can be signed and submitted to the network. + * + * The returned transaction is unsigned - signatures should be added before + * broadcasting. + * + * @param intent - The transaction intent describing what to build + * @returns Serialized unsigned transaction bytes (Uint8Array) + * @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 txBytes = buildTransaction({ + * feePayer: sender, + * nonce: { type: 'blockhash', value: blockhash }, + * instructions: [ + * { type: 'transfer', from: sender, to: recipient, lamports: '1000000' } + * ] + * }); + * + * // The returned bytes can be signed and broadcast + * ``` + * + * @example + * ```typescript + * // Build with durable nonce and priority fee + * const txBytes = 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: '1000000' }, + * { type: 'memo', message: 'BitGo transfer' } + * ] + * }); + * ``` + */ +export function buildTransaction(intent: TransactionIntent): Uint8Array { + return BuilderNamespace.build_transaction(intent); +} diff --git a/packages/wasm-solana/js/index.ts b/packages/wasm-solana/js/index.ts index 5b2e8e2..473cb28 100644 --- a/packages/wasm-solana/js/index.ts +++ b/packages/wasm-solana/js/index.ts @@ -8,6 +8,7 @@ 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"; @@ -16,6 +17,24 @@ export { Transaction } from "./transaction.js"; // Top-level function exports export { parseTransaction } from "./parser.js"; +export { buildTransaction } from "./builder.js"; + +// Program ID constants (from WASM) +export { + system_program_id as systemProgramId, + stake_program_id as stakeProgramId, + compute_budget_program_id as computeBudgetProgramId, + memo_program_id as memoProgramId, + token_program_id as tokenProgramId, + token_2022_program_id as token2022ProgramId, + ata_program_id as ataProgramId, + stake_pool_program_id as stakePoolProgramId, + stake_account_space as stakeAccountSpace, + nonce_account_space as nonceAccountSpace, + // PDA derivation functions (eliminates @solana/web3.js dependency) + get_associated_token_address as getAssociatedTokenAddress, + find_withdraw_authority_program_address as findWithdrawAuthorityProgramAddress, +} from "./wasm/wasm_solana.js"; // Type exports export type { AccountMeta, Instruction } from "./transaction.js"; @@ -44,3 +63,37 @@ export type { StakePoolWithdrawStakeParams, UnknownInstructionParams, } from "./parser.js"; + +// Builder type exports (prefixed to avoid conflict with parser/transaction types) +export type { + TransactionIntent, + NonceSource, + BlockhashNonceSource, + DurableNonceSource, + 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, +} from "./builder.js"; diff --git a/packages/wasm-solana/js/parser.ts b/packages/wasm-solana/js/parser.ts index 22f185e..f7e018b 100644 --- a/packages/wasm-solana/js/parser.ts +++ b/packages/wasm-solana/js/parser.ts @@ -105,6 +105,14 @@ export interface StakingAuthorizeParams { custodianAddress?: string; } +/** Stake initialize parameters (intermediate type) */ +export interface StakeInitializeParams { + type: "StakeInitialize"; + stakingAddress: string; + staker: string; + withdrawer: string; +} + /** Set compute unit limit parameters */ export interface SetComputeUnitLimitParams { type: "SetComputeUnitLimit"; @@ -255,6 +263,9 @@ export interface ParsedTransaction { /** All account keys (base58 strings) */ accountKeys: string[]; + + /** All signatures (base58 strings). Non-empty signatures indicate signed transaction. */ + signatures: string[]; } // ============================================================================= diff --git a/packages/wasm-solana/js/transaction.ts b/packages/wasm-solana/js/transaction.ts index a29baf8..a3b8908 100644 --- a/packages/wasm-solana/js/transaction.ts +++ b/packages/wasm-solana/js/transaction.ts @@ -88,6 +88,16 @@ export class Transaction { return this._wasm.signable_payload(); } + /** + * Serialize the message portion of the transaction. + * Alias for signablePayload() - provides compatibility with @solana/web3.js API. + * Returns a Buffer for compatibility with code expecting .toString('base64'). + * @returns The serialized message bytes as a Buffer + */ + serializeMessage(): Buffer { + return Buffer.from(this.signablePayload()); + } + /** * Serialize the transaction to bytes * @returns The serialized transaction bytes @@ -122,6 +132,43 @@ export class Transaction { return Array.from(rawInstructions) as Instruction[]; } + /** + * Add a signature for a given public key. + * + * The pubkey must be one of the required signers in the transaction. + * The signature must be exactly 64 bytes (Ed25519 signature). + * + * @param pubkey - The public key as a base58 string + * @param signature - The 64-byte signature as Uint8Array + * @throws Error if pubkey is not a signer or signature is invalid + * + * @example + * ```typescript + * // Add a pre-computed signature (e.g., from TSS) + * tx.addSignature(signerPubkey, signatureBytes); + * + * // Serialize and broadcast + * const signedTxBytes = tx.toBytes(); + * ``` + */ + addSignature(pubkey: string, signature: Uint8Array): void { + this._wasm.add_signature(pubkey, signature); + } + + /** + * Get the signer index for a public key. + * + * Returns the index in the signatures array where this pubkey's + * signature should be placed, or null if the pubkey is not a signer. + * + * @param pubkey - The public key as a base58 string + * @returns The signer index, or null if not a signer + */ + signerIndex(pubkey: string): number | null { + const idx = this._wasm.signer_index(pubkey); + return idx ?? null; + } + /** * Get the underlying WASM instance (internal use only) * @internal diff --git a/packages/wasm-solana/src/builder/build.rs b/packages/wasm-solana/src/builder/build.rs new file mode 100644 index 0000000..e0803d1 --- /dev/null +++ b/packages/wasm-solana/src/builder/build.rs @@ -0,0 +1,1509 @@ +//! 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). +pub fn build_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)) + })?, + "base64" | _ => { + 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, + }], + }; + + 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(), + }, + ], + }; + + 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, + }, + ], + }; + + 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![], + }; + + 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(), + }], + }; + + 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(), + }], + }; + + 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(), + }], + }; + + 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(), + }], + }; + + 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(), + }], + }; + + 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(), + }], + }; + + 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(), + }], + }; + + 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(), + }], + }; + + 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(), + }], + }; + + 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 + }], + }; + + 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 + }], + }; + + 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 + }], + }; + + 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 new file mode 100644 index 0000000..0325cab --- /dev/null +++ b/packages/wasm-solana/src/builder/mod.rs @@ -0,0 +1,10 @@ +//! Transaction building module. +//! +//! This module provides the `buildTransaction()` function which creates Solana +//! transactions from a high-level `TransactionIntent` structure. + +mod build; +mod types; + +pub use build::build_transaction; +pub use types::{Instruction, Nonce, TransactionIntent}; diff --git a/packages/wasm-solana/src/builder/types.rs b/packages/wasm-solana/src/builder/types.rs new file mode 100644 index 0000000..d6b927b --- /dev/null +++ b/packages/wasm-solana/src/builder/types.rs @@ -0,0 +1,365 @@ +//! 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, +} + +/// 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() +} diff --git a/packages/wasm-solana/src/instructions/decode.rs b/packages/wasm-solana/src/instructions/decode.rs index e1354d9..d57c37f 100644 --- a/packages/wasm-solana/src/instructions/decode.rs +++ b/packages/wasm-solana/src/instructions/decode.rs @@ -337,14 +337,25 @@ fn decode_token_instruction(ctx: InstructionContext) -> ParsedInstruction { fn decode_ata_instruction(ctx: InstructionContext) -> ParsedInstruction { // ATA program: Create instruction has no data (discriminator 0 or empty) - // Accounts: [0] payer, [1] ata, [2] owner, [3] mint, [4] system, [5] token - if ctx.accounts.len() >= 4 { + // Accounts: [0] payer, [1] ata, [2] owner, [3] mint, [4] system, [5] token program + // Note: We return the token program (index 5) as programId, not the ATA program, + // because BitGoJS uses programId to indicate which token program owns the ATA. + if ctx.accounts.len() >= 6 { ParsedInstruction::CreateAssociatedTokenAccount(CreateAtaParams { payer_address: ctx.accounts[0].clone(), ata_address: ctx.accounts[1].clone(), owner_address: ctx.accounts[2].clone(), mint_address: ctx.accounts[3].clone(), - program_id: ctx.program_id.to_string(), + program_id: ctx.accounts[5].clone(), // Token program, not ATA program + }) + } else if ctx.accounts.len() >= 4 { + // Fallback for transactions without token program in accounts (older format) + ParsedInstruction::CreateAssociatedTokenAccount(CreateAtaParams { + payer_address: ctx.accounts[0].clone(), + ata_address: ctx.accounts[1].clone(), + owner_address: ctx.accounts[2].clone(), + mint_address: ctx.accounts[3].clone(), + program_id: TOKEN_PROGRAM_ID.to_string(), // Default to standard token program }) } else { make_unknown(ctx) diff --git a/packages/wasm-solana/src/lib.rs b/packages/wasm-solana/src/lib.rs index d88c037..b63ec05 100644 --- a/packages/wasm-solana/src/lib.rs +++ b/packages/wasm-solana/src/lib.rs @@ -23,6 +23,7 @@ //! let pubkey = Pubkey::from_base58("FKjSjCqByQRwSzZoMXA7bKnDbJe41YgJTHFFzBeC42bH").unwrap(); //! ``` +pub mod builder; mod error; mod instructions; pub mod keypair; @@ -38,4 +39,4 @@ pub use pubkey::{Pubkey, PubkeyExt}; pub use transaction::{Transaction, TransactionExt}; // Re-export WASM types -pub use wasm::{ParserNamespace, WasmKeypair, WasmPubkey, WasmTransaction}; +pub use wasm::{BuilderNamespace, ParserNamespace, WasmKeypair, WasmPubkey, WasmTransaction}; diff --git a/packages/wasm-solana/src/parser.rs b/packages/wasm-solana/src/parser.rs index 5688220..94c58f2 100644 --- a/packages/wasm-solana/src/parser.rs +++ b/packages/wasm-solana/src/parser.rs @@ -35,6 +35,9 @@ pub struct ParsedTransaction { /// All account keys (base58 strings). pub account_keys: Vec, + + /// All signatures (base58 strings). Non-empty signatures indicate signed transaction. + pub signatures: Vec, } /// Durable nonce information for nonce-based transactions. @@ -64,7 +67,8 @@ impl TryIntoJsValue for ParsedTransaction { "nonce" => self.nonce, "durableNonce" => self.durable_nonce, "instructionsData" => self.instructions_data, - "accountKeys" => self.account_keys + "accountKeys" => self.account_keys, + "signatures" => self.signatures ) } } @@ -139,6 +143,9 @@ pub fn parse_transaction(bytes: &[u8]) -> Result { // (which is the nonce value from the nonce account) let nonce = message.recent_blockhash.to_string(); + // Extract signatures as base58 strings + let signatures: Vec = tx.signatures.iter().map(|s| s.to_string()).collect(); + Ok(ParsedTransaction { fee_payer, num_signatures: message.header.num_required_signatures, @@ -146,6 +153,7 @@ pub fn parse_transaction(bytes: &[u8]) -> Result { durable_nonce, instructions_data, account_keys, + signatures, }) } diff --git a/packages/wasm-solana/src/transaction.rs b/packages/wasm-solana/src/transaction.rs index 0684d7b..08f47d8 100644 --- a/packages/wasm-solana/src/transaction.rs +++ b/packages/wasm-solana/src/transaction.rs @@ -1,4 +1,4 @@ -//! Solana transaction deserialization. +//! Solana transaction deserialization and manipulation. //! //! Wraps `solana_transaction::Transaction` for WASM compatibility. //! @@ -8,10 +8,13 @@ //! - Signatures (variable length array) //! - Message (contains instructions, accounts, blockhash) //! -//! This module deserializes transaction bytes. Base64 encoding/decoding -//! is handled in the TypeScript layer. +//! This module deserializes transaction bytes and provides signature +//! manipulation. Base64 encoding/decoding is handled in the TypeScript layer. use crate::error::WasmSolanaError; +use solana_address::Address; +use solana_signature::Signature; +use std::str::FromStr; /// Re-export the underlying Solana Transaction type. pub use solana_transaction::Transaction; @@ -38,6 +41,15 @@ pub trait TransactionExt { /// Serialize transaction to bytes (wire format). fn to_bytes(&self) -> Result, WasmSolanaError>; + + /// Add a signature for a given public key. + /// + /// The pubkey must be one of the required signers in the transaction. + /// The signature bytes must be exactly 64 bytes (Ed25519 signature). + fn add_signature(&mut self, pubkey: &str, signature: &[u8]) -> Result<(), WasmSolanaError>; + + /// Get the index of a pubkey in the account keys, if it's a signer. + fn signer_index(&self, pubkey: &str) -> Option; } impl TransactionExt for Transaction { @@ -70,6 +82,48 @@ impl TransactionExt for Transaction { bincode::serialize(self) .map_err(|e| WasmSolanaError::new(&format!("Failed to serialize transaction: {}", e))) } + + fn signer_index(&self, pubkey: &str) -> Option { + let target_address = Address::from_str(pubkey).ok()?; + let num_signers = self.message.header.num_required_signatures as usize; + + // Use the same pattern as Solana's get_signing_keypair_positions + let signed_keys = &self.message.account_keys[0..num_signers]; + signed_keys.iter().position(|x| *x == target_address) + } + + fn add_signature( + &mut self, + pubkey: &str, + signature_bytes: &[u8], + ) -> Result<(), WasmSolanaError> { + // Validate signature length (Ed25519 signature is 64 bytes) + if signature_bytes.len() != 64 { + return Err(WasmSolanaError::new(&format!( + "Invalid signature length: expected 64 bytes, got {}", + signature_bytes.len() + ))); + } + + // Find the signer index using the same approach as Solana's get_signing_keypair_positions + let signer_idx = self + .signer_index(pubkey) + .ok_or_else(|| WasmSolanaError::new(&format!("unknown signer: {}", pubkey)))?; + + // Create signature from bytes + let signature = Signature::from(<[u8; 64]>::try_from(signature_bytes).unwrap()); + + // Ensure signatures array is properly sized (same as Solana's internal pattern) + let num_signers = self.message.header.num_required_signatures as usize; + if self.signatures.len() < num_signers { + self.signatures.resize(num_signers, Signature::default()); + } + + // Set the signature at the correct index (same pattern as try_partial_sign_unchecked) + self.signatures[signer_idx] = signature; + + Ok(()) + } } #[cfg(test)] @@ -137,4 +191,56 @@ mod tests { let result = Transaction::from_bytes(&[0, 1, 2, 3]); assert!(result.is_err()); } + + #[test] + fn test_signer_index() { + let tx = decode_test_tx(); + let fee_payer = tx.fee_payer_string().unwrap(); + + // Fee payer should be at index 0 + let idx = tx.signer_index(&fee_payer); + assert_eq!(idx, Some(0)); + + // Non-existent pubkey should return None + let fake_pubkey = "11111111111111111111111111111111"; + assert_eq!(tx.signer_index(fake_pubkey), None); + } + + #[test] + fn test_add_signature() { + let mut tx = decode_test_tx(); + let fee_payer = tx.fee_payer_string().unwrap(); + + // Create a dummy 64-byte signature + let signature = [42u8; 64]; + + // Add the signature + let result = tx.add_signature(&fee_payer, &signature); + assert!(result.is_ok()); + + // Verify the signature was added + assert_eq!(tx.signatures[0].as_ref(), &signature); + } + + #[test] + fn test_add_signature_invalid_length() { + let mut tx = decode_test_tx(); + let fee_payer = tx.fee_payer_string().unwrap(); + + // Try to add a signature with wrong length + let bad_signature = [0u8; 32]; + let result = tx.add_signature(&fee_payer, &bad_signature); + assert!(result.is_err()); + } + + #[test] + fn test_add_signature_invalid_pubkey() { + let mut tx = decode_test_tx(); + let signature = [0u8; 64]; + + // Try to add signature for non-signer pubkey + let non_signer = "11111111111111111111111111111111"; // System program + let result = tx.add_signature(non_signer, &signature); + assert!(result.is_err()); + } } diff --git a/packages/wasm-solana/src/wasm/builder.rs b/packages/wasm-solana/src/wasm/builder.rs new file mode 100644 index 0000000..c129bcf --- /dev/null +++ b/packages/wasm-solana/src/wasm/builder.rs @@ -0,0 +1,65 @@ +//! WASM binding for transaction building. +//! +//! Exposes a `buildTransaction` function that creates transactions from +//! a high-level intent structure. + +use crate::builder; +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 + /// + /// Serialized unsigned transaction bytes (Uint8Array). + /// The transaction will have empty signature placeholders that can be + /// filled in later by signing. + /// + /// @param intent - The transaction intent as a JSON object + /// @returns Serialized transaction bytes + #[wasm_bindgen] + pub fn build_transaction(intent: JsValue) -> Result, JsValue> { + // 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 + builder::build_transaction(intent).map_err(|e| JsValue::from_str(&e.to_string())) + } +} diff --git a/packages/wasm-solana/src/wasm/constants.rs b/packages/wasm-solana/src/wasm/constants.rs new file mode 100644 index 0000000..8ba0a31 --- /dev/null +++ b/packages/wasm-solana/src/wasm/constants.rs @@ -0,0 +1,140 @@ +//! Program ID constants exported via WASM. +//! +//! These constants allow JavaScript code to reference well-known Solana program IDs +//! without needing to import @solana/web3.js. + +use wasm_bindgen::prelude::*; + +// Use re-exported constants from instructions module +use crate::instructions::{ + ATA_PROGRAM_ID, COMPUTE_BUDGET_PROGRAM_ID, MEMO_PROGRAM_ID, STAKE_POOL_PROGRAM_ID, + STAKE_PROGRAM_ID, SYSTEM_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID, +}; + +/// System Program ID +#[wasm_bindgen] +pub fn system_program_id() -> String { + SYSTEM_PROGRAM_ID.to_string() +} + +/// Stake Program ID +#[wasm_bindgen] +pub fn stake_program_id() -> String { + STAKE_PROGRAM_ID.to_string() +} + +/// Compute Budget Program ID +#[wasm_bindgen] +pub fn compute_budget_program_id() -> String { + COMPUTE_BUDGET_PROGRAM_ID.to_string() +} + +/// Memo Program ID +#[wasm_bindgen] +pub fn memo_program_id() -> String { + MEMO_PROGRAM_ID.to_string() +} + +/// Token Program ID (SPL Token) +#[wasm_bindgen] +pub fn token_program_id() -> String { + TOKEN_PROGRAM_ID.to_string() +} + +/// Token 2022 Program ID +#[wasm_bindgen] +pub fn token_2022_program_id() -> String { + TOKEN_2022_PROGRAM_ID.to_string() +} + +/// Associated Token Account Program ID +#[wasm_bindgen] +pub fn ata_program_id() -> String { + ATA_PROGRAM_ID.to_string() +} + +/// Stake Pool Program ID (Jito) +#[wasm_bindgen] +pub fn stake_pool_program_id() -> String { + STAKE_POOL_PROGRAM_ID.to_string() +} + +/// Stake account space in bytes (200) +#[wasm_bindgen] +pub fn stake_account_space() -> u64 { + 200 +} + +/// Nonce account space in bytes (80) +#[wasm_bindgen] +pub fn nonce_account_space() -> u64 { + 80 +} + +/// Derive the Associated Token Account address for a given wallet and mint. +/// +/// This allows JavaScript code to compute ATA addresses without needing @solana/web3.js. +/// The ATA is a PDA derived from seeds: [wallet_address, token_program_id, mint_address] +/// +/// @param wallet_address - Owner wallet address (base58) +/// @param mint_address - Token mint address (base58) +/// @param token_program_id - Token program ID (base58), use TOKEN_PROGRAM_ID or TOKEN_2022_PROGRAM_ID +/// @returns The derived ATA address (base58) +#[wasm_bindgen] +pub fn get_associated_token_address( + wallet_address: &str, + mint_address: &str, + token_program_id: &str, +) -> Result { + use solana_sdk::pubkey::Pubkey; + + let wallet: Pubkey = wallet_address + .parse() + .map_err(|_| JsValue::from_str(&format!("Invalid wallet address: {}", wallet_address)))?; + let mint: Pubkey = mint_address + .parse() + .map_err(|_| JsValue::from_str(&format!("Invalid mint address: {}", mint_address)))?; + let token_program: Pubkey = token_program_id.parse().map_err(|_| { + JsValue::from_str(&format!("Invalid token program ID: {}", token_program_id)) + })?; + + // ATA PDA derivation: seeds = [wallet, token_program, mint], program = ATA_PROGRAM + let ata_program: Pubkey = ATA_PROGRAM_ID + .parse() + .map_err(|_| JsValue::from_str("Failed to parse ATA program ID"))?; + + let seeds = &[wallet.as_ref(), token_program.as_ref(), mint.as_ref()]; + let (ata, _bump) = Pubkey::find_program_address(seeds, &ata_program); + + Ok(ata.to_string()) +} + +/// Derive the Stake Pool withdraw authority PDA. +/// +/// This allows JavaScript code to compute the withdraw authority without needing @solana/spl-stake-pool. +/// The withdraw authority is a PDA derived from seeds: ["withdraw", stake_pool_address] +/// +/// @param stake_pool_address - Stake pool address (base58) +/// @returns The derived withdraw authority address (base58) +#[wasm_bindgen] +pub fn find_withdraw_authority_program_address( + stake_pool_address: &str, +) -> Result { + use solana_sdk::pubkey::Pubkey; + + let stake_pool: Pubkey = stake_pool_address.parse().map_err(|_| { + JsValue::from_str(&format!( + "Invalid stake pool address: {}", + stake_pool_address + )) + })?; + + let stake_pool_program: Pubkey = STAKE_POOL_PROGRAM_ID + .parse() + .map_err(|_| JsValue::from_str("Failed to parse stake pool program ID"))?; + + let seeds = &[stake_pool.as_ref(), b"withdraw".as_ref()]; + let (withdraw_authority, _bump) = Pubkey::find_program_address(seeds, &stake_pool_program); + + Ok(withdraw_authority.to_string()) +} diff --git a/packages/wasm-solana/src/wasm/mod.rs b/packages/wasm-solana/src/wasm/mod.rs index 7aa90e5..ed01a40 100644 --- a/packages/wasm-solana/src/wasm/mod.rs +++ b/packages/wasm-solana/src/wasm/mod.rs @@ -1,10 +1,16 @@ +mod builder; +mod constants; mod keypair; mod parser; mod pubkey; mod transaction; pub mod try_into_js_value; +pub use builder::BuilderNamespace; pub use keypair::WasmKeypair; pub use parser::ParserNamespace; pub use pubkey::WasmPubkey; pub use transaction::WasmTransaction; + +// Re-export constants functions +pub use constants::*; diff --git a/packages/wasm-solana/src/wasm/transaction.rs b/packages/wasm-solana/src/wasm/transaction.rs index 4d6c295..e4d4464 100644 --- a/packages/wasm-solana/src/wasm/transaction.rs +++ b/packages/wasm-solana/src/wasm/transaction.rs @@ -11,8 +11,9 @@ use wasm_bindgen::prelude::*; /// WASM wrapper for Solana transactions. /// -/// This type provides low-level access to transaction structure. -/// For high-level semantic parsing, use `ParserNamespace.parse_transaction()`. +/// This type provides low-level access to transaction structure and +/// signature manipulation. For high-level semantic parsing, use +/// `ParserNamespace.parse_transaction()`. #[wasm_bindgen] pub struct WasmTransaction { inner: Transaction, @@ -91,6 +92,27 @@ impl WasmTransaction { arr } + /// Add a signature for a given public key. + /// + /// The pubkey must be one of the required signers in the transaction. + /// The signature must be exactly 64 bytes (Ed25519 signature). + /// + /// @param pubkey - The public key as a base58 string + /// @param signature - The 64-byte signature + #[wasm_bindgen] + pub fn add_signature(&mut self, pubkey: &str, signature: &[u8]) -> Result<(), WasmSolanaError> { + self.inner.add_signature(pubkey, signature) + } + + /// Check if a public key is a required signer for this transaction. + /// + /// @param pubkey - The public key as a base58 string + /// @returns The signer index if the pubkey is a signer, null otherwise + #[wasm_bindgen] + pub fn signer_index(&self, pubkey: &str) -> Option { + self.inner.signer_index(pubkey) + } + /// Get all instructions as an array. /// /// Each instruction is a JS object with programId, accounts, and data. diff --git a/packages/wasm-solana/test/builder.ts b/packages/wasm-solana/test/builder.ts new file mode 100644 index 0000000..8aaa18f --- /dev/null +++ b/packages/wasm-solana/test/builder.ts @@ -0,0 +1,693 @@ +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 txBytes = buildTransaction(intent); + assert.ok(txBytes instanceof Uint8Array); + assert.ok(txBytes.length > 0); + + // Parse it back to verify structure + const parsed = parseTransaction(txBytes); + 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 txBytes = buildTransaction(intent); + const parsed = parseTransaction(txBytes); + + 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 txBytes = buildTransaction(intent); + const parsed = parseTransaction(txBytes); + + 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 txBytes = buildTransaction(intent); + const parsed = parseTransaction(txBytes); + + 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 txBytes = buildTransaction(intent); + const parsed = parseTransaction(txBytes); + + 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 txBytes = buildTransaction(intent); + const parsed = parseTransaction(txBytes); + + // 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 txBytes = buildTransaction(intent); + const parsed = parseTransaction(txBytes); + + 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 txBytes1 = buildTransaction(intent); + const txBytes2 = buildTransaction(intent); + + assert.deepStrictEqual(txBytes1, txBytes2); + }); + }); + + // ===== 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 txBytes = buildTransaction(intent); + const parsed = parseTransaction(txBytes); + + 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 txBytes = buildTransaction(intent); + const parsed = parseTransaction(txBytes); + + 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 txBytes = buildTransaction(intent); + const parsed = parseTransaction(txBytes); + + 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 txBytes = buildTransaction(intent); + const parsed = parseTransaction(txBytes); + + 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 txBytes = buildTransaction(intent); + const parsed = parseTransaction(txBytes); + + // 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 txBytes = buildTransaction(intent); + const parsed = parseTransaction(txBytes); + + 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 txBytes = buildTransaction(intent); + const parsed = parseTransaction(txBytes); + + 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 txBytes = buildTransaction(intent); + const parsed = parseTransaction(txBytes); + + 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 txBytes = buildTransaction(intent); + const parsed = parseTransaction(txBytes); + + 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 txBytes = buildTransaction(intent); + const parsed = parseTransaction(txBytes); + + 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 txBytes = buildTransaction(intent); + const parsed = parseTransaction(txBytes); + + 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 txBytes = buildTransaction(intent); + const parsed = parseTransaction(txBytes); + + 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 528d3a2..8730ff9 100644 --- a/packages/wasm-solana/test/transaction.ts +++ b/packages/wasm-solana/test/transaction.ts @@ -124,4 +124,78 @@ describe("Transaction", () => { // System program ID is 11111111111111111111111111111111 assert.strictEqual(instr.programId, "11111111111111111111111111111111"); }); + + describe("signerIndex", () => { + it("should return signer index for fee payer", () => { + const tx = Transaction.fromBytes(TEST_TX_BYTES); + const feePayer = tx.feePayer; + + const idx = tx.signerIndex(feePayer); + assert.strictEqual(idx, 0); // Fee payer is always at index 0 + }); + + it("should return null for non-signer pubkey", () => { + const tx = Transaction.fromBytes(TEST_TX_BYTES); + + // System program is not a signer + const idx = tx.signerIndex("11111111111111111111111111111111"); + assert.strictEqual(idx, null); + }); + }); + + describe("addSignature", () => { + it("should add signature for valid signer", () => { + const tx = Transaction.fromBytes(TEST_TX_BYTES); + const feePayer = tx.feePayer; + + // Create a dummy 64-byte signature + const signature = new Uint8Array(64).fill(42); + + // Add the signature + tx.addSignature(feePayer, signature); + + // Verify the signature was added + const sigs = tx.signatures(); + assert.strictEqual(sigs.length, 1); + assert.deepStrictEqual(sigs[0], signature); + }); + + it("should throw for invalid signature length", () => { + const tx = Transaction.fromBytes(TEST_TX_BYTES); + const feePayer = tx.feePayer; + + // Try to add a signature with wrong length + const badSignature = new Uint8Array(32); + assert.throws(() => tx.addSignature(feePayer, badSignature), /Invalid signature length/); + }); + + it("should throw for non-signer pubkey", () => { + const tx = Transaction.fromBytes(TEST_TX_BYTES); + const signature = new Uint8Array(64); + + // Try to add signature for non-signer (System program) + assert.throws( + () => tx.addSignature("11111111111111111111111111111111", signature), + /unknown signer:/, + ); + }); + + it("should roundtrip after adding signature", () => { + const tx = Transaction.fromBytes(TEST_TX_BYTES); + const feePayer = tx.feePayer; + + // Add a signature + const signature = new Uint8Array(64); + for (let i = 0; i < 64; i++) signature[i] = i; + tx.addSignature(feePayer, signature); + + // Serialize and deserialize + const bytes = tx.toBytes(); + const tx2 = Transaction.fromBytes(bytes); + + // Verify signature is preserved + const sigs = tx2.signatures(); + assert.deepStrictEqual(sigs[0], signature); + }); + }); });