diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 7a405983..3b752351 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -73,13 +73,13 @@ jobs: - name: Run Program Scope Tests with nextest run: | - cargo nextest run --config-file nextest.toml --profile ci --all --workspace --no-fail-fast --features=program_scope_test + cargo nextest run --config-file nextest.toml --profile ci --all --workspace --no-fail-fast --exclude test-program-authority --features=program_scope_test mkdir -p target/nextest/reports cp target/nextest/ci/output.xml target/nextest/reports/program-scope-tests.xml - name: Run Rust SDK Tests with nextest run: | - cargo nextest run --config-file nextest.toml --profile ci --all --workspace --no-fail-fast --features=rust_sdk_test,program_scope_test + cargo nextest run --config-file nextest.toml --profile ci --all --workspace --no-fail-fast --exclude test-program-authority --features=rust_sdk_test,program_scope_test mkdir -p target/nextest/reports cp target/nextest/ci/output.xml target/nextest/reports/rust-sdk-tests.xml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index a4a485bf..d28edf8b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -15,7 +15,7 @@ env: jobs: lint: - runs-on: ubuntu-latest + runs-on: ubuntu-latest-m steps: - uses: actions/checkout@v4 diff --git a/Cargo.lock b/Cargo.lock index bedd21ae..0c3567ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,6 +89,22 @@ dependencies = [ "solana-secp256r1-program", ] +[[package]] +name = "agave-transaction-view" +version = "2.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6249a9fb672efff152c578ee561597b52a8eaab9cc6fe7c3ad7ba44359f83ae" +dependencies = [ + "solana-hash", + "solana-message", + "solana-packet", + "solana-pubkey", + "solana-sdk-ids", + "solana-short-vec", + "solana-signature", + "solana-svm-transaction", +] + [[package]] name = "ahash" version = "0.8.11" @@ -573,6 +589,20 @@ version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +[[package]] +name = "aquamarine" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f50776554130342de4836ba542aa85a4ddb361690d7e8df13774d7284c3d5c2" +dependencies = [ + "include_dir", + "itertools 0.10.5", + "proc-macro-error2", + "proc-macro2 1.0.94", + "quote 1.0.40", + "syn 2.0.100", +] + [[package]] name = "arbitrary" version = "1.4.1" @@ -819,6 +849,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + [[package]] name = "async-channel" version = "1.9.0" @@ -1000,6 +1036,15 @@ dependencies = [ "serde", ] +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + [[package]] name = "bitvec" version = "1.0.1" @@ -1211,6 +1256,26 @@ dependencies = [ "serde", ] +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "c-kzg" version = "2.1.0" @@ -1299,7 +1364,16 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.1.1", +] + +[[package]] +name = "chrono-humanize" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799627e6b4d27827a814e837b9d8a504832086081806d45b1afa34dc982b023b" +dependencies = [ + "chrono", ] [[package]] @@ -1707,6 +1781,7 @@ dependencies = [ "lock_api", "once_cell", "parking_lot_core", + "rayon", ] [[package]] @@ -1842,6 +1917,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "digest" version = "0.9.0" @@ -1863,6 +1944,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "dir-diff" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7ad16bf5f84253b50d6557681c58c3ab67c47c77d39fed9aeb56e947290bd10" +dependencies = [ + "walkdir", +] + [[package]] name = "directories" version = "5.0.1" @@ -1947,6 +2037,12 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + [[package]] name = "dunce" version = "1.0.5" @@ -2009,6 +2105,18 @@ dependencies = [ "sha2 0.10.8", ] +[[package]] +name = "educe" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0042ff8246a363dbe77d2ceedb073339e85a804b9a47636c6e016a9a32c05f" +dependencies = [ + "enum-ordinalize", + "proc-macro2 1.0.94", + "quote 1.0.40", + "syn 1.0.109", +] + [[package]] name = "either" version = "1.15.0" @@ -2073,6 +2181,19 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "enum-ordinalize" +version = "3.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf1fa3f06bbff1ea5b1a9c7b14aa992a39657db60a2759457328d7e058f49ee" +dependencies = [ + "num-bigint 0.4.6", + "num-traits", + "proc-macro2 1.0.94", + "quote 1.0.40", + "syn 2.0.100", +] + [[package]] name = "env_filter" version = "0.1.3" @@ -2200,6 +2321,18 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + [[package]] name = "five8_const" version = "0.1.4" @@ -2237,6 +2370,15 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + [[package]] name = "fnv" version = "1.0.7" @@ -2273,6 +2415,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fragile" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" + [[package]] name = "funty" version = "2.0.0" @@ -2499,7 +2647,7 @@ dependencies = [ "indexmap 2.9.0", "slab", "tokio", - "tokio-util", + "tokio-util 0.7.14", "tracing", ] @@ -2552,6 +2700,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -2874,6 +3028,22 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "im" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9" +dependencies = [ + "bitmaps", + "rand_core 0.6.4", + "rand_xoshiro", + "rayon", + "serde", + "sized-chunks", + "typenum", + "version_check", +] + [[package]] name = "impl-codec" version = "0.6.0" @@ -2894,6 +3064,31 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2 1.0.94", + "quote 1.0.40", +] + +[[package]] +name = "index_list" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30141a73bc8a129ac1ce472e33f45af3e2091d86b3479061b9c2f92fdbe9a28c" + [[package]] name = "indexmap" version = "1.9.3" @@ -3099,6 +3294,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.9.0", "libc", + "redox_syscall", ] [[package]] @@ -3316,6 +3512,25 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "lz4" +version = "1.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a20b523e860d03443e98350ceaac5e71c6ba89aea7d960769ec3ce37f4de5af4" +dependencies = [ + "lz4-sys", +] + +[[package]] +name = "lz4-sys" +version = "1.11.1+lz4-1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "macro-string" version = "0.1.4" @@ -3414,6 +3629,54 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "mockall" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c84490118f2ee2d74570d114f3d0493cbf02790df303d2707606c3e14e07c96" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "lazy_static", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ce75669015c4f47b289fd4d4f56e894e4c96003ffdf3ac51313126f94c6cbb" +dependencies = [ + "cfg-if", + "proc-macro2 1.0.94", + "quote 1.0.40", + "syn 1.0.109", +] + +[[package]] +name = "modular-bitfield" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a53d79ba8304ac1c4f9eb3b9d281f21f7be9d4626f72ce7df4ad8fbde4f38a74" +dependencies = [ + "modular-bitfield-impl", + "static_assertions", +] + +[[package]] +name = "modular-bitfield-impl" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a7d5f7076603ebc68de2dc6a650ec331a062a13abaa346975be747bbfa4b789" +dependencies = [ + "proc-macro2 1.0.94", + "quote 1.0.40", + "syn 1.0.109", +] + [[package]] name = "murmur3" version = "0.5.2" @@ -3464,6 +3727,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -3712,6 +3981,25 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "opentelemetry" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6105e89802af13fdf48c49d7646d3b533a70e536d818aae7e78ba0433d01acb8" +dependencies = [ + "async-trait", + "crossbeam-channel", + "futures-channel", + "futures-executor", + "futures-util", + "js-sys", + "lazy_static", + "percent-encoding", + "pin-project", + "rand 0.8.5", + "thiserror 1.0.69", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -3831,6 +4119,26 @@ dependencies = [ "ucd-trie", ] +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2 1.0.94", + "quote 1.0.40", + "syn 2.0.100", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -3945,6 +4253,36 @@ dependencies = [ "zerocopy 0.8.24", ] +[[package]] +name = "predicates" +version = "2.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" +dependencies = [ + "difflib", + "float-cmp", + "itertools 0.10.5", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" + +[[package]] +name = "predicates-tree" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "primitive-types" version = "0.12.2" @@ -4273,6 +4611,15 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "raw-cpuid" version = "11.5.0" @@ -4400,7 +4747,7 @@ dependencies = [ "system-configuration", "tokio", "tokio-rustls", - "tokio-util", + "tokio-util 0.7.14", "tower-service", "url", "wasm-bindgen", @@ -4794,6 +5141,15 @@ dependencies = [ "pest", ] +[[package]] +name = "seqlock" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5c67b6f14ecc5b86c66fa63d76b5092352678545a8a3cdae80aef5128371910" +dependencies = [ + "parking_lot", +] + [[package]] name = "serde" version = "1.0.219" @@ -5052,16 +5408,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "signal-hook" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" -dependencies = [ - "libc", - "signal-hook-registry", -] - [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -5094,10 +5440,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] -name = "slab" -version = "0.4.9" +name = "sized-chunks" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] @@ -5168,6 +5524,55 @@ dependencies = [ "solana-pubkey", ] +[[package]] +name = "solana-accounts-db" +version = "2.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bebb1bbe676467db67aa8f05f86ef681be958e1f4e87dc949ec2996b588729d" +dependencies = [ + "ahash", + "bincode", + "blake3", + "bv", + "bytemuck", + "bytemuck_derive", + "bzip2", + "crossbeam-channel", + "dashmap", + "index_list", + "indexmap 2.9.0", + "itertools 0.12.1", + "lazy_static", + "log", + "lz4", + "memmap2", + "modular-bitfield", + "num_cpus", + "num_enum", + "rand 0.8.5", + "rayon", + "seqlock", + "serde", + "serde_derive", + "smallvec", + "solana-bucket-map", + "solana-clock", + "solana-hash", + "solana-inline-spl", + "solana-lattice-hash", + "solana-measure", + "solana-metrics", + "solana-nohash-hasher", + "solana-pubkey", + "solana-rayon-threadlimit", + "solana-sdk", + "solana-svm-transaction", + "static_assertions", + "tar", + "tempfile", + "thiserror 2.0.12", +] + [[package]] name = "solana-address-lookup-table-interface" version = "2.2.2" @@ -5219,6 +5624,57 @@ dependencies = [ "parking_lot", ] +[[package]] +name = "solana-banks-client" +version = "2.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e8b93a73f583fb03c9a43be9185c2e04c8a5df84e3c20fd813f0ff79a12142" +dependencies = [ + "borsh 1.5.7", + "futures", + "solana-banks-interface", + "solana-program", + "solana-sdk", + "tarpc", + "thiserror 2.0.12", + "tokio", + "tokio-serde", +] + +[[package]] +name = "solana-banks-interface" +version = "2.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e54bdc2f951d900289a3de58f8fc835fcea67fdaaea390b447e16a8a403a2399" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk", + "tarpc", +] + +[[package]] +name = "solana-banks-server" +version = "2.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d31f902ad3ea81a92fb48619e5d852ce7500f1aecb5adc52621ae4856cc53ef0" +dependencies = [ + "bincode", + "crossbeam-channel", + "futures", + "solana-banks-interface", + "solana-client", + "solana-feature-set", + "solana-runtime", + "solana-runtime-transaction", + "solana-sdk", + "solana-send-transaction-service", + "solana-svm", + "tarpc", + "tokio", + "tokio-serde", +] + [[package]] name = "solana-big-mod-exp" version = "2.2.1" @@ -5327,6 +5783,26 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "solana-bucket-map" +version = "2.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c11df4362edfa9e3157e37cdba2f10cafe23c550aa4038f3c3b302573937af9d" +dependencies = [ + "bv", + "bytemuck", + "bytemuck_derive", + "log", + "memmap2", + "modular-bitfield", + "num_enum", + "rand 0.8.5", + "solana-clock", + "solana-measure", + "solana-pubkey", + "tempfile", +] + [[package]] name = "solana-builtins" version = "2.2.4" @@ -5575,6 +6051,35 @@ dependencies = [ "tokio", ] +[[package]] +name = "solana-cost-model" +version = "2.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4046449f81662b0b79b3f7a5e88412ae58fd63f8ef4af472a528d522a44500a" +dependencies = [ + "ahash", + "lazy_static", + "log", + "solana-bincode", + "solana-borsh", + "solana-builtins-default-costs", + "solana-clock", + "solana-compute-budget", + "solana-compute-budget-instruction", + "solana-compute-budget-interface", + "solana-feature-set", + "solana-fee-structure", + "solana-metrics", + "solana-packet", + "solana-pubkey", + "solana-runtime-transaction", + "solana-sdk-ids", + "solana-svm-transaction", + "solana-system-interface", + "solana-transaction-error", + "solana-vote-program", +] + [[package]] name = "solana-cpi" version = "2.2.1" @@ -5938,6 +6443,18 @@ dependencies = [ "solana-sysvar-id", ] +[[package]] +name = "solana-lattice-hash" +version = "2.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "780e8609adadf99e09b08a4f45f30fedd82b29ed31a3b3a921bb811ffd1652cc" +dependencies = [ + "base64 0.22.1", + "blake3", + "bs58", + "bytemuck", +] + [[package]] name = "solana-loader-v2-interface" version = "2.2.1" @@ -6019,15 +6536,13 @@ dependencies = [ [[package]] name = "solana-logger" -version = "2.3.1" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8e777ec1afd733939b532a42492d888ec7c88d8b4127a5d867eb45c6eb5cd5" +checksum = "593dbcb81439d37b02757e90bd9ab56364de63f378c55db92a6fbd6a2e47ab36" dependencies = [ "env_logger 0.9.3", "lazy_static", - "libc", "log", - "signal-hook", ] [[package]] @@ -6114,6 +6629,12 @@ dependencies = [ "url", ] +[[package]] +name = "solana-nohash-hasher" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b8a731ed60e89177c8a7ab05fe0f1511cedd3e70e773f288f9de33a9cfdc21e" + [[package]] name = "solana-nonce" version = "2.2.1" @@ -6436,6 +6957,43 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "solana-program-test" +version = "2.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15cedbd823f64af662551bb801687ea98067bbddf06e0394876a40485542667" +dependencies = [ + "assert_matches", + "async-trait", + "base64 0.22.1", + "bincode", + "chrono-humanize", + "crossbeam-channel", + "log", + "serde", + "solana-accounts-db", + "solana-banks-client", + "solana-banks-interface", + "solana-banks-server", + "solana-bpf-loader-program", + "solana-compute-budget", + "solana-feature-set", + "solana-inline-spl", + "solana-instruction", + "solana-log-collector", + "solana-logger", + "solana-program-runtime", + "solana-runtime", + "solana-sbpf", + "solana-sdk", + "solana-sdk-ids", + "solana-svm", + "solana-timings", + "solana-vote-program", + "thiserror 2.0.12", + "tokio", +] + [[package]] name = "solana-pubkey" version = "2.2.1" @@ -6688,6 +7246,113 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "solana-runtime" +version = "2.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13f58e4566fb3d2e28719ff00646841bb1269fc091fb75ee51da3eb855deef17" +dependencies = [ + "ahash", + "aquamarine", + "arrayref", + "base64 0.22.1", + "bincode", + "blake3", + "bv", + "bytemuck", + "bzip2", + "crossbeam-channel", + "dashmap", + "dir-diff", + "flate2", + "fnv", + "im", + "index_list", + "itertools 0.12.1", + "lazy_static", + "libc", + "log", + "lz4", + "memmap2", + "mockall", + "modular-bitfield", + "num-derive", + "num-traits", + "num_cpus", + "num_enum", + "percentage", + "qualifier_attr", + "rand 0.8.5", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "serde_with", + "solana-accounts-db", + "solana-bpf-loader-program", + "solana-bucket-map", + "solana-builtins", + "solana-compute-budget", + "solana-compute-budget-instruction", + "solana-config-program", + "solana-cost-model", + "solana-feature-set", + "solana-fee", + "solana-inline-spl", + "solana-lattice-hash", + "solana-measure", + "solana-metrics", + "solana-nohash-hasher", + "solana-nonce-account", + "solana-perf", + "solana-program", + "solana-program-runtime", + "solana-pubkey", + "solana-rayon-threadlimit", + "solana-runtime-transaction", + "solana-sdk", + "solana-stake-program", + "solana-svm", + "solana-svm-rent-collector", + "solana-svm-transaction", + "solana-timings", + "solana-transaction-status-client-types", + "solana-unified-scheduler-logic", + "solana-version", + "solana-vote", + "solana-vote-program", + "static_assertions", + "strum", + "strum_macros", + "symlink", + "tar", + "tempfile", + "thiserror 2.0.12", + "zstd", +] + +[[package]] +name = "solana-runtime-transaction" +version = "2.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eeea366d9c748124f0e955c7cbc1f80f86c3eb587a49b1ebf52bb2e3da65158" +dependencies = [ + "agave-transaction-view", + "log", + "solana-compute-budget", + "solana-compute-budget-instruction", + "solana-hash", + "solana-message", + "solana-pubkey", + "solana-sdk-ids", + "solana-signature", + "solana-svm-transaction", + "solana-transaction", + "solana-transaction-error", + "thiserror 2.0.12", +] + [[package]] name = "solana-sanitize" version = "2.2.1" @@ -6873,6 +7538,25 @@ dependencies = [ "sha2 0.10.8", ] +[[package]] +name = "solana-send-transaction-service" +version = "2.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1616c476086eb9c1d80131111b3a6dc7fdbaf844bf594a15daa9c034e1fe68e3" +dependencies = [ + "crossbeam-channel", + "itertools 0.12.1", + "log", + "solana-client", + "solana-connection-cache", + "solana-measure", + "solana-metrics", + "solana-runtime", + "solana-sdk", + "solana-tpu-client", + "tokio", +] + [[package]] name = "solana-serde" version = "2.2.1" @@ -7088,10 +7772,64 @@ dependencies = [ "solana-transaction-metrics-tracker", "thiserror 2.0.12", "tokio", - "tokio-util", + "tokio-util 0.7.14", "x509-parser", ] +[[package]] +name = "solana-svm" +version = "2.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68aae7788fea1a3b85f91be1c260b720ee7496e585a96831bb2a6f4758121e85" +dependencies = [ + "ahash", + "itertools 0.12.1", + "log", + "percentage", + "serde", + "serde_derive", + "solana-account", + "solana-bpf-loader-program", + "solana-clock", + "solana-compute-budget", + "solana-compute-budget-instruction", + "solana-feature-set", + "solana-fee-structure", + "solana-hash", + "solana-instruction", + "solana-instructions-sysvar", + "solana-loader-v4-program", + "solana-log-collector", + "solana-measure", + "solana-message", + "solana-nonce", + "solana-nonce-account", + "solana-precompiles", + "solana-program", + "solana-program-runtime", + "solana-pubkey", + "solana-rent", + "solana-rent-debits", + "solana-sdk", + "solana-sdk-ids", + "solana-svm-rent-collector", + "solana-svm-transaction", + "solana-timings", + "solana-transaction-context", + "solana-transaction-error", + "solana-type-overrides", + "thiserror 2.0.12", +] + +[[package]] +name = "solana-svm-rent-collector" +version = "2.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bcbacd010528375e02121c48446b0ebe15d5a62e69ef638113ee117280aa18e" +dependencies = [ + "solana-sdk", +] + [[package]] name = "solana-svm-transaction" version = "2.2.4" @@ -7425,6 +8163,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "solana-unified-scheduler-logic" +version = "2.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c404e2acb884234ae96e0af48b001b6270915e7060d6f70bdec79df5dcaf646" +dependencies = [ + "assert_matches", + "solana-pubkey", + "solana-runtime-transaction", + "solana-transaction", + "static_assertions", +] + [[package]] name = "solana-validator-exit" version = "2.2.1" @@ -7445,6 +8196,31 @@ dependencies = [ "solana-serde-varint", ] +[[package]] +name = "solana-vote" +version = "2.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "954d23ac6e7d5e57701870182409b59116543058be82ae9a8ffa9cb9549fa4aa" +dependencies = [ + "itertools 0.12.1", + "log", + "serde", + "serde_derive", + "solana-account", + "solana-bincode", + "solana-clock", + "solana-hash", + "solana-instruction", + "solana-packet", + "solana-pubkey", + "solana-sdk-ids", + "solana-signature", + "solana-svm-transaction", + "solana-transaction", + "solana-vote-interface", + "thiserror 2.0.12", +] + [[package]] name = "solana-vote-interface" version = "2.2.1" @@ -8027,6 +8803,28 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck 0.4.1", + "proc-macro2 1.0.94", + "quote 1.0.40", + "rustversion", + "syn 1.0.109", +] + [[package]] name = "subtle" version = "2.6.1" @@ -8182,6 +8980,12 @@ dependencies = [ "swig-assertions", ] +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + [[package]] name = "syn" version = "0.15.44" @@ -8283,6 +9087,52 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tarpc" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c38a012bed6fb9681d3bf71ffaa4f88f3b4b9ed3198cda6e4c8462d24d4bb80" +dependencies = [ + "anyhow", + "fnv", + "futures", + "humantime", + "opentelemetry", + "pin-project", + "rand 0.8.5", + "serde", + "static_assertions", + "tarpc-plugins", + "thiserror 1.0.69", + "tokio", + "tokio-serde", + "tokio-util 0.6.10", + "tracing", + "tracing-opentelemetry", +] + +[[package]] +name = "tarpc-plugins" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee42b4e559f17bce0385ebf511a7beb67d5cc33c12c96b7f4e9789919d9c10f" +dependencies = [ + "proc-macro2 1.0.94", + "quote 1.0.40", + "syn 1.0.109", +] + [[package]] name = "task-local-extensions" version = "0.1.4" @@ -8314,6 +9164,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + [[package]] name = "test-log" version = "0.2.17" @@ -8336,6 +9192,14 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "test-program-authority" +version = "1.2.0" +dependencies = [ + "solana-program", + "solana-program-test", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -8499,6 +9363,22 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-serde" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "911a61637386b789af998ee23f50aa30d5fd7edcec8d6d3dedae5e5815205466" +dependencies = [ + "bincode", + "bytes", + "educe", + "futures-core", + "futures-sink", + "pin-project", + "serde", + "serde_json", +] + [[package]] name = "tokio-stream" version = "0.1.17" @@ -8525,6 +9405,21 @@ dependencies = [ "webpki-roots 0.25.4", ] +[[package]] +name = "tokio-util" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "slab", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.14" @@ -8631,6 +9526,19 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-opentelemetry" +version = "0.17.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbbe89715c1dbbb790059e2565353978564924ee85017b5fff365c872ff6721f" +dependencies = [ + "once_cell", + "opentelemetry", + "tracing", + "tracing-core", + "tracing-subscriber", +] + [[package]] name = "tracing-subscriber" version = "0.3.19" @@ -9038,7 +9946,7 @@ checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.1.1", "windows-result", "windows-strings", ] @@ -9071,13 +9979,19 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-result" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" dependencies = [ - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -9086,7 +10000,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" dependencies = [ - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -9125,6 +10039,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -9164,13 +10087,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -9189,6 +10129,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -9207,6 +10153,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -9225,12 +10177,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -9249,6 +10213,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -9267,6 +10237,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -9285,6 +10261,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -9303,6 +10285,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.7.6" @@ -9370,6 +10358,16 @@ dependencies = [ "time", ] +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "yoke" version = "0.7.5" diff --git a/Cargo.toml b/Cargo.toml index af1daa85..6b8190b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "no-padding", "rust-sdk", "cli", + "test-program-authority", ] [workspace.package] diff --git a/interface/src/lib.rs b/interface/src/lib.rs index 91416c59..0e4c1106 100644 --- a/interface/src/lib.rs +++ b/interface/src/lib.rs @@ -37,7 +37,7 @@ use swig_state::{ secp256k1::{hex_encode, AccountsPayload}, AuthorityType, }, - swig::swig_account_seeds, + swig::{swig_account_seeds, swig_wallet_address_seeds}, IntoBytes, Transmutable, }; @@ -148,8 +148,18 @@ pub fn program_id() -> Pubkey { swig::ID.into() } -pub fn swig_key(id: String) -> Pubkey { - Pubkey::find_program_address(&swig_account_seeds(id.as_bytes()), &program_id()).0 +pub const PROGRAM_ID: [u8; 32] = swig::ID; + +pub fn swig_key_bytes(id: &[u8; 32]) -> Pubkey { + Pubkey::find_program_address(&swig_account_seeds(id), &program_id()).0 +} + +pub fn swig_wallet_address(config_address: &Pubkey) -> Pubkey { + Pubkey::find_program_address( + &swig_wallet_address_seeds(config_address.as_ref()), + &program_id(), + ) + .0 } pub struct AuthorityConfig<'a> { @@ -443,6 +453,65 @@ impl AddAuthorityInstruction { Ok(vec![secp256r1_verify_ix, main_ix]) } + + pub fn new_with_program_exec( + swig_account: Pubkey, + payer: Pubkey, + preceding_instruction: Instruction, + acting_role_id: u32, + new_authority_config: AuthorityConfig, + actions: Vec, + ) -> anyhow::Result> { + use solana_sdk::sysvar::instructions::ID as INSTRUCTIONS_ID; + + let mut accounts = vec![ + AccountMeta::new(swig_account, false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(system_program::ID, false), + ]; + + // Add instructions sysvar at a stable index + let instruction_sysvar_index = accounts.len() as u8; + accounts.push(AccountMeta::new_readonly(INSTRUCTIONS_ID, false)); + + let mut action_bytes = Vec::new(); + let num_actions = actions.len() as u8; + for action in actions { + action + .write(&mut action_bytes) + .map_err(|e| anyhow::anyhow!("Failed to serialize action {:?}", e))?; + } + + let args = AddAuthorityV1Args::new( + acting_role_id, + new_authority_config.authority_type, + new_authority_config.authority.len() as u16, + action_bytes.len() as u16, + num_actions, + ); + + let arg_bytes = args + .into_bytes() + .map_err(|e| anyhow::anyhow!("Failed to serialize args {:?}", e))?; + + // Build authority payload for ProgramExec: [instruction_sysvar_index: 1 byte] + let authority_payload = vec![instruction_sysvar_index]; + + let main_ix = Instruction { + program_id: Pubkey::from(swig::ID), + accounts, + data: [ + arg_bytes, + new_authority_config.authority, + &action_bytes, + &authority_payload, + ] + .concat(), + }; + + // Return both instructions - preceding instruction must come first + Ok(vec![preceding_instruction, main_ix]) + } } pub struct SignInstruction; @@ -605,6 +674,80 @@ impl SignInstruction { Ok(vec![secp256r1_verify_ix, main_ix]) } + + /// Creates a sign instruction for ProgramExec authority. + /// + /// This method creates a sign instruction that will be validated by + /// checking that the preceding instruction in the transaction matches + /// the configured program ID and instruction discriminator. + /// + /// The ProgramExec authority validates that: + /// - The preceding instruction was executed by the expected program + /// - The instruction data matches the expected discriminator/prefix + /// - The preceding instruction's first two accounts are the swig config and + /// wallet + /// + /// # Arguments + /// + /// * `swig_account` - The Swig wallet account + /// * `payer` - The transaction fee payer + /// * `preceding_instruction` - The instruction that must precede this sign + /// instruction + /// * `inner_instruction` - The instruction to be signed by the Swig wallet + /// * `role_id` - The role ID that has ProgramExec authority + /// * `instruction_sysvar_index` - Index where the instructions sysvar will + /// be in accounts + /// + /// # Returns + /// + /// Returns a vector containing both the preceding instruction and the sign + /// instruction. These must be executed in the same transaction in this + /// order. + pub fn new_program_exec( + swig_account: Pubkey, + payer: Pubkey, + preceding_instruction: Instruction, + inner_instruction: Instruction, + role_id: u32, + instruction_sysvar_index: u8, + ) -> anyhow::Result> { + use solana_sdk::sysvar::instructions::ID as INSTRUCTIONS_ID; + + let accounts = vec![ + AccountMeta::new(swig_account, false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(INSTRUCTIONS_ID, false), // Instructions sysvar + ]; + + let (accounts, ixs) = compact_instructions(swig_account, accounts, vec![inner_instruction]); + + // Validate the instructions sysvar index + if instruction_sysvar_index as usize >= accounts.len() { + return Err(anyhow::anyhow!( + "instruction_sysvar_index out of bounds: {} >= {}", + instruction_sysvar_index, + accounts.len() + )); + } + + let ix_bytes = ixs.into_bytes(); + let args = swig::actions::sign_v1::SignV1Args::new(role_id, ix_bytes.len() as u16); + let arg_bytes = args + .into_bytes() + .map_err(|e| anyhow::anyhow!("Failed to serialize args {:?}", e))?; + + // Build authority payload for ProgramExec: [instruction_sysvar_index: 1 byte] + let authority_payload = vec![instruction_sysvar_index]; + + let sign_ix = Instruction { + program_id: Pubkey::from(swig::ID), + accounts, + data: [arg_bytes, &ix_bytes, &authority_payload].concat(), + }; + + // Return both instructions - preceding instruction must come first + Ok(vec![preceding_instruction, sign_ix]) + } } pub struct SignV2Instruction; @@ -661,6 +804,48 @@ impl SignV2Instruction { }) } + pub fn new_program_exec( + swig_account: Pubkey, + swig_wallet_address: Pubkey, + payer: Pubkey, + preceding_instruction: Instruction, + inner_instruction: Instruction, + role_id: u32, + ) -> anyhow::Result> { + use solana_sdk::sysvar::instructions::ID as INSTRUCTIONS_ID; + + let accounts = vec![ + AccountMeta::new(swig_account, false), + AccountMeta::new(swig_wallet_address, false), + AccountMeta::new(payer, true), + ]; + + let (mut accounts, ixs) = + compact_instructions(swig_account, accounts, vec![inner_instruction]); + + // Add instructions sysvar AFTER compact_instructions to ensure stable index + let instruction_sysvar_index = accounts.len() as u8; + accounts.push(AccountMeta::new_readonly(INSTRUCTIONS_ID, false)); + + let ix_bytes = ixs.into_bytes(); + let args = swig::actions::sign_v2::SignV2Args::new(role_id, ix_bytes.len() as u16); + let arg_bytes = args + .into_bytes() + .map_err(|e| anyhow::anyhow!("Failed to serialize args {:?}", e))?; + + // Build authority payload for ProgramExec: [instruction_sysvar_index: 1 byte] + let authority_payload = vec![instruction_sysvar_index]; + + let sign_ix = Instruction { + program_id: Pubkey::from(swig::ID), + accounts, + data: [arg_bytes, &ix_bytes, &authority_payload].concat(), + }; + + // Return both instructions - preceding instruction must come first + Ok(vec![preceding_instruction, sign_ix]) + } + pub fn new_secp256k1( swig_account: Pubkey, swig_wallet_address: Pubkey, @@ -1021,6 +1206,43 @@ impl RemoveAuthorityInstruction { Ok(vec![secp256r1_verify_ix, main_ix]) } + + pub fn new_with_program_exec( + swig_account: Pubkey, + payer: Pubkey, + preceding_instruction: Instruction, + acting_role_id: u32, + authority_to_remove_id: u32, + ) -> anyhow::Result> { + use solana_sdk::sysvar::instructions::ID as INSTRUCTIONS_ID; + + let mut accounts = vec![ + AccountMeta::new(swig_account, false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(system_program::ID, false), + ]; + + // Add instructions sysvar at a stable index + let instruction_sysvar_index = accounts.len() as u8; + accounts.push(AccountMeta::new_readonly(INSTRUCTIONS_ID, false)); + + let args = RemoveAuthorityV1Args::new(acting_role_id, authority_to_remove_id, 1); + let arg_bytes = args + .into_bytes() + .map_err(|e| anyhow::anyhow!("Failed to serialize args {:?}", e))?; + + // Build authority payload for ProgramExec: [instruction_sysvar_index: 1 byte] + let authority_payload = vec![instruction_sysvar_index]; + + let main_ix = Instruction { + program_id: Pubkey::from(swig::ID), + accounts, + data: [arg_bytes, &authority_payload].concat(), + }; + + // Return both instructions - preceding instruction must come first + Ok(vec![preceding_instruction, main_ix]) + } } pub enum UpdateAuthorityData { ReplaceAll(Vec), @@ -1338,6 +1560,56 @@ impl UpdateAuthorityInstruction { Ok(vec![secp256r1_verify_ix, main_ix]) } + + pub fn new_with_program_exec( + swig_account: Pubkey, + payer: Pubkey, + preceding_instruction: Instruction, + acting_role_id: u32, + authority_to_update_id: u32, + update_data: UpdateAuthorityData, + ) -> anyhow::Result> { + use solana_sdk::sysvar::instructions::ID as INSTRUCTIONS_ID; + + let mut accounts = vec![ + AccountMeta::new(swig_account, false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(system_program::ID, false), + ]; + + // Add instructions sysvar at a stable index + let instruction_sysvar_index = accounts.len() as u8; + accounts.push(AccountMeta::new_readonly(INSTRUCTIONS_ID, false)); + + let (operation, operation_data) = update_data.to_operation_and_data()?; + + // Encode operation type in the first byte of the data + let mut encoded_data = Vec::new(); + encoded_data.push(operation as u8); + encoded_data.extend_from_slice(&operation_data); + + let args = UpdateAuthorityV1Args::new( + acting_role_id, + authority_to_update_id, + encoded_data.len() as u16, + 0, // num_actions will be calculated by the program + ); + let arg_bytes = args + .into_bytes() + .map_err(|e| anyhow::anyhow!("Failed to serialize args {:?}", e))?; + + // Build authority payload for ProgramExec: [instruction_sysvar_index: 1 byte] + let authority_payload = vec![instruction_sysvar_index]; + + let main_ix = Instruction { + program_id: Pubkey::from(swig::ID), + accounts, + data: [arg_bytes, &encoded_data, &authority_payload].concat(), + }; + + // Return both instructions - preceding instruction must come first + Ok(vec![preceding_instruction, main_ix]) + } } pub struct CreateSessionInstruction; @@ -1500,6 +1772,45 @@ impl CreateSessionInstruction { Ok(vec![secp256r1_verify_ix, main_ix]) } + + pub fn new_with_program_exec( + swig_account: Pubkey, + payer: Pubkey, + preceding_instruction: Instruction, + role_id: u32, + session_duration: u64, + session_key: Pubkey, + ) -> anyhow::Result> { + use solana_sdk::sysvar::instructions::ID as INSTRUCTIONS_ID; + + let mut accounts = vec![ + AccountMeta::new(swig_account, false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(system_program::ID, false), + ]; + + // Add instructions sysvar at a stable index + let instruction_sysvar_index = accounts.len() as u8; + accounts.push(AccountMeta::new_readonly(INSTRUCTIONS_ID, false)); + + let create_session_args = + CreateSessionV1Args::new(role_id, session_duration, session_key.to_bytes()); + let args_bytes = create_session_args + .into_bytes() + .map_err(|e| anyhow::anyhow!("Failed to serialize args {:?}", e))?; + + // Build authority payload for ProgramExec: [instruction_sysvar_index: 1 byte] + let authority_payload = vec![instruction_sysvar_index]; + + let main_ix = Instruction { + program_id: Pubkey::from(swig::ID), + accounts, + data: [args_bytes, &authority_payload].concat(), + }; + + // Return both instructions - preceding instruction must come first + Ok(vec![preceding_instruction, main_ix]) + } } // Sub-account instruction structures @@ -1661,6 +1972,45 @@ impl CreateSubAccountInstruction { Ok(vec![secp256r1_verify_ix, main_ix]) } + + pub fn new_with_program_exec( + swig_account: Pubkey, + payer: Pubkey, + preceding_instruction: Instruction, + sub_account: Pubkey, + role_id: u32, + sub_account_bump: u8, + ) -> anyhow::Result> { + use solana_sdk::sysvar::instructions::ID as INSTRUCTIONS_ID; + + let mut accounts = vec![ + AccountMeta::new(swig_account, false), + AccountMeta::new(payer, true), + AccountMeta::new(sub_account, false), + AccountMeta::new_readonly(system_program::ID, false), + ]; + + // Add instructions sysvar at a stable index + let instruction_sysvar_index = accounts.len() as u8; + accounts.push(AccountMeta::new_readonly(INSTRUCTIONS_ID, false)); + + let args = CreateSubAccountV1Args::new(role_id, sub_account_bump); + let args_bytes = args + .into_bytes() + .map_err(|e| anyhow::anyhow!("Failed to serialize args {:?}", e))?; + + // Build authority payload for ProgramExec: [instruction_sysvar_index: 1 byte] + let authority_payload = vec![instruction_sysvar_index]; + + let main_ix = Instruction { + program_id: program_id(), + accounts, + data: [args_bytes, &authority_payload].concat(), + }; + + // Return both instructions - preceding instruction must come first + Ok(vec![preceding_instruction, main_ix]) + } } pub struct WithdrawFromSubAccountInstruction; @@ -2009,6 +2359,94 @@ impl WithdrawFromSubAccountInstruction { Ok(vec![secp256r1_verify_ix, main_ix]) } + + pub fn new_with_program_exec( + swig_account: Pubkey, + payer: Pubkey, + preceding_instruction: Instruction, + sub_account: Pubkey, + swig_wallet_address: Pubkey, + role_id: u32, + amount: u64, + ) -> anyhow::Result> { + use solana_sdk::sysvar::instructions::ID as INSTRUCTIONS_ID; + + let mut accounts = vec![ + AccountMeta::new(swig_account, false), + AccountMeta::new_readonly(payer, true), + AccountMeta::new(sub_account, false), + AccountMeta::new(swig_wallet_address, false), + AccountMeta::new_readonly(system_program::ID, false), + ]; + + // Add instructions sysvar at a stable index + let instruction_sysvar_index = accounts.len() as u8; + accounts.push(AccountMeta::new_readonly(INSTRUCTIONS_ID, false)); + + let args = WithdrawFromSubAccountV1Args::new(role_id, amount); + let args_bytes = args + .into_bytes() + .map_err(|e| anyhow::anyhow!("Failed to serialize args {:?}", e))?; + + // Build authority payload for ProgramExec: [instruction_sysvar_index: 1 byte] + let authority_payload = vec![instruction_sysvar_index]; + + let main_ix = Instruction { + program_id: program_id(), + accounts, + data: [args_bytes, &authority_payload].concat(), + }; + + // Return both instructions - preceding instruction must come first + Ok(vec![preceding_instruction, main_ix]) + } + + pub fn new_token_with_program_exec( + swig_account: Pubkey, + payer: Pubkey, + preceding_instruction: Instruction, + sub_account: Pubkey, + swig_wallet_address: Pubkey, + sub_account_token: Pubkey, + swig_token: Pubkey, + token_program: Pubkey, + role_id: u32, + amount: u64, + ) -> anyhow::Result> { + use solana_sdk::sysvar::instructions::ID as INSTRUCTIONS_ID; + + let mut accounts = vec![ + AccountMeta::new(swig_account, false), + AccountMeta::new_readonly(payer, true), + AccountMeta::new(sub_account, false), + AccountMeta::new(swig_wallet_address, false), + AccountMeta::new(sub_account_token, false), + AccountMeta::new(swig_token, false), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(token_program, false), + ]; + + // Add instructions sysvar at a stable index + let instruction_sysvar_index = accounts.len() as u8; + accounts.push(AccountMeta::new_readonly(INSTRUCTIONS_ID, false)); + + let args = WithdrawFromSubAccountV1Args::new(role_id, amount); + let args_bytes = args + .into_bytes() + .map_err(|e| anyhow::anyhow!("Failed to serialize args {:?}", e))?; + + // Build authority payload for ProgramExec: [instruction_sysvar_index: 1 byte] + let authority_payload = vec![instruction_sysvar_index]; + + let main_ix = Instruction { + program_id: program_id(), + accounts, + data: [args_bytes, &authority_payload].concat(), + }; + + // Return both instructions - preceding instruction must come first + Ok(vec![preceding_instruction, main_ix]) + } } pub struct SubAccountSignInstruction; @@ -2168,6 +2606,48 @@ impl SubAccountSignInstruction { Ok(vec![secp256r1_verify_ix, main_ix]) } + + pub fn new_with_program_exec( + swig_account: Pubkey, + sub_account: Pubkey, + payer: Pubkey, + preceding_instruction: Instruction, + role_id: u32, + instructions: Vec, + ) -> anyhow::Result> { + use solana_sdk::sysvar::instructions::ID as INSTRUCTIONS_ID; + + let mut accounts = vec![ + AccountMeta::new_readonly(swig_account, false), + AccountMeta::new_readonly(payer, true), + AccountMeta::new(sub_account, false), + AccountMeta::new_readonly(system_program::ID, false), + ]; + + // Add instructions sysvar at a stable index + let instruction_sysvar_index = accounts.len() as u8; + accounts.push(AccountMeta::new_readonly(INSTRUCTIONS_ID, false)); + + let (accounts, ixs) = + compact_instructions_sub_account(swig_account, sub_account, accounts, instructions); + let ix_bytes = ixs.into_bytes(); + let args = SubAccountSignV1Args::new(role_id, ix_bytes.len() as u16); + let args_bytes = args + .into_bytes() + .map_err(|e| anyhow::anyhow!("Failed to serialize args {:?}", e))?; + + // Build authority payload for ProgramExec: [instruction_sysvar_index: 1 byte] + let authority_payload = vec![instruction_sysvar_index]; + + let main_ix = Instruction { + program_id: program_id(), + accounts, + data: [args_bytes, &ix_bytes, &authority_payload].concat(), + }; + + // Return both instructions - preceding instruction must come first + Ok(vec![preceding_instruction, main_ix]) + } } pub struct ToggleSubAccountInstruction; @@ -2336,6 +2816,45 @@ impl ToggleSubAccountInstruction { Ok(vec![secp256r1_verify_ix, main_ix]) } + + pub fn new_with_program_exec( + swig_account: Pubkey, + payer: Pubkey, + preceding_instruction: Instruction, + sub_account: Pubkey, + role_id: u32, + auth_role_id: u32, + enabled: bool, + ) -> anyhow::Result> { + use solana_sdk::sysvar::instructions::ID as INSTRUCTIONS_ID; + + let mut accounts = vec![ + AccountMeta::new(swig_account, false), + AccountMeta::new_readonly(payer, true), + AccountMeta::new(sub_account, false), + ]; + + // Add instructions sysvar at a stable index + let instruction_sysvar_index = accounts.len() as u8; + accounts.push(AccountMeta::new_readonly(INSTRUCTIONS_ID, false)); + + let args = ToggleSubAccountV1Args::new(role_id, auth_role_id, enabled); + let args_bytes = args + .into_bytes() + .map_err(|e| anyhow::anyhow!("Failed to serialize args {:?}", e))?; + + // Build authority payload for ProgramExec: [instruction_sysvar_index: 1 byte] + let authority_payload = vec![instruction_sysvar_index]; + + let main_ix = Instruction { + program_id: program_id(), + accounts, + data: [args_bytes, &authority_payload].concat(), + }; + + // Return both instructions - preceding instruction must come first + Ok(vec![preceding_instruction, main_ix]) + } } pub struct TransferAssetsV1Instruction; @@ -2500,4 +3019,42 @@ impl TransferAssetsV1Instruction { Ok(vec![secp256r1_verify_ix, main_ix]) } + + pub fn new_with_program_exec( + swig_account: Pubkey, + swig_wallet_address: Pubkey, + payer: Pubkey, + preceding_instruction: Instruction, + role_id: u32, + ) -> anyhow::Result> { + use solana_sdk::sysvar::instructions::ID as INSTRUCTIONS_ID; + + let mut accounts = vec![ + AccountMeta::new(swig_account, false), + AccountMeta::new(swig_wallet_address, false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(system_program::ID, false), + ]; + + // Add instructions sysvar at a stable index + let instruction_sysvar_index = accounts.len() as u8; + accounts.push(AccountMeta::new_readonly(INSTRUCTIONS_ID, false)); + + let args = TransferAssetsV1Args::new(role_id); + let args_bytes = args + .into_bytes() + .map_err(|e| anyhow::anyhow!("Failed to serialize args {:?}", e))?; + + // Build authority payload for ProgramExec: [instruction_sysvar_index: 1 byte] + let authority_payload = vec![instruction_sysvar_index]; + + let main_ix = Instruction { + program_id: program_id(), + accounts, + data: [args_bytes, &authority_payload].concat(), + }; + + // Return both instructions - preceding instruction must come first + Ok(vec![preceding_instruction, main_ix]) + } } diff --git a/program/build.rs b/program/build.rs index 4bb8a9da..8deffc0b 100644 --- a/program/build.rs +++ b/program/build.rs @@ -1,10 +1,9 @@ //! Shank IDL build script. -use { - anyhow::anyhow, - shank_idl::{extract_idl, manifest::Manifest, ParseIdlOpts}, - std::{env, fs, path::Path}, -}; +use std::{env, fs, path::Path}; + +use anyhow::anyhow; +use shank_idl::{extract_idl, manifest::Manifest, ParseIdlOpts}; fn main() { println!("cargo:rerun-if-changed=src/"); diff --git a/program/src/error.rs b/program/src/error.rs index 7fbba81e..5d6b272c 100644 --- a/program/src/error.rs +++ b/program/src/error.rs @@ -116,6 +116,8 @@ pub enum SwigError { SignV1CannotBeUsedWithSwigV2, /// SignV2 instruction cannot be used with Swig v1 accounts SignV2CannotBeUsedWithSwigV1, + /// Reserved ID prefix, must use deterministic create ID + ReservedIdPrefix, } /// Implements conversion from SwigError to ProgramError. diff --git a/program/tests/program_authority_test.rs b/program/tests/program_authority_test.rs new file mode 100644 index 00000000..f83f8b6c --- /dev/null +++ b/program/tests/program_authority_test.rs @@ -0,0 +1,999 @@ +#![cfg(not(feature = "program_scope_test"))] + +mod common; + +use common::*; +use litesvm_token::spl_token::{self, instruction::TokenInstruction}; +use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + message::{v0, VersionedMessage}, + program_pack::Pack, + pubkey::Pubkey, + signature::Keypair, + signer::Signer, + system_instruction, + transaction::VersionedTransaction, +}; +use swig_interface::{AuthorityConfig, ClientAction}; +use swig_state::{ + action::{all::All, program::Program}, + authority::{programexec::ProgramExecAuthority, AuthorityType}, + swig::{swig_account_seeds, swig_wallet_address_seeds, SwigWithRoles}, + IntoBytes, Transmutable, +}; + +// Test program ID - matches the declared ID in +// test-program-authority/src/lib.rs +solana_sdk::declare_id!("BXAu5ZWHnGun2XZjUZ9nqwiZ5dNVmofPGYdMC4rx4qLV"); +const TEST_PROGRAM_ID: Pubkey = ID; + +// Test program binary path +const TEST_PROGRAM_PATH: &str = "../target/deploy/test_program_authority.so"; + +// Test program instruction discriminators (must match +// test-program-authority/src/processor.rs) +const VALID_DISCRIMINATOR: [u8; 8] = [1, 2, 3, 4, 5, 6, 7, 8]; +const INVALID_DISCRIMINATOR: [u8; 8] = [9, 9, 9, 9, 9, 9, 9, 9]; + +/// Helper function to deploy the test program +fn deploy_test_program(context: &mut SwigTestContext) -> anyhow::Result<()> { + let program_data = std::fs::read(TEST_PROGRAM_PATH).map_err(|e| { + anyhow::anyhow!( + "Failed to read test program: {}. Make sure to run `cargo build-sbf` first.", + e + ) + })?; + + context.svm.add_program(TEST_PROGRAM_ID, &program_data); + Ok(()) +} + +/// Helper function to create or update the test program state account +fn set_test_program_state( + context: &mut SwigTestContext, + state_account: &Pubkey, + should_fail: bool, +) -> anyhow::Result<()> { + let state_data = vec![if should_fail { 1u8 } else { 0u8 }]; + + // Create or update account + let account = solana_sdk::account::Account { + lamports: 1_000_000, + data: state_data, + owner: TEST_PROGRAM_ID, + executable: false, + rent_epoch: 0, + }; + + context.svm.set_account(*state_account, account)?; + Ok(()) +} + +/// Helper function to create a ProgramExec authority +fn create_program_exec_authority_data(program_id: Pubkey, instruction_prefix: &[u8]) -> Vec { + const IX_PREFIX_OFFSET: usize = 32 + 1 + 7; // program_id + instruction_prefix_len + padding + + let mut data = vec![0u8; ProgramExecAuthority::LEN]; + // First 32 bytes: program_id + data[..32].copy_from_slice(&program_id.to_bytes()); + // Byte 32: instruction_prefix_len + data[32] = instruction_prefix.len() as u8; + // Bytes 33-39: padding (already zeroed) + // Bytes 40+: instruction_prefix + data[IX_PREFIX_OFFSET..IX_PREFIX_OFFSET + instruction_prefix.len()] + .copy_from_slice(instruction_prefix); + data +} + +/// Test creating a swig with a ProgramExec authority +#[test_log::test] +fn test_create_program_exec_authority() { + let mut context = setup_test_context().unwrap(); + let swig_authority = Keypair::new(); + + context + .svm + .airdrop(&swig_authority.pubkey(), 10_000_000_000) + .unwrap(); + + let id = rand::random::<[u8; 32]>(); + + // Create swig with root Ed25519 authority first + let (swig_key, _) = create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + // Now add a ProgramExec authority + let program_exec_data = + create_program_exec_authority_data(TEST_PROGRAM_ID, &VALID_DISCRIMINATOR); + + let result = add_authority_with_ed25519_root( + &mut context, + &swig_key, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::ProgramExec, + authority: &program_exec_data, + }, + vec![ + ClientAction::Program(Program { + program_id: spl_token::ID.to_bytes(), + }), + ClientAction::All(All {}), + ], + ); + + assert!( + result.is_ok(), + "Failed to add ProgramExec authority: {:?}", + result.err() + ); + + // Verify the authority was added + let swig_account = context.svm.get_account(&swig_key).unwrap(); + let swig = SwigWithRoles::from_bytes(&swig_account.data).unwrap(); + + assert_eq!( + swig.state.roles, 2, + "Should have 2 roles (root + program exec)" + ); + + // Verify the program exec authority + let role_1 = swig.get_role(1).unwrap().unwrap(); + assert_eq!( + role_1.position.authority_type().unwrap(), + AuthorityType::ProgramExec, + "Second authority should be ProgramExec" + ); +} + +/// Test changing a ProgramExec authority's discriminator via remove + add +/// Note: UpdateAuthority only modifies permissions/actions, not authority data +/// itself, so to change the discriminator we need to remove and re-add the +/// authority. +#[test_log::test] +fn test_update_program_exec_authority() { + let mut context = setup_test_context().unwrap(); + let swig_authority = Keypair::new(); + + context + .svm + .airdrop(&swig_authority.pubkey(), 10_000_000_000) + .unwrap(); + + let id = rand::random::<[u8; 32]>(); + let (swig_key, _) = create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + // Add initial ProgramExec authority + let initial_discriminator = [1, 2, 3, 4, 5, 6, 7, 8]; + let program_exec_data = + create_program_exec_authority_data(TEST_PROGRAM_ID, &initial_discriminator); + + add_authority_with_ed25519_root( + &mut context, + &swig_key, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::ProgramExec, + authority: &program_exec_data, + }, + vec![ClientAction::All(All {})], + ) + .unwrap(); + + // Verify initial state + let swig_account = context.svm.get_account(&swig_key).unwrap(); + let swig = SwigWithRoles::from_bytes(&swig_account.data).unwrap(); + assert_eq!( + swig.state.roles, 2, + "Should have 2 roles (root + program exec)" + ); + + // To "update" the discriminator, we need to remove the old authority and add a + // new one because UpdateAuthority only modifies permissions/actions, not + // authority data itself + + // Step 1: Remove the existing ProgramExec authority + use swig_interface::RemoveAuthorityInstruction; + + let authority_to_remove_id = 1; // The ProgramExec authority we just added + + let remove_ix = RemoveAuthorityInstruction::new_with_ed25519_authority( + swig_key, + swig_authority.pubkey(), + swig_authority.pubkey(), + 0, // acting_role_id (root authority) + authority_to_remove_id, + ) + .unwrap(); + + // Execute remove instruction + let msg = v0::Message::try_compile( + &swig_authority.pubkey(), + &[remove_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx = VersionedTransaction::try_new(VersionedMessage::V0(msg), &[&swig_authority]).unwrap(); + + let result = context.svm.send_transaction(tx); + assert!( + result.is_ok(), + "Failed to remove ProgramExec authority: {:?}", + result.err() + ); + + // Verify we're back to 1 role + let swig_account = context.svm.get_account(&swig_key).unwrap(); + let swig = SwigWithRoles::from_bytes(&swig_account.data).unwrap(); + assert_eq!(swig.state.roles, 1, "Should have 1 role after removal"); + + // Step 2: Add a new ProgramExec authority with updated discriminator + let new_discriminator = [9, 8, 7, 6, 5, 4, 3, 2]; + let updated_program_exec_data = + create_program_exec_authority_data(TEST_PROGRAM_ID, &new_discriminator); + + add_authority_with_ed25519_root( + &mut context, + &swig_key, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::ProgramExec, + authority: &updated_program_exec_data, + }, + vec![ClientAction::All(All {})], + ) + .unwrap(); + + // Verify we have 2 roles again + let swig_account = context.svm.get_account(&swig_key).unwrap(); + let swig = SwigWithRoles::from_bytes(&swig_account.data).unwrap(); + assert_eq!(swig.state.roles, 2, "Should have 2 roles after re-adding"); + + // Verify the new discriminator by checking the authority data + // The new authority will have role_id = 2 (since role_id = 1 was removed) + let new_role_id = 2; + let role = swig.get_role(new_role_id).unwrap().unwrap(); + + // The authority should be ProgramExec + assert_eq!( + role.authority.authority_type(), + AuthorityType::ProgramExec, + "Authority type should be ProgramExec" + ); + + // Downcast to ProgramExecAuthority to access the concrete type + let program_exec_auth: &ProgramExecAuthority = role.authority.as_any().downcast_ref().unwrap(); + + // Verify the program_id is correct + assert_eq!( + program_exec_auth.program_id, + TEST_PROGRAM_ID.to_bytes(), + "Program ID should match" + ); + + // Verify the discriminator was updated + let stored_discriminator = &program_exec_auth.instruction_prefix[..new_discriminator.len()]; + assert_eq!( + stored_discriminator, &new_discriminator, + "Discriminator should be updated to new value" + ); +} + +/// Helper to build program exec sign instructions using the ergonomic interface +/// This now uses SignV2Instruction::new_program_exec() which returns both the +/// preceding instruction and the sign instruction that must be executed +/// together. +/// +/// authority_payload format: [instruction_sysvar_index: 1] +fn build_program_exec_sign_instructions( + swig_account: Pubkey, + swig_wallet_address: Pubkey, + payer: Pubkey, + preceding_instruction: Instruction, + inner_instruction: Instruction, + role_id: u32, +) -> anyhow::Result> { + use swig_interface::SignV2Instruction; + + SignV2Instruction::new_program_exec( + swig_account, + swig_wallet_address, + payer, + preceding_instruction, + inner_instruction, + role_id, + ) +} + +/// Test successful execution with valid program and state set to succeed +#[test_log::test] +fn test_program_exec_successful_execution() { + let mut context = setup_test_context().unwrap(); + let swig_authority = Keypair::new(); + let state_account = Keypair::new(); + + // Deploy the test program + deploy_test_program(&mut context).expect("Failed to deploy test program"); + + context + .svm + .airdrop(&swig_authority.pubkey(), 10_000_000_000) + .unwrap(); + + let id = rand::random::<[u8; 32]>(); + let swig = Pubkey::find_program_address(&swig_account_seeds(&id), &program_id()).0; + let swig_wallet = + Pubkey::find_program_address(&swig_wallet_address_seeds(swig.as_ref()), &program_id()).0; + + // Create swig with Ed25519 root + create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + // Airdrop to swig and swig_wallet after creation so they can execute transfers + context.svm.airdrop(&swig, 10_000_000).unwrap(); + context.svm.airdrop(&swig_wallet, 10_000_000).unwrap(); + + // Add ProgramExec authority that expects test program calls + let program_exec_data = + create_program_exec_authority_data(TEST_PROGRAM_ID, &VALID_DISCRIMINATOR); + + add_authority_with_ed25519_root( + &mut context, + &swig, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::ProgramExec, + authority: &program_exec_data, + }, + vec![ClientAction::All(All {})], + ) + .unwrap(); + + // Set test program state to succeed (0) + set_test_program_state(&mut context, &state_account.pubkey(), false).unwrap(); + + context.svm.warp_to_slot(100); + + // Build test program instruction (with config and wallet as first two accounts) + let test_program_ix = Instruction { + program_id: TEST_PROGRAM_ID, + accounts: vec![ + AccountMeta::new_readonly(swig, false), // config (swig account) + AccountMeta::new_readonly(swig_wallet, false), // wallet (swig wallet address PDA) + AccountMeta::new_readonly(state_account.pubkey(), false), // state account + AccountMeta::new_readonly(program_id(), false), // swig program + ], + data: VALID_DISCRIMINATOR.to_vec(), + }; + + // Create a dummy inner instruction - swig will sign this transfer as a PDA + // Transfer FROM swig wallet TO authority (swig wallet can sign as PDA) + let inner_ix = system_instruction::transfer(&swig_wallet, &swig_authority.pubkey(), 1000); + + // Use the ergonomic interface to create both the preceding and sign + // instructions + let instructions = build_program_exec_sign_instructions( + swig, + swig_wallet, + swig_authority.pubkey(), + test_program_ix, // preceding instruction that validates the authority + inner_ix, // instruction to be signed by swig + 1, // role_id for ProgramExec authority + ) + .unwrap(); + + // Build transaction with both instructions returned from the interface + let message = v0::Message::try_compile( + &swig_authority.pubkey(), + &instructions, + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx = + VersionedTransaction::try_new(VersionedMessage::V0(message), &[&swig_authority]).unwrap(); + + let res = context.svm.send_transaction(tx); + + if res.is_err() { + println!("Transaction failed: {:?}", res.as_ref().err()); + if let Some(logs) = res.as_ref().err().map(|e| &e.meta.logs) { + for log in logs { + println!("{}", log); + } + } + } + + assert!( + res.is_ok(), + "Transaction should succeed with valid program execution and state=0" + ); +} + +/// Test that execution fails when test program state is set to fail +#[test_log::test] +fn test_program_exec_fails_with_state_set_to_fail() { + let mut context = setup_test_context().unwrap(); + let swig_authority = Keypair::new(); + let state_account = Keypair::new(); + + // Deploy the test program + deploy_test_program(&mut context).expect("Failed to deploy test program"); + + context + .svm + .airdrop(&swig_authority.pubkey(), 10_000_000_000) + .unwrap(); + + let id = rand::random::<[u8; 32]>(); + let swig = Pubkey::find_program_address(&swig_account_seeds(&id), &program_id()).0; + let swig_wallet = + Pubkey::find_program_address(&swig_wallet_address_seeds(swig.as_ref()), &program_id()).0; + + create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + // Airdrop to swig and swig_wallet after creation so they can execute transfers + context.svm.airdrop(&swig, 10_000_000).unwrap(); + context.svm.airdrop(&swig_wallet, 10_000_000).unwrap(); + + let program_exec_data = + create_program_exec_authority_data(TEST_PROGRAM_ID, &VALID_DISCRIMINATOR); + + add_authority_with_ed25519_root( + &mut context, + &swig, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::ProgramExec, + authority: &program_exec_data, + }, + vec![ClientAction::All(All {})], + ) + .unwrap(); + + // Set test program state to FAIL (1) + set_test_program_state(&mut context, &state_account.pubkey(), true).unwrap(); + + context.svm.warp_to_slot(100); + + let test_program_ix = Instruction { + program_id: TEST_PROGRAM_ID, + accounts: vec![ + AccountMeta::new_readonly(swig, false), + AccountMeta::new_readonly(swig_wallet, false), + AccountMeta::new_readonly(state_account.pubkey(), false), + AccountMeta::new_readonly(program_id(), false), + ], + data: VALID_DISCRIMINATOR.to_vec(), + }; + + // Create a dummy inner instruction - swig wallet will sign this transfer as a + // PDA Transfer FROM swig wallet TO authority + let inner_ix = system_instruction::transfer(&swig_wallet, &swig_authority.pubkey(), 1000); + + // Use the ergonomic interface to create both instructions + let instructions = build_program_exec_sign_instructions( + swig, + swig_wallet, + swig_authority.pubkey(), + test_program_ix, + inner_ix, + 1, // role_id + ) + .unwrap(); + + let message = v0::Message::try_compile( + &swig_authority.pubkey(), + &instructions, + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx = + VersionedTransaction::try_new(VersionedMessage::V0(message), &[&swig_authority]).unwrap(); + + let res = context.svm.send_transaction(tx); + + // Should fail because test program state is set to 1 + assert!(res.is_err(), "Transaction should fail when state=1"); + + if let Err(err) = res { + println!("Got expected error: {:?}", err.err); + } +} + +/// Test failed token transfer with wrong program +#[test_log::test] +fn test_program_exec_wrong_program_fails() { + let mut context = setup_test_context().unwrap(); + let swig_authority = Keypair::new(); + let recipient = Keypair::new(); + + context + .svm + .airdrop(&swig_authority.pubkey(), 10_000_000_000) + .unwrap(); + context + .svm + .airdrop(&recipient.pubkey(), 10_000_000_000) + .unwrap(); + + let id = rand::random::<[u8; 32]>(); + let swig = Pubkey::find_program_address(&swig_account_seeds(&id), &program_id()).0; + let swig_wallet = + Pubkey::find_program_address(&swig_wallet_address_seeds(swig.as_ref()), &program_id()).0; + + create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + // Add ProgramExec authority expecting TEST_PROGRAM_ID + let program_exec_data = + create_program_exec_authority_data(TEST_PROGRAM_ID, &VALID_DISCRIMINATOR); + + add_authority_with_ed25519_root( + &mut context, + &swig, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::ProgramExec, + authority: &program_exec_data, + }, + vec![ + ClientAction::Program(Program { + program_id: solana_sdk::system_program::ID.to_bytes(), + }), + ClientAction::All(All {}), + ], + ) + .unwrap(); + + context.svm.warp_to_slot(100); + context.svm.airdrop(&swig, 10_000_000_000).unwrap(); + context.svm.airdrop(&swig_wallet, 10_000_000_000).unwrap(); + + // Try to use with system program instead (wrong program) + // Mock instruction with system program, not TEST_PROGRAM_ID + let mock_program_ix = Instruction { + program_id: solana_sdk::system_program::ID, + accounts: vec![ + AccountMeta::new_readonly(swig, false), + AccountMeta::new_readonly(swig_wallet, false), + AccountMeta::new(swig_authority.pubkey(), true), + ], + data: VALID_DISCRIMINATOR.to_vec(), + }; + + let transfer_ix = system_instruction::transfer(&swig_wallet, &recipient.pubkey(), 1000); + + // Use the ergonomic interface + let instructions = build_program_exec_sign_instructions( + swig, + swig_wallet, + swig_authority.pubkey(), + mock_program_ix, + transfer_ix, + 1, // role_id + ) + .unwrap(); + + let transfer_message = v0::Message::try_compile( + &swig_authority.pubkey(), + &instructions, + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let transfer_tx = + VersionedTransaction::try_new(VersionedMessage::V0(transfer_message), &[&swig_authority]) + .unwrap(); + + let res = context.svm.send_transaction(transfer_tx); + + // Should fail because the preceding instruction is from system program, not + // TEST_PROGRAM_ID + assert!(res.is_err(), "Transaction should fail with wrong program"); + + if let Err(err) = res { + println!("Got expected error: {:?}", err.err); + } +} + +/// Test failed token transfer with invalid discriminator +#[test_log::test] +fn test_program_exec_invalid_discriminator_fails() { + let mut context = setup_test_context().unwrap(); + let swig_authority = Keypair::new(); + let recipient = Keypair::new(); + + context + .svm + .airdrop(&swig_authority.pubkey(), 10_000_000_000) + .unwrap(); + context + .svm + .airdrop(&recipient.pubkey(), 10_000_000_000) + .unwrap(); + + let id = rand::random::<[u8; 32]>(); + let swig = Pubkey::find_program_address(&swig_account_seeds(&id), &program_id()).0; + let swig_wallet = + Pubkey::find_program_address(&swig_wallet_address_seeds(swig.as_ref()), &program_id()).0; + + let mint_pubkey = setup_mint(&mut context.svm, &context.default_payer).unwrap(); + let swig_wallet_ata = setup_ata( + &mut context.svm, + &mint_pubkey, + &swig_wallet, + &context.default_payer, + ) + .unwrap(); + let recipient_ata = setup_ata( + &mut context.svm, + &mint_pubkey, + &recipient.pubkey(), + &recipient, + ) + .unwrap(); + + mint_to( + &mut context.svm, + &mint_pubkey, + &context.default_payer, + &swig_wallet_ata, + 1000, + ) + .unwrap(); + + create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + // Add ProgramExec authority expecting VALID_DISCRIMINATOR + let program_exec_data = create_program_exec_authority_data(spl_token::ID, &VALID_DISCRIMINATOR); + + add_authority_with_ed25519_root( + &mut context, + &swig, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::ProgramExec, + authority: &program_exec_data, + }, + vec![ + ClientAction::Program(Program { + program_id: spl_token::ID.to_bytes(), + }), + ClientAction::All(All {}), + ], + ) + .unwrap(); + + context.svm.warp_to_slot(100); + + // Create mock program call with INVALID_DISCRIMINATOR + let mut invalid_instruction_data = INVALID_DISCRIMINATOR.to_vec(); + invalid_instruction_data.extend_from_slice(&100u64.to_le_bytes()); // amount + + let mock_program_ix = Instruction { + program_id: spl_token::ID, + accounts: vec![ + AccountMeta::new_readonly(swig, false), + AccountMeta::new_readonly(swig_wallet, false), + AccountMeta::new(swig_wallet_ata, false), + ], + data: invalid_instruction_data, + }; + + let transfer_ix = Instruction { + program_id: spl_token::ID, + accounts: vec![ + AccountMeta::new(swig_wallet_ata, false), + AccountMeta::new(recipient_ata, false), + AccountMeta::new(swig_wallet, false), + ], + data: TokenInstruction::Transfer { amount: 100 }.pack(), + }; + + // Use the ergonomic interface + let instructions = build_program_exec_sign_instructions( + swig, + swig_wallet, + swig_authority.pubkey(), + mock_program_ix, + transfer_ix, + 1, // role_id + ) + .unwrap(); + + let transfer_message = v0::Message::try_compile( + &swig_authority.pubkey(), + &instructions, + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let transfer_tx = + VersionedTransaction::try_new(VersionedMessage::V0(transfer_message), &[&swig_authority]) + .unwrap(); + + let res = context.svm.send_transaction(transfer_tx); + + // Should fail because the discriminator doesn't match + assert!( + res.is_err(), + "Transaction should fail with invalid discriminator" + ); + + if let Err(err) = res { + println!("Got expected error: {:?}", err.err); + } +} + +/// Test failed authentication with mismatched config account +#[test_log::test] +fn test_program_exec_mismatched_config_fails() { + let mut context = setup_test_context().unwrap(); + let swig_authority = Keypair::new(); + let recipient = Keypair::new(); + let wrong_config = Keypair::new(); + + context + .svm + .airdrop(&swig_authority.pubkey(), 10_000_000_000) + .unwrap(); + context + .svm + .airdrop(&recipient.pubkey(), 10_000_000_000) + .unwrap(); + context + .svm + .airdrop(&wrong_config.pubkey(), 10_000_000_000) + .unwrap(); + + let id = rand::random::<[u8; 32]>(); + let swig = Pubkey::find_program_address(&swig_account_seeds(&id), &program_id()).0; + let swig_wallet = + Pubkey::find_program_address(&swig_wallet_address_seeds(swig.as_ref()), &program_id()).0; + + let mint_pubkey = setup_mint(&mut context.svm, &context.default_payer).unwrap(); + let swig_wallet_ata = setup_ata( + &mut context.svm, + &mint_pubkey, + &swig_wallet, + &context.default_payer, + ) + .unwrap(); + let recipient_ata = setup_ata( + &mut context.svm, + &mint_pubkey, + &recipient.pubkey(), + &recipient, + ) + .unwrap(); + + mint_to( + &mut context.svm, + &mint_pubkey, + &context.default_payer, + &swig_wallet_ata, + 1000, + ) + .unwrap(); + + create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + let program_exec_data = create_program_exec_authority_data( + spl_token::ID, + &[3, 0, 0, 0, 0, 0, 0, 0], // Transfer discriminator + ); + + add_authority_with_ed25519_root( + &mut context, + &swig, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::ProgramExec, + authority: &program_exec_data, + }, + vec![ + ClientAction::Program(Program { + program_id: spl_token::ID.to_bytes(), + }), + ClientAction::All(All {}), + ], + ) + .unwrap(); + + context.svm.warp_to_slot(100); + + // Create mock program call with WRONG config account (first account) + let mock_program_ix = Instruction { + program_id: spl_token::ID, + accounts: vec![ + AccountMeta::new_readonly(wrong_config.pubkey(), false), // Wrong config! + AccountMeta::new_readonly(swig_wallet, false), + AccountMeta::new(swig_wallet_ata, false), + ], + data: TokenInstruction::Transfer { amount: 0 }.pack(), + }; + + let transfer_ix = Instruction { + program_id: spl_token::ID, + accounts: vec![ + AccountMeta::new(swig_wallet_ata, false), + AccountMeta::new(recipient_ata, false), + AccountMeta::new(swig_wallet, false), + ], + data: TokenInstruction::Transfer { amount: 100 }.pack(), + }; + + // Use the ergonomic interface + let instructions = build_program_exec_sign_instructions( + swig, + swig_wallet, + swig_authority.pubkey(), + mock_program_ix, + transfer_ix, + 1, // role_id + ) + .unwrap(); + + let transfer_message = v0::Message::try_compile( + &swig_authority.pubkey(), + &instructions, + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let transfer_tx = + VersionedTransaction::try_new(VersionedMessage::V0(transfer_message), &[&swig_authority]) + .unwrap(); + + let res = context.svm.send_transaction(transfer_tx); + + // Should fail because config account doesn't match + assert!( + res.is_err(), + "Transaction should fail with mismatched config account" + ); + + if let Err(err) = res { + println!("Got expected error: {:?}", err.err); + } +} + +/// Test failed authentication with mismatched wallet account +#[test_log::test] +fn test_program_exec_mismatched_wallet_fails() { + let mut context = setup_test_context().unwrap(); + let swig_authority = Keypair::new(); + let recipient = Keypair::new(); + let wrong_wallet = Keypair::new(); + + context + .svm + .airdrop(&swig_authority.pubkey(), 10_000_000_000) + .unwrap(); + context + .svm + .airdrop(&recipient.pubkey(), 10_000_000_000) + .unwrap(); + context + .svm + .airdrop(&wrong_wallet.pubkey(), 10_000_000_000) + .unwrap(); + + let id = rand::random::<[u8; 32]>(); + let swig = Pubkey::find_program_address(&swig_account_seeds(&id), &program_id()).0; + let swig_wallet = + Pubkey::find_program_address(&swig_wallet_address_seeds(swig.as_ref()), &program_id()).0; + + let mint_pubkey = setup_mint(&mut context.svm, &context.default_payer).unwrap(); + let swig_wallet_ata = setup_ata( + &mut context.svm, + &mint_pubkey, + &swig_wallet, + &context.default_payer, + ) + .unwrap(); + let recipient_ata = setup_ata( + &mut context.svm, + &mint_pubkey, + &recipient.pubkey(), + &recipient, + ) + .unwrap(); + + mint_to( + &mut context.svm, + &mint_pubkey, + &context.default_payer, + &swig_wallet_ata, + 1000, + ) + .unwrap(); + + create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + let program_exec_data = create_program_exec_authority_data( + spl_token::ID, + &[3, 0, 0, 0, 0, 0, 0, 0], // Transfer discriminator + ); + + add_authority_with_ed25519_root( + &mut context, + &swig, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::ProgramExec, + authority: &program_exec_data, + }, + vec![ + ClientAction::Program(Program { + program_id: spl_token::ID.to_bytes(), + }), + ClientAction::All(All {}), + ], + ) + .unwrap(); + + context.svm.warp_to_slot(100); + + // Create mock program call with WRONG wallet account (second account) + let mock_program_ix = Instruction { + program_id: spl_token::ID, + accounts: vec![ + AccountMeta::new_readonly(swig, false), + AccountMeta::new_readonly(wrong_wallet.pubkey(), false), // Wrong wallet! + AccountMeta::new(swig_wallet_ata, false), + ], + data: TokenInstruction::Transfer { amount: 0 }.pack(), + }; + + let transfer_ix = Instruction { + program_id: spl_token::ID, + accounts: vec![ + AccountMeta::new(swig_wallet_ata, false), + AccountMeta::new(recipient_ata, false), + AccountMeta::new(swig_wallet, false), + ], + data: TokenInstruction::Transfer { amount: 100 }.pack(), + }; + + // Use the ergonomic interface + let instructions = build_program_exec_sign_instructions( + swig, + swig_wallet, + swig_authority.pubkey(), + mock_program_ix, + transfer_ix, + 1, // role_id + ) + .unwrap(); + + let transfer_message = v0::Message::try_compile( + &swig_authority.pubkey(), + &instructions, + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let transfer_tx = + VersionedTransaction::try_new(VersionedMessage::V0(transfer_message), &[&swig_authority]) + .unwrap(); + + let res = context.svm.send_transaction(transfer_tx); + + // Should fail because wallet account doesn't match + assert!( + res.is_err(), + "Transaction should fail with mismatched wallet account" + ); + + if let Err(err) = res { + println!("Got expected error: {:?}", err.err); + } +} diff --git a/program/tests/sign_performance_test.rs b/program/tests/sign_performance_test.rs index fd7326c1..104f337b 100644 --- a/program/tests/sign_performance_test.rs +++ b/program/tests/sign_performance_test.rs @@ -191,7 +191,7 @@ fn test_token_transfer_performance_comparison() { ); // 3744 is the max difference in CU between the two transactions lets lower // this as far as possible but never increase it - assert!(swig_transfer_cu - regular_transfer_cu <= 3851); + assert!(swig_transfer_cu - regular_transfer_cu <= 3853); } #[test_log::test] @@ -305,5 +305,5 @@ fn test_sol_transfer_performance_comparison() { // Set a reasonable limit for the CU difference to avoid regressions // Similar to the token transfer test assertion - assert!(swig_transfer_cu - regular_transfer_cu <= 2196); + assert!(swig_transfer_cu - regular_transfer_cu <= 2198); } diff --git a/program/tests/sign_performance_v2_test.rs b/program/tests/sign_performance_v2_test.rs index 887d066c..0a52958b 100644 --- a/program/tests/sign_performance_v2_test.rs +++ b/program/tests/sign_performance_v2_test.rs @@ -190,7 +190,7 @@ fn test_token_transfer_performance_comparison_v2() { "Account difference (swig - regular): {} accounts", account_difference ); - assert!(swig_transfer_cu - regular_transfer_cu <= 3798); + assert!(swig_transfer_cu - regular_transfer_cu <= 3800); } #[test_log::test] @@ -310,5 +310,5 @@ fn test_sol_transfer_performance_comparison_v2() { account_difference ); - assert!(swig_transfer_cu - regular_transfer_cu <= 3253); + assert!(swig_transfer_cu - regular_transfer_cu <= 3255); } diff --git a/program/tests/sign_v2.rs b/program/tests/sign_v2.rs index 54cc1a77..e4446f64 100644 --- a/program/tests/sign_v2.rs +++ b/program/tests/sign_v2.rs @@ -2384,7 +2384,8 @@ fn test_sign_v2_minimum_rent_check() { .airdrop(&swig_wallet_address, 1_000_000_000) .unwrap(); - // Failure case - transfer amount is greater than the swig wallet balance and the rent exempt minimum + // Failure case - transfer amount is greater than the swig wallet balance and + // the rent exempt minimum let transfer_amount = 1_000_000_000 + 1; // swig wallet balance + 1 let transfer_ix = system_instruction::transfer(&swig_wallet_address, &recipient.pubkey(), transfer_amount); @@ -2430,7 +2431,8 @@ fn test_sign_v2_minimum_rent_check() { assert!(result.is_err(), "Transfer should be rejected"); - // Success case - transfer amount is less than the swig wallet balance and the rent exempt minimum + // Success case - transfer amount is less than the swig wallet balance and the + // rent exempt minimum let transfer_amount = 1_000_000_000; // swig wallet balance let transfer_ix = system_instruction::transfer(&swig_wallet_address, &recipient.pubkey(), transfer_amount); diff --git a/rust-sdk/src/client_role.rs b/rust-sdk/src/client_role.rs index 116269f8..4f816daf 100644 --- a/rust-sdk/src/client_role.rs +++ b/rust-sdk/src/client_role.rs @@ -1203,14 +1203,15 @@ impl ClientRole for Ed25519SessionClientRole { instructions: Vec, _current_slot: Option, ) -> Result, SwigError> { - let session_authority_pubkey = Pubkey::new_from_array(self.session_authority.public_key); + // Session authorities sign as Ed25519 with the session_key + let session_key = Pubkey::new_from_array(self.session_authority.session_key); let mut signed_instructions = Vec::new(); for instruction in instructions { let swig_signed_instruction = SignInstruction::new_ed25519( swig_account, payer, - session_authority_pubkey, + session_key, instruction, role_id, )?; @@ -1229,10 +1230,13 @@ impl ClientRole for Ed25519SessionClientRole { actions: Vec, _current_slot: Option, ) -> Result, SwigError> { + // Session authorities sign as Ed25519 with the session_key + let session_key = Pubkey::new_from_array(self.session_authority.session_key); + let instructions = AddAuthorityInstruction::new_with_ed25519_authority( swig_account, payer, - self.session_authority.public_key.into(), + session_key, role_id, AuthorityConfig { authority_type: new_authority_type, @@ -1252,11 +1256,14 @@ impl ClientRole for Ed25519SessionClientRole { authority_to_remove_id: u32, _current_slot: Option, ) -> Result, SwigError> { + // Session authorities sign as Ed25519 with the session_key + let session_key = Pubkey::new_from_array(self.session_authority.session_key); + Ok(vec![ RemoveAuthorityInstruction::new_with_ed25519_authority( swig_account, payer, - self.session_authority.public_key.into(), + session_key, role_id, authority_to_remove_id, )?, @@ -1272,11 +1279,14 @@ impl ClientRole for Ed25519SessionClientRole { update_data: UpdateAuthorityData, current_slot: Option, ) -> Result, SwigError> { + // Session authorities sign as Ed25519 with the session_key + let session_key = Pubkey::new_from_array(self.session_authority.session_key); + Ok(vec![ UpdateAuthorityInstruction::new_with_ed25519_authority( swig_account, payer, - self.session_authority.public_key.into(), + session_key, role_id, authority_to_update_id, update_data, @@ -1293,10 +1303,13 @@ impl ClientRole for Ed25519SessionClientRole { session_duration: u64, _current_slot: Option, ) -> Result, SwigError> { + // For create_session, use the native authority type (Ed25519) + let authority = Pubkey::new_from_array(self.session_authority.public_key); + Ok(vec![CreateSessionInstruction::new_with_ed25519_authority( swig_account, payer, - self.session_authority.public_key.into(), + authority, role_id, session_key, session_duration, @@ -1305,65 +1318,132 @@ impl ClientRole for Ed25519SessionClientRole { fn create_sub_account_instruction( &self, - _swig_account: Pubkey, - _payer: Pubkey, - _role_id: u32, - _sub_account: Pubkey, - _sub_account_bump: u8, + swig_account: Pubkey, + payer: Pubkey, + role_id: u32, + sub_account: Pubkey, + sub_account_bump: u8, _current_slot: Option, ) -> Result, SwigError> { - todo!("Session authorities don't support sub-account creation") + let session_key = Pubkey::new_from_array(self.session_authority.public_key); + Ok(vec![ + CreateSubAccountInstruction::new_with_ed25519_authority( + swig_account, + session_key, + payer, + sub_account, + role_id, + sub_account_bump, + )?, + ]) } fn sub_account_sign_instruction( &self, - _swig_account: Pubkey, - _sub_account: Pubkey, - _role_id: u32, - _instructions: Vec, + swig_account: Pubkey, + sub_account: Pubkey, + role_id: u32, + instructions: Vec, _current_slot: Option, ) -> Result, SwigError> { - todo!("Session authorities don't support sub-account signing") + let session_key = Pubkey::new_from_array(self.session_authority.public_key); + Ok(vec![SubAccountSignInstruction::new_with_ed25519_authority( + swig_account, + sub_account, + session_key, + role_id, + instructions, + )?]) } fn withdraw_from_sub_account_instruction( &self, - _swig_account: Pubkey, - _payer: Pubkey, - _sub_account: Pubkey, - _role_id: u32, - _amount: u64, + swig_account: Pubkey, + payer: Pubkey, + sub_account: Pubkey, + role_id: u32, + amount: u64, _current_slot: Option, ) -> Result, SwigError> { - todo!("Session authorities don't support sub-account operations") + let session_key = Pubkey::new_from_array(self.session_authority.public_key); + + // Derive the swig wallet address + let (swig_wallet_address, _) = Pubkey::find_program_address( + &swig_state::swig::swig_wallet_address_seeds(swig_account.as_ref()), + &swig_interface::program_id(), + ); + + Ok(vec![ + WithdrawFromSubAccountInstruction::new_with_ed25519_authority( + swig_account, + session_key, + payer, + sub_account, + swig_wallet_address, + role_id, + amount, + )?, + ]) } fn withdraw_token_from_sub_account_instruction( &self, - _swig_account: Pubkey, - _payer: Pubkey, - _sub_account: Pubkey, - _sub_account_token: Pubkey, - _swig_token: Pubkey, - _token_program: Pubkey, - _role_id: u32, - _amount: u64, + swig_account: Pubkey, + payer: Pubkey, + sub_account: Pubkey, + sub_account_token: Pubkey, + swig_token: Pubkey, + token_program: Pubkey, + role_id: u32, + amount: u64, _current_slot: Option, ) -> Result, SwigError> { - todo!("Session authorities don't support sub-account operations") + let session_key = Pubkey::new_from_array(self.session_authority.public_key); + + // Derive the swig wallet address + let (swig_wallet_address, _) = Pubkey::find_program_address( + &swig_state::swig::swig_wallet_address_seeds(swig_account.as_ref()), + &swig_interface::program_id(), + ); + + Ok(vec![ + WithdrawFromSubAccountInstruction::new_token_with_ed25519_authority( + swig_account, + session_key, + payer, + sub_account, + swig_wallet_address, + sub_account_token, + swig_token, + token_program, + role_id, + amount, + )?, + ]) } fn toggle_sub_account_instruction( &self, - _swig_account: Pubkey, - _payer: Pubkey, - _sub_account: Pubkey, - _role_id: u32, - _auth_role_id: u32, - _enabled: bool, + swig_account: Pubkey, + payer: Pubkey, + sub_account: Pubkey, + role_id: u32, + auth_role_id: u32, + enabled: bool, _current_slot: Option, ) -> Result, SwigError> { - todo!("Session authorities don't support sub-account operations") + let session_key = Pubkey::new_from_array(self.session_authority.public_key); + Ok(vec![ + ToggleSubAccountInstruction::new_with_ed25519_authority( + swig_account, + session_key, + payer, + sub_account, + role_id, + auth_role_id, + enabled, + )?, + ]) } fn authority_type(&self) -> AuthorityType { @@ -1452,18 +1532,17 @@ impl ClientRole for Secp256k1SessionClientRole { payer: Pubkey, role_id: u32, instructions: Vec, - current_slot: Option, + _current_slot: Option, ) -> Result, SwigError> { - let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; + // Session authorities sign as Ed25519 with the session_key + let session_key = Pubkey::new_from_array(self.session_authority.session_key); let mut signed_instructions = Vec::new(); for instruction in instructions { - let swig_signed_instruction = SignInstruction::new_secp256k1( + let swig_signed_instruction = SignInstruction::new_ed25519( swig_account, payer, - &self.signing_fn, - current_slot, - 0u32, + session_key, instruction, role_id, )?; @@ -1480,16 +1559,15 @@ impl ClientRole for Secp256k1SessionClientRole { new_authority_type: AuthorityType, new_authority: &[u8], actions: Vec, - current_slot: Option, + _current_slot: Option, ) -> Result, SwigError> { - let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; + // Session authorities sign as Ed25519 with the session_key + let session_key = Pubkey::new_from_array(self.session_authority.session_key); - let instructions = AddAuthorityInstruction::new_with_secp256k1_authority( + let instructions = AddAuthorityInstruction::new_with_ed25519_authority( swig_account, payer, - &self.signing_fn, - current_slot, - 0u32, + session_key, role_id, AuthorityConfig { authority_type: new_authority_type, @@ -1507,20 +1585,18 @@ impl ClientRole for Secp256k1SessionClientRole { payer: Pubkey, role_id: u32, authority_to_remove_id: u32, - current_slot: Option, + _current_slot: Option, ) -> Result, SwigError> { - let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; - let new_odometer = self.odometer.wrapping_add(1); + // Session authorities sign as Ed25519 with the session_key + let session_key = Pubkey::new_from_array(self.session_authority.session_key); Ok(vec![ - RemoveAuthorityInstruction::new_with_secp256k1_authority( + RemoveAuthorityInstruction::new_with_ed25519_authority( swig_account, payer, - &self.signing_fn, - current_slot, - new_odometer, - authority_to_remove_id, + session_key, role_id, + authority_to_remove_id, )?, ]) } @@ -1532,18 +1608,16 @@ impl ClientRole for Secp256k1SessionClientRole { role_id: u32, authority_to_update_id: u32, update_data: UpdateAuthorityData, - current_slot: Option, + _current_slot: Option, ) -> Result, SwigError> { - let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; - let new_odometer = self.odometer.wrapping_add(1); + // Session authorities sign as Ed25519 with the session_key + let session_key = Pubkey::new_from_array(self.session_authority.session_key); Ok(vec![ - UpdateAuthorityInstruction::new_with_secp256k1_authority( + UpdateAuthorityInstruction::new_with_ed25519_authority( swig_account, payer, - &self.signing_fn, - current_slot, - new_odometer, + session_key, role_id, authority_to_update_id, update_data, @@ -1560,9 +1634,9 @@ impl ClientRole for Secp256k1SessionClientRole { session_duration: u64, current_slot: Option, ) -> Result, SwigError> { + // For create_session, use the native authority type (Secp256k1) let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; let new_odometer = self.odometer.wrapping_add(1); - Ok(vec![ CreateSessionInstruction::new_with_secp256k1_authority( swig_account, @@ -1579,65 +1653,140 @@ impl ClientRole for Secp256k1SessionClientRole { fn create_sub_account_instruction( &self, - _swig_account: Pubkey, - _payer: Pubkey, - _role_id: u32, - _sub_account: Pubkey, - _sub_account_bump: u8, + swig_account: Pubkey, + payer: Pubkey, + role_id: u32, + sub_account: Pubkey, + sub_account_bump: u8, _current_slot: Option, ) -> Result, SwigError> { - todo!("Session authorities don't support sub-account creation") + // Session authorities sign as Ed25519 with the session_key + let session_key = Pubkey::new_from_array(self.session_authority.session_key); + + Ok(vec![ + CreateSubAccountInstruction::new_with_ed25519_authority( + swig_account, + session_key, + payer, + sub_account, + role_id, + sub_account_bump, + )?, + ]) } fn sub_account_sign_instruction( &self, - _swig_account: Pubkey, - _sub_account: Pubkey, - _role_id: u32, - _instructions: Vec, + swig_account: Pubkey, + sub_account: Pubkey, + role_id: u32, + instructions: Vec, _current_slot: Option, ) -> Result, SwigError> { - todo!("Session authorities don't support sub-account signing") + // Session authorities sign as Ed25519 with the session_key + let session_key = Pubkey::new_from_array(self.session_authority.session_key); + + Ok(vec![SubAccountSignInstruction::new_with_ed25519_authority( + swig_account, + sub_account, + session_key, + role_id, + instructions, + )?]) } fn withdraw_from_sub_account_instruction( &self, - _swig_account: Pubkey, - _payer: Pubkey, - _sub_account: Pubkey, - _role_id: u32, - _amount: u64, + swig_account: Pubkey, + payer: Pubkey, + sub_account: Pubkey, + role_id: u32, + amount: u64, _current_slot: Option, ) -> Result, SwigError> { - todo!("Session authorities don't support sub-account operations") + // Session authorities sign as Ed25519 with the session_key + let session_key = Pubkey::new_from_array(self.session_authority.session_key); + + // Derive the swig wallet address + let (swig_wallet_address, _) = Pubkey::find_program_address( + &swig_state::swig::swig_wallet_address_seeds(swig_account.as_ref()), + &swig_interface::program_id(), + ); + + Ok(vec![ + WithdrawFromSubAccountInstruction::new_with_ed25519_authority( + swig_account, + session_key, + payer, + sub_account, + swig_wallet_address, + role_id, + amount, + )?, + ]) } fn withdraw_token_from_sub_account_instruction( &self, - _swig_account: Pubkey, - _payer: Pubkey, - _sub_account: Pubkey, - _sub_account_token: Pubkey, - _swig_token: Pubkey, - _token_program: Pubkey, - _role_id: u32, - _amount: u64, + swig_account: Pubkey, + payer: Pubkey, + sub_account: Pubkey, + sub_account_token: Pubkey, + swig_token: Pubkey, + token_program: Pubkey, + role_id: u32, + amount: u64, _current_slot: Option, ) -> Result, SwigError> { - todo!("Session authorities don't support sub-account operations") + // Session authorities sign as Ed25519 with the session_key + let session_key = Pubkey::new_from_array(self.session_authority.session_key); + + // Derive the swig wallet address + let (swig_wallet_address, _) = Pubkey::find_program_address( + &swig_state::swig::swig_wallet_address_seeds(swig_account.as_ref()), + &swig_interface::program_id(), + ); + + Ok(vec![ + WithdrawFromSubAccountInstruction::new_token_with_ed25519_authority( + swig_account, + session_key, + payer, + sub_account, + swig_wallet_address, + sub_account_token, + swig_token, + token_program, + role_id, + amount, + )?, + ]) } fn toggle_sub_account_instruction( &self, - _swig_account: Pubkey, - _payer: Pubkey, - _sub_account: Pubkey, - _role_id: u32, - _auth_role_id: u32, - _enabled: bool, + swig_account: Pubkey, + payer: Pubkey, + sub_account: Pubkey, + role_id: u32, + auth_role_id: u32, + enabled: bool, _current_slot: Option, ) -> Result, SwigError> { - todo!("Session authorities don't support sub-account operations") + // Session authorities sign as Ed25519 with the session_key + let session_key = Pubkey::new_from_array(self.session_authority.session_key); + + Ok(vec![ + ToggleSubAccountInstruction::new_with_ed25519_authority( + swig_account, + session_key, + payer, + sub_account, + role_id, + auth_role_id, + enabled, + )?, + ]) } fn authority_type(&self) -> AuthorityType { @@ -1668,20 +1817,18 @@ impl ClientRole for Secp256k1SessionClientRole { swig_wallet_address: Pubkey, role_id: u32, instructions: Vec, - current_slot: Option, + _current_slot: Option, transaction_signers: &[Pubkey], ) -> Result, SwigError> { - let mut signed_instructions = Vec::new(); - let current_slot = current_slot.ok_or(SwigError::SlotRequired)?; - let new_odometer = self.odometer.wrapping_add(1); + // Session authorities sign as Ed25519 with the session_key + let session_key = Pubkey::new_from_array(self.session_authority.session_key); + let mut signed_instructions = Vec::new(); for instruction in instructions { - let swig_signed_instruction = SignV2Instruction::new_secp256k1_with_signers( + let swig_signed_instruction = SignV2Instruction::new_ed25519_with_signers( swig_account, swig_wallet_address, - &self.signing_fn, - current_slot, - new_odometer, + session_key, instruction, role_id, transaction_signers, @@ -1731,24 +1878,21 @@ impl ClientRole for Secp256r1SessionClientRole { payer: Pubkey, role_id: u32, instructions: Vec, - current_slot: Option, + _current_slot: Option, ) -> Result, SwigError> { - let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; - let new_odometer = self.odometer.wrapping_add(1); + // Session authorities sign as Ed25519 with the session_key + let session_key = Pubkey::new_from_array(self.session_authority.session_key); let mut signed_instructions = Vec::new(); for instruction in instructions { - let swig_signed_instruction = SignInstruction::new_secp256r1( + let swig_signed_instruction = SignInstruction::new_ed25519( swig_account, payer, - &self.signing_fn, - current_slot, - new_odometer, + session_key, instruction, role_id, - &self.session_authority.public_key, )?; - signed_instructions.extend(swig_signed_instruction); + signed_instructions.push(swig_signed_instruction); } Ok(signed_instructions) } @@ -1761,18 +1905,16 @@ impl ClientRole for Secp256r1SessionClientRole { new_authority_type: AuthorityType, new_authority: &[u8], actions: Vec, - current_slot: Option, + _current_slot: Option, ) -> Result, SwigError> { - let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; - let new_odometer = self.odometer.wrapping_add(1); - let instructions = AddAuthorityInstruction::new_with_secp256r1_authority( + // Session authorities sign as Ed25519 with the session_key + let session_key = Pubkey::new_from_array(self.session_authority.session_key); + + let instructions = AddAuthorityInstruction::new_with_ed25519_authority( swig_account, payer, - &self.signing_fn, - current_slot, - new_odometer, + session_key, role_id, - &self.session_authority.public_key, AuthorityConfig { authority_type: new_authority_type, authority: new_authority, @@ -1780,7 +1922,7 @@ impl ClientRole for Secp256r1SessionClientRole { actions, )?; - Ok(instructions) + Ok(vec![instructions]) } fn remove_authority_instruction( @@ -1789,23 +1931,20 @@ impl ClientRole for Secp256r1SessionClientRole { payer: Pubkey, role_id: u32, authority_to_remove_id: u32, - current_slot: Option, + _current_slot: Option, ) -> Result, SwigError> { - let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; - let new_odometer = self.odometer.wrapping_add(1); + // Session authorities sign as Ed25519 with the session_key + let session_key = Pubkey::new_from_array(self.session_authority.session_key); - let instructions = RemoveAuthorityInstruction::new_with_secp256r1_authority( - swig_account, - payer, - &self.signing_fn, - current_slot, - new_odometer, - role_id, - authority_to_remove_id, - &self.session_authority.public_key, - )?; - - Ok(instructions) + Ok(vec![ + RemoveAuthorityInstruction::new_with_ed25519_authority( + swig_account, + payer, + session_key, + role_id, + authority_to_remove_id, + )?, + ]) } fn update_authority_instruction( @@ -1815,22 +1954,21 @@ impl ClientRole for Secp256r1SessionClientRole { role_id: u32, authority_to_update_id: u32, update_data: UpdateAuthorityData, - current_slot: Option, + _current_slot: Option, ) -> Result, SwigError> { - let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; - let new_odometer = self.odometer.wrapping_add(1); + // Session authorities sign as Ed25519 with the session_key + let session_key = Pubkey::new_from_array(self.session_authority.session_key); - Ok(UpdateAuthorityInstruction::new_with_secp256r1_authority( - swig_account, - payer, - &self.signing_fn, - current_slot, - new_odometer, - role_id, - authority_to_update_id, - update_data, - &self.session_authority.public_key, - )?) + Ok(vec![ + UpdateAuthorityInstruction::new_with_ed25519_authority( + swig_account, + payer, + session_key, + role_id, + authority_to_update_id, + update_data, + )?, + ]) } fn create_session_instruction( @@ -1842,6 +1980,7 @@ impl ClientRole for Secp256r1SessionClientRole { session_duration: u64, current_slot: Option, ) -> Result, SwigError> { + // For create_session, use the native authority type (Secp256r1) let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; let new_odometer = self.odometer.wrapping_add(1); @@ -1862,65 +2001,140 @@ impl ClientRole for Secp256r1SessionClientRole { fn create_sub_account_instruction( &self, - _swig_account: Pubkey, - _payer: Pubkey, - _role_id: u32, - _sub_account: Pubkey, - _sub_account_bump: u8, + swig_account: Pubkey, + payer: Pubkey, + role_id: u32, + sub_account: Pubkey, + sub_account_bump: u8, _current_slot: Option, ) -> Result, SwigError> { - todo!("Session authorities don't support sub-account creation") + // Session authorities sign as Ed25519 with the session_key + let session_key = Pubkey::new_from_array(self.session_authority.session_key); + + Ok(vec![ + CreateSubAccountInstruction::new_with_ed25519_authority( + swig_account, + session_key, + payer, + sub_account, + role_id, + sub_account_bump, + )?, + ]) } fn sub_account_sign_instruction( &self, - _swig_account: Pubkey, - _sub_account: Pubkey, - _role_id: u32, - _instructions: Vec, + swig_account: Pubkey, + sub_account: Pubkey, + role_id: u32, + instructions: Vec, _current_slot: Option, ) -> Result, SwigError> { - todo!("Session authorities don't support sub-account signing") + // Session authorities sign as Ed25519 with the session_key + let session_key = Pubkey::new_from_array(self.session_authority.session_key); + + Ok(vec![SubAccountSignInstruction::new_with_ed25519_authority( + swig_account, + sub_account, + session_key, + role_id, + instructions, + )?]) } fn withdraw_from_sub_account_instruction( &self, - _swig_account: Pubkey, - _payer: Pubkey, - _sub_account: Pubkey, - _role_id: u32, - _amount: u64, + swig_account: Pubkey, + payer: Pubkey, + sub_account: Pubkey, + role_id: u32, + amount: u64, _current_slot: Option, ) -> Result, SwigError> { - todo!("Session authorities don't support sub-account operations") + // Session authorities sign as Ed25519 with the session_key + let session_key = Pubkey::new_from_array(self.session_authority.session_key); + + // Derive the swig wallet address + let (swig_wallet_address, _) = Pubkey::find_program_address( + &swig_state::swig::swig_wallet_address_seeds(swig_account.as_ref()), + &swig_interface::program_id(), + ); + + Ok(vec![ + WithdrawFromSubAccountInstruction::new_with_ed25519_authority( + swig_account, + session_key, + payer, + sub_account, + swig_wallet_address, + role_id, + amount, + )?, + ]) } fn withdraw_token_from_sub_account_instruction( &self, - _swig_account: Pubkey, - _payer: Pubkey, - _sub_account: Pubkey, - _sub_account_token: Pubkey, - _swig_token: Pubkey, - _token_program: Pubkey, - _role_id: u32, - _amount: u64, + swig_account: Pubkey, + payer: Pubkey, + sub_account: Pubkey, + sub_account_token: Pubkey, + swig_token: Pubkey, + token_program: Pubkey, + role_id: u32, + amount: u64, _current_slot: Option, ) -> Result, SwigError> { - todo!("Session authorities don't support sub-account operations") + // Session authorities sign as Ed25519 with the session_key + let session_key = Pubkey::new_from_array(self.session_authority.session_key); + + // Derive the swig wallet address + let (swig_wallet_address, _) = Pubkey::find_program_address( + &swig_state::swig::swig_wallet_address_seeds(swig_account.as_ref()), + &swig_interface::program_id(), + ); + + Ok(vec![ + WithdrawFromSubAccountInstruction::new_token_with_ed25519_authority( + swig_account, + session_key, + payer, + sub_account, + swig_wallet_address, + sub_account_token, + swig_token, + token_program, + role_id, + amount, + )?, + ]) } fn toggle_sub_account_instruction( &self, - _swig_account: Pubkey, - _payer: Pubkey, - _sub_account: Pubkey, - _role_id: u32, - _auth_role_id: u32, - _enabled: bool, + swig_account: Pubkey, + payer: Pubkey, + sub_account: Pubkey, + role_id: u32, + auth_role_id: u32, + enabled: bool, _current_slot: Option, ) -> Result, SwigError> { - todo!("Session authorities don't support sub-account operations") + // Session authorities sign as Ed25519 with the session_key + let session_key = Pubkey::new_from_array(self.session_authority.session_key); + + Ok(vec![ + ToggleSubAccountInstruction::new_with_ed25519_authority( + swig_account, + session_key, + payer, + sub_account, + role_id, + auth_role_id, + enabled, + )?, + ]) } fn authority_type(&self) -> AuthorityType { @@ -1951,26 +2165,440 @@ impl ClientRole for Secp256r1SessionClientRole { swig_wallet_address: Pubkey, role_id: u32, instructions: Vec, - current_slot: Option, + _current_slot: Option, transaction_signers: &[Pubkey], ) -> Result, SwigError> { + // Session authorities sign as Ed25519 with the session_key + let session_key = Pubkey::new_from_array(self.session_authority.session_key); + let mut signed_instructions = Vec::new(); - let current_slot = current_slot.ok_or(SwigError::SlotRequired)?; - let new_odometer = self.odometer.wrapping_add(1); for instruction in instructions { - let swig_signed_instructions = SignV2Instruction::new_secp256r1_with_signers( + let swig_signed_instruction = SignV2Instruction::new_ed25519_with_signers( swig_account, swig_wallet_address, - &self.signing_fn, - current_slot, - new_odometer, + session_key, instruction, role_id, - &self.session_authority.public_key, transaction_signers, )?; - signed_instructions.extend(swig_signed_instructions); + signed_instructions.push(swig_signed_instruction); } Ok(signed_instructions) } } + +/// Client role for ProgramExec authority. +/// +/// This authority type validates that a preceding instruction in the +/// transaction matches the configured program ID and instruction discriminator. +/// The preceding instruction must be provided when creating sign instructions. +/// +/// ProgramExec authority works with SignV2 only, as it requires separate config +/// and wallet address accounts. +pub struct ProgramExecClientRole +where + F: Fn() -> Instruction, +{ + /// The program ID that must execute the preceding instruction + pub program_id: Pubkey, + /// The instruction discriminator/prefix to match + pub instruction_prefix: Vec, + /// Function that provides the preceding instruction for authentication + pub preceding_instruction_fn: F, +} + +impl ProgramExecClientRole +where + F: Fn() -> Instruction, +{ + /// Creates a new ProgramExecClientRole. + /// + /// # Arguments + /// * `program_id` - The program ID that must execute the preceding + /// instruction + /// * `instruction_prefix` - The instruction discriminator/prefix to match + /// (up to 40 bytes) + /// * `preceding_instruction_fn` - Function that generates the preceding + /// instruction for authentication + pub fn new( + program_id: Pubkey, + instruction_prefix: Vec, + preceding_instruction_fn: F, + ) -> Self { + Self { + program_id, + instruction_prefix, + preceding_instruction_fn, + } + } + + /// Creates authority data for a ProgramExec authority. + /// + /// This is a convenience method that generates the authority data bytes + /// needed when adding a ProgramExec authority to a Swig wallet. + pub fn authority_data(&self) -> Vec { + use swig_state::authority::programexec::ProgramExecAuthority; + ProgramExecAuthority::create_authority_data( + &self.program_id.to_bytes(), + &self.instruction_prefix, + ) + } + + /// Creates a sign instruction with a preceding program instruction. + /// + /// This method creates both the preceding instruction and the sign + /// instruction that must be executed together in the same transaction. + /// + /// # Arguments + /// * `swig_account` - The Swig wallet config account + /// * `swig_wallet_address` - The Swig wallet address PDA + /// * `payer` - The transaction fee payer + /// * `preceding_instruction` - The instruction that must precede the sign + /// instruction + /// * `inner_instruction` - The instruction to be signed by the Swig wallet + /// * `role_id` - The role ID that has ProgramExec authority + /// + /// # Returns + /// Returns a vector containing both instructions that must be executed in + /// order: [preceding_instruction, sign_instruction] + pub fn sign_with_program_exec( + &self, + swig_account: Pubkey, + swig_wallet_address: Pubkey, + payer: Pubkey, + preceding_instruction: Instruction, + inner_instruction: Instruction, + role_id: u32, + ) -> Result, SwigError> { + SignV2Instruction::new_program_exec( + swig_account, + swig_wallet_address, + payer, + preceding_instruction, + inner_instruction, + role_id, + ) + .map_err(|e| SwigError::InterfaceError(e.to_string())) + } +} + +impl ClientRole for ProgramExecClientRole +where + F: Fn() -> Instruction, +{ + fn sign_instruction( + &self, + swig_account: Pubkey, + payer: Pubkey, + role_id: u32, + instructions: Vec, + _current_slot: Option, + ) -> Result, SwigError> { + Err(SwigError::InterfaceError( + "ProgramExecClientRole only supports SignV2 instructions".to_string(), + )) + } + + fn add_authority_instruction( + &self, + swig_account: Pubkey, + payer: Pubkey, + role_id: u32, + new_authority_type: AuthorityType, + new_authority: &[u8], + actions: Vec, + _current_slot: Option, + ) -> Result, SwigError> { + let preceding_instruction = (self.preceding_instruction_fn)(); + + AddAuthorityInstruction::new_with_program_exec( + swig_account, + payer, + preceding_instruction, + role_id, + AuthorityConfig { + authority_type: new_authority_type, + authority: new_authority, + }, + actions, + ) + .map_err(|e| SwigError::InterfaceError(e.to_string())) + } + + fn update_authority_instruction( + &self, + swig_account: Pubkey, + payer: Pubkey, + role_id: u32, + authority_to_update_id: u32, + update_data: UpdateAuthorityData, + _current_slot: Option, + ) -> Result, SwigError> { + let preceding_instruction = (self.preceding_instruction_fn)(); + + UpdateAuthorityInstruction::new_with_program_exec( + swig_account, + payer, + preceding_instruction, + role_id, + authority_to_update_id, + update_data, + ) + .map_err(|e| SwigError::InterfaceError(e.to_string())) + } + + fn remove_authority_instruction( + &self, + swig_account: Pubkey, + payer: Pubkey, + role_id: u32, + authority_to_remove_id: u32, + _current_slot: Option, + ) -> Result, SwigError> { + let preceding_instruction = (self.preceding_instruction_fn)(); + + RemoveAuthorityInstruction::new_with_program_exec( + swig_account, + payer, + preceding_instruction, + role_id, + authority_to_remove_id, + ) + .map_err(|e| SwigError::InterfaceError(e.to_string())) + } + + fn create_session_instruction( + &self, + swig_account: Pubkey, + payer: Pubkey, + role_id: u32, + session_key: Pubkey, + session_duration: u64, + _current_slot: Option, + ) -> Result, SwigError> { + let preceding_instruction = (self.preceding_instruction_fn)(); + + CreateSessionInstruction::new_with_program_exec( + swig_account, + payer, + preceding_instruction, + role_id, + session_duration, + session_key, + ) + .map_err(|e| SwigError::InterfaceError(e.to_string())) + } + + fn sub_account_sign_instruction( + &self, + swig_account: Pubkey, + sub_account: Pubkey, + role_id: u32, + instructions: Vec, + _current_slot: Option, + ) -> Result, SwigError> { + // Note: SubAccountSign requires a payer parameter but the trait doesn't provide + // it We'll use the swig_account as a placeholder since the actual payer + // needs to be determined by the caller + let payer = swig_account; // Caller should ensure correct payer is set + + let preceding_instruction = (self.preceding_instruction_fn)(); + + SubAccountSignInstruction::new_with_program_exec( + swig_account, + sub_account, + payer, + preceding_instruction, + role_id, + instructions, + ) + .map_err(|e| SwigError::InterfaceError(e.to_string())) + } + + fn withdraw_token_from_sub_account_instruction( + &self, + swig_account: Pubkey, + payer: Pubkey, + sub_account: Pubkey, + sub_account_token: Pubkey, + swig_token: Pubkey, + token_program: Pubkey, + role_id: u32, + amount: u64, + _current_slot: Option, + ) -> Result, SwigError> { + // Get swig_wallet_address + let (swig_wallet_address, _) = Pubkey::find_program_address( + &swig_state::swig::swig_wallet_address_seeds(swig_account.as_ref()), + &swig_interface::program_id(), + ); + + let preceding_instruction = (self.preceding_instruction_fn)(); + + WithdrawFromSubAccountInstruction::new_token_with_program_exec( + swig_account, + payer, + preceding_instruction, + sub_account, + swig_wallet_address, + sub_account_token, + swig_token, + token_program, + role_id, + amount, + ) + .map_err(|e| SwigError::InterfaceError(e.to_string())) + } + + fn create_sub_account_instruction( + &self, + swig_account: Pubkey, + payer: Pubkey, + role_id: u32, + sub_account: Pubkey, + sub_account_bump: u8, + _current_slot: Option, + ) -> Result, SwigError> { + let preceding_instruction = (self.preceding_instruction_fn)(); + + CreateSubAccountInstruction::new_with_program_exec( + swig_account, + payer, + preceding_instruction, + sub_account, + role_id, + sub_account_bump, + ) + .map_err(|e| SwigError::InterfaceError(e.to_string())) + } + + fn withdraw_from_sub_account_instruction( + &self, + swig_account: Pubkey, + payer: Pubkey, + sub_account: Pubkey, + role_id: u32, + amount: u64, + _current_slot: Option, + ) -> Result, SwigError> { + // Get swig_wallet_address + let (swig_wallet_address, _) = Pubkey::find_program_address( + &swig_state::swig::swig_wallet_address_seeds(swig_account.as_ref()), + &swig_interface::program_id(), + ); + + let preceding_instruction = (self.preceding_instruction_fn)(); + + WithdrawFromSubAccountInstruction::new_with_program_exec( + swig_account, + payer, + preceding_instruction, + sub_account, + swig_wallet_address, + role_id, + amount, + ) + .map_err(|e| SwigError::InterfaceError(e.to_string())) + } + + fn toggle_sub_account_instruction( + &self, + swig_account: Pubkey, + payer: Pubkey, + sub_account: Pubkey, + role_id: u32, + auth_role_id: u32, + enabled: bool, + _current_slot: Option, + ) -> Result, SwigError> { + let preceding_instruction = (self.preceding_instruction_fn)(); + + ToggleSubAccountInstruction::new_with_program_exec( + swig_account, + payer, + preceding_instruction, + sub_account, + role_id, + auth_role_id, + enabled, + ) + .map_err(|e| SwigError::InterfaceError(e.to_string())) + } + + fn authority_type(&self) -> AuthorityType { + AuthorityType::ProgramExec + } + + fn authority_bytes(&self) -> Result, SwigError> { + Ok(self.authority_data()) + } + + fn odometer(&self) -> Result { + Err(SwigError::InterfaceError( + "ProgramExec authority does not use odometer".to_string(), + )) + } + + fn increment_odometer(&mut self) -> Result<(), SwigError> { + Err(SwigError::InterfaceError( + "ProgramExec authority does not use odometer".to_string(), + )) + } + + fn update_odometer(&mut self, _odometer: u32) -> Result<(), SwigError> { + Err(SwigError::InterfaceError( + "ProgramExec authority does not use odometer".to_string(), + )) + } + + fn sign_v2_instruction( + &self, + swig_account: Pubkey, + swig_wallet_address: Pubkey, + role_id: u32, + instructions: Vec, + _current_slot: Option, + transaction_signers: &[Pubkey], + ) -> Result, SwigError> { + // Build the inner instruction using compact_instructions + let base_accounts = vec![ + solana_program::instruction::AccountMeta::new(swig_account, false), + solana_program::instruction::AccountMeta::new(swig_wallet_address, false), + ]; + + // Add transaction signers as readonly signers + let mut accounts_with_signers = base_accounts; + for signer in transaction_signers { + accounts_with_signers.push(solana_program::instruction::AccountMeta::new_readonly( + *signer, true, + )); + } + + let (_, compact_ixs) = + swig_interface::compact_instructions(swig_account, accounts_with_signers, instructions); + + let inner_instruction = solana_program::instruction::Instruction { + program_id: swig_interface::program_id(), + accounts: vec![], + data: compact_ixs.into_bytes(), + }; + + // Get the preceding instruction from the function + let preceding_instruction = (self.preceding_instruction_fn)(); + + // Determine payer from transaction_signers (first signer is typically the + // payer) + let payer = transaction_signers.first().copied().unwrap_or(swig_account); + + // Use SignV2 with ProgramExec + SignV2Instruction::new_program_exec( + swig_account, + swig_wallet_address, + payer, + preceding_instruction, + inner_instruction, + role_id, + ) + .map_err(|e| SwigError::InterfaceError(e.to_string())) + } +} diff --git a/rust-sdk/src/tests/ix_builder/mod.rs b/rust-sdk/src/tests/ix_builder/mod.rs index 9e047b90..c675a829 100644 --- a/rust-sdk/src/tests/ix_builder/mod.rs +++ b/rust-sdk/src/tests/ix_builder/mod.rs @@ -48,6 +48,7 @@ use crate::{ pub mod authority_tests; pub mod destination_tests; pub mod program_all_tests; +pub mod program_exec_tests; pub mod program_scope_tests; pub mod secp256r1_tests; pub mod session_tests; diff --git a/rust-sdk/src/tests/ix_builder/program_exec_tests.rs b/rust-sdk/src/tests/ix_builder/program_exec_tests.rs new file mode 100644 index 00000000..585234e1 --- /dev/null +++ b/rust-sdk/src/tests/ix_builder/program_exec_tests.rs @@ -0,0 +1,286 @@ +use solana_program::pubkey::Pubkey; +use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + message::{v0, VersionedMessage}, + signature::{Keypair, Signer}, + system_instruction, + transaction::VersionedTransaction, +}; +use swig_interface::program_id; +use swig_state::{ + authority::{programexec::ProgramExecAuthority, AuthorityType}, + swig::{swig_account_seeds, swig_wallet_address_seeds, SwigWithRoles}, +}; + +use super::*; +use crate::{client_role::ProgramExecClientRole, types::Permission, Ed25519ClientRole}; + +// Test program ID (same as used in program tests) +const TEST_PROGRAM_ID: Pubkey = + solana_program::pubkey!("BXAu5ZWHnGun2XZjUZ9nqwiZ5dNVmofPGYdMC4rx4qLV"); +const VALID_DISCRIMINATOR: [u8; 8] = [1, 2, 3, 4, 5, 6, 7, 8]; + +#[test_log::test] +fn test_program_exec_sign_with_preceding_instruction() { + let mut context = setup_test_context().unwrap(); + let swig_id = [42u8; 32]; + let ed25519_authority = Keypair::new(); + let root_role_id = 0; + + // Create Swig wallet with Ed25519 root authority + let (swig_key, _, _) = create_swig_ed25519(&mut context, &ed25519_authority, swig_id).unwrap(); + + let payer = context.default_payer.pubkey(); + + // Get swig wallet address PDA + let (swig_wallet_address, _) = + Pubkey::find_program_address(&swig_wallet_address_seeds(swig_key.as_ref()), &program_id()); + + // Airdrop to swig wallet so it can execute transfers + context + .svm + .airdrop(&swig_wallet_address, 10_000_000) + .unwrap(); + + // Create the preceding instruction that the test program will execute + let swig_key_for_closure = swig_key; + let swig_wallet_for_closure = swig_wallet_address; + + // Create ProgramExec authority with function that generates preceding + // instruction + let program_exec_role = + ProgramExecClientRole::new(TEST_PROGRAM_ID, VALID_DISCRIMINATOR.to_vec(), move || { + Instruction { + program_id: TEST_PROGRAM_ID, + accounts: vec![ + AccountMeta::new_readonly(swig_key_for_closure, false), // config + AccountMeta::new_readonly(swig_wallet_for_closure, false), // wallet + ], + data: VALID_DISCRIMINATOR.to_vec(), + } + }); + + // Add ProgramExec authority using root authority + let mut root_builder = SwigInstructionBuilder::new( + swig_id, + Box::new(Ed25519ClientRole::new(ed25519_authority.pubkey())), + payer, + root_role_id, + ); + + let current_slot = context.svm.get_sysvar::().slot; + let permissions = vec![Permission::All]; + + let add_auth_ix = root_builder + .add_authority_instruction( + AuthorityType::ProgramExec, + &program_exec_role.authority_data(), + permissions, + Some(current_slot), + ) + .unwrap(); + + // Execute add authority instruction + let msg = v0::Message::try_compile(&payer, &add_auth_ix, &[], context.svm.latest_blockhash()) + .unwrap(); + + let tx = VersionedTransaction::try_new( + VersionedMessage::V0(msg), + &[&context.default_payer, &ed25519_authority], + ) + .unwrap(); + + let result = context.svm.send_transaction(tx); + assert!( + result.is_ok(), + "Failed to add ProgramExec authority: {:?}", + result.err() + ); + + // Verify authority was added + let swig_account = context.svm.get_account(&swig_key).unwrap(); + let swig_data = SwigWithRoles::from_bytes(&swig_account.data).unwrap(); + assert_eq!( + swig_data.state.roles, 2, + "Should have 2 roles (root + program exec)" + ); + + println!("✓ Successfully added ProgramExec authority"); + println!(" - Total roles: {}", swig_data.state.roles); + + // Note: To actually test signing with ProgramExec, the TEST_PROGRAM would + // need to be deployed and executed. This test verifies that the + // authority can be added and the authority data is correctly generated + // with the closure-based function pattern. +} + +#[test_log::test] +fn test_program_exec_authority_data_generation() { + // Test that authority data is generated correctly + // The function doesn't matter for authority_data() generation, so use a dummy + // one + let program_exec_role = + ProgramExecClientRole::new(TEST_PROGRAM_ID, VALID_DISCRIMINATOR.to_vec(), || { + Instruction { + program_id: TEST_PROGRAM_ID, + accounts: vec![], + data: vec![], + } + }); + + let authority_data = program_exec_role.authority_data(); + + // Verify the authority data is not empty + assert!( + !authority_data.is_empty(), + "Authority data should not be empty" + ); + + // The authority data should contain the program ID and discriminator + println!("✓ Authority data length: {} bytes", authority_data.len()); + + // Verify the authority data contains the expected information + // The format is: [program_id: 32 bytes][instruction_prefix_len: 1 + // byte][padding: 7 bytes][instruction_prefix: 40 bytes] + assert_eq!( + authority_data.len(), + 80, + "Authority data should be exactly 80 bytes" + ); + + // Verify program ID (first 32 bytes) + assert_eq!( + &authority_data[0..32], + &TEST_PROGRAM_ID.to_bytes(), + "Program ID should match in authority data" + ); + + // Verify discriminator length (at offset 32) + let prefix_len = authority_data[32] as usize; + assert_eq!( + prefix_len, + VALID_DISCRIMINATOR.len(), + "Prefix length should match" + ); + + // Verify discriminator (starts at offset 40 after program_id + prefix_len + + // padding) + const IX_PREFIX_OFFSET: usize = 40; // 32 + 1 + 7 + assert_eq!( + &authority_data[IX_PREFIX_OFFSET..IX_PREFIX_OFFSET + prefix_len], + &VALID_DISCRIMINATOR, + "Discriminator should match in authority data" + ); +} + +#[test_log::test] +fn test_program_exec_with_multiple_authorities() { + let mut context = setup_test_context().unwrap(); + let swig_id = [99u8; 32]; + let ed25519_authority = Keypair::new(); + + // Create Swig wallet + let (swig_key, _, _) = create_swig_ed25519(&mut context, &ed25519_authority, swig_id).unwrap(); + + let payer = context.default_payer.pubkey(); + let (swig_wallet_address, _) = + Pubkey::find_program_address(&swig_wallet_address_seeds(swig_key.as_ref()), &program_id()); + + context + .svm + .airdrop(&swig_wallet_address, 10_000_000) + .unwrap(); + + // Add multiple ProgramExec authorities with different discriminators + let discriminator1 = vec![1, 2, 3, 4, 5, 6, 7, 8]; + let discriminator2 = vec![9, 10, 11, 12, 13, 14, 15, 16]; + + let swig_key_for_closure = swig_key; + let swig_wallet_for_closure = swig_wallet_address; + + let program_exec_role1 = + ProgramExecClientRole::new(TEST_PROGRAM_ID, discriminator1.clone(), move || { + Instruction { + program_id: TEST_PROGRAM_ID, + accounts: vec![ + AccountMeta::new_readonly(swig_key_for_closure, false), + AccountMeta::new_readonly(swig_wallet_for_closure, false), + ], + data: discriminator1.clone(), + } + }); + + let program_exec_role2 = + ProgramExecClientRole::new(TEST_PROGRAM_ID, discriminator2.clone(), move || { + Instruction { + program_id: TEST_PROGRAM_ID, + accounts: vec![ + AccountMeta::new_readonly(swig_key_for_closure, false), + AccountMeta::new_readonly(swig_wallet_for_closure, false), + ], + data: discriminator2.clone(), + } + }); + + let mut root_builder = SwigInstructionBuilder::new( + swig_id, + Box::new(Ed25519ClientRole::new(ed25519_authority.pubkey())), + payer, + 0, + ); + + let current_slot = context.svm.get_sysvar::().slot; + + // Add first ProgramExec authority + let add_auth_ix1 = root_builder + .add_authority_instruction( + AuthorityType::ProgramExec, + &program_exec_role1.authority_data(), + vec![Permission::All], + Some(current_slot), + ) + .unwrap(); + + let msg = v0::Message::try_compile(&payer, &add_auth_ix1, &[], context.svm.latest_blockhash()) + .unwrap(); + + let tx = VersionedTransaction::try_new( + VersionedMessage::V0(msg), + &[&context.default_payer, &ed25519_authority], + ) + .unwrap(); + + context.svm.send_transaction(tx).unwrap(); + + // Add second ProgramExec authority + let add_auth_ix2 = root_builder + .add_authority_instruction( + AuthorityType::ProgramExec, + &program_exec_role2.authority_data(), + vec![Permission::All], + Some(current_slot), + ) + .unwrap(); + + let msg = v0::Message::try_compile(&payer, &add_auth_ix2, &[], context.svm.latest_blockhash()) + .unwrap(); + + let tx = VersionedTransaction::try_new( + VersionedMessage::V0(msg), + &[&context.default_payer, &ed25519_authority], + ) + .unwrap(); + + context.svm.send_transaction(tx).unwrap(); + + // Verify both authorities were added + let swig_account = context.svm.get_account(&swig_key).unwrap(); + let swig_data = SwigWithRoles::from_bytes(&swig_account.data).unwrap(); + assert_eq!( + swig_data.state.roles, 3, + "Should have 3 roles (root + 2 program exec)" + ); + + println!("✓ Successfully added multiple ProgramExec authorities"); + println!(" - Total roles: {}", swig_data.state.roles); +} diff --git a/rust-sdk/src/wallet.rs b/rust-sdk/src/wallet.rs index aa55d883..aeedb9b0 100644 --- a/rust-sdk/src/wallet.rs +++ b/rust-sdk/src/wallet.rs @@ -24,7 +24,7 @@ use spl_associated_token_account::{ get_associated_token_address, instruction::create_associated_token_account, }; use spl_token::ID as TOKEN_PROGRAM_ID; -use swig_interface::{swig, swig_key}; +use swig_interface::swig; use swig_state::{ action::{ all::All, manage_authority::ManageAuthority, program_scope::ProgramScope, diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 760c07f9..a7cb1eca 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] channel = "1.84.0" -components = ["rustfmt", "clippy", "rust-analyzer"] \ No newline at end of file +components = ["rustfmt", "clippy", "rust-analyzer"] diff --git a/state/src/authority/mod.rs b/state/src/authority/mod.rs index 6ae64831..52d379ce 100644 --- a/state/src/authority/mod.rs +++ b/state/src/authority/mod.rs @@ -6,6 +6,7 @@ //! session-based variants. pub mod ed25519; +pub mod programexec; pub mod secp256k1; pub mod secp256r1; @@ -13,6 +14,7 @@ use std::any::Any; use ed25519::{ED25519Authority, Ed25519SessionAuthority}; use pinocchio::{account_info::AccountInfo, program_error::ProgramError}; +use programexec::{session::ProgramExecSessionAuthority, ProgramExecAuthority}; use secp256k1::{Secp256k1Authority, Secp256k1SessionAuthority}; use secp256r1::{Secp256r1Authority, Secp256r1SessionAuthority}; @@ -41,7 +43,7 @@ pub trait Authority: Transmutable + TransmutableMut + IntoBytes { /// /// This trait defines the interface for interacting with authorities, /// including authentication and session management. -pub trait AuthorityInfo: IntoBytes { +pub trait AuthorityInfo { /// Returns the type of this authority fn authority_type(&self) -> AuthorityType; @@ -129,6 +131,10 @@ pub enum AuthorityType { Secp256r1, /// Session-based Secp256r1 authority Secp256r1Session, + /// Program execution authority + ProgramExec, + /// Session-based Program execution authority + ProgramExecSession, } impl TryFrom for AuthorityType { @@ -144,6 +150,8 @@ impl TryFrom for AuthorityType { 4 => Ok(AuthorityType::Secp256k1Session), 5 => Ok(AuthorityType::Secp256r1), 6 => Ok(AuthorityType::Secp256r1Session), + 7 => Ok(AuthorityType::ProgramExec), + 8 => Ok(AuthorityType::ProgramExecSession), _ => Err(ProgramError::InvalidInstructionData), } } @@ -167,6 +175,8 @@ pub const fn authority_type_to_length( AuthorityType::Secp256k1Session => Ok(Secp256k1SessionAuthority::LEN), AuthorityType::Secp256r1 => Ok(Secp256r1Authority::LEN), AuthorityType::Secp256r1Session => Ok(Secp256r1SessionAuthority::LEN), + AuthorityType::ProgramExec => Ok(ProgramExecAuthority::LEN), + AuthorityType::ProgramExecSession => Ok(ProgramExecSessionAuthority::LEN), _ => Err(ProgramError::InvalidInstructionData), } } diff --git a/state/src/authority/programexec/mod.rs b/state/src/authority/programexec/mod.rs new file mode 100644 index 00000000..43d74cad --- /dev/null +++ b/state/src/authority/programexec/mod.rs @@ -0,0 +1,319 @@ +//! Program execution authority implementation. +//! +//! This module provides implementations for program execution-based authority +//! types in the Swig wallet system. This authority type validates that a +//! preceding instruction in the transaction matches configured program and +//! instruction prefix requirements, and that the instruction was successful. + +pub mod session; + +use core::any::Any; + +use pinocchio::{ + account_info::AccountInfo, + program_error::ProgramError, + sysvars::instructions::{Instructions, INSTRUCTIONS_ID}, +}; +use swig_assertions::sol_assert_bytes_eq; + +use super::{Authority, AuthorityInfo, AuthorityType}; +use crate::{IntoBytes, SwigAuthenticateError, SwigStateError, Transmutable, TransmutableMut}; + +const MAX_INSTRUCTION_PREFIX_LEN: usize = 40; +const IX_PREFIX_OFFSET: usize = 32 + 1 + 7; // program_id + instruction_prefix_len + padding + +/// Standard Program Execution authority implementation. +/// +/// This struct represents a program execution authority that validates +/// a preceding instruction matches the configured program and instruction +/// prefix. +#[repr(C, align(8))] +#[derive(Debug, PartialEq, no_padding::NoPadding)] +pub struct ProgramExecAuthority { + /// The program ID that must execute the preceding instruction + pub program_id: [u8; 32], + /// Length of the instruction prefix to match (0-40) + pub instruction_prefix_len: u8, + /// Padding for alignment + _padding: [u8; 7], + pub instruction_prefix: [u8; MAX_INSTRUCTION_PREFIX_LEN], +} + +impl ProgramExecAuthority { + /// Creates a new ProgramExecAuthority. + /// + /// # Arguments + /// * `program_id` - The program ID to validate against + /// * `instruction_prefix_len` - Length of the prefix to match + pub fn new(program_id: [u8; 32], instruction_prefix_len: u8) -> Self { + Self { + program_id, + instruction_prefix_len, + _padding: [0; 7], + instruction_prefix: [0; MAX_INSTRUCTION_PREFIX_LEN], + } + } + + /// Creates authority data bytes for creating a ProgramExec authority. + /// + /// # Arguments + /// * `program_id` - The program ID that must execute the preceding + /// instruction + /// * `instruction_prefix` - The instruction discriminator/prefix to match + /// (up to 40 bytes) + /// + /// # Returns + /// Returns a vector of bytes that can be used as authority data when + /// creating a ProgramExec authority + pub fn create_authority_data(program_id: &[u8; 32], instruction_prefix: &[u8]) -> Vec { + let prefix_len = instruction_prefix.len().min(MAX_INSTRUCTION_PREFIX_LEN); + let mut data = Vec::with_capacity(Self::LEN); + + // program_id: 32 bytes + data.extend_from_slice(program_id); + + // instruction_prefix_len: 1 byte + data.push(prefix_len as u8); + + // padding: 7 bytes + data.extend_from_slice(&[0u8; 7]); + + // instruction_prefix: up to MAX_INSTRUCTION_PREFIX_LEN bytes + data.extend_from_slice(&instruction_prefix[..prefix_len]); + + // Pad remaining bytes to MAX_INSTRUCTION_PREFIX_LEN + data.extend_from_slice(&vec![0u8; MAX_INSTRUCTION_PREFIX_LEN - prefix_len]); + + data + } +} + +/// + +impl Transmutable for ProgramExecAuthority { + // len of header + const LEN: usize = core::mem::size_of::(); +} + +impl TransmutableMut for ProgramExecAuthority {} + +impl Authority for ProgramExecAuthority { + const TYPE: AuthorityType = AuthorityType::ProgramExec; + const SESSION_BASED: bool = false; + + fn set_into_bytes(create_data: &[u8], bytes: &mut [u8]) -> Result<(), ProgramError> { + if create_data.len() != Self::LEN { + return Err(SwigStateError::InvalidRoleData.into()); + } + + let prefix_len = create_data[32] as usize; + if prefix_len > MAX_INSTRUCTION_PREFIX_LEN { + return Err(SwigStateError::InvalidRoleData.into()); + } + + let authority = unsafe { ProgramExecAuthority::load_mut_unchecked(bytes)? }; + let create_data_program_id = &create_data[..32]; + assert_program_exec_cant_be_swig(create_data_program_id)?; + authority.program_id.copy_from_slice(create_data_program_id); + authority.instruction_prefix_len = prefix_len as u8; + authority.instruction_prefix[..prefix_len] + .copy_from_slice(&create_data[IX_PREFIX_OFFSET..IX_PREFIX_OFFSET + prefix_len]); + Ok(()) + } +} + +impl AuthorityInfo for ProgramExecAuthority { + fn authority_type(&self) -> AuthorityType { + Self::TYPE + } + + fn length(&self) -> usize { + Self::LEN + } + + fn session_based(&self) -> bool { + Self::SESSION_BASED + } + + fn match_data(&self, data: &[u8]) -> bool { + if data.len() < 32 { + return false; + } + // The identity slice spans the full struct (80 bytes) to include both + // program_id and instruction_prefix which are separated by + // instruction_prefix_len and padding + if data.len() != Self::LEN { + return false; + } + // The identity slice includes intermediate bytes (instruction_prefix_len + + // padding) so we need to read instruction_prefix from IX_PREFIX_OFFSET + sol_assert_bytes_eq(&self.program_id, &data[..32], 32) + && sol_assert_bytes_eq( + &self.instruction_prefix[..self.instruction_prefix_len as usize], + &data[IX_PREFIX_OFFSET..IX_PREFIX_OFFSET + self.instruction_prefix_len as usize], + self.instruction_prefix_len as usize, + ) + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn identity(&self) -> Result<&[u8], ProgramError> { + Ok(&self.instruction_prefix[..self.instruction_prefix_len as usize]) + } + + fn signature_odometer(&self) -> Option { + None + } + + fn authenticate( + &mut self, + account_infos: &[AccountInfo], + authority_payload: &[u8], + _data_payload: &[u8], + _slot: u64, + ) -> Result<(), ProgramError> { + // authority_payload format: [instruction_sysvar_index: 1 byte] + // Config is always at index 0, wallet is always at index 0 (same as config) + if authority_payload.len() != 1 { + return Err(SwigAuthenticateError::InvalidAuthorityPayload.into()); + } + + let instruction_sysvar_index = authority_payload[0] as usize; + let config_account_index = 0; // Config is always the first account (swig account) + let wallet_account_index = 1; // Wallet is the second account (swig wallet address) + + program_exec_authenticate( + account_infos, + instruction_sysvar_index, + config_account_index, + wallet_account_index, + &self.program_id, + &self.instruction_prefix, + self.instruction_prefix_len as usize, + ) + } +} + +impl IntoBytes for ProgramExecAuthority { + fn into_bytes(&self) -> Result<&[u8], ProgramError> { + let bytes = + unsafe { core::slice::from_raw_parts(self as *const Self as *const u8, Self::LEN) }; + Ok(bytes) + } +} + +fn assert_program_exec_cant_be_swig(program_id: &[u8]) -> Result<(), ProgramError> { + if sol_assert_bytes_eq(program_id, &swig_assertions::id(), 32) { + return Err(SwigAuthenticateError::PermissionDeniedProgramExecCannotBeSwig.into()); + } + Ok(()) +} + +/// Authenticates a program execution authority. +/// +/// Validates that a preceding instruction: +/// - Was executed by the expected program +/// - Has instruction data matching the expected prefix +/// - Passed the config and wallet accounts as its first two accounts +/// - Executed successfully (implied by the transaction being valid) +/// +/// # Arguments +/// * `account_infos` - List of accounts involved in the transaction +/// * `instruction_sysvar_index` - Index of the instructions sysvar account +/// * `config_account_index` - Index of the config account +/// * `wallet_account_index` - Index of the wallet account +/// * `expected_program_id` - The program ID that should have executed +/// * `expected_instruction_prefix` - The instruction data prefix to match +/// * `prefix_len` - Length of the prefix to match +pub fn program_exec_authenticate( + account_infos: &[AccountInfo], + instruction_sysvar_index: usize, + config_account_index: usize, + wallet_account_index: usize, + expected_program_id: &[u8; 32], + expected_instruction_prefix: &[u8; MAX_INSTRUCTION_PREFIX_LEN], + prefix_len: usize, +) -> Result<(), ProgramError> { + // Get the sysvar instructions account + let sysvar_instructions = account_infos + .get(instruction_sysvar_index) + .ok_or(SwigAuthenticateError::InvalidAuthorityPayload)?; + + // Verify this is the sysvar instructions account + if sysvar_instructions.key().as_ref() != &INSTRUCTIONS_ID { + return Err(SwigAuthenticateError::PermissionDeniedProgramExecInvalidInstruction.into()); + } + + // Get the config and wallet accounts + let config_account = account_infos + .get(config_account_index) + .ok_or(SwigAuthenticateError::InvalidAuthorityPayload)?; + let wallet_account = account_infos + .get(wallet_account_index) + .ok_or(SwigAuthenticateError::InvalidAuthorityPayload)?; + + // Load instructions sysvar + let sysvar_instructions_data = unsafe { sysvar_instructions.borrow_data_unchecked() }; + let ixs = unsafe { Instructions::new_unchecked(sysvar_instructions_data) }; + let current_index = ixs.load_current_index() as usize; + + // Must have at least one preceding instruction + if current_index == 0 { + return Err(SwigAuthenticateError::PermissionDeniedProgramExecInvalidInstruction.into()); + } + + // Get the preceding instruction + let preceding_ix = unsafe { ixs.deserialize_instruction_unchecked(current_index - 1) }; + let num_accounts = u16::from_le_bytes(unsafe { + *(preceding_ix.get_instruction_data().as_ptr() as *const [u8; 2]) + }); + if num_accounts < 2 { + return Err( + SwigAuthenticateError::PermissionDeniedProgramExecInvalidInstructionData.into(), + ); + } + + // Verify the instruction is calling the expected program + if !sol_assert_bytes_eq(preceding_ix.get_program_id(), expected_program_id, 32) { + return Err(SwigAuthenticateError::PermissionDeniedProgramExecInvalidProgram.into()); + } + + // Verify the instruction data prefix matches + let instruction_data = preceding_ix.get_instruction_data(); + if instruction_data.len() < prefix_len { + return Err( + SwigAuthenticateError::PermissionDeniedProgramExecInvalidInstructionData.into(), + ); + } + + if !sol_assert_bytes_eq( + &instruction_data[..prefix_len], + &expected_instruction_prefix[..prefix_len], + prefix_len, + ) { + return Err( + SwigAuthenticateError::PermissionDeniedProgramExecInvalidInstructionData.into(), + ); + } + + // Verify the first two accounts of the preceding instruction are config and + // wallet Get account meta at index 0 (should be config) + let account_0 = unsafe { preceding_ix.get_account_meta_at_unchecked(0) }; + let account_1 = unsafe { preceding_ix.get_account_meta_at_unchecked(1) }; + + // Verify the accounts match the config and wallet keys + if !sol_assert_bytes_eq(account_0.key.as_ref(), config_account.key(), 32) { + return Err(SwigAuthenticateError::PermissionDeniedProgramExecInvalidConfigAccount.into()); + } + + if !sol_assert_bytes_eq(account_1.key.as_ref(), wallet_account.key(), 32) { + return Err(SwigAuthenticateError::PermissionDeniedProgramExecInvalidWalletAccount.into()); + } + + // If we get here, all checks passed - the instruction executed successfully + // (implied by the transaction being valid) with the correct program, data, and + // accounts + Ok(()) +} diff --git a/state/src/authority/programexec/session.rs b/state/src/authority/programexec/session.rs new file mode 100644 index 00000000..be83b4d0 --- /dev/null +++ b/state/src/authority/programexec/session.rs @@ -0,0 +1,275 @@ +//! Session-based program execution authority implementation. + +use core::any::Any; + +use pinocchio::{account_info::AccountInfo, program_error::ProgramError}; + +use super::{ + super::{ed25519::ed25519_authenticate, Authority, AuthorityInfo, AuthorityType}, + program_exec_authenticate, MAX_INSTRUCTION_PREFIX_LEN, +}; +use crate::{ + authority::programexec::assert_program_exec_cant_be_swig, IntoBytes, SwigAuthenticateError, + SwigStateError, Transmutable, TransmutableMut, +}; + +/// Creation parameters for a session-based program execution authority. +#[repr(C, align(8))] +#[derive(Debug, PartialEq, no_padding::NoPadding)] +pub struct CreateProgramExecSessionAuthority { + /// The program ID that must execute the preceding instruction + pub program_id: [u8; 32], + /// Length of the instruction prefix to match (0-32) + pub instruction_prefix_len: u8, + /// Padding for alignment + _padding: [u8; 7], + /// The instruction data prefix that must match + pub instruction_prefix: [u8; MAX_INSTRUCTION_PREFIX_LEN], + /// The session key for temporary authentication + pub session_key: [u8; 32], + /// Maximum duration a session can be valid for + pub max_session_length: u64, +} + +impl CreateProgramExecSessionAuthority { + /// Creates a new set of session authority parameters. + /// + /// # Arguments + /// * `program_id` - The program ID to validate against + /// * `instruction_prefix` - The instruction data prefix to match + /// * `instruction_prefix_len` - Length of the prefix to match + /// * `session_key` - The initial session key + /// * `max_session_length` - Maximum allowed session duration + pub fn new( + program_id: [u8; 32], + instruction_prefix_len: u8, + instruction_prefix: [u8; MAX_INSTRUCTION_PREFIX_LEN], + session_key: [u8; 32], + max_session_length: u64, + ) -> Self { + Self { + program_id, + instruction_prefix, + instruction_prefix_len, + _padding: [0; 7], + session_key, + max_session_length, + } + } +} + +impl Transmutable for CreateProgramExecSessionAuthority { + const LEN: usize = core::mem::size_of::(); +} + +impl IntoBytes for CreateProgramExecSessionAuthority { + fn into_bytes(&self) -> Result<&[u8], ProgramError> { + let bytes = + unsafe { core::slice::from_raw_parts(self as *const Self as *const u8, Self::LEN) }; + Ok(bytes) + } +} + +/// Session-based Program Execution authority implementation. +/// +/// This struct represents a program execution authority that supports temporary +/// session keys with expiration times. It validates preceding instructions +/// and maintains session state. +#[repr(C, align(8))] +#[derive(Debug, PartialEq, no_padding::NoPadding)] +pub struct ProgramExecSessionAuthority { + /// The program ID that must execute the preceding instruction + pub program_id: [u8; 32], + /// Length of the instruction prefix to match (0-32) + pub instruction_prefix_len: u8, + /// Padding for alignment + _padding: [u8; 7], + /// The instruction data prefix that must match + pub instruction_prefix: [u8; MAX_INSTRUCTION_PREFIX_LEN], + + /// The current session key + pub session_key: [u8; 32], + /// Maximum allowed session duration + pub max_session_length: u64, + /// Slot when the current session expires + pub current_session_expiration: u64, +} + +impl ProgramExecSessionAuthority { + /// Creates a new session-based program execution authority. + /// + /// # Arguments + /// * `program_id` - The program ID to validate against + /// * `instruction_prefix` - The instruction data prefix to match + /// * `instruction_prefix_len` - Length of the prefix to match + /// * `session_key` - The initial session key + /// * `max_session_length` - Maximum allowed session duration + pub fn new( + program_id: [u8; 32], + instruction_prefix_len: u8, + instruction_prefix: [u8; MAX_INSTRUCTION_PREFIX_LEN], + session_key: [u8; 32], + max_session_length: u64, + ) -> Self { + Self { + program_id, + instruction_prefix_len, + _padding: [0; 7], + instruction_prefix, + session_key, + max_session_length, + current_session_expiration: 0, + } + } +} + +impl Transmutable for ProgramExecSessionAuthority { + const LEN: usize = core::mem::size_of::(); +} + +impl TransmutableMut for ProgramExecSessionAuthority {} + +impl IntoBytes for ProgramExecSessionAuthority { + fn into_bytes(&self) -> Result<&[u8], ProgramError> { + let bytes = + unsafe { core::slice::from_raw_parts(self as *const Self as *const u8, Self::LEN) }; + Ok(bytes) + } +} + +impl Authority for ProgramExecSessionAuthority { + const TYPE: AuthorityType = AuthorityType::ProgramExecSession; + const SESSION_BASED: bool = true; + + fn set_into_bytes(create_data: &[u8], bytes: &mut [u8]) -> Result<(), ProgramError> { + let create = unsafe { CreateProgramExecSessionAuthority::load_unchecked(create_data)? }; + let authority = unsafe { ProgramExecSessionAuthority::load_mut_unchecked(bytes)? }; + + if create_data.len() != Self::LEN { + return Err(SwigStateError::InvalidRoleData.into()); + } + + let prefix_len = create_data[32] as usize; + if prefix_len > MAX_INSTRUCTION_PREFIX_LEN { + return Err(SwigStateError::InvalidRoleData.into()); + } + let create_data_program_id = &create_data[..32]; + assert_program_exec_cant_be_swig(create_data_program_id)?; + authority.program_id = create.program_id; + authority.instruction_prefix = create.instruction_prefix; + authority.instruction_prefix_len = create.instruction_prefix_len; + authority.session_key = create.session_key; + authority.max_session_length = create.max_session_length; + authority.current_session_expiration = 0; + + Ok(()) + } +} + +impl AuthorityInfo for ProgramExecSessionAuthority { + fn authority_type(&self) -> AuthorityType { + Self::TYPE + } + + fn length(&self) -> usize { + Self::LEN + } + + fn session_based(&self) -> bool { + Self::SESSION_BASED + } + + fn identity(&self) -> Result<&[u8], ProgramError> { + Ok(&self.instruction_prefix[..self.instruction_prefix_len as usize]) + } + + fn signature_odometer(&self) -> Option { + None + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn match_data(&self, data: &[u8]) -> bool { + use swig_assertions::sol_assert_bytes_eq; + + if data.len() < 33 { + return false; + } + let prefix_len = data[32] as usize; + if prefix_len != self.instruction_prefix_len as usize { + return false; + } + if data.len() != 33 + prefix_len { + return false; + } + sol_assert_bytes_eq(&self.program_id, &data[..32], 32) + && sol_assert_bytes_eq( + &self.instruction_prefix[..prefix_len], + &data[33..33 + prefix_len], + prefix_len, + ) + } + + fn start_session( + &mut self, + session_key: [u8; 32], + current_slot: u64, + duration: u64, + ) -> Result<(), ProgramError> { + if duration > self.max_session_length { + return Err(SwigAuthenticateError::InvalidSessionDuration.into()); + } + self.current_session_expiration = current_slot + duration; + self.session_key = session_key; + Ok(()) + } + + fn authenticate_session( + &mut self, + account_infos: &[AccountInfo], + authority_payload: &[u8], + _data_payload: &[u8], + slot: u64, + ) -> Result<(), ProgramError> { + if authority_payload.len() != 1 { + return Err(SwigAuthenticateError::InvalidAuthorityPayload.into()); + } + if slot > self.current_session_expiration { + return Err(SwigAuthenticateError::PermissionDeniedSessionExpired.into()); + } + ed25519_authenticate( + account_infos, + authority_payload[0] as usize, + &self.session_key, + ) + } + + fn authenticate( + &mut self, + account_infos: &[AccountInfo], + authority_payload: &[u8], + _data_payload: &[u8], + _slot: u64, + ) -> Result<(), ProgramError> { + // authority_payload format: [instruction_sysvar_index: 1 byte] + if authority_payload.len() != 1 { + return Err(SwigAuthenticateError::InvalidAuthorityPayload.into()); + } + + let instruction_sysvar_index = authority_payload[0] as usize; + let config_account_index = 0; + let wallet_account_index = 1; + + program_exec_authenticate( + account_infos, + instruction_sysvar_index, + config_account_index, + wallet_account_index, + &self.program_id, + &self.instruction_prefix, + self.instruction_prefix_len as usize, + ) + } +} diff --git a/state/src/lib.rs b/state/src/lib.rs index 447faa7e..80aab7ca 100644 --- a/state/src/lib.rs +++ b/state/src/lib.rs @@ -10,7 +10,9 @@ pub mod authority; pub mod constants; pub mod role; pub mod swig; +pub mod transmute; pub mod util; +pub use transmute::{IntoBytes, Transmutable, TransmutableMut}; /// Represents the type discriminator for different account types in the system. #[repr(u8)] @@ -178,6 +180,20 @@ pub enum SwigAuthenticateError { PermissionDeniedTokenDestinationLimitExceeded, /// Token destination recurring limit exceeded PermissionDeniedRecurringTokenDestinationLimitExceeded, + /// Program execution instruction is invalid + PermissionDeniedProgramExecInvalidInstruction, + /// Program execution program ID does not match + PermissionDeniedProgramExecInvalidProgram, + /// Program execution instruction data does not match prefix + PermissionDeniedProgramExecInvalidInstructionData, + /// Program execution missing required accounts + PermissionDeniedProgramExecMissingAccounts, + /// Program execution config account index mismatch + PermissionDeniedProgramExecInvalidConfigAccount, + /// Program execution wallet account index mismatch + PermissionDeniedProgramExecInvalidWalletAccount, + /// Program execution cannot be the Swig program + PermissionDeniedProgramExecCannotBeSwig, } impl From for ProgramError { @@ -191,54 +207,3 @@ impl From for ProgramError { ProgramError::Custom(e as u32) } } - -/// Marker trait for types that can be safely cast from a raw pointer. -/// -/// Types implementing this trait must guarantee that the cast is safe, -/// ensuring proper field alignment and absence of padding bytes. -pub trait Transmutable: Sized { - /// The length of the type in bytes. - /// - /// Must equal the total size of all fields in the type. - const LEN: usize; - - /// Creates a reference to `Self` from a byte slice. - /// - /// # Safety - /// - /// The caller must ensure that `bytes` contains a valid representation of - /// the implementing type. - #[inline(always)] - unsafe fn load_unchecked(bytes: &[u8]) -> Result<&Self, ProgramError> { - if bytes.len() != Self::LEN { - return Err(ProgramError::InvalidAccountData); - } - Ok(&*(bytes.as_ptr() as *const Self)) - } -} - -/// Marker trait for types that can be mutably cast from a raw pointer. -/// -/// Types implementing this trait must guarantee that the mutable cast is safe, -/// ensuring proper field alignment and absence of padding bytes. -pub trait TransmutableMut: Transmutable { - /// Creates a mutable reference to `Self` from a mutable byte slice. - /// - /// # Safety - /// - /// The caller must ensure that `bytes` contains a valid representation of - /// the implementing type. - #[inline(always)] - unsafe fn load_mut_unchecked(bytes: &mut [u8]) -> Result<&mut Self, ProgramError> { - if bytes.len() != Self::LEN { - return Err(ProgramError::InvalidAccountData); - } - Ok(&mut *(bytes.as_mut_ptr() as *mut Self)) - } -} - -/// Trait for types that can be converted into a byte slice representation. -pub trait IntoBytes { - /// Converts the implementing type into a byte slice. - fn into_bytes(&self) -> Result<&[u8], ProgramError>; -} diff --git a/state/src/role.rs b/state/src/role.rs index d1851e6a..c7f9c679 100644 --- a/state/src/role.rs +++ b/state/src/role.rs @@ -79,7 +79,7 @@ impl<'a> Role<'a> { } /// Retrieves all actions associated with this role. - pub fn get_all_actions(&'a self) -> Result, ProgramError> { + pub fn get_all_actions(&self) -> Result, ProgramError> { let mut actions = Vec::new(); let mut cursor = 0; while cursor < self.actions.len() { diff --git a/state/src/swig.rs b/state/src/swig.rs index fa6ec416..91cd8f41 100644 --- a/state/src/swig.rs +++ b/state/src/swig.rs @@ -8,12 +8,13 @@ extern crate alloc; use no_padding::NoPadding; -use pinocchio::{instruction::Seed, msg, program_error::ProgramError}; +use pinocchio::{instruction::Seed, program_error::ProgramError}; use crate::{ action::{program_scope::ProgramScope, Action, ActionLoader, Actionable}, authority::{ ed25519::{ED25519Authority, Ed25519SessionAuthority}, + programexec::{session::ProgramExecSessionAuthority, ProgramExecAuthority}, secp256k1::{Secp256k1Authority, Secp256k1SessionAuthority}, secp256r1::{Secp256r1Authority, Secp256r1SessionAuthority}, Authority, AuthorityInfo, AuthorityType, @@ -97,37 +98,6 @@ pub fn sub_account_signer<'a>( ] } -/// Represents a Swig sub-account with its associated metadata. -// #[repr(C, align(8))] -// #[derive(Debug, PartialEq, NoPadding)] -// pub struct SwigSubAccount { -// /// Account type discriminator -// pub discriminator: u8, -// /// PDA bump seed -// pub bump: u8, -// /// Whether the sub-account is enabled -// pub enabled: bool, -// _padding: [u8; 1], -// /// ID of the role associated with this sub-account -// pub role_id: u32, -// /// ID of the parent Swig account -// pub swig_id: [u8; 32], -// /// Amount of lamports reserved for rent -// pub reserved_lamports: u64, -// } - -// impl Transmutable for SwigSubAccount { -// const LEN: usize = core::mem::size_of::(); -// } - -// impl TransmutableMut for SwigSubAccount {} - -// impl IntoBytes for SwigSubAccount { -// fn into_bytes(&self) -> Result<&[u8], ProgramError> { -// Ok(unsafe { core::slice::from_raw_parts(self as *const Self as *const u8, Self::LEN) }) -// } -// } - /// Builder for constructing and modifying Swig accounts. pub struct SwigBuilder<'a> { /// Buffer for role data @@ -370,6 +340,21 @@ impl<'a> SwigBuilder<'a> { )?; Secp256r1SessionAuthority::LEN }, + AuthorityType::ProgramExec => { + ProgramExecAuthority::set_into_bytes( + authority_data, + &mut self.role_buffer[auth_offset..auth_offset + ProgramExecAuthority::LEN], + )?; + ProgramExecAuthority::LEN + }, + AuthorityType::ProgramExecSession => { + ProgramExecSessionAuthority::set_into_bytes( + authority_data, + &mut self.role_buffer + [auth_offset..auth_offset + ProgramExecSessionAuthority::LEN], + )?; + ProgramExecSessionAuthority::LEN + }, _ => return Err(SwigStateError::InvalidAuthorityData.into()), }; let size = authority_length + actions_data.len(); @@ -435,8 +420,7 @@ pub struct Swig { pub roles: u16, /// Counter for generating unique role IDs pub role_counter: u32, - /// Amount of lamports reserved for rent - // pub reserved_lamports: u64, + /// Wallet address bump seed pub wallet_bump: u8, pub _padding: [u8; 7], } @@ -499,6 +483,12 @@ impl Swig { AuthorityType::Secp256r1Session => unsafe { Secp256r1SessionAuthority::load_mut_unchecked(authority)? }, + AuthorityType::ProgramExec => unsafe { + ProgramExecAuthority::load_mut_unchecked(authority)? + }, + AuthorityType::ProgramExecSession => unsafe { + ProgramExecSessionAuthority::load_mut_unchecked(authority)? + }, _ => return Err(ProgramError::InvalidAccountData), }; @@ -596,6 +586,16 @@ impl<'a> SwigWithRoles<'a> { self.roles.get_unchecked(offset..offset + auth_len), )? }, + AuthorityType::ProgramExec => unsafe { + ProgramExecAuthority::load_unchecked( + self.roles.get_unchecked(offset..offset + auth_len), + )? + }, + AuthorityType::ProgramExecSession => unsafe { + ProgramExecSessionAuthority::load_unchecked( + self.roles.get_unchecked(offset..offset + auth_len), + )? + }, _ => return Err(ProgramError::InvalidAccountData), }; @@ -653,6 +653,16 @@ impl<'a> SwigWithRoles<'a> { offset..offset + position.authority_length() as usize, ))? }, + AuthorityType::ProgramExec => unsafe { + ProgramExecAuthority::load_unchecked(self.roles.get_unchecked( + offset..offset + position.authority_length() as usize, + ))? + }, + AuthorityType::ProgramExecSession => unsafe { + ProgramExecSessionAuthority::load_unchecked(self.roles.get_unchecked( + offset..offset + position.authority_length() as usize, + ))? + }, _ => return Err(ProgramError::InvalidAccountData), }; @@ -733,12 +743,24 @@ mod tests { use super::*; use crate::{ action::{all::All, manage_authority::ManageAuthority, sol_limit::SolLimit, Actionable}, - authority::{ed25519::ED25519Authority, secp256k1::CreateSecp256k1SessionAuthority}, + authority::ed25519::ED25519Authority, + Transmutable, }; - // Calculate exact buffer size needed for a test with N roles + #[repr(C, align(8))] + struct AlignedBuffer([u8; N]); + + impl AlignedBuffer { + fn new() -> Self { + Self([0u8; N]) + } + + fn as_mut_slice(&mut self) -> &mut [u8] { + &mut self.0 + } + } + fn calculate_buffer_size(num_roles: usize, action_bytes_per_role: usize) -> usize { - // Add extra buffer space to account for any alignment or boundary calculations Swig::LEN + (num_roles * (Position::LEN + ED25519Authority::LEN + action_bytes_per_role)) + 64 @@ -749,22 +771,26 @@ mod tests { action_bytes_per_role: usize, ) -> (Vec, [u8; 32], u8) { let buffer_size = calculate_buffer_size(num_roles, action_bytes_per_role); - let account_buffer = vec![0u8; buffer_size]; + let mut account_buffer = vec![0u8; buffer_size + 8]; + let offset = account_buffer.as_ptr().align_offset(8); + if offset != 0 { + account_buffer.drain(..offset); + } + account_buffer.truncate(buffer_size); let id = [1; 32]; let bump = 255; (account_buffer, id, bump) } - // Keep existing setup functions for backward compatibility - fn setup_test_buffer() -> ([u8; Swig::LEN + 256], [u8; 32], u8) { - let account_buffer = [0u8; Swig::LEN + 256]; + fn setup_test_buffer() -> (AlignedBuffer<{ Swig::LEN + 256 }>, [u8; 32], u8) { + let account_buffer = AlignedBuffer::new(); let id = [1; 32]; let bump = 255; (account_buffer, id, bump) } - fn setup_large_test_buffer() -> ([u8; Swig::LEN + 512], [u8; 32], u8) { - let account_buffer = [0u8; Swig::LEN + 512]; + fn setup_large_test_buffer() -> (AlignedBuffer<{ Swig::LEN + 512 }>, [u8; 32], u8) { + let account_buffer = AlignedBuffer::new(); let id = [1; 32]; let bump = 255; (account_buffer, id, bump) @@ -786,12 +812,14 @@ mod tests { // assert_eq!(swig.reserved_lamports, 0); // Test builder creation and verify buffer state - let builder = SwigBuilder::create(&mut account_buffer, swig).unwrap(); + let buffer_slice = account_buffer.as_mut_slice(); + let buffer_len = buffer_slice.len(); + let builder = SwigBuilder::create(buffer_slice, swig).unwrap(); assert_eq!(builder.swig.id, id); assert_eq!(builder.swig.bump, bump); assert_eq!(builder.swig.roles, 0); assert_eq!(builder.swig.role_counter, 0); - assert_eq!(builder.role_buffer.len(), account_buffer.len() - Swig::LEN); + assert_eq!(builder.role_buffer.len(), buffer_len - Swig::LEN); } #[test] @@ -824,7 +852,7 @@ mod tests { fn test_add_single_role() { let (mut account_buffer, id, bump) = setup_test_buffer(); let swig = Swig::new(id, bump, 0); - let mut builder = SwigBuilder::create(&mut account_buffer, swig).unwrap(); + let mut builder = SwigBuilder::create(account_buffer.as_mut_slice(), swig).unwrap(); let authority = ED25519Authority { public_key: [2; 32], @@ -853,7 +881,7 @@ mod tests { assert_eq!(builder.swig.role_counter, 1); // Verify role can be found and has correct data - let swig_with_roles = SwigWithRoles::from_bytes(&account_buffer).unwrap(); + let swig_with_roles = SwigWithRoles::from_bytes(account_buffer.as_mut_slice()).unwrap(); let role = swig_with_roles.get_role(0).unwrap().unwrap(); // Verify authority type @@ -873,7 +901,7 @@ mod tests { fn test_role_lookup() { let (mut account_buffer, id, bump) = setup_test_buffer(); let swig = Swig::new(id, bump, 0); - let mut builder = SwigBuilder::create(&mut account_buffer, swig).unwrap(); + let mut builder = SwigBuilder::create(account_buffer.as_mut_slice(), swig).unwrap(); let authority = ED25519Authority { public_key: [2; 32], @@ -896,7 +924,7 @@ mod tests { ) .unwrap(); - let swig_with_roles = SwigWithRoles::from_bytes(&account_buffer).unwrap(); + let swig_with_roles = SwigWithRoles::from_bytes(account_buffer.as_mut_slice()).unwrap(); // Test successful role lookup let role = swig_with_roles.get_role(0).unwrap(); @@ -922,7 +950,7 @@ mod tests { fn test_multiple_roles() { let (mut account_buffer, id, bump) = setup_test_buffer(); let swig = Swig::new(id, bump, 0); - let mut builder = SwigBuilder::create(&mut account_buffer, swig).unwrap(); + let mut builder = SwigBuilder::create(account_buffer.as_mut_slice(), swig).unwrap(); let authority1 = ED25519Authority { public_key: [2; 32], @@ -963,7 +991,7 @@ mod tests { assert_eq!(builder.swig.roles, 2); assert_eq!(builder.swig.role_counter, 2); - let swig_with_roles = SwigWithRoles::from_bytes(&account_buffer).unwrap(); + let swig_with_roles = SwigWithRoles::from_bytes(account_buffer.as_mut_slice()).unwrap(); // Verify roles have correct IDs and types let role1 = swig_with_roles.get_role(0).unwrap().unwrap(); @@ -985,7 +1013,7 @@ mod tests { fn test_get_mut_role() -> Result<(), ProgramError> { let (mut account_buffer, id, bump) = setup_test_buffer(); let swig = Swig::new(id, bump, 0); - let mut builder = SwigBuilder::create(&mut account_buffer, swig).unwrap(); + let mut builder = SwigBuilder::create(account_buffer.as_mut_slice(), swig).unwrap(); let authority = ED25519Authority { public_key: [2; 32], @@ -1011,7 +1039,8 @@ mod tests { .unwrap(); // Get a reference to the roles buffer for later modification - let roles_buffer = &mut account_buffer[Swig::LEN..]; + let buffer_slice = account_buffer.as_mut_slice(); + let roles_buffer = &mut buffer_slice[Swig::LEN..]; // Get mutable role and modify SolLimit let role_id = 0; @@ -1042,7 +1071,7 @@ mod tests { } // Verify the change persisted - let swig_with_roles = SwigWithRoles::from_bytes(&account_buffer).unwrap(); + let swig_with_roles = SwigWithRoles::from_bytes(account_buffer.as_mut_slice()).unwrap(); let role = swig_with_roles.get_role(0)?.unwrap(); // Navigate the actions data to find the SolLimit action @@ -1076,7 +1105,7 @@ mod tests { fn test_multiple_actions_with_token_limit() -> Result<(), ProgramError> { let (mut account_buffer, id, bump) = setup_test_buffer(); let swig = Swig::new(id, bump, 0); - let mut builder = SwigBuilder::create(&mut account_buffer, swig).unwrap(); + let mut builder = SwigBuilder::create(account_buffer.as_mut_slice(), swig).unwrap(); let authority = ED25519Authority { public_key: [2; 32], @@ -1125,7 +1154,8 @@ mod tests { .unwrap(); // Get a reference to the roles buffer for later modification - let roles_buffer = &mut account_buffer[Swig::LEN..]; + let buffer_slice = account_buffer.as_mut_slice(); + let roles_buffer = &mut buffer_slice[Swig::LEN..]; // Get mutable role and modify TokenLimit let role_id = 0; @@ -1172,7 +1202,7 @@ mod tests { } // Verify the changes persisted by checking each action - let swig_with_roles = SwigWithRoles::from_bytes(&account_buffer).unwrap(); + let swig_with_roles = SwigWithRoles::from_bytes(account_buffer.as_mut_slice()).unwrap(); let role = swig_with_roles.get_role(0)?.unwrap(); // Navigate actions data to find both actions and verify changes @@ -1220,7 +1250,7 @@ mod tests { fn test_lookup_role_id_comprehensive() -> Result<(), ProgramError> { let (mut account_buffer, id, bump) = setup_large_test_buffer(); let swig = Swig::new(id, bump, 0); - let mut builder = SwigBuilder::create(&mut account_buffer, swig).unwrap(); + let mut builder = SwigBuilder::create(account_buffer.as_mut_slice(), swig).unwrap(); // Create authorities with different public keys let authority1 = ED25519Authority { @@ -1274,7 +1304,7 @@ mod tests { .unwrap(); // Create SwigWithRoles for testing - let swig_with_roles = SwigWithRoles::from_bytes(&account_buffer).unwrap(); + let swig_with_roles = SwigWithRoles::from_bytes(account_buffer.as_mut_slice()).unwrap(); // Test basic lookup of each authority by public key println!("Looking up authority1"); @@ -1311,9 +1341,9 @@ mod tests { // Test duplicate authority test println!("Testing duplicate authority"); - let (mut new_buffer, _, _) = setup_large_test_buffer(); - let new_swig = Swig::new(id, bump, 0); - let mut new_builder = SwigBuilder::create(&mut new_buffer, new_swig).unwrap(); + let (mut new_buffer, id, bump) = setup_large_test_buffer(); + let swig = Swig::new(id, bump, 0); + let mut new_builder = SwigBuilder::create(new_buffer.as_mut_slice(), swig).unwrap(); // Add two roles with the same authority but different actions new_builder @@ -1331,7 +1361,7 @@ mod tests { ) .unwrap(); - let new_swig_with_roles = SwigWithRoles::from_bytes(&new_buffer).unwrap(); + let new_swig_with_roles = SwigWithRoles::from_bytes(new_buffer.as_mut_slice()).unwrap(); let duplicate_role_id = new_swig_with_roles.lookup_role_id(&authority1.public_key)?; assert_eq!( duplicate_role_id, diff --git a/state/src/transmute.rs b/state/src/transmute.rs new file mode 100644 index 00000000..cb2a6f1d --- /dev/null +++ b/state/src/transmute.rs @@ -0,0 +1,52 @@ +use pinocchio::program_error::ProgramError; + +/// Marker trait for types that can be safely cast from a raw pointer. +/// +/// Types implementing this trait must guarantee that the cast is safe, +/// ensuring proper field alignment and absence of padding bytes. +pub trait Transmutable: Sized { + /// The length of the type in bytes. + /// + /// Must equal the total size of all fields in the type. + const LEN: usize; + + /// Creates a reference to `Self` from a byte slice. + /// + /// # Safety + /// + /// The caller must ensure that `bytes` contains a valid representation of + /// the implementing type. + #[inline(always)] + unsafe fn load_unchecked(bytes: &[u8]) -> Result<&Self, ProgramError> { + if bytes.len() != Self::LEN { + return Err(ProgramError::InvalidAccountData); + } + Ok(&*(bytes.as_ptr() as *const Self)) + } +} + +/// Marker trait for types that can be mutably cast from a raw pointer. +/// +/// Types implementing this trait must guarantee that the mutable cast is safe, +/// ensuring proper field alignment and absence of padding bytes. +pub trait TransmutableMut: Transmutable { + /// Creates a mutable reference to `Self` from a mutable byte slice. + /// + /// # Safety + /// + /// The caller must ensure that `bytes` contains a valid representation of + /// the implementing type. + #[inline(always)] + unsafe fn load_mut_unchecked(bytes: &mut [u8]) -> Result<&mut Self, ProgramError> { + if bytes.len() != Self::LEN { + return Err(ProgramError::InvalidAccountData); + } + Ok(&mut *(bytes.as_mut_ptr() as *mut Self)) + } +} + +/// Trait for types that can be converted into a byte slice representation. +pub trait IntoBytes { + /// Converts the implementing type into a byte slice. + fn into_bytes(&self) -> Result<&[u8], ProgramError>; +} diff --git a/test-program-authority/Cargo.toml b/test-program-authority/Cargo.toml new file mode 100644 index 00000000..041e8a0d --- /dev/null +++ b/test-program-authority/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "test-program-authority" +version = "1.2.0" +edition = "2021" +license = "Apache-2.0" +publish = false + +[lib] +crate-type = ["cdylib", "lib"] +name = "test_program_authority" + +[features] +no-entrypoint = [] +test-sbf = [] +program_scope_test = [] + +[dependencies] +solana-program = "2.2.1" + +[dev-dependencies] +solana-program-test = "2.2.1" diff --git a/test-program-authority/Xargo.toml b/test-program-authority/Xargo.toml new file mode 100644 index 00000000..1ce1cf9e --- /dev/null +++ b/test-program-authority/Xargo.toml @@ -0,0 +1,2 @@ +[target.sbf-solana-solana.dependencies.std] +features = [] diff --git a/test-program-authority/src/lib.rs b/test-program-authority/src/lib.rs new file mode 100644 index 00000000..e4b31ead --- /dev/null +++ b/test-program-authority/src/lib.rs @@ -0,0 +1,25 @@ +//! Test program for ProgramExec authority testing +//! +//! This program is used to test the ProgramExec authority functionality. + +pub mod processor; + +#[cfg(not(feature = "no-entrypoint"))] +use solana_program::entrypoint; +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError, + pubkey::Pubkey, +}; + +#[cfg(not(feature = "no-entrypoint"))] +entrypoint!(process_instruction); + +solana_program::declare_id!("BXAu5ZWHnGun2XZjUZ9nqwiZ5dNVmofPGYdMC4rx4qLV"); + +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + processor::process_instruction(program_id, accounts, instruction_data) +} diff --git a/test-program-authority/src/processor.rs b/test-program-authority/src/processor.rs new file mode 100644 index 00000000..34d8651d --- /dev/null +++ b/test-program-authority/src/processor.rs @@ -0,0 +1,105 @@ +//! Test program instruction processor + +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, msg, program_error::ProgramError, + pubkey::Pubkey, +}; + +/// Instruction discriminators +pub mod instructions { + /// Test token transfer instruction - discriminator matches what ProgramExec + /// authority expects This instruction will call swig's sign instruction + /// via CPI + pub const TEST_TOKEN_TRANSFER: [u8; 8] = [1, 2, 3, 4, 5, 6, 7, 8]; + + /// Invalid discriminator for testing failures + pub const INVALID_DISCRIMINATOR: [u8; 8] = [9, 9, 9, 9, 9, 9, 9, 9]; +} + +/// State account data format: +/// - Byte 0: 0 = success, 1 = fail +pub const STATE_SIZE: usize = 1; + +pub fn process_instruction( + _program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + if instruction_data.len() < 8 { + return Err(ProgramError::InvalidInstructionData); + } + + let discriminator: [u8; 8] = instruction_data[0..8] + .try_into() + .map_err(|_| ProgramError::InvalidInstructionData)?; + let remaining_data = &instruction_data[8..]; + + match discriminator { + instructions::TEST_TOKEN_TRANSFER => process_test_token_transfer(accounts, remaining_data), + instructions::INVALID_DISCRIMINATOR => { + process_invalid_instruction(accounts, remaining_data) + }, + _ => Err(ProgramError::InvalidInstructionData), + } +} + +/// Process test token transfer - calls swig via CPI +/// +/// Expected accounts: +/// 0. `[]` Swig config account (first account for ProgramExec validation) +/// 1. `[]` Swig wallet address (second account for ProgramExec validation) +/// 2. `[]` State account (owned by this program, controls success/failure) +/// 3. `[]` Swig program +/// 4+. Additional accounts needed for the inner instruction +fn process_test_token_transfer(accounts: &[AccountInfo], _data: &[u8]) -> ProgramResult { + if accounts.len() < 4 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + let swig_config = &accounts[0]; + let swig_wallet = &accounts[1]; + let state_account = &accounts[2]; + let _swig_program = &accounts[3]; + + msg!("Test program: validating config and wallet accounts"); + msg!("Config: {}", swig_config.key); + msg!("Wallet: {}", swig_wallet.key); + + // Check the state account to determine if we should succeed or fail + let state_data = state_account.try_borrow_data()?; + if state_data.is_empty() { + msg!("State account is empty, defaulting to success"); + return Ok(()); + } + + let should_fail = state_data[0]; + + if should_fail == 0 { + msg!("State account indicates success"); + Ok(()) + } else { + msg!("State account indicates failure"); + Err(ProgramError::Custom(999)) + } +} + +/// Process invalid instruction - for testing failure cases +fn process_invalid_instruction(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + // Same as test_token_transfer but with invalid discriminator + process_test_token_transfer(accounts, data) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_discriminators() { + assert_eq!(instructions::TEST_TOKEN_TRANSFER.len(), 8); + assert_eq!(instructions::INVALID_DISCRIMINATOR.len(), 8); + assert_ne!( + instructions::TEST_TOKEN_TRANSFER, + instructions::INVALID_DISCRIMINATOR + ); + } +}