diff --git a/Cargo.lock b/Cargo.lock index 690f7b1c..69b93995 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,16 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "Inflector" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" -dependencies = [ - "lazy_static", - "regex", -] - [[package]] name = "addr2line" version = "0.24.2" @@ -64,14 +54,39 @@ dependencies = [ ] [[package]] -name = "ahash" -version = "0.7.8" +name = "agave-feature-set" +version = "2.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +checksum = "cf37e04221e454874cc9cebf92482ee40e4f1fb5412f701cd712835725582190" dependencies = [ - "getrandom 0.2.15", - "once_cell", - "version_check", + "ahash", + "solana-epoch-schedule", + "solana-hash", + "solana-pubkey", + "solana-sha256-hasher", +] + +[[package]] +name = "agave-precompiles" +version = "2.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4d3dd7591df608931ace909f1948fa14a8850d4919250ca6977f26a135b8665" +dependencies = [ + "agave-feature-set", + "bincode", + "digest 0.10.7", + "ed25519-dalek", + "lazy_static", + "libsecp256k1 0.6.0", + "openssl", + "sha3", + "solana-ed25519-program", + "solana-message", + "solana-precompile-error", + "solana-pubkey", + "solana-sdk-ids", + "solana-secp256k1-program", + "solana-secp256r1-program", ] [[package]] @@ -290,7 +305,7 @@ dependencies = [ "proptest", "rand 0.9.0", "ruint", - "rustc-hash 2.1.1", + "rustc-hash", "serde", "sha3", "tiny-keccak", @@ -851,12 +866,6 @@ dependencies = [ "syn 2.0.100", ] -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - [[package]] name = "atty" version = "0.2.14" @@ -960,22 +969,6 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" -[[package]] -name = "bitcoin-io" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" - -[[package]] -name = "bitcoin_hashes" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" -dependencies = [ - "bitcoin-io", - "hex-conservative", -] - [[package]] name = "bitflags" version = "1.3.2" @@ -1167,28 +1160,6 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" -[[package]] -name = "bytecheck" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" -dependencies = [ - "bytecheck_derive", - "ptr_meta", - "simdutf8", -] - -[[package]] -name = "bytecheck_derive" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "bytemuck" version = "1.22.0" @@ -1315,105 +1286,12 @@ dependencies = [ "inout", ] -[[package]] -name = "clap" -version = "2.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" -dependencies = [ - "ansi_term", - "atty", - "bitflags 1.3.2", - "strsim 0.8.0", - "textwrap", - "unicode-width 0.1.14", - "vec_map", -] - -[[package]] -name = "clap" -version = "4.5.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim 0.11.1", -] - -[[package]] -name = "clap_derive" -version = "4.5.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.100", -] - -[[package]] -name = "clap_lex" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" - -[[package]] -name = "cli-x" -version = "0.1.0" -dependencies = [ - "alloy-primitives", - "alloy-signer", - "alloy-signer-local", - "anyhow", - "bs58", - "clap 4.5.36", - "colored", - "console", - "dialoguer 0.11.0", - "directories 5.0.1", - "dirs", - "hex", - "indicatif", - "rand 0.8.5", - "secp256k1 0.30.0", - "serde", - "serde_json", - "solana-sdk", - "spl-associated-token-account 7.0.0", - "spl-token 8.0.0", - "swig-sdk", - "tokio", -] - [[package]] name = "colorchoice" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" -[[package]] -name = "colored" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" -dependencies = [ - "lazy_static", - "windows-sys 0.59.0", -] - [[package]] name = "combine" version = "3.8.1" @@ -1455,7 +1333,7 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width 0.2.0", + "unicode-width", "windows-sys 0.59.0", ] @@ -1727,7 +1605,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim 0.11.1", + "strsim", "syn 2.0.100", ] @@ -1864,31 +1742,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "dialoguer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59c6f2989294b9a498d3ad5491a79c6deb604617378e1cdc4bfc1c1361fe2f87" -dependencies = [ - "console", - "shell-words", - "tempfile", - "zeroize", -] - -[[package]] -name = "dialoguer" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" -dependencies = [ - "console", - "shell-words", - "tempfile", - "thiserror 1.0.69", - "zeroize", -] - [[package]] name = "digest" version = "0.9.0" @@ -1910,78 +1763,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "directories" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" -dependencies = [ - "dirs-sys 0.4.1", -] - -[[package]] -name = "directories" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" -dependencies = [ - "dirs-sys 0.5.0", -] - -[[package]] -name = "dirs" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" -dependencies = [ - "dirs-sys 0.4.1", -] - -[[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - -[[package]] -name = "dirs-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" -dependencies = [ - "libc", - "option-ext", - "redox_users 0.4.6", - "windows-sys 0.48.0", -] - -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users 0.5.0", - "windows-sys 0.59.0", -] - -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users 0.4.6", - "winapi", -] - [[package]] name = "displaydoc" version = "0.2.5" @@ -2564,26 +2345,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http 0.2.12", - "indexmap 2.9.0", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "h2" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75249d144030531f8dee69fe9cea04d3edf809a017ae445e2abdff6629e86633" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http 1.3.1", + "http", "indexmap 2.9.0", "slab", "tokio", @@ -2605,9 +2367,6 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash 0.7.8", -] [[package]] name = "hashbrown" @@ -2615,7 +2374,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash 0.8.11", + "ahash", ] [[package]] @@ -2664,28 +2423,6 @@ dependencies = [ "serde", ] -[[package]] -name = "hex-conservative" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" -dependencies = [ - "arrayvec", -] - -[[package]] -name = "hidapi" -version = "2.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03b876ecf37e86b359573c16c8366bc3eba52b689884a0fc42ba3f67203d2a8b" -dependencies = [ - "cc", - "cfg-if", - "libc", - "pkg-config", - "windows-sys 0.48.0", -] - [[package]] name = "histogram" version = "0.6.9" @@ -2733,17 +2470,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - [[package]] name = "http-body" version = "0.4.6" @@ -2751,30 +2477,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http 0.2.12", - "pin-project-lite", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http 1.3.1", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http 1.3.1", - "http-body 1.0.1", + "http", "pin-project-lite", ] @@ -2806,9 +2509,9 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", + "h2", + "http", + "http-body", "httparse", "httpdate", "itoa", @@ -2821,90 +2524,17 @@ dependencies = [ ] [[package]] -name = "hyper" -version = "1.6.0" +name = "hyper-rustls" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ - "bytes", - "futures-channel", "futures-util", - "h2 0.4.9", - "http 1.3.1", - "http-body 1.0.1", - "httparse", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" -dependencies = [ - "futures-util", - "http 0.2.12", - "hyper 0.14.32", + "http", + "hyper", "rustls 0.21.12", "tokio", - "tokio-rustls 0.24.1", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" -dependencies = [ - "futures-util", - "http 1.3.1", - "hyper 1.6.0", - "hyper-util", - "rustls 0.23.26", - "rustls-pki-types", - "tokio", - "tokio-rustls 0.26.2", - "tower-service", -] - -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper 1.6.0", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "hyper 1.6.0", - "libc", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", + "tokio-rustls", ] [[package]] @@ -3127,7 +2757,7 @@ dependencies = [ "console", "number_prefix", "portable-atomic", - "unicode-width 0.2.0", + "unicode-width", "web-time", ] @@ -3242,23 +2872,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "jupiter-swap-api-client" -version = "0.1.0" -source = "git+https://github.com/austbot/jupiter-swap-api-client.git#b12348206cc85033fdf859129ce68b2e39a078ed" -dependencies = [ - "anyhow", - "base64 0.22.1", - "reqwest 0.12.15", - "rust_decimal", - "serde", - "serde_json", - "serde_qs", - "solana-account-decoder", - "solana-sdk", - "thiserror 2.0.12", -] - [[package]] name = "k256" version = "0.13.4" @@ -3310,16 +2923,6 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" -[[package]] -name = "libredox" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" -dependencies = [ - "bitflags 2.9.0", - "libc", -] - [[package]] name = "libsecp256k1" version = "0.6.0" @@ -3348,14 +2951,11 @@ dependencies = [ "arrayref", "base64 0.22.1", "digest 0.9.0", - "hmac-drbg", "libsecp256k1-core 0.3.0", "libsecp256k1-gen-ecmult 0.3.0", "libsecp256k1-gen-genmult 0.3.0", "rand 0.8.5", "serde", - "sha2 0.9.9", - "typenum", ] [[package]] @@ -3549,22 +3149,6 @@ dependencies = [ "syn 2.0.100", ] -[[package]] -name = "macro_rules_attribute" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a82271f7bc033d84bbca59a3ce3e4159938cb08a9c3aebbe54d215131518a13" -dependencies = [ - "macro_rules_attribute-proc_macro", - "paste", -] - -[[package]] -name = "macro_rules_attribute-proc_macro" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dd856d451cc0da70e2ef2ce95a18e39a93b7558bedf10201ad28503f918568" - [[package]] name = "matchers" version = "0.1.0" @@ -3658,23 +3242,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9252111cf132ba0929b6f8e030cac2a24b507f3a4d6db6fb2896f27b354c714b" -[[package]] -name = "native-tls" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework 2.11.1", - "security-framework-sys", - "tempfile", -] - [[package]] name = "nix" version = "0.29.0" @@ -3945,6 +3512,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-src" +version = "300.5.0+3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8ce546f549326b0e6052b649198487d91320875da901e7bd11a06d1ee3f9c2f" +dependencies = [ + "cc", +] + [[package]] name = "openssl-sys" version = "0.9.107" @@ -3953,16 +3529,11 @@ checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" dependencies = [ "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - [[package]] name = "overload" version = "0.1.1" @@ -4032,15 +3603,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" -[[package]] -name = "pbkdf2" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216eaa586a190f0a738f2f918511eecfa90f13295abec0e457cdebcceda80cbd" -dependencies = [ - "crypto-mac", -] - [[package]] name = "pbkdf2" version = "0.11.0" @@ -4268,26 +3830,6 @@ dependencies = [ "unarray", ] -[[package]] -name = "ptr_meta" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" -dependencies = [ - "ptr_meta_derive", -] - -[[package]] -name = "ptr_meta_derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "qstring" version = "0.7.2" @@ -4340,7 +3882,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.1", + "rustc-hash", "rustls 0.23.26", "socket2", "thiserror 2.0.12", @@ -4359,7 +3901,7 @@ dependencies = [ "getrandom 0.3.2", "rand 0.9.0", "ring", - "rustc-hash 2.1.1", + "rustc-hash", "rustls 0.23.26", "rustls-pki-types", "rustls-platform-verifier", @@ -4556,28 +4098,6 @@ dependencies = [ "bitflags 2.9.0", ] -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom 0.2.15", - "libredox", - "thiserror 1.0.69", -] - -[[package]] -name = "redox_users" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" -dependencies = [ - "getrandom 0.2.15", - "libredox", - "thiserror 2.0.12", -] - [[package]] name = "regex" version = "1.11.1" @@ -4622,15 +4142,6 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" -[[package]] -name = "rend" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" -dependencies = [ - "bytecheck", -] - [[package]] name = "reqwest" version = "0.11.27" @@ -4643,11 +4154,11 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.32", - "hyper-rustls 0.24.2", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", "ipnet", "js-sys", "log", @@ -4657,14 +4168,14 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rustls 0.21.12", - "rustls-pemfile 1.0.4", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 0.1.2", - "system-configuration 0.5.1", + "sync_wrapper", + "system-configuration", "tokio", - "tokio-rustls 0.24.1", + "tokio-rustls", "tokio-util", "tower-service", "url", @@ -4675,50 +4186,6 @@ dependencies = [ "winreg", ] -[[package]] -name = "reqwest" -version = "0.12.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" -dependencies = [ - "base64 0.22.1", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2 0.4.9", - "http 1.3.1", - "http-body 1.0.1", - "http-body-util", - "hyper 1.6.0", - "hyper-rustls 0.27.5", - "hyper-tls", - "hyper-util", - "ipnet", - "js-sys", - "log", - "mime", - "native-tls", - "once_cell", - "percent-encoding", - "pin-project-lite", - "rustls-pemfile 2.2.0", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper 1.0.2", - "system-configuration 0.6.1", - "tokio", - "tokio-native-tls", - "tower", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows-registry", -] - [[package]] name = "reqwest-middleware" version = "0.2.5" @@ -4727,8 +4194,8 @@ checksum = "5a735987236a8e238bf0296c7e351b999c188ccc11477f311b82b55c93984216" dependencies = [ "anyhow", "async-trait", - "http 0.2.12", - "reqwest 0.11.27", + "http", + "reqwest", "serde", "task-local-extensions", "thiserror 1.0.69", @@ -4758,35 +4225,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rkyv" -version = "0.7.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" -dependencies = [ - "bitvec", - "bytecheck", - "bytes", - "hashbrown 0.12.3", - "ptr_meta", - "rend", - "rkyv_derive", - "seahash", - "tinyvec", - "uuid", -] - -[[package]] -name = "rkyv_derive" -version = "0.7.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "rlp" version = "0.5.2" @@ -4797,27 +4235,6 @@ dependencies = [ "rustc-hex", ] -[[package]] -name = "rpassword" -version = "7.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" -dependencies = [ - "libc", - "rtoolbox", - "windows-sys 0.48.0", -] - -[[package]] -name = "rtoolbox" -version = "0.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" -dependencies = [ - "libc", - "windows-sys 0.48.0", -] - [[package]] name = "ruint" version = "1.14.0" @@ -4851,34 +4268,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" -[[package]] -name = "rust_decimal" -version = "1.37.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faa7de2ba56ac291bd90c6b9bece784a52ae1411f9506544b3eae36dd2356d50" -dependencies = [ - "arrayvec", - "borsh 1.5.7", - "bytes", - "num-traits", - "rand 0.8.5", - "rkyv", - "serde", - "serde_json", -] - [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -4966,7 +4361,7 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.2.0", + "security-framework", ] [[package]] @@ -4978,15 +4373,6 @@ dependencies = [ "base64 0.21.7", ] -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "rustls-pki-types" version = "1.11.0" @@ -5011,7 +4397,7 @@ dependencies = [ "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki 0.103.1", - "security-framework 3.2.0", + "security-framework", "security-framework-sys", "webpki-root-certs", "windows-sys 0.59.0", @@ -5102,12 +4488,6 @@ dependencies = [ "untrusted", ] -[[package]] -name = "seahash" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" - [[package]] name = "sec1" version = "0.7.3" @@ -5129,18 +4509,7 @@ version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c42e6f1735c5f00f51e43e28d6634141f2bcad10931b2609ddd74a86d751260" dependencies = [ - "secp256k1-sys 0.4.2", -] - -[[package]] -name = "secp256k1" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" -dependencies = [ - "bitcoin_hashes", - "rand 0.8.5", - "secp256k1-sys 0.10.1", + "secp256k1-sys", ] [[package]] @@ -5152,36 +4521,14 @@ dependencies = [ "cc", ] -[[package]] -name = "secp256k1-sys" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" -dependencies = [ - "cc", -] - [[package]] name = "security-framework" -version = "2.11.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ "bitflags 2.9.0", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" -dependencies = [ - "bitflags 2.9.0", - "core-foundation 0.10.0", + "core-foundation 0.10.0", "core-foundation-sys", "libc", "security-framework-sys", @@ -5271,17 +4618,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_qs" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd34f36fe4c5ba9654417139a9b3a20d2e1de6012ee678ad14d240c22c78d8d6" -dependencies = [ - "percent-encoding", - "serde", - "thiserror 1.0.69", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -5324,19 +4660,6 @@ dependencies = [ "syn 2.0.100", ] -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap 2.9.0", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - [[package]] name = "serdect" version = "0.2.0" @@ -5453,12 +4776,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shell-words" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" - [[package]] name = "shlex" version = "1.3.0" @@ -5500,12 +4817,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - [[package]] name = "siphasher" version = "0.3.11" @@ -5558,45 +4869,6 @@ dependencies = [ "solana-sysvar", ] -[[package]] -name = "solana-account-decoder" -version = "2.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a8e4aacc7c681419ae63c1de36349d2156d5a4f4ffaea8e507335013e57189" -dependencies = [ - "Inflector", - "base64 0.22.1", - "bincode", - "bs58", - "bv", - "lazy_static", - "serde", - "serde_derive", - "serde_json", - "solana-account", - "solana-account-decoder-client-types", - "solana-clock", - "solana-config-program", - "solana-epoch-schedule", - "solana-fee-calculator", - "solana-instruction", - "solana-nonce", - "solana-program", - "solana-program-pack", - "solana-pubkey", - "solana-rent", - "solana-sdk-ids", - "solana-slot-hashes", - "solana-slot-history", - "solana-sysvar", - "spl-token 7.0.0", - "spl-token-2022 7.0.0", - "spl-token-group-interface 0.5.0", - "spl-token-metadata-interface 0.6.0", - "thiserror 2.0.12", - "zstd", -] - [[package]] name = "solana-account-decoder-client-types" version = "2.2.4" @@ -5813,7 +5085,7 @@ version = "2.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fb6728141dc45bdde9d68b67bb914013be28f94a2aea8bb7131ea8c6161c30e" dependencies = [ - "ahash 0.8.11", + "ahash", "lazy_static", "log", "qualifier_attr", @@ -5830,51 +5102,6 @@ dependencies = [ "solana-vote-program", ] -[[package]] -name = "solana-clap-utils" -version = "2.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3c210e89742f6c661eb4e7549eb779dbc56cab4b700a1fd761cd7c9b2de6e6" -dependencies = [ - "chrono", - "clap 2.34.0", - "rpassword", - "solana-clock", - "solana-cluster-type", - "solana-commitment-config", - "solana-derivation-path", - "solana-hash", - "solana-keypair", - "solana-message", - "solana-native-token", - "solana-presigner", - "solana-pubkey", - "solana-remote-wallet", - "solana-seed-phrase", - "solana-signature", - "solana-signer", - "thiserror 2.0.12", - "tiny-bip39", - "uriparse", - "url", -] - -[[package]] -name = "solana-cli-config" -version = "2.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cdb4a08bb852494082cd115e3b654b5505af4d2c0e9d24e602553d36dc2f1f5" -dependencies = [ - "dirs-next", - "lazy_static", - "serde", - "serde_derive", - "serde_yaml", - "solana-clap-utils", - "solana-commitment-config", - "url", -] - [[package]] name = "solana-client" version = "2.2.4" @@ -6134,9 +5361,9 @@ dependencies = [ [[package]] name = "solana-ed25519-program" -version = "2.2.1" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0c4dfce08d71d8f1e9b7d1b4e2c7101a8109903ad481acbbc1119a73d459f2" +checksum = "a1feafa1691ea3ae588f99056f4bdd1293212c7ece28243d7da257c443e84753" dependencies = [ "bytemuck", "bytemuck_derive", @@ -6241,7 +5468,7 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89e1d3b52b4a014efeaaab67f14e40af3972a4be61c523d612860db8e3145529" dependencies = [ - "ahash 0.8.11", + "ahash", "lazy_static", "solana-epoch-schedule", "solana-hash", @@ -6572,7 +5799,7 @@ dependencies = [ "gethostname", "lazy_static", "log", - "reqwest 0.11.27", + "reqwest", "solana-clock", "solana-cluster-type", "solana-sha256-hasher", @@ -6679,7 +5906,7 @@ version = "2.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b87939c18937f8bfad6028779a02fa123b27e986fb2c55fbbf683952a0e4932" dependencies = [ - "ahash 0.8.11", + "ahash", "bincode", "bv", "caps", @@ -6975,7 +6202,7 @@ dependencies = [ "crossbeam-channel", "futures-util", "log", - "reqwest 0.11.27", + "reqwest", "semver 1.0.26", "serde", "serde_derive", @@ -7043,30 +6270,6 @@ dependencies = [ "num_cpus", ] -[[package]] -name = "solana-remote-wallet" -version = "2.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e71f9dfc6f2a5df04c3fed2b90b0fbf0da3939f3383a9bf24a5c0bcf994f2b10" -dependencies = [ - "console", - "dialoguer 0.10.4", - "hidapi", - "log", - "num-derive", - "num-traits", - "parking_lot", - "qstring", - "semver 1.0.26", - "solana-derivation-path", - "solana-offchain-message", - "solana-pubkey", - "solana-signature", - "solana-signer", - "thiserror 2.0.12", - "uriparse", -] - [[package]] name = "solana-rent" version = "2.2.1" @@ -7141,7 +6344,7 @@ dependencies = [ "bs58", "indicatif", "log", - "reqwest 0.11.27", + "reqwest", "reqwest-middleware", "semver 1.0.26", "serde", @@ -7177,7 +6380,7 @@ dependencies = [ "base64 0.22.1", "bs58", "jsonrpc-core", - "reqwest 0.11.27", + "reqwest", "reqwest-middleware", "semver 1.0.26", "serde", @@ -7362,9 +6565,9 @@ dependencies = [ [[package]] name = "solana-secp256r1-program" -version = "2.2.1" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9ea9282950921611bd9e0200da7236fbb1d4f8388942f8451bd55e9f3cb228f" +checksum = "cf903cbdc36a161533812f90acfccdb434ed48982bd5dd71f3217930572c4a80" dependencies = [ "bytemuck", "openssl", @@ -7396,7 +6599,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36187af2324f079f65a675ec22b31c24919cb4ac22c79472e85d819db9bbbc15" dependencies = [ "hmac 0.12.1", - "pbkdf2 0.11.0", + "pbkdf2", "sha2 0.10.8", ] @@ -7903,47 +7106,6 @@ dependencies = [ "solana-signature", ] -[[package]] -name = "solana-transaction-status" -version = "2.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05fc20dd8feb089562b113a80115dab32b22fc64d63ca45c14d7b71e5e98d67" -dependencies = [ - "Inflector", - "base64 0.22.1", - "bincode", - "borsh 1.5.7", - "bs58", - "lazy_static", - "log", - "serde", - "serde_derive", - "serde_json", - "solana-account-decoder", - "solana-clock", - "solana-hash", - "solana-instruction", - "solana-loader-v2-interface", - "solana-message", - "solana-program", - "solana-pubkey", - "solana-reserved-account-keys", - "solana-reward-info", - "solana-sdk-ids", - "solana-signature", - "solana-system-interface", - "solana-transaction", - "solana-transaction-error", - "solana-transaction-status-client-types", - "spl-associated-token-account 6.0.0", - "spl-memo", - "spl-token 7.0.0", - "spl-token-2022 7.0.0", - "spl-token-group-interface 0.5.0", - "spl-token-metadata-interface 0.6.0", - "thiserror 2.0.12", -] - [[package]] name = "solana-transaction-status-client-types" version = "2.2.4" @@ -8196,22 +7358,6 @@ dependencies = [ "der", ] -[[package]] -name = "spl-associated-token-account" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76fee7d65013667032d499adc3c895e286197a35a0d3a4643c80e7fd3e9969e3" -dependencies = [ - "borsh 1.5.7", - "num-derive", - "num-traits", - "solana-program", - "spl-associated-token-account-client", - "spl-token 7.0.0", - "spl-token-2022 6.0.0", - "thiserror 1.0.69", -] - [[package]] name = "spl-associated-token-account" version = "7.0.0" @@ -8224,7 +7370,7 @@ dependencies = [ "solana-program", "spl-associated-token-account-client", "spl-token 8.0.0", - "spl-token-2022 8.0.1", + "spl-token-2022", "thiserror 2.0.12", ] @@ -8274,19 +7420,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "spl-elgamal-registry" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce0f668975d2b0536e8a8fd60e56a05c467f06021dae037f1d0cfed0de2e231d" -dependencies = [ - "bytemuck", - "solana-program", - "solana-zk-sdk", - "spl-pod", - "spl-token-confidential-transfer-proof-extraction 0.2.1", -] - [[package]] name = "spl-elgamal-registry" version = "0.2.0" @@ -8307,7 +7440,7 @@ dependencies = [ "solana-sysvar", "solana-zk-sdk", "spl-pod", - "spl-token-confidential-transfer-proof-extraction 0.3.0", + "spl-token-confidential-transfer-proof-extraction", ] [[package]] @@ -8344,19 +7477,6 @@ dependencies = [ "thiserror 2.0.12", ] -[[package]] -name = "spl-program-error" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d39b5186f42b2b50168029d81e58e800b690877ef0b30580d107659250da1d1" -dependencies = [ - "num-derive", - "num-traits", - "solana-program", - "spl-program-error-derive 0.4.1", - "thiserror 1.0.69", -] - [[package]] name = "spl-program-error" version = "0.7.0" @@ -8368,22 +7488,10 @@ dependencies = [ "solana-decode-error", "solana-msg", "solana-program-error", - "spl-program-error-derive 0.5.0", + "spl-program-error-derive", "thiserror 2.0.12", ] -[[package]] -name = "spl-program-error-derive" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d375dd76c517836353e093c2dbb490938ff72821ab568b545fd30ab3256b3e" -dependencies = [ - "proc-macro2", - "quote", - "sha2 0.10.8", - "syn 2.0.100", -] - [[package]] name = "spl-program-error-derive" version = "0.5.0" @@ -8396,28 +7504,6 @@ dependencies = [ "syn 2.0.100", ] -[[package]] -name = "spl-tlv-account-resolution" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd99ff1e9ed2ab86e3fd582850d47a739fec1be9f4661cba1782d3a0f26805f3" -dependencies = [ - "bytemuck", - "num-derive", - "num-traits", - "solana-account-info", - "solana-decode-error", - "solana-instruction", - "solana-msg", - "solana-program-error", - "solana-pubkey", - "spl-discriminator", - "spl-pod", - "spl-program-error 0.6.0", - "spl-type-length-value 0.7.0", - "thiserror 1.0.69", -] - [[package]] name = "spl-tlv-account-resolution" version = "0.10.0" @@ -8435,8 +7521,8 @@ dependencies = [ "solana-pubkey", "spl-discriminator", "spl-pod", - "spl-program-error 0.7.0", - "spl-type-length-value 0.8.0", + "spl-program-error", + "spl-type-length-value", "thiserror 2.0.12", ] @@ -8485,142 +7571,60 @@ dependencies = [ [[package]] name = "spl-token-2022" -version = "6.0.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b27f7405010ef816587c944536b0eafbcc35206ab6ba0f2ca79f1d28e488f4f" +checksum = "31f0dfbb079eebaee55e793e92ca5f433744f4b71ee04880bfd6beefba5973e5" dependencies = [ "arrayref", "bytemuck", "num-derive", "num-traits", "num_enum", - "solana-program", + "solana-account-info", + "solana-clock", + "solana-cpi", + "solana-decode-error", + "solana-instruction", + "solana-msg", + "solana-native-token", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-program-option", + "solana-program-pack", + "solana-pubkey", + "solana-rent", + "solana-sdk-ids", "solana-security-txt", + "solana-system-interface", + "solana-sysvar", "solana-zk-sdk", - "spl-elgamal-registry 0.1.1", + "spl-elgamal-registry", "spl-memo", "spl-pod", - "spl-token 7.0.0", - "spl-token-confidential-transfer-ciphertext-arithmetic 0.2.1", - "spl-token-confidential-transfer-proof-extraction 0.2.1", - "spl-token-confidential-transfer-proof-generation 0.2.0", - "spl-token-group-interface 0.5.0", - "spl-token-metadata-interface 0.6.0", - "spl-transfer-hook-interface 0.9.0", - "spl-type-length-value 0.7.0", - "thiserror 1.0.69", + "spl-token 8.0.0", + "spl-token-confidential-transfer-ciphertext-arithmetic", + "spl-token-confidential-transfer-proof-extraction", + "spl-token-confidential-transfer-proof-generation", + "spl-token-group-interface", + "spl-token-metadata-interface", + "spl-transfer-hook-interface", + "spl-type-length-value", + "thiserror 2.0.12", ] [[package]] -name = "spl-token-2022" -version = "7.0.0" +name = "spl-token-confidential-transfer-ciphertext-arithmetic" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9048b26b0df0290f929ff91317c83db28b3ef99af2b3493dd35baa146774924c" +checksum = "94ab20faf7b5edaa79acd240e0f21d5a2ef936aa99ed98f698573a2825b299c4" dependencies = [ - "arrayref", - "bytemuck", - "num-derive", - "num-traits", - "num_enum", - "solana-program", - "solana-security-txt", - "solana-zk-sdk", - "spl-elgamal-registry 0.1.1", - "spl-memo", - "spl-pod", - "spl-token 7.0.0", - "spl-token-confidential-transfer-ciphertext-arithmetic 0.2.1", - "spl-token-confidential-transfer-proof-extraction 0.2.1", - "spl-token-confidential-transfer-proof-generation 0.3.0", - "spl-token-group-interface 0.5.0", - "spl-token-metadata-interface 0.6.0", - "spl-transfer-hook-interface 0.9.0", - "spl-type-length-value 0.7.0", - "thiserror 2.0.12", -] - -[[package]] -name = "spl-token-2022" -version = "8.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f0dfbb079eebaee55e793e92ca5f433744f4b71ee04880bfd6beefba5973e5" -dependencies = [ - "arrayref", - "bytemuck", - "num-derive", - "num-traits", - "num_enum", - "solana-account-info", - "solana-clock", - "solana-cpi", - "solana-decode-error", - "solana-instruction", - "solana-msg", - "solana-native-token", - "solana-program-entrypoint", - "solana-program-error", - "solana-program-memory", - "solana-program-option", - "solana-program-pack", - "solana-pubkey", - "solana-rent", - "solana-sdk-ids", - "solana-security-txt", - "solana-system-interface", - "solana-sysvar", - "solana-zk-sdk", - "spl-elgamal-registry 0.2.0", - "spl-memo", - "spl-pod", - "spl-token 8.0.0", - "spl-token-confidential-transfer-ciphertext-arithmetic 0.3.0", - "spl-token-confidential-transfer-proof-extraction 0.3.0", - "spl-token-confidential-transfer-proof-generation 0.4.0", - "spl-token-group-interface 0.6.0", - "spl-token-metadata-interface 0.7.0", - "spl-transfer-hook-interface 0.10.0", - "spl-type-length-value 0.8.0", - "thiserror 2.0.12", -] - -[[package]] -name = "spl-token-confidential-transfer-ciphertext-arithmetic" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "170378693c5516090f6d37ae9bad2b9b6125069be68d9acd4865bbe9fc8499fd" -dependencies = [ - "base64 0.22.1", - "bytemuck", - "solana-curve25519", - "solana-zk-sdk", -] - -[[package]] -name = "spl-token-confidential-transfer-ciphertext-arithmetic" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94ab20faf7b5edaa79acd240e0f21d5a2ef936aa99ed98f698573a2825b299c4" -dependencies = [ - "base64 0.22.1", + "base64 0.22.1", "bytemuck", "solana-curve25519", "solana-zk-sdk", ] -[[package]] -name = "spl-token-confidential-transfer-proof-extraction" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff2d6a445a147c9d6dd77b8301b1e116c8299601794b558eafa409b342faf96" -dependencies = [ - "bytemuck", - "solana-curve25519", - "solana-program", - "solana-zk-sdk", - "spl-pod", - "thiserror 2.0.12", -] - [[package]] name = "spl-token-confidential-transfer-proof-extraction" version = "0.3.0" @@ -8641,28 +7645,6 @@ dependencies = [ "thiserror 2.0.12", ] -[[package]] -name = "spl-token-confidential-transfer-proof-generation" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8627184782eec1894de8ea26129c61303f1f0adeed65c20e0b10bc584f09356d" -dependencies = [ - "curve25519-dalek 4.1.3", - "solana-zk-sdk", - "thiserror 1.0.69", -] - -[[package]] -name = "spl-token-confidential-transfer-proof-generation" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e3597628b0d2fe94e7900fd17cdb4cfbb31ee35c66f82809d27d86e44b2848b" -dependencies = [ - "curve25519-dalek 4.1.3", - "solana-zk-sdk", - "thiserror 2.0.12", -] - [[package]] name = "spl-token-confidential-transfer-proof-generation" version = "0.4.0" @@ -8674,25 +7656,6 @@ dependencies = [ "thiserror 2.0.12", ] -[[package]] -name = "spl-token-group-interface" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d595667ed72dbfed8c251708f406d7c2814a3fa6879893b323d56a10bedfc799" -dependencies = [ - "bytemuck", - "num-derive", - "num-traits", - "solana-decode-error", - "solana-instruction", - "solana-msg", - "solana-program-error", - "solana-pubkey", - "spl-discriminator", - "spl-pod", - "thiserror 1.0.69", -] - [[package]] name = "spl-token-group-interface" version = "0.6.0" @@ -8712,27 +7675,6 @@ dependencies = [ "thiserror 2.0.12", ] -[[package]] -name = "spl-token-metadata-interface" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfb9c89dbc877abd735f05547dcf9e6e12c00c11d6d74d8817506cab4c99fdbb" -dependencies = [ - "borsh 1.5.7", - "num-derive", - "num-traits", - "solana-borsh", - "solana-decode-error", - "solana-instruction", - "solana-msg", - "solana-program-error", - "solana-pubkey", - "spl-discriminator", - "spl-pod", - "spl-type-length-value 0.7.0", - "thiserror 1.0.69", -] - [[package]] name = "spl-token-metadata-interface" version = "0.7.0" @@ -8750,35 +7692,10 @@ dependencies = [ "solana-pubkey", "spl-discriminator", "spl-pod", - "spl-type-length-value 0.8.0", + "spl-type-length-value", "thiserror 2.0.12", ] -[[package]] -name = "spl-transfer-hook-interface" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aa7503d52107c33c88e845e1351565050362c2314036ddf19a36cd25137c043" -dependencies = [ - "arrayref", - "bytemuck", - "num-derive", - "num-traits", - "solana-account-info", - "solana-cpi", - "solana-decode-error", - "solana-instruction", - "solana-msg", - "solana-program-error", - "solana-pubkey", - "spl-discriminator", - "spl-pod", - "spl-program-error 0.6.0", - "spl-tlv-account-resolution 0.9.0", - "spl-type-length-value 0.7.0", - "thiserror 1.0.69", -] - [[package]] name = "spl-transfer-hook-interface" version = "0.10.0" @@ -8798,30 +7715,12 @@ dependencies = [ "solana-pubkey", "spl-discriminator", "spl-pod", - "spl-program-error 0.7.0", - "spl-tlv-account-resolution 0.10.0", - "spl-type-length-value 0.8.0", + "spl-program-error", + "spl-tlv-account-resolution", + "spl-type-length-value", "thiserror 2.0.12", ] -[[package]] -name = "spl-type-length-value" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba70ef09b13af616a4c987797870122863cba03acc4284f226a4473b043923f9" -dependencies = [ - "bytemuck", - "num-derive", - "num-traits", - "solana-account-info", - "solana-decode-error", - "solana-msg", - "solana-program-error", - "spl-discriminator", - "spl-pod", - "thiserror 1.0.69", -] - [[package]] name = "spl-type-length-value" version = "0.8.0" @@ -8852,12 +7751,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - [[package]] name = "strsim" version = "0.11.1" @@ -8878,15 +7771,18 @@ dependencies = [ "alloy-signer", "alloy-signer-local", "anyhow", + "base64 0.22.1", "bincode", "bs58", "bytemuck", "ecdsa", + "hex", "litesvm", "litesvm-token", "no-padding", "num_enum", "once_cell", + "openssl", "pinocchio 0.8.1", "pinocchio-pubkey", "pinocchio-system", @@ -8897,6 +7793,7 @@ dependencies = [ "solana-clock", "solana-program", "solana-sdk", + "solana-secp256r1-program", "solana-stake-interface", "spl-memo", "static_assertions", @@ -8916,36 +7813,6 @@ dependencies = [ "pinocchio-system", ] -[[package]] -name = "swig-cli" -version = "0.0.1" -dependencies = [ - "anyhow", - "borsh 1.5.7", - "bs58", - "clap 4.5.36", - "directories 6.0.0", - "hex", - "jupiter-swap-api-client", - "libsecp256k1 0.7.2", - "macro_rules_attribute", - "rand 0.8.5", - "secp256k1 0.30.0", - "serde", - "serde_json", - "solana-account-decoder-client-types", - "solana-cli-config", - "solana-client", - "solana-pubkey", - "solana-sdk", - "solana-transaction-status", - "spl-associated-token-account 6.0.0", - "spl-token 7.0.0", - "swig-interface", - "swig-state-x", - "tokio", -] - [[package]] name = "swig-compact-instructions" version = "0.0.1" @@ -8963,6 +7830,7 @@ dependencies = [ "anyhow", "bytemuck", "solana-sdk", + "solana-secp256r1-program", "swig", "swig-compact-instructions", "swig-state-x", @@ -8981,12 +7849,12 @@ dependencies = [ "litesvm", "litesvm-token", "rand 0.8.5", - "secp256k1 0.21.3", + "secp256k1", "solana-account-decoder-client-types", "solana-client", "solana-program", "solana-sdk", - "spl-associated-token-account 7.0.0", + "spl-associated-token-account", "spl-token 8.0.0", "swig-interface", "swig-state-x", @@ -8998,12 +7866,16 @@ dependencies = [ name = "swig-state-x" version = "0.1.0" dependencies = [ + "agave-precompiles", "hex", "libsecp256k1 0.7.2", "murmur3", "no-padding", + "openssl", "pinocchio 0.8.1", + "pinocchio-pubkey", "rand 0.9.0", + "solana-secp256r1-program", "swig-assertions", ] @@ -9047,15 +7919,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - [[package]] name = "synstructure" version = "0.12.6" @@ -9087,18 +7950,7 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", - "system-configuration-sys 0.5.0", -] - -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags 2.9.0", - "core-foundation 0.9.4", - "system-configuration-sys 0.6.0", + "system-configuration-sys", ] [[package]] @@ -9111,16 +7963,6 @@ dependencies = [ "libc", ] -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "tap" version = "1.0.1" @@ -9180,15 +8022,6 @@ dependencies = [ "syn 2.0.100", ] -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width 0.1.14", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -9279,25 +8112,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tiny-bip39" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc59cb9dfc85bb312c3a78fd6aa8a8582e310b0fa885d5bb877f6dcc601839d" -dependencies = [ - "anyhow", - "hmac 0.8.1", - "once_cell", - "pbkdf2 0.4.0", - "rand 0.7.3", - "rustc-hash 1.1.0", - "sha2 0.9.9", - "thiserror 1.0.69", - "unicode-normalization", - "wasm-bindgen", - "zeroize", -] - [[package]] name = "tiny-keccak" version = "2.0.2" @@ -9361,16 +8175,6 @@ dependencies = [ "syn 2.0.100", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.24.1" @@ -9381,16 +8185,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-rustls" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" -dependencies = [ - "rustls 0.23.26", - "tokio", -] - [[package]] name = "tokio-stream" version = "0.1.17" @@ -9412,7 +8206,7 @@ dependencies = [ "log", "rustls 0.21.12", "tokio", - "tokio-rustls 0.24.1", + "tokio-rustls", "tungstenite", "webpki-roots 0.25.4", ] @@ -9456,27 +8250,6 @@ dependencies = [ "winnow", ] -[[package]] -name = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper 1.0.2", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - [[package]] name = "tower-service" version = "0.3.3" @@ -9559,7 +8332,7 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http 0.2.12", + "http", "httparse", "log", "rand 0.8.5", @@ -9613,21 +8386,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" -[[package]] -name = "unicode-normalization" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - [[package]] name = "unicode-width" version = "0.2.0" @@ -9659,12 +8417,6 @@ dependencies = [ "void", ] -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - [[package]] name = "untrusted" version = "0.9.0" @@ -9716,12 +8468,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "uuid" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" - [[package]] name = "valuable" version = "0.1.1" @@ -9734,12 +8480,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - [[package]] name = "version_check" version = "0.9.5" @@ -9957,7 +8697,7 @@ dependencies = [ "windows-interface", "windows-link", "windows-result", - "windows-strings 0.4.0", + "windows-strings", ] [[package]] @@ -9988,17 +8728,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" -[[package]] -name = "windows-registry" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" -dependencies = [ - "windows-result", - "windows-strings 0.3.1", - "windows-targets 0.53.0", -] - [[package]] name = "windows-result" version = "0.3.2" @@ -10008,15 +8737,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-strings" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" -dependencies = [ - "windows-link", -] - [[package]] name = "windows-strings" version = "0.4.0" @@ -10101,29 +8821,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" -dependencies = [ - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -10142,12 +8846,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -10166,12 +8864,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -10190,24 +8882,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - [[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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -10226,12 +8906,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -10250,12 +8924,6 @@ 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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -10274,12 +8942,6 @@ 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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -10298,12 +8960,6 @@ 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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" - [[package]] name = "winnow" version = "0.7.6" diff --git a/Cargo.toml b/Cargo.toml index 16f7d10f..6a943b24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,8 +8,8 @@ members = [ "assertions", "no-padding", "rust-sdk", - "cli", - "cli-x", + #"cli", + #"cli-x", ] [workspace.lints.rust] diff --git a/interface/Cargo.toml b/interface/Cargo.toml index 59dd753f..e7a7f8cb 100644 --- a/interface/Cargo.toml +++ b/interface/Cargo.toml @@ -17,3 +17,4 @@ swig-compact-instructions = { path = "../instructions", default-features = false ] } swig-state-x = { path = "../state-x" } anyhow = "1.0.75" +solana-secp256r1-program = "2.2.1" diff --git a/interface/src/lib.rs b/interface/src/lib.rs index 231ba644..2abeac10 100644 --- a/interface/src/lib.rs +++ b/interface/src/lib.rs @@ -5,6 +5,7 @@ use solana_sdk::{ pubkey::Pubkey, system_program, }; +use solana_secp256r1_program::new_secp256r1_instruction_with_signature; pub use swig; use swig::actions::{ add_authority_v1::AddAuthorityV1Args, create_session_v1::CreateSessionV1Args, @@ -16,11 +17,11 @@ use swig::actions::{ pub use swig_compact_instructions::*; use swig_state_x::{ action::{ - all::All, manage_authority::ManageAuthority, program::Program, program_scope::ProgramScope, - sol_limit::SolLimit, sol_recurring_limit::SolRecurringLimit, stake_all::StakeAll, - stake_limit::StakeLimit, stake_recurring_limit::StakeRecurringLimit, - sub_account::SubAccount, token_limit::TokenLimit, - token_recurring_limit::TokenRecurringLimit, Action, Permission, + all::All, manage_authority::ManageAuthority, oracle_limits::OracleTokenLimit, + program::Program, program_scope::ProgramScope, sol_limit::SolLimit, + sol_recurring_limit::SolRecurringLimit, stake_all::StakeAll, stake_limit::StakeLimit, + stake_recurring_limit::StakeRecurringLimit, sub_account::SubAccount, + token_limit::TokenLimit, token_recurring_limit::TokenRecurringLimit, Action, Permission, }, authority::{ secp256k1::{hex_encode, AccountsPayload}, @@ -43,6 +44,7 @@ pub enum ClientAction { StakeLimit(StakeLimit), StakeRecurringLimit(StakeRecurringLimit), StakeAll(StakeAll), + OracleTokenLimit(OracleTokenLimit), } impl ClientAction { @@ -66,6 +68,9 @@ impl ClientAction { (Permission::StakeRecurringLimit, StakeRecurringLimit::LEN) }, ClientAction::StakeAll(_) => (Permission::StakeAll, StakeAll::LEN), + ClientAction::OracleTokenLimit(_) => { + (Permission::OracleTokenLimit, OracleTokenLimit::LEN) + }, }; let offset = data.len() as u32; let header = Action::new( @@ -90,6 +95,7 @@ impl ClientAction { ClientAction::StakeLimit(action) => action.into_bytes(), ClientAction::StakeRecurringLimit(action) => action.into_bytes(), ClientAction::StakeAll(action) => action.into_bytes(), + ClientAction::OracleTokenLimit(action) => action.into_bytes(), }; data.extend_from_slice( bytes_res.map_err(|e| anyhow::anyhow!("Failed to serialize action {:?}", e))?, @@ -111,7 +117,7 @@ pub struct AuthorityConfig<'a> { pub authority: &'a [u8], } -fn prepare_secp_payload( +fn prepare_secp256k1_payload( current_slot: u64, counter: u32, data_payload: &[u8], @@ -152,7 +158,6 @@ impl CreateInstruction { swig_bump_seed, initial_authority.authority_type, initial_authority.authority.len() as u16, - actions.len() as u8, ); let mut write = Vec::new(); write.extend_from_slice( @@ -270,7 +275,7 @@ impl AddAuthorityInstruction { signature_bytes.extend_from_slice(arg_bytes); signature_bytes.extend_from_slice(new_authority_config.authority); signature_bytes.extend_from_slice(&action_bytes); - let nonced_payload = prepare_secp_payload( + let nonced_payload = prepare_secp256k1_payload( current_slot, counter, &signature_bytes, @@ -295,6 +300,105 @@ impl AddAuthorityInstruction { .concat(), }) } + + pub fn new_with_secp256r1_authority( + swig_account: Pubkey, + payer: Pubkey, + mut authority_payload_fn: F, + current_slot: u64, + counter: u32, + acting_role_id: u32, + public_key: &[u8; 33], + new_authority_config: AuthorityConfig, + actions: Vec, + ) -> anyhow::Result> + where + F: FnMut(&[u8]) -> [u8; 64], + { + let accounts = vec![ + AccountMeta::new(swig_account, false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(solana_sdk::sysvar::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 args_bytes = args + .into_bytes() + .map_err(|e| anyhow::anyhow!("Failed to serialize args {:?}", e))?; + + // Create the message hash for secp256r1 authentication + let mut account_payload_bytes = Vec::new(); + for account in &accounts { + account_payload_bytes.extend_from_slice( + accounts_payload_from_meta(account) + .into_bytes() + .map_err(|e| anyhow::anyhow!("Failed to serialize account meta {:?}", e))?, + ); + } + + let mut data_to_besigned_bytes = Vec::new(); + data_to_besigned_bytes.extend_from_slice(args_bytes); + data_to_besigned_bytes.extend_from_slice(new_authority_config.authority); + data_to_besigned_bytes.extend_from_slice(&action_bytes); + + // Compute message hash (keccak for secp256r1 compatibility) + let slot_bytes = current_slot.to_le_bytes(); + let counter_bytes = counter.to_le_bytes(); + let message_hash = keccak::hash( + &[ + &data_to_besigned_bytes, + &account_payload_bytes, + &slot_bytes[..], + &counter_bytes[..], + ] + .concat(), + ) + .to_bytes(); + + // Get signature from authority function + let signature = authority_payload_fn(&message_hash); + // Create secp256r1 verify instruction + let secp256r1_verify_ix = + new_secp256r1_instruction_with_signature(&message_hash, &signature, public_key); + // For secp256r1, the authority payload includes slot, counter, instruction + // index, and padding Must be at least 17 bytes to satisfy + // secp256r1_authority_authenticate() requirements + let instruction_sysvar_index = 3; // Instructions sysvar is at index 3 + let mut authority_payload = Vec::new(); + authority_payload.extend_from_slice(¤t_slot.to_le_bytes()); // 8 bytes + authority_payload.extend_from_slice(&counter.to_le_bytes()); // 4 bytes + authority_payload.push(instruction_sysvar_index as u8); // 1 byte: index of instruction sysvar + authority_payload.extend_from_slice(&[0u8; 4]); // 4 bytes padding to meet 17 byte minimum + + let main_ix = Instruction { + program_id: Pubkey::from(swig::ID), + accounts, + data: [ + args_bytes, + new_authority_config.authority, + &action_bytes, + &authority_payload, + ] + .concat(), + }; + + Ok(vec![secp256r1_verify_ix, main_ix]) + } } pub struct SignInstruction; @@ -361,7 +465,7 @@ impl SignInstruction { let mut signature_bytes = Vec::new(); signature_bytes.extend_from_slice(&ix_bytes); - let nonced_payload = prepare_secp_payload( + let nonced_payload = prepare_secp256k1_payload( current_slot, counter, &signature_bytes, @@ -380,6 +484,83 @@ impl SignInstruction { data: [arg_bytes, &ix_bytes, &authority_payload].concat(), }) } + + pub fn new_secp256r1( + swig_account: Pubkey, + payer: Pubkey, + mut authority_payload_fn: F, + current_slot: u64, + counter: u32, + inner_instruction: Instruction, + role_id: u32, + public_key: &[u8; 33], + ) -> anyhow::Result> + where + F: FnMut(&[u8]) -> [u8; 64], + { + let accounts = vec![ + AccountMeta::new(swig_account, false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ]; + let (accounts, ixs) = compact_instructions(swig_account, accounts, vec![inner_instruction]); + 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))?; + + // Create the message hash for secp256r1 authentication + let mut account_payload_bytes = Vec::new(); + for account in &accounts { + account_payload_bytes.extend_from_slice( + accounts_payload_from_meta(account) + .into_bytes() + .map_err(|e| anyhow::anyhow!("Failed to serialize account meta {:?}", e))?, + ); + } + + // Compute message hash (keccak for secp256r1 compatibility) + let slot_bytes = current_slot.to_le_bytes(); + let counter_bytes = counter.to_le_bytes(); + let message_hash = keccak::hash( + &[ + &ix_bytes, + &account_payload_bytes, + &slot_bytes[..], + &counter_bytes[..], + ] + .concat(), + ) + .to_bytes(); + + // Get signature from authority function + let signature = authority_payload_fn(&message_hash); + + // Create secp256r1 verify instruction + let secp256r1_verify_ix = + new_secp256r1_instruction_with_signature(&message_hash, &signature, public_key); + + // For secp256r1, the authority payload includes slot, counter, instruction + // index, and padding Must be at least 17 bytes to satisfy + // secp256r1_authority_authenticate() requirements + let instruction_sysvar_index = 3; // Try hardcoded index 3 for debugging + let mut authority_payload = Vec::new(); + authority_payload.extend_from_slice(¤t_slot.to_le_bytes()); // 8 bytes + authority_payload.extend_from_slice(&counter.to_le_bytes()); // 4 bytes + authority_payload.push(instruction_sysvar_index as u8); // 1 byte: index of instruction sysvar + authority_payload.extend_from_slice(&[0u8; 4]); // 4 bytes padding to meet 17 byte minimum + + let main_ix = Instruction { + program_id: Pubkey::from(swig::ID), + accounts, + data: [arg_bytes, &ix_bytes, &authority_payload].concat(), + }; + + Ok(vec![secp256r1_verify_ix, main_ix]) + } } pub struct RemoveAuthorityInstruction; @@ -413,9 +594,10 @@ impl RemoveAuthorityInstruction { swig_account: Pubkey, payer: Pubkey, mut authority_payload_fn: F, + current_slot: u64, + counter: u32, acting_role_id: u32, authority_to_remove_id: u32, - current_slot: u64, ) -> anyhow::Result where F: FnMut(&[u8]) -> [u8; 65], @@ -441,9 +623,9 @@ impl RemoveAuthorityInstruction { let mut signature_bytes = Vec::new(); signature_bytes.extend_from_slice(arg_bytes); - let nonced_payload = prepare_secp_payload( + let nonced_payload = prepare_secp256k1_payload( current_slot, - 0u32, + counter, &signature_bytes, &account_payload_bytes, &[], @@ -451,6 +633,7 @@ impl RemoveAuthorityInstruction { let signature = authority_payload_fn(&nonced_payload); let mut authority_payload = Vec::new(); authority_payload.extend_from_slice(¤t_slot.to_le_bytes()); + authority_payload.extend_from_slice(&counter.to_le_bytes()); authority_payload.extend_from_slice(&signature); Ok(Instruction { @@ -459,6 +642,82 @@ impl RemoveAuthorityInstruction { data: [arg_bytes, &authority_payload].concat(), }) } + + pub fn new_with_secp256r1_authority( + swig_account: Pubkey, + payer: Pubkey, + mut authority_payload_fn: F, + current_slot: u64, + counter: u32, + acting_role_id: u32, + authority_to_remove_id: u32, + public_key: &[u8; 33], + ) -> anyhow::Result> + where + F: FnMut(&[u8]) -> [u8; 64], + { + let accounts = vec![ + AccountMeta::new(swig_account, false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ]; + let args = RemoveAuthorityV1Args::new(acting_role_id, authority_to_remove_id, 17); // 17 bytes for secp256r1 authority payload + let arg_bytes = args + .into_bytes() + .map_err(|e| anyhow::anyhow!("Failed to serialize args {:?}", e))?; + + // Create the message hash for secp256r1 authentication + let mut account_payload_bytes = Vec::new(); + for account in &accounts { + account_payload_bytes.extend_from_slice( + accounts_payload_from_meta(account) + .into_bytes() + .map_err(|e| anyhow::anyhow!("Failed to serialize account meta {:?}", e))?, + ); + } + + let mut data_to_be_signed_bytes = Vec::new(); + data_to_be_signed_bytes.extend_from_slice(arg_bytes); + + // Compute message hash (keccak for secp256r1 compatibility) + let slot_bytes = current_slot.to_le_bytes(); + let counter_bytes = counter.to_le_bytes(); + let message_hash = keccak::hash( + &[ + &data_to_be_signed_bytes, + &account_payload_bytes, + &slot_bytes[..], + &counter_bytes[..], + ] + .concat(), + ) + .to_bytes(); + + // Get signature from authority function + let signature = authority_payload_fn(&message_hash); + + // Create secp256r1 verify instruction + let secp256r1_verify_ix = + new_secp256r1_instruction_with_signature(&message_hash, &signature, public_key); + + // For secp256r1, the authority payload includes slot, counter, instruction + // index, and padding + let instruction_sysvar_index = 3; // Instructions sysvar is at index 3 + let mut authority_payload = Vec::new(); + authority_payload.extend_from_slice(¤t_slot.to_le_bytes()); // 8 bytes + authority_payload.extend_from_slice(&counter.to_le_bytes()); // 4 bytes + authority_payload.push(instruction_sysvar_index as u8); // 1 byte: index of instruction sysvar + authority_payload.extend_from_slice(&[0u8; 4]); // 4 bytes padding to meet 17 byte minimum + + let main_ix = Instruction { + program_id: Pubkey::from(swig::ID), + accounts, + data: [arg_bytes, &authority_payload].concat(), + }; + + Ok(vec![secp256r1_verify_ix, main_ix]) + } } pub struct CreateSessionInstruction; @@ -478,7 +737,7 @@ impl CreateSessionInstruction { ]; let create_session_args = - CreateSessionV1Args::new(role_id, 1, session_duration, session_key.to_bytes()); + 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))?; @@ -494,6 +753,7 @@ impl CreateSessionInstruction { payer: Pubkey, mut authority_payload_fn: F, current_slot: u64, + counter: u32, role_id: u32, session_key: Pubkey, session_duration: u64, @@ -507,7 +767,7 @@ impl CreateSessionInstruction { AccountMeta::new_readonly(system_program::ID, false), ]; let create_session_args = - CreateSessionV1Args::new(role_id, 1, session_duration, session_key.to_bytes()); + 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))?; @@ -523,9 +783,9 @@ impl CreateSessionInstruction { let mut signature_bytes = Vec::new(); signature_bytes.extend_from_slice(args_bytes); - let nonced_payload = prepare_secp_payload( + let nonced_payload = prepare_secp256k1_payload( current_slot, - 0u32, + counter, &signature_bytes, &account_payload_bytes, &[], @@ -533,6 +793,7 @@ impl CreateSessionInstruction { let signature = authority_payload_fn(&nonced_payload); let mut authority_payload = Vec::new(); authority_payload.extend_from_slice(¤t_slot.to_le_bytes()); + authority_payload.extend_from_slice(&counter.to_le_bytes()); authority_payload.extend_from_slice(&signature); Ok(Instruction { @@ -541,6 +802,84 @@ impl CreateSessionInstruction { data: [args_bytes, &authority_payload].concat(), }) } + + pub fn new_with_secp256r1_authority( + swig_account: Pubkey, + payer: Pubkey, + mut authority_payload_fn: F, + current_slot: u64, + counter: u32, + role_id: u32, + session_key: Pubkey, + session_duration: u64, + public_key: &[u8; 33], + ) -> anyhow::Result> + where + F: FnMut(&[u8]) -> [u8; 64], + { + let accounts = vec![ + AccountMeta::new(swig_account, false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(solana_sdk::sysvar::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))?; + + // Create the message hash for secp256r1 authentication + let mut account_payload_bytes = Vec::new(); + for account in &accounts { + account_payload_bytes.extend_from_slice( + accounts_payload_from_meta(account) + .into_bytes() + .map_err(|e| anyhow::anyhow!("Failed to serialize account meta {:?}", e))?, + ); + } + + let mut data_to_be_signed_bytes = Vec::new(); + data_to_be_signed_bytes.extend_from_slice(args_bytes); + + // Compute message hash (keccak for secp256r1 compatibility) + let slot_bytes = current_slot.to_le_bytes(); + let counter_bytes = counter.to_le_bytes(); + let message_hash = keccak::hash( + &[ + &data_to_be_signed_bytes, + &account_payload_bytes, + &slot_bytes[..], + &counter_bytes[..], + ] + .concat(), + ) + .to_bytes(); + + // Get signature from authority function + let signature = authority_payload_fn(&message_hash); + + // Create secp256r1 verify instruction + let secp256r1_verify_ix = + new_secp256r1_instruction_with_signature(&message_hash, &signature, public_key); + + // For secp256r1, the authority payload includes slot, counter, instruction + // index, and padding + let instruction_sysvar_index = 3; // Instructions sysvar is at index 3 + let mut authority_payload = Vec::new(); + authority_payload.extend_from_slice(¤t_slot.to_le_bytes()); // 8 bytes + authority_payload.extend_from_slice(&counter.to_le_bytes()); // 4 bytes + authority_payload.push(instruction_sysvar_index as u8); // 1 byte: index of instruction sysvar + authority_payload.extend_from_slice(&[0u8; 4]); // 4 bytes padding to meet 17 byte minimum + + let main_ix = Instruction { + program_id: Pubkey::from(swig::ID), + accounts, + data: [args_bytes, &authority_payload].concat(), + }; + + Ok(vec![secp256r1_verify_ix, main_ix]) + } } // Sub-account instruction structures @@ -611,7 +950,7 @@ impl CreateSubAccountInstruction { // Sign the payload let nonced_payload = - prepare_secp_payload(current_slot, 0u32, args_bytes, &account_payload_bytes, &[]); + prepare_secp256k1_payload(current_slot, 0u32, args_bytes, &account_payload_bytes, &[]); let signature = authority_payload_fn(&nonced_payload); // Add authority payload @@ -625,6 +964,83 @@ impl CreateSubAccountInstruction { data: [args_bytes, &authority_payload].concat(), }) } + + pub fn new_with_secp256r1_authority( + swig_account: Pubkey, + payer: Pubkey, + mut authority_payload_fn: F, + current_slot: u64, + counter: u32, + sub_account: Pubkey, + role_id: u32, + sub_account_bump: u8, + public_key: &[u8; 33], + ) -> anyhow::Result> + where + F: FnMut(&[u8]) -> [u8; 64], + { + let accounts = vec![ + AccountMeta::new(swig_account, false), + AccountMeta::new(payer, true), + AccountMeta::new(sub_account, false), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(solana_sdk::sysvar::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))?; + + // Create the message hash for secp256r1 authentication + let mut account_payload_bytes = Vec::new(); + for account in &accounts { + account_payload_bytes.extend_from_slice( + accounts_payload_from_meta(account) + .into_bytes() + .map_err(|e| anyhow::anyhow!("Failed to serialize account meta {:?}", e))?, + ); + } + + let mut data_to_be_signed_bytes = Vec::new(); + data_to_be_signed_bytes.extend_from_slice(args_bytes); + + // Compute message hash (keccak for secp256r1 compatibility) + let slot_bytes = current_slot.to_le_bytes(); + let counter_bytes = counter.to_le_bytes(); + let message_hash = keccak::hash( + &[ + &data_to_be_signed_bytes, + &account_payload_bytes, + &slot_bytes[..], + &counter_bytes[..], + ] + .concat(), + ) + .to_bytes(); + + // Get signature from authority function + let signature = authority_payload_fn(&message_hash); + + // Create secp256r1 verify instruction + let secp256r1_verify_ix = + new_secp256r1_instruction_with_signature(&message_hash, &signature, public_key); + + // For secp256r1, the authority payload includes slot, counter, instruction + // index, and padding + let mut authority_payload = Vec::new(); + authority_payload.extend_from_slice(¤t_slot.to_le_bytes()); // 8 bytes + authority_payload.extend_from_slice(&counter.to_le_bytes()); // 4 bytes + authority_payload.push(4); // this is the index of the instruction sysvar + + let main_ix = Instruction { + program_id: program_id(), + accounts, + data: [args_bytes, &authority_payload].concat(), + }; + + Ok(vec![secp256r1_verify_ix, main_ix]) + } } pub struct WithdrawFromSubAccountInstruction; @@ -692,7 +1108,7 @@ impl WithdrawFromSubAccountInstruction { // Sign the payload let nonced_payload = - prepare_secp_payload(current_slot, 0u32, args_bytes, &account_payload_bytes, &[]); + prepare_secp256k1_payload(current_slot, 0u32, args_bytes, &account_payload_bytes, &[]); let signature = authority_payload_fn(&nonced_payload); // Add authority payload @@ -781,7 +1197,7 @@ impl WithdrawFromSubAccountInstruction { // Sign the payload let nonced_payload = - prepare_secp_payload(current_slot, 0u32, args_bytes, &account_payload_bytes, &[]); + prepare_secp256k1_payload(current_slot, 0u32, args_bytes, &account_payload_bytes, &[]); let signature = authority_payload_fn(&nonced_payload); // Add authority payload @@ -795,6 +1211,168 @@ impl WithdrawFromSubAccountInstruction { data: [args_bytes, &authority_payload].concat(), }) } + + pub fn new_with_secp256r1_authority( + swig_account: Pubkey, + payer: Pubkey, + mut authority_payload_fn: F, + current_slot: u64, + counter: u32, + sub_account: Pubkey, + role_id: u32, + amount: u64, + public_key: &[u8; 33], + ) -> anyhow::Result> + where + F: FnMut(&[u8]) -> [u8; 64], + { + let accounts = vec![ + AccountMeta::new(swig_account, false), + AccountMeta::new_readonly(payer, true), + AccountMeta::new(sub_account, false), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(solana_sdk::sysvar::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))?; + + // Create the message hash for secp256r1 authentication + let mut account_payload_bytes = Vec::new(); + for account in &accounts { + account_payload_bytes.extend_from_slice( + accounts_payload_from_meta(account) + .into_bytes() + .map_err(|e| anyhow::anyhow!("Failed to serialize account meta {:?}", e))?, + ); + } + + let mut data_to_be_signed_bytes = Vec::new(); + data_to_be_signed_bytes.extend_from_slice(args_bytes); + + // Compute message hash (keccak for secp256r1 compatibility) + let slot_bytes = current_slot.to_le_bytes(); + let counter_bytes = counter.to_le_bytes(); + let message_hash = keccak::hash( + &[ + &data_to_be_signed_bytes, + &account_payload_bytes, + &slot_bytes[..], + &counter_bytes[..], + ] + .concat(), + ) + .to_bytes(); + + // Get signature from authority function + let signature = authority_payload_fn(&message_hash); + + // Create secp256r1 verify instruction + let secp256r1_verify_ix = + new_secp256r1_instruction_with_signature(&message_hash, &signature, public_key); + + // For secp256r1, the authority payload includes slot, counter, instruction + // index, and padding + let instruction_sysvar_index = 3; // Instructions sysvar is at index 3 + let mut authority_payload = Vec::new(); + authority_payload.extend_from_slice(¤t_slot.to_le_bytes()); // 8 bytes + authority_payload.extend_from_slice(&counter.to_le_bytes()); // 4 bytes + authority_payload.push(instruction_sysvar_index as u8); // 1 byte: index of instruction sysvar + authority_payload.extend_from_slice(&[0u8; 4]); // 4 bytes padding to meet 17 byte minimum + + let main_ix = Instruction { + program_id: program_id(), + accounts, + data: [args_bytes, &authority_payload].concat(), + }; + + Ok(vec![secp256r1_verify_ix, main_ix]) + } + + pub fn new_token_with_secp256r1_authority( + swig_account: Pubkey, + payer: Pubkey, + mut authority_payload_fn: F, + current_slot: u64, + counter: u32, + sub_account: Pubkey, + sub_account_token: Pubkey, + swig_token: Pubkey, + token_program: Pubkey, + role_id: u32, + amount: u64, + public_key: &[u8; 33], + ) -> anyhow::Result> + where + F: FnMut(&[u8]) -> [u8; 64], + { + let accounts = vec![ + AccountMeta::new(swig_account, false), + AccountMeta::new_readonly(payer, true), + AccountMeta::new(sub_account, false), + AccountMeta::new(sub_account_token, false), + AccountMeta::new(swig_token, false), + AccountMeta::new_readonly(token_program, false), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(solana_sdk::sysvar::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))?; + + // Create the message hash for secp256r1 authentication + let mut account_payload_bytes = Vec::new(); + for account in &accounts { + account_payload_bytes.extend_from_slice( + accounts_payload_from_meta(account) + .into_bytes() + .map_err(|e| anyhow::anyhow!("Failed to serialize account meta {:?}", e))?, + ); + } + + let mut data_to_be_signed_bytes = Vec::new(); + data_to_be_signed_bytes.extend_from_slice(args_bytes); + + // Compute message hash (keccak for secp256r1 compatibility) + let slot_bytes = current_slot.to_le_bytes(); + let counter_bytes = counter.to_le_bytes(); + let message_hash = keccak::hash( + &[ + &data_to_be_signed_bytes, + &account_payload_bytes, + &slot_bytes[..], + &counter_bytes[..], + ] + .concat(), + ) + .to_bytes(); + + // Get signature from authority function + let signature = authority_payload_fn(&message_hash); + + // Create secp256r1 verify instruction + let secp256r1_verify_ix = + new_secp256r1_instruction_with_signature(&message_hash, &signature, public_key); + + // For secp256r1, the authority payload includes slot, counter, instruction + // index, and padding + let mut authority_payload = Vec::new(); + authority_payload.extend_from_slice(¤t_slot.to_le_bytes()); // 8 bytes + authority_payload.extend_from_slice(&counter.to_le_bytes()); // 4 bytes + authority_payload.push(7); // this is the index of the instruction sysvar (account 7) + + let main_ix = Instruction { + program_id: program_id(), + accounts, + data: [args_bytes, &authority_payload].concat(), + }; + + Ok(vec![secp256r1_verify_ix, main_ix]) + } } pub struct SubAccountSignInstruction; @@ -866,7 +1444,7 @@ impl SubAccountSignInstruction { // Sign the payload let nonced_payload = - prepare_secp_payload(current_slot, 0u32, &ix_bytes, &account_payload_bytes, &[]); + prepare_secp256k1_payload(current_slot, 0u32, &ix_bytes, &account_payload_bytes, &[]); let signature = authority_payload_fn(&nonced_payload); // Add authority payload @@ -880,6 +1458,86 @@ impl SubAccountSignInstruction { data: [args_bytes, &ix_bytes, &authority_payload].concat(), }) } + + pub fn new_with_secp256r1_authority( + swig_account: Pubkey, + sub_account: Pubkey, + payer: Pubkey, + mut authority_payload_fn: F, + current_slot: u64, + counter: u32, + role_id: u32, + instructions: Vec, + public_key: &[u8; 33], + ) -> anyhow::Result> + where + F: FnMut(&[u8]) -> [u8; 64], + { + let 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), + AccountMeta::new_readonly(solana_sdk::sysvar::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))?; + + // Create the message hash for secp256r1 authentication + let mut account_payload_bytes = Vec::new(); + for account in &accounts { + account_payload_bytes.extend_from_slice( + accounts_payload_from_meta(account) + .into_bytes() + .map_err(|e| anyhow::anyhow!("Failed to serialize account meta {:?}", e))?, + ); + } + + let mut data_to_be_signed_bytes = Vec::new(); + data_to_be_signed_bytes.extend_from_slice(&ix_bytes); + + // Compute message hash (keccak for secp256r1 compatibility) + let slot_bytes = current_slot.to_le_bytes(); + let counter_bytes = counter.to_le_bytes(); + let message_hash = keccak::hash( + &[ + &data_to_be_signed_bytes, + &account_payload_bytes, + &slot_bytes[..], + &counter_bytes[..], + ] + .concat(), + ) + .to_bytes(); + + // Get signature from authority function + let signature = authority_payload_fn(&message_hash); + + // Create secp256r1 verify instruction + let secp256r1_verify_ix = + new_secp256r1_instruction_with_signature(&message_hash, &signature, public_key); + + // For secp256r1, the authority payload includes slot, counter, instruction + // index, and padding + let mut authority_payload = Vec::new(); + authority_payload.extend_from_slice(¤t_slot.to_le_bytes()); // 8 bytes + authority_payload.extend_from_slice(&counter.to_le_bytes()); // 4 bytes + authority_payload.push(4); // this is the index of the instruction sysvar + + let main_ix = Instruction { + program_id: program_id(), + accounts, + data: [args_bytes, &ix_bytes, &authority_payload].concat(), + }; + + Ok(vec![secp256r1_verify_ix, main_ix]) + } } pub struct ToggleSubAccountInstruction; @@ -948,7 +1606,7 @@ impl ToggleSubAccountInstruction { let prefix = &[]; // Sign the payload - let nonced_payload = prepare_secp_payload( + let nonced_payload = prepare_secp256k1_payload( current_slot, 0u32, args_bytes, @@ -968,4 +1626,81 @@ impl ToggleSubAccountInstruction { data: [args_bytes, &authority_payload].concat(), }) } + + pub fn new_with_secp256r1_authority( + swig_account: Pubkey, + payer: Pubkey, + mut authority_payload_fn: F, + current_slot: u64, + counter: u32, + sub_account: Pubkey, + role_id: u32, + enabled: bool, + public_key: &[u8; 33], + ) -> anyhow::Result> + where + F: FnMut(&[u8]) -> [u8; 64], + { + let accounts = vec![ + AccountMeta::new(swig_account, false), + AccountMeta::new_readonly(payer, true), + AccountMeta::new(sub_account, false), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ]; + + let args = ToggleSubAccountV1Args::new(role_id, enabled); + let args_bytes = args + .into_bytes() + .map_err(|e| anyhow::anyhow!("Failed to serialize args {:?}", e))?; + + // Create the message hash for secp256r1 authentication + let mut account_payload_bytes = Vec::new(); + for account in &accounts { + account_payload_bytes.extend_from_slice( + accounts_payload_from_meta(account) + .into_bytes() + .map_err(|e| anyhow::anyhow!("Failed to serialize account meta {:?}", e))?, + ); + } + + let mut data_to_be_signed_bytes = Vec::new(); + data_to_be_signed_bytes.extend_from_slice(args_bytes); + + // Compute message hash (keccak for secp256r1 compatibility) + let slot_bytes = current_slot.to_le_bytes(); + let counter_bytes = counter.to_le_bytes(); + let message_hash = keccak::hash( + &[ + &data_to_be_signed_bytes, + &account_payload_bytes, + &slot_bytes[..], + &counter_bytes[..], + ] + .concat(), + ) + .to_bytes(); + + // Get signature from authority function + let signature = authority_payload_fn(&message_hash); + + // Create secp256r1 verify instruction + let secp256r1_verify_ix = + new_secp256r1_instruction_with_signature(&message_hash, &signature, public_key); + + // For secp256r1, the authority payload includes slot, counter, instruction + // index, and padding + let mut authority_payload = Vec::new(); + authority_payload.extend_from_slice(¤t_slot.to_le_bytes()); // 8 bytes + authority_payload.extend_from_slice(&counter.to_le_bytes()); // 4 bytes + authority_payload.push(4); // this is the index of the instruction sysvar + + let main_ix = Instruction { + program_id: program_id(), + accounts, + data: [args_bytes, &authority_payload].concat(), + }; + + Ok(vec![secp256r1_verify_ix, main_ix]) + } } diff --git a/program/Cargo.toml b/program/Cargo.toml index c1d73756..badfe892 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -43,6 +43,10 @@ solana-client = "=2.2.4" solana-program = "=2.2.1" once_cell = "1.21.3" spl-memo = "=6.0.0" +base64 = "0.22.1" +solana-secp256r1-program = "2.2.1" +openssl = { version = "0.10.72", features = ["vendored"] } +hex = "0.4.3" [features] test-bpf = [] diff --git a/program/src/actions/add_authority_v1.rs b/program/src/actions/add_authority_v1.rs index ef360af4..112c0ceb 100644 --- a/program/src/actions/add_authority_v1.rs +++ b/program/src/actions/add_authority_v1.rs @@ -167,9 +167,7 @@ pub fn add_authority_v1( // closure here to avoid borrowing swig_account_data for the whole function so // that we can mutate after realloc - if add_authority_v1.args.num_actions == 0 { - return Err(SwigError::InvalidAuthorityMustHaveAtLeastOneAction.into()); - } + // Note: num_actions validation is now done internally in add_role let swig_account_data = unsafe { ctx.accounts.swig.borrow_mut_data_unchecked() }; let swig_data_len = swig_account_data.len(); let new_authority_type = AuthorityType::try_from(add_authority_v1.args.new_authority_type)?; @@ -242,7 +240,6 @@ pub fn add_authority_v1( swig_builder.add_role( new_authority_type, add_authority_v1.authority_data, - add_authority_v1.args.num_actions, add_authority_v1.actions, )?; Ok(()) diff --git a/program/src/actions/create_session_v1.rs b/program/src/actions/create_session_v1.rs index 4591f409..13f2ef17 100644 --- a/program/src/actions/create_session_v1.rs +++ b/program/src/actions/create_session_v1.rs @@ -25,7 +25,7 @@ use crate::{ /// /// # Fields /// * `instruction` - The instruction type identifier -/// * `authority_payload_len` - Length of the authority payload +/// * `_padding` - Padding bytes for alignment /// * `role_id` - ID of the role creating the session /// * `session_duration` - Duration of the session in slots /// * `session_key` - Unique key for the session @@ -33,7 +33,7 @@ use crate::{ #[repr(C, align(8))] pub struct CreateSessionV1Args { pub instruction: SwigInstruction, - pub authority_payload_len: u16, + _padding: u16, pub role_id: u32, pub session_duration: u64, pub session_key: [u8; 32], @@ -54,19 +54,13 @@ impl CreateSessionV1Args { /// /// # Arguments /// * `role_id` - ID of the role creating the session - /// * `authority_payload_len` - Length of the authority payload /// * `session_duration` - Duration of the session in slots /// * `session_key` - Unique key for the session - pub fn new( - role_id: u32, - authority_payload_len: u16, - session_duration: u64, - session_key: [u8; 32], - ) -> Self { + pub fn new(role_id: u32, session_duration: u64, session_key: [u8; 32]) -> Self { Self { instruction: SwigInstruction::CreateSessionV1, + _padding: 0, role_id, - authority_payload_len, session_duration, session_key, } diff --git a/program/src/actions/create_sub_account_v1.rs b/program/src/actions/create_sub_account_v1.rs index 5b5d1cae..35e8fd00 100644 --- a/program/src/actions/create_sub_account_v1.rs +++ b/program/src/actions/create_sub_account_v1.rs @@ -143,7 +143,7 @@ pub fn create_sub_account_v1( // Check that the swig account is owned by our program check_self_owned(ctx.accounts.swig, SwigError::OwnerMismatchSwigAccount)?; check_system_owner(ctx.accounts.sub_account, SwigError::OwnerMismatchSubAccount)?; - check_zero_balance(ctx.accounts.sub_account, SwigError::SubAccountAlreadyExists)?; + check_zero_data(ctx.accounts.sub_account, SwigError::SubAccountAlreadyExists)?; // Parse the instruction data let create_sub_account = CreateSubAccountV1::from_instruction_bytes(data)?; @@ -183,12 +183,20 @@ pub fn create_sub_account_v1( slot, )?; } - // Check if the role has the SubAccount permission - let sub_account_action = RoleMut::get_action_mut::(role.actions, &[])?; - if sub_account_action.is_none() { + // Check if the role has the required permissions (All or SubAccount) + let has_all_permission = { + let all_action = RoleMut::get_action_mut::(role.actions, &[])?; + all_action.is_some() + }; + + let has_sub_account_permission = { + let sub_account_action = RoleMut::get_action_mut::(role.actions, &[])?; + sub_account_action.is_some() + }; + + if !has_all_permission && !has_sub_account_permission { return Err(SwigError::AuthorityCannotCreateSubAccount.into()); } - let sub_account_action = sub_account_action.unwrap(); // Derive the sub-account address using the authority index as seed let role_id_bytes = create_sub_account.args.role_id.to_le_bytes(); let bump_byte = [create_sub_account.args.sub_account_bump]; @@ -202,11 +210,22 @@ pub fn create_sub_account_v1( // Create the sub-account let account_size = SwigSubAccount::LEN; let lamports_needed = Rent::get()?.minimum_balance(account_size); - // Create account + + // Get current lamports in the account + let current_lamports = unsafe { *ctx.accounts.sub_account.borrow_lamports_unchecked() }; + + // Only transfer additional lamports if needed for rent exemption + let lamports_to_transfer = if current_lamports >= lamports_needed { + 0 + } else { + lamports_needed - current_lamports + }; + + // Create account with proper space allocation and ownership assignment let create_account_ix = CreateAccount { from: ctx.accounts.payer, to: ctx.accounts.sub_account, - lamports: lamports_needed, + lamports: lamports_to_transfer, space: account_size as u64, owner: &crate::ID, }; @@ -224,5 +243,13 @@ pub fn create_sub_account_v1( sub_account.enabled = true; // Set reserved lamports to the minimum rent-exempt amount sub_account.reserved_lamports = lamports_needed; + + // Update the SubAccount action to store the newly created sub-account's public + // key + if let Some(sub_account_action_mut) = RoleMut::get_action_mut::(role.actions, &[])? + { + sub_account_action_mut.sub_account = *ctx.accounts.sub_account.key(); + } + Ok(()) } diff --git a/program/src/actions/create_v1.rs b/program/src/actions/create_v1.rs index 9f96771d..823e6178 100644 --- a/program/src/actions/create_v1.rs +++ b/program/src/actions/create_v1.rs @@ -33,7 +33,6 @@ use crate::{ /// * `authority_type` - Type of authority to be created /// * `authority_data_len` - Length of the authority data /// * `bump` - Bump seed for PDA derivation -/// * `num_actions` - Number of actions associated with the authority /// * `id` - Unique identifier for the wallet #[repr(C, align(8))] #[derive(Debug, NoPadding)] @@ -42,7 +41,7 @@ pub struct CreateV1Args { pub authority_type: u16, pub authority_data_len: u16, pub bump: u8, - pub num_actions: u8, + _padding: u8, pub id: [u8; 32], } @@ -54,13 +53,11 @@ impl CreateV1Args { /// * `bump` - Bump seed for PDA derivation /// * `authority_type` - Type of authority to create /// * `authority_data_len` - Length of the authority data - /// * `num_actions` - Number of actions to associate pub fn new( id: [u8; 32], bump: u8, authority_type: AuthorityType, authority_data_len: u16, - num_actions: u8, ) -> Self { Self { discriminator: SwigInstruction::CreateV1, @@ -68,7 +65,7 @@ impl CreateV1Args { bump, authority_type: authority_type as u16, authority_data_len, - num_actions, + _padding: 0, } } } @@ -141,7 +138,7 @@ impl<'a> CreateV1<'a> { #[inline(always)] pub fn create_v1(ctx: Context, create: &[u8]) -> ProgramResult { check_system_owner(ctx.accounts.swig, SwigError::OwnerMismatchSwigAccount)?; - check_zero_balance(ctx.accounts.swig, SwigError::AccountNotEmptySwigAccount)?; + check_zero_data(ctx.accounts.swig, SwigError::AccountNotEmptySwigAccount)?; let create_v1 = CreateV1::from_instruction_bytes(create)?; let bump = check_self_pda( @@ -168,10 +165,20 @@ pub fn create_v1(ctx: Context, create: &[u8]) -> ProgramResult let lamports_needed = Rent::get()?.minimum_balance(account_size); let swig = Swig::new(create_v1.args.id, bump, lamports_needed); + // Get current lamports in the account + let current_lamports = unsafe { *ctx.accounts.swig.borrow_lamports_unchecked() }; + + // Only transfer additional lamports if needed for rent exemption + let lamports_to_transfer = if current_lamports >= lamports_needed { + 0 + } else { + lamports_needed - current_lamports + }; + CreateAccount { from: ctx.accounts.payer, to: ctx.accounts.swig, - lamports: lamports_needed, + lamports: lamports_to_transfer, space: account_size as u64, owner: &crate::ID, } @@ -181,11 +188,6 @@ pub fn create_v1(ctx: Context, create: &[u8]) -> ProgramResult let swig_data = unsafe { ctx.accounts.swig.borrow_mut_data_unchecked() }; let mut swig_builder = SwigBuilder::create(swig_data, swig)?; - swig_builder.add_role( - authority_type, - create_v1.authority_data, - create_v1.args.num_actions, - create_v1.actions, - )?; + swig_builder.add_role(authority_type, create_v1.authority_data, create_v1.actions)?; Ok(()) } diff --git a/program/src/actions/remove_authority_v1.rs b/program/src/actions/remove_authority_v1.rs index 2563dcb3..3cb42095 100644 --- a/program/src/actions/remove_authority_v1.rs +++ b/program/src/actions/remove_authority_v1.rs @@ -172,7 +172,6 @@ pub fn remove_authority_v1( // Authenticate the caller let clock = Clock::get()?; let slot = clock.slot; - if acting_role.authority.session_based() { acting_role.authority.authenticate_session( all_accounts, diff --git a/program/src/actions/sign_v1.rs b/program/src/actions/sign_v1.rs index bea7ac5d..5b3cced6 100644 --- a/program/src/actions/sign_v1.rs +++ b/program/src/actions/sign_v1.rs @@ -19,6 +19,7 @@ use swig_compact_instructions::InstructionIterator; use swig_state_x::{ action::{ all::All, + oracle_limits::OracleTokenLimit, program_scope::{NumericType, ProgramScope}, sol_limit::SolLimit, sol_recurring_limit::SolRecurringLimit, @@ -40,6 +41,7 @@ use crate::{ accounts::{Context, SignV1Accounts}, SwigInstruction, }, + util::{build_restricted_keys, get_price_data_from_bytes, hash_except}, AccountClassification, }; // use swig_instructions::InstructionIterator; @@ -47,6 +49,27 @@ use crate::{ pub const INSTRUCTION_SYSVAR_ACCOUNT: Pubkey = from_str("Sysvar1nstructions1111111111111111111111111"); +/// Exclude range for token account balance field (bytes 64-72) +const TOKEN_BALANCE_EXCLUDE_RANGE: core::ops::Range = 64..72; + +/// Exclude range for stake account balance field (bytes 184-192) +const STAKE_BALANCE_EXCLUDE_RANGE: core::ops::Range = 184..192; + +/// Token account field ranges +const TOKEN_MINT_RANGE: core::ops::Range = 0..32; +const TOKEN_AUTHORITY_RANGE: core::ops::Range = 32..64; +const TOKEN_BALANCE_RANGE: core::ops::Range = 64..72; +const TOKEN_STATE_INDEX: usize = 108; + +/// Stake account field ranges +const STAKE_BALANCE_RANGE: core::ops::Range = 184..192; + +/// Account state constants +const TOKEN_ACCOUNT_INITIALIZED_STATE: u8 = 1; + +/// Empty exclude ranges for hash_except when no exclusions are needed +const NO_EXCLUDE_RANGES: &[core::ops::Range] = &[]; + /// Arguments for signing a transaction with a Swig wallet. /// /// # Fields @@ -182,15 +205,16 @@ pub fn sign_v1( const UNINIT_KEY: MaybeUninit<&Pubkey> = MaybeUninit::uninit(); let mut restricted_keys: [MaybeUninit<&Pubkey>; 2] = [UNINIT_KEY; 2]; let rkeys: &[&Pubkey] = unsafe { - if role.position.authority_type()? == AuthorityType::Ed25519 { + if role.position.authority_type()? == AuthorityType::Secp256k1 + || role.position.authority_type()? == AuthorityType::Secp256r1 + { + restricted_keys[0].write(ctx.accounts.payer.key()); + core::slice::from_raw_parts(restricted_keys.as_ptr() as _, 1) + } else { let authority_index = *sign_v1.authority_payload.get_unchecked(0) as usize; restricted_keys[0].write(ctx.accounts.payer.key()); restricted_keys[1].write(all_accounts[authority_index].key()); core::slice::from_raw_parts(restricted_keys.as_ptr() as _, 2) - } else { - restricted_keys[0].write(ctx.accounts.payer.key()); - - core::slice::from_raw_parts(restricted_keys.as_ptr() as _, 1) } }; let ix_iter = InstructionIterator::new( @@ -203,6 +227,72 @@ pub fn sign_v1( let seeds = swig_account_signer(&swig.id, &b); let signer = seeds.as_slice(); + // Capture account snapshots before instruction execution + const UNINIT_HASH: MaybeUninit<[u8; 32]> = MaybeUninit::uninit(); + let mut account_snapshots: [MaybeUninit<[u8; 32]>; 100] = [UNINIT_HASH; 100]; + + // Build exclusion ranges for each account type for snapshots + for (index, account_classifier) in account_classifiers.iter().enumerate() { + let account = unsafe { all_accounts.get_unchecked(index) }; + + // Only check writable accounts as read-only accounts won't modify data + if !account.is_writable() { + continue; + } + + let hash = match account_classifier { + AccountClassification::ThisSwig { .. } => { + let data = unsafe { account.borrow_data_unchecked() }; + // For ThisSwig accounts, hash the entire account data to ensure no unexpected + // modifications Lamports are handled separately in the + // permission check, but we still need to verify + // that the account data itself hasn't been tampered with + let hash = hash_except(&data, NO_EXCLUDE_RANGES); + Some(hash) + }, + AccountClassification::SwigTokenAccount { .. } => { + let data = unsafe { account.borrow_data_unchecked() }; + // Exclude token balance field (bytes 64-72) + let exclude_ranges = [TOKEN_BALANCE_EXCLUDE_RANGE]; + let hash = hash_except(&data, &exclude_ranges); + Some(hash) + }, + AccountClassification::SwigStakeAccount { .. } => { + let data = unsafe { account.borrow_data_unchecked() }; + // Exclude stake balance field (bytes 184-192) + let exclude_ranges = [STAKE_BALANCE_EXCLUDE_RANGE]; + let hash = hash_except(&data, &exclude_ranges); + Some(hash) + }, + AccountClassification::ProgramScope { .. } => { + let data = unsafe { account.borrow_data_unchecked() }; + // For program scope, we need to get the actual program scope to know what to + // exclude + let owner = unsafe { all_accounts.get_unchecked(index).owner() }; + if let Some(program_scope) = + RoleMut::get_action_mut::(role.actions, owner.as_ref())? + { + let start = program_scope.balance_field_start as usize; + let end = program_scope.balance_field_end as usize; + if start < end && end <= data.len() { + let exclude_ranges = [start..end]; + let hash = hash_except(&data, &exclude_ranges); + Some(hash) + } else { + None + } + } else { + None + } + }, + _ => None, + }; + + if hash != None && index < 100 { + account_snapshots[index].write(hash.unwrap()); + } + } + for ix in ix_iter { if let Ok(instruction) = ix { instruction.execute(all_accounts, ctx.accounts.swig.key(), &[signer.into()])?; @@ -210,6 +300,7 @@ pub fn sign_v1( return Err(SwigError::InstructionExecutionError.into()); } } + let actions = role.actions; if RoleMut::get_action_mut::(actions, &[])?.is_some() { return Ok(()); @@ -217,7 +308,19 @@ pub fn sign_v1( for (index, account) in account_classifiers.iter().enumerate() { match account { AccountClassification::ThisSwig { lamports } => { - let current_lamports = all_accounts[index].lamports(); + let account = unsafe { all_accounts.get_unchecked(index) }; + + // Only validate snapshots for writable accounts + if account.is_writable() { + let data = unsafe { &account.borrow_data_unchecked() }; + let current_hash = hash_except(&data, NO_EXCLUDE_RANGES); + let snapshot_hash = unsafe { account_snapshots[index].assume_init_ref() }; + if *snapshot_hash != current_hash { + return Err(SwigError::AccountDataModifiedUnexpectedly.into()); + } + } + + let current_lamports = account.lamports(); let mut matched = false; if current_lamports < swig.reserved_lamports { return Err( @@ -226,7 +329,36 @@ pub fn sign_v1( } if lamports > ¤t_lamports { let amount_diff = lamports - current_lamports; - + { + if let Some(action) = + RoleMut::get_action_mut::(actions, &[0u8])? + { + let oracle_data = unsafe { + &all_accounts + .get_unchecked(all_accounts.len() - 1) + .borrow_data_unchecked() + }; + + let current_timestamp = Clock::get()?.unix_timestamp; + let feed_id: [u8; 32] = [ + 239, 13, 139, 111, 218, 44, 235, 164, 29, 161, 93, 64, 149, + 209, 218, 57, 42, 13, 47, 142, 208, 198, 199, 188, 15, 76, 250, + 200, 194, 128, 181, 109, + ]; + let (price, confidence, exponent) = get_price_data_from_bytes( + oracle_data, + current_timestamp, + 100, + &feed_id, + )?; + + action.run_for_sol(amount_diff, price, confidence, exponent)?; + + if !action.passthrough_check { + continue; + } + }; + } { if let Some(action) = RoleMut::get_action_mut::(actions, &[])? { @@ -246,32 +378,37 @@ pub fn sign_v1( } }, AccountClassification::SwigTokenAccount { balance } => { - let data = - unsafe { &all_accounts.get_unchecked(index).borrow_data_unchecked() }; - let mint = unsafe { data.get_unchecked(0..32) }; - let delegate = unsafe { data.get_unchecked(72..76) }; - let state = unsafe { *data.get_unchecked(108) }; - let authority = unsafe { data.get_unchecked(32..64) }; - let close_authority = unsafe { data.get_unchecked(128..132) }; + let account = unsafe { all_accounts.get_unchecked(index) }; + + // Only validate snapshots for writable accounts + if account.is_writable() { + let data = unsafe { &account.borrow_data_unchecked() }; + let exclude_ranges = [TOKEN_BALANCE_EXCLUDE_RANGE]; + let current_hash = hash_except(&data, &exclude_ranges); + let snapshot_hash = unsafe { account_snapshots[index].assume_init_ref() }; + if *snapshot_hash != current_hash { + return Err(SwigError::AccountDataModifiedUnexpectedly.into()); + } + } + + let data = unsafe { &account.borrow_data_unchecked() }; + let mint = unsafe { data.get_unchecked(TOKEN_MINT_RANGE) }; + let state = unsafe { *data.get_unchecked(TOKEN_STATE_INDEX) }; + + let authority = unsafe { data.get_unchecked(TOKEN_AUTHORITY_RANGE) }; let current_token_balance = u64::from_le_bytes(unsafe { - data.get_unchecked(64..72) + data.get_unchecked(TOKEN_BALANCE_RANGE) .try_into() .map_err(|_| ProgramError::InvalidAccountData)? }); - if delegate != [0u8; 4] || close_authority != [0u8; 4] { - return Err( - SwigAuthenticateError::PermissionDeniedTokenAccountDelegatePresent - .into(), - ); - } if authority != ctx.accounts.swig.key() { return Err( SwigAuthenticateError::PermissionDeniedTokenAccountAuthorityNotSwig .into(), ); } - if state != 1 { + if state != TOKEN_ACCOUNT_INITIALIZED_STATE { return Err( SwigAuthenticateError::PermissionDeniedTokenAccountNotInitialized .into(), @@ -280,6 +417,34 @@ pub fn sign_v1( if balance > ¤t_token_balance { let diff = balance - current_token_balance; + // Check oracle token limit + { + if let Some(action) = + RoleMut::get_action_mut::(actions, &[0u8])? + { + let oracle_data = unsafe { + &all_accounts + .get_unchecked(all_accounts.len() - 1) + .borrow_data_unchecked() + }; + + let current_timestamp = Clock::get()?.unix_timestamp; + let (feed_id, decimal) = + OracleTokenLimit::get_feed_id_and_decimal_from_mint(mint)?; + + let (price, confidence, exponent) = get_price_data_from_bytes( + oracle_data, + current_timestamp, + 100, + &feed_id, + )?; + + action.run_for_token(diff, price, confidence, exponent, decimal)?; + if !action.passthrough_check { + continue; + } + }; + } { if let Some(action) = RoleMut::get_action_mut::(actions, mint)? @@ -300,13 +465,25 @@ pub fn sign_v1( } }, AccountClassification::SwigStakeAccount { state, balance } => { + let account = unsafe { all_accounts.get_unchecked(index) }; + + // Only validate snapshots for writable accounts + if account.is_writable() { + let data = unsafe { &account.borrow_data_unchecked() }; + let exclude_ranges = [STAKE_BALANCE_EXCLUDE_RANGE]; + let current_hash = hash_except(&data, &exclude_ranges); + let snapshot_hash = unsafe { account_snapshots[index].assume_init_ref() }; + if *snapshot_hash != current_hash { + return Err(SwigError::AccountDataModifiedUnexpectedly.into()); + } + } + // Get current stake balance from account data - let data = - unsafe { &all_accounts.get_unchecked(index).borrow_data_unchecked() }; + let data = unsafe { &account.borrow_data_unchecked() }; // Extract current stake balance from account let current_stake_balance = u64::from_le_bytes(unsafe { - data.get_unchecked(184..192) + data.get_unchecked(STAKE_BALANCE_RANGE) .try_into() .map_err(|_| ProgramError::InvalidAccountData)? }); @@ -354,9 +531,7 @@ pub fn sign_v1( role_index, balance, } => { - // Get the data from the account - let data = - unsafe { &all_accounts.get_unchecked(index).borrow_data_unchecked() }; + let account = unsafe { all_accounts.get_unchecked(index) }; // Get the role with the ProgramScope action let owner = unsafe { all_accounts.get_unchecked(index).owner() }; @@ -376,15 +551,32 @@ pub fn sign_v1( // Get the current balance by using the program_scope's // read_account_balance method - let account = unsafe { all_accounts.get_unchecked(index) }; let data = unsafe { account.borrow_data_unchecked() }; - let current_balance = if program_scope.balance_field_end - - program_scope.balance_field_start + // Check if balance field range is valid + if program_scope.balance_field_end - program_scope.balance_field_start > 0 + && program_scope.balance_field_end as usize <= data.len() { - // Use the defined balance field indices to read the balance - match program_scope.read_account_balance(data) { + // Only validate snapshots for writable accounts + if account.is_writable() { + // Hash the data excluding the balance field + let exclude_ranges = + [program_scope.balance_field_start as usize + ..program_scope.balance_field_end as usize]; + let current_hash = hash_except(&data, &exclude_ranges); + let snapshot_hash = + unsafe { account_snapshots[index].assume_init_ref() }; + if *snapshot_hash != current_hash { + return Err( + SwigError::AccountDataModifiedUnexpectedly.into() + ); + } + } + + // Read the current balance from the account data + let current_balance = match program_scope.read_account_balance(data) + { Ok(bal) => bal, Err(err) => { msg!("Error reading balance from account data: {:?}", err); @@ -392,15 +584,15 @@ pub fn sign_v1( SwigError::InvalidProgramScopeBalanceFields.into() ); }, - } - } else { - return Err(SwigError::InvalidProgramScopeBalanceFields.into()); - }; + }; - let amount_spent = balance - current_balance; + let amount_spent = balance - current_balance; - // Execute the program scope run with proper amount and slot - program_scope.run(amount_spent, Some(slot))?; + // Execute the program scope run with proper amount and slot + program_scope.run(amount_spent, Some(slot))?; + } else { + return Err(SwigError::InvalidProgramScopeBalanceFields.into()); + } }, None => { return Err( diff --git a/program/src/actions/sub_account_sign_v1.rs b/program/src/actions/sub_account_sign_v1.rs index fdc6bbfe..5b745f98 100644 --- a/program/src/actions/sub_account_sign_v1.rs +++ b/program/src/actions/sub_account_sign_v1.rs @@ -28,6 +28,7 @@ use crate::{ accounts::{Context, SubAccountSignV1Accounts}, SwigInstruction, }, + util::build_restricted_keys, AccountClassification, }; @@ -134,6 +135,7 @@ pub fn sub_account_sign_v1( account_classifiers: &[AccountClassification], ) -> ProgramResult { check_stack_height(1, SwigError::Cpi)?; + check_self_owned(ctx.accounts.swig, SwigError::OwnerMismatchSubAccount)?; check_self_owned(ctx.accounts.sub_account, SwigError::OwnerMismatchSubAccount)?; let sign_v1 = SubAccountSignV1::from_instruction_bytes(data)?; let sub_account_data = unsafe { ctx.accounts.sub_account.borrow_data_unchecked() }; @@ -187,14 +189,14 @@ pub fn sub_account_sign_v1( const UNINIT_KEY: MaybeUninit<&Pubkey> = MaybeUninit::uninit(); let mut restricted_keys: [MaybeUninit<&Pubkey>; 2] = [UNINIT_KEY; 2]; let rkeys: &[&Pubkey] = unsafe { - if role.position.authority_type()? == AuthorityType::Ed25519 { + if role.position.authority_type()? == AuthorityType::Secp256k1 { + restricted_keys[0].write(ctx.accounts.payer.key()); + core::slice::from_raw_parts(restricted_keys.as_ptr() as _, 1) + } else { let authority_index = *sign_v1.authority_payload.get_unchecked(0) as usize; restricted_keys[0].write(ctx.accounts.payer.key()); restricted_keys[1].write(all_accounts[authority_index].key()); core::slice::from_raw_parts(restricted_keys.as_ptr() as _, 2) - } else { - restricted_keys[0].write(ctx.accounts.payer.key()); - core::slice::from_raw_parts(restricted_keys.as_ptr() as _, 1) } }; let ix_iter = InstructionIterator::new( diff --git a/program/src/actions/toggle_sub_account_v1.rs b/program/src/actions/toggle_sub_account_v1.rs index 8fcd9f1a..048426f6 100644 --- a/program/src/actions/toggle_sub_account_v1.rs +++ b/program/src/actions/toggle_sub_account_v1.rs @@ -11,7 +11,7 @@ use pinocchio::{ }; use swig_assertions::*; use swig_state_x::{ - action::{all::All, manage_authority::ManageAuthority, ActionLoader, Actionable}, + action::{all::All, sub_account::SubAccount, ActionLoader, Actionable}, authority::AuthorityType, role::RoleMut, swig::{Swig, SwigSubAccount}, @@ -173,10 +173,11 @@ pub fn toggle_sub_account_v1( } // Check if the role has the required permissions - let manage_authority_action = role.get_action::(&[])?; let all_action = role.get_action::(&[])?; + let sub_account_action = + role.get_action::(ctx.accounts.sub_account.key().as_ref())?; - if manage_authority_action.is_none() && all_action.is_none() { + if all_action.is_none() && sub_account_action.is_none() { return Err(SwigAuthenticateError::PermissionDeniedMissingPermission.into()); } diff --git a/program/src/actions/withdraw_from_sub_account_v1.rs b/program/src/actions/withdraw_from_sub_account_v1.rs index 5d2e59a3..5da2463a 100644 --- a/program/src/actions/withdraw_from_sub_account_v1.rs +++ b/program/src/actions/withdraw_from_sub_account_v1.rs @@ -18,7 +18,7 @@ use pinocchio::{ use pinocchio_token::instructions::Transfer; use swig_assertions::*; use swig_state_x::{ - action::{all::All, manage_authority::ManageAuthority, sub_account::SubAccount}, + action::{all::All, sub_account::SubAccount}, authority::AuthorityType, role::RoleMut, swig::{sub_account_signer, Swig, SwigSubAccount}, @@ -111,11 +111,19 @@ pub fn withdraw_from_sub_account_v1( data: &[u8], account_classifiers: &[AccountClassification], ) -> ProgramResult { + // Verify that both the swig account and sub_account are owned by the current + // program + check_self_owned(ctx.accounts.swig, SwigError::OwnerMismatchSwigAccount)?; check_self_owned(ctx.accounts.sub_account, SwigError::OwnerMismatchSubAccount)?; let withdraw = WithdrawFromSubAccountV1::from_instruction_bytes(data)?; let swig_account_data = unsafe { ctx.accounts.swig.borrow_mut_data_unchecked() }; let (swig_header, swig_roles) = unsafe { swig_account_data.split_at_mut_unchecked(Swig::LEN) }; let swig = unsafe { Swig::load_unchecked(&swig_header)? }; + + // Verify the swig account has the correct discriminator + if unsafe { *swig_header.get_unchecked(0) } != Discriminator::SwigAccount as u8 { + return Err(SwigError::InvalidSwigAccountDiscriminator.into()); + } let sub_account_data = unsafe { ctx.accounts.sub_account.borrow_data_unchecked() }; if unsafe { *sub_account_data.get_unchecked(0) } != Discriminator::SwigSubAccount as u8 { return Err(SwigError::InvalidSwigSubAccountDiscriminator.into()); @@ -150,17 +158,21 @@ pub fn withdraw_from_sub_account_v1( slot, )?; } - let (action_accounts_index, action_accounts_len) = - if role.position.authority_type()? == AuthorityType::Ed25519 { - (4, 7) - } else { - (3, 6) - }; - let manage_authority_action = role.get_action::(&[])?; + // Check if the role has the required permissions let all_action = role.get_action::(&[])?; - if manage_authority_action.is_none() && all_action.is_none() { + let sub_account_action = + role.get_action::(ctx.accounts.sub_account.key().as_ref())?; + + if all_action.is_none() && sub_account_action.is_none() { return Err(SwigAuthenticateError::PermissionDeniedMissingPermission.into()); } + + let (action_accounts_index, action_accounts_len) = + if role.position.authority_type()? == AuthorityType::Secp256k1 { + (3, 6) + } else { + (4, 7) + }; let amount = withdraw.args.amount; if all_accounts.len() >= action_accounts_len { let token_account = &all_accounts[action_accounts_index]; diff --git a/program/src/error.rs b/program/src/error.rs index afe4aff8..c2562fd1 100644 --- a/program/src/error.rs +++ b/program/src/error.rs @@ -105,6 +105,17 @@ pub enum SwigError { InvalidSwigTokenAccountOwner, /// Invalid program scope balance field configuration InvalidProgramScopeBalanceFields, + /// Account data was modified in unexpected ways during instruction + /// execution + AccountDataModifiedUnexpectedly, + /// Oracle account required for token limit check was not found + PermissionDeniedMissingOracleAccount, + /// Failed to read oracle price data + InvalidOraclePriceData, + /// Failed to verify the oracle level to FULL + OracleVerficationLevelFailed, + /// Failed to fetch latest oracle price + OraclePriceTooOld, } /// Implements conversion from SwigError to ProgramError. diff --git a/program/src/instruction.rs b/program/src/instruction.rs index c39bcf43..2b9e9542 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -28,7 +28,7 @@ pub enum SwigInstruction { /// 3. `[writable]` System program account #[account(0, writable, name="swig", desc="the swig smart wallet")] #[account(1, writable, signer, name="payer", desc="the payer")] - #[account(2, writable, name="system_program", desc="the system program")] + #[account(2, name="system_program", desc="the system program")] #[num_enum(default)] CreateV1 = 0, diff --git a/program/src/lib.rs b/program/src/lib.rs index 8f1f60d4..f339c9e6 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -9,7 +9,7 @@ pub mod actions; mod error; pub mod instruction; -mod util; +pub mod util; use core::mem::MaybeUninit; use actions::process_action; @@ -169,7 +169,8 @@ unsafe fn execute( /// /// This function determines the type and role of an account in the Swig wallet /// system. It handles several special cases: -/// - Swig accounts (must be first in the account list) +/// - Swig accounts (the first one must be at index 0 for signing/permission +/// checking) /// - Stake accounts (with validation of withdrawer authority) /// - Token accounts (SPL Token and Token-2022) /// - Program-scoped accounts (using the program scope cache) @@ -207,7 +208,19 @@ unsafe fn classify_account( lamports: account.lamports(), }), Discriminator::SwigAccount if index != 0 => { - return Err(SwigError::InvalidAccountsSwigMustBeFirst.into()); + // Additional Swig accounts are only allowed if the first account is also a Swig + // account + let first_account = accounts.get_unchecked(0).assume_init_ref(); + let first_data = first_account.borrow_data_unchecked(); + + if first_account.owner() == &crate::ID + && first_data.len() >= 8 + && *first_data.get_unchecked(0) == Discriminator::SwigAccount as u8 + { + Ok(AccountClassification::None) + } else { + Err(SwigError::InvalidAccountsSwigMustBeFirst.into()) + } }, _ => Ok(AccountClassification::None), } diff --git a/program/src/util/mod.rs b/program/src/util/mod.rs index fda032d5..a016c6b7 100644 --- a/program/src/util/mod.rs +++ b/program/src/util/mod.rs @@ -4,6 +4,7 @@ //! - Program scope caching and lookup //! - Account balance reading //! - Token transfer operations +//! - Oracle Program for token price fetch //! The utilities are optimized for performance and safety. use std::mem::MaybeUninit; @@ -12,8 +13,10 @@ use pinocchio::{ account_info::AccountInfo, cpi::invoke_signed, instruction::{AccountMeta, Instruction, Signer}, + msg, program_error::ProgramError, pubkey::Pubkey, + syscalls::sol_sha256, ProgramResult, }; use swig_state_x::{ @@ -21,8 +24,10 @@ use swig_state_x::{ program_scope::{NumericType, ProgramScope}, Action, Permission, }, + authority::AuthorityType, constants::PROGRAM_SCOPE_BYTE_SIZE, read_numeric_field, + role::RoleMut, swig::{Swig, SwigWithRoles}, Transmutable, }; @@ -282,3 +287,174 @@ impl<'a> TokenTransfer<'a> { invoke_signed(&instruction, &[self.from, self.to, self.authority], signers) } } + +pub fn get_price_data_from_bytes( + price_update_data: &[u8], + current_timestamp: i64, + maximum_age: u64, + feed_id: &[u8], +) -> Result<(u64, u64, i32), SwigError> { + let verification_level = unsafe { *price_update_data.get_unchecked(40) }; + if verification_level != 1 { + return Err(SwigError::OracleVerficationLevelFailed); + } + + // Feed id + if unsafe { price_update_data.get_unchecked(41..73) } != feed_id { + return Err(SwigError::InvalidOraclePriceData); + } + + // price (8 bytes) [73..81] + let price = i64::from_le_bytes(unsafe { + price_update_data + .get_unchecked(73..81) + .try_into() + .map_err(|_| SwigError::InvalidOraclePriceData)? + }); + + // conf (8 bytes) [81..89] + let confidence = u64::from_le_bytes(unsafe { + price_update_data + .get_unchecked(81..89) + .try_into() + .map_err(|_| SwigError::InvalidOraclePriceData)? + }); + + // exponent (4 bytes) [89..93] + let exponent = i32::from_le_bytes(unsafe { + price_update_data + .get_unchecked(89..93) + .try_into() + .map_err(|_| SwigError::InvalidOraclePriceData)? + }); + + // publish_time (8 bytes) [93..101] + let publish_time = i64::from_le_bytes(unsafe { + price_update_data + .get_unchecked(93..101) + .try_into() + .map_err(|_| SwigError::InvalidOraclePriceData)? + }); + + if publish_time.saturating_add(maximum_age.try_into().unwrap()) < current_timestamp { + return Err(SwigError::OraclePriceTooOld); + } + + Ok((price as u64, confidence, exponent)) +} + +/// Builds a restricted keys array for transaction signing. +/// +/// This function creates an array of public keys that are restricted from being +/// used as signers in the transaction. The behavior differs based on the +/// authority type: +/// - For Secp256k1 and Secp256r1: Only includes the payer key +/// - For other authority types: Includes both the payer key and the authority +/// key +/// +/// # Arguments +/// * `role` - The role containing the authority type information +/// * `payer_key` - The payer account's public key +/// * `authority_payload` - The authority payload containing the authority index +/// * `all_accounts` - All accounts involved in the transaction +/// +/// # Returns +/// * `Result<&[&Pubkey], ProgramError>` - A slice of restricted public keys +/// +/// # Safety +/// This function uses unsafe operations for performance. The caller must +/// ensure: +/// - `authority_payload` has at least one byte when authority type is not +/// Secp256k1/r1 +/// - `all_accounts` contains the account at the specified authority index +#[inline(always)] +pub unsafe fn build_restricted_keys<'a>( + role: &RoleMut, + payer_key: &'a Pubkey, + authority_payload: &[u8], + all_accounts: &'a [AccountInfo], + restricted_keys_storage: &'a mut [MaybeUninit<&'a Pubkey>; 2], +) -> Result<&'a [&'a Pubkey], ProgramError> { + if role.position.authority_type()? == AuthorityType::Secp256k1 + || role.position.authority_type()? == AuthorityType::Secp256r1 + { + restricted_keys_storage[0].write(payer_key); + Ok(core::slice::from_raw_parts( + restricted_keys_storage.as_ptr() as _, + 1, + )) + } else { + let authority_index = *authority_payload.get_unchecked(0) as usize; + restricted_keys_storage[0].write(payer_key); + restricted_keys_storage[1].write(all_accounts[authority_index].key()); + Ok(core::slice::from_raw_parts( + restricted_keys_storage.as_ptr() as _, + 2, + )) + } +} + +/// Computes a hash of data while excluding specified byte ranges. +/// +/// This function uses the SHA256 hash algorithm which is optimized +/// for low compute units on Solana. It hashes all bytes in the +/// account's data except those in the specified exclusion ranges. +/// +/// # Arguments +/// * `data` - The data to hash +/// * `exclude_ranges` - Sorted list of byte ranges to exclude from hashing +/// +/// # Returns +/// * `[u8; 32]` - The computed SHA256 hash (32 bytes) +/// +/// # Safety +/// This function assumes that: +/// - The exclude_ranges are non-overlapping and sorted by start position +/// - All ranges are within the bounds of the data +#[inline(always)] +pub fn hash_except(data: &[u8], exclude_ranges: &[core::ops::Range]) -> [u8; 32] { + // Maximum possible segments: one before each exclude range + one after all ranges + const MAX_SEGMENTS: usize = 16; // Reasonable upper bound, however most cases are <= 3 + let mut segments: [&[u8]; MAX_SEGMENTS] = [&[]; MAX_SEGMENTS]; + let mut segment_count = 0; + let mut position = 0; + + // If no exclude ranges, hash the entire data + #[allow(unused)] + if exclude_ranges.is_empty() { + segments[0] = data; + segment_count = 1; + } else { + for range in exclude_ranges { + // Add bytes before this exclusion range + if position < range.start { + segments[segment_count] = &data[position..range.start]; + segment_count += 1; + } + // Skip to end of exclusion range + position = range.end; + } + + // Add any remaining bytes after the last exclusion range + if position < data.len() { + segments[segment_count] = &data[position..]; + segment_count += 1; + } + } + + let mut data_payload_hash = [0u8; 32]; + + #[cfg(target_os = "solana")] + unsafe { + let res = sol_sha256( + segments.as_ptr() as *const u8, + segment_count as u64, + data_payload_hash.as_mut_ptr() as *mut u8, + ); + } + + #[cfg(not(target_os = "solana"))] + let res = 0; + + data_payload_hash +} diff --git a/program/tests/common/mod.rs b/program/tests/common/mod.rs index b4041836..bd1109ec 100644 --- a/program/tests/common/mod.rs +++ b/program/tests/common/mod.rs @@ -1,11 +1,12 @@ use alloy_signer_local::{LocalSigner, PrivateKeySigner}; -use anyhow::Result; +use anyhow::{Ok, Result}; use litesvm::{types::TransactionMetadata, LiteSVM}; use litesvm_token::{spl_token, CreateAssociatedTokenAccount, CreateMint, MintTo}; use solana_sdk::{ compute_budget::ComputeBudgetInstruction, instruction::Instruction, message::{v0, VersionedMessage}, + program_pack::Pack, pubkey::Pubkey, signature::Keypair, signer::Signer, @@ -20,7 +21,7 @@ use swig_state_x::{ action::{all::All, manage_authority::ManageAuthority, sub_account::SubAccount}, authority::{ ed25519::CreateEd25519SessionAuthority, secp256k1::CreateSecp256k1SessionAuthority, - AuthorityType, + secp256r1::CreateSecp256r1SessionAuthority, AuthorityType, }, swig::{sub_account_seeds, swig_account_seeds, SwigWithRoles}, IntoBytes, Transmutable, @@ -283,6 +284,60 @@ pub fn create_swig_secp256k1_session( Ok((swig, bench)) } +pub fn create_swig_secp256r1_session( + context: &mut SwigTestContext, + public_key: &[u8; 33], + id: [u8; 32], + session_max_length: u64, + initial_session_key: [u8; 32], +) -> anyhow::Result<(Pubkey, TransactionMetadata)> { + use swig_state_x::authority::secp256r1::CreateSecp256r1SessionAuthority; + + let payer_pubkey = context.default_payer.pubkey(); + let (swig, bump) = Pubkey::find_program_address(&swig_account_seeds(&id), &program_id()); + + // Create the session authority data + let authority_data = + CreateSecp256r1SessionAuthority::new(*public_key, initial_session_key, session_max_length); + + let initial_authority = AuthorityConfig { + authority_type: AuthorityType::Secp256r1Session, + authority: authority_data + .into_bytes() + .map_err(|e| anyhow::anyhow!("Failed to serialize authority data {:?}", e))?, + }; + + let create_ix = CreateInstruction::new( + swig, + bump, + payer_pubkey, + initial_authority, + vec![ClientAction::All(All {})], + id, + )?; + + let msg = v0::Message::try_compile( + &payer_pubkey, + &[create_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx = VersionedTransaction::try_new( + VersionedMessage::V0(msg), + &[context.default_payer.insecure_clone()], + ) + .unwrap(); + + let bench = context + .svm + .send_transaction(tx) + .map_err(|e| anyhow::anyhow!("Failed to send transaction {:?}", e))?; + + Ok((swig, bench)) +} + pub struct SwigTestContext { pub svm: LiteSVM, pub default_payer: Keypair, @@ -306,6 +361,28 @@ pub fn load_program(svm: &mut LiteSVM) -> anyhow::Result<()> { .map_err(|_| anyhow::anyhow!("Failed to load program")) } +pub fn load_sample_pyth_accounts(svm: &mut LiteSVM) -> anyhow::Result<()> { + use base64; + use solana_program::pubkey::Pubkey; + use solana_sdk::account::Account; + use std::str::FromStr; + + let pubkey = Pubkey::from_str("7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE").unwrap(); + let owner = Pubkey::from_str("rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ").unwrap(); + + let mut data = Account { + lamports: 1825020, + data: base64::decode("IvEjY51+9M1gMUcENA3t3zcf1CRyFI8kjp0abRpesqw6zYt/1dayQwHvDYtv2izrpB2hXUCV0do5Kg0vjtDGx7wPTPrIwoC1bbLod+YDAAAA6QJ4AAAAAAD4////lZpJaAAAAACVmkloAAAAAMC2EeIDAAAALL2AAAAAAADcSaEUAAAAAAA=").unwrap(), + owner, + executable: false, + rent_epoch: 18446744073709551615, + }; + + svm.set_account(pubkey, data); + + Ok(()) +} + pub fn setup_mint(svm: &mut LiteSVM, payer: &Keypair) -> anyhow::Result { let mint = CreateMint::new(svm, payer) .decimals(9) @@ -315,6 +392,51 @@ pub fn setup_mint(svm: &mut LiteSVM, payer: &Keypair) -> anyhow::Result Ok(mint) } +pub fn setup_oracle_mint(context: &mut SwigTestContext) -> anyhow::Result { + load_sample_pyth_accounts(&mut context.svm).unwrap(); + + // Setup token accounts + let mint_key_bytes = [ + 193, 17, 76, 51, 120, 6, 8, 131, 149, 6, 187, 31, 102, 121, 14, 198, 202, 133, 249, 221, + 22, 60, 55, 46, 12, 43, 226, 195, 167, 208, 193, 78, 247, 169, 151, 255, 215, 241, 92, 175, + 239, 134, 208, 37, 97, 234, 209, 161, 53, 165, 40, 34, 193, 65, 166, 81, 164, 72, 62, 60, + 149, 224, 228, 83, + ]; + let mint_kp = Keypair::from_bytes(&mint_key_bytes).unwrap(); + let mint_pubkey = mint_kp.pubkey(); + use solana_program::system_instruction::create_account; + use solana_sdk::transaction::Transaction; + use spl_token::instruction::initialize_mint2; + use spl_token::state::Mint; + + let mint_size = Mint::LEN; + + let ix1 = create_account( + &context.default_payer.pubkey(), + &mint_kp.pubkey(), + context.svm.minimum_balance_for_rent_exemption(mint_size), + mint_size as u64, + &spl_token::ID, + ); + let ix2 = initialize_mint2( + &spl_token::ID, + &mint_kp.pubkey(), + &context.default_payer.pubkey(), + None, + 9, + ) + .unwrap(); + let block_hash = context.svm.latest_blockhash(); + let tx = Transaction::new_signed_with_payer( + &[ix1, ix2], + Some(&context.default_payer.pubkey()), + &[&context.default_payer, &mint_kp], + block_hash, + ); + let tx_sig = context.svm.send_transaction(tx).unwrap(); + Ok(mint_pubkey) +} + pub fn mint_to( svm: &mut LiteSVM, mint: &Pubkey, @@ -619,3 +741,172 @@ pub fn add_sub_account_permission( Ok(bench) } + +use alloy_primitives::hex; +use solana_sdk::account::Account; +use swig_state_x::action::{oracle_limits::OracleTokenLimit, sol_limit::SolLimit}; +use swig_state_x::action::{program_scope::ProgramScope, sol_recurring_limit::SolRecurringLimit}; +use swig_state_x::role::Role; + +pub fn display_swig(swig_pubkey: Pubkey, swig_account: &Account) -> Result<(), anyhow::Error> { + let swig_with_roles = SwigWithRoles::from_bytes(&swig_account.data).unwrap(); + + println!("╔══════════════════════════════════════════════════════════════════"); + println!("║ SWIG WALLET DETAILS"); + println!("╠══════════════════════════════════════════════════════════════════"); + println!("║ Account Address: {}", swig_pubkey); + println!("║ Total Roles: {}", swig_with_roles.state.role_counter); + println!( + "║ Balance: {} SOL", + swig_account.lamports as f64 / 1_000_000_000.0 + ); + + println!("╠══════════════════════════════════════════════════════════════════"); + println!("║ ROLES & PERMISSIONS"); + println!("╠══════════════════════════════════════════════════════════════════"); + + for i in 0..swig_with_roles.state.role_counter { + let role = swig_with_roles.get_role(i).unwrap(); + + if let Some(role) = role { + println!("║"); + println!("║ Role ID: {}", i); + println!( + "║ ├─ Type: {}", + if role.authority.session_based() { + "Session-based Authority" + } else { + "Permanent Authority" + } + ); + println!("║ ├─ Authority Type: {:?}", role.authority.authority_type()); + println!( + "║ ├─ Authority: {}", + match role.authority.authority_type() { + AuthorityType::Ed25519 | AuthorityType::Ed25519Session => { + let authority = role.authority.identity().unwrap(); + let authority = bs58::encode(authority).into_string(); + authority + }, + AuthorityType::Secp256k1 | AuthorityType::Secp256k1Session => { + let authority = role.authority.identity().unwrap(); + let authority_hex = hex::encode([&[0x4].as_slice(), authority].concat()); + // get eth address from public key + let mut hasher = solana_sdk::keccak::Hasher::default(); + hasher.hash(authority_hex.as_bytes()); + let hash = hasher.result(); + let address = format!("0x{}", hex::encode(&hash.0[12..32])); + address + }, + _ => todo!(), + } + ); + + println!("║ ├─ Permissions:"); + + let actions = role.get_all_actions().unwrap(); + println!("║ │ ├─ Actions length: {}", actions.len()); + for action in actions { + println!("║ │ ├─ Action: {:?}", action); + } + + // Check All permission + if (Role::get_action::(&role, &[]).unwrap()).is_some() { + println!("║ │ ├─ Full Access (All Permissions)"); + } + + // Check Manage Authority permission + if (Role::get_action::(&role, &[]).unwrap()).is_some() { + println!("║ │ ├─ Manage Authority"); + } + + // Check Sol Limit + if let Some(action) = Role::get_action::(&role, &[]).unwrap() { + println!( + "║ │ ├─ SOL Limit: {} SOL", + action.amount as f64 / 1_000_000_000.0 + ); + } + + // Check Sol Recurring Limit + if let Some(action) = Role::get_action::(&role, &[]).unwrap() { + println!("║ │ ├─ Recurring SOL Limit:"); + println!( + "║ │ │ ├─ Amount: {} SOL", + action.recurring_amount as f64 / 1_000_000_000.0 + ); + println!("║ │ │ ├─ Window: {} slots", action.window); + println!( + "║ │ │ ├─ Current Usage: {} SOL", + action.current_amount as f64 / 1_000_000_000.0 + ); + println!("║ │ │ └─ Last Reset: Slot {}", action.last_reset); + } + + // Check Program Scope + if let Some(action) = + Role::get_action::(&role, &spl_token::ID.to_bytes()).unwrap() + { + let program_id = Pubkey::from(action.program_id); + let target_account = Pubkey::from(action.target_account); + println!("║ │ ├─ Program Scope"); + println!("║ │ │ ├─ Program ID: {}", program_id); + println!("║ │ │ ├─ Target Account: {}", target_account); + println!( + "║ │ │ ├─ Scope Type: {}", + match action.scope_type { + 0 => "Basic", + 1 => "Limit", + 2 => "Recurring Limit", + _ => "Unknown", + } + ); + println!( + "║ │ │ ├─ Numeric Type: {}", + match action.numeric_type { + 0 => "U64", + 1 => "U128", + 2 => "F64", + _ => "Unknown", + } + ); + if action.scope_type > 0 { + println!("║ │ │ ├─ Limit: {} ", action.limit); + println!("║ │ │ ├─ Current Usage: {} ", action.current_amount); + } + if action.scope_type == 2 { + println!("║ │ │ ├─ Window: {} slots", action.window); + println!("║ │ │ ├─ Last Reset: Slot {}", action.last_reset); + } + println!("║ │ │ "); + } + + // Oracle limits + if let Some(action) = Role::get_action::(&role, &[0u8]).unwrap() { + println!("║ │ ├─ Oracle Token Limit:"); + println!( + "║ │ │ ├─ Base Asset: {}", + match action.base_asset_type { + 0 => "USDC", + 1 => "EURC", + _ => "Unknown", + } + ); + println!( + "║ │ │ ├─ Value Limit: {} {}", + action.value_limit as f64 / 1_000_000.0, // Divide by 10^6 since USDC/EURC have 6 decimals + match action.base_asset_type { + 0 => "USDC", + 1 => "EURC", + _ => "Unknown", + } + ); + } + println!("║ │ "); + } + } + + println!("╚══════════════════════════════════════════════════════════════════"); + + Ok(()) +} diff --git a/program/tests/create_session_test.rs b/program/tests/create_session_test.rs index 1e54366f..97d8be05 100644 --- a/program/tests/create_session_test.rs +++ b/program/tests/create_session_test.rs @@ -518,6 +518,7 @@ fn test_secp256k1_session() { assert_eq!(auth.public_key, compressed_eth_pubkey.as_ref()); assert_eq!(auth.current_session_expiration, 0); assert_eq!(auth.session_key, [0; 32]); + assert_eq!(auth.signature_odometer, 0, "Initial odometer should be 0"); context .svm @@ -541,6 +542,7 @@ fn test_secp256k1_session() { context.default_payer.pubkey(), signing_fn, current_slot, + 1, // Counter for session authorities (starting from 1) 0, // Role ID 0 is the root authority session_key.pubkey(), session_duration, @@ -580,6 +582,10 @@ fn test_secp256k1_session() { current_slot + session_duration ); assert_eq!(auth.session_key, session_key.pubkey().to_bytes()); + assert_eq!( + auth.signature_odometer, 1, + "Odometer should be 1 after session creation" + ); // Create a receiver keypair let receiver = Keypair::new(); diff --git a/program/tests/oracle_limit_tests.rs b/program/tests/oracle_limit_tests.rs new file mode 100644 index 00000000..70ed9446 --- /dev/null +++ b/program/tests/oracle_limit_tests.rs @@ -0,0 +1,715 @@ +#![cfg(not(feature = "program_scope_test"))] +// This feature flag ensures these tests are only run when the +// "program_scope_test" feature is not enabled. This allows us to isolate +// and run only program_scope tests or only the regular tests. + +mod common; + +use std::str::FromStr; + +use common::*; +use litesvm::LiteSVM; +use litesvm_token::spl_token; +use solana_program::{pubkey::Pubkey, system_instruction}; +use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + message::{v0, VersionedMessage}, + signature::{Keypair, Signer}, + transaction::{Transaction, VersionedTransaction}, +}; +use swig_interface::{AuthorityConfig, ClientAction}; +use swig_state_x::{ + action::{ + all::All, + oracle_limits::{BaseAsset, OracleTokenLimit}, + sol_limit::SolLimit, + token_limit::TokenLimit, + Permission, + }, + authority::AuthorityType, + role::Role, + swig::SwigWithRoles, +}; + +/// Test 1: Verify oracle limit permission is added correctly +#[test_log::test] +fn test_oracle_limit_permission_add() { + let mut context = setup_test_context().unwrap(); + let swig_authority = Keypair::new(); + context + .svm + .airdrop(&swig_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Create a swig wallet + let id = rand::random::<[u8; 32]>(); + let oracle_program = Keypair::new(); + let (swig_key, _) = create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + // Create secondary authority + let secondary_authority = Keypair::new(); + context + .svm + .airdrop(&secondary_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Add multiple permissions: Oracle Token Limit (200 USDC) and SOL Limit (1 SOL) + let oracle_limit = OracleTokenLimit::new( + BaseAsset::USDC, + 200_000_000, // 200 USDC + false, + ); + + let sol_limit = SolLimit { + amount: 1_000_000_000, // 1 SOL + }; + + // Add authority with multiple permissions + add_authority_with_ed25519_root( + &mut context, + &swig_key, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::Ed25519, + authority: secondary_authority.pubkey().as_ref(), + }, + vec![ + ClientAction::OracleTokenLimit(oracle_limit), + ClientAction::SolLimit(sol_limit), + ], + ) + .unwrap(); + + // Verify permissions were added correctly + let swig_account = context.svm.get_account(&swig_key).unwrap(); + let swig = SwigWithRoles::from_bytes(&swig_account.data).unwrap(); + let role_id = swig + .lookup_role_id(secondary_authority.pubkey().as_ref()) + .unwrap() + .unwrap(); + let role = swig.get_role(role_id).unwrap().unwrap(); + + // Verify both permissions exist + assert_eq!(role.position.num_actions(), 2, "Should have 2 actions"); + + let oracle_action = role + .get_action::(&[BaseAsset::USDC as u8]) + .unwrap() + .unwrap(); + assert_eq!(oracle_action.value_limit, 200_000_000); + assert_eq!(oracle_action.base_asset_type, BaseAsset::USDC as u8); + + let sol_action = role.get_action::(&[]).unwrap().unwrap(); + assert_eq!(sol_action.amount, 1_000_000_000); +} + +/// Test 2: Test SOL transfers with oracle limits +#[test_log::test] +fn test_oracle_limit_sol_transfer() { + let mut context = setup_test_context().unwrap(); + load_sample_pyth_accounts(&mut context.svm); + let swig_authority = Keypair::new(); + context + .svm + .airdrop(&swig_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Create wallet and setup + let id = rand::random::<[u8; 32]>(); + let oracle_program = Keypair::new(); + let (swig_key, _) = create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + let secondary_authority = Keypair::new(); + context + .svm + .airdrop(&secondary_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Add oracle limit permission (200 USDC limit) + let oracle_limit = OracleTokenLimit::new( + BaseAsset::USDC, + 200_000_000, // 200 USDC limit + false, + ); + + add_authority_with_ed25519_root( + &mut context, + &swig_key, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::Ed25519, + authority: secondary_authority.pubkey().as_ref(), + }, + vec![ClientAction::OracleTokenLimit(oracle_limit)], + ) + .unwrap(); + + // Fund swig wallet + context.svm.airdrop(&swig_key, 20_000_000_000).unwrap(); + + // Test 1: Transfer below limit (1 SOL ≈ 150 USDC at mock price) + let transfer_ix = + system_instruction::transfer(&swig_key, &secondary_authority.pubkey(), 1_000_000_000); + let mut sign_ix = swig_interface::SignInstruction::new_ed25519( + swig_key, + secondary_authority.pubkey(), + secondary_authority.pubkey(), + transfer_ix, + 1, + ) + .unwrap(); + + sign_ix.accounts.extend(vec![AccountMeta::new_readonly( + Pubkey::from_str("7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE").unwrap(), + false, + )]); + + let message = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx = VersionedTransaction::try_new(VersionedMessage::V0(message), &[&secondary_authority]) + .unwrap(); + + let result = context.svm.send_transaction(tx); + assert!(result.is_ok(), "Transfer below limit should succeed"); + println!( + "Compute units consumed for below limit transfer: {}", + result.unwrap().compute_units_consumed + ); + + // Test 2: Transfer above limit (2 SOL ≈ 300 USDC at mock price) + let transfer_ix = + system_instruction::transfer(&swig_key, &secondary_authority.pubkey(), 2_000_000_000); + let mut sign_ix = swig_interface::SignInstruction::new_ed25519( + swig_key, + secondary_authority.pubkey(), + secondary_authority.pubkey(), + transfer_ix, + 1, + ) + .unwrap(); + + sign_ix.accounts.extend(vec![AccountMeta::new_readonly( + Pubkey::from_str("7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE").unwrap(), + false, + )]); + + let message = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx = VersionedTransaction::try_new(VersionedMessage::V0(message), &[&secondary_authority]) + .unwrap(); + + let result = context.svm.send_transaction(tx); + assert!(result.is_err(), "Transfer above limit should fail"); + assert_eq!( + result.unwrap_err().err, + solana_sdk::transaction::TransactionError::InstructionError( + 0, + solana_sdk::instruction::InstructionError::Custom(3027) + ), + "Expected error code 3027" + ); +} + +/// Test 3: Test token transfers with oracle limits +#[test_log::test] +fn test_oracle_limit_token_transfer() { + let mut context = setup_test_context().unwrap(); + let swig_authority = Keypair::new(); + context + .svm + .airdrop(&swig_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Create wallet and setup + let id = rand::random::<[u8; 32]>(); + let oracle_program = Keypair::new(); + let (swig_key, _) = create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + let secondary_authority = Keypair::new(); + context + .svm + .airdrop(&secondary_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Add oracle limit permission (300 USDC limit) + let oracle_limit = OracleTokenLimit::new( + BaseAsset::USDC, + 300_000_000, // 300 USDC with 6 decimals + false, + ); + + add_authority_with_ed25519_root( + &mut context, + &swig_key, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::Ed25519, + authority: secondary_authority.pubkey().as_ref(), + }, + vec![ClientAction::OracleTokenLimit(oracle_limit)], + ) + .unwrap(); + + let mint_pubkey = setup_oracle_mint(&mut context).unwrap(); + let swig_ata = setup_ata( + &mut context.svm, + &mint_pubkey, + &swig_key, + &context.default_payer, + ) + .unwrap(); + let recipient_ata = setup_ata( + &mut context.svm, + &mint_pubkey, + &secondary_authority.pubkey(), + &context.default_payer, + ) + .unwrap(); + + // Fund swig's token account with 10 tokens + mint_to( + &mut context.svm, + &mint_pubkey, + &context.default_payer, + &swig_ata, + 100_000_000_000, // 10 tokens with 9 decimals + ) + .unwrap(); + + // Test 1: Transfer below limit (0.5 tokens ≈ 0.75 USDC at mock price of 1.5 USDC per token) + let transfer_ix = spl_token::instruction::transfer( + &spl_token::id(), + &swig_ata, + &recipient_ata, + &swig_key, + &[], + 500_000_000, // 0.5 tokens with 9 decimals + ) + .unwrap(); + + let mut sign_ix = swig_interface::SignInstruction::new_ed25519( + swig_key, + secondary_authority.pubkey(), + secondary_authority.pubkey(), + transfer_ix, + 1, + ) + .unwrap(); + + sign_ix.accounts.extend(vec![AccountMeta::new_readonly( + Pubkey::from_str("7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE").unwrap(), + false, + )]); + + let message = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx = VersionedTransaction::try_new(VersionedMessage::V0(message), &[&secondary_authority]) + .unwrap(); + + let result = context.svm.send_transaction(tx); + assert!(result.is_ok(), "Transfer below limit should succeed"); + println!( + "Compute units consumed for below limit transfer: {}", + result.unwrap().compute_units_consumed + ); + + // Test 2: Transfer above limit (2.5 tokens ≈ 3.75 USDC at mock price) + let transfer_ix = spl_token::instruction::transfer( + &spl_token::id(), + &swig_ata, + &recipient_ata, + &swig_key, + &[], + 2_500_000_000, // 2.5 tokens with 9 decimals + ) + .unwrap(); + + let mut sign_ix = swig_interface::SignInstruction::new_ed25519( + swig_key, + secondary_authority.pubkey(), + secondary_authority.pubkey(), + transfer_ix, + 1, + ) + .unwrap(); + + sign_ix.accounts.extend(vec![AccountMeta::new_readonly( + Pubkey::from_str("7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE").unwrap(), + false, + )]); + + let message = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx = VersionedTransaction::try_new(VersionedMessage::V0(message), &[&secondary_authority]) + .unwrap(); + + let result = context.svm.send_transaction(tx); + assert!(result.is_err(), "Transfer above limit should fail"); + assert_eq!( + result.unwrap_err().err, + solana_sdk::transaction::TransactionError::InstructionError( + 0, + solana_sdk::instruction::InstructionError::Custom(3027) + ), + "Expected error code 3027" + ); +} + +/// Test 4: Test SOL transfers with oracle limits and passthrough enabled +#[test_log::test] +fn test_oracle_limit_sol_passthrough() { + let mut context = setup_test_context().unwrap(); + load_sample_pyth_accounts(&mut context.svm); + + let swig_authority = Keypair::new(); + context + .svm + .airdrop(&swig_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Create wallet and setup + let id = rand::random::<[u8; 32]>(); + let oracle_program = Keypair::new(); + let (swig_key, _) = create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + let secondary_authority = Keypair::new(); + context + .svm + .airdrop(&secondary_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Add oracle limit permission (200 USDC limit) with passthrough enabled + let oracle_limit = OracleTokenLimit::new( + BaseAsset::USDC, + 200_000_000, // 200 USDC limit + true, + ); + + add_authority_with_ed25519_root( + &mut context, + &swig_key, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::Ed25519, + authority: secondary_authority.pubkey().as_ref(), + }, + vec![ + ClientAction::OracleTokenLimit(oracle_limit), + ClientAction::SolLimit(SolLimit { + amount: 100_000_000_000, + }), + ], + ) + .unwrap(); + + // Fund swig wallet + context.svm.airdrop(&swig_key, 20_000_000_000).unwrap(); + + // Test 1: Transfer below limit (1 SOL ≈ 150 USDC at mock price) + let transfer_ix = + system_instruction::transfer(&swig_key, &secondary_authority.pubkey(), 1_000_000_000); + let mut sign_ix = swig_interface::SignInstruction::new_ed25519( + swig_key, + secondary_authority.pubkey(), + secondary_authority.pubkey(), + transfer_ix, + 1, + ) + .unwrap(); + + sign_ix.accounts.extend(vec![AccountMeta::new_readonly( + Pubkey::from_str("7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE").unwrap(), + false, + )]); + + let message = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx = VersionedTransaction::try_new(VersionedMessage::V0(message), &[&secondary_authority]) + .unwrap(); + + let result = context.svm.send_transaction(tx); + assert!(result.is_ok(), "Transfer below limit should succeed"); + println!( + "Compute units consumed for below limit transfer: {}", + result.unwrap().compute_units_consumed + ); + + // Test 2: Transfer above limit (2 SOL ≈ 300 USDC at mock price) + let transfer_ix = + system_instruction::transfer(&swig_key, &secondary_authority.pubkey(), 2_000_000_000); + let mut sign_ix = swig_interface::SignInstruction::new_ed25519( + swig_key, + secondary_authority.pubkey(), + secondary_authority.pubkey(), + transfer_ix, + 1, + ) + .unwrap(); + + sign_ix.accounts.extend(vec![AccountMeta::new_readonly( + Pubkey::from_str("7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE").unwrap(), + false, + )]); + + let message = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx = VersionedTransaction::try_new(VersionedMessage::V0(message), &[&secondary_authority]) + .unwrap(); + + let result = context.svm.send_transaction(tx); + assert!(result.is_err(), "Transfer above limit should fail"); + assert_eq!( + result.unwrap_err().err, + solana_sdk::transaction::TransactionError::InstructionError( + 0, + solana_sdk::instruction::InstructionError::Custom(3027) + ), + "Expected error code 3027" + ); +} + +/// Test 5: Test token transfers with oracle limits and passthrough enabled +#[test_log::test] +fn test_oracle_limit_passthrough() { + let mut context = setup_test_context().unwrap(); + let swig_authority = Keypair::new(); + context + .svm + .airdrop(&swig_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Create wallet and setup + let id = rand::random::<[u8; 32]>(); + let (swig_key, _) = create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + let secondary_authority = Keypair::new(); + context + .svm + .airdrop(&secondary_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Add oracle limit permission (300 USDC limit) with passthrough enabled + let oracle_limit = OracleTokenLimit::new( + BaseAsset::USDC, + 300_000_000, // 300 USDC with 6 decimals + true, + ); + + let mint_pubkey = setup_oracle_mint(&mut context).unwrap(); + + // Setup token accounts + let swig_ata = setup_ata( + &mut context.svm, + &mint_pubkey, + &swig_key, + &context.default_payer, + ) + .unwrap(); + let recipient_ata = setup_ata( + &mut context.svm, + &mint_pubkey, + &secondary_authority.pubkey(), + &context.default_payer, + ) + .unwrap(); + + let mint_bytes = mint_pubkey.to_bytes(); + + add_authority_with_ed25519_root( + &mut context, + &swig_key, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::Ed25519, + authority: secondary_authority.pubkey().as_ref(), + }, + vec![ + ClientAction::OracleTokenLimit(oracle_limit), + ClientAction::TokenLimit(TokenLimit { + token_mint: mint_bytes, + current_amount: 600_000_000, + }), + ], + ) + .unwrap(); + + // Fund swig's token account with 10 tokens + mint_to( + &mut context.svm, + &mint_pubkey, + &context.default_payer, + &swig_ata, + 10_000_000_000, // 10 tokens with 9 decimals + ) + .unwrap(); + + // Test 1: Transfer below limit (0.5 tokens ≈ 0.75 USDC at mock price of 1.5 USDC per token) + let transfer_ix = spl_token::instruction::transfer( + &spl_token::id(), + &swig_ata, + &recipient_ata, + &swig_key, + &[], + 500_000_000, // 0.5 tokens with 9 decimals + ) + .unwrap(); + + let mut sign_ix = swig_interface::SignInstruction::new_ed25519( + swig_key, + secondary_authority.pubkey(), + secondary_authority.pubkey(), + transfer_ix, + 1, + ) + .unwrap(); + + sign_ix.accounts.extend(vec![AccountMeta::new_readonly( + Pubkey::from_str("7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE").unwrap(), + false, + )]); + + let message = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx = VersionedTransaction::try_new(VersionedMessage::V0(message), &[&secondary_authority]) + .unwrap(); + + let result = context.svm.send_transaction(tx); + assert!(result.is_ok(), "Transfer below limit should succeed"); + println!( + "Compute units consumed for below limit transfer: {}", + result.unwrap().compute_units_consumed + ); + + // Test 2: Transfer above limit (2.5 tokens ≈ 3.75 USDC at mock price) + let transfer_ix = spl_token::instruction::transfer( + &spl_token::id(), + &swig_ata, + &recipient_ata, + &swig_key, + &[], + 2_500_000_000, // 2.5 tokens with 9 decimals + ) + .unwrap(); + + let mut sign_ix = swig_interface::SignInstruction::new_ed25519( + swig_key, + secondary_authority.pubkey(), + secondary_authority.pubkey(), + transfer_ix, + 1, + ) + .unwrap(); + + sign_ix.accounts.extend(vec![AccountMeta::new_readonly( + Pubkey::from_str("7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE").unwrap(), + false, + )]); + + let message = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx = VersionedTransaction::try_new(VersionedMessage::V0(message), &[&secondary_authority]) + .unwrap(); + + let result = context.svm.send_transaction(tx); + assert!(result.is_err(), "Transfer above limit should fail"); + assert_eq!( + result.unwrap_err().err, + solana_sdk::transaction::TransactionError::InstructionError( + 0, + solana_sdk::instruction::InstructionError::Custom(3027) + ), + "Expected error code 3027" + ); +} + +// fn create_alt_and_add(context: &mut SwigTestContext) -> Result { +// // Create the lookup table +// let (create_lookup_table_ix, lookup_table_address) = +// solana_sdk::address_lookup_table::instruction::create_lookup_table( +// context.default_payer.pubkey(), +// context.default_payer.pubkey(), +// 0, +// ); + +// let tx = Transaction::new_signed_with_payer( +// &[create_lookup_table_ix], +// Some(&context.default_payer.pubkey()), +// &[&context.default_payer], +// context.svm.latest_blockhash(), +// ); + +// context.svm.send_transaction(tx).unwrap(); + +// // Add addresses to the lookup table +// let addresses_to_add = vec![ +// Pubkey::from_str("7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE").unwrap(), +// Pubkey::from_str("AxaxyeDT8JnWERSaTKvFXvPKkEdxnamKSqpWbsSjYg1g").unwrap(), +// ]; + +// let extend_lookup_table_ix = solana_sdk::address_lookup_table::instruction::extend_lookup_table( +// lookup_table_address, +// context.default_payer.pubkey(), +// Some(context.default_payer.pubkey()), +// addresses_to_add, +// ); + +// let tx = Transaction::new_signed_with_payer( +// &[extend_lookup_table_ix], +// Some(&context.default_payer.pubkey()), +// &[&context.default_payer], +// context.svm.latest_blockhash(), +// ); + +// context.svm.send_transaction(tx).unwrap(); + +// println!("ALT address: {:?}", lookup_table_address); +// Ok(lookup_table_address) +// } diff --git a/program/tests/program_scope_test.rs b/program/tests/program_scope_test.rs index 1fadc99e..169a0bf7 100644 --- a/program/tests/program_scope_test.rs +++ b/program/tests/program_scope_test.rs @@ -222,7 +222,7 @@ fn test_token_transfer_with_program_scope() { "Account difference (swig - regular): {} accounts", account_difference ); - assert!(swig_transfer_cu - regular_transfer_cu <= 4500); + assert!(swig_transfer_cu - regular_transfer_cu <= 5106); } /// Helper function to perform token transfers through the swig diff --git a/program/tests/remove_authority_test.rs b/program/tests/remove_authority_test.rs index 2e9fe34d..865af454 100644 --- a/program/tests/remove_authority_test.rs +++ b/program/tests/remove_authority_test.rs @@ -235,6 +235,126 @@ fn test_create_remove_secp_authority() { assert!(result.is_err(), "Removing the last authority should fail"); } +#[test_log::test] +fn test_secp256k1_root_remove_authority() { + use alloy_primitives::B256; + use alloy_signer::SignerSync; + use alloy_signer_local::LocalSigner; + + let mut context = setup_test_context().unwrap(); + + // Create a secp256k1 root authority wallet + let root_wallet = LocalSigner::random(); + let id = rand::random::<[u8; 32]>(); + + // Create a swig wallet with secp256k1 root authority + let (swig_key, _) = create_swig_secp256k1(&mut context, &root_wallet, id).unwrap(); + context.svm.airdrop(&swig_key, 10_000_000_000).unwrap(); + + // Create a second Ed25519 authority to add and later remove + let second_authority = Keypair::new(); + context + .svm + .airdrop(&second_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Create signing function for secp256k1 authority (same pattern as working + // test) + let signing_fn = |payload: &[u8]| -> [u8; 65] { + let mut hash = [0u8; 32]; + hash.copy_from_slice(&payload[..32]); + let hash = B256::from(hash); + root_wallet.sign_hash_sync(&hash).unwrap().as_bytes() + }; + + // Add the second authority using secp256k1 root authority (same as working + // test) + let add_authority_ix = swig_interface::AddAuthorityInstruction::new_with_secp256k1_authority( + swig_key, + context.default_payer.pubkey(), + signing_fn, + 0, // current slot (same as working test) + 1, // counter = 1 (first transaction, same as working test) + 0, // role_id of the primary wallet + AuthorityConfig { + authority_type: AuthorityType::Ed25519, + authority: second_authority.pubkey().as_ref(), + }, + vec![ClientAction::ManageAuthority(ManageAuthority {})], + ) + .unwrap(); + + let message = v0::Message::try_compile( + &context.default_payer.pubkey(), + &[add_authority_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx = + VersionedTransaction::try_new(VersionedMessage::V0(message), &[&context.default_payer]) + .unwrap(); + + // Transaction should succeed + let result = context.svm.send_transaction(tx); + context.svm.expire_blockhash(); + context.svm.warp_to_slot(1); + assert!( + result.is_ok(), + "Failed to add ed25519 authority: {:?}", + result.err() + ); + + // Verify the authority was added + let swig_account = context.svm.get_account(&swig_key).unwrap(); + let swig_state = SwigWithRoles::from_bytes(&swig_account.data).unwrap(); + assert_eq!(swig_state.state.roles, 2); + + let remove_ix = RemoveAuthorityInstruction::new_with_secp256k1_authority( + swig_key, + context.default_payer.pubkey(), + signing_fn, + 1, // current slot + 2, // counter = 2 (second transaction), + 0, // role_id of the primary wallet (secp256k1 root authority) + 1, // Authority to remove (the Ed25519 authority) + ) + .unwrap(); + + let message = v0::Message::try_compile( + &context.default_payer.pubkey(), + &[remove_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx = + VersionedTransaction::try_new(VersionedMessage::V0(message), &[&context.default_payer]) + .unwrap(); + + // Transaction should succeed + let result = context.svm.send_transaction(tx); + assert!( + result.is_ok(), + "Failed to remove authority with secp256k1: {:?}", + result.err() + ); + + // Verify that only one authority remains (the secp256k1 root) + let swig_account = context.svm.get_account(&swig_key).unwrap(); + let swig_state = SwigWithRoles::from_bytes(&swig_account.data).unwrap(); + assert_eq!(swig_state.state.roles, 1); + + // Verify it's the secp256k1 root authority by checking the authority type + let role = swig_state.get_role(0).unwrap().unwrap(); + assert_eq!(role.authority.authority_type(), AuthorityType::Secp256k1); + + println!("✓ Secp256k1 root authority successfully removed Ed25519 authority"); + println!("✓ Signature counter functionality verified for secp256k1 remove authority operation"); +} + #[test_log::test] fn test_remove_authority_permissions() { let mut context = setup_test_context().unwrap(); diff --git a/program/tests/sign.rs b/program/tests/sign.rs index 9c99e3be..65b92085 100644 --- a/program/tests/sign.rs +++ b/program/tests/sign.rs @@ -107,7 +107,7 @@ fn test_transfer_sol_with_additional_authority() { assert!(false); } else { let txn = res.unwrap(); - println!("logs {:?}", txn.logs); + println!("logs {}", txn.pretty_logs()); println!("Sign Transfer CU {:?}", txn.compute_units_consumed); } @@ -941,3 +941,100 @@ fn test_transfer_token_with_recurring_limit() { .unwrap(); assert_eq!(action.current, action.limit - amount3); } + +#[test_log::test] +fn test_transfer_between_swig_accounts() { + let mut context = setup_test_context().unwrap(); + + // Create first Swig account (sender) + let sender_authority = Keypair::new(); + context + .svm + .airdrop(&sender_authority.pubkey(), 10_000_000_000) + .unwrap(); + let sender_id = rand::random::<[u8; 32]>(); + let sender_swig = + Pubkey::find_program_address(&swig_account_seeds(&sender_id), &program_id()).0; + + // Create second Swig account (recipient) + let recipient_authority = Keypair::new(); + context + .svm + .airdrop(&recipient_authority.pubkey(), 10_000_000_000) + .unwrap(); + let recipient_id = rand::random::<[u8; 32]>(); + let recipient_swig = + Pubkey::find_program_address(&swig_account_seeds(&recipient_id), &program_id()).0; + + // Create both Swig accounts + let sender_create_result = create_swig_ed25519(&mut context, &sender_authority, sender_id); + assert!( + sender_create_result.is_ok(), + "Failed to create sender Swig account" + ); + + let recipient_create_result = + create_swig_ed25519(&mut context, &recipient_authority, recipient_id); + assert!( + recipient_create_result.is_ok(), + "Failed to create recipient Swig account" + ); + + // Fund the sender Swig account + context.svm.airdrop(&sender_swig, 5_000_000_000).unwrap(); + + // Create transfer instruction from sender Swig to recipient Swig + let transfer_amount = 1_000_000_000; // 1 SOL + let transfer_ix = system_instruction::transfer(&sender_swig, &recipient_swig, transfer_amount); + + // Sign the transfer with sender authority + let sign_ix = swig_interface::SignInstruction::new_ed25519( + sender_swig, + sender_authority.pubkey(), + sender_authority.pubkey(), + transfer_ix, + 0, // root authority role + ) + .unwrap(); + + let transfer_message = v0::Message::try_compile( + &sender_authority.pubkey(), + &[sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let transfer_tx = + VersionedTransaction::try_new(VersionedMessage::V0(transfer_message), &[&sender_authority]) + .unwrap(); + + let result = context.svm.send_transaction(transfer_tx); + assert!( + result.is_ok(), + "Transfer between Swig accounts failed: {:?}", + result.err() + ); + + // Verify the transfer was successful + let sender_account = context.svm.get_account(&sender_swig).unwrap(); + let recipient_account = context.svm.get_account(&recipient_swig).unwrap(); + + // Get initial recipient balance (should include the rent-exempt amount plus + // transfer) + let recipient_initial_balance = { + let recipient_swig_state = SwigWithRoles::from_bytes(&recipient_account.data).unwrap(); + recipient_swig_state.state.reserved_lamports + }; + + assert_eq!( + recipient_account.lamports, + recipient_initial_balance + transfer_amount, + "Recipient Swig account did not receive the correct amount" + ); + + println!( + "Successfully transferred {} lamports from Swig {} to Swig {}", + transfer_amount, sender_swig, recipient_swig + ); +} diff --git a/program/tests/sign_performance_test.rs b/program/tests/sign_performance_test.rs index 54fa99fa..c428cb16 100644 --- a/program/tests/sign_performance_test.rs +++ b/program/tests/sign_performance_test.rs @@ -188,9 +188,9 @@ fn test_token_transfer_performance_comparison() { "Account difference (swig - regular): {} accounts", account_difference ); - // 3760 is the max difference in CU between the two transactions lets lower + // 3717 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 <= 3949); + assert!(swig_transfer_cu - regular_transfer_cu <= 3717); } #[test_log::test] @@ -303,5 +303,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 <= 2506); + assert!(swig_transfer_cu - regular_transfer_cu <= 1967); } diff --git a/program/tests/sign_seckp256k1.rs b/program/tests/sign_secp256k1.rs similarity index 91% rename from program/tests/sign_seckp256k1.rs rename to program/tests/sign_secp256k1.rs index 350b6f14..de534d2d 100644 --- a/program/tests/sign_seckp256k1.rs +++ b/program/tests/sign_secp256k1.rs @@ -17,10 +17,13 @@ use solana_sdk::{ system_instruction, transaction::{TransactionError, VersionedTransaction}, }; -use swig_interface::{AuthorityConfig, ClientAction}; +use swig_interface::{AuthorityConfig, ClientAction, CreateSessionInstruction}; use swig_state_x::{ action::all::All, - authority::{secp256k1::Secp256k1Authority, AuthorityType}, + authority::{ + secp256k1::{Secp256k1Authority, Secp256k1SessionAuthority}, + AuthorityType, + }, swig::SwigWithRoles, }; @@ -934,11 +937,6 @@ fn test_secp256k1_replay_scenario_2() { .unwrap(); let result2 = context.svm.send_transaction(tx2); - // assert!( - // result2.is_ok(), - // "Expected second transaction to succeed (demonstrating vulnerability): - // {:?}", result2.err() - // ); assert!( result2.is_err(), "Expected second transaction to succeed (demonstrating vulnerability): {:?}", @@ -952,3 +950,75 @@ fn test_secp256k1_replay_scenario_2() { 1_000_000 + 2 * transfer_amount ); } + +#[test_log::test] +fn test_secp256k1_session_authority_odometer() { + let mut context = setup_test_context().unwrap(); + + // Generate a random Ethereum wallet + let wallet = LocalSigner::random(); + + let id = rand::random::<[u8; 32]>(); + + // Create a swig with secp256k1 session authority type + let (swig_key, _) = + create_swig_secp256k1_session(&mut context, &wallet, id, 100, [0; 32]).unwrap(); + + // Helper function to read the current counter for session authorities + let get_session_counter = |ctx: &SwigTestContext| -> Result { + let swig_account = ctx + .svm + .get_account(&swig_key) + .ok_or("Swig account not found")?; + let swig = SwigWithRoles::from_bytes(&swig_account.data) + .map_err(|e| format!("Failed to parse swig data: {:?}", e))?; + + let role = swig + .get_role(0) + .map_err(|e| format!("Failed to get role: {:?}", e))? + .ok_or("Role not found")?; + + if let Some(auth) = role + .authority + .as_any() + .downcast_ref::() + { + Ok(auth.signature_odometer) + } else { + Err("Authority is not a Secp256k1SessionAuthority".to_string()) + } + }; + + // Initial counter should be 0 + let initial_counter = get_session_counter(&context).unwrap(); + assert_eq!(initial_counter, 0, "Initial session counter should be 0"); + + // Verify the session authority structure is correctly initialized + 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); + let role = swig.get_role(0).unwrap().unwrap(); + + assert_eq!( + role.authority.authority_type(), + AuthorityType::Secp256k1Session + ); + assert!(role.authority.session_based()); + + let auth: &Secp256k1SessionAuthority = role.authority.as_any().downcast_ref().unwrap(); + assert_eq!(auth.max_session_age, 100); + let compressed_eth_pubkey = wallet + .credential() + .verifying_key() + .to_encoded_point(true) + .to_bytes(); + assert_eq!(auth.public_key, compressed_eth_pubkey.as_ref()); + assert_eq!(auth.current_session_expiration, 0); + assert_eq!(auth.session_key, [0; 32]); + assert_eq!(auth.signature_odometer, 0, "Initial odometer should be 0"); + + println!("✓ Secp256k1 session authority structure correctly initialized"); + println!("✓ Signature odometer field present and initialized to 0"); + println!("✓ Session authority has proper session-based behavior"); + println!("✓ All other fields remain intact after adding odometer"); +} diff --git a/program/tests/sign_secp256r1.rs b/program/tests/sign_secp256r1.rs new file mode 100644 index 00000000..8800d55a --- /dev/null +++ b/program/tests/sign_secp256r1.rs @@ -0,0 +1,600 @@ +#![cfg(not(feature = "program_scope_test"))] +// This feature flag ensures these tests are only run when the +// "program_scope_test" feature is not enabled. This allows us to isolate +// and run only program_scope tests or only the regular tests. + +mod common; +use common::*; +use solana_sdk::{ + clock::Clock, + instruction::InstructionError, + message::{v0, VersionedMessage}, + signature::Keypair, + signer::Signer, + system_instruction, + transaction::{TransactionError, VersionedTransaction}, +}; +use swig_interface::{AuthorityConfig, ClientAction}; +use swig_state_x::{ + action::all::All, + authority::{ + secp256r1::{Secp256r1Authority, Secp256r1SessionAuthority}, + AuthorityType, + }, + swig::SwigWithRoles, +}; + +/// Helper to generate a real secp256r1 key pair for testing +fn create_test_secp256r1_keypair() -> (openssl::ec::EcKey, [u8; 33]) { + use openssl::{ + bn::BigNumContext, + ec::{EcGroup, EcKey, PointConversionForm}, + nid::Nid, + }; + + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let signing_key = EcKey::generate(&group).unwrap(); + + let mut ctx = BigNumContext::new().unwrap(); + let pubkey_bytes = signing_key + .public_key() + .to_bytes(&group, PointConversionForm::COMPRESSED, &mut ctx) + .unwrap(); + + let pubkey_array: [u8; 33] = pubkey_bytes.try_into().unwrap(); + (signing_key, pubkey_array) +} + +/// Helper function to create a secp256r1 authority with a test public key +fn create_test_secp256r1_authority() -> [u8; 33] { + let (_, pubkey) = create_test_secp256r1_keypair(); + pubkey +} + +/// Helper function to get the current signature counter for a secp256r1 +/// authority +fn get_secp256r1_counter( + context: &SwigTestContext, + swig_key: &solana_sdk::pubkey::Pubkey, + public_key: &[u8; 33], +) -> Result { + // Get the swig account data + let swig_account = context + .svm + .get_account(swig_key) + .ok_or("Swig account not found")?; + let swig = SwigWithRoles::from_bytes(&swig_account.data) + .map_err(|e| format!("Failed to parse swig data: {:?}", e))?; + + // Look up the role ID for this authority + let role_id = swig + .lookup_role_id(public_key) + .map_err(|e| format!("Failed to lookup role: {:?}", e))? + .ok_or("Authority not found in swig account")?; + + // Get the role + let role = swig + .get_role(role_id) + .map_err(|e| format!("Failed to get role: {:?}", e))? + .ok_or("Role not found")?; + + // The authority should be a Secp256r1Authority + if matches!(role.authority.authority_type(), AuthorityType::Secp256r1) { + // Get the authority from the any() interface + let secp_authority = role + .authority + .as_any() + .downcast_ref::() + .ok_or("Failed to downcast to Secp256r1Authority")?; + + Ok(secp_authority.signature_odometer) + } else { + Err("Authority is not a Secp256r1Authority".to_string()) + } +} + +#[test_log::test] +fn test_secp256r1_basic_signing() { + let mut context = setup_test_context().unwrap(); + + // Create a real secp256r1 key pair for testing + let (signing_key, public_key) = create_test_secp256r1_keypair(); + + // Create a new swig with the secp256r1 authority + let id = rand::random::<[u8; 32]>(); + let (swig_key, _) = create_swig_secp256r1(&mut context, &public_key, id).unwrap(); + context.svm.airdrop(&swig_key, 10_000_000_000).unwrap(); + + // Set up a recipient and transaction + let recipient = Keypair::new(); + context.svm.airdrop(&recipient.pubkey(), 1_000_000).unwrap(); + let transfer_amount = 5_000_000; + let transfer_ix = system_instruction::transfer(&swig_key, &recipient.pubkey(), transfer_amount); + + // Get current slot and counter + let current_slot = context.svm.get_sysvar::().slot; + let current_counter = get_secp256r1_counter(&context, &swig_key, &public_key).unwrap(); + let next_counter = current_counter + 1; + + println!( + "Current counter: {}, using next counter: {}", + current_counter, next_counter + ); + + // Create authority function that signs the message hash + let mut authority_fn = |message_hash: &[u8]| -> [u8; 64] { + use solana_secp256r1_program::sign_message; + let signature = + sign_message(message_hash, &signing_key.private_key_to_der().unwrap()).unwrap(); + signature + }; + + // Create the secp256r1 signing instructions (returns Vec) + let instructions = swig_interface::SignInstruction::new_secp256r1( + swig_key, + context.default_payer.pubkey(), + authority_fn, + current_slot, + next_counter, + transfer_ix.clone(), + 0, // Role ID 0 + &public_key, + ) + .unwrap(); + + let message = v0::Message::try_compile( + &context.default_payer.pubkey(), + &instructions, // Use the returned instructions directly + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx = + VersionedTransaction::try_new(VersionedMessage::V0(message), &[&context.default_payer]) + .unwrap(); + + // Send the transaction - should now succeed with real cryptography + let result = context.svm.send_transaction(tx); + + println!("Transaction result: {:?}", result); + + // Verify the transaction succeeded + assert!( + result.is_ok(), + "Transaction should succeed with real secp256r1 signature: {:?}", + result.err() + ); + + // Verify the counter was incremented + let new_counter = get_secp256r1_counter(&context, &swig_key, &public_key).unwrap(); + assert_eq!( + new_counter, next_counter, + "Counter should be incremented after successful transaction" + ); + + // Verify the transfer actually happened + let recipient_balance = context + .svm + .get_account(&recipient.pubkey()) + .unwrap() + .lamports; + assert_eq!( + recipient_balance, + 1_000_000 + transfer_amount, + "Recipient should receive the transferred amount" + ); + + println!("✓ Secp256r1 signing test passed with real cryptography"); +} + +#[test_log::test] +fn test_secp256r1_counter_increment() { + let mut context = setup_test_context().unwrap(); + + // Create a real secp256r1 key pair for testing + let (_, public_key) = create_test_secp256r1_keypair(); + + // Create a new swig with the secp256r1 authority + let id = rand::random::<[u8; 32]>(); + let (swig_key, _) = create_swig_secp256r1(&mut context, &public_key, id).unwrap(); + context.svm.airdrop(&swig_key, 10_000_000_000).unwrap(); + + // Verify initial counter is 0 + let initial_counter = get_secp256r1_counter(&context, &swig_key, &public_key).unwrap(); + assert_eq!(initial_counter, 0, "Initial counter should be 0"); + + println!("✓ Initial counter verified as 0"); + println!("✓ Secp256r1 authority structure works correctly"); +} + +#[test_log::test] +fn test_secp256r1_replay_protection() { + let mut context = setup_test_context().unwrap(); + + // Create a real secp256r1 key pair for testing + let (signing_key, public_key) = create_test_secp256r1_keypair(); + + // Create a new swig with the secp256r1 authority + let id = rand::random::<[u8; 32]>(); + let (swig_key, _) = create_swig_secp256r1(&mut context, &public_key, id).unwrap(); + context.svm.airdrop(&swig_key, 10_000_000_000).unwrap(); + + // Set up transfer instruction + let recipient = Keypair::new(); + context.svm.airdrop(&recipient.pubkey(), 1_000_000).unwrap(); + let transfer_amount = 1_000_000; + let transfer_ix = system_instruction::transfer(&swig_key, &recipient.pubkey(), transfer_amount); + + let current_slot = context.svm.get_sysvar::().slot; + + // First transaction with counter 1 + let counter1 = 1; + + // Create authority function that signs the message hash + let mut authority_fn1 = |message_hash: &[u8]| -> [u8; 64] { + use solana_secp256r1_program::sign_message; + let signature = + sign_message(message_hash, &signing_key.private_key_to_der().unwrap()).unwrap(); + signature + }; + + let instructions1 = swig_interface::SignInstruction::new_secp256r1( + swig_key, + context.default_payer.pubkey(), + authority_fn1, + current_slot, + counter1, + transfer_ix.clone(), + 0, + &public_key, + ) + .unwrap(); + + // Execute first transaction + let message1 = v0::Message::try_compile( + &context.default_payer.pubkey(), + &instructions1, + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx1 = + VersionedTransaction::try_new(VersionedMessage::V0(message1), &[&context.default_payer]) + .unwrap(); + let result1 = context.svm.send_transaction(tx1); + + assert!( + result1.is_ok(), + "First transaction should succeed: {:?}", + result1.err() + ); + println!("✓ First transaction with counter 1 succeeded"); + + // Try second transaction with same counter (should fail due to replay + // protection) + let mut authority_fn2 = |message_hash: &[u8]| -> [u8; 64] { + use solana_secp256r1_program::sign_message; + let signature = + sign_message(message_hash, &signing_key.private_key_to_der().unwrap()).unwrap(); + signature + }; + + let instructions2 = swig_interface::SignInstruction::new_secp256r1( + swig_key, + context.default_payer.pubkey(), + authority_fn2, + current_slot, + counter1, // Same counter - should trigger replay protection + transfer_ix.clone(), + 0, + &public_key, + ) + .unwrap(); + + let message2 = v0::Message::try_compile( + &context.default_payer.pubkey(), + &instructions2, + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx2 = + VersionedTransaction::try_new(VersionedMessage::V0(message2), &[&context.default_payer]) + .unwrap(); + let result2 = context.svm.send_transaction(tx2); + + assert!( + result2.is_err(), + "Second transaction with same counter should fail due to replay protection" + ); + println!("✓ Second transaction with same counter failed (replay protection working)"); + + // Verify counter is now 1 + let current_counter = get_secp256r1_counter(&context, &swig_key, &public_key).unwrap(); + assert_eq!( + current_counter, 1, + "Counter should be 1 after first transaction" + ); + + println!("✓ Replay protection test passed - counter-based protection is working"); +} + +#[test_log::test] +fn test_secp256r1_add_authority() { + let mut context = setup_test_context().unwrap(); + + // Create primary Ed25519 authority + let primary_authority = Keypair::new(); + let id = rand::random::<[u8; 32]>(); + + // Create a new swig with Ed25519 authority + let (swig_key, _) = create_swig_ed25519(&mut context, &primary_authority, id).unwrap(); + context.svm.airdrop(&swig_key, 10_000_000_000).unwrap(); + + // Create a real secp256r1 public key to add as second authority + let (_, secp256r1_pubkey) = create_test_secp256r1_keypair(); + + // Create instruction to add the Secp256r1 authority + let add_authority_ix = swig_interface::AddAuthorityInstruction::new_with_ed25519_authority( + swig_key, + context.default_payer.pubkey(), + primary_authority.pubkey(), + 0, // role_id of the primary wallet + AuthorityConfig { + authority_type: AuthorityType::Secp256r1, + authority: &secp256r1_pubkey, + }, + vec![ClientAction::All(All {})], + ) + .unwrap(); + + let message = v0::Message::try_compile( + &context.default_payer.pubkey(), + &[add_authority_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx = VersionedTransaction::try_new( + VersionedMessage::V0(message), + &[&context.default_payer, &primary_authority], + ) + .unwrap(); + + let result = context.svm.send_transaction(tx); + assert!( + result.is_ok(), + "Failed to add Secp256r1 authority: {:?}", + result.err() + ); + + // Verify the authority was added + let swig_account = context.svm.get_account(&swig_key).unwrap(); + let swig_state = SwigWithRoles::from_bytes(&swig_account.data).unwrap(); + assert_eq!(swig_state.state.roles, 2); + + println!("✓ Successfully added Secp256r1 authority"); + println!("✓ Authority count increased to 2"); +} + +#[test_log::test] +fn test_secp256r1_session_authority() { + let mut context = setup_test_context().unwrap(); + + // Create a real secp256r1 public key for session authority + let (_, public_key) = create_test_secp256r1_keypair(); + + // Create session authority parameters + let session_key = rand::random::<[u8; 32]>(); + let max_session_length = 1000; // 1000 slots + + let create_params = swig_state_x::authority::secp256r1::CreateSecp256r1SessionAuthority::new( + public_key, + session_key, + max_session_length, + ); + + // Verify the structure works + assert_eq!(create_params.public_key, public_key); + assert_eq!(create_params.session_key, session_key); + assert_eq!(create_params.max_session_length, max_session_length); + + println!("✓ Secp256r1 session authority structure works correctly"); + println!( + "✓ Session parameters: max_length = {} slots", + max_session_length + ); +} + +#[test_log::test] +fn test_secp256r1_session_authority_odometer() { + let mut context = setup_test_context().unwrap(); + + // Create a real secp256r1 key pair for testing + let (_, public_key) = create_test_secp256r1_keypair(); + + let id = rand::random::<[u8; 32]>(); + + // Create a swig with secp256r1 session authority type using the helper function + let (swig_key, _) = + create_swig_secp256r1_session(&mut context, &public_key, id, 100, [0; 32]).unwrap(); + + // Helper function to read the current counter for session authorities + let get_session_counter = |ctx: &SwigTestContext| -> Result { + let swig_account = ctx + .svm + .get_account(&swig_key) + .ok_or("Swig account not found")?; + let swig = SwigWithRoles::from_bytes(&swig_account.data) + .map_err(|e| format!("Failed to parse swig data: {:?}", e))?; + + let role = swig + .get_role(0) + .map_err(|e| format!("Failed to get role: {:?}", e))? + .ok_or("Role not found")?; + + if let Some(auth) = role + .authority + .as_any() + .downcast_ref::() + { + Ok(auth.signature_odometer) + } else { + Err("Authority is not a Secp256r1SessionAuthority".to_string()) + } + }; + + // Initial counter should be 0 + let initial_counter = get_session_counter(&context).unwrap(); + assert_eq!(initial_counter, 0, "Initial session counter should be 0"); + + // Verify the session authority structure is correctly initialized + 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); + let role = swig.get_role(0).unwrap().unwrap(); + + assert_eq!( + role.authority.authority_type(), + AuthorityType::Secp256r1Session + ); + assert!(role.authority.session_based()); + + let auth: &Secp256r1SessionAuthority = role.authority.as_any().downcast_ref().unwrap(); + assert_eq!(auth.max_session_age, 100); + assert_eq!(auth.public_key, public_key); + assert_eq!(auth.current_session_expiration, 0); + assert_eq!(auth.session_key, [0; 32]); + assert_eq!(auth.signature_odometer, 0, "Initial odometer should be 0"); + + println!("✓ Secp256r1 session authority structure correctly initialized"); + println!("✓ Signature odometer field present and initialized to 0"); + println!("✓ Session authority has proper session-based behavior"); +} + +/// Helper function to create a swig account with secp256r1 authority for +/// testing +fn create_swig_secp256r1( + context: &mut SwigTestContext, + public_key: &[u8; 33], + id: [u8; 32], +) -> Result<(solana_sdk::pubkey::Pubkey, u8), Box> { + use swig_state_x::swig::swig_account_seeds; + + let payer_pubkey = context.default_payer.pubkey(); + let (swig_address, swig_bump) = solana_sdk::pubkey::Pubkey::find_program_address( + &swig_account_seeds(&id), + &common::program_id(), + ); + + let create_ix = swig_interface::CreateInstruction::new( + swig_address, + swig_bump, + payer_pubkey, + AuthorityConfig { + authority_type: AuthorityType::Secp256r1, + authority: public_key, + }, + vec![ClientAction::All(All {})], + id, + )?; + + let message = v0::Message::try_compile( + &payer_pubkey, + &[create_ix], + &[], + context.svm.latest_blockhash(), + )?; + + let tx = + VersionedTransaction::try_new(VersionedMessage::V0(message), &[&context.default_payer])?; + + context.svm.send_transaction(tx).unwrap(); + + Ok((swig_address, swig_bump)) +} + +#[test_log::test] +fn test_secp256r1_add_authority_with_secp256r1() { + let mut context = setup_test_context().unwrap(); + + // Create a real secp256r1 key pair for the primary authority + let (signing_key, public_key) = create_test_secp256r1_keypair(); + let id = rand::random::<[u8; 32]>(); + + // Create a new swig with secp256r1 authority + let (swig_key, _) = create_swig_secp256r1(&mut context, &public_key, id).unwrap(); + context.svm.airdrop(&swig_key, 10_000_000_000).unwrap(); + + // Create a second secp256r1 public key to add as a new authority + let (_, new_public_key) = create_test_secp256r1_keypair(); + + // Get current slot and counter for the authority + let current_slot = context.svm.get_sysvar::().slot; + let current_counter = get_secp256r1_counter(&context, &swig_key, &public_key).unwrap(); + let next_counter = current_counter + 1; + + // Create authority function that signs the message hash + let mut authority_fn = |message_hash: &[u8]| -> [u8; 64] { + use solana_secp256r1_program::sign_message; + let signature = + sign_message(message_hash, &signing_key.private_key_to_der().unwrap()).unwrap(); + signature + }; + + // Create instruction to add the new Secp256r1 authority + let instructions = swig_interface::AddAuthorityInstruction::new_with_secp256r1_authority( + swig_key, + context.default_payer.pubkey(), + authority_fn, + current_slot, + next_counter, + 0, // role_id of the primary authority + &public_key, + AuthorityConfig { + authority_type: AuthorityType::Secp256r1, + authority: &new_public_key, + }, + vec![ClientAction::All(All {})], + ) + .unwrap(); + + let message = v0::Message::try_compile( + &context.default_payer.pubkey(), + &instructions, + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx = + VersionedTransaction::try_new(VersionedMessage::V0(message), &[&context.default_payer]) + .unwrap(); + + let result = context.svm.send_transaction(tx); + assert!( + result.is_ok(), + "Failed to add Secp256r1 authority using secp256r1 signature: {:?}", + result.err() + ); + + // Verify the authority was added + let swig_account = context.svm.get_account(&swig_key).unwrap(); + let swig_state = SwigWithRoles::from_bytes(&swig_account.data).unwrap(); + assert_eq!(swig_state.state.roles, 2); + + // Verify the counter was incremented + let new_counter = get_secp256r1_counter(&context, &swig_key, &public_key).unwrap(); + assert_eq!( + new_counter, next_counter, + "Counter should be incremented after successful transaction" + ); + + println!("✓ Successfully added Secp256r1 authority using secp256r1 signature"); + println!("✓ Authority count increased to 2"); + println!("✓ Counter incremented correctly"); +} diff --git a/program/tests/sub_account_test.rs b/program/tests/sub_account_test.rs index 1844d871..378f584d 100644 --- a/program/tests/sub_account_test.rs +++ b/program/tests/sub_account_test.rs @@ -1,3 +1,7 @@ +#![cfg(not(feature = "program_scope_test"))] +// This feature flag ensures these tests are only run when the +// "program_scope_test" feature is not enabled. This allows us to isolate +// and run only program_scope tests or only the regular tests. mod common; use common::*; diff --git a/rust-sdk/src/client_role.rs b/rust-sdk/src/client_role.rs new file mode 100644 index 00000000..1f697da7 --- /dev/null +++ b/rust-sdk/src/client_role.rs @@ -0,0 +1,1022 @@ +use solana_program::{instruction::Instruction, pubkey::Pubkey}; +use swig_interface::{ + AddAuthorityInstruction, AuthorityConfig, ClientAction, CreateSessionInstruction, + CreateSubAccountInstruction, RemoveAuthorityInstruction, SignInstruction, + SubAccountSignInstruction, ToggleSubAccountInstruction, WithdrawFromSubAccountInstruction, +}; +use swig_state_x::{ + authority::{ + ed25519::CreateEd25519SessionAuthority, secp256k1::CreateSecp256k1SessionAuthority, + AuthorityType, + }, + IntoBytes, +}; + +use crate::{error::SwigError, types::Permission as ClientPermission}; + +/// Trait for client-side role implementations that handle instruction creation +/// for different authority types. +pub trait ClientRole { + /// Creates a sign instruction for the given inner instructions + fn sign_instruction( + &self, + swig_account: Pubkey, + payer: Pubkey, + role_id: u32, + instructions: Vec, + current_slot: Option, + ) -> Result, SwigError>; + + /// Creates an add authority instruction + 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; + + /// Creates a remove authority instruction + fn remove_authority_instruction( + &self, + swig_account: Pubkey, + payer: Pubkey, + role_id: u32, + authority_to_remove_id: u32, + current_slot: Option, + ) -> Result; + + /// Creates a create session instruction + fn create_session_instruction( + &self, + swig_account: Pubkey, + payer: Pubkey, + role_id: u32, + session_key: Pubkey, + session_duration: u64, + current_slot: Option, + ) -> Result; + + /// Creates a create sub account instruction + 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; + + /// Creates a sub account sign instruction + fn sub_account_sign_instruction( + &self, + swig_account: Pubkey, + sub_account: Pubkey, + payer: Pubkey, + role_id: u32, + instructions: Vec, + current_slot: Option, + ) -> Result; + + /// Creates a withdraw from sub account instruction + fn withdraw_from_sub_account_instruction( + &self, + swig_account: Pubkey, + payer: Pubkey, + sub_account: Pubkey, + role_id: u32, + amount: u64, + current_slot: Option, + ) -> Result; + + /// Creates a withdraw token from sub account instruction + 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; + + /// Creates a toggle sub account instruction + fn toggle_sub_account_instruction( + &self, + swig_account: Pubkey, + payer: Pubkey, + sub_account: Pubkey, + role_id: u32, + enabled: bool, + current_slot: Option, + ) -> Result; + + /// Returns the authority type + fn authority_type(&self) -> AuthorityType; + + /// Returns the authority bytes for creating the Swig account + fn authority_bytes(&self) -> Result, SwigError>; + + /// Returns the odometer for the current authority if it is a Secp256k1 authority + fn odometer(&self) -> Result; + + /// Increments the odometer for the current authority if it is a Secp256k1 authority + fn increment_odometer(&mut self) -> Result<(), SwigError>; + + /// Update the odometer for the authority + fn update_odometer(&mut self, odometer: u32) -> Result<(), SwigError>; +} + +/// Ed25519 authority implementation +pub struct Ed25519ClientRole { + pub authority: Pubkey, +} + +impl Ed25519ClientRole { + pub fn new(authority: Pubkey) -> Self { + Self { authority } + } +} + +impl ClientRole for Ed25519ClientRole { + fn sign_instruction( + &self, + swig_account: Pubkey, + payer: Pubkey, + role_id: u32, + instructions: Vec, + current_slot: Option, + ) -> Result, SwigError> { + let mut signed_instructions = Vec::new(); + for instruction in instructions { + let swig_signed_instruction = SignInstruction::new_ed25519( + swig_account, + payer, + self.authority, + instruction, + role_id, + )?; + signed_instructions.push(swig_signed_instruction); + } + Ok(signed_instructions) + } + + 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 { + Ok(AddAuthorityInstruction::new_with_ed25519_authority( + swig_account, + payer, + self.authority, + role_id, + AuthorityConfig { + authority_type: new_authority_type, + authority: new_authority, + }, + actions, + )?) + } + + fn remove_authority_instruction( + &self, + swig_account: Pubkey, + payer: Pubkey, + role_id: u32, + authority_to_remove_id: u32, + _current_slot: Option, + ) -> Result { + Ok(RemoveAuthorityInstruction::new_with_ed25519_authority( + swig_account, + payer, + self.authority, + role_id, + authority_to_remove_id, + )?) + } + + fn create_session_instruction( + &self, + swig_account: Pubkey, + payer: Pubkey, + role_id: u32, + session_key: Pubkey, + session_duration: u64, + _current_slot: Option, + ) -> Result { + Ok(CreateSessionInstruction::new_with_ed25519_authority( + swig_account, + payer, + self.authority, + role_id, + session_key, + session_duration, + )?) + } + + 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 { + Ok(CreateSubAccountInstruction::new_with_ed25519_authority( + swig_account, + self.authority, + payer, + sub_account, + role_id, + sub_account_bump, + )?) + } + + fn sub_account_sign_instruction( + &self, + swig_account: Pubkey, + sub_account: Pubkey, + payer: Pubkey, + role_id: u32, + instructions: Vec, + _current_slot: Option, + ) -> Result { + Ok(SubAccountSignInstruction::new_with_ed25519_authority( + swig_account, + sub_account, + self.authority, + payer, + role_id, + instructions, + )?) + } + + fn withdraw_from_sub_account_instruction( + &self, + swig_account: Pubkey, + payer: Pubkey, + sub_account: Pubkey, + role_id: u32, + amount: u64, + _current_slot: Option, + ) -> Result { + WithdrawFromSubAccountInstruction::new_with_ed25519_authority( + swig_account, + self.authority, + payer, + sub_account, + role_id, + amount, + ) + .map_err(|e| anyhow::anyhow!("Failed to create withdraw instruction: {:?}", e).into()) + } + + 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 { + WithdrawFromSubAccountInstruction::new_token_with_ed25519_authority( + swig_account, + self.authority, + payer, + sub_account, + sub_account_token, + swig_token, + token_program, + role_id, + amount, + ) + .map_err(|e| anyhow::anyhow!("Failed to create withdraw token instruction: {:?}", e).into()) + } + + fn toggle_sub_account_instruction( + &self, + swig_account: Pubkey, + payer: Pubkey, + sub_account: Pubkey, + role_id: u32, + enabled: bool, + _current_slot: Option, + ) -> Result { + ToggleSubAccountInstruction::new_with_ed25519_authority( + swig_account, + self.authority, + payer, + sub_account, + role_id, + enabled, + ) + .map_err(|e| anyhow::anyhow!("Failed to create toggle instruction: {:?}", e).into()) + } + + fn authority_type(&self) -> AuthorityType { + AuthorityType::Ed25519 + } + + fn authority_bytes(&self) -> Result, SwigError> { + Ok(self.authority.to_bytes().to_vec()) + } + + fn odometer(&self) -> Result { + Ok(0) + } + + fn increment_odometer(&mut self) -> Result<(), SwigError> { + // Ed25519 authorities don't use odometer-based replay protection + Ok(()) + } + + fn update_odometer(&mut self, odometer: u32) -> Result<(), SwigError> { + Ok(()) + } +} + +/// Secp256k1 authority implementation +pub struct Secp256k1ClientRole { + pub authority: Box<[u8]>, + pub signing_fn: Box [u8; 65]>, + pub odometer: u32, +} + +impl Secp256k1ClientRole { + pub fn new(authority: Box<[u8]>, signing_fn: Box [u8; 65]>) -> Self { + Self { + authority, + signing_fn, + odometer: 0, + } + } + + pub fn new_without_odometer( + authority: Box<[u8]>, + signing_fn: Box [u8; 65]>, + ) -> Self { + Self { + authority, + signing_fn, + odometer: 0, + } + } +} + +impl ClientRole for Secp256k1ClientRole { + fn sign_instruction( + &self, + swig_account: Pubkey, + payer: Pubkey, + role_id: u32, + instructions: Vec, + current_slot: Option, + ) -> Result, SwigError> { + let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; + let new_odometer = self.odometer.wrapping_add(1); + let mut signed_instructions = Vec::new(); + for instruction in instructions { + let swig_signed_instruction = SignInstruction::new_secp256k1( + swig_account, + payer, + &self.signing_fn, + current_slot, + new_odometer, + instruction, + role_id, + )?; + signed_instructions.push(swig_signed_instruction); + } + Ok(signed_instructions) + } + + 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 { + let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; + let new_odometer = self.odometer.wrapping_add(1); + + Ok(AddAuthorityInstruction::new_with_secp256k1_authority( + swig_account, + payer, + &self.signing_fn, + current_slot, + new_odometer, + role_id, + AuthorityConfig { + authority_type: new_authority_type, + authority: &new_authority[1..], + }, + actions, + )?) + } + + fn remove_authority_instruction( + &self, + swig_account: Pubkey, + payer: Pubkey, + role_id: u32, + authority_to_remove_id: u32, + current_slot: Option, + ) -> Result { + let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; + let new_odometer = self.odometer.wrapping_add(1); + + Ok(RemoveAuthorityInstruction::new_with_secp256k1_authority( + swig_account, + payer, + &self.signing_fn, + current_slot, + new_odometer, + role_id, + authority_to_remove_id, + )?) + } + + fn create_session_instruction( + &self, + swig_account: Pubkey, + payer: Pubkey, + role_id: u32, + session_key: Pubkey, + session_duration: u64, + current_slot: Option, + ) -> Result { + let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; + let new_odometer = self.odometer.wrapping_add(1); + Ok(CreateSessionInstruction::new_with_secp256k1_authority( + swig_account, + payer, + &self.signing_fn, + current_slot, + new_odometer, + role_id, + session_key, + session_duration, + )?) + } + + 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 { + let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; + + Ok(CreateSubAccountInstruction::new_with_secp256k1_authority( + swig_account, + payer, + &self.signing_fn, + current_slot, + sub_account, + role_id, + sub_account_bump, + )?) + } + + fn sub_account_sign_instruction( + &self, + swig_account: Pubkey, + sub_account: Pubkey, + payer: Pubkey, + role_id: u32, + instructions: Vec, + current_slot: Option, + ) -> Result { + let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; + + Ok(SubAccountSignInstruction::new_with_secp256k1_authority( + swig_account, + sub_account, + payer, + &self.signing_fn, + current_slot, + role_id, + instructions, + )?) + } + + fn withdraw_from_sub_account_instruction( + &self, + swig_account: Pubkey, + payer: Pubkey, + sub_account: Pubkey, + role_id: u32, + amount: u64, + current_slot: Option, + ) -> Result { + let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; + + WithdrawFromSubAccountInstruction::new_with_secp256k1_authority( + swig_account, + payer, + &self.signing_fn, + current_slot, + sub_account, + role_id, + amount, + ) + .map_err(|e| anyhow::anyhow!("Failed to create withdraw instruction: {:?}", e).into()) + } + + 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 { + let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; + + WithdrawFromSubAccountInstruction::new_token_with_secp256k1_authority( + swig_account, + payer, + &self.signing_fn, + current_slot, + sub_account, + sub_account_token, + swig_token, + token_program, + role_id, + amount, + ) + .map_err(|e| anyhow::anyhow!("Failed to create withdraw token instruction: {:?}", e).into()) + } + + fn toggle_sub_account_instruction( + &self, + swig_account: Pubkey, + payer: Pubkey, + sub_account: Pubkey, + role_id: u32, + enabled: bool, + current_slot: Option, + ) -> Result { + let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; + + ToggleSubAccountInstruction::new_with_secp256k1_authority( + swig_account, + payer, + &self.signing_fn, + current_slot, + sub_account, + role_id, + enabled, + ) + .map_err(|e| anyhow::anyhow!("Failed to create toggle instruction: {:?}", e).into()) + } + + fn authority_type(&self) -> AuthorityType { + AuthorityType::Secp256k1 + } + + fn authority_bytes(&self) -> Result, SwigError> { + Ok(self.authority[1..].to_vec()) + } + + fn odometer(&self) -> Result { + Ok(self.odometer) + } + + fn increment_odometer(&mut self) -> Result<(), SwigError> { + self.odometer = self.odometer.wrapping_add(1); + Ok(()) + } + + fn update_odometer(&mut self, odometer: u32) -> Result<(), SwigError> { + self.odometer = odometer; + Ok(()) + } +} + +/// Ed25519 Session authority implementation +pub struct Ed25519SessionClientRole { + pub session_authority: CreateEd25519SessionAuthority, +} + +impl Ed25519SessionClientRole { + pub fn new(session_authority: CreateEd25519SessionAuthority) -> Self { + Self { session_authority } + } +} + +impl ClientRole for Ed25519SessionClientRole { + fn sign_instruction( + &self, + swig_account: Pubkey, + payer: Pubkey, + role_id: u32, + instructions: Vec, + _current_slot: Option, + ) -> Result, SwigError> { + let session_authority_pubkey = Pubkey::new_from_array(self.session_authority.public_key); + + let mut signed_instructions = Vec::new(); + for instruction in instructions { + let swig_signed_instruction = SignInstruction::new_ed25519( + swig_account, + payer, + session_authority_pubkey, + instruction, + role_id, + )?; + signed_instructions.push(swig_signed_instruction); + } + Ok(signed_instructions) + } + + 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 { + Ok(AddAuthorityInstruction::new_with_ed25519_authority( + swig_account, + payer, + self.session_authority.public_key.into(), + role_id, + AuthorityConfig { + authority_type: new_authority_type, + authority: new_authority, + }, + actions, + )?) + } + + fn remove_authority_instruction( + &self, + swig_account: Pubkey, + payer: Pubkey, + role_id: u32, + authority_to_remove_id: u32, + _current_slot: Option, + ) -> Result { + Ok(RemoveAuthorityInstruction::new_with_ed25519_authority( + swig_account, + payer, + self.session_authority.public_key.into(), + role_id, + authority_to_remove_id, + )?) + } + + fn create_session_instruction( + &self, + swig_account: Pubkey, + payer: Pubkey, + role_id: u32, + session_key: Pubkey, + session_duration: u64, + _current_slot: Option, + ) -> Result { + Ok(CreateSessionInstruction::new_with_ed25519_authority( + swig_account, + payer, + self.session_authority.public_key.into(), + role_id, + session_key, + session_duration, + )?) + } + + 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 { + todo!("Session authorities don't support sub-account creation") + } + + fn sub_account_sign_instruction( + &self, + _swig_account: Pubkey, + _sub_account: Pubkey, + _payer: Pubkey, + _role_id: u32, + _instructions: Vec, + _current_slot: Option, + ) -> Result { + todo!("Session authorities don't support sub-account signing") + } + + fn withdraw_from_sub_account_instruction( + &self, + _swig_account: Pubkey, + _payer: Pubkey, + _sub_account: Pubkey, + _role_id: u32, + _amount: u64, + _current_slot: Option, + ) -> Result { + todo!("Session authorities don't support sub-account operations") + } + + 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 { + todo!("Session authorities don't support sub-account operations") + } + + fn toggle_sub_account_instruction( + &self, + _swig_account: Pubkey, + _payer: Pubkey, + _sub_account: Pubkey, + _role_id: u32, + _enabled: bool, + _current_slot: Option, + ) -> Result { + todo!("Session authorities don't support sub-account operations") + } + + fn authority_type(&self) -> AuthorityType { + AuthorityType::Ed25519Session + } + + fn authority_bytes(&self) -> Result, SwigError> { + Ok(self.session_authority.into_bytes().unwrap().to_vec()) + } + + fn odometer(&self) -> Result { + Ok(0) + } + + fn increment_odometer(&mut self) -> Result<(), SwigError> { + // Ed25519 session authorities don't use odometer-based replay protection + Ok(()) + } + + fn update_odometer(&mut self, odometer: u32) -> Result<(), SwigError> { + Ok(()) + } +} + +/// Secp256k1 Session authority implementation +pub struct Secp256k1SessionClientRole { + pub session_authority: CreateSecp256k1SessionAuthority, + pub signing_fn: Box [u8; 65]>, + pub odometer: u32, +} + +impl Secp256k1SessionClientRole { + pub fn new( + session_authority: CreateSecp256k1SessionAuthority, + signing_fn: Box [u8; 65]>, + ) -> Self { + Self { + session_authority, + signing_fn, + odometer: 0, + } + } + + pub fn new_with_odometer( + session_authority: CreateSecp256k1SessionAuthority, + signing_fn: Box [u8; 65]>, + odometer: u32, + ) -> Self { + Self { + session_authority, + signing_fn, + odometer, + } + } +} + +impl ClientRole for Secp256k1SessionClientRole { + fn sign_instruction( + &self, + swig_account: Pubkey, + payer: Pubkey, + role_id: u32, + instructions: Vec, + current_slot: Option, + ) -> Result, SwigError> { + let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; + + let mut signed_instructions = Vec::new(); + for instruction in instructions { + let swig_signed_instruction = SignInstruction::new_secp256k1( + swig_account, + payer, + &self.signing_fn, + current_slot, + 0u32, + instruction, + role_id, + )?; + signed_instructions.push(swig_signed_instruction); + } + Ok(signed_instructions) + } + + 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 { + let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; + + Ok(AddAuthorityInstruction::new_with_secp256k1_authority( + swig_account, + payer, + &self.signing_fn, + current_slot, + 0u32, + role_id, + AuthorityConfig { + authority_type: new_authority_type, + authority: new_authority, + }, + actions, + )?) + } + + fn remove_authority_instruction( + &self, + swig_account: Pubkey, + payer: Pubkey, + role_id: u32, + authority_to_remove_id: u32, + current_slot: Option, + ) -> Result { + let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; + let new_odometer = self.odometer.wrapping_add(1); + + Ok(RemoveAuthorityInstruction::new_with_secp256k1_authority( + swig_account, + payer, + &self.signing_fn, + current_slot, + new_odometer, + authority_to_remove_id, + role_id, + )?) + } + + fn create_session_instruction( + &self, + swig_account: Pubkey, + payer: Pubkey, + role_id: u32, + session_key: Pubkey, + session_duration: u64, + current_slot: Option, + ) -> Result { + let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; + let new_odometer = self.odometer.wrapping_add(1); + + Ok(CreateSessionInstruction::new_with_secp256k1_authority( + swig_account, + payer, + &self.signing_fn, + current_slot, + new_odometer, + role_id, + session_key, + session_duration, + )?) + } + + 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 { + todo!("Session authorities don't support sub-account creation") + } + + fn sub_account_sign_instruction( + &self, + _swig_account: Pubkey, + _sub_account: Pubkey, + _payer: Pubkey, + _role_id: u32, + _instructions: Vec, + _current_slot: Option, + ) -> Result { + todo!("Session authorities don't support sub-account signing") + } + + fn withdraw_from_sub_account_instruction( + &self, + _swig_account: Pubkey, + _payer: Pubkey, + _sub_account: Pubkey, + _role_id: u32, + _amount: u64, + _current_slot: Option, + ) -> Result { + todo!("Session authorities don't support sub-account operations") + } + + 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 { + todo!("Session authorities don't support sub-account operations") + } + + fn toggle_sub_account_instruction( + &self, + _swig_account: Pubkey, + _payer: Pubkey, + _sub_account: Pubkey, + _role_id: u32, + _enabled: bool, + _current_slot: Option, + ) -> Result { + todo!("Session authorities don't support sub-account operations") + } + + fn authority_type(&self) -> AuthorityType { + AuthorityType::Secp256k1Session + } + + fn authority_bytes(&self) -> Result, SwigError> { + Ok(self.session_authority.into_bytes().unwrap().to_vec()) + } + + fn odometer(&self) -> Result { + Ok(self.odometer) + } + + fn increment_odometer(&mut self) -> Result<(), SwigError> { + self.odometer = self.odometer.wrapping_add(1); + Ok(()) + } + + fn update_odometer(&mut self, odometer: u32) -> Result<(), SwigError> { + self.odometer = odometer; + Ok(()) + } +} diff --git a/rust-sdk/src/error.rs b/rust-sdk/src/error.rs index c3b7e26b..756c0348 100644 --- a/rust-sdk/src/error.rs +++ b/rust-sdk/src/error.rs @@ -73,6 +73,10 @@ pub enum SwigError { /// Invalid program scope #[error("Invalid program scope")] InvalidProgramScope, + + /// Counter not set + #[error("Counter not set")] + CounterNotSet, } impl From for SwigError { diff --git a/rust-sdk/src/instruction_builder.rs b/rust-sdk/src/instruction_builder.rs index 93903303..fc3121ef 100644 --- a/rust-sdk/src/instruction_builder.rs +++ b/rust-sdk/src/instruction_builder.rs @@ -14,18 +14,7 @@ use swig_state_x::{ IntoBytes, }; -use crate::{error::SwigError, types::Permission as ClientPermission}; - -/// Represents the type of authority used for signing transactions -pub enum AuthorityManager { - Ed25519(Pubkey), - Secp256k1(Box<[u8]>, Box [u8; 65]>), - Ed25519Session(CreateEd25519SessionAuthority), - Secp256k1Session( - CreateSecp256k1SessionAuthority, - Box [u8; 65]>, - ), -} +use crate::{client_role::ClientRole, error::SwigError, types::Permission as ClientPermission}; /// A builder for creating and managing Swig wallet instructions. /// @@ -37,8 +26,8 @@ pub struct SwigInstructionBuilder { swig_id: [u8; 32], /// The public key of the Swig account swig_account: Pubkey, - /// The type of authority for this wallet - authority_manager: AuthorityManager, + /// The client role implementation for this wallet + client_role: Box, /// The public key of the fee payer payer: Pubkey, /// The role id of the wallet @@ -51,7 +40,7 @@ impl SwigInstructionBuilder { /// # Arguments /// /// * `swig_id` - The unique identifier for the Swig account - /// * `authority_manager` - The authority manager specifying the type of + /// * `client_role` - The client role implementation specifying the type of /// signing authority /// * `payer` - The public key of the fee payer /// * `role_id` - The role identifier for this wallet @@ -61,7 +50,7 @@ impl SwigInstructionBuilder { /// Returns a new instance of `SwigInstructionBuilder` pub fn new( swig_id: [u8; 32], - authority_manager: AuthorityManager, + client_role: Box, payer: Pubkey, role_id: u32, ) -> Self { @@ -70,7 +59,7 @@ impl SwigInstructionBuilder { Self { swig_id, swig_account, - authority_manager, + client_role, payer, role_id, } @@ -91,20 +80,8 @@ impl SwigInstructionBuilder { let (swig_account, swig_bump_seed) = Pubkey::find_program_address(&swig_account_seeds(&self.swig_id), &program_id); - let (authority_type, auth_bytes): (AuthorityType, &[u8]) = match &self.authority_manager { - AuthorityManager::Ed25519(authority) => (AuthorityType::Ed25519, &authority.to_bytes()), - AuthorityManager::Secp256k1(authority, _) => { - (AuthorityType::Secp256k1, &authority[1..]) - }, - AuthorityManager::Ed25519Session(session_authority) => ( - AuthorityType::Ed25519Session, - &session_authority.into_bytes().unwrap(), - ), - AuthorityManager::Secp256k1Session(session_authority, _) => ( - AuthorityType::Secp256k1Session, - &session_authority.into_bytes().unwrap(), - ), - }; + let authority_type = self.client_role.authority_type(); + let auth_bytes = self.client_role.authority_bytes()?; let actions = vec![ClientAction::All(swig_state_x::action::all::All {})]; @@ -114,7 +91,7 @@ impl SwigInstructionBuilder { self.payer, AuthorityConfig { authority_type, - authority: auth_bytes, + authority: &auth_bytes, }, actions, self.swig_id, @@ -137,64 +114,14 @@ impl SwigInstructionBuilder { &mut self, instructions: Vec, current_slot: Option, - signature_counter: Option, ) -> Result, SwigError> { - let mut signed_instructions = Vec::new(); - for instruction in instructions { - match &mut self.authority_manager { - AuthorityManager::Ed25519(authority) => { - let swig_signed_instruction = SignInstruction::new_ed25519( - self.swig_account, - self.payer, - *authority, - instruction, - self.role_id, - )?; - signed_instructions.push(swig_signed_instruction); - }, - AuthorityManager::Secp256k1(authority, signing_fn) => { - let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; - let counter = signature_counter.unwrap_or(1u32); // Default to 1 if not provided - let swig_signed_instruction = SignInstruction::new_secp256k1( - self.swig_account, - self.payer, - signing_fn, - current_slot, - counter, - instruction, - self.role_id, - )?; - signed_instructions.push(swig_signed_instruction); - }, - AuthorityManager::Ed25519Session(session_authority) => { - let session_authority_pubkey = - Pubkey::new_from_array(session_authority.public_key); - let swig_signed_instruction = SignInstruction::new_ed25519( - self.swig_account, - self.payer, - session_authority_pubkey, - instruction, - self.role_id, - )?; - signed_instructions.push(swig_signed_instruction); - }, - AuthorityManager::Secp256k1Session(session_authority, signing_fn) => { - let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; - // Session authorities always use 0 for counter - let swig_signed_instruction = SignInstruction::new_secp256k1( - self.swig_account, - self.payer, - signing_fn, - current_slot, - 0u32, - instruction, - self.role_id, - )?; - signed_instructions.push(swig_signed_instruction); - }, - } - } - Ok(signed_instructions) + self.client_role.sign_instruction( + self.swig_account, + self.payer, + self.role_id, + instructions, + current_slot, + ) } /// Creates an instruction to add a new authority to the wallet @@ -216,73 +143,18 @@ impl SwigInstructionBuilder { new_authority: &[u8], permissions: Vec, current_slot: Option, - signature_counter: Option, ) -> Result { let actions = ClientPermission::to_client_actions(permissions); - match &mut self.authority_manager { - AuthorityManager::Ed25519(authority) => { - Ok(AddAuthorityInstruction::new_with_ed25519_authority( - self.swig_account, - self.payer, - *authority, - self.role_id, - AuthorityConfig { - authority_type: new_authority_type, - authority: new_authority, - }, - actions, - )?) - }, - AuthorityManager::Secp256k1(authority, signing_fn) => { - let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; - let counter = signature_counter.unwrap_or(1u32); // Default to 1 if not provided - Ok(AddAuthorityInstruction::new_with_secp256k1_authority( - self.swig_account, - self.payer, - signing_fn, - current_slot, - counter, - self.role_id, - AuthorityConfig { - authority_type: new_authority_type, - authority: &new_authority[1..], - }, - actions, - )?) - }, - AuthorityManager::Ed25519Session(session_authority) => { - Ok(AddAuthorityInstruction::new_with_ed25519_authority( - self.swig_account, - self.payer, - session_authority.public_key.into(), - self.role_id, - AuthorityConfig { - authority_type: new_authority_type, - authority: new_authority, - }, - actions, - )?) - }, - AuthorityManager::Secp256k1Session(session_authority, signing_fn) => { - let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; - // Session authorities always use 0 for counter (no replay protection based on - // counter) - Ok(AddAuthorityInstruction::new_with_secp256k1_authority( - self.swig_account, - self.payer, - signing_fn, - current_slot, - 0u32, // Session authorities don't use counter-based replay protection - self.role_id, - AuthorityConfig { - authority_type: new_authority_type, - authority: new_authority, - }, - actions, - )?) - }, - } + self.client_role.add_authority_instruction( + self.swig_account, + self.payer, + self.role_id, + new_authority_type, + new_authority, + actions, + current_slot, + ) } /// Creates an instruction to remove an authority from the wallet @@ -301,48 +173,13 @@ impl SwigInstructionBuilder { authority_to_remove_id: u32, current_slot: Option, ) -> Result { - match &mut self.authority_manager { - AuthorityManager::Ed25519(authority) => { - Ok(RemoveAuthorityInstruction::new_with_ed25519_authority( - self.swig_account, - self.payer, - *authority, - self.role_id, - authority_to_remove_id, - )?) - }, - AuthorityManager::Secp256k1(authority, signing_fn) => { - let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; - Ok(RemoveAuthorityInstruction::new_with_secp256k1_authority( - self.swig_account, - self.payer, - signing_fn, - self.role_id, - authority_to_remove_id, - current_slot, - )?) - }, - AuthorityManager::Ed25519Session(session_authority) => { - Ok(RemoveAuthorityInstruction::new_with_ed25519_authority( - self.swig_account, - self.payer, - session_authority.public_key.into(), - self.role_id, - authority_to_remove_id, - )?) - }, - AuthorityManager::Secp256k1Session(session_authority, signing_fn) => { - let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; - Ok(RemoveAuthorityInstruction::new_with_secp256k1_authority( - self.swig_account, - self.payer, - signing_fn, - self.role_id, - authority_to_remove_id, - current_slot, - )?) - }, - } + self.client_role.remove_authority_instruction( + self.swig_account, + self.payer, + self.role_id, + authority_to_remove_id, + current_slot, + ) } /// Creates instructions to replace an existing authority with a new one @@ -365,92 +202,32 @@ impl SwigInstructionBuilder { new_authority: &[u8], permissions: Vec, current_slot: Option, + counter: Option, ) -> Result, SwigError> { let actions = ClientPermission::to_client_actions(permissions); - match &mut self.authority_manager { - AuthorityManager::Ed25519(authority) => { - let remove_authority_instruction = - RemoveAuthorityInstruction::new_with_ed25519_authority( - self.swig_account, - self.payer, - *authority, - self.role_id, - authority_to_replace_id, - )?; - let add_authority_instruction = - AddAuthorityInstruction::new_with_ed25519_authority( - self.swig_account, - self.payer, - *authority, - self.role_id, - AuthorityConfig { - authority_type: new_authority_type, - authority: new_authority, - }, - actions, - )?; - Ok(vec![ - remove_authority_instruction, - add_authority_instruction, - ]) - }, - AuthorityManager::Ed25519Session(session_authority) => { - let authority: Pubkey = session_authority.public_key.into(); - let remove_authority_instruction = - RemoveAuthorityInstruction::new_with_ed25519_authority( - self.swig_account, - self.payer, - authority, - self.role_id, - authority_to_replace_id, - )?; - let add_authority_instruction = - AddAuthorityInstruction::new_with_ed25519_authority( - self.swig_account, - self.payer, - authority, - self.role_id, - AuthorityConfig { - authority_type: new_authority_type, - authority: new_authority, - }, - actions, - )?; - Ok(vec![ - remove_authority_instruction, - add_authority_instruction, - ]) - }, - AuthorityManager::Secp256k1(authority, signing_fn) => { - todo!("Must manually remove and add authority due to Signing Function") - // let current_slot = - // current_slot.ok_or(SwigError::CurrentSlotNotSet)?; - // Ok(vec![ - // RemoveAuthorityInstruction::new_with_secp256k1_authority( - // self.swig_account, - // self.payer, - // signing_fn, - // self.role_id, - // authority_to_replace_id, - // current_slot, - // )?, - // AddAuthorityInstruction::new_with_secp256k1_authority( - // self.swig_account, - // self.payer, - // signing_fn, - // current_slot, - // self.role_id, - // AuthorityConfig { - // authority_type: new_authority_type, - // authority: new_authority, - // }, - // actions, - // )?, - // ]) - }, - _ => todo!(), - } + let remove_authority_instruction = self.client_role.remove_authority_instruction( + self.swig_account, + self.payer, + self.role_id, + authority_to_replace_id, + current_slot, + )?; + + let add_authority_instruction = self.client_role.add_authority_instruction( + self.swig_account, + self.payer, + self.role_id, + new_authority_type, + new_authority, + actions, + current_slot, + )?; + + Ok(vec![ + remove_authority_instruction, + add_authority_instruction, + ]) } /// Creates an instruction to create a new session @@ -470,53 +247,16 @@ impl SwigInstructionBuilder { session_key: Pubkey, session_duration: u64, current_slot: Option, + counter: Option, ) -> Result { - match &self.authority_manager { - AuthorityManager::Ed25519Session(session_authority) => { - Ok(CreateSessionInstruction::new_with_ed25519_authority( - self.swig_account, - self.payer, - session_authority.public_key.into(), - self.role_id, - session_key, - session_duration, - )?) - }, - AuthorityManager::Secp256k1Session(session_authority, signing_fn) => { - let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; - Ok(CreateSessionInstruction::new_with_secp256k1_authority( - self.swig_account, - self.payer, - signing_fn, - current_slot, - self.role_id, - session_key, - session_duration, - )?) - }, - AuthorityManager::Ed25519(authority) => { - Ok(CreateSessionInstruction::new_with_ed25519_authority( - self.swig_account, - self.payer, - *authority, - self.role_id, - session_key, - session_duration, - )?) - }, - AuthorityManager::Secp256k1(authority, signing_fn) => { - let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; - Ok(CreateSessionInstruction::new_with_secp256k1_authority( - self.swig_account, - self.payer, - signing_fn, - current_slot, - self.role_id, - session_key, - session_duration, - )?) - }, - } + self.client_role.create_session_instruction( + self.swig_account, + self.payer, + self.role_id, + session_key, + session_duration, + current_slot, + ) } /// Returns the public key of the Swig account @@ -565,7 +305,7 @@ impl SwigInstructionBuilder { /// # Arguments /// /// * `role_id` - The new role ID to switch to - /// * `authority` - The new authority's public key + /// * `client_role` - The new client role implementation /// /// # Returns /// @@ -573,10 +313,10 @@ impl SwigInstructionBuilder { pub fn switch_authority( &mut self, role_id: u32, - new_authority_manager: AuthorityManager, + client_role: Box, ) -> Result<(), SwigError> { self.role_id = role_id; - self.authority_manager = new_authority_manager; + self.client_role = client_role; Ok(()) } @@ -612,31 +352,14 @@ impl SwigInstructionBuilder { &swig_interface::program_id(), ); - match &self.authority_manager { - AuthorityManager::Ed25519(authority) => { - Ok(CreateSubAccountInstruction::new_with_ed25519_authority( - self.swig_account, - *authority, - self.payer, - sub_account, - self.role_id, - sub_account_bump, - )?) - }, - AuthorityManager::Secp256k1(authority, signing_fn) => { - let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; - Ok(CreateSubAccountInstruction::new_with_secp256k1_authority( - self.swig_account, - self.payer, - signing_fn, - current_slot, - sub_account, - self.role_id, - sub_account_bump, - )?) - }, - _ => todo!(), - } + self.client_role.create_sub_account_instruction( + self.swig_account, + self.payer, + self.role_id, + sub_account, + sub_account_bump, + current_slot, + ) } /// Signs a instruction with sub-account @@ -668,33 +391,14 @@ impl SwigInstructionBuilder { ); println!("Sub-account: {}", sub_account); - match &self.authority_manager { - AuthorityManager::Ed25519(authority) => { - println!("authority: {:?}", &authority); - - Ok(SubAccountSignInstruction::new_with_ed25519_authority( - self.swig_account, - sub_account, - *authority, - self.payer, - self.role_id, - instructions, - )?) - }, - AuthorityManager::Secp256k1(authority, signing_fn) => { - let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; - Ok(SubAccountSignInstruction::new_with_secp256k1_authority( - self.swig_account, - sub_account, - self.payer, - signing_fn, - current_slot, - self.role_id, - instructions, - )?) - }, - _ => todo!(), - } + self.client_role.sub_account_sign_instruction( + self.swig_account, + sub_account, + self.payer, + self.role_id, + instructions, + current_slot, + ) } /// Withdraws funds from a sub-account @@ -714,37 +418,14 @@ impl SwigInstructionBuilder { amount: u64, current_slot: Option, ) -> Result { - match &self.authority_manager { - AuthorityManager::Ed25519(authority) => { - WithdrawFromSubAccountInstruction::new_with_ed25519_authority( - self.swig_account, - *authority, - self.payer, - sub_account, - self.role_id, - amount, - ) - .map_err(|e| { - anyhow::anyhow!("Failed to create withdraw instruction: {:?}", e).into() - }) - }, - AuthorityManager::Secp256k1(authority, signing_fn) => { - let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; - WithdrawFromSubAccountInstruction::new_with_secp256k1_authority( - self.swig_account, - self.payer, - signing_fn, - current_slot, - sub_account, - self.role_id, - amount, - ) - .map_err(|e| { - anyhow::anyhow!("Failed to create withdraw instruction: {:?}", e).into() - }) - }, - _ => todo!(), - } + self.client_role.withdraw_from_sub_account_instruction( + self.swig_account, + self.payer, + sub_account, + self.role_id, + amount, + current_slot, + ) } /// Withdraws tokens from a sub-account @@ -771,43 +452,18 @@ impl SwigInstructionBuilder { amount: u64, current_slot: Option, ) -> Result { - match &self.authority_manager { - AuthorityManager::Ed25519(authority) => { - WithdrawFromSubAccountInstruction::new_token_with_ed25519_authority( - self.swig_account, - *authority, - self.payer, - sub_account, - sub_account_token, - swig_token, - token_program, - self.role_id, - amount, - ) - .map_err(|e| { - anyhow::anyhow!("Failed to create withdraw token instruction: {:?}", e).into() - }) - }, - AuthorityManager::Secp256k1(authority, signing_fn) => { - let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; - WithdrawFromSubAccountInstruction::new_token_with_secp256k1_authority( - self.swig_account, - self.payer, - signing_fn, - current_slot, - sub_account, - sub_account_token, - swig_token, - token_program, - self.role_id, - amount, - ) - .map_err(|e| { - anyhow::anyhow!("Failed to create withdraw token instruction: {:?}", e).into() - }) - }, - _ => todo!(), - } + self.client_role + .withdraw_token_from_sub_account_instruction( + self.swig_account, + self.payer, + sub_account, + sub_account_token, + swig_token, + token_program, + self.role_id, + amount, + current_slot, + ) } /// Toggles a sub-account's enabled state @@ -827,33 +483,14 @@ impl SwigInstructionBuilder { enabled: bool, current_slot: Option, ) -> Result { - match &self.authority_manager { - AuthorityManager::Ed25519(authority) => { - ToggleSubAccountInstruction::new_with_ed25519_authority( - self.swig_account, - *authority, - self.payer, - sub_account, - self.role_id, - enabled, - ) - .map_err(|e| anyhow::anyhow!("Failed to create toggle instruction: {:?}", e).into()) - }, - AuthorityManager::Secp256k1(authority, signing_fn) => { - let current_slot = current_slot.ok_or(SwigError::CurrentSlotNotSet)?; - ToggleSubAccountInstruction::new_with_secp256k1_authority( - self.swig_account, - self.payer, - signing_fn, - current_slot, - sub_account, - self.role_id, - enabled, - ) - .map_err(|e| anyhow::anyhow!("Failed to create toggle instruction: {:?}", e).into()) - }, - _ => todo!(), - } + self.client_role.toggle_sub_account_instruction( + self.swig_account, + self.payer, + sub_account, + self.role_id, + enabled, + current_slot, + ) } /// Returns the current authority's public key as bytes @@ -863,15 +500,23 @@ impl SwigInstructionBuilder { /// Returns a `Result` containing the authority's public key as bytes or a /// `SwigError` pub fn get_current_authority(&self) -> Result, SwigError> { - match &self.authority_manager { - AuthorityManager::Ed25519(authority) => Ok(authority.to_bytes().to_vec()), - AuthorityManager::Secp256k1(authority, _) => Ok(authority[1..].to_vec()), - AuthorityManager::Ed25519Session(session_authority) => { - Ok(session_authority.public_key.to_vec()) - }, - AuthorityManager::Secp256k1Session(session_authority, _) => { - Ok(session_authority.public_key.to_vec()) - }, - } + self.client_role.authority_bytes() + } + + /// Returns the odometer for the current authority if it is a Secp based authority + /// + /// # Returns + /// + /// Returns a `Result` containing the odometer or a `SwigError` + pub fn get_odometer(&self) -> Result { + self.client_role.odometer() + } + + /// Increments the odometer for the current authority if it is Secp based authority + /// + /// + /// + pub fn increment_odometer(&mut self) -> Result<(), SwigError> { + self.client_role.increment_odometer() } } diff --git a/rust-sdk/src/lib.rs b/rust-sdk/src/lib.rs index 922c5502..a70145e3 100644 --- a/rust-sdk/src/lib.rs +++ b/rust-sdk/src/lib.rs @@ -1,4 +1,5 @@ // Public modules +pub mod client_role; pub mod error; pub mod instruction_builder; pub mod types; @@ -6,8 +7,12 @@ pub mod utils; pub mod wallet; // Re-exports for convenient public API +pub use client_role::{ + ClientRole, Ed25519ClientRole, Ed25519SessionClientRole, Secp256k1ClientRole, + Secp256k1SessionClientRole, +}; pub use error::SwigError; -pub use instruction_builder::{AuthorityManager, SwigInstructionBuilder}; +pub use instruction_builder::SwigInstructionBuilder; pub use swig_state_x::{authority, swig}; pub use types::{Permission, RecurringConfig}; pub use utils::*; diff --git a/rust-sdk/src/tests/ix_builder/authority_tests.rs b/rust-sdk/src/tests/ix_builder/authority_tests.rs index 2fa56bbf..8ac9e570 100644 --- a/rust-sdk/src/tests/ix_builder/authority_tests.rs +++ b/rust-sdk/src/tests/ix_builder/authority_tests.rs @@ -1,3 +1,6 @@ +use crate::Ed25519ClientRole; +use crate::Secp256k1ClientRole; + use alloy_primitives::B256; use alloy_signer::SignerSync; use alloy_signer_local::LocalSigner; @@ -27,7 +30,7 @@ fn test_add_authority_with_ed25519_root() { let mut builder = SwigInstructionBuilder::new( swig_id, - AuthorityManager::Ed25519(authority.pubkey()), + Box::new(Ed25519ClientRole::new(authority.pubkey())), context.default_payer.pubkey(), role_id, ); @@ -47,7 +50,6 @@ fn test_add_authority_with_ed25519_root() { &new_authority_bytes, permissions, Some(current_slot), - None, ) .unwrap(); @@ -102,7 +104,7 @@ fn test_add_authority_with_secp256k1_root() { let mut builder = SwigInstructionBuilder::new( swig_id, - AuthorityManager::Secp256k1(secp_pubkey, Box::new(signing_fn)), + Box::new(Secp256k1ClientRole::new(secp_pubkey, Box::new(signing_fn))), payer.pubkey(), role_id, ); @@ -139,7 +141,6 @@ fn test_add_authority_with_secp256k1_root() { // Get current counter for the signing wallet (not the new authority being // added) let current_counter = get_secp256k1_counter_from_wallet(&context, &swig_key, &wallet).unwrap(); - let next_counter = current_counter + 1; let add_auth_ix = builder .add_authority_instruction( @@ -147,7 +148,6 @@ fn test_add_authority_with_secp256k1_root() { &secp_pubkey_bytes, permissions, Some(current_slot), - Some(next_counter), ) .unwrap(); @@ -194,7 +194,7 @@ fn test_remove_authority_with_ed25519_root() { let mut builder = SwigInstructionBuilder::new( swig_id, - AuthorityManager::Ed25519(authority.pubkey()), + Box::new(Ed25519ClientRole::new(authority.pubkey())), payer.pubkey(), role_id, ); @@ -205,7 +205,6 @@ fn test_remove_authority_with_ed25519_root() { &authority_pubkey.to_bytes(), permissions, None, - None, ) .unwrap(); @@ -259,7 +258,7 @@ fn test_switch_authority_and_payer() { let mut builder = SwigInstructionBuilder::new( swig_id, - AuthorityManager::Ed25519(authority.pubkey()), + Box::new(Ed25519ClientRole::new(authority.pubkey())), payer.pubkey(), role_id, ); @@ -268,7 +267,7 @@ fn test_switch_authority_and_payer() { let new_payer = Keypair::new(); builder - .switch_authority(1, AuthorityManager::Ed25519(new_authority.pubkey())) + .switch_authority(1, Box::new(Ed25519ClientRole::new(new_authority.pubkey()))) .unwrap(); assert_eq!(builder.get_role_id(), 1); assert_eq!( diff --git a/rust-sdk/src/tests/ix_builder/mod.rs b/rust-sdk/src/tests/ix_builder/mod.rs index 671a2a46..31200b1f 100644 --- a/rust-sdk/src/tests/ix_builder/mod.rs +++ b/rust-sdk/src/tests/ix_builder/mod.rs @@ -37,8 +37,7 @@ use swig_state_x::{ use super::*; use crate::{ - error::SwigError, instruction_builder::AuthorityManager, types::Permission, RecurringConfig, - SwigInstructionBuilder, SwigWallet, + error::SwigError, types::Permission, RecurringConfig, SwigInstructionBuilder, SwigWallet, }; pub mod authority_tests; @@ -47,3 +46,155 @@ pub mod session_tests; pub mod sub_account_test; pub mod swig_account_tests; pub mod transfer_tests; + +use solana_sdk::account::Account; +pub fn display_swig(swig_pubkey: Pubkey, swig_account: &Account) -> Result<(), SwigError> { + let swig_with_roles = + SwigWithRoles::from_bytes(&swig_account.data).map_err(|e| SwigError::InvalidSwigData)?; + + println!("╔══════════════════════════════════════════════════════════════════"); + println!("║ SWIG WALLET DETAILS"); + println!("╠══════════════════════════════════════════════════════════════════"); + println!("║ Account Address: {}", swig_pubkey); + println!("║ Total Roles: {}", swig_with_roles.state.role_counter); + println!( + "║ Balance: {} SOL", + swig_account.lamports() as f64 / 1_000_000_000.0 + ); + + println!("╠══════════════════════════════════════════════════════════════════"); + println!("║ ROLES & PERMISSIONS"); + println!("╠══════════════════════════════════════════════════════════════════"); + + for i in 0..swig_with_roles.state.role_counter { + let role = swig_with_roles + .get_role(i) + .map_err(|e| SwigError::AuthorityNotFound)?; + + if let Some(role) = role { + println!("║"); + println!("║ Role ID: {}", i); + println!( + "║ ├─ Type: {}", + if role.authority.session_based() { + "Session-based Authority" + } else { + "Permanent Authority" + } + ); + println!("║ ├─ Authority Type: {:?}", role.authority.authority_type()); + println!( + "║ ├─ Authority: {}", + match role.authority.authority_type() { + AuthorityType::Ed25519 | AuthorityType::Ed25519Session => { + let authority = role.authority.identity().unwrap(); + let authority = bs58::encode(authority).into_string(); + authority + }, + AuthorityType::Secp256k1 | AuthorityType::Secp256k1Session => { + let authority = role.authority.identity().unwrap(); + let authority_hex = hex::encode([&[0x4].as_slice(), authority].concat()); + // get eth address from public key + let mut hasher = solana_sdk::keccak::Hasher::default(); + hasher.hash(authority_hex.as_bytes()); + let hash = hasher.result(); + let address = format!("0x{}", hex::encode(&hash.0[12..32])); + format!( + "{} \n║ │ ├─ odometer: {:?}", + address, + role.authority.signature_odometer() + ) + }, + _ => todo!(), + } + ); + + println!("║ ├─ Permissions:"); + + // Check All permission + if (Role::get_action::(&role, &[]).map_err(|_| SwigError::AuthorityNotFound)?) + .is_some() + { + println!("║ │ ├─ Full Access (All Permissions)"); + } + + // Check Manage Authority permission + if (Role::get_action::(&role, &[]) + .map_err(|_| SwigError::AuthorityNotFound)?) + .is_some() + { + println!("║ │ ├─ Manage Authority"); + } + + // Check Sol Limit + if let Some(action) = Role::get_action::(&role, &[]) + .map_err(|_| SwigError::AuthorityNotFound)? + { + println!( + "║ │ ├─ SOL Limit: {} SOL", + action.amount as f64 / 1_000_000_000.0 + ); + } + + // Check Sol Recurring Limit + if let Some(action) = Role::get_action::(&role, &[]) + .map_err(|_| SwigError::AuthorityNotFound)? + { + println!("║ │ ├─ Recurring SOL Limit:"); + println!( + "║ │ │ ├─ Amount: {} SOL", + action.recurring_amount as f64 / 1_000_000_000.0 + ); + println!("║ │ │ ├─ Window: {} slots", action.window); + println!( + "║ │ │ ├─ Current Usage: {} SOL", + action.current_amount as f64 / 1_000_000_000.0 + ); + println!("║ │ │ └─ Last Reset: Slot {}", action.last_reset); + } + + // Check Program Scope + if let Some(action) = Role::get_action::(&role, &spl_token::ID.to_bytes()) + .map_err(|_| SwigError::AuthorityNotFound)? + { + let program_id = Pubkey::from(action.program_id); + let target_account = Pubkey::from(action.target_account); + println!("║ │ ├─ Program Scope"); + println!("║ │ │ ├─ Program ID: {}", program_id); + println!("║ │ │ ├─ Target Account: {}", target_account); + println!( + "║ │ │ ├─ Scope Type: {}", + match action.scope_type { + 0 => "Basic", + 1 => "Limit", + 2 => "Recurring Limit", + _ => "Unknown", + } + ); + println!( + "║ │ │ ├─ Numeric Type: {}", + match action.numeric_type { + 0 => "U64", + 1 => "U128", + 2 => "F64", + _ => "Unknown", + } + ); + if action.scope_type > 0 { + println!("║ │ │ ├─ Limit: {} ", action.limit); + println!("║ │ │ ├─ Current Usage: {} ", action.current_amount); + } + if action.scope_type == 2 { + println!("║ │ │ ├─ Window: {} slots", action.window); + println!("║ │ │ ├─ Last Reset: Slot {}", action.last_reset); + } + println!("║ │ │ "); + } + println!("║ │ "); + } + } + + println!("╚══════════════════════════════════════════════════════════════════"); + + Ok(()) +} diff --git a/rust-sdk/src/tests/ix_builder/program_scope_tests.rs b/rust-sdk/src/tests/ix_builder/program_scope_tests.rs index f74203fe..5f0ced6c 100644 --- a/rust-sdk/src/tests/ix_builder/program_scope_tests.rs +++ b/rust-sdk/src/tests/ix_builder/program_scope_tests.rs @@ -19,6 +19,8 @@ use swig_state_x::{ }; use super::*; +use crate::client_role::Ed25519ClientRole; +use crate::tests::common::{mint_to, setup_ata, setup_mint}; #[test_log::test] fn test_token_transfer_with_program_scope() { @@ -77,7 +79,7 @@ fn test_token_transfer_with_program_scope() { let mut ix_builder = SwigInstructionBuilder::new( id, - AuthorityManager::Ed25519(swig_authority.pubkey()), + Box::new(Ed25519ClientRole::new(swig_authority.pubkey())), context.default_payer.pubkey(), 0, ); @@ -91,7 +93,6 @@ fn test_token_transfer_with_program_scope() { &new_authority.pubkey().to_bytes(), permissions, None, - None, ) .unwrap(); @@ -140,7 +141,7 @@ fn test_token_transfer_with_program_scope() { let mut new_authority_ix = SwigInstructionBuilder::new( id, - AuthorityManager::Ed25519(new_authority.pubkey()), + Box::new(Ed25519ClientRole::new(new_authority.pubkey())), context.default_payer.pubkey(), 1, ); @@ -149,7 +150,6 @@ fn test_token_transfer_with_program_scope() { .sign_instruction( vec![swig_transfer_ix], Some(context.svm.get_sysvar::().slot), - None, ) .unwrap(); @@ -236,7 +236,7 @@ fn test_recurring_limit_program_scope() { let mut ix_builder = SwigInstructionBuilder::new( id, - AuthorityManager::Ed25519(swig_authority.pubkey()), + Box::new(Ed25519ClientRole::new(swig_authority.pubkey())), context.default_payer.pubkey(), 0, ); @@ -249,7 +249,6 @@ fn test_recurring_limit_program_scope() { &new_authority.pubkey().to_bytes(), permissions, None, - None, ) .unwrap(); @@ -290,7 +289,7 @@ fn test_recurring_limit_program_scope() { let mut new_ix_builder = SwigInstructionBuilder::new( id, - AuthorityManager::Ed25519(new_authority.pubkey()), + Box::new(Ed25519ClientRole::new(new_authority.pubkey())), context.default_payer.pubkey(), 1, ); @@ -310,7 +309,7 @@ fn test_recurring_limit_program_scope() { let current_slot = context.svm.get_sysvar::().slot; let sign_ix = new_ix_builder - .sign_instruction(vec![transfer_ix.clone()], Some(current_slot), None) + .sign_instruction(vec![transfer_ix.clone()], Some(current_slot)) .unwrap(); let transfer_message = v0::Message::try_compile( @@ -342,7 +341,6 @@ fn test_recurring_limit_program_scope() { .sign_instruction( vec![transfer_ix], Some(context.svm.get_sysvar::().slot), - None, ) .unwrap(); @@ -386,7 +384,6 @@ fn test_recurring_limit_program_scope() { .sign_instruction( vec![transfer_ix], Some(context.svm.get_sysvar::().slot), - None, ) .unwrap(); @@ -411,151 +408,3 @@ fn test_recurring_limit_program_scope() { transfer_result.err() ); } - -use solana_sdk::account::Account; -pub fn display_swig(swig_pubkey: Pubkey, swig_account: &Account) -> Result<(), SwigError> { - let swig_with_roles = - SwigWithRoles::from_bytes(&swig_account.data).map_err(|e| SwigError::InvalidSwigData)?; - - println!("╔══════════════════════════════════════════════════════════════════"); - println!("║ SWIG WALLET DETAILS"); - println!("╠══════════════════════════════════════════════════════════════════"); - println!("║ Account Address: {}", swig_pubkey); - println!("║ Total Roles: {}", swig_with_roles.state.role_counter); - println!( - "║ Balance: {} SOL", - swig_account.lamports() as f64 / 1_000_000_000.0 - ); - - println!("╠══════════════════════════════════════════════════════════════════"); - println!("║ ROLES & PERMISSIONS"); - println!("╠══════════════════════════════════════════════════════════════════"); - - for i in 0..swig_with_roles.state.role_counter { - let role = swig_with_roles - .get_role(i) - .map_err(|e| SwigError::AuthorityNotFound)?; - - if let Some(role) = role { - println!("║"); - println!("║ Role ID: {}", i); - println!( - "║ ├─ Type: {}", - if role.authority.session_based() { - "Session-based Authority" - } else { - "Permanent Authority" - } - ); - println!("║ ├─ Authority Type: {:?}", role.authority.authority_type()); - println!( - "║ ├─ Authority: {}", - match role.authority.authority_type() { - AuthorityType::Ed25519 | AuthorityType::Ed25519Session => { - let authority = role.authority.identity().unwrap(); - let authority = bs58::encode(authority).into_string(); - authority - }, - AuthorityType::Secp256k1 | AuthorityType::Secp256k1Session => { - let authority = role.authority.identity().unwrap(); - let authority_hex = hex::encode([&[0x4].as_slice(), authority].concat()); - // get eth address from public key - let mut hasher = solana_sdk::keccak::Hasher::default(); - hasher.hash(authority_hex.as_bytes()); - let hash = hasher.result(); - let address = format!("0x{}", hex::encode(&hash.0[12..32])); - address - }, - _ => todo!(), - } - ); - - println!("║ ├─ Permissions:"); - - // Check All permission - if (Role::get_action::(&role, &[]).map_err(|_| SwigError::AuthorityNotFound)?) - .is_some() - { - println!("║ │ ├─ Full Access (All Permissions)"); - } - - // Check Manage Authority permission - if (Role::get_action::(&role, &[]) - .map_err(|_| SwigError::AuthorityNotFound)?) - .is_some() - { - println!("║ │ ├─ Manage Authority"); - } - - // Check Sol Limit - if let Some(action) = Role::get_action::(&role, &[]) - .map_err(|_| SwigError::AuthorityNotFound)? - { - println!( - "║ │ ├─ SOL Limit: {} SOL", - action.amount as f64 / 1_000_000_000.0 - ); - } - - // Check Sol Recurring Limit - if let Some(action) = Role::get_action::(&role, &[]) - .map_err(|_| SwigError::AuthorityNotFound)? - { - println!("║ │ ├─ Recurring SOL Limit:"); - println!( - "║ │ │ ├─ Amount: {} SOL", - action.recurring_amount as f64 / 1_000_000_000.0 - ); - println!("║ │ │ ├─ Window: {} slots", action.window); - println!( - "║ │ │ ├─ Current Usage: {} SOL", - action.current_amount as f64 / 1_000_000_000.0 - ); - println!("║ │ │ └─ Last Reset: Slot {}", action.last_reset); - } - - // Check Program Scope - if let Some(action) = Role::get_action::(&role, &spl_token::ID.to_bytes()) - .map_err(|_| SwigError::AuthorityNotFound)? - { - let program_id = Pubkey::from(action.program_id); - let target_account = Pubkey::from(action.target_account); - println!("║ │ ├─ Program Scope"); - println!("║ │ │ ├─ Program ID: {}", program_id); - println!("║ │ │ ├─ Target Account: {}", target_account); - println!( - "║ │ │ ├─ Scope Type: {}", - match action.scope_type { - 0 => "Basic", - 1 => "Limit", - 2 => "Recurring Limit", - _ => "Unknown", - } - ); - println!( - "║ │ │ ├─ Numeric Type: {}", - match action.numeric_type { - 0 => "U64", - 1 => "U128", - 2 => "F64", - _ => "Unknown", - } - ); - if action.scope_type > 0 { - println!("║ │ │ ├─ Limit: {} ", action.limit); - println!("║ │ │ ├─ Current Usage: {} ", action.current_amount); - } - if action.scope_type == 2 { - println!("║ │ │ ├─ Window: {} slots", action.window); - println!("║ │ │ ├─ Last Reset: Slot {}", action.last_reset); - } - println!("║ │ │ "); - } - println!("║ │ "); - } - } - - println!("╚══════════════════════════════════════════════════════════════════"); - - Ok(()) -} diff --git a/rust-sdk/src/tests/ix_builder/session_tests.rs b/rust-sdk/src/tests/ix_builder/session_tests.rs index b1632f08..985f83eb 100644 --- a/rust-sdk/src/tests/ix_builder/session_tests.rs +++ b/rust-sdk/src/tests/ix_builder/session_tests.rs @@ -18,6 +18,7 @@ use swig_state_x::{ }; use super::*; +use crate::client_role::{Ed25519SessionClientRole, Secp256k1SessionClientRole}; #[test_log::test] fn test_create_ed25519_session() { @@ -33,10 +34,8 @@ fn test_create_ed25519_session() { let mut swig_ix_builder = SwigInstructionBuilder::new( id, - AuthorityManager::Ed25519Session(CreateEd25519SessionAuthority::new( - swig_authority.pubkey().to_bytes(), - [0; 32], - 100, + Box::new(Ed25519SessionClientRole::new( + CreateEd25519SessionAuthority::new(swig_authority.pubkey().to_bytes(), [0; 32], 100), )), context.default_payer.pubkey(), 0, @@ -85,7 +84,7 @@ fn test_create_ed25519_session() { // Create a session let session_authority = Keypair::new(); let create_session_ix = swig_ix_builder - .create_session_instruction(session_authority.pubkey(), 100, None) + .create_session_instruction(session_authority.pubkey(), 100, None, None) .unwrap(); let msg = v0::Message::try_compile( @@ -146,14 +145,14 @@ fn test_create_secp256k1_session() { let mut swig_ix_builder = SwigInstructionBuilder::new( id, - AuthorityManager::Secp256k1Session( + Box::new(Secp256k1SessionClientRole::new( CreateSecp256k1SessionAuthority::new( secp_pubkey[1..].try_into().unwrap(), [0; 32], 100, ), Box::new(signing_fn), - ), + )), context.default_payer.pubkey(), 0, ); @@ -178,6 +177,15 @@ fn test_create_secp256k1_session() { result.err() ); + display_swig( + swig_ix_builder.get_swig_account().unwrap(), + &context + .svm + .get_account(&swig_ix_builder.get_swig_account().unwrap()) + .unwrap(), + ) + .unwrap(); + let swig_key = swig_ix_builder.get_swig_account().unwrap(); context.svm.airdrop(&swig_key, 50_000_000_000).unwrap(); @@ -202,8 +210,14 @@ fn test_create_secp256k1_session() { let session_authority = Keypair::new(); let current_slot = context.svm.get_sysvar::().slot; + let counter = 1; let create_session_ix = swig_ix_builder - .create_session_instruction(session_authority.pubkey(), 100, Some(current_slot)) + .create_session_instruction( + session_authority.pubkey(), + 100, + Some(current_slot), + Some(counter), + ) .unwrap(); let msg = v0::Message::try_compile( @@ -224,6 +238,15 @@ fn test_create_secp256k1_session() { result.err() ); + display_swig( + swig_ix_builder.get_swig_account().unwrap(), + &context + .svm + .get_account(&swig_ix_builder.get_swig_account().unwrap()) + .unwrap(), + ) + .unwrap(); + let swig_account = context.svm.get_account(&swig_key).unwrap(); let swig_data = swig_account.data; diff --git a/rust-sdk/src/tests/ix_builder/sub_account_test.rs b/rust-sdk/src/tests/ix_builder/sub_account_test.rs index af443d3d..08c7c325 100644 --- a/rust-sdk/src/tests/ix_builder/sub_account_test.rs +++ b/rust-sdk/src/tests/ix_builder/sub_account_test.rs @@ -1,5 +1,6 @@ use alloy_primitives::B256; use alloy_signer::SignerSync; +use alloy_signer_local::LocalSigner; use litesvm_token::spl_token; use solana_program::pubkey::Pubkey; use solana_sdk::{ @@ -15,6 +16,7 @@ use swig_state_x::{ }; use super::*; +use crate::client_role::Ed25519ClientRole; use crate::tests::common::*; #[test_log::test] @@ -43,7 +45,7 @@ fn test_sub_account_functionality() { // Create instruction builder with root authority let mut builder = SwigInstructionBuilder::new( swig_id, - AuthorityManager::Ed25519(root_authority.pubkey()), + Box::new(Ed25519ClientRole::new(root_authority.pubkey())), context.default_payer.pubkey(), role_id, ); @@ -57,7 +59,6 @@ fn test_sub_account_functionality() { sub_account: [0; 32], }], None, - None, ) .unwrap(); @@ -81,7 +82,7 @@ fn test_sub_account_functionality() { let sub_account_role_id = 1; // The sub-account authority has role_id 1 let mut sub_account_builder = SwigInstructionBuilder::new( swig_id, - AuthorityManager::Ed25519(sub_account_authority.pubkey()), + Box::new(Ed25519ClientRole::new(sub_account_authority.pubkey())), context.default_payer.pubkey(), sub_account_role_id, ); diff --git a/rust-sdk/src/tests/ix_builder/swig_account_tests.rs b/rust-sdk/src/tests/ix_builder/swig_account_tests.rs index 3b6f5b1d..8ed9f24d 100644 --- a/rust-sdk/src/tests/ix_builder/swig_account_tests.rs +++ b/rust-sdk/src/tests/ix_builder/swig_account_tests.rs @@ -8,9 +8,13 @@ use solana_sdk::{ transaction::VersionedTransaction, }; use swig_interface::program_id; -use swig_state_x::swig::{swig_account_seeds, SwigWithRoles}; +use swig_state_x::{ + authority::AuthorityType, + swig::{swig_account_seeds, SwigWithRoles}, +}; use super::*; +use crate::client_role::{Ed25519ClientRole, Secp256k1ClientRole}; #[test_log::test] fn test_create_swig_account_with_ed25519_authority() { @@ -22,7 +26,7 @@ fn test_create_swig_account_with_ed25519_authority() { let builder = SwigInstructionBuilder::new( swig_id, - AuthorityManager::Ed25519(authority.pubkey()), + Box::new(Ed25519ClientRole::new(authority.pubkey())), payer.pubkey(), role_id, ); @@ -69,7 +73,10 @@ fn test_create_swig_account_with_secp256k1_authority() { let builder = SwigInstructionBuilder::new( swig_id, - AuthorityManager::Secp256k1(secp_pubkey, Box::new(|_| [0u8; 65])), + Box::new(Secp256k1ClientRole::new( + secp_pubkey, + Box::new(|_| [0u8; 65]), + )), payer.pubkey(), role_id, ); diff --git a/rust-sdk/src/tests/ix_builder/transfer_tests.rs b/rust-sdk/src/tests/ix_builder/transfer_tests.rs index f50cf48b..88839a8c 100644 --- a/rust-sdk/src/tests/ix_builder/transfer_tests.rs +++ b/rust-sdk/src/tests/ix_builder/transfer_tests.rs @@ -14,6 +14,7 @@ use swig_state_x::{ }; use super::*; +use crate::client_role::{Ed25519ClientRole, Secp256k1ClientRole}; #[test_log::test] fn test_sign_instruction_with_ed25519_authority() { @@ -26,7 +27,7 @@ fn test_sign_instruction_with_ed25519_authority() { let builder = SwigInstructionBuilder::new( swig_id, - AuthorityManager::Ed25519(authority.pubkey()), + Box::new(Ed25519ClientRole::new(authority.pubkey())), payer.pubkey(), role_id, ); @@ -51,7 +52,7 @@ fn test_sign_instruction_with_ed25519_authority() { let mut builder = SwigInstructionBuilder::new( swig_id, - AuthorityManager::Ed25519(authority.pubkey()), + Box::new(Ed25519ClientRole::new(authority.pubkey())), context.default_payer.pubkey(), role_id, ); @@ -68,7 +69,7 @@ fn test_sign_instruction_with_ed25519_authority() { let current_slot = context.svm.get_sysvar::().slot; let sign_ix = builder - .sign_instruction(vec![transfer_ix], Some(current_slot), None) + .sign_instruction(vec![transfer_ix], Some(current_slot)) .unwrap(); let msg = v0::Message::try_compile( @@ -122,7 +123,7 @@ fn test_sign_instruction_with_secp256k1_authority() { let mut builder = SwigInstructionBuilder::new( swig_id, - AuthorityManager::Secp256k1(secp_pubkey, Box::new(signing_fn)), + Box::new(Secp256k1ClientRole::new(secp_pubkey, Box::new(signing_fn))), payer.pubkey(), role_id, ); @@ -155,10 +156,9 @@ fn test_sign_instruction_with_secp256k1_authority() { // Get current counter and calculate next counter let current_counter = get_secp256k1_counter_from_wallet(&context, &swig_key, &wallet).unwrap(); - let next_counter = current_counter + 1; let sign_ix = builder - .sign_instruction(vec![transfer_ix], Some(current_slot), Some(next_counter)) + .sign_instruction(vec![transfer_ix], Some(current_slot)) .unwrap(); let msg = v0::Message::try_compile( diff --git a/rust-sdk/src/tests/swig_ix_test.rs b/rust-sdk/src/tests/swig_ix_test.rs deleted file mode 100644 index 4079334d..00000000 --- a/rust-sdk/src/tests/swig_ix_test.rs +++ /dev/null @@ -1,1541 +0,0 @@ -use alloy_primitives::B256; -use alloy_signer::SignerSync; -use alloy_signer_local::LocalSigner; -use common::*; -use litesvm::{types::TransactionMetadata, LiteSVM}; -use litesvm_token::spl_token; -use solana_program::{pubkey::Pubkey, system_program}; -use solana_sdk::{ - account::ReadableAccount, - clock::Clock, - message::{v0, VersionedMessage}, - signature::Keypair, - signer::Signer, - system_instruction, - transaction::VersionedTransaction, -}; -use swig_interface::{ - program_id, AuthorityConfig, ClientAction, CreateInstruction, CreateSessionInstruction, - SignInstruction, -}; -use swig_state_x::{ - action::{ - all::All, manage_authority::ManageAuthority, program_scope::ProgramScope, - sol_limit::SolLimit, sol_recurring_limit::SolRecurringLimit, - }, - authority::{ - ed25519::{CreateEd25519SessionAuthority, ED25519Authority, Ed25519SessionAuthority}, - secp256k1::{ - CreateSecp256k1SessionAuthority, Secp256k1Authority, Secp256k1SessionAuthority, - }, - AuthorityType, - }, - role::Role, - swig::{swig_account_seeds, SwigWithRoles}, - IntoBytes, -}; - -use super::*; -use crate::{ - error::SwigError, instruction_builder::AuthorityManager, types::Permission, RecurringConfig, - SwigInstructionBuilder, SwigWallet, -}; - -#[test_log::test] -fn test_create_swig_account_with_ed25519_authority() { - let mut context = setup_test_context().unwrap(); - let swig_id = [1u8; 32]; - let authority = Keypair::new(); - let payer = context.default_payer; - let role_id = 0; - - let builder = SwigInstructionBuilder::new( - swig_id, - AuthorityManager::Ed25519(authority.pubkey()), - payer.pubkey(), - role_id, - ); - - let ix = builder.build_swig_account().unwrap(); - - let msg = v0::Message::try_compile(&payer.pubkey(), &[ix], &[], context.svm.latest_blockhash()) - .unwrap(); - - let tx = VersionedTransaction::try_new(VersionedMessage::V0(msg), &[payer]).unwrap(); - - let result = context.svm.send_transaction(tx); - assert!( - result.is_ok(), - "Failed to create Swig account: {:?}", - result.err() - ); - - // Verify the account was created correctly - let (swig_key, _) = Pubkey::find_program_address(&swig_account_seeds(&swig_id), &program_id()); - let swig_account = context.svm.get_account(&swig_key).unwrap(); - let swig_data = SwigWithRoles::from_bytes(&swig_account.data).unwrap(); - let root_role = swig_data.get_role(0).unwrap().unwrap(); - - assert_eq!(swig_data.state.id, swig_id); - assert_eq!(swig_data.state.roles, 1); -} - -#[test_log::test] -fn test_create_swig_account_with_secp256k1_authority() { - let mut context = setup_test_context().unwrap(); - let swig_id = [1u8; 32]; - - let wallet = LocalSigner::random(); - - let secp_pubkey = wallet - .credential() - .verifying_key() - .to_encoded_point(false) - .to_bytes(); - - let payer = &context.default_payer; - let role_id = 0; - - let builder = SwigInstructionBuilder::new( - swig_id, - AuthorityManager::Secp256k1(secp_pubkey, Box::new(|_| [0u8; 65])), - payer.pubkey(), - role_id, - ); - - let ix = builder.build_swig_account().unwrap(); - let msg = v0::Message::try_compile(&payer.pubkey(), &[ix], &[], context.svm.latest_blockhash()) - .unwrap(); - - let tx = VersionedTransaction::try_new(VersionedMessage::V0(msg), &[payer]).unwrap(); - - let result = context.svm.send_transaction(tx); - assert!( - result.is_ok(), - "Failed to create Swig account: {:?}", - result.err() - ); - - // Verify the account was created correctly - let (swig_key, _) = Pubkey::find_program_address(&swig_account_seeds(&swig_id), &program_id()); - let swig_account = context.svm.get_account(&swig_key).unwrap(); - let swig_data = SwigWithRoles::from_bytes(&swig_account.data).unwrap(); - let root_role = swig_data.get_role(0).unwrap().unwrap(); - - assert_eq!(swig_data.state.id, swig_id); - assert_eq!(swig_data.state.roles, 1); - assert_eq!( - root_role.authority.authority_type(), - AuthorityType::Secp256k1 - ); -} - -#[test_log::test] -fn test_sign_instruction_with_ed25519_authority() { - // First create the Swig account - let mut context = setup_test_context().unwrap(); - let swig_id = [1u8; 32]; - let authority = Keypair::new(); - let payer = &context.default_payer; - let role_id = 0; - - let builder = SwigInstructionBuilder::new( - swig_id, - AuthorityManager::Ed25519(authority.pubkey()), - payer.pubkey(), - role_id, - ); - - let ix = builder.build_swig_account().unwrap(); - let msg = v0::Message::try_compile(&payer.pubkey(), &[ix], &[], context.svm.latest_blockhash()) - .unwrap(); - - let tx = VersionedTransaction::try_new(VersionedMessage::V0(msg), &[payer]).unwrap(); - - let result = context.svm.send_transaction(tx); - assert!( - result.is_ok(), - "Failed to create Swig account: {:?}", - result.err() - ); - - let swig_key = builder.get_swig_account().unwrap(); - - // Fund the Swig account - context.svm.airdrop(&swig_key, 1_000_000_000).unwrap(); - - let mut builder = SwigInstructionBuilder::new( - swig_id, - AuthorityManager::Ed25519(authority.pubkey()), - context.default_payer.pubkey(), - role_id, - ); - - // Create a transfer instruction to test signing - let recipient = Keypair::new(); - let transfer_amount = 100_000; - let transfer_ix = solana_program::system_instruction::transfer( - &swig_key, - &recipient.pubkey(), - transfer_amount, - ); - - let current_slot = context.svm.get_sysvar::().slot; - - let sign_ix = builder - .sign_instruction(vec![transfer_ix], Some(current_slot)) - .unwrap(); - - let msg = v0::Message::try_compile( - &context.default_payer.pubkey(), - &sign_ix, - &[], - context.svm.latest_blockhash(), - ) - .unwrap(); - - let tx = VersionedTransaction::try_new( - VersionedMessage::V0(msg), - &[&context.default_payer, &authority], - ); - - assert!(tx.is_ok(), "Failed to create transaction {:?}", tx.err()); - - let result = context.svm.send_transaction(tx.unwrap()); - assert!( - result.is_ok(), - "Failed to execute signed instruction: {:?}", - result.err() - ); - - // Verify the transfer was successful - let recipient_account = context.svm.get_account(&recipient.pubkey()).unwrap(); - assert_eq!(recipient_account.lamports, transfer_amount); -} - -#[test_log::test] -fn test_add_authority_with_ed25519_root() { - let mut context = setup_test_context().unwrap(); - let swig_id = [3u8; 32]; - let authority = Keypair::new(); - let role_id = 0; - - // First create the Swig account - let (swig_key, _) = create_swig_ed25519(&mut context, &authority, swig_id).unwrap(); - - let mut builder = SwigInstructionBuilder::new( - swig_id, - AuthorityManager::Ed25519(authority.pubkey()), - context.default_payer.pubkey(), - role_id, - ); - - let new_authority = Keypair::new(); - let new_authority_bytes = new_authority.pubkey().to_bytes(); - let permissions = vec![Permission::Sol { - amount: 100000 / 2, - recurring: None, - }]; - - let current_slot = context.svm.get_sysvar::().slot; - - let add_auth_ix = builder - .add_authority_instruction( - AuthorityType::Ed25519, - &new_authority_bytes, - permissions, - Some(current_slot), - ) - .unwrap(); - - let msg = v0::Message::try_compile( - &context.default_payer.pubkey(), - &[add_auth_ix], - &[], - context.svm.latest_blockhash(), - ) - .unwrap(); - - let tx = VersionedTransaction::try_new( - VersionedMessage::V0(msg), - &[&context.default_payer, &authority], - ) - .unwrap(); - - let result = context.svm.send_transaction(tx); - assert!( - result.is_ok(), - "Failed to add authority: {:?}", - result.err() - ); - - // Verify the new 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); // Root authority + new authority -} - -#[test_log::test] -fn test_add_authority_and_transfer_with_ed25519_root() { - let mut context = setup_test_context().unwrap(); - let swig_id = [4u8; 32]; - let authority = Keypair::new(); - let role_id = 0; - - let (swig_key, _) = create_swig_ed25519(&mut context, &authority, swig_id).unwrap(); - - context.svm.airdrop(&swig_key, 100_000_000_000).unwrap(); - - let mut builder = SwigInstructionBuilder::new( - swig_id, - AuthorityManager::Ed25519(authority.pubkey()), - context.default_payer.pubkey(), - role_id, - ); - - let new_authority = Keypair::new(); - let new_authority_bytes = new_authority.pubkey().to_bytes(); - let permissions = vec![Permission::Sol { - amount: 1_000_000_000, - recurring: None, - }]; - - let recipient = Keypair::new(); - - let current_slot = context.svm.get_sysvar::().slot; - - let add_auth_ix = builder - .add_authority_instruction( - AuthorityType::Ed25519, - &new_authority_bytes, - permissions, - Some(current_slot), - ) - .unwrap(); - - let msg = v0::Message::try_compile( - &context.default_payer.pubkey(), - &[add_auth_ix], - &[], - context.svm.latest_blockhash(), - ) - .unwrap(); - - let tx = VersionedTransaction::try_new( - VersionedMessage::V0(msg), - &[&context.default_payer, &authority], - ) - .unwrap(); - - let result = context.svm.send_transaction(tx); - - assert!( - result.is_ok(), - "Failed to add authority: {:?}", - result.err() - ); - - let transfer_ix = - solana_program::system_instruction::transfer(&swig_key, &recipient.pubkey(), 100000); - - let current_slot = context.svm.get_sysvar::().slot; - - let sign_ix = builder - .sign_instruction(vec![transfer_ix], Some(current_slot)) - .unwrap(); - - let msg = v0::Message::try_compile( - &context.default_payer.pubkey(), - &sign_ix, - &[], - context.svm.latest_blockhash(), - ) - .unwrap(); - - let tx = VersionedTransaction::try_new( - VersionedMessage::V0(msg), - &[&context.default_payer, &authority], - ) - .unwrap(); - - let result = context.svm.send_transaction(tx); - - assert!( - result.is_ok(), - "Failed to execute signed instruction: {:?}", - result.err() - ); - - let recipient_account = context.svm.get_account(&recipient.pubkey()).unwrap(); - assert_eq!(recipient_account.lamports, 100000); -} - -#[test_log::test] -fn test_create_ed25519_session_with_add_authority() { - let mut context = setup_test_context().unwrap(); - let swig_id = [5u8; 32]; - let authority = Keypair::new(); - let session_key = Keypair::new(); - - let mut builder = SwigInstructionBuilder::new( - swig_id, - AuthorityManager::Ed25519Session(CreateEd25519SessionAuthority::new( - authority.pubkey().to_bytes(), - [0; 32], - 100, - )), - context.default_payer.pubkey(), - 0, - ); - - let ix = builder.build_swig_account().unwrap(); - let msg = v0::Message::try_compile( - &context.default_payer.pubkey(), - &[ix], - &[], - context.svm.latest_blockhash(), - ) - .unwrap(); - - let tx = VersionedTransaction::try_new(VersionedMessage::V0(msg), &[&context.default_payer]) - .unwrap(); - - let result = context.svm.send_transaction(tx); - assert!( - result.is_ok(), - "Failed to create Swig account: {:?}", - result.err() - ); - - let swig_key = builder.get_swig_account().unwrap(); - - // start a session - let session_ix = builder - .create_session_instruction(session_key.pubkey(), 100, None) - .unwrap(); - - let msg = v0::Message::try_compile( - &context.default_payer.pubkey(), - &[session_ix], - &[], - context.svm.latest_blockhash(), - ) - .unwrap(); - - let tx_session = VersionedTransaction::try_new( - VersionedMessage::V0(msg), - &[&context.default_payer, &authority], - ) - .unwrap(); - - let result = context.svm.send_transaction(tx_session); - assert!( - result.is_ok(), - "Failed to create session: {:?}", - result.err() - ); -} - -#[test_log::test] -fn test_add_authority_and_transfer_with_secp256k1_root() { - // First create the Swig account - let mut context = setup_test_context().unwrap(); - let swig_id = [1u8; 32]; - let payer = &context.default_payer; - let role_id = 0; - - // Create Swig Wallet with Secp256k1 authority - let wallet = LocalSigner::random(); - let secp_pubkey = wallet - .credential() - .verifying_key() - .to_encoded_point(false) - .to_bytes(); - - let mut sig = [0u8; 65]; - let wallet = wallet.clone(); - let signing_fn = move |payload: &[u8]| -> [u8; 65] { - let mut hash = [0u8; 32]; - hash.copy_from_slice(&payload[..32]); - let hash = B256::from(hash); - wallet.sign_hash_sync(&hash).unwrap().as_bytes() - }; - - let mut builder = SwigInstructionBuilder::new( - swig_id, - AuthorityManager::Secp256k1(secp_pubkey.clone(), Box::new(signing_fn.clone())), - payer.pubkey(), - role_id, - ); - - let ix = builder.build_swig_account().unwrap(); - let msg = v0::Message::try_compile(&payer.pubkey(), &[ix], &[], context.svm.latest_blockhash()) - .unwrap(); - - let tx = VersionedTransaction::try_new(VersionedMessage::V0(msg), &[payer]).unwrap(); - - let result = context.svm.send_transaction(tx); - assert!( - result.is_ok(), - "Failed to create Swig account: {:?}", - result.err() - ); - - // Add a new authority to the Swig account - let swig_key = builder.get_swig_account().unwrap(); - - let new_authority = LocalSigner::random(); - let new_secp_pubkey = new_authority - .credential() - .verifying_key() - .to_encoded_point(false) - .to_bytes(); - - let permissions = vec![Permission::Sol { - amount: 10_000_000_000, - recurring: None, - }]; - - let current_slot = context.svm.get_sysvar::().slot; - - let add_auth_ix = builder - .add_authority_instruction( - AuthorityType::Secp256k1, - &new_secp_pubkey, - permissions, - Some(current_slot), - ) - .unwrap(); - - let msg = v0::Message::try_compile( - &context.default_payer.pubkey(), - &[add_auth_ix], - &[], - context.svm.latest_blockhash(), - ) - .unwrap(); - - let tx = VersionedTransaction::try_new(VersionedMessage::V0(msg), &[&context.default_payer]) - .unwrap(); - - let result = context.svm.send_transaction(tx); - assert!( - result.is_ok(), - "Failed to add authority: {:?}", - result.err() - ); - - // Transfer 1SOL to the new authority - let mut sign_fn2 = move |payload: &[u8]| -> [u8; 65] { - let mut hash = [0u8; 32]; - hash.copy_from_slice(&payload[..32]); - let hash = B256::from(hash); - new_authority.sign_hash_sync(&hash).unwrap().as_bytes() - }; - - let mut builder_with_new_authority = SwigInstructionBuilder::new( - swig_id, - AuthorityManager::Secp256k1(new_secp_pubkey, Box::new(sign_fn2)), - payer.pubkey(), - 1, - ); - - // Fund the Swig account - context.svm.airdrop(&swig_key, 1_000_000_000).unwrap(); - - // Create a transfer instruction to test signing - let recipient = Keypair::new(); - let transfer_amount = 100_000_000; - let transfer_ix = solana_program::system_instruction::transfer( - &swig_key, - &recipient.pubkey(), - transfer_amount, - ); - let current_slot = context.svm.get_sysvar::().slot; - let sign_ix = builder_with_new_authority - .sign_instruction(vec![transfer_ix], Some(current_slot)) - .unwrap(); - - let msg = v0::Message::try_compile( - &context.default_payer.pubkey(), - &sign_ix, - &[], - context.svm.latest_blockhash(), - ) - .unwrap(); - - let tx = VersionedTransaction::try_new(VersionedMessage::V0(msg), &[&context.default_payer]); - - assert!(tx.is_ok(), "Failed to create transaction {:?}", tx.err()); - - let result = context.svm.send_transaction(tx.unwrap()); - assert!( - result.is_ok(), - "Failed to execute signed instruction: {:?}", - result.err() - ); - - // Verify the transfer was successful - let recipient_account = context.svm.get_account(&recipient.pubkey()).unwrap(); - assert_eq!(recipient_account.lamports, transfer_amount); -} - -#[test_log::test] -fn test_create_ed25519_session() { - 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 = [0; 32]; - - let mut swig_ix_builder = SwigInstructionBuilder::new( - id, - AuthorityManager::Ed25519Session(CreateEd25519SessionAuthority::new( - swig_authority.pubkey().to_bytes(), - [0; 32], - 100, - )), - context.default_payer.pubkey(), - 0, - ); - - let create_ix = swig_ix_builder.build_swig_account().unwrap(); - - let msg = v0::Message::try_compile( - &context.default_payer.pubkey(), - &[create_ix], - &[], - context.svm.latest_blockhash(), - ) - .unwrap(); - - let tx = VersionedTransaction::try_new(VersionedMessage::V0(msg), &[&context.default_payer]) - .unwrap(); - - let result = context.svm.send_transaction(tx); - assert!( - result.is_ok(), - "Failed to create Swig account: {:?}", - result.err() - ); - - let swig_key = swig_ix_builder.get_swig_account().unwrap(); - - context.svm.airdrop(&swig_key, 50_000_000_000).unwrap(); - - 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); - let role = swig.get_role(0).unwrap().unwrap(); - - assert_eq!( - role.authority.authority_type(), - AuthorityType::Ed25519Session - ); - assert!(role.authority.session_based()); - let auth: &Ed25519SessionAuthority = role.authority.as_any().downcast_ref().unwrap(); - assert_eq!(auth.max_session_length, 100); - assert_eq!(auth.public_key, swig_authority.pubkey().to_bytes()); - assert_eq!(auth.current_session_expiration, 0); - assert_eq!(auth.session_key, [0; 32]); - - let swig_pubkey = swig_key; - let swig_account = context.svm.get_account(&swig_key).unwrap(); - let swig_data = swig_account.data; - - let swig_with_roles = SwigWithRoles::from_bytes(&swig_data) - .map_err(|e| SwigError::InvalidSwigData) - .unwrap(); - - let auth: &Ed25519SessionAuthority = role.authority.as_any().downcast_ref().unwrap(); - - // create an ed25519 session authority - let session_authority = Keypair::new(); - let session_authority_pubkey = session_authority.pubkey().to_bytes(); - - let create_session_ix = swig_ix_builder - .create_session_instruction(session_authority.pubkey(), 100, None) - .unwrap(); - - let msg = v0::Message::try_compile( - &context.default_payer.pubkey(), - &[create_session_ix], - &[], - context.svm.latest_blockhash(), - ) - .unwrap(); - - let tx = VersionedTransaction::try_new( - VersionedMessage::V0(msg), - &[&context.default_payer, &swig_authority], - ) - .unwrap(); - - let result = context.svm.send_transaction(tx); - assert!( - result.is_ok(), - "Failed to create session: {:?}", - result.err() - ); - - let swig_account = context.svm.get_account(&swig_key).unwrap(); - let swig_data = swig_account.data; - - let swig_with_roles = SwigWithRoles::from_bytes(&swig_data) - .map_err(|e| SwigError::InvalidSwigData) - .unwrap(); - - let role = swig_with_roles.get_role(0).unwrap().unwrap(); - - let auth: &Ed25519SessionAuthority = role.authority.as_any().downcast_ref().unwrap(); -} - -#[test_log::test] -fn test_create_secp256k1_session() { - let mut context = setup_test_context().unwrap(); - - let wallet = LocalSigner::random(); - - let id = [0; 32]; - - let secp_pubkey = wallet - .credential() - .verifying_key() - .to_encoded_point(false) - .to_bytes(); - - let wallet = wallet.clone(); - let payer = &context.default_payer; - - let signing_fn = move |payload: &[u8]| -> [u8; 65] { - let mut hash = [0u8; 32]; - hash.copy_from_slice(&payload[..32]); - let hash = B256::from(hash); - wallet.sign_hash_sync(&hash).unwrap().as_bytes() - }; - - let mut swig_ix_builder = SwigInstructionBuilder::new( - id, - AuthorityManager::Secp256k1Session( - CreateSecp256k1SessionAuthority::new( - secp_pubkey[1..].try_into().unwrap(), - [0; 32], - 100, - ), - Box::new(signing_fn), - ), - context.default_payer.pubkey(), - 0, - ); - - let create_ix = swig_ix_builder.build_swig_account().unwrap(); - - let msg = v0::Message::try_compile( - &context.default_payer.pubkey(), - &[create_ix], - &[], - context.svm.latest_blockhash(), - ) - .unwrap(); - - let tx = VersionedTransaction::try_new(VersionedMessage::V0(msg), &[&context.default_payer]) - .unwrap(); - - let result = context.svm.send_transaction(tx); - assert!( - result.is_ok(), - "Failed to create Swig account: {:?}", - result.err() - ); - - let swig_key = swig_ix_builder.get_swig_account().unwrap(); - - context.svm.airdrop(&swig_key, 50_000_000_000).unwrap(); - - 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); - let role = swig.get_role(0).unwrap().unwrap(); - - assert_eq!( - role.authority.authority_type(), - AuthorityType::Secp256k1Session - ); - assert!(role.authority.session_based()); - let auth: &Secp256k1SessionAuthority = role.authority.as_any().downcast_ref().unwrap(); - - assert_eq!(auth.max_session_age, 100); - // assert_eq!(auth.public_key, secp_pubkey[1..].try_into().unwrap()); - assert_eq!(auth.current_session_expiration, 0); - assert_eq!(auth.session_key, [0; 32]); - - let swig_pubkey = swig_key; - let swig_account = context.svm.get_account(&swig_key).unwrap(); - let swig_data = swig_account.data; - - let swig_with_roles = SwigWithRoles::from_bytes(&swig_data) - .map_err(|e| SwigError::InvalidSwigData) - .unwrap(); - - let auth: &Secp256k1SessionAuthority = role.authority.as_any().downcast_ref().unwrap(); - - // create an ed25519 session authority - let session_authority = Keypair::new(); - let session_authority_pubkey = session_authority.pubkey().to_bytes(); - - let current_slot = context.svm.get_sysvar::().slot; - let create_session_ix = swig_ix_builder - .create_session_instruction(session_authority.pubkey(), 100, Some(current_slot)) - .unwrap(); - - let msg = v0::Message::try_compile( - &context.default_payer.pubkey(), - &[create_session_ix], - &[], - context.svm.latest_blockhash(), - ) - .unwrap(); - - let tx = VersionedTransaction::try_new(VersionedMessage::V0(msg), &[&context.default_payer]) - .unwrap(); - - let result = context.svm.send_transaction(tx); - assert!( - result.is_ok(), - "Failed to create session: {:?}", - result.err() - ); - - let swig_account = context.svm.get_account(&swig_key).unwrap(); - let swig_data = swig_account.data; - - let swig_with_roles = SwigWithRoles::from_bytes(&swig_data) - .map_err(|e| SwigError::InvalidSwigData) - .unwrap(); - - let role = swig_with_roles.get_role(0).unwrap().unwrap(); - - let auth: &Secp256k1SessionAuthority = role.authority.as_any().downcast_ref().unwrap(); -} - -#[test_log::test] -fn test_sign_instruction_with_secp256k1_authority() { - let mut context = setup_test_context().unwrap(); - let swig_id = [6u8; 32]; - let payer = &context.default_payer; - let role_id = 0; - - let wallet = LocalSigner::random(); - let secp_pubkey = wallet - .credential() - .verifying_key() - .to_encoded_point(false) - .to_bytes(); - - let wallet = wallet.clone(); - let signing_fn = move |payload: &[u8]| -> [u8; 65] { - let mut hash = [0u8; 32]; - hash.copy_from_slice(&payload[..32]); - let hash = B256::from(hash); - wallet.sign_hash_sync(&hash).unwrap().as_bytes() - }; - - let mut builder = SwigInstructionBuilder::new( - swig_id, - AuthorityManager::Secp256k1(secp_pubkey, Box::new(signing_fn)), - payer.pubkey(), - role_id, - ); - - let ix = builder.build_swig_account().unwrap(); - let msg = v0::Message::try_compile(&payer.pubkey(), &[ix], &[], context.svm.latest_blockhash()) - .unwrap(); - - let tx = VersionedTransaction::try_new(VersionedMessage::V0(msg), &[payer]).unwrap(); - - let result = context.svm.send_transaction(tx); - assert!( - result.is_ok(), - "Failed to create Swig account: {:?}", - result.err() - ); - - let swig_key = builder.get_swig_account().unwrap(); - context.svm.airdrop(&swig_key, 1_000_000_000).unwrap(); - - let recipient = Keypair::new(); - let transfer_amount = 100_000; - let transfer_ix = solana_program::system_instruction::transfer( - &swig_key, - &recipient.pubkey(), - transfer_amount, - ); - - let current_slot = context.svm.get_sysvar::().slot; - let sign_ix = builder - .sign_instruction(vec![transfer_ix], Some(current_slot)) - .unwrap(); - - let msg = v0::Message::try_compile( - &context.default_payer.pubkey(), - &sign_ix, - &[], - context.svm.latest_blockhash(), - ) - .unwrap(); - - let tx = VersionedTransaction::try_new(VersionedMessage::V0(msg), &[&context.default_payer]); - assert!(tx.is_ok(), "Failed to create transaction {:?}", tx.err()); - - let result = context.svm.send_transaction(tx.unwrap()); - assert!( - result.is_ok(), - "Failed to execute signed instruction: {:?}", - result.err() - ); - - let recipient_account = context.svm.get_account(&recipient.pubkey()).unwrap(); - assert_eq!(recipient_account.lamports, transfer_amount); -} - -#[test_log::test] -fn test_add_authority_with_secp256k1_root() { - let mut context = setup_test_context().unwrap(); - let swig_id = [7u8; 32]; - let payer = &context.default_payer; - let role_id = 0; - - let wallet = LocalSigner::random(); - let secp_pubkey = wallet - .credential() - .verifying_key() - .to_encoded_point(false) - .to_bytes(); - - let wallet = wallet.clone(); - let signing_fn = move |payload: &[u8]| -> [u8; 65] { - let mut hash = [0u8; 32]; - hash.copy_from_slice(&payload[..32]); - let hash = B256::from(hash); - wallet.sign_hash_sync(&hash).unwrap().as_bytes() - }; - - let mut builder = SwigInstructionBuilder::new( - swig_id, - AuthorityManager::Secp256k1(secp_pubkey, Box::new(signing_fn)), - payer.pubkey(), - role_id, - ); - - let ix = builder.build_swig_account().unwrap(); - let msg = v0::Message::try_compile(&payer.pubkey(), &[ix], &[], context.svm.latest_blockhash()) - .unwrap(); - - let tx = VersionedTransaction::try_new(VersionedMessage::V0(msg), &[payer]).unwrap(); - - let result = context.svm.send_transaction(tx); - assert!( - result.is_ok(), - "Failed to create Swig account: {:?}", - result.err() - ); - - let swig_key = builder.get_swig_account().unwrap(); - - let new_authority = LocalSigner::random(); - let secp_pubkey_bytes = new_authority - .credential() - .verifying_key() - .to_encoded_point(false) - .to_bytes(); - - let permissions = vec![Permission::Sol { - amount: 1_000_000_000, - recurring: None, - }]; - - let current_slot = context.svm.get_sysvar::().slot; - - let add_auth_ix = builder - .add_authority_instruction( - AuthorityType::Secp256k1, - &secp_pubkey_bytes, - permissions, - Some(current_slot), - ) - .unwrap(); - - let msg = v0::Message::try_compile( - &context.default_payer.pubkey(), - &[add_auth_ix], - &[], - context.svm.latest_blockhash(), - ) - .unwrap(); - - let tx = VersionedTransaction::try_new(VersionedMessage::V0(msg), &[&context.default_payer]) - .unwrap(); - - let result = context.svm.send_transaction(tx); - assert!( - result.is_ok(), - "Failed to add authority: {:?}", - result.err() - ); - - 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); // Root authority + new authority -} - -#[test_log::test] -fn test_remove_authority_with_ed25519_root() { - let mut context = setup_test_context().unwrap(); - let swig_id = [8u8; 32]; - let authority = Keypair::new(); - let authority_pubkey = authority.pubkey(); - let role_id = 0; - - let (swig_key, _) = create_swig_ed25519(&mut context, &authority, swig_id).unwrap(); - - let new_authority = Keypair::new(); - let permissions = vec![Permission::Sol { - amount: 1_000_000_000, - recurring: None, - }]; - - let payer = &context.default_payer; - - let mut builder = SwigInstructionBuilder::new( - swig_id, - AuthorityManager::Ed25519(authority.pubkey()), - payer.pubkey(), - role_id, - ); - - let add_auth_ix = builder - .add_authority_instruction( - AuthorityType::Ed25519, - &authority_pubkey.to_bytes(), - permissions, - None, - ) - .unwrap(); - - let msg = v0::Message::try_compile( - &payer.pubkey(), - &[add_auth_ix], - &[], - context.svm.latest_blockhash(), - ) - .unwrap(); - - let tx = - VersionedTransaction::try_new(VersionedMessage::V0(msg), &[&payer, &authority]).unwrap(); - let result = context.svm.send_transaction(tx); - assert!( - result.is_ok(), - "Failed to add authority: {:?}", - result.err() - ); - - let remove_auth_ix = builder.remove_authority(1, None).unwrap(); - let msg = v0::Message::try_compile( - &payer.pubkey(), - &[remove_auth_ix], - &[], - context.svm.latest_blockhash(), - ) - .unwrap(); - - let tx = - VersionedTransaction::try_new(VersionedMessage::V0(msg), &[&payer, &authority]).unwrap(); - let result = context.svm.send_transaction(tx); - assert!( - result.is_ok(), - "Failed to remove authority: {:?}", - result.err() - ); - - 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, 1); // Only root authority remains -} - -#[test_log::test] -fn test_switch_authority_and_payer() { - let mut context = setup_test_context().unwrap(); - let swig_id = [9u8; 32]; - let authority = Keypair::new(); - let payer = &context.default_payer; - let role_id = 0; - - let mut builder = SwigInstructionBuilder::new( - swig_id, - AuthorityManager::Ed25519(authority.pubkey()), - payer.pubkey(), - role_id, - ); - - let new_authority = Keypair::new(); - let new_payer = Keypair::new(); - - builder - .switch_authority(1, AuthorityManager::Ed25519(new_authority.pubkey())) - .unwrap(); - assert_eq!(builder.get_role_id(), 1); - assert_eq!( - builder.get_current_authority().unwrap(), - new_authority.pubkey().to_bytes() - ); - - builder.switch_payer(new_payer.pubkey()).unwrap(); - let ix = builder.build_swig_account().unwrap(); - assert_eq!(ix.accounts[1].pubkey, new_payer.pubkey()); -} - -fn test_token_transfer_with_program_scope() { - let mut context = setup_test_context().unwrap(); - - // Setup payers and recipients - let swig_authority = Keypair::new(); - let regular_sender = Keypair::new(); - let recipient = Keypair::new(); - - // Airdrop to participants - context - .svm - .airdrop(&swig_authority.pubkey(), 10_000_000_000) - .unwrap(); - context - .svm - .airdrop(®ular_sender.pubkey(), 10_000_000_000) - .unwrap(); - context - .svm - .airdrop(&recipient.pubkey(), 10_000_000_000) - .unwrap(); - - // Setup token mint - let mint_pubkey = setup_mint(&mut context.svm, &context.default_payer).unwrap(); - - // Setup swig account - let id = rand::random::<[u8; 32]>(); - let (swig, _) = Pubkey::find_program_address(&swig_account_seeds(&id), &program_id()); - let swig_create_result = create_swig_ed25519(&mut context, &swig_authority, id); - assert!(swig_create_result.is_ok()); - - // Setup token accounts - let swig_ata = setup_ata( - &mut context.svm, - &mint_pubkey, - &swig, - &context.default_payer, - ) - .unwrap(); - - let new_authority = Keypair::new(); - - let program_scope = Permission::ProgramScope { - program_id: spl_token::ID, - target_account: swig_ata, - numeric_type: 1, - limit: Some(1_000_000), - window: Some(0), - balance_field_start: Some(64), - balance_field_end: Some(72), - }; - - let current_slot = context.svm.get_sysvar::().slot; - - let mut builder = SwigInstructionBuilder::new( - id, - AuthorityManager::Ed25519(swig_authority.pubkey()), - context.default_payer.pubkey(), - 0, - ); - - let add_auth_ix = builder - .add_authority_instruction( - AuthorityType::Ed25519, - &new_authority.pubkey().to_bytes(), - vec![program_scope], - Some(current_slot), - ) - .unwrap(); - - let msg = v0::Message::try_compile( - &context.default_payer.pubkey(), - &[add_auth_ix], - &[], - context.svm.latest_blockhash(), - ) - .unwrap(); - - let tx = VersionedTransaction::try_new( - VersionedMessage::V0(msg), - &[&context.default_payer, &new_authority], - ) - .unwrap(); - - let result = context.svm.send_transaction(tx); - assert!( - result.is_ok(), - "Failed to add authority: {:?}", - result.err() - ); - - println!("Added ProgramScope action for token program"); - - let swig_account = context.svm.get_account(&swig).unwrap(); - - let regular_sender_ata = setup_ata( - &mut context.svm, - &mint_pubkey, - ®ular_sender.pubkey(), - &context.default_payer, - ) - .unwrap(); - - let recipient_ata = setup_ata( - &mut context.svm, - &mint_pubkey, - &recipient.pubkey(), - &context.default_payer, - ) - .unwrap(); - - // Mint tokens to both sending accounts - let initial_token_amount = 1000; - mint_to( - &mut context.svm, - &mint_pubkey, - &context.default_payer, - &swig_ata, - initial_token_amount, - ) - .unwrap(); - - mint_to( - &mut context.svm, - &mint_pubkey, - &context.default_payer, - ®ular_sender_ata, - initial_token_amount, - ) - .unwrap(); - - // Test regular token transfer - let transfer_amount = 100; - let token_program_id = spl_token::ID; - - let regular_transfer_ix = spl_token::instruction::transfer( - &token_program_id, - ®ular_sender_ata, - &recipient_ata, - ®ular_sender.pubkey(), - &[], - transfer_amount, - ) - .unwrap(); - - let regular_transfer_message = v0::Message::try_compile( - ®ular_sender.pubkey(), - &[regular_transfer_ix], - &[], - context.svm.latest_blockhash(), - ) - .unwrap(); - - let regular_transfer_tx = VersionedTransaction::try_new( - VersionedMessage::V0(regular_transfer_message), - &[regular_sender], - ) - .unwrap(); - - let result = context.svm.send_transaction(regular_transfer_tx); - assert!( - result.is_ok(), - "Regular transfer failed: {:?}", - result.err() - ); - - // Test swig token transfer - let swig_transfer_ix = spl_token::instruction::transfer( - &token_program_id, - &swig_ata, - &recipient_ata, - &swig, - &[], - transfer_amount, - ) - .unwrap(); - - let sign_ix = swig_interface::SignInstruction::new_ed25519( - swig, - swig_authority.pubkey(), - swig_authority.pubkey(), - swig_transfer_ix, - 1, // authority role id - ) - .unwrap(); - - let swig_transfer_message = v0::Message::try_compile( - &swig_authority.pubkey(), - &[sign_ix], - &[], - context.svm.latest_blockhash(), - ) - .unwrap(); - - let swig_transfer_tx = VersionedTransaction::try_new( - VersionedMessage::V0(swig_transfer_message), - &[swig_authority], - ) - .unwrap(); - - let result = context.svm.send_transaction(swig_transfer_tx); - assert!(result.is_ok(), "Swig transfer failed: {:?}", result.err()); -} - -/// Helper function to perform token transfers through the swig -fn perform_token_transfer( - context: &mut SwigTestContext, - swig: Pubkey, - swig_authority: &Keypair, - swig_ata: Pubkey, - recipient_ata: Pubkey, - amount: u64, - expected_success: bool, -) -> Vec { - // Expire the blockhash to ensure we don't get AlreadyProcessed errors - context.svm.expire_blockhash(); - - // Get the current token balance before the transfer - let before_token_account = context.svm.get_account(&swig_ata).unwrap(); - let before_balance = if before_token_account.data.len() >= 72 { - // SPL token accounts have their balance at offset 64-72 - u64::from_le_bytes(before_token_account.data[64..72].try_into().unwrap()) - } else { - 0 - }; - println!("Before transfer, token balance: {}", before_balance); - - let token_program_id = spl_token::ID; - - let transfer_ix = spl_token::instruction::transfer( - &token_program_id, - &swig_ata, - &recipient_ata, - &swig, - &[], - amount, - ) - .unwrap(); - - let sign_ix = swig_interface::SignInstruction::new_ed25519( - swig, - swig_authority.pubkey(), - swig_authority.pubkey(), - transfer_ix, - 1, // authority role id - ) - .unwrap(); - - let transfer_message = v0::Message::try_compile( - &swig_authority.pubkey(), - &[sign_ix], - &[], - context.svm.latest_blockhash(), - ) - .unwrap(); - - let transfer_tx = - VersionedTransaction::try_new(VersionedMessage::V0(transfer_message), &[swig_authority]) - .unwrap(); - - let result = context.svm.send_transaction(transfer_tx); - - // Get the current token balance after the transfer - let after_token_account = context.svm.get_account(&swig_ata).unwrap(); - let after_balance = if after_token_account.data.len() >= 72 { - // SPL token accounts have their balance at offset 64-72 - u64::from_le_bytes(after_token_account.data[64..72].try_into().unwrap()) - } else { - 0 - }; - println!("After transfer, token balance: {}", after_balance); - - if expected_success { - assert!( - result.is_ok(), - "Expected successful transfer, but got error: {:?}", - result.err() - ); - // Verify the token balance actually decreased by the expected amount - assert_eq!( - before_balance - after_balance, - amount, - "Token balance did not decrease by the expected amount" - ); - println!("Successfully transferred {} tokens", amount); - println!( - "Token balance decreased from {} to {}", - before_balance, after_balance - ); - result.unwrap().logs - } else { - println!("result: {:?}", result); - assert!( - result.is_err(), - "Expected transfer to fail, but it succeeded" - ); - // Verify the balance didn't change - assert_eq!( - before_balance, after_balance, - "Token balance should not have changed for a failed transfer" - ); - println!("Transfer of {} tokens was correctly rejected", amount); - Vec::new() - } -} - -use solana_sdk::account::Account; - -pub fn display_swig(swig_pubkey: Pubkey, swig_account: &Account) -> Result<(), SwigError> { - let swig_with_roles = - SwigWithRoles::from_bytes(&swig_account.data).map_err(|e| SwigError::InvalidSwigData)?; - - println!("╔══════════════════════════════════════════════════════════════════"); - println!("║ SWIG WALLET DETAILS"); - println!("╠══════════════════════════════════════════════════════════════════"); - println!("║ Account Address: {}", swig_pubkey); - println!("║ Total Roles: {}", swig_with_roles.state.role_counter); - println!( - "║ Balance: {} SOL", - swig_account.lamports() as f64 / 1_000_000_000.0 - ); - - println!("╠══════════════════════════════════════════════════════════════════"); - println!("║ ROLES & PERMISSIONS"); - println!("╠══════════════════════════════════════════════════════════════════"); - - for i in 0..swig_with_roles.state.role_counter { - let role = swig_with_roles - .get_role(i) - .map_err(|e| SwigError::AuthorityNotFound)?; - - if let Some(role) = role { - println!("║"); - println!("║ Role ID: {}", i); - println!( - "║ ├─ Type: {}", - if role.authority.session_based() { - "Session-based Authority" - } else { - "Permanent Authority" - } - ); - println!("║ ├─ Authority Type: {:?}", role.authority.authority_type()); - println!( - "║ ├─ Authority: {}", - match role.authority.authority_type() { - AuthorityType::Ed25519 | AuthorityType::Ed25519Session => { - let authority = role.authority.identity().unwrap(); - let authority = bs58::encode(authority).into_string(); - authority - }, - AuthorityType::Secp256k1 | AuthorityType::Secp256k1Session => { - let authority = role.authority.identity().unwrap(); - let authority_hex = hex::encode([&[0x4].as_slice(), authority].concat()); - // get eth address from public key - let mut hasher = solana_sdk::keccak::Hasher::default(); - hasher.hash(authority_hex.as_bytes()); - let hash = hasher.result(); - let address = format!("0x{}", hex::encode(&hash.0[12..32])); - address - }, - _ => todo!(), - } - ); - - println!("║ ├─ Permissions:"); - - // Check All permission - if (Role::get_action::(&role, &[]).map_err(|_| SwigError::AuthorityNotFound)?) - .is_some() - { - println!("║ │ ├─ Full Access (All Permissions)"); - } - - // Check Manage Authority permission - if (Role::get_action::(&role, &[]) - .map_err(|_| SwigError::AuthorityNotFound)?) - .is_some() - { - println!("║ │ ├─ Manage Authority"); - } - - // Check Sol Limit - if let Some(action) = Role::get_action::(&role, &[]) - .map_err(|_| SwigError::AuthorityNotFound)? - { - println!( - "║ │ ├─ SOL Limit: {} SOL", - action.amount as f64 / 1_000_000_000.0 - ); - } - - // Check Sol Recurring Limit - if let Some(action) = Role::get_action::(&role, &[]) - .map_err(|_| SwigError::AuthorityNotFound)? - { - println!("║ │ ├─ Recurring SOL Limit:"); - println!( - "║ │ │ ├─ Amount: {} SOL", - action.recurring_amount as f64 / 1_000_000_000.0 - ); - println!("║ │ │ ├─ Window: {} slots", action.window); - println!( - "║ │ │ ├─ Current Usage: {} SOL", - action.current_amount as f64 / 1_000_000_000.0 - ); - println!("║ │ │ └─ Last Reset: Slot {}", action.last_reset); - } - - // Check Program Scope - if let Some(action) = Role::get_action::( - &role, - &[ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, - ], - ) - .map_err(|_| SwigError::AuthorityNotFound)? - { - let program_id = Pubkey::from(action.program_id); - let target_account = Pubkey::from(action.target_account); - println!("║ │ ├─ Program Scope:"); - println!("║ │ │ ├─ Program ID: {:?}", program_id); - println!("║ │ │ ├─ Target Account: {:?}", target_account); - println!("║ │ │ ├─ Numeric Type: {}", action.numeric_type); - println!("║ │ │ ├─ Window: {}", action.window); - println!("║ │ │ ├─ Limit: {}", action.limit); - println!( - "║ │ │ ├─ Balance Field Start: {}", - action.balance_field_start - ); - println!("║ │ │ └─ Balance Field End: {}", action.balance_field_end); - } - println!("║ │ "); - } - } - - println!("╚══════════════════════════════════════════════════════════════════"); - - Ok(()) -} diff --git a/rust-sdk/src/tests/wallet/authority_tests.rs b/rust-sdk/src/tests/wallet/authority_tests.rs index 08ad99d8..358c69e7 100644 --- a/rust-sdk/src/tests/wallet/authority_tests.rs +++ b/rust-sdk/src/tests/wallet/authority_tests.rs @@ -5,14 +5,15 @@ use solana_sdk::signature::{Keypair, Signer}; use swig_state_x::authority::AuthorityType; use super::*; +use crate::client_role::{Ed25519ClientRole, Secp256k1ClientRole}; #[test_log::test] fn should_manage_authorities_successfully() { let (mut litesvm, main_authority) = setup_test_environment(); let mut swig_wallet = create_test_wallet(litesvm, &main_authority); - let secondary_authority = Keypair::new(); // Add secondary authority with SOL permission + let secondary_authority = Keypair::new(); swig_wallet .add_authority( AuthorityType::Ed25519, @@ -25,14 +26,21 @@ fn should_manage_authorities_successfully() { .unwrap(); // Verify both authorities exist - swig_wallet.display_swig().unwrap(); + assert_eq!(swig_wallet.get_role_count().unwrap(), 2); + assert!(swig_wallet + .get_role_id(&secondary_authority.pubkey().to_bytes()) + .is_ok()); // Remove secondary authority swig_wallet .remove_authority(&secondary_authority.pubkey().to_bytes()) .unwrap(); - swig_wallet.display_swig().unwrap(); + // Verify authority was removed + assert_eq!(swig_wallet.get_role_count().unwrap(), 2); + assert!(swig_wallet + .get_role_id(&secondary_authority.pubkey().to_bytes()) + .is_err()); // Add third authority with recurring permissions let third_authority = Keypair::new(); @@ -48,13 +56,17 @@ fn should_manage_authorities_successfully() { ) .unwrap(); - swig_wallet.display_swig().unwrap(); + // Verify third authority was added + assert_eq!(swig_wallet.get_role_count().unwrap(), 3); + assert!(swig_wallet + .get_role_id(&third_authority.pubkey().to_bytes()) + .is_ok()); // Switch to third authority swig_wallet .switch_authority( - 1, - AuthorityManager::Ed25519(third_authority.pubkey()), + 2, + Box::new(Ed25519ClientRole::new(third_authority.pubkey())), Some(&third_authority), ) .unwrap(); @@ -108,14 +120,17 @@ fn should_add_secp256k1_authority() { .unwrap(); // Verify both authorities exist - swig_wallet.display_swig().unwrap(); + assert_eq!(swig_wallet.get_role_count().unwrap(), 2); + assert!(swig_wallet.get_role_id(&secp_pubkey.as_ref()[1..]).is_ok()); // Remove secondary authority swig_wallet .remove_authority(&secp_pubkey.as_ref()[1..]) .unwrap(); - swig_wallet.display_swig().unwrap(); + // Verify authority was removed + assert_eq!(swig_wallet.get_role_count().unwrap(), 2); + assert!(swig_wallet.get_role_id(&secp_pubkey.as_ref()[1..]).is_err()); // Add third authority with recurring permissions let third_authority = Keypair::new(); @@ -131,13 +146,17 @@ fn should_add_secp256k1_authority() { ) .unwrap(); - swig_wallet.display_swig().unwrap(); + // Verify third authority was added + assert_eq!(swig_wallet.get_role_count().unwrap(), 3); + assert!(swig_wallet + .get_role_id(&third_authority.pubkey().to_bytes()) + .is_ok()); // Switch to third authority swig_wallet .switch_authority( - 1, - AuthorityManager::Ed25519(third_authority.pubkey()), + 2, + Box::new(Ed25519ClientRole::new(third_authority.pubkey())), Some(&third_authority), ) .unwrap(); @@ -172,13 +191,16 @@ fn should_switch_authority_and_payer() { swig_wallet .switch_authority( 1, - AuthorityManager::Ed25519(secondary_authority.pubkey()), + Box::new(Ed25519ClientRole::new(secondary_authority.pubkey())), Some(&secondary_authority), ) .unwrap(); swig_wallet.switch_payer(&secondary_authority).unwrap(); - swig_wallet.display_swig().unwrap(); + + // Verify authority switch and payer change + assert_eq!(swig_wallet.get_current_role_id().unwrap(), 1); + assert_eq!(swig_wallet.get_fee_payer(), secondary_authority.pubkey()); } #[test_log::test] @@ -203,7 +225,10 @@ fn should_replace_authority() { .unwrap(); // Verify old authority exists - swig_wallet.display_swig().unwrap(); + assert_eq!(swig_wallet.get_role_count().unwrap(), 2); + assert!(swig_wallet + .get_role_id(&old_authority.pubkey().to_bytes()) + .is_ok()); // Replace old authority with new authority swig_wallet @@ -219,7 +244,13 @@ fn should_replace_authority() { .unwrap(); // Verify the replacement - swig_wallet.display_swig().unwrap(); + assert_eq!(swig_wallet.get_role_count().unwrap(), 3); + assert!(swig_wallet + .get_role_id(&new_authority.pubkey().to_bytes()) + .is_ok()); + assert!(swig_wallet + .get_role_id(&old_authority.pubkey().to_bytes()) + .is_err()); // Try to authenticate with new authority (should succeed) assert!(swig_wallet diff --git a/rust-sdk/src/tests/wallet/creation_tests.rs b/rust-sdk/src/tests/wallet/creation_tests.rs index b3250241..8f57ef86 100644 --- a/rust-sdk/src/tests/wallet/creation_tests.rs +++ b/rust-sdk/src/tests/wallet/creation_tests.rs @@ -9,7 +9,11 @@ use super::*; fn should_create_ed25519_wallet() { let (litesvm, main_authority) = setup_test_environment(); let mut swig_wallet = create_test_wallet(litesvm, &main_authority); - swig_wallet.display_swig().unwrap(); + + // Verify wallet was created successfully + assert!(swig_wallet.get_swig_account().is_ok()); + assert_eq!(swig_wallet.get_role_count().unwrap(), 1); + assert_eq!(swig_wallet.get_current_role_id().unwrap(), 0); let swig_pubkey = swig_wallet.get_swig_account().unwrap(); let swig_data = swig_wallet.litesvm().get_account(&swig_pubkey).unwrap(); @@ -44,13 +48,16 @@ fn should_create_secp256k1_wallet() { let swig_wallet = SwigWallet::new( [0; 32], - AuthorityManager::Secp256k1(secp_pubkey, Box::new(sign_fn)), - &main_authority, + Box::new(Secp256k1ClientRole::new(secp_pubkey, Box::new(sign_fn))), &main_authority, "http://localhost:8899".to_string(), + Some(&main_authority), litesvm, ) .unwrap(); - swig_wallet.display_swig().unwrap(); + // Verify wallet was created successfully + assert!(swig_wallet.get_swig_account().is_ok()); + assert_eq!(swig_wallet.get_role_count().unwrap(), 1); + assert_eq!(swig_wallet.get_current_role_id().unwrap(), 0); } diff --git a/rust-sdk/src/tests/wallet/helper_tests.rs b/rust-sdk/src/tests/wallet/helper_tests.rs new file mode 100644 index 00000000..938f9c6a --- /dev/null +++ b/rust-sdk/src/tests/wallet/helper_tests.rs @@ -0,0 +1,548 @@ +use alloy_primitives::B256; +use alloy_signer::SignerSync; +use alloy_signer_local::LocalSigner; +use solana_sdk::signature::{Keypair, Signer}; +use swig_state_x::authority::AuthorityType; + +use super::*; +use crate::client_role::{Ed25519ClientRole, Secp256k1ClientRole}; + +#[test_log::test] +fn should_get_swig_account_successfully() { + let (mut litesvm, main_authority) = setup_test_environment(); + let swig_wallet = create_test_wallet(litesvm, &main_authority); + + let swig_account = swig_wallet.get_swig_account().unwrap(); + assert!(swig_account != Pubkey::default()); + println!("Swig account: {}", swig_account); +} + +#[test_log::test] +fn should_get_current_authority_permissions() { + let (mut litesvm, main_authority) = setup_test_environment(); + let swig_wallet = create_test_wallet(litesvm, &main_authority); + + let permissions = swig_wallet.get_current_authority_permissions().unwrap(); + assert!(!permissions.is_empty()); + assert!(permissions.contains(&Permission::All)); + println!("Current permissions: {:?}", permissions); +} + +#[test_log::test] +fn should_get_role_id_for_authority() { + let (mut litesvm, main_authority) = setup_test_environment(); + let mut swig_wallet = create_test_wallet(litesvm, &main_authority); + + // Add a secondary authority + let secondary_authority = Keypair::new(); + swig_wallet + .add_authority( + AuthorityType::Ed25519, + &secondary_authority.pubkey().to_bytes(), + vec![Permission::Sol { + amount: 10_000_000_000, + recurring: None, + }], + ) + .unwrap(); + + // Get role ID for the secondary authority + let role_id = swig_wallet + .get_role_id(&secondary_authority.pubkey().to_bytes()) + .unwrap(); + assert_eq!(role_id, 1); // Should be role ID 1 (0 is the main authority) + + // Get role ID for the main authority + let main_role_id = swig_wallet + .get_role_id(&main_authority.pubkey().to_bytes()) + .unwrap(); + assert_eq!(main_role_id, 0); // Should be role ID 0 +} + +#[test_log::test] +fn should_get_current_role_id() { + let (mut litesvm, main_authority) = setup_test_environment(); + let swig_wallet = create_test_wallet(litesvm, &main_authority); + + let role_id = swig_wallet.get_current_role_id().unwrap(); + assert_eq!(role_id, 0); // Main authority should be role ID 0 +} + +#[test_log::test] +fn should_get_current_permissions() { + let (mut litesvm, main_authority) = setup_test_environment(); + let swig_wallet = create_test_wallet(litesvm, &main_authority); + + let permissions = swig_wallet.get_current_permissions().unwrap(); + assert!(!permissions.is_empty()); + assert!(permissions.contains(&Permission::All)); +} + +#[test_log::test] +fn should_authenticate_authority() { + let (mut litesvm, main_authority) = setup_test_environment(); + let swig_wallet = create_test_wallet(litesvm, &main_authority); + + // Authenticate the main authority (should succeed) + swig_wallet + .authenticate_authority(&main_authority.pubkey().to_bytes()) + .unwrap(); + + // Try to authenticate a non-existent authority (should fail) + let fake_authority = Keypair::new(); + assert!(swig_wallet + .authenticate_authority(&fake_authority.pubkey().to_bytes()) + .is_err()); +} + +#[test_log::test] +fn should_get_sub_account() { + let (mut litesvm, main_authority) = setup_test_environment(); + let mut swig_wallet = create_test_wallet(litesvm, &main_authority); + + // Initially, no sub account should exist + let sub_account = swig_wallet.get_sub_account().unwrap(); + assert!(sub_account.is_none()); + + // Add an authority with SubAccount permission + let sub_account_authority = Keypair::new(); + swig_wallet + .add_authority( + AuthorityType::Ed25519, + &sub_account_authority.pubkey().to_bytes(), + vec![Permission::SubAccount { + sub_account: [0; 32], + }], + ) + .unwrap(); + + // Switch to the sub-account authority + swig_wallet + .switch_authority( + 1, + Box::new(Ed25519ClientRole::new(sub_account_authority.pubkey())), + Some(&sub_account_authority), + ) + .unwrap(); + + // Create a sub account + swig_wallet.create_sub_account().unwrap(); + + // Now a sub account should exist (in test env, may not be detected) + let sub_account = swig_wallet.get_sub_account().unwrap(); + println!("Sub account after creation: {:?}", sub_account); +} + +#[test_log::test] +fn should_get_current_slot() { + let (mut litesvm, main_authority) = setup_test_environment(); + let swig_wallet = create_test_wallet(litesvm, &main_authority); + + let slot = swig_wallet.get_current_slot().unwrap(); + assert!(slot >= 0); // Slot can be 0 in test environment + println!("Current slot: {}", slot); +} + +#[test_log::test] +fn should_get_current_blockhash() { + let (mut litesvm, main_authority) = setup_test_environment(); + let swig_wallet = create_test_wallet(litesvm, &main_authority); + + let blockhash = swig_wallet.get_current_blockhash().unwrap(); + assert!(blockhash != solana_program::hash::Hash::default()); + println!("Current blockhash: {}", blockhash); +} + +#[test_log::test] +fn should_get_balance() { + let (mut litesvm, main_authority) = setup_test_environment(); + let swig_wallet = create_test_wallet(litesvm, &main_authority); + + let balance = swig_wallet.get_balance().unwrap(); + assert!(balance > 0); + println!("Balance: {} lamports", balance); +} + +#[test_log::test] +fn should_get_swig_id() { + let (mut litesvm, main_authority) = setup_test_environment(); + let swig_wallet = create_test_wallet(litesvm, &main_authority); + + let swig_id = swig_wallet.get_swig_id(); + assert_eq!(swig_id, &[0; 32]); // We use [0; 32] in create_test_wallet +} + +#[test_log::test] +fn should_get_fee_payer() { + let (mut litesvm, main_authority) = setup_test_environment(); + let swig_wallet = create_test_wallet(litesvm, &main_authority); + + let fee_payer = swig_wallet.get_fee_payer(); + assert_eq!(fee_payer, main_authority.pubkey()); +} + +#[test_log::test] +fn should_check_permissions() { + let (mut litesvm, main_authority) = setup_test_environment(); + let swig_wallet = create_test_wallet(litesvm, &main_authority); + + // Check if has all permissions + let has_all = swig_wallet.has_all_permissions().unwrap(); + assert!(has_all); + + // Check specific permission + let has_permission = swig_wallet.has_permission(&Permission::All).unwrap(); + assert!(has_permission); +} + +#[test_log::test] +fn should_get_sol_limits() { + let (mut litesvm, main_authority) = setup_test_environment(); + let mut swig_wallet = create_test_wallet(litesvm, &main_authority); + + // Add an authority with SOL limits + let limited_authority = Keypair::new(); + swig_wallet + .add_authority( + AuthorityType::Ed25519, + &limited_authority.pubkey().to_bytes(), + vec![Permission::Sol { + amount: 5_000_000_000, + recurring: Some(RecurringConfig::new(100)), + }], + ) + .unwrap(); + + // Switch to the limited authority + swig_wallet + .switch_authority( + 1, + Box::new(Ed25519ClientRole::new(limited_authority.pubkey())), + Some(&limited_authority), + ) + .unwrap(); + + // Check SOL limit + let sol_limit = swig_wallet.get_sol_limit().unwrap(); + assert_eq!(sol_limit, Some(5_000_000_000)); + + // Check recurring SOL limit + let recurring_limit = swig_wallet.get_recurring_sol_limit().unwrap(); + assert!(recurring_limit.is_some()); + if let Some(config) = recurring_limit { + assert_eq!(config.window, 100); + } +} + +#[test_log::test] +fn should_check_sol_spending_ability() { + let (mut litesvm, main_authority) = setup_test_environment(); + let mut swig_wallet = create_test_wallet(litesvm, &main_authority); + + // Add an authority with SOL limits + let limited_authority = Keypair::new(); + swig_wallet + .add_authority( + AuthorityType::Ed25519, + &limited_authority.pubkey().to_bytes(), + vec![Permission::Sol { + amount: 5_000_000_000, + recurring: Some(RecurringConfig::new(100)), + }], + ) + .unwrap(); + + // Switch to the limited authority + swig_wallet + .switch_authority( + 1, + Box::new(Ed25519ClientRole::new(limited_authority.pubkey())), + Some(&limited_authority), + ) + .unwrap(); + + // Check if can spend within limit + let can_spend_small = swig_wallet.can_spend_sol(1_000_000_000).unwrap(); + assert!(can_spend_small); + + // Check if can spend at limit + let can_spend_at_limit = swig_wallet.can_spend_sol(5_000_000_000).unwrap(); + assert!(can_spend_at_limit); + + // Check if cannot spend over limit + let can_spend_over_limit = swig_wallet.can_spend_sol(10_000_000_000).unwrap(); + assert!(!can_spend_over_limit); +} + +#[test_log::test] +fn should_get_role_count() { + let (mut litesvm, main_authority) = setup_test_environment(); + let mut swig_wallet = create_test_wallet(litesvm, &main_authority); + + // Initially should have 1 role (main authority) + let initial_count = swig_wallet.get_role_count().unwrap(); + assert_eq!(initial_count, 1); + + // Add another authority + let secondary_authority = Keypair::new(); + swig_wallet + .add_authority( + AuthorityType::Ed25519, + &secondary_authority.pubkey().to_bytes(), + vec![Permission::Sol { + amount: 10_000_000_000, + recurring: None, + }], + ) + .unwrap(); + + // Should now have 2 roles + let new_count = swig_wallet.get_role_count().unwrap(); + assert_eq!(new_count, 2); +} + +#[test_log::test] +fn should_get_authority_type() { + let (mut litesvm, main_authority) = setup_test_environment(); + let mut swig_wallet = create_test_wallet(litesvm, &main_authority); + + // Check main authority type + let main_authority_type = swig_wallet.get_authority_type(0).unwrap(); + assert_eq!(main_authority_type, AuthorityType::Ed25519); + + // Add a Secp256k1 authority + let wallet = LocalSigner::random(); + let secp_pubkey = wallet + .credential() + .verifying_key() + .to_encoded_point(false) + .to_bytes(); + + swig_wallet + .add_authority( + AuthorityType::Secp256k1, + &secp_pubkey.as_ref()[1..], + vec![Permission::Sol { + amount: 10_000_000_000, + recurring: None, + }], + ) + .unwrap(); + + // Check Secp256k1 authority type + let secp_authority_type = swig_wallet.get_authority_type(1).unwrap(); + assert_eq!(secp_authority_type, AuthorityType::Secp256k1); +} + +#[test_log::test] +fn should_get_authority_identity() { + let (mut litesvm, main_authority) = setup_test_environment(); + let swig_wallet = create_test_wallet(litesvm, &main_authority); + + // Get main authority identity + let main_identity = swig_wallet.get_authority_identity(0).unwrap(); + assert_eq!(main_identity, main_authority.pubkey().to_bytes()); +} + +#[test_log::test] +fn should_check_session_based() { + let (mut litesvm, main_authority) = setup_test_environment(); + let swig_wallet = create_test_wallet(litesvm, &main_authority); + + // Main authority should not be session-based + let is_session = swig_wallet.is_session_based(0).unwrap(); + assert!(!is_session); +} + +#[test_log::test] +fn should_get_role_permissions() { + let (mut litesvm, main_authority) = setup_test_environment(); + let mut swig_wallet = create_test_wallet(litesvm, &main_authority); + + // Get main role permissions + let main_permissions = swig_wallet.get_role_permissions(0).unwrap(); + assert!(main_permissions.contains(&Permission::All)); + + // Add an authority with specific permissions + let limited_authority = Keypair::new(); + swig_wallet + .add_authority( + AuthorityType::Ed25519, + &limited_authority.pubkey().to_bytes(), + vec![Permission::Sol { + amount: 5_000_000_000, + recurring: None, + }], + ) + .unwrap(); + + // Get the new role permissions + let limited_permissions = swig_wallet.get_role_permissions(1).unwrap(); + assert!(limited_permissions.contains(&Permission::Sol { + amount: 5_000_000_000, + recurring: None, + })); + assert!(!limited_permissions.contains(&Permission::All)); +} + +#[test_log::test] +fn should_check_role_has_permission() { + let (mut litesvm, main_authority) = setup_test_environment(); + let mut swig_wallet = create_test_wallet(litesvm, &main_authority); + + // Main role should have all permissions + let has_all = swig_wallet + .role_has_permission(0, &Permission::All) + .unwrap(); + assert!(has_all); + + // Add an authority with specific permissions + let limited_authority = Keypair::new(); + swig_wallet + .add_authority( + AuthorityType::Ed25519, + &limited_authority.pubkey().to_bytes(), + vec![Permission::Sol { + amount: 5_000_000_000, + recurring: None, + }], + ) + .unwrap(); + + // Check if the new role has the specific permission + let has_sol = swig_wallet + .role_has_permission( + 1, + &Permission::Sol { + amount: 5_000_000_000, + recurring: None, + }, + ) + .unwrap(); + assert!(has_sol); + + // Check if the new role doesn't have all permissions + let has_all_in_new = swig_wallet + .role_has_permission(1, &Permission::All) + .unwrap(); + assert!(!has_all_in_new); +} + +#[test_log::test] +fn should_get_formatted_authority_address() { + let (mut litesvm, main_authority) = setup_test_environment(); + let mut swig_wallet = create_test_wallet(litesvm, &main_authority); + + // Get formatted address for Ed25519 authority + let ed25519_address = swig_wallet.get_formatted_authority_address(0).unwrap(); + assert!(!ed25519_address.is_empty()); + assert!(ed25519_address.len() > 30); // Base58 encoded addresses are typically long + println!("Ed25519 address: {}", ed25519_address); + + // Add a Secp256k1 authority + let wallet = LocalSigner::random(); + let secp_pubkey = wallet + .credential() + .verifying_key() + .to_encoded_point(false) + .to_bytes(); + + swig_wallet + .add_authority( + AuthorityType::Secp256k1, + &secp_pubkey.as_ref()[1..], + vec![Permission::Sol { + amount: 10_000_000_000, + recurring: None, + }], + ) + .unwrap(); + + // Get formatted address for Secp256k1 authority + let secp256k1_address = swig_wallet.get_formatted_authority_address(1).unwrap(); + assert!(!secp256k1_address.is_empty()); + assert!(secp256k1_address.starts_with("0x")); // Ethereum addresses start with 0x + println!("Secp256k1 address: {}", secp256k1_address); +} + +#[test_log::test] +fn should_refresh_permissions() { + let (mut litesvm, main_authority) = setup_test_environment(); + let mut swig_wallet = create_test_wallet(litesvm, &main_authority); + + // Get initial permissions + let initial_permissions = swig_wallet.get_current_permissions().unwrap().to_vec(); + + // Refresh permissions + swig_wallet.refresh_permissions().unwrap(); + + // Get permissions after refresh + let refreshed_permissions = swig_wallet.get_current_permissions().unwrap(); + + // Permissions should be the same + assert_eq!(initial_permissions.len(), refreshed_permissions.len()); + for permission in &initial_permissions { + assert!(refreshed_permissions.contains(permission)); + } +} + +#[test_log::test] +fn should_handle_invalid_role_id() { + let (mut litesvm, main_authority) = setup_test_environment(); + let swig_wallet = create_test_wallet(litesvm, &main_authority); + + // Try to get information for a non-existent role + assert!(swig_wallet.get_authority_type(999).is_err()); + assert!(swig_wallet.get_authority_identity(999).is_err()); + assert!(swig_wallet.is_session_based(999).is_err()); + assert!(swig_wallet.get_role_permissions(999).is_err()); + assert!(swig_wallet.get_formatted_authority_address(999).is_err()); +} + +#[test_log::test] +fn should_handle_permission_checks_with_different_authorities() { + let (mut litesvm, main_authority) = setup_test_environment(); + let mut swig_wallet = create_test_wallet(litesvm, &main_authority); + + // Main authority should have all permissions + assert!(swig_wallet.has_all_permissions().unwrap()); + assert!(swig_wallet.has_permission(&Permission::All).unwrap()); + + // Add an authority with only SOL permissions + let sol_only_authority = Keypair::new(); + swig_wallet + .add_authority( + AuthorityType::Ed25519, + &sol_only_authority.pubkey().to_bytes(), + vec![Permission::Sol { + amount: 5_000_000_000, + recurring: None, + }], + ) + .unwrap(); + + // Switch to the SOL-only authority + swig_wallet + .switch_authority( + 1, + Box::new(Ed25519ClientRole::new(sol_only_authority.pubkey())), + Some(&sol_only_authority), + ) + .unwrap(); + + // This authority should not have all permissions + assert!(!swig_wallet.has_all_permissions().unwrap()); + assert!(!swig_wallet.has_permission(&Permission::All).unwrap()); + + // Print permissions for debug + let perms = swig_wallet.get_current_permissions().unwrap(); + println!("SOL-only authority permissions: {:?}", perms); + + // But should have SOL permissions + assert!(swig_wallet + .has_permission(&Permission::Sol { + amount: 5_000_000_000, + recurring: None, + }) + .unwrap()); +} diff --git a/rust-sdk/src/tests/wallet/mod.rs b/rust-sdk/src/tests/wallet/mod.rs index 27afd606..4daa5449 100644 --- a/rust-sdk/src/tests/wallet/mod.rs +++ b/rust-sdk/src/tests/wallet/mod.rs @@ -1,6 +1,8 @@ pub mod authority_tests; pub mod creation_tests; +pub mod helper_tests; pub mod program_scope_test; +pub mod secp_tests; pub mod session_tests; pub mod sub_accounts_test; pub mod transfer_tests; @@ -24,8 +26,13 @@ use swig_state_x::{ use super::*; use crate::{ - error::SwigError, instruction_builder::AuthorityManager, types::Permission, RecurringConfig, - SwigWallet, + client_role::{ + Ed25519ClientRole, Ed25519SessionClientRole, Secp256k1ClientRole, + Secp256k1SessionClientRole, + }, + error::SwigError, + types::Permission, + RecurringConfig, SwigWallet, }; // Test helper functions @@ -47,10 +54,10 @@ fn setup_test_environment() -> (LiteSVM, Keypair) { fn create_test_wallet(litesvm: LiteSVM, authority: &Keypair) -> SwigWallet { SwigWallet::new( [0; 32], - AuthorityManager::Ed25519(authority.pubkey()), - authority, + Box::new(Ed25519ClientRole::new(authority.pubkey())), authority, "http://localhost:8899".to_string(), + Some(authority), litesvm, ) .unwrap() diff --git a/rust-sdk/src/tests/wallet/program_scope_test.rs b/rust-sdk/src/tests/wallet/program_scope_test.rs index 971889cb..f1cc2122 100644 --- a/rust-sdk/src/tests/wallet/program_scope_test.rs +++ b/rust-sdk/src/tests/wallet/program_scope_test.rs @@ -5,6 +5,7 @@ use solana_program::pubkey::Pubkey; use solana_sdk::{clock::Clock, signature::Keypair, transaction::VersionedTransaction}; use super::*; +use crate::client_role::Ed25519ClientRole; use crate::tests::common::*; #[test_log::test] @@ -33,18 +34,14 @@ fn should_token_transfer_with_program_scope() { let mut swig_wallet = create_test_wallet(litesvm, &main_authority); let swig_ata = swig_wallet.create_ata(&mint_pubkey).unwrap(); - // Setup a RecurringLimit program scope - // Set a limit of 500 tokens per 100 slots - let window_size = 100; - let transfer_limit = 500_u64; - + // Setup a basic program scope let new_authority = Keypair::new(); let permissions = vec![Permission::ProgramScope { program_id: spl_token::ID, target_account: swig_ata, numeric_type: 2, // U64 - limit: None, + limit: Some(1000), window: None, balance_field_start: Some(64), balance_field_end: Some(72), @@ -65,9 +62,12 @@ fn should_token_transfer_with_program_scope() { assert_eq!(swig_with_roles.state.roles, 2); // Switch to the new authority - let authority_manager = AuthorityManager::Ed25519(new_authority.pubkey()); swig_wallet - .switch_authority(1, authority_manager, Some(&new_authority)) + .switch_authority( + 1, + Box::new(Ed25519ClientRole::new(new_authority.pubkey())), + Some(&new_authority), + ) .unwrap(); // Mint initial tokens to swig wallet @@ -152,9 +152,12 @@ fn should_token_transfer_with_recurring_limit_program_scope() { assert_eq!(swig_with_roles.state.roles, 2); // Switch to the new authority - let authority_manager = AuthorityManager::Ed25519(new_authority.pubkey()); swig_wallet - .switch_authority(1, authority_manager, Some(&new_authority)) + .switch_authority( + 1, + Box::new(Ed25519ClientRole::new(new_authority.pubkey())), + Some(&new_authority), + ) .unwrap(); // Mint initial tokens to swig wallet @@ -208,7 +211,9 @@ fn should_token_transfer_with_recurring_limit_program_scope() { }; println!("After transfer, token balance: {}", after_balance); - swig_wallet.display_swig().unwrap(); + // Verify transfer was successful + assert!(sign_ix != solana_sdk::signature::Signature::default()); + assert!(after_balance < before_balance); } // Try to transfer one more batch (should fail) diff --git a/rust-sdk/src/tests/wallet/secp_tests.rs b/rust-sdk/src/tests/wallet/secp_tests.rs new file mode 100644 index 00000000..68fde9eb --- /dev/null +++ b/rust-sdk/src/tests/wallet/secp_tests.rs @@ -0,0 +1,542 @@ +use super::*; +use crate::client_role::{Ed25519ClientRole, Secp256k1ClientRole}; +use alloy_primitives::B256; +use alloy_signer::SignerSync; +use alloy_signer_local::{LocalSigner, PrivateKeySigner}; +use solana_sdk::sysvar::clock::Clock; +use solana_sdk::{ + signature::{Keypair, Signer}, + system_instruction, +}; +use swig_state_x::authority::AuthorityType; + +fn create_secp256k1_wallet() -> (PrivateKeySigner, Vec) { + let wallet = PrivateKeySigner::random(); + let secp_pubkey = wallet + .credential() + .verifying_key() + .to_encoded_point(false) + .to_bytes(); + (wallet, secp_pubkey.as_ref()[1..].to_vec()) +} + +fn get_secp256k1_counter( + swig_wallet: &mut SwigWallet, + authority_pubkey: &[u8], +) -> Result { + let role_id = swig_wallet.get_role_id(authority_pubkey)?; + + let swig_account = swig_wallet.get_swig_account()?; + let account_data = swig_wallet + .litesvm() + .get_account(&swig_account) + .unwrap() + .data; + + let swig_with_roles = swig_state_x::swig::SwigWithRoles::from_bytes(&account_data) + .map_err(|_| SwigError::InvalidSwigData)?; + + let role = swig_with_roles + .get_role(role_id) + .map_err(|_| SwigError::AuthorityNotFound)? + .ok_or(SwigError::AuthorityNotFound)?; + + if matches!(role.authority.authority_type(), AuthorityType::Secp256k1) { + let auth = role + .authority + .as_any() + .downcast_ref::() + .ok_or(SwigError::AuthorityNotFound)?; + Ok(auth.signature_odometer) + } else { + Err(SwigError::AuthorityNotFound) + } +} + +#[test_log::test] +fn test_secp256k1_signature_reuse_error() { + let (mut litesvm, main_authority) = setup_test_environment(); + let mut swig_wallet = create_test_wallet(litesvm, &main_authority); + + let (secp_wallet, secp_pubkey) = create_secp256k1_wallet(); + + swig_wallet + .add_authority( + AuthorityType::Secp256k1, + &secp_pubkey, + vec![Permission::Sol { + amount: 10_000_000_000, + recurring: None, + }], + ) + .unwrap(); + + let signing_fn = Box::new(move |payload: &[u8]| -> [u8; 65] { + let mut hash = [0u8; 32]; + hash.copy_from_slice(&payload[..32]); + let hash = B256::from(hash); + secp_wallet.sign_hash_sync(&hash).unwrap().as_bytes() + }); + + swig_wallet + .switch_authority( + 1, + Box::new(Secp256k1ClientRole::new( + secp_pubkey.clone().into(), + signing_fn, + )), + None, + ) + .unwrap(); + + let swig_pubkey = &swig_wallet.get_swig_account().unwrap(); + + swig_wallet + .litesvm() + .airdrop(&swig_pubkey, 10_000_000_000) + .unwrap(); + + let initial_counter = get_secp256k1_counter(&mut swig_wallet, &secp_pubkey).unwrap(); + assert_eq!(initial_counter, 0); + + let recipient = Keypair::new(); + let transfer_amount = 1_000_000; + + // First transaction should succeed + let transfer_ix = system_instruction::transfer( + &swig_wallet.get_swig_account().unwrap(), + &recipient.pubkey(), + transfer_amount, + ); + let result = swig_wallet.sign(vec![transfer_ix], None); + assert!(result.is_ok(), "First transaction should succeed"); + + // Verify counter was incremented + let counter_after_first = get_secp256k1_counter(&mut swig_wallet, &secp_pubkey).unwrap(); + assert_eq!(counter_after_first, 1); + + // Try to reuse the same signature (this should fail) + let transfer_ix2 = system_instruction::transfer( + &swig_wallet.get_swig_account().unwrap(), + &recipient.pubkey(), + transfer_amount, + ); + let result = swig_wallet.sign(vec![transfer_ix2], None); + + // The transaction should fail due to signature reuse protection + if result.is_err() { + // The error code should correspond to PermissionDeniedSecp256k1SignatureReused + println!("Transaction failed as expected: {:?}", result.err()); + } +} + +#[test_log::test] +fn test_secp256k1_invalid_signature_age_error() { + let (mut litesvm, main_authority) = setup_test_environment(); + let mut swig_wallet = create_test_wallet(litesvm, &main_authority); + + let (secp_wallet, secp_pubkey) = create_secp256k1_wallet(); + + swig_wallet + .add_authority( + AuthorityType::Secp256k1, + &secp_pubkey, + vec![Permission::Sol { + amount: 10_000_000_000, + recurring: None, + }], + ) + .unwrap(); + + let signing_fn = Box::new(move |payload: &[u8]| -> [u8; 65] { + let mut hash = [0u8; 32]; + hash.copy_from_slice(&payload[..32]); + let hash = B256::from(hash); + secp_wallet.sign_hash_sync(&hash).unwrap().as_bytes() + }); + + swig_wallet + .switch_authority( + 1, + Box::new(Secp256k1ClientRole::new( + secp_pubkey.clone().into(), + signing_fn, + )), + None, + ) + .unwrap(); + + let recipient = Keypair::new(); + let transfer_amount = 1_000_000; + + // Advance the slot by more than MAX_SIGNATURE_AGE_IN_SLOTS (60) + // This simulates an old signature that should be rejected + swig_wallet.litesvm().warp_to_slot(100); + + // Try to execute transaction with old signature + let transfer_ix = system_instruction::transfer( + &swig_wallet.get_swig_account().unwrap(), + &recipient.pubkey(), + transfer_amount, + ); + let result = swig_wallet.sign(vec![transfer_ix], None); + + // The transaction should fail due to invalid signature age + if result.is_err() { + // Check if it's the expected invalid signature age error + // The error code should correspond to PermissionDeniedSecp256k1InvalidSignatureAge + println!( + "Transaction failed due to old signature as expected: {:?}", + result.err() + ); + } +} + +#[test_log::test] +fn test_secp256k1_invalid_signature_error() { + let (mut litesvm, main_authority) = setup_test_environment(); + let mut swig_wallet = create_test_wallet(litesvm, &main_authority); + + let (secp_wallet, secp_pubkey) = create_secp256k1_wallet(); + + swig_wallet + .add_authority( + AuthorityType::Secp256k1, + &secp_pubkey, + vec![Permission::Sol { + amount: 10_000_000_000, + recurring: None, + }], + ) + .unwrap(); + + let different_wallet = PrivateKeySigner::random(); + + let signing_fn = Box::new(move |payload: &[u8]| -> [u8; 65] { + let mut hash = [0u8; 32]; + hash.copy_from_slice(&payload[..32]); + let hash = B256::from(hash); + different_wallet.sign_hash_sync(&hash).unwrap().as_bytes() + }); + + swig_wallet + .switch_authority( + 1, + Box::new(Secp256k1ClientRole::new( + secp_pubkey.clone().into(), + signing_fn, + )), + None, + ) + .unwrap(); + + let recipient = Keypair::new(); + let transfer_amount = 1_000_000; + + // Try to execute transaction with invalid signature + let transfer_ix = system_instruction::transfer( + &swig_wallet.get_swig_account().unwrap(), + &recipient.pubkey(), + transfer_amount, + ); + let result = swig_wallet.sign(vec![transfer_ix], None); + + // The transaction should fail due to invalid signature + if result.is_err() { + // The error code should correspond to PermissionDeniedSecp256k1InvalidSignature + println!( + "Transaction failed due to invalid signature as expected: {:?}", + result.err() + ); + } +} + +#[test_log::test] +fn test_secp256k1_invalid_hash_error() { + let (mut litesvm, main_authority) = setup_test_environment(); + let mut swig_wallet = create_test_wallet(litesvm, &main_authority); + + let (secp_wallet, secp_pubkey) = create_secp256k1_wallet(); + + swig_wallet + .add_authority( + AuthorityType::Secp256k1, + &secp_pubkey, + vec![Permission::Sol { + amount: 10_000_000_000, + recurring: None, + }], + ) + .unwrap(); + + let signing_fn = Box::new(move |payload: &[u8]| -> [u8; 65] { + let mut hash = [0u8; 32]; + hash.copy_from_slice(&payload[..32]); + let hash = B256::from(hash); + secp_wallet.sign_hash_sync(&hash).unwrap().as_bytes() + }); + + swig_wallet + .switch_authority( + 1, + Box::new(Secp256k1ClientRole::new( + secp_pubkey.clone().into(), + signing_fn, + )), + None, + ) + .unwrap(); + + // This simulates a scenario where hash computation would fail + let recipient = Keypair::new(); + let transfer_amount = 1_000_000; + + // Try to execute transaction with potentially corrupted data + let transfer_ix = system_instruction::transfer( + &swig_wallet.get_swig_account().unwrap(), + &recipient.pubkey(), + transfer_amount, + ); + let result = swig_wallet.sign(vec![transfer_ix], None); + + // The transaction should fail due to invalid hash + if result.is_err() { + // The error code should correspond to PermissionDeniedSecp256k1InvalidHash + println!( + "Transaction failed due to invalid hash as expected: {:?}", + result.err() + ); + } +} + +#[test_log::test] +fn test_secp256k1_counter_increment() { + let (mut litesvm, main_authority) = setup_test_environment(); + let mut swig_wallet = create_test_wallet(litesvm, &main_authority); + + let (secp_wallet, secp_pubkey) = create_secp256k1_wallet(); + + swig_wallet + .add_authority( + AuthorityType::Secp256k1, + &secp_pubkey, + vec![Permission::Sol { + amount: 10_000_000_000, + recurring: None, + }], + ) + .unwrap(); + + let signing_fn = Box::new(move |payload: &[u8]| -> [u8; 65] { + let mut hash = [0u8; 32]; + hash.copy_from_slice(&payload[..32]); + let hash = B256::from(hash); + secp_wallet.sign_hash_sync(&hash).unwrap().as_bytes() + }); + + swig_wallet + .switch_authority( + 1, + Box::new(Secp256k1ClientRole::new( + secp_pubkey.clone().into(), + signing_fn.clone(), + )), + None, + ) + .unwrap(); + + let initial_counter = get_secp256k1_counter(&mut swig_wallet, &secp_pubkey).unwrap(); + assert_eq!(initial_counter, 0); + + // Execute multiple transactions and verify counter increments + let recipient = Keypair::new(); + let transfer_amount = 1_000_000; + + let swig_pubkey = &swig_wallet.get_swig_account().unwrap(); + + swig_wallet + .litesvm() + .airdrop(&swig_pubkey, 10_000_000_000) + .unwrap(); + + for i in 1..=5 { + let transfer_ix = + system_instruction::transfer(&swig_pubkey, &recipient.pubkey(), transfer_amount); + let result = swig_wallet.sign(vec![transfer_ix], None); + assert!(result.is_ok(), "Transaction {} should succeed", i); + + let litesvm = swig_wallet.litesvm(); + litesvm.warp_to_slot(litesvm.get_sysvar::().slot + 1); + + let current_counter = get_secp256k1_counter(&mut swig_wallet, &secp_pubkey).unwrap(); + assert_eq!( + current_counter, i, + "Counter should be {} after transaction {}", + i, i + ); + } + + let before_switch_odo = swig_wallet.get_odometer().unwrap(); + + swig_wallet + .switch_authority( + 0, + Box::new(Ed25519ClientRole::new(main_authority.pubkey())), + None, + ) + .unwrap(); + + swig_wallet + .switch_authority( + 1, + Box::new(Secp256k1ClientRole::new( + secp_pubkey.clone().into(), + signing_fn, + )), + None, + ) + .unwrap(); + + assert_eq!( + before_switch_odo, + swig_wallet.get_odometer().unwrap(), + "Odometer state not consistent" + ); +} + +#[test_log::test] +fn test_secp256k1_authority_odometer() { + let (mut litesvm, main_authority) = setup_test_environment(); + let mut swig_wallet = create_test_wallet(litesvm, &main_authority); + + let (secp_wallet, secp_pubkey) = create_secp256k1_wallet(); + + swig_wallet + .add_authority( + AuthorityType::Secp256k1, + &secp_pubkey, + vec![Permission::Sol { + amount: 10_000_000_000, + recurring: None, + }], + ) + .unwrap(); + + // Fund the wallet + let swig_pubkey = &swig_wallet.get_swig_account().unwrap(); + swig_wallet + .litesvm() + .airdrop(&swig_pubkey, 10_000_000_000) + .unwrap(); + + let signing_fn = Box::new(move |payload: &[u8]| -> [u8; 65] { + let mut hash = [0u8; 32]; + hash.copy_from_slice(&payload[..32]); + let hash = B256::from(hash); + secp_wallet.sign_hash_sync(&hash).unwrap().as_bytes() + }); + + swig_wallet + .switch_authority( + 1, + Box::new(Secp256k1ClientRole::new( + secp_pubkey.clone().into(), + signing_fn, + )), + None, + ) + .unwrap(); + + let initial_counter = get_secp256k1_counter(&mut swig_wallet, &secp_pubkey).unwrap(); + assert_eq!(initial_counter, 0); + + // Execute transactions and verify counter increments for Secp256k1 authority + let recipient = Keypair::new(); + let transfer_amount = 1_000_000; + + for i in 1..=3 { + let transfer_ix = system_instruction::transfer( + &swig_wallet.get_swig_account().unwrap(), + &recipient.pubkey(), + transfer_amount, + ); + let result = swig_wallet.sign(vec![transfer_ix], None); + assert!(result.is_ok(), "Secp256k1 transaction {} should succeed", i); + + let current_counter = get_secp256k1_counter(&mut swig_wallet, &secp_pubkey).unwrap(); + assert_eq!( + current_counter, i, + "Secp256k1 counter should be {} after transaction {}", + i, i + ); + } +} + +#[test_log::test] +fn test_secp256k1_odometer_wrapping() { + let (mut litesvm, main_authority) = setup_test_environment(); + let mut swig_wallet = create_test_wallet(litesvm, &main_authority); + + let (secp_wallet, secp_pubkey) = create_secp256k1_wallet(); + + swig_wallet + .add_authority( + AuthorityType::Secp256k1, + &secp_pubkey, + vec![Permission::Sol { + amount: 10_000_000_000, + recurring: None, + }], + ) + .unwrap(); + + let swig_pubkey = &swig_wallet.get_swig_account().unwrap(); + + swig_wallet + .litesvm() + .airdrop(&swig_pubkey, 10_000_000_000) + .unwrap(); + + let signing_fn = Box::new(move |payload: &[u8]| -> [u8; 65] { + let mut hash = [0u8; 32]; + hash.copy_from_slice(&payload[..32]); + let hash = B256::from(hash); + secp_wallet.sign_hash_sync(&hash).unwrap().as_bytes() + }); + + swig_wallet + .switch_authority( + 1, + Box::new(Secp256k1ClientRole::new( + secp_pubkey.clone().into(), + signing_fn, + )), + None, + ) + .unwrap(); + + let recipient = Keypair::new(); + let transfer_amount = 1_000_000; + + // Execute transactions to test odometer behavior + for i in 1..=10 { + let transfer_ix = system_instruction::transfer( + &swig_wallet.get_swig_account().unwrap(), + &recipient.pubkey(), + transfer_amount, + ); + let result = swig_wallet.sign(vec![transfer_ix], None); + assert!(result.is_ok(), "Transaction {} should succeed", i); + + let current_counter = get_secp256k1_counter(&mut swig_wallet, &secp_pubkey).unwrap(); + assert_eq!( + current_counter, i, + "Counter should be {} after transaction {}", + i, i + ); + } + + // Verify that the odometer continues to work correctly after multiple transactions + let final_counter = get_secp256k1_counter(&mut swig_wallet, &secp_pubkey).unwrap(); + assert_eq!(final_counter, 10, "Final counter should be 10"); +} diff --git a/rust-sdk/src/tests/wallet/session_tests.rs b/rust-sdk/src/tests/wallet/session_tests.rs index 5109b4a8..f4979eff 100644 --- a/rust-sdk/src/tests/wallet/session_tests.rs +++ b/rust-sdk/src/tests/wallet/session_tests.rs @@ -9,6 +9,7 @@ use swig_state_x::authority::{ }; use super::*; +use crate::client_role::{Ed25519SessionClientRole, Secp256k1SessionClientRole}; #[test_log::test] fn should_create_ed25519_session_authority() { @@ -17,14 +18,16 @@ fn should_create_ed25519_session_authority() { let mut swig_wallet = SwigWallet::new( [0; 32], - AuthorityManager::Ed25519Session(CreateEd25519SessionAuthority::new( - main_authority.pubkey().to_bytes(), - session_key.pubkey().to_bytes(), - 100, + Box::new(Ed25519SessionClientRole::new( + CreateEd25519SessionAuthority::new( + main_authority.pubkey().to_bytes(), + session_key.pubkey().to_bytes(), + 100, + ), )), &main_authority, - &main_authority, "http://localhost:8899".to_string(), + Some(&main_authority), litesvm, ) .unwrap(); @@ -40,7 +43,10 @@ fn should_create_ed25519_session_authority() { .create_session(new_session_key.pubkey(), 100) .unwrap(); - swig_wallet.display_swig().unwrap(); + // Verify session authority was created successfully + assert!(swig_wallet.get_swig_account().is_ok()); + assert_eq!(swig_wallet.get_role_count().unwrap(), 1); + assert!(swig_wallet.get_balance().unwrap() > 0); } #[test_log::test] @@ -62,20 +68,22 @@ fn should_create_secp256k1_session_authority() { let swig_wallet = SwigWallet::new( [0; 32], - AuthorityManager::Secp256k1Session( + Box::new(Secp256k1SessionClientRole::new( CreateSecp256k1SessionAuthority::new( secp_pubkey[1..].try_into().unwrap(), [0; 32], 100, ), Box::new(sign_fn), - ), - &main_authority, + )), &main_authority, "http://localhost:8899".to_string(), + None, litesvm, ) .unwrap(); - swig_wallet.display_swig().unwrap(); + // Verify session authority was created successfully + assert!(swig_wallet.get_swig_account().is_ok()); + assert_eq!(swig_wallet.get_role_count().unwrap(), 1); } diff --git a/rust-sdk/src/tests/wallet/sub_accounts_test.rs b/rust-sdk/src/tests/wallet/sub_accounts_test.rs index eeffaffc..e419e4e3 100644 --- a/rust-sdk/src/tests/wallet/sub_accounts_test.rs +++ b/rust-sdk/src/tests/wallet/sub_accounts_test.rs @@ -4,15 +4,21 @@ use alloy_signer_local::LocalSigner; use solana_program::system_instruction; use solana_sdk::signature::{Keypair, Signer}; use spl_token::ID as TOKEN_PROGRAM_ID; +use swig_interface::program_id; +use swig_state_x::{ + authority::AuthorityType, + swig::{sub_account_seeds, SwigWithRoles}, +}; use super::*; +use crate::client_role::Ed25519ClientRole; #[test_log::test] fn test_sub_account_creation_and_setup() { let (mut litesvm, main_authority) = setup_test_environment(); let mut swig_wallet = create_test_wallet(litesvm, &main_authority); - // Create a secondary authority with sub-account permissions + // Setup secondary authority let secondary_authority = Keypair::new(); let secondary_role_id = 1; swig_wallet @@ -25,14 +31,11 @@ fn test_sub_account_creation_and_setup() { ) .unwrap(); - // Verify initial setup - swig_wallet.display_swig().unwrap(); - // Switch to secondary authority and create sub-account swig_wallet .switch_authority( secondary_role_id, - AuthorityManager::Ed25519(secondary_authority.pubkey()), + Box::new(Ed25519ClientRole::new(secondary_authority.pubkey())), Some(&secondary_authority), ) .unwrap(); @@ -71,8 +74,8 @@ fn test_sub_account_sol_operations() { swig_wallet .switch_authority( secondary_role_id, - AuthorityManager::Ed25519(secondary_authority.pubkey()), - Some(&secondary_authority), // Pass the actual keypair + Box::new(Ed25519ClientRole::new(secondary_authority.pubkey())), + Some(&secondary_authority), ) .unwrap(); @@ -149,7 +152,7 @@ fn test_sub_account_sol_operations() { swig_wallet .switch_authority( 0, - AuthorityManager::Ed25519(main_authority.pubkey()), + Box::new(Ed25519ClientRole::new(main_authority.pubkey())), Some(&main_authority), ) .unwrap(); @@ -189,7 +192,7 @@ fn test_sub_account_token_operations() { swig_wallet .switch_authority( secondary_role_id, - AuthorityManager::Ed25519(secondary_authority.pubkey()), + Box::new(Ed25519ClientRole::new(secondary_authority.pubkey())), Some(&secondary_authority), ) .unwrap(); @@ -225,7 +228,7 @@ fn test_sub_account_token_operations() { swig_wallet .switch_authority( 0, - AuthorityManager::Ed25519(main_authority.pubkey()), + Box::new(Ed25519ClientRole::new(main_authority.pubkey())), Some(&main_authority), ) .unwrap(); @@ -262,7 +265,7 @@ fn test_sub_account_toggle_operations() { swig_wallet .switch_authority( secondary_role_id, - AuthorityManager::Ed25519(secondary_authority.pubkey()), + Box::new(Ed25519ClientRole::new(secondary_authority.pubkey())), Some(&secondary_authority), ) .unwrap(); @@ -277,7 +280,7 @@ fn test_sub_account_toggle_operations() { swig_wallet .switch_authority( 0, - AuthorityManager::Ed25519(main_authority.pubkey()), + Box::new(Ed25519ClientRole::new(main_authority.pubkey())), Some(&main_authority), ) .unwrap(); @@ -312,7 +315,7 @@ fn test_secondary_authority_operations() { swig_wallet .switch_authority( secondary_role_id, - AuthorityManager::Ed25519(secondary_authority.pubkey()), + Box::new(Ed25519ClientRole::new(secondary_authority.pubkey())), Some(&secondary_authority), ) .unwrap(); @@ -340,7 +343,12 @@ fn test_secondary_authority_operations() { println!("Secondary authority signature: {:?}", signature); // Verify final state - swig_wallet.display_swig().unwrap(); + assert!(signature != solana_sdk::signature::Signature::default()); + assert_eq!( + swig_wallet.get_current_role_id().unwrap(), + secondary_role_id + ); + assert!(swig_wallet.get_sub_account().unwrap().is_some()); } #[test_log::test] @@ -374,7 +382,7 @@ fn test_sub_account_error_cases() { swig_wallet .switch_authority( 1, - AuthorityManager::Ed25519(unauthorized_authority.pubkey()), + Box::new(Ed25519ClientRole::new(unauthorized_authority.pubkey())), Some(&unauthorized_authority), ) .unwrap(); @@ -398,7 +406,7 @@ fn test_sub_account_error_cases() { swig_wallet .switch_authority( 0, - AuthorityManager::Ed25519(main_authority.pubkey()), + Box::new(Ed25519ClientRole::new(main_authority.pubkey())), Some(&main_authority), ) .unwrap(); @@ -416,7 +424,7 @@ fn test_sub_account_error_cases() { swig_wallet .switch_authority( 2, - AuthorityManager::Ed25519(main_authority.pubkey()), + Box::new(Ed25519ClientRole::new(main_authority.pubkey())), Some(&main_authority), ) .unwrap(); @@ -441,7 +449,7 @@ fn test_sub_account_error_cases() { swig_wallet .switch_authority( 1, - AuthorityManager::Ed25519(unauthorized_authority.pubkey()), + Box::new(Ed25519ClientRole::new(unauthorized_authority.pubkey())), Some(&unauthorized_authority), ) .unwrap(); @@ -464,7 +472,7 @@ fn test_sub_account_error_cases() { swig_wallet .switch_authority( 0, - AuthorityManager::Ed25519(main_authority.pubkey()), + Box::new(Ed25519ClientRole::new(main_authority.pubkey())), Some(&main_authority), ) .unwrap(); diff --git a/rust-sdk/src/tests/wallet/transfer_tests.rs b/rust-sdk/src/tests/wallet/transfer_tests.rs index ddbccac5..89910ecb 100644 --- a/rust-sdk/src/tests/wallet/transfer_tests.rs +++ b/rust-sdk/src/tests/wallet/transfer_tests.rs @@ -2,6 +2,7 @@ use solana_program::system_instruction; use solana_sdk::signature::{Keypair, Signer}; use super::*; +use crate::client_role::Ed25519ClientRole; #[test_log::test] fn should_transfer_within_limits() { @@ -28,7 +29,7 @@ fn should_transfer_within_limits() { swig_wallet .switch_authority( 1, - AuthorityManager::Ed25519(secondary_authority.pubkey()), + Box::new(Ed25519ClientRole::new(secondary_authority.pubkey())), Some(&secondary_authority), ) .unwrap(); @@ -45,8 +46,12 @@ fn should_transfer_within_limits() { // Transfer within limits let transfer_ix = system_instruction::transfer(&swig_account, &recipient.pubkey(), 100_000_000); - assert!(swig_wallet.sign(vec![transfer_ix], None).is_ok()); - swig_wallet.display_swig().unwrap(); + let signature = swig_wallet.sign(vec![transfer_ix], None).unwrap(); + println!("signature: {:?}", signature); + + // Verify transfer was successful + assert!(signature != solana_sdk::signature::Signature::default()); + assert_eq!(swig_wallet.get_current_role_id().unwrap(), 1); } #[test_log::test] @@ -74,7 +79,7 @@ fn should_fail_transfer_beyond_limits() { swig_wallet .switch_authority( 1, - AuthorityManager::Ed25519(secondary_authority.pubkey()), + Box::new(Ed25519ClientRole::new(secondary_authority.pubkey())), Some(&secondary_authority), ) .unwrap(); @@ -121,7 +126,15 @@ fn should_get_role_id() { ) .unwrap(); - swig_wallet.display_swig().unwrap(); + // Verify authorities were added correctly + assert_eq!(swig_wallet.get_role_count().unwrap(), 3); + assert!(swig_wallet + .get_role_id(&authority_2.pubkey().to_bytes()) + .is_ok()); + assert!(swig_wallet + .get_role_id(&authority_3.pubkey().to_bytes()) + .is_ok()); + let role_id = swig_wallet .get_role_id(&authority_3.pubkey().to_bytes()) .unwrap(); diff --git a/rust-sdk/src/tests/wallet_tests.rs b/rust-sdk/src/tests/wallet_tests.rs deleted file mode 100644 index b89f0a0c..00000000 --- a/rust-sdk/src/tests/wallet_tests.rs +++ /dev/null @@ -1,524 +0,0 @@ -use alloy_primitives::B256; -use alloy_signer::SignerSync; -use alloy_signer_local::LocalSigner; -use litesvm::LiteSVM; -use solana_program::pubkey::Pubkey; -use solana_sdk::signature::{Keypair, Signer}; -use swig_interface::swig; -use swig_state_x::{ - authority::{ - ed25519::{CreateEd25519SessionAuthority, Ed25519SessionAuthority}, - secp256k1::{CreateSecp256k1SessionAuthority, Secp256k1SessionAuthority}, - AuthorityType, - }, - swig::{swig_account_seeds, SwigWithRoles}, - IntoBytes, -}; - -use super::*; -use crate::{ - error::SwigError, instruction_builder::AuthorityManager, types::Permission, RecurringConfig, - SwigWallet, -}; - -// Test helper functions -fn setup_test_environment() -> (LiteSVM, Keypair) { - let mut litesvm = LiteSVM::new(); - let main_authority = Keypair::new(); - - litesvm - .add_program_from_file(Pubkey::new_from_array(swig::ID), "../target/deploy/swig.so") - .map_err(|_| anyhow::anyhow!("Failed to load program")) - .unwrap(); - litesvm - .airdrop(&main_authority.pubkey(), 10_000_000_000) - .unwrap(); - - (litesvm, main_authority) -} - -fn create_test_wallet(litesvm: LiteSVM, authority: &Keypair) -> SwigWallet { - SwigWallet::new( - [0; 32], - AuthorityManager::Ed25519(authority.pubkey()), - authority, - authority, - "http://localhost:8899".to_string(), - litesvm, - ) - .unwrap() -} - -mod wallet_creation_tests { - use super::*; - - #[test_log::test] - fn should_create_ed25519_wallet() { - let (litesvm, main_authority) = setup_test_environment(); - let mut swig_wallet = create_test_wallet(litesvm, &main_authority); - swig_wallet.display_swig().unwrap(); - - let swig_pubkey = swig_wallet.get_swig_account().unwrap(); - let swig_data = swig_wallet.litesvm().get_account(&swig_pubkey).unwrap(); - let swig_with_roles = SwigWithRoles::from_bytes(&swig_data.data).unwrap(); - - assert_eq!(swig_with_roles.state.id, [0; 32]); - } - - #[test_log::test] - fn should_create_secp256k1_wallet() { - let (mut litesvm, main_authority) = setup_test_environment(); - let wallet = LocalSigner::random(); - let secp_pubkey = wallet - .credential() - .verifying_key() - .to_encoded_point(false) - .to_bytes(); - - let sign_fn = move |payload: &[u8]| -> [u8; 65] { - let mut hash = [0u8; 32]; - hash.copy_from_slice(&payload[..32]); - let hash = B256::from(hash); - let tsig = wallet - .sign_hash_sync(&hash) - .map_err(|_| SwigError::InvalidSecp256k1) - .unwrap() - .as_bytes(); - let mut sig = [0u8; 65]; - sig.copy_from_slice(&tsig); - sig - }; - - let swig_wallet = SwigWallet::new( - [0; 32], - AuthorityManager::Secp256k1(secp_pubkey, Box::new(sign_fn)), - &main_authority, - &main_authority, - "http://localhost:8899".to_string(), - litesvm, - ) - .unwrap(); - - swig_wallet.display_swig().unwrap(); - } -} - -mod session_authority_tests { - use super::*; - - #[test_log::test] - fn should_create_ed25519_session_authority() { - let (mut litesvm, main_authority) = setup_test_environment(); - let session_key = Keypair::new(); - - let mut swig_wallet = SwigWallet::new( - [0; 32], - AuthorityManager::Ed25519Session(CreateEd25519SessionAuthority::new( - main_authority.pubkey().to_bytes(), - session_key.pubkey().to_bytes(), - 100, - )), - &main_authority, - &main_authority, - "http://localhost:8899".to_string(), - litesvm, - ) - .unwrap(); - - let swig_pubkey = swig_wallet.get_swig_account().unwrap(); - swig_wallet - .litesvm() - .airdrop(&swig_pubkey, 10_000_000_000) - .unwrap(); - - let new_session_key = Keypair::new(); - swig_wallet - .create_session(new_session_key.pubkey(), 100) - .unwrap(); - - swig_wallet.display_swig().unwrap(); - } - - #[test_log::test] - fn should_create_secp256k1_session_authority() { - let (mut litesvm, main_authority) = setup_test_environment(); - let wallet = LocalSigner::random(); - let secp_pubkey = wallet - .credential() - .verifying_key() - .to_encoded_point(false) - .to_bytes(); - - let sign_fn = move |payload: &[u8]| -> [u8; 65] { - let mut hash = [0u8; 32]; - hash.copy_from_slice(&payload[..32]); - let hash = B256::from(hash); - wallet.sign_hash_sync(&hash).unwrap().as_bytes() - }; - - let swig_wallet = SwigWallet::new( - [0; 32], - AuthorityManager::Secp256k1Session( - CreateSecp256k1SessionAuthority::new( - secp_pubkey[1..].try_into().unwrap(), - [0; 32], - 100, - ), - Box::new(sign_fn), - ), - &main_authority, - &main_authority, - "http://localhost:8899".to_string(), - litesvm, - ) - .unwrap(); - - swig_wallet.display_swig().unwrap(); - } -} - -mod authority_management_tests { - use super::*; - - #[test_log::test] - fn should_manage_authorities_successfully() { - let (mut litesvm, main_authority) = setup_test_environment(); - let mut swig_wallet = create_test_wallet(litesvm, &main_authority); - let secondary_authority = Keypair::new(); - - // Add secondary authority with SOL permission - swig_wallet - .add_authority( - AuthorityType::Ed25519, - &secondary_authority.pubkey().to_bytes(), - vec![Permission::Sol { - amount: 10_000_000_000, - recurring: None, - }], - ) - .unwrap(); - - // Verify both authorities exist - swig_wallet.display_swig().unwrap(); - - // Remove secondary authority - swig_wallet - .remove_authority(&secondary_authority.pubkey().to_bytes()) - .unwrap(); - - swig_wallet.display_swig().unwrap(); - - // Add third authority with recurring permissions - let third_authority = Keypair::new(); - - swig_wallet - .add_authority( - AuthorityType::Ed25519, - &third_authority.pubkey().to_bytes(), - vec![Permission::Sol { - amount: 10_000_000_000, - recurring: Some(RecurringConfig::new(100)), - }], - ) - .unwrap(); - - swig_wallet.display_swig().unwrap(); - - // Switch to third authority - swig_wallet - .switch_authority(1, AuthorityManager::Ed25519(third_authority.pubkey())) - .unwrap(); - - swig_wallet - .authenticate_authority(&third_authority.pubkey().to_bytes()) - .unwrap(); - } - - #[test_log::test] - fn should_add_secp256k1_authority() { - let (mut litesvm, main_authority) = setup_test_environment(); - let mut swig_wallet = create_test_wallet(litesvm, &main_authority); - let secondary_authority = Keypair::new(); - - let wallet = LocalSigner::random(); - println!("wallet: {:?}", wallet.address()); - - let wallet2 = wallet.clone(); - let secp_pubkey = wallet - .credential() - .verifying_key() - .to_encoded_point(false) - .to_bytes(); - - let sec1_bytes = wallet2.credential().verifying_key().to_sec1_bytes(); - let secp1_pubkey = sec1_bytes.as_ref(); - - let authority_hex = hex::encode([&[0x4].as_slice(), secp1_pubkey].concat()); - //get eth address from public key - let mut hasher = solana_sdk::keccak::Hasher::default(); - hasher.hash(authority_hex.as_bytes()); - let hash = hasher.result(); - let address = format!("0x{}", hex::encode(&hash.0[12..32])); - println!("address: {:?}", address); - - println!( - "\t\tAuthority Public Key: 0x{} address {}", - authority_hex, address - ); - println!("secp_pubkey length: {:?}", secp_pubkey); - println!("secp1_pubkey length: {:?}", secp1_pubkey); - // Add secondary authority with SOL permission - swig_wallet - .add_authority( - AuthorityType::Secp256k1, - &secp_pubkey.as_ref()[1..], - vec![Permission::Sol { - amount: 10_000_000_000, - recurring: None, - }], - ) - .unwrap(); - - // Verify both authorities exist - swig_wallet.display_swig().unwrap(); - - // Remove secondary authority - swig_wallet - .remove_authority(&secp_pubkey.as_ref()[1..]) - .unwrap(); - - swig_wallet.display_swig().unwrap(); - - // Add third authority with recurring permissions - let third_authority = Keypair::new(); - - swig_wallet - .add_authority( - AuthorityType::Ed25519, - &third_authority.pubkey().to_bytes(), - vec![Permission::Sol { - amount: 10_000_000_000, - recurring: Some(RecurringConfig::new(100)), - }], - ) - .unwrap(); - - swig_wallet.display_swig().unwrap(); - - // Switch to third authority - swig_wallet - .switch_authority(1, AuthorityManager::Ed25519(third_authority.pubkey())) - .unwrap(); - - swig_wallet - .authenticate_authority(&third_authority.pubkey().to_bytes()) - .unwrap(); - } - - #[test_log::test] - fn should_switch_authority_and_payer() { - let (mut litesvm, main_authority) = setup_test_environment(); - let secondary_authority = Keypair::new(); - litesvm - .airdrop(&secondary_authority.pubkey(), 10_000_000_000) - .unwrap(); - - let mut swig_wallet = create_test_wallet(litesvm, &main_authority); - - // Add and switch to secondary authority - swig_wallet - .add_authority( - AuthorityType::Ed25519, - &secondary_authority.pubkey().to_bytes(), - vec![Permission::Sol { - amount: 10_000_000_000, - recurring: Some(RecurringConfig::new(100)), - }], - ) - .unwrap(); - - swig_wallet - .switch_authority(1, AuthorityManager::Ed25519(secondary_authority.pubkey())) - .unwrap(); - - swig_wallet.switch_payer(&secondary_authority).unwrap(); - swig_wallet.display_swig().unwrap(); - } - - #[test_log::test] - fn should_replace_authority() { - let (mut litesvm, main_authority) = setup_test_environment(); - let mut swig_wallet = create_test_wallet(litesvm, &main_authority); - let old_authority = Keypair::new(); - let new_authority = Keypair::new(); - - println!("old authority: {:?}", old_authority.pubkey()); - println!("new authority: {:?}", new_authority.pubkey()); - // Add old authority with SOL permission - swig_wallet - .add_authority( - AuthorityType::Ed25519, - &old_authority.pubkey().to_bytes(), - vec![Permission::Sol { - amount: 10_000_000_000, - recurring: None, - }], - ) - .unwrap(); - - // Verify old authority exists - swig_wallet.display_swig().unwrap(); - - // Replace old authority with new authority - swig_wallet - .replace_authority( - 1, - AuthorityType::Ed25519, - &new_authority.pubkey().to_bytes(), - vec![Permission::Sol { - amount: 5_000_000_000, // Different amount to verify the replacement - recurring: None, - }], - ) - .unwrap(); - - // Verify the replacement - swig_wallet.display_swig().unwrap(); - - // Try to authenticate with new authority (should succeed) - assert!(swig_wallet - .authenticate_authority(&new_authority.pubkey().to_bytes()) - .is_ok()); - - // Try to authenticate with old authority (should fail) - assert!(swig_wallet - .authenticate_authority(&old_authority.pubkey().to_bytes()) - .is_err()); - } -} - -mod transfer_tests { - use solana_program::system_instruction; - - use super::*; - - #[test_log::test] - fn should_transfer_within_limits() { - let (mut litesvm, main_authority) = setup_test_environment(); - let secondary_authority = Keypair::new(); - litesvm - .airdrop(&secondary_authority.pubkey(), 10_000_000_000) - .unwrap(); - - let mut swig_wallet = create_test_wallet(litesvm, &main_authority); - - // Setup secondary authority with permissions - swig_wallet - .add_authority( - AuthorityType::Ed25519, - &secondary_authority.pubkey().to_bytes(), - vec![Permission::Sol { - amount: 1_000_000_000, - recurring: None, - }], - ) - .unwrap(); - - swig_wallet - .switch_authority(1, AuthorityManager::Ed25519(secondary_authority.pubkey())) - .unwrap(); - swig_wallet.switch_payer(&secondary_authority).unwrap(); - - let swig_account = swig_wallet.get_swig_account().unwrap(); - let recipient = Keypair::new(); - - // Airdrop funds to swig account - swig_wallet - .litesvm() - .airdrop(&swig_account, 5_000_000_000) - .unwrap(); - - // Transfer within limits - let transfer_ix = - system_instruction::transfer(&swig_account, &recipient.pubkey(), 100_000_000); - - assert!(swig_wallet.sign(vec![transfer_ix], None).is_ok()); - swig_wallet.display_swig().unwrap(); - } - - #[test_log::test] - fn should_fail_transfer_beyond_limits() { - let (mut litesvm, main_authority) = setup_test_environment(); - let secondary_authority = Keypair::new(); - litesvm - .airdrop(&secondary_authority.pubkey(), 10_000_000_000) - .unwrap(); - - let mut swig_wallet = create_test_wallet(litesvm, &main_authority); - - // Add secondary authority with limited SOL permission - swig_wallet - .add_authority( - AuthorityType::Ed25519, - &secondary_authority.pubkey().to_bytes(), - vec![Permission::Sol { - amount: 1_000_000_000, - recurring: None, - }], - ) - .unwrap(); - - swig_wallet - .switch_authority(1, AuthorityManager::Ed25519(secondary_authority.pubkey())) - .unwrap(); - swig_wallet.switch_payer(&secondary_authority).unwrap(); - - // Attempt transfer beyond limits - let recipient = Keypair::new(); - let transfer_ix = system_instruction::transfer( - &swig_wallet.get_swig_account().unwrap(), - &recipient.pubkey(), - 2_000_000_000, // Amount greater than permission limit - ); - - assert!(swig_wallet.sign(vec![transfer_ix], None).is_err()); - } - - #[test_log::test] - fn should_get_role_id() { - let (mut litesvm, main_authority) = setup_test_environment(); - let mut swig_wallet = create_test_wallet(litesvm, &main_authority); - - let authority_2 = Keypair::new(); - let authority_3 = Keypair::new(); - - swig_wallet - .add_authority( - AuthorityType::Ed25519, - &authority_2.pubkey().to_bytes(), - vec![Permission::Sol { - amount: 10_000_000_000, - recurring: None, - }], - ) - .unwrap(); - - swig_wallet - .add_authority( - AuthorityType::Ed25519, - &authority_3.pubkey().to_bytes(), - vec![Permission::Sol { - amount: 10_000_000_000, - recurring: None, - }], - ) - .unwrap(); - - swig_wallet.display_swig().unwrap(); - let role_id = swig_wallet - .get_role_id(&authority_3.pubkey().to_bytes()) - .unwrap(); - println!("role_id: {:?}", role_id); - assert_eq!(role_id, 2); - } -} diff --git a/rust-sdk/src/types.rs b/rust-sdk/src/types.rs index 54605b0c..ca8eea76 100644 --- a/rust-sdk/src/types.rs +++ b/rust-sdk/src/types.rs @@ -1,24 +1,28 @@ use solana_program::pubkey::Pubkey; use swig_interface::ClientAction; -use swig_state_x::action::{ - all::All, - manage_authority::ManageAuthority, - program::Program, - program_scope::{NumericType, ProgramScope, ProgramScopeType}, - sol_limit::SolLimit, - sol_recurring_limit::SolRecurringLimit, - stake_all::StakeAll, - stake_limit::StakeLimit, - stake_recurring_limit::StakeRecurringLimit, - sub_account::SubAccount, - token_limit::TokenLimit, - token_recurring_limit::TokenRecurringLimit, +use swig_state_x::{ + action::{ + all::All, + manage_authority::ManageAuthority, + program::Program, + program_scope::{NumericType, ProgramScope, ProgramScopeType}, + sol_limit::SolLimit, + sol_recurring_limit::SolRecurringLimit, + stake_all::StakeAll, + stake_limit::StakeLimit, + stake_recurring_limit::StakeRecurringLimit, + sub_account::SubAccount, + token_limit::TokenLimit, + token_recurring_limit::TokenRecurringLimit, + }, + role::Role, + Transmutable, }; use crate::SwigError; /// Configuration for recurring limits that reset after a specified time window -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct RecurringConfig { /// The time window in slots after which the limit resets pub window: u64, @@ -39,7 +43,7 @@ impl RecurringConfig { /// Represents the permissions that can be granted to a wallet authority. /// Each permission type maps to specific actions that can be performed on the /// wallet. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum Permission { /// Full permissions for all actions. This is the highest level of /// permission that grants unrestricted access to all wallet operations. @@ -220,4 +224,171 @@ impl Permission { } actions } + + /// Converts a Role reference to a vector of Permission types + /// + /// # Arguments + /// + /// * `role` - Reference to a Role + /// + /// # Returns + /// + /// Returns a `Result` containing a vector of permissions or a `SwigError` + pub fn from_role<'a>( + role: &swig_state_x::role::Role<'a>, + ) -> Result, SwigError> { + let mut permissions = Vec::new(); + + // Check for All permission + if swig_state_x::role::Role::get_action::(role, &[]) + .map_err(|_| SwigError::InvalidSwigData)? + .is_some() + { + permissions.push(Permission::All); + } + + // Check for ManageAuthority permission + if swig_state_x::role::Role::get_action::(role, &[]) + .map_err(|_| SwigError::InvalidSwigData)? + .is_some() + { + permissions.push(Permission::ManageAuthority); + } + + // Check for SolLimit permission + if let Some(action) = swig_state_x::role::Role::get_action::(role, &[]) + .map_err(|_| SwigError::InvalidSwigData)? + { + permissions.push(Permission::Sol { + amount: action.amount, + recurring: None, + }); + } + + // Check for SolRecurringLimit permission + if let Some(action) = swig_state_x::role::Role::get_action::(role, &[]) + .map_err(|_| SwigError::InvalidSwigData)? + { + permissions.push(Permission::Sol { + amount: action.recurring_amount, + recurring: Some(RecurringConfig { + window: action.window, + last_reset: action.last_reset, + current_amount: action.current_amount, + }), + }); + } + + // Check for TokenLimit permission + if let Some(action) = swig_state_x::role::Role::get_action::(role, &[]) + .map_err(|_| SwigError::InvalidSwigData)? + { + permissions.push(Permission::Token { + mint: Pubkey::new_from_array(action.token_mint), + amount: action.current_amount, + recurring: None, + }); + } + + // Check for TokenRecurringLimit permission + if let Some(action) = swig_state_x::role::Role::get_action::(role, &[]) + .map_err(|_| SwigError::InvalidSwigData)? + { + permissions.push(Permission::Token { + mint: Pubkey::new_from_array(action.token_mint), + amount: action.limit, + recurring: Some(RecurringConfig { + window: action.window, + last_reset: action.last_reset, + current_amount: action.current, + }), + }); + } + + // Check for Program permission + if let Some(action) = swig_state_x::role::Role::get_action::(role, &[]) + .map_err(|_| SwigError::InvalidSwigData)? + { + permissions.push(Permission::Program { + program_id: Pubkey::new_from_array(action.program_id), + }); + } + + // Check for ProgramScope permission + if let Some(action) = + swig_state_x::role::Role::get_action::(role, &spl_token::ID.to_bytes()) + .map_err(|_| SwigError::InvalidSwigData)? + { + permissions.push(Permission::ProgramScope { + program_id: Pubkey::new_from_array(action.program_id), + target_account: Pubkey::new_from_array(action.target_account), + numeric_type: action.numeric_type, + limit: if action.scope_type > 0 { + Some(action.limit as u64) + } else { + None + }, + window: if action.scope_type == 2 { + Some(action.window) + } else { + None + }, + balance_field_start: Some(action.balance_field_start), + balance_field_end: Some(action.balance_field_end), + }); + } + + // Check for SubAccount permission + if let Some(action) = swig_state_x::role::Role::get_action::(role, &[]) + .map_err(|_| SwigError::InvalidSwigData)? + { + permissions.push(Permission::SubAccount { + sub_account: action.sub_account, + }); + } + + // Check for StakeLimit permission + if let Some(action) = swig_state_x::role::Role::get_action::(role, &[]) + .map_err(|_| SwigError::InvalidSwigData)? + { + permissions.push(Permission::Stake { + amount: action.amount, + recurring: None, + }); + } + + // Check for StakeRecurringLimit permission + if let Some(action) = swig_state_x::role::Role::get_action::(role, &[]) + .map_err(|_| SwigError::InvalidSwigData)? + { + permissions.push(Permission::Stake { + amount: action.recurring_amount, + recurring: Some(RecurringConfig { + window: action.window, + last_reset: action.last_reset, + current_amount: action.current_amount, + }), + }); + } + + // Check for StakeAll permission + if swig_state_x::role::Role::get_action::(role, &[]) + .map_err(|_| SwigError::InvalidSwigData)? + .is_some() + { + permissions.push(Permission::StakeAll); + } + + Ok(permissions) + } +} + +/// Stores all details about the current role for a wallet session +#[derive(Debug)] +pub struct CurrentRole { + pub role_id: u32, + pub authority_type: swig_state_x::authority::AuthorityType, + pub authority_identity: Vec, + pub permissions: Vec, + pub session_based: bool, } diff --git a/rust-sdk/src/wallet.rs b/rust-sdk/src/wallet.rs index ecfe6902..2e9afb87 100644 --- a/rust-sdk/src/wallet.rs +++ b/rust-sdk/src/wallet.rs @@ -32,15 +32,13 @@ use swig_state_x::{ }, authority::{self, secp256k1::Secp256k1Authority, AuthorityType}, role::Role, - swig::{sub_account_seeds, SwigWithRoles}, + swig::{sub_account_seeds, Swig, SwigWithRoles}, }; const TOKEN_22_PROGRAM_ID: Pubkey = pubkey!("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"); use crate::{ - error::SwigError, - instruction_builder::{AuthorityManager, SwigInstructionBuilder}, - types::Permission, - RecurringConfig, + client_role::ClientRole, error::SwigError, instruction_builder::SwigInstructionBuilder, + types::Permission, RecurringConfig, }; /// Swig protocol for transaction signing and authority management. @@ -53,8 +51,10 @@ pub struct SwigWallet<'a> { pub rpc_client: RpcClient, /// The wallet's fee payer keypair fee_payer: &'a Keypair, - /// The authority keypair for signing transactions - authority: &'a Keypair, + /// The authority keypair (for Ed25519 authorities) + authority_keypair: Option<&'a Keypair>, + /// The current role details for the wallet + pub current_role: crate::types::CurrentRole, /// The LiteSVM instance for testing #[cfg(all(feature = "rust_sdk_test", test))] litesvm: LiteSVM, @@ -66,11 +66,11 @@ impl<'c> SwigWallet<'c> { /// # Arguments /// /// * `swig_id` - The unique identifier for the Swig account - /// * `authority_manager` - The authority manager specifying the type of + /// * `client_role` - The client role implementation specifying the type of /// signing authority /// * `fee_payer` - The keypair that will pay for transactions - /// * `authority` - The wallet's authority keypair /// * `rpc_url` - The URL of the Solana RPC endpoint + /// * `authority_keypair` - Optional authority keypair (required for Ed25519 authorities) /// * `litesvm` - (test only) The LiteSVM instance for testing /// /// # Returns @@ -79,10 +79,10 @@ impl<'c> SwigWallet<'c> { /// `SwigError` pub fn new( swig_id: [u8; 32], - authority_manager: AuthorityManager, + client_role: Box, fee_payer: &'c Keypair, - authority: &'c Keypair, rpc_url: String, + authority_keypair: Option<&'c Keypair>, #[cfg(all(feature = "rust_sdk_test", test))] mut litesvm: LiteSVM, ) -> Result { let rpc_client = @@ -103,7 +103,7 @@ impl<'c> SwigWallet<'c> { if !account_exists { let instruction_builder = - SwigInstructionBuilder::new(swig_id, authority_manager, fee_payer.pubkey(), 0); + SwigInstructionBuilder::new(swig_id, client_role, fee_payer.pubkey(), 0); let create_ix = instruction_builder.build_swig_account()?; @@ -124,13 +124,31 @@ impl<'c> SwigWallet<'c> { #[cfg(all(feature = "rust_sdk_test", test))] let signature = litesvm.send_transaction(tx).unwrap().signature; + // Fetch the just-created account data to get the initial role + #[cfg(not(all(feature = "rust_sdk_test", test)))] + let swig_data = rpc_client.get_account_data(&swig_account)?; + #[cfg(all(feature = "rust_sdk_test", test))] + let swig_data = litesvm.get_account(&swig_account).unwrap().data; + + let swig_with_roles = + SwigWithRoles::from_bytes(&swig_data).map_err(|e| SwigError::InvalidSwigData)?; + let role = swig_with_roles + .get_role(0) + .map_err(|_| SwigError::AuthorityNotFound)?; + let current_role = if let Some(role) = role { + build_current_role(0, &role) + } else { + return Err(SwigError::AuthorityNotFound); + }; + Ok(Self { instruction_builder, rpc_client, fee_payer, - authority, #[cfg(all(feature = "rust_sdk_test", test))] litesvm, + authority_keypair, + current_role, }) } else { // Safe unwrap because we know the account exists @@ -142,41 +160,35 @@ impl<'c> SwigWallet<'c> { let swig_with_roles = SwigWithRoles::from_bytes(&swig_data).map_err(|e| SwigError::InvalidSwigData)?; - let role_id = match &authority_manager { - AuthorityManager::Ed25519(authority) => swig_with_roles - .lookup_role_id(authority.as_ref()) - .map_err(|_| SwigError::AuthorityNotFound)?, - AuthorityManager::Secp256k1(authority, _) => swig_with_roles - .lookup_role_id(authority.as_ref()) - .map_err(|_| SwigError::AuthorityNotFound)?, - AuthorityManager::Ed25519Session(session_authority) => swig_with_roles - .lookup_role_id(session_authority.public_key.as_ref()) - .map_err(|_| SwigError::AuthorityNotFound)?, - AuthorityManager::Secp256k1Session(session_authority, _) => swig_with_roles - .lookup_role_id(session_authority.public_key.as_ref()) - .map_err(|_| SwigError::AuthorityNotFound)?, - } - .ok_or(SwigError::AuthorityNotFound)?; + let authority_bytes = client_role.authority_bytes()?; + let role_id = swig_with_roles + .lookup_role_id(authority_bytes.as_ref()) + .map_err(|_| SwigError::AuthorityNotFound)? + .ok_or(SwigError::AuthorityNotFound)?; // Get the role to verify it exists and has the correct type let role = swig_with_roles .get_role(role_id) .map_err(|_| SwigError::AuthorityNotFound)?; - let instruction_builder = SwigInstructionBuilder::new( - swig_id, - authority_manager, - fee_payer.pubkey(), - role_id, - ); + // Extract the role data for storage + let current_role = if let Some(role) = role { + build_current_role(role_id, &role) + } else { + return Err(SwigError::AuthorityNotFound); + }; + + let instruction_builder = + SwigInstructionBuilder::new(swig_id, client_role, fee_payer.pubkey(), role_id); Ok(Self { instruction_builder, rpc_client, fee_payer: &fee_payer, - authority: &authority, #[cfg(all(feature = "rust_sdk_test", test))] litesvm, + authority_keypair, + current_role, }) } } @@ -204,7 +216,6 @@ impl<'c> SwigWallet<'c> { new_authority, permissions, Some(self.get_current_slot()?), - None, )?; let msg = v0::Message::try_compile( &self.fee_payer.pubkey(), @@ -258,7 +269,12 @@ impl<'c> SwigWallet<'c> { &[self.fee_payer.insecure_clone()], )?; - self.send_and_confirm_transaction(tx) + let tx_result = self.send_and_confirm_transaction(tx); + if tx_result.is_ok() { + self.refresh_permissions()?; + self.instruction_builder.increment_odometer()?; + } + tx_result } else { return Err(SwigError::AuthorityNotFound); } @@ -280,11 +296,9 @@ impl<'c> SwigWallet<'c> { inner_instructions: Vec, alt: Option<&[AddressLookupTableAccount]>, ) -> Result { - let sign_ix = self.instruction_builder.sign_instruction( - inner_instructions, - Some(self.get_current_slot()?), - None, - )?; + let sign_ix = self + .instruction_builder + .sign_instruction(inner_instructions, Some(self.get_current_slot()?))?; let alt = if alt.is_some() { alt.unwrap() } else { &[] }; @@ -297,7 +311,12 @@ impl<'c> SwigWallet<'c> { let tx = VersionedTransaction::try_new(VersionedMessage::V0(msg), &self.get_keypairs()?)?; - self.send_and_confirm_transaction(tx) + let tx_result = self.send_and_confirm_transaction(tx); + if tx_result.is_ok() { + self.refresh_permissions()?; + self.instruction_builder.increment_odometer()?; + } + tx_result } /// Replaces an existing authority with a new one @@ -327,6 +346,7 @@ impl<'c> SwigWallet<'c> { new_authority, permissions, Some(current_slot), + None, )?; let msg = v0::Message::try_compile( @@ -338,7 +358,12 @@ impl<'c> SwigWallet<'c> { let tx = VersionedTransaction::try_new(VersionedMessage::V0(msg), &self.get_keypairs()?)?; - self.send_and_confirm_transaction(tx) + let tx_result = self.send_and_confirm_transaction(tx); + if tx_result.is_ok() { + self.refresh_permissions()?; + self.instruction_builder.increment_odometer()?; + } + tx_result } /// Creates a new sub-account for the Swig wallet @@ -364,7 +389,12 @@ impl<'c> SwigWallet<'c> { let tx = VersionedTransaction::try_new(VersionedMessage::V0(msg), &self.get_keypairs()?)?; - self.send_and_confirm_transaction(tx) + let tx_result = self.send_and_confirm_transaction(tx); + if tx_result.is_ok() { + self.refresh_permissions()?; + self.instruction_builder.increment_odometer()?; + } + tx_result } /// Signs instructions with a sub-account @@ -400,7 +430,12 @@ impl<'c> SwigWallet<'c> { // We need both the fee payer and the authority to sign let tx = VersionedTransaction::try_new(VersionedMessage::V0(msg), &self.get_keypairs()?)?; - self.send_and_confirm_transaction(tx) + let tx_result = self.send_and_confirm_transaction(tx); + if tx_result.is_ok() { + self.refresh_permissions()?; + self.instruction_builder.increment_odometer()?; + } + tx_result } /// Withdraws native SOL from a sub-account @@ -434,7 +469,12 @@ impl<'c> SwigWallet<'c> { let tx = VersionedTransaction::try_new(VersionedMessage::V0(msg), &self.get_keypairs()?)?; - self.send_and_confirm_transaction(tx) + let tx_result = self.send_and_confirm_transaction(tx); + if tx_result.is_ok() { + self.refresh_permissions()?; + self.instruction_builder.increment_odometer()?; + } + tx_result } /// Withdraws SPL tokens from a sub-account @@ -477,7 +517,12 @@ impl<'c> SwigWallet<'c> { let tx = VersionedTransaction::try_new(VersionedMessage::V0(msg), &self.get_keypairs()?)?; - self.send_and_confirm_transaction(tx) + let tx_result = self.send_and_confirm_transaction(tx); + if tx_result.is_ok() { + self.refresh_permissions()?; + self.instruction_builder.increment_odometer()?; + } + tx_result } /// Toggles a sub-account's enabled state @@ -741,9 +786,9 @@ impl<'c> SwigWallet<'c> { }, AuthorityType::Secp256k1 | AuthorityType::Secp256k1Session => { let authority = role.authority.identity().unwrap(); - let authority_hex = - hex::encode([&[0x4].as_slice(), authority].concat()); - // get eth address from public key + let mut authority_hex = vec![0x4]; + authority_hex.extend_from_slice(authority); + let authority_hex = hex::encode(authority_hex); let mut hasher = solana_sdk::keccak::Hasher::default(); hasher.hash(authority_hex.as_bytes()); let hash = hasher.result(); @@ -900,14 +945,52 @@ impl<'c> SwigWallet<'c> { Ok(self.instruction_builder.get_role_id()) } + /// Returns the current role permissions if available + /// + /// # Returns + /// + /// Returns a `Result` containing the current role permissions or a `SwigError` + pub fn get_current_permissions(&self) -> Result<&[Permission], SwigError> { + Ok(&self.current_role.permissions) + } + + /// Updates the stored role permissions by fetching them from the chain + /// + /// # Returns + /// + /// Returns a `Result` containing unit type or a `SwigError` + pub fn refresh_permissions(&mut self) -> Result<(), SwigError> { + let swig_pubkey = self.get_swig_account()?; + #[cfg(not(all(feature = "rust_sdk_test", test)))] + let swig_data = self.rpc_client.get_account_data(&swig_pubkey)?; + #[cfg(all(feature = "rust_sdk_test", test))] + let swig_data = self.litesvm.get_account(&swig_pubkey).unwrap().data; + + let swig_with_roles = + SwigWithRoles::from_bytes(&swig_data).map_err(|e| SwigError::InvalidSwigData)?; + + let role_id = self.get_current_role_id()?; + let role = swig_with_roles + .get_role(role_id) + .map_err(|_| SwigError::AuthorityNotFound)?; + + if let Some(role) = role { + self.current_role = build_current_role(role_id, &role); + } else { + return Err(SwigError::AuthorityNotFound); + } + + Ok(()) + } + /// Switches to a different authority for the Swig wallet /// /// # Arguments /// /// * `role_id` - The new role ID to switch to - /// * `authority_manager` - The authority manager specifying the type of + /// * `client_role` - The client role implementation specifying the type of /// signing authority - /// * `authority_kp` - The public key of the new authority + /// * `authority_kp` - The public key of the new authority (unused in new implementation) /// /// # Returns /// @@ -915,18 +998,24 @@ impl<'c> SwigWallet<'c> { pub fn switch_authority( &mut self, role_id: u32, - authority_manager: AuthorityManager, + mut client_role: Box, authority_kp: Option<&'c Keypair>, ) -> Result<(), SwigError> { - // Ensure authority keypair is provided when switching authorities - let authority_kp = authority_kp.ok_or(SwigError::AuthorityNotFound)?; + // The odometer is stored in client role and must be updated to match the on chain odometer + let odometer = self.with_role_data(role_id, |role| role.authority.signature_odometer())?; + if let Some(onchain_odometer) = odometer { + client_role.update_odometer(onchain_odometer)?; + } // Update the instruction builder's authority self.instruction_builder - .switch_authority(role_id, authority_manager)?; + .switch_authority(role_id, client_role)?; + + self.authority_keypair = authority_kp; + + // Update the stored role data for the new authority + self.refresh_permissions()?; - // Update the authority keypair that will be used for signing - self.authority = authority_kp; Ok(()) } @@ -966,7 +1055,6 @@ impl<'c> SwigWallet<'c> { let indexed_authority = swig_with_roles.lookup_role_id(authority.as_ref()).unwrap(); - println!("Indexed Authority: {:?}", indexed_authority); if indexed_authority.is_some() { Ok(()) } else { @@ -990,6 +1078,7 @@ impl<'c> SwigWallet<'c> { session_key, duration, Some(current_slot), + None, )?; let msg = v0::Message::try_compile( @@ -1026,7 +1115,7 @@ impl<'c> SwigWallet<'c> { #[cfg(not(all(feature = "rust_sdk_test", test)))] let account_exists = self.rpc_client.get_account(&sub_account).is_ok(); #[cfg(all(feature = "rust_sdk_test", test))] - let account_exists = self.litesvm.get_balance(&sub_account).unwrap() > 0; + let account_exists = self.litesvm.get_balance(&sub_account).is_some(); if account_exists { Ok(Some(sub_account)) @@ -1082,12 +1171,16 @@ impl<'c> SwigWallet<'c> { /// Returns a `Result` containing the keypairs for signing transactions or a /// `SwigError` fn get_keypairs(&self) -> Result, SwigError> { - // Check if the authority and fee payer are the same - if self.fee_payer.pubkey() == self.authority.pubkey() { - Ok(vec![&self.fee_payer]) - } else { - Ok(vec![&self.fee_payer, &self.authority]) + let mut keypairs = vec![self.fee_payer]; + if let Some(authority_kp) = self.authority_keypair { + // Only add the authority keypair if it's different from the fee payer + if std::ptr::eq(authority_kp, self.fee_payer) { + // Authority and fee payer are the same, so we already have it + } else { + keypairs.push(authority_kp); + } } + Ok(keypairs) } /// Returns the swig id @@ -1184,6 +1277,287 @@ impl<'c> SwigWallet<'c> { pub fn litesvm(&mut self) -> &mut LiteSVM { &mut self.litesvm } + + /// Checks if the current authority has a specific permission + /// + /// # Arguments + /// + /// * `permission` - The permission to check for + /// + /// # Returns + /// + /// Returns a `Result` containing whether the permission exists or a `SwigError` + pub fn has_permission(&self, permission: &Permission) -> Result { + let permissions = self.get_current_permissions()?; + Ok(permissions.contains(permission)) + } + + /// Checks if the current authority has "All" permissions + /// + /// # Returns + /// + /// Returns a `Result` containing whether the authority has all permissions or a `SwigError` + pub fn has_all_permissions(&self) -> Result { + let permissions = self.get_current_permissions()?; + Ok(permissions.iter().any(|p| matches!(p, Permission::All))) + } + + /// Checks if the current authority has manage authority permissions + /// + /// # Returns + /// + /// Returns a `Result` containing whether the authority can manage other authorities or a `SwigError` + pub fn can_manage_authority(&self) -> Result { + let permissions = self.get_current_permissions()?; + Ok(permissions + .iter() + .any(|p| matches!(p, Permission::ManageAuthority))) + } + + /// Gets the SOL spending limit for the current authority + /// + /// # Returns + /// + /// Returns a `Result` containing the SOL limit in lamports or a `SwigError` + pub fn get_sol_limit(&self) -> Result, SwigError> { + let permissions = self.get_current_permissions()?; + for permission in permissions { + if let Permission::Sol { amount, .. } = permission { + return Ok(Some(*amount)); + } + } + Ok(None) + } + + /// Gets the recurring SOL limit configuration for the current authority + /// + /// # Returns + /// + /// Returns a `Result` containing the recurring SOL limit config or a `SwigError` + pub fn get_recurring_sol_limit(&self) -> Result, SwigError> { + let permissions = self.get_current_permissions()?; + for permission in permissions { + if let Permission::Sol { recurring, .. } = permission { + return Ok(recurring.clone()); + } + } + Ok(None) + } + + /// Checks if the current authority can spend a specific amount of SOL + /// + /// # Arguments + /// + /// * `amount` - The amount to check in lamports + /// + /// # Returns + /// + /// Returns a `Result` containing whether the authority can spend the amount or a `SwigError` + pub fn can_spend_sol(&self, amount: u64) -> Result { + // If they have all permissions, they can spend any amount + if self.has_all_permissions()? { + return Ok(true); + } + + let permissions = self.get_current_permissions()?; + + for permission in permissions { + match permission { + Permission::Sol { + amount: limit, + recurring, + } => { + // Check one-time limit + if amount <= *limit { + return Ok(true); + } + + // Check recurring limit if it exists + if let Some(recurring_config) = recurring { + if amount <= recurring_config.current_amount { + return Ok(true); + } + } + }, + _ => continue, + } + } + + Ok(false) + } + + /// Gets the total number of roles in the Swig account + /// + /// # Returns + /// + /// Returns a `Result` containing the number of roles or a `SwigError` + pub fn get_role_count(&self) -> Result { + let swig_pubkey = self.get_swig_account()?; + #[cfg(not(all(feature = "rust_sdk_test", test)))] + let swig_data = self.rpc_client.get_account_data(&swig_pubkey)?; + #[cfg(all(feature = "rust_sdk_test", test))] + let swig_data = self.litesvm.get_account(&swig_pubkey).unwrap().data; + + let swig_with_roles = + SwigWithRoles::from_bytes(&swig_data).map_err(|e| SwigError::InvalidSwigData)?; + + Ok(swig_with_roles.state.role_counter) + } + + /// Gets the authority type for a specific role + /// + /// # Arguments + /// + /// * `role_id` - The ID of the role to check + /// + /// # Returns + /// + /// Returns a `Result` containing the authority type or a `SwigError` + pub fn get_authority_type(&self, role_id: u32) -> Result { + self.with_role_data(role_id, |role| role.authority.authority_type()) + } + + /// Gets the authority identity for a specific role + /// + /// # Arguments + /// + /// * `role_id` - The ID of the role to check + /// + /// # Returns + /// + /// Returns a `Result` containing the authority identity bytes or a `SwigError` + pub fn get_authority_identity(&self, role_id: u32) -> Result, SwigError> { + self.with_role_data(role_id, |role| { + role.authority.identity().unwrap_or_default().to_vec() + }) + } + + /// Checks if a role is session-based + /// + /// # Arguments + /// + /// * `role_id` - The ID of the role to check + /// + /// # Returns + /// + /// Returns a `Result` containing whether the role is session-based or a `SwigError` + pub fn is_session_based(&self, role_id: u32) -> Result { + self.with_role_data(role_id, |role| role.authority.session_based()) + } + + /// Gets all permissions for a specific role + /// + /// # Arguments + /// + /// * `role_id` - The ID of the role to get permissions for + /// + /// # Returns + /// + /// Returns a `Result` containing the permissions for the role or a `SwigError` + pub fn get_role_permissions(&self, role_id: u32) -> Result, SwigError> { + self.with_role_data(role_id, |role| Permission::from_role(role))? + } + + /// Checks if a role has a specific permission + /// + /// # Arguments + /// + /// * `role_id` - The ID of the role to check + /// * `permission` - The permission to check for + /// + /// # Returns + /// + /// Returns a `Result` containing whether the role has the permission or a `SwigError` + pub fn role_has_permission( + &self, + role_id: u32, + permission: &Permission, + ) -> Result { + let permissions = self.get_role_permissions(role_id)?; + Ok(permissions.contains(permission)) + } + + /// Gets the formatted authority address for display + /// + /// # Arguments + /// + /// * `role_id` - The ID of the role to get the address for + /// + /// # Returns + /// + /// Returns a `Result` containing the formatted address string or a `SwigError` + pub fn get_formatted_authority_address(&self, role_id: u32) -> Result { + self.with_role_data(role_id, |role| match role.authority.authority_type() { + AuthorityType::Ed25519 | AuthorityType::Ed25519Session => { + let authority = role.authority.identity().unwrap_or_default(); + Ok(bs58::encode(authority).into_string()) + }, + AuthorityType::Secp256k1 | AuthorityType::Secp256k1Session => { + let authority = role.authority.identity().unwrap_or_default(); + let mut authority_hex = vec![0x4]; + authority_hex.extend_from_slice(authority); + let authority_hex = hex::encode(authority_hex); + let mut hasher = solana_sdk::keccak::Hasher::default(); + hasher.hash(authority_hex.as_bytes()); + let hash = hasher.result(); + Ok(format!("0x{}", hex::encode(&hash.0[12..32]))) + }, + _ => Err(SwigError::AuthorityNotFound), + })? + } + + /// Get the odometer for the current authority if it is a Secp256k1 authority + /// + /// # Returns + /// + /// Returns a `Result` containing the odometer or a `SwigError` + pub fn get_odometer(&self) -> Result { + self.instruction_builder.get_odometer() + } + + /// Helper method to work with role data by ID using a closure + /// + /// # Arguments + /// + /// * `role_id` - The ID of the role to retrieve + /// * `f` - Closure to execute with the role data + /// + /// # Returns + /// + /// Returns a `Result` containing the result of the closure or a `SwigError` + fn with_role_data(&self, role_id: u32, f: F) -> Result + where + F: FnOnce(&Role) -> T, + { + let swig_pubkey = self.get_swig_account()?; + #[cfg(not(all(feature = "rust_sdk_test", test)))] + let swig_data = self.rpc_client.get_account_data(&swig_pubkey)?; + #[cfg(all(feature = "rust_sdk_test", test))] + let swig_data = self.litesvm.get_account(&swig_pubkey).unwrap().data; + + let swig_with_roles = + SwigWithRoles::from_bytes(&swig_data).map_err(|_| SwigError::InvalidSwigData)?; + + if let Some(role) = swig_with_roles + .get_role(role_id) + .map_err(|_| SwigError::InvalidSwigData)? + { + Ok(f(&role)) + } else { + Err(SwigError::AuthorityNotFound) + } + } +} + +// Helper to build CurrentRole from a Role and role_id +fn build_current_role(role_id: u32, role: &Role) -> crate::types::CurrentRole { + crate::types::CurrentRole { + role_id, + authority_type: role.authority.authority_type(), + authority_identity: role.authority.identity().unwrap_or_default().to_vec(), + permissions: crate::types::Permission::from_role(role).unwrap_or_default(), + session_based: role.authority.session_based(), + } } #[cfg(all(feature = "rust_sdk_test", test))] diff --git a/state-x/Cargo.toml b/state-x/Cargo.toml index 5dcf759a..b2504d4d 100644 --- a/state-x/Cargo.toml +++ b/state-x/Cargo.toml @@ -6,10 +6,12 @@ edition = "2021" [dependencies] pinocchio = { version = "=0.8.1", features = ["std"] } +pinocchio-pubkey = { version = "0.2.4" } swig-assertions = { path = "../assertions" } no-padding = { path = "../no-padding" } murmur3 = { version = "0.5.2", optional = true } libsecp256k1 = { version = "0.7.2", default-features = false} +hex = "0.4.3" [target.'cfg(not(feature = "static_syscalls"))'.dependencies] murmur3 = "0.5.2" @@ -18,6 +20,9 @@ murmur3 = "0.5.2" [dev-dependencies] rand = "0.9.0" hex = "0.4.3" +openssl = { version = "0.10.72", features = ["vendored"] } +agave-precompiles = "2.2.14" +solana-secp256r1-program = "2.2.1" [lints.clippy] unexpected_cfgs = "allow" diff --git a/state-x/src/action/manage_authority.rs b/state-x/src/action/manage_authority.rs index 4614a94a..ea4ddc30 100644 --- a/state-x/src/action/manage_authority.rs +++ b/state-x/src/action/manage_authority.rs @@ -33,4 +33,9 @@ impl<'a> Actionable<'a> for ManageAuthority { const TYPE: Permission = Permission::ManageAuthority; /// Only one instance of authority management permissions can exist per role const REPEATABLE: bool = false; + + /// Always returns true since this represents authority management access. + fn match_data(&self, _data: &[u8]) -> bool { + true + } } diff --git a/state-x/src/action/mod.rs b/state-x/src/action/mod.rs index 8e945a1f..c9b01c0d 100644 --- a/state-x/src/action/mod.rs +++ b/state-x/src/action/mod.rs @@ -7,6 +7,7 @@ pub mod all; pub mod manage_authority; +pub mod oracle_limits; pub mod program; pub mod program_scope; pub mod sol_limit; @@ -17,9 +18,11 @@ pub mod stake_recurring_limit; pub mod sub_account; pub mod token_limit; pub mod token_recurring_limit; +use crate::{IntoBytes, SwigStateError, Transmutable, TransmutableMut}; use all::All; use manage_authority::ManageAuthority; use no_padding::NoPadding; +use oracle_limits::OracleTokenLimit; use pinocchio::program_error::ProgramError; use program::Program; use program_scope::ProgramScope; @@ -32,8 +35,6 @@ use sub_account::SubAccount; use token_limit::TokenLimit; use token_recurring_limit::TokenRecurringLimit; -use crate::{IntoBytes, SwigStateError, Transmutable, TransmutableMut}; - /// Represents an action in the Swig wallet system. /// /// Actions define what operations can be performed and under what conditions. @@ -130,6 +131,8 @@ pub enum Permission { StakeRecurringLimit = 11, /// Permission to perform all stake operations StakeAll = 12, + /// Permission to perform token operations with oracle-based limits + OracleTokenLimit = 13, } impl TryFrom for Permission { @@ -139,7 +142,7 @@ impl TryFrom for Permission { fn try_from(value: u16) -> Result { match value { // SAFETY: `value` is guaranteed to be in the range of the enum variants. - 0..=12 => Ok(unsafe { core::mem::transmute::(value) }), + 0..=13 => Ok(unsafe { core::mem::transmute::(value) }), _ => Err(SwigStateError::PermissionLoadError.into()), } } @@ -196,6 +199,7 @@ impl ActionLoader { Permission::StakeLimit => StakeLimit::valid_layout(data), Permission::StakeRecurringLimit => StakeRecurringLimit::valid_layout(data), Permission::StakeAll => StakeAll::valid_layout(data), + Permission::OracleTokenLimit => OracleTokenLimit::valid_layout(data), _ => Ok(false), } } diff --git a/state-x/src/action/oracle_limits.rs b/state-x/src/action/oracle_limits.rs new file mode 100644 index 00000000..94e7430d --- /dev/null +++ b/state-x/src/action/oracle_limits.rs @@ -0,0 +1,611 @@ +/// Oracle-based token limit action type. +/// +/// This module defines the OracleTokenLimit action type which enforces value-based limits on +/// token operations within the Swig wallet system. It uses oracle price feeds to convert token +/// amounts to a base asset value (e.g. USDC) for limit enforcement. +/// +/// The system supports: +/// - Different base assets (e.g. USDC, EURC) for value denomination +/// - Oracle price feed integration for real-time value conversion +/// - Configurable value limits per base asset +/// - Decimal precision handling for different token types +/// +/// The limits are enforced by: +/// 1. Converting token amounts to base asset value using oracle prices +/// 2. Tracking cumulative usage against the configured limit +/// 3. Preventing operations that would exceed the limit +/// 4. Supporting both token and native SOL operations +use super::{Actionable, Permission}; +use crate::{IntoBytes, SwigAuthenticateError, SwigStateError, Transmutable, TransmutableMut}; +use no_padding::NoPadding; +use pinocchio::msg; +use pinocchio::program_error::ProgramError; +use pinocchio_pubkey::pubkey; + +/// Represents the base asset type for value denomination. +/// +/// This enum defines the supported base assets that can be used to denominate +/// token value limits. Each base asset has a specific decimal precision that +/// is used in value calculations. +#[repr(u8)] +pub enum BaseAsset { + /// USDC stablecoin with 6 decimal places precision + USDC = 0, + /// EURC stablecoin with 6 decimal places precision + EURC = 1, +} + +impl TryFrom for BaseAsset { + type Error = ProgramError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(BaseAsset::USDC), + // 1 => Ok(BaseAsset::EURC), + _ => Err(SwigAuthenticateError::InvalidDataPayload.into()), + } + } +} + +/// Represents a limit on token operations based on oracle base asset value. +/// +/// This struct tracks and enforces a maximum value of tokens that can be +/// used in operations, denominated in a base asset (e.g. USDC). The limit is enforced +/// by converting token amounts to the base asset value using oracle price feeds. +/// +/// # Fields +/// * `value_limit` - The current remaining amount that can be used (in base asset) +/// * `base_asset_type` - The base asset type used to denominate the limit (e.g. USDC) +/// * `passthrough_check` - Flag to check remaining actions after oracle limit check +/// * `_padding` - Padding bytes to ensure proper struct alignment +#[repr(C, align(8))] +#[derive(Debug, NoPadding)] +pub struct OracleTokenLimit { + /// The current remaining amount that can be used (in base asset) + pub value_limit: u64, + /// The base asset type used to denominate the limit (e.g. USDC) + pub base_asset_type: u8, + /// The passthrough flag, it will check the remaining actions + /// and not just stop with oracle limit check + pub passthrough_check: bool, + /// Padding bytes to ensure proper struct alignment + _padding: [u8; 6], +} + +impl OracleTokenLimit { + /// Creates a new OracleTokenLimit with the specified parameters. + /// + /// # Arguments + /// * `base_asset` - The base asset to denominate the limit in (e.g. USDC) + /// * `value_limit` - The maximum value allowed in base asset + /// * `passthrough_check` - Whether to check remaining actions after oracle limit check + /// + /// # Returns + /// A new OracleTokenLimit instance configured with the specified parameters + pub fn new(base_asset: BaseAsset, value_limit: u64, passthrough_check: bool) -> Self { + Self { + base_asset_type: base_asset as u8, + value_limit, + passthrough_check, + _padding: [0; 6], + } + } + + /// Gets the decimal places for the configured base asset type. + /// + /// # Returns + /// The number of decimal places for the base asset (e.g. 6 for USDC) + fn get_base_asset_decimals(&self) -> u8 { + match BaseAsset::try_from(self.base_asset_type).unwrap() { + BaseAsset::USDC => 6, + BaseAsset::EURC => 6, + } + } + + /// Processes a token operation by checking the oracle price and value limit. + /// + /// This method: + /// 1. Converts the token amount to oracle decimal precision + /// 2. Multiplies by the oracle price to get the value + /// 3. Adjusts for price exponent + /// 4. Converts to base asset decimal precision + /// 5. Checks against and updates the remaining limit + /// + /// # Arguments + /// * `amount` - The amount of tokens to be used (in token decimals) + /// * `oracle_price` - The current oracle price for the token + /// * `_confidence` - The confidence interval for the oracle price (unused) + /// * `exponent` - The exponent for price calculation + /// * `token_decimals` - The number of decimal places for the token + /// + /// # Returns + /// * `Ok(())` - If the operation is within limits + /// * `Err(ProgramError)` - If the operation would exceed the limit or encounters an error + pub fn run_for_token( + &mut self, + amount: u64, + oracle_price: u64, + _confidence: u64, + exponent: i32, + token_decimals: u8, + ) -> Result<(), ProgramError> { + // Early return if amount is 0 + if amount == 0 { + return Ok(()); + } + + // Get base asset decimals + let base_decimals = self.get_base_asset_decimals(); + + // Convert to u128 for intermediate calculations to prevent overflow + let amount = amount as u128; + let oracle_price = oracle_price as u128; + + // Calculate value with proper decimal handling + let value = if exponent >= 0 { + // For positive exponent: + // (amount * price * 10^exponent) / 10^token_decimals + amount + .checked_mul(oracle_price) + .and_then(|v| v.checked_mul(10u128.pow(exponent as u32))) + .and_then(|v| v.checked_div(10u128.pow(token_decimals as u32))) + .ok_or(SwigAuthenticateError::PermissionDeniedInsufficientBalance)? + } else { + // For negative exponent: + // (amount * price) / (10^|exponent| * 10^token_decimals) + // First multiply by 10^base_decimals to preserve precision + amount + .checked_mul(oracle_price) + .and_then(|v| v.checked_mul(10u128.pow(base_decimals as u32))) + .and_then(|v| v.checked_div(10u128.pow((-exponent) as u32))) + .and_then(|v| v.checked_div(10u128.pow(token_decimals as u32))) + .ok_or(SwigAuthenticateError::PermissionDeniedInsufficientBalance)? + }; + + // No need for additional decimal conversion since we already handled it above + let value = value + .try_into() + .map_err(|_| SwigAuthenticateError::PermissionDeniedInsufficientBalance)?; + + // Check if operation would exceed limit + if value > self.value_limit { + msg!("Operation denied: Would exceed value limit"); + return Err(SwigAuthenticateError::PermissionDeniedOracleLimitReached.into()); + } + + // Safe to subtract since we verified value <= value_limit + self.value_limit = self + .value_limit + .checked_sub(value) + .ok_or(SwigAuthenticateError::PermissionDeniedInsufficientBalance)?; + + Ok(()) + } + + /// Processes a Solana operation by checking the oracle price and value limit. + /// + /// This method handles native SOL operations by: + /// 1. Checking for potential multiplication overflow + /// 2. Converting SOL amount to base asset value using oracle price + /// 3. Adjusting for price exponent + /// 4. Checking against and updating the remaining limit + /// + /// # Arguments + /// * `amount` - The amount of SOL lamports to be used + /// * `oracle_price` - The current oracle price for SOL + /// * `_confidence` - The confidence interval for the oracle price (unused) + /// * `exponent` - The exponent for price calculation + /// + /// # Returns + /// * `Ok(())` - If the operation is within limits + /// * `Err(ProgramError)` - If the operation would exceed the limit or encounters an error + pub fn run_for_sol( + &mut self, + amount: u64, + oracle_price: u64, + _confidence: u64, + exponent: i32, + ) -> Result<(), ProgramError> { + // First check if amount * oracle_price would overflow u64 + if amount != 0 && oracle_price as u128 > u128::MAX / amount as u128 { + return Err(SwigAuthenticateError::PermissionDeniedInsufficientBalance.into()); + } + + let value = if exponent >= 0 { + amount * oracle_price * 10u64.pow(exponent as u32) / 1000 + } else { + amount * oracle_price / (1000 * 10u64.pow((-exponent) as u32)) + }; + + // Check if we have enough limit + if value > self.value_limit { + return Err(SwigAuthenticateError::PermissionDeniedOracleLimitReached.into()); + } + + // Safe to subtract since we verified value <= value_limit + self.value_limit -= value; + Ok(()) + } + + /// Gets the token feed ID and decimal from the token mint address. + /// + /// This function maps token mint addresses to their corresponding oracle feed IDs and mint decimals. + /// Each token has a unique feed ID for price lookups and a specific decimal precision. + /// + /// # Arguments + /// * `token_mint` - The 32-byte array representing the token's mint address + /// + /// # Returns + /// * `Ok(([u8; 32], u8))` - A tuple containing: + /// - The oracle feed ID as a 32-byte array + /// - The token's mint decimal precision + /// * `Err(SwigStateError)` - If the token mint is not recognized + pub fn get_feed_id_and_decimal_from_mint( + token_mint: &[u8], + ) -> Result<([u8; 32], u8), SwigStateError> { + TOKEN_CONFIGS + .iter() + .find(|(mint, _)| mint.as_ref() == token_mint) + .map(|(_, config)| { + let feed_id = get_feed_id_from_hex(config.feed_id)?; + Ok((feed_id, config.decimals)) + }) + .unwrap_or(Err(SwigStateError::FeedIdMustBe32Bytes)) + } +} + +impl Transmutable for OracleTokenLimit { + /// Size of the OracleTokenLimit struct in bytes + const LEN: usize = core::mem::size_of::(); +} + +impl TransmutableMut for OracleTokenLimit {} + +impl IntoBytes for OracleTokenLimit { + /// Converts the OracleTokenLimit struct into a byte slice. + /// + /// # Returns + /// * `Ok(&[u8])` - A byte slice representing the struct + /// * `Err(ProgramError)` - If the conversion fails + 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<'a> Actionable<'a> for OracleTokenLimit { + /// This action represents the OracleTokenLimit permission type + const TYPE: Permission = Permission::OracleTokenLimit; + /// Multiple oracle token limits can exist per role (one per base asset) + const REPEATABLE: bool = true; + + /// Checks if this token limit matches the provided base asset type. + /// + /// # Arguments + /// * `data` - The base asset type to check against (first byte) + /// + /// # Returns + /// `true` if the base asset type matches, `false` otherwise + fn match_data(&self, data: &[u8]) -> bool { + !data.is_empty() && data[0] == self.base_asset_type + } +} + +/// Converts a hex string to a 32-byte array. +/// +/// # Arguments +/// * `input` - A hex string representing the feed ID (with or without "0x" prefix) +/// +/// # Returns +/// * `Ok([u8; 32])` - The feed ID as a 32-byte array +/// * `Err(SwigStateError)` - If the input is invalid +fn get_feed_id_from_hex(input: &str) -> Result<[u8; 32], SwigStateError> { + let mut feed_id = [0; 32]; + match input.len() { + 66 => feed_id.copy_from_slice( + &hex::decode(&input[2..]).map_err(|_| SwigStateError::FeedIdNonHexCharacter)?, + ), + 64 => feed_id.copy_from_slice( + &hex::decode(input).map_err(|_| SwigStateError::FeedIdNonHexCharacter)?, + ), + _ => return Err(SwigStateError::FeedIdMustBe32Bytes), + } + Ok(feed_id) +} + +/// Represents a token mint configuration with its feed ID and decimals +struct TokenConfig { + /// The hex string representation of the oracle feed ID + feed_id: &'static str, + /// The number of decimal places for the token + decimals: u8, +} + +/// Static lookup table mapping mint addresses to their configurations. +/// +/// Each entry contains: +/// - A 32-byte mint address +/// - A TokenConfig with the feed ID and decimals +const TOKEN_CONFIGS: &[(&[u8; 32], TokenConfig)] = &[ + // JitoSOL / USD + ( + &pubkey!("J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn"), + TokenConfig { + feed_id: "67be9f519b95cf24338801051f9a808eff0a578ccb388db73b7f6fe1de019ffb", + decimals: 9, + }, + ), + // mSOL / USD + ( + &pubkey!("mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So"), + TokenConfig { + feed_id: "c2289a6a43d2ce91c6f55caec370f4acc38a2ed477f58813334c6d03749ff2a4", + decimals: 9, + }, + ), + // bSOL / USD + ( + &pubkey!("bSo13r4TkiE4KumL71LsHTPpL2euBYLFx6h9HP3piy1"), + TokenConfig { + feed_id: "89875379e70f8fbadc17aef315adf3a8d5d160b811435537e03c97e8aac97d9c", + decimals: 9, + }, + ), + // BONK / USD + ( + &pubkey!("DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263"), + TokenConfig { + feed_id: "72b021217ca3fe68922a19aaf990109cb9d84e9ad004b4d2025ad6f529314419", + decimals: 5, + }, + ), + // W / USD + ( + &pubkey!("85VBFQZC9TZkfaptBWjvUw7YbZjy52A6mjtPGjstQAmQ"), + TokenConfig { + feed_id: "eff7446475e218517566ea99e72a4abec2e1bd8498b43b7d8331e29dcb059389", + decimals: 9, + }, + ), + // KMNO / USD + ( + &pubkey!("KMNo4fXk3qpr8HTnKydyHqWXyUz1eWQNv6i6gY6A7hN"), + TokenConfig { + feed_id: "b17e5bc5de742a8a378b54c9c75442b7d51e30ada63f28d9bd28d3c0e26511a0", + decimals: 9, + }, + ), + // MEW / USD + ( + &pubkey!("MEW1gQWJ3nEXg2qgERiKu7mFZqun83UZpZUWxT5CakH"), + TokenConfig { + feed_id: "514aed52ca5294177f20187ae883cec4a018619772ddce41efcc36a6448f5d5d", + decimals: 9, + }, + ), + // TNSR / USD + ( + &pubkey!("TNSRxcUxoT9xBG3de7PiJyTDYu7kskLqcpddxnEJAS6"), + TokenConfig { + feed_id: "05ecd4597cd48fe13d6cc3596c62af4f9675aee06e2e0b94c06d8bee2b659e05", + decimals: 9, + }, + ), + // USDC / USD + ( + &pubkey!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"), + TokenConfig { + feed_id: "eaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a", + decimals: 6, + }, + ), + // JTO / USD + ( + &pubkey!("jtojtomepa8beP8AuQc6eXt5FriJwfFMwQx2v2f9mCL"), + TokenConfig { + feed_id: "b43660a5f790c69354b0729a5ef9d50d68f1df92107540210b9cccba1f947cc2", + decimals: 9, + }, + ), + // USDT / USD + ( + &pubkey!("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"), + TokenConfig { + feed_id: "2b89b9dc8fdf9f34709a5b106b472f0f39bb6ca9ce04b0fd7f2e971688e2e53b", + decimals: 6, + }, + ), + // JUP / USD + ( + &pubkey!("JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN"), + TokenConfig { + feed_id: "0a0408d619e9380abad35060f9192039ed5042fa6f82301d0e48bb52be830996", + decimals: 6, + }, + ), + // PYTH / USD + ( + &pubkey!("HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3"), + TokenConfig { + feed_id: "0bbf28e9a841a1cc788f6a361b17ca072d0ea3098a1e5df1c3922d06719579ff", + decimals: 6, + }, + ), + // HNT / USD + ( + &pubkey!("hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux"), + TokenConfig { + feed_id: "649fdd7ec08e8e2a20f425729854e90293dcbe2376abc47197a14da6ff339756", + decimals: 8, + }, + ), + // RENDER / USD + ( + &pubkey!("rndrizKT3MK1iimdxRdWabcF7Zg7AR5T4nud4EkHBof"), + TokenConfig { + feed_id: "3d4a2bd9535be6ce8059d75eadeba507b043257321aa544717c56fa19b49e35d", + decimals: 9, + }, + ), + // ORCA / USD + ( + &pubkey!("orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE"), + TokenConfig { + feed_id: "37505261e557e251290b8c8899453064e8d760ed5c65a779726f2490980da74c", + decimals: 6, + }, + ), + // SAMO / USD + ( + &pubkey!("7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"), + TokenConfig { + feed_id: "49601625e1a342c1f90c3fe6a03ae0251991a1d76e480d2741524c29037be28a", + decimals: 9, + }, + ), + // WIF / USD + ( + &pubkey!("EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm"), + TokenConfig { + feed_id: "4ca4beeca86f0d164160323817a4e42b10010a724c2217c6ee41b54cd4cc61fc", + decimals: 9, + }, + ), + // LST / USD + ( + &pubkey!("LSTxxxnJzKDFSLr4dUkPcmCf5VyryEqzPLz5j4bpxFp"), + TokenConfig { + feed_id: "12fb674ee496045b1d9cf7d5e65379acb026133c2ad69f3ed996fb9fe68e3a37", + decimals: 9, + }, + ), + // PRCL / USD + ( + &pubkey!("PRT88RkA4Kg5z7pKnezeNH4mafTvtQdfFgpQTGRjz44"), + TokenConfig { + feed_id: "5bbd1ce617792b476c55991c27cdfd89794f9f13356babc9c92405f5f0079683", + decimals: 9, + }, + ), + // RAY / USD + ( + &pubkey!("4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R"), + TokenConfig { + feed_id: "91568baa8beb53db23eb3fb7f22c6e8bd303d103919e19733f2bb642d3e7987a", + decimals: 6, + }, + ), + // FIDA / USD + ( + &pubkey!("EchesyfXePKdLtoiZSL8pBe8Myagyy8ZRqsACNCFGnvp"), + TokenConfig { + feed_id: "c80657b7f6f3eac27218d09d5a4e54e47b25768d9f5e10ac15fe2cf900881400", + decimals: 6, + }, + ), + // MNDE / USD + ( + &pubkey!("MNDEFzGvMt87ueuHvVU9VcTqsAP5b3fTGPsHuuPA5ey"), + TokenConfig { + feed_id: "3607bf4d7b78666bd3736c7aacaf2fd2bc56caa8667d3224971ebe3c0623292a", + decimals: 9, + }, + ), + // IOT / USD + ( + &pubkey!("iotEVVZLEywoTn1QdwNPddxPWszn3zFhEot3MfL9fns"), + TokenConfig { + feed_id: "6b701e292e0836d18a5904a08fe94534f9ab5c3d4ff37dc02c74dd0f4901944d", + decimals: 9, + }, + ), + // NEON / USD + ( + &pubkey!("NeonTjSjsuo3rexg9o6vHuMXw62f9V7zvmu8M8Zut44"), + TokenConfig { + feed_id: "d82183dd487bef3208a227bb25d748930db58862c5121198e723ed0976eb92b7", + decimals: 9, + }, + ), + // SLND / USD + ( + &pubkey!("SLNDpmoWTVADgEdndyvWzroNL7zSi1dF9PC3xHGtPwp"), + TokenConfig { + feed_id: "f8d030e4ef460b91ad23eabbbb27aec463e3c30ecc8d5c4b71e92f54a36ccdbd", + decimals: 6, + }, + ), + // WEN / USD + ( + &pubkey!("WENWENvqqNya429ubCdR81ZmD69brwQaaBYY6p3LCpk"), + TokenConfig { + feed_id: "5169491cd7e2a44c98353b779d5eb612e4ac32e073f5cc534303d86307c2f1bc", + decimals: 9, + }, + ), + // BLZE / USD + ( + &pubkey!("BLZEEuZUBV2FNLJ5h5UKxBg2u8mKYbf5Zo6W8PE7d9y"), + TokenConfig { + feed_id: "93c3def9b169f49eed14c9d73ed0e942c666cf0e1290657ec82038ebb792c2a8", + decimals: 9, + }, + ), + // JLP / USD + ( + &pubkey!("JLPx6n7WC1Y3yQ4q8GkARyMHVZ5noqYjF8qyWY5y2P6"), + TokenConfig { + feed_id: "c811abc82b4bad1f9bd711a2773ccaa935b03ecef974236942cec5e0eb845a3a", + decimals: 9, + }, + ), + // WBTC / USD + ( + &pubkey!("9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E"), + TokenConfig { + feed_id: "c9d8b075a5c69303365ae23633d4e085199bf5c520a3b90fed1322a0342ffc33", + decimals: 8, + }, + ), + // PENGU / USD + ( + &pubkey!("PENGUxLhrQwB1QJ2kQ3Y1F5Sq8UdVFQo5Xvg4Z5AxAt"), + TokenConfig { + feed_id: "bed3097008b9b5e3c93bec20be79cb43986b85a996475589351a21e67bae9b61", + decimals: 9, + }, + ), + // TRUMP / USD + ( + &pubkey!("2d9FCSx5QYAJs3YQeS2WATx3W98v3QH1N2k2ZkF5XQ5F"), + TokenConfig { + feed_id: "879551021853eec7a7dc827578e8e69da7e4fa8148339aa0d3d5296405be4b1a", + decimals: 9, + }, + ), + // FARTCOIN / USD + ( + &pubkey!("2t8eUbYKjidMs3uSeYM9jXM9uudYZwGkSeTB4TKjmvnC"), + TokenConfig { + feed_id: "58cd29ef0e714c5affc44f269b2c1899a52da4169d7acc147b9da692e6953608", + decimals: 9, + }, + ), + // ACRED / USD + ( + &pubkey!("6gyQ2TKvvV1JB5oWDobndv6BLRWcJzeBNk9PLQ5uPQms"), + TokenConfig { + feed_id: "40ac3329933a6b5b65cf31496018c5764ac0567316146f7d0de00095886b480d", + decimals: 9, + }, + ), + // TEST / USD + ( + &pubkey!("Hfmh5FEBkR17ame7dhjMFjaFLtP1Mbp6mgT1Cv86YhLW"), + TokenConfig { + feed_id: "ef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d", + decimals: 9, + }, + ), +]; diff --git a/state-x/src/action/sub_account.rs b/state-x/src/action/sub_account.rs index 9290e1ce..6e15ce6b 100644 --- a/state-x/src/action/sub_account.rs +++ b/state-x/src/action/sub_account.rs @@ -6,6 +6,7 @@ use no_padding::NoPadding; use pinocchio::program_error::ProgramError; +use swig_assertions::sol_assert_bytes_eq; use super::{Actionable, Permission}; use crate::{IntoBytes, Transmutable, TransmutableMut}; @@ -41,9 +42,30 @@ impl<'a> Actionable<'a> for SubAccount { /// Multiple sub-account permissions can exist per role const REPEATABLE: bool = true; - /// Always returns true as matching is handled elsewhere + /// Checks if this sub-account permission matches the provided data. + /// + /// For sub-account creation, matches against empty data (since sub_account + /// is initially zeroed). For toggle/withdraw operations, matches + /// against the actual sub-account pubkey. + /// + /// # Arguments + /// * `data` - The data to match against (empty for creation, pubkey for + /// operations) + /// + /// # Returns + /// * `true` if the data matches this sub-account permission + /// * `false` if the data doesn't match fn match_data(&self, data: &[u8]) -> bool { - true + if data.is_empty() { + // For sub-account creation, match against zeroed sub_account field + sol_assert_bytes_eq(&self.sub_account, &[0u8; 32], 32); + self.sub_account == [0u8; 32] + } else if data.len() == 32 { + // For other operations, match against the actual sub-account pubkey + sol_assert_bytes_eq(&self.sub_account, data, 32) + } else { + false + } } /// Validates that the data has the correct length and is zeroed. diff --git a/state-x/src/authority/ed25519.rs b/state-x/src/authority/ed25519.rs index b6076572..c705d37e 100644 --- a/state-x/src/authority/ed25519.rs +++ b/state-x/src/authority/ed25519.rs @@ -83,6 +83,10 @@ impl AuthorityInfo for ED25519Authority { Ok(self.public_key.as_ref()) } + fn signature_odometer(&self) -> Option { + None + } + fn authenticate( &mut self, account_infos: &[AccountInfo], @@ -235,6 +239,10 @@ impl AuthorityInfo for Ed25519SessionAuthority { Ok(self.public_key.as_ref()) } + fn signature_odometer(&self) -> Option { + None + } + fn as_any(&self) -> &dyn Any { self } diff --git a/state-x/src/authority/mod.rs b/state-x/src/authority/mod.rs index 192dcd7d..a433b2b5 100644 --- a/state-x/src/authority/mod.rs +++ b/state-x/src/authority/mod.rs @@ -7,12 +7,14 @@ pub mod ed25519; pub mod secp256k1; +pub mod secp256r1; use std::any::Any; use ed25519::{ED25519Authority, Ed25519SessionAuthority}; use pinocchio::{account_info::AccountInfo, program_error::ProgramError}; use secp256k1::{Secp256k1Authority, Secp256k1SessionAuthority}; +use secp256r1::{Secp256r1Authority, Secp256r1SessionAuthority}; use crate::{IntoBytes, SwigAuthenticateError, Transmutable, TransmutableMut}; @@ -58,6 +60,9 @@ pub trait AuthorityInfo: IntoBytes { /// Returns the identity bytes for this authority fn identity(&self) -> Result<&[u8], ProgramError>; + /// Returns the signature odometer for this authority if it exists + fn signature_odometer(&self) -> Option; + /// Authenticates a session-based operation. /// /// # Arguments @@ -120,10 +125,10 @@ pub enum AuthorityType { Secp256k1, /// Session-based Secp256k1 authority Secp256k1Session, + /// Standard Secp256r1 authority (for passkeys) + Secp256r1, /// Session-based Secp256r1 authority Secp256r1Session, - /// Session-based R1 Passkey authority - R1PasskeySession, } impl TryFrom for AuthorityType { @@ -137,8 +142,8 @@ impl TryFrom for AuthorityType { 2 => Ok(AuthorityType::Ed25519Session), 3 => Ok(AuthorityType::Secp256k1), 4 => Ok(AuthorityType::Secp256k1Session), - 5 => Ok(AuthorityType::Secp256r1Session), - 6 => Ok(AuthorityType::R1PasskeySession), + 5 => Ok(AuthorityType::Secp256r1), + 6 => Ok(AuthorityType::Secp256r1Session), _ => Err(ProgramError::InvalidInstructionData), } } @@ -160,6 +165,8 @@ pub const fn authority_type_to_length( AuthorityType::Ed25519Session => Ok(Ed25519SessionAuthority::LEN), AuthorityType::Secp256k1 => Ok(Secp256k1Authority::LEN), AuthorityType::Secp256k1Session => Ok(Secp256k1SessionAuthority::LEN), + AuthorityType::Secp256r1 => Ok(Secp256r1Authority::LEN), + AuthorityType::Secp256r1Session => Ok(Secp256r1SessionAuthority::LEN), _ => Err(ProgramError::InvalidInstructionData), } } diff --git a/state-x/src/authority/secp256k1.rs b/state-x/src/authority/secp256k1.rs index 1b2fbcea..58e8b9ae 100644 --- a/state-x/src/authority/secp256k1.rs +++ b/state-x/src/authority/secp256k1.rs @@ -12,7 +12,7 @@ use core::mem::MaybeUninit; #[allow(unused_imports)] use pinocchio::syscalls::{sol_keccak256, sol_secp256k1_recover, sol_sha256}; -use pinocchio::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey}; +use pinocchio::{account_info::AccountInfo, msg, program_error::ProgramError, pubkey::Pubkey}; use swig_assertions::sol_assert_bytes_eq; use super::{ed25519::ed25519_authenticate, Authority, AuthorityInfo, AuthorityType}; @@ -128,12 +128,16 @@ impl AuthorityInfo for Secp256k1Authority { Ok(self.public_key.as_ref()) } + fn signature_odometer(&self) -> Option { + Some(self.signature_odometer) + } + fn match_data(&self, data: &[u8]) -> bool { if data.len() != 64 { return false; } let expanded = compress(data.try_into().unwrap()); - sol_assert_bytes_eq(&self.public_key, &expanded, 32) + sol_assert_bytes_eq(&self.public_key, &expanded, 33) } fn as_any(&self) -> &dyn std::any::Any { @@ -169,7 +173,9 @@ impl IntoBytes for Secp256k1Authority { pub struct Secp256k1SessionAuthority { /// The compressed Secp256k1 public key (33 bytes) pub public_key: [u8; 33], - _padding: [u8; 7], + _padding: [u8; 3], + /// Signature counter to prevent signature replay attacks + pub signature_odometer: u32, /// The current session key pub session_key: [u8; 32], /// Maximum allowed session duration @@ -193,6 +199,7 @@ impl Authority for Secp256k1SessionAuthority { let authority = unsafe { Secp256k1SessionAuthority::load_mut_unchecked(bytes)? }; let compressed = compress(&create.public_key); authority.public_key = compressed; + authority.signature_odometer = 0; authority.session_key = create.session_key; authority.max_session_age = create.max_session_length; Ok(()) @@ -217,13 +224,17 @@ impl AuthorityInfo for Secp256k1SessionAuthority { return false; } let expanded = compress(data.try_into().unwrap()); - sol_assert_bytes_eq(data, &expanded, 33) + sol_assert_bytes_eq(&self.public_key, &expanded, 33) } fn identity(&self) -> Result<&[u8], ProgramError> { Ok(self.public_key.as_ref()) } + fn signature_odometer(&self) -> Option { + Some(self.signature_odometer) + } + fn as_any(&self) -> &dyn std::any::Any { self } @@ -236,7 +247,7 @@ impl AuthorityInfo for Secp256k1SessionAuthority { slot: u64, ) -> Result<(), ProgramError> { secp_session_authority_authenticate( - &self.public_key, + self, authority_payload, data_payload, slot, @@ -306,17 +317,25 @@ fn secp_authority_authenticate( if authority_payload.len() < 77 { return Err(SwigAuthenticateError::InvalidAuthorityPayload.into()); } - let authority_slot = - u64::from_le_bytes(unsafe { authority_payload.get_unchecked(..8).try_into().unwrap() }); - let counter = - u32::from_le_bytes(unsafe { authority_payload.get_unchecked(8..12).try_into().unwrap() }); + let authority_slot = u64::from_le_bytes(unsafe { + authority_payload + .get_unchecked(..8) + .try_into() + .map_err(|_| SwigAuthenticateError::InvalidAuthorityPayload)? + }); + + let counter = u32::from_le_bytes(unsafe { + authority_payload + .get_unchecked(8..12) + .try_into() + .map_err(|_| SwigAuthenticateError::InvalidAuthorityPayload)? + }); let expected_counter = authority.signature_odometer.wrapping_add(1); if counter != expected_counter { return Err(SwigAuthenticateError::PermissionDeniedSecp256k1SignatureReused.into()); } - secp256k1_authenticate( &authority.public_key, authority_payload[12..77].try_into().unwrap(), @@ -335,34 +354,45 @@ fn secp_authority_authenticate( /// Authenticates a Secp256k1 session authority with additional payload data. /// /// # Arguments -/// * `expected_key` - The expected compressed public key -/// * `authority_payload` - The authority payload including slot and signature +/// * `authority` - The mutable authority reference for counter updates +/// * `authority_payload` - The authority payload including slot, counter, and +/// signature /// * `data_payload` - Additional data to be included in signature verification /// * `current_slot` - The current slot number /// * `account_infos` - List of accounts involved in the transaction fn secp_session_authority_authenticate( - expected_key: &[u8; 33], + authority: &mut Secp256k1SessionAuthority, authority_payload: &[u8], data_payload: &[u8], current_slot: u64, account_infos: &[AccountInfo], ) -> Result<(), ProgramError> { - if authority_payload.len() < 73 { + if authority_payload.len() < 77 { return Err(SwigAuthenticateError::InvalidAuthorityPayload.into()); } let authority_slot = u64::from_le_bytes(unsafe { authority_payload.get_unchecked(..8).try_into().unwrap() }); + let counter = + u32::from_le_bytes(unsafe { authority_payload.get_unchecked(8..12).try_into().unwrap() }); + + let expected_counter = authority.signature_odometer.wrapping_add(1); + if counter != expected_counter { + return Err(SwigAuthenticateError::PermissionDeniedSecp256k1SignatureReused.into()); + } + secp256k1_authenticate( - expected_key, - authority_payload[8..73].try_into().unwrap(), + &authority.public_key, + authority_payload[12..77].try_into().unwrap(), data_payload, authority_slot, current_slot, account_infos, - authority_payload[73..].try_into().unwrap(), - 0u32, // Session authorities don't use counter-based replay protection + authority_payload[77..].try_into().unwrap(), + counter, // Now use proper counter-based replay protection )?; + + authority.signature_odometer = counter; Ok(()) } @@ -478,7 +508,7 @@ fn secp256k1_authenticate( } // First compress the recovered key to 33 bytes let compressed_recovered_key = compress(&recovered_key.assume_init()); - sol_assert_bytes_eq(&compressed_recovered_key, expected_key, 32) + sol_assert_bytes_eq(&compressed_recovered_key, expected_key, 33) }; if !matches { return Err(SwigAuthenticateError::PermissionDenied.into()); diff --git a/state-x/src/authority/secp256r1.rs b/state-x/src/authority/secp256r1.rs new file mode 100644 index 00000000..893c45ed --- /dev/null +++ b/state-x/src/authority/secp256r1.rs @@ -0,0 +1,838 @@ +//! Secp256r1 authority implementation for passkey support. +//! +//! This module provides implementations for Secp256r1-based authority types in +//! the Swig wallet system, designed to work with passkeys and WebAuthn. It +//! includes both standard Secp256r1 authority and session-based Secp256r1 +//! authority with expiration support. The implementation relies on the Solana +//! secp256r1 precompile program for signature verification. + +#![warn(unexpected_cfgs)] + +use core::mem::MaybeUninit; + +#[allow(unused_imports)] +use pinocchio::syscalls::sol_sha256; +use pinocchio::{ + account_info::AccountInfo, + program_error::ProgramError, + sysvars::instructions::{Instructions, INSTRUCTIONS_ID}, +}; +use pinocchio_pubkey::pubkey; +use swig_assertions::sol_assert_bytes_eq; + +use super::{Authority, AuthorityInfo, AuthorityType}; +use crate::{IntoBytes, SwigAuthenticateError, SwigStateError, Transmutable, TransmutableMut}; + +/// Maximum age (in slots) for a Secp256r1 signature to be considered valid +const MAX_SIGNATURE_AGE_IN_SLOTS: u64 = 60; + +/// Secp256r1 program ID +const SECP256R1_PROGRAM_ID: [u8; 32] = pubkey!("Secp256r1SigVerify1111111111111111111111111"); + +/// Constants from the secp256r1 program +const COMPRESSED_PUBKEY_SERIALIZED_SIZE: usize = 33; +const SIGNATURE_SERIALIZED_SIZE: usize = 64; +const SIGNATURE_OFFSETS_SERIALIZED_SIZE: usize = 14; +const SIGNATURE_OFFSETS_START: usize = 2; +const DATA_START: usize = SIGNATURE_OFFSETS_SERIALIZED_SIZE + SIGNATURE_OFFSETS_START; +const PUBKEY_DATA_OFFSET: usize = DATA_START; +const SIGNATURE_DATA_OFFSET: usize = DATA_START + COMPRESSED_PUBKEY_SERIALIZED_SIZE; +const MESSAGE_DATA_OFFSET: usize = SIGNATURE_DATA_OFFSET + SIGNATURE_SERIALIZED_SIZE; +const MESSAGE_DATA_SIZE: usize = 32; +const WEBAUTHN_AUTHENTICATOR_DATA_MAX_SIZE: usize = 196; + +/// Secp256r1 signature offsets structure (matches solana-secp256r1-program) +#[derive(Debug, Copy, Clone)] +#[repr(C)] +pub struct Secp256r1SignatureOffsets { + /// Offset to compact secp256r1 signature of 64 bytes + pub signature_offset: u16, + /// Instruction index where the signature can be found + pub signature_instruction_index: u16, + /// Offset to compressed public key of 33 bytes + pub public_key_offset: u16, + /// Instruction index where the public key can be found + pub public_key_instruction_index: u16, + /// Offset to the start of message data + pub message_data_offset: u16, + /// Size of message data in bytes + pub message_data_size: u16, + /// Instruction index where the message data can be found + pub message_instruction_index: u16, +} + +/// Creation parameters for a session-based Secp256r1 authority. +#[derive(Debug, no_padding::NoPadding)] +#[repr(C, align(8))] +pub struct CreateSecp256r1SessionAuthority { + /// The compressed Secp256r1 public key (33 bytes) + pub public_key: [u8; 33], + /// Padding for alignment + _padding: [u8; 7], + /// 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 CreateSecp256r1SessionAuthority { + /// Creates a new set of session authority parameters. + /// + /// # Arguments + /// * `public_key` - The compressed Secp256r1 public key + /// * `session_key` - The initial session key + /// * `max_session_length` - Maximum allowed session duration + pub fn new(public_key: [u8; 33], session_key: [u8; 32], max_session_length: u64) -> Self { + Self { + public_key, + _padding: [0; 7], + session_key, + max_session_length, + } + } +} + +impl Transmutable for CreateSecp256r1SessionAuthority { + const LEN: usize = 33 + 7 + 32 + 8; // Include the 7 bytes of padding +} + +impl TransmutableMut for CreateSecp256r1SessionAuthority {} + +impl IntoBytes for CreateSecp256r1SessionAuthority { + 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) + } +} + +/// Standard Secp256r1 authority implementation for passkey support. +/// +/// This struct represents a Secp256r1 authority with a compressed public key +/// for signature verification using the Solana secp256r1 precompile program. +#[derive(Debug, no_padding::NoPadding)] +#[repr(C, align(8))] +pub struct Secp256r1Authority { + /// The compressed Secp256r1 public key (33 bytes) + pub public_key: [u8; 33], + /// Padding for u32 alignment + _padding: [u8; 3], + /// Signature counter to prevent signature replay attacks + pub signature_odometer: u32, +} + +impl Secp256r1Authority { + /// Creates a new Secp256r1Authority with a compressed public key. + pub fn new(public_key: [u8; 33]) -> Self { + Self { + public_key, + _padding: [0; 3], + signature_odometer: 0, + } + } +} + +impl Transmutable for Secp256r1Authority { + const LEN: usize = core::mem::size_of::(); +} + +impl TransmutableMut for Secp256r1Authority {} + +impl Authority for Secp256r1Authority { + const TYPE: AuthorityType = AuthorityType::Secp256r1; + const SESSION_BASED: bool = false; + + fn set_into_bytes(create_data: &[u8], bytes: &mut [u8]) -> Result<(), ProgramError> { + if create_data.len() != 33 { + return Err(SwigStateError::InvalidRoleData.into()); + } + let authority = unsafe { Secp256r1Authority::load_mut_unchecked(bytes)? }; + authority.public_key.copy_from_slice(create_data); + authority.signature_odometer = 0; + Ok(()) + } +} + +impl AuthorityInfo for Secp256r1Authority { + 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.public_key.as_ref()) + } + + fn signature_odometer(&self) -> Option { + Some(self.signature_odometer) + } + + fn match_data(&self, data: &[u8]) -> bool { + if data.len() != 33 { + return false; + } + sol_assert_bytes_eq(&self.public_key, data, 33) + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn authenticate( + &mut self, + account_infos: &[pinocchio::account_info::AccountInfo], + authority_payload: &[u8], + data_payload: &[u8], + slot: u64, + ) -> Result<(), ProgramError> { + secp256r1_authority_authenticate(self, authority_payload, data_payload, slot, account_infos) + } +} + +impl IntoBytes for Secp256r1Authority { + 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 Secp256r1 authority implementation. +/// +/// This struct represents a Secp256r1 authority that supports temporary session +/// keys with expiration times. It maintains both a root public key and a +/// session key. +#[derive(Debug, no_padding::NoPadding)] +#[repr(C, align(8))] +pub struct Secp256r1SessionAuthority { + /// The compressed Secp256r1 public key (33 bytes) + pub public_key: [u8; 33], + _padding: [u8; 3], + /// Signature counter to prevent signature replay attacks + pub signature_odometer: u32, + /// The current session key + pub session_key: [u8; 32], + /// Maximum allowed session duration + pub max_session_age: u64, + /// Slot when the current session expires + pub current_session_expiration: u64, +} + +impl Transmutable for Secp256r1SessionAuthority { + const LEN: usize = core::mem::size_of::(); +} + +impl TransmutableMut for Secp256r1SessionAuthority {} + +impl Authority for Secp256r1SessionAuthority { + const TYPE: AuthorityType = AuthorityType::Secp256r1Session; + const SESSION_BASED: bool = true; + + fn set_into_bytes(create_data: &[u8], bytes: &mut [u8]) -> Result<(), ProgramError> { + let create = unsafe { CreateSecp256r1SessionAuthority::load_unchecked(create_data)? }; + let authority = unsafe { Secp256r1SessionAuthority::load_mut_unchecked(bytes)? }; + authority.public_key = create.public_key; + authority.signature_odometer = 0; + authority.session_key = create.session_key; + authority.max_session_age = create.max_session_length; + Ok(()) + } +} + +impl AuthorityInfo for Secp256r1SessionAuthority { + 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() != 33 { + return false; + } + sol_assert_bytes_eq(&self.public_key, data, 33) + } + + fn identity(&self) -> Result<&[u8], ProgramError> { + Ok(self.public_key.as_ref()) + } + + fn signature_odometer(&self) -> Option { + Some(self.signature_odometer) + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn authenticate( + &mut self, + account_infos: &[pinocchio::account_info::AccountInfo], + authority_payload: &[u8], + data_payload: &[u8], + slot: u64, + ) -> Result<(), ProgramError> { + secp256r1_session_authority_authenticate( + self, + authority_payload, + data_payload, + slot, + account_infos, + ) + } + + fn authenticate_session( + &mut self, + account_infos: &[AccountInfo], + authority_payload: &[u8], + _data_payload: &[u8], + slot: u64, + ) -> Result<(), ProgramError> { + use super::ed25519::ed25519_authenticate; + + if slot > self.current_session_expiration { + return Err(SwigAuthenticateError::PermissionDeniedSessionExpired.into()); + } + ed25519_authenticate( + account_infos, + authority_payload[0] as usize, + &self.session_key, + ) + } + + fn start_session( + &mut self, + session_key: [u8; 32], + current_slot: u64, + duration: u64, + ) -> Result<(), ProgramError> { + if sol_assert_bytes_eq(&self.session_key, &session_key, 32) { + return Err(SwigAuthenticateError::InvalidSessionKeyCannotReuseSessionKey.into()); + } + if duration > self.max_session_age { + return Err(SwigAuthenticateError::InvalidSessionDuration.into()); + } + self.current_session_expiration = current_slot + duration; + self.session_key = session_key; + Ok(()) + } +} + +impl IntoBytes for Secp256r1SessionAuthority { + 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) + } +} + +/// Authenticates a Secp256r1 authority with additional payload data. +/// +/// # Arguments +/// * `authority` - The mutable authority reference for counter updates +/// * `authority_payload` - The authority payload including slot, counter, +/// instruction index, and signature +/// * `data_payload` - Additional data to be included in signature verification +/// * `current_slot` - The current slot number +/// * `account_infos` - List of accounts involved in the transaction +fn secp256r1_authority_authenticate( + authority: &mut Secp256r1Authority, + authority_payload: &[u8], + data_payload: &[u8], + current_slot: u64, + account_infos: &[AccountInfo], +) -> Result<(), ProgramError> { + if authority_payload.len() < 17 { + // 8 + 4 + 1 + 4 = slot + counter + instructions_account_index + extra data + return Err(SwigAuthenticateError::InvalidAuthorityPayload.into()); + } + + let authority_slot = u64::from_le_bytes(unsafe { + authority_payload + .get_unchecked(..8) + .try_into() + .map_err(|_| SwigAuthenticateError::InvalidAuthorityPayload)? + }); + + let counter = u32::from_le_bytes(unsafe { + authority_payload + .get_unchecked(8..12) + .try_into() + .map_err(|_| SwigAuthenticateError::InvalidAuthorityPayload)? + }); + + let instruction_account_index = authority_payload[12] as usize; + + let expected_counter = authority.signature_odometer.wrapping_add(1); + if counter != expected_counter { + return Err(SwigAuthenticateError::PermissionDeniedSecp256r1SignatureReused.into()); + } + + secp256r1_authenticate( + &authority.public_key, + data_payload, + authority_slot, + current_slot, + account_infos, + instruction_account_index, + counter, + &authority_payload[17..], + )?; + + authority.signature_odometer = counter; + Ok(()) +} + +/// Authenticates a Secp256r1 session authority with additional payload data. +/// +/// # Arguments +/// * `authority` - The mutable authority reference for counter updates +/// * `authority_payload` - The authority payload including slot, counter, and +/// instruction index +/// * `data_payload` - Additional data to be included in signature verification +/// * `current_slot` - The current slot number +/// * `account_infos` - List of accounts involved in the transaction +fn secp256r1_session_authority_authenticate( + authority: &mut Secp256r1SessionAuthority, + authority_payload: &[u8], + data_payload: &[u8], + current_slot: u64, + account_infos: &[AccountInfo], +) -> Result<(), ProgramError> { + if authority_payload.len() < 13 { + // 8 + 4 + 1 = slot + counter + instruction_index + return Err(SwigAuthenticateError::InvalidAuthorityPayload.into()); + } + + let authority_slot = + u64::from_le_bytes(unsafe { authority_payload.get_unchecked(..8).try_into().unwrap() }); + + let counter = + u32::from_le_bytes(unsafe { authority_payload.get_unchecked(8..12).try_into().unwrap() }); + + let instruction_index = authority_payload[12] as usize; + + let expected_counter = authority.signature_odometer.wrapping_add(1); + if counter != expected_counter { + return Err(SwigAuthenticateError::PermissionDeniedSecp256r1SignatureReused.into()); + } + + secp256r1_authenticate( + &authority.public_key, + data_payload, + authority_slot, + current_slot, + account_infos, + instruction_index, + counter, // Now use proper counter-based replay protection + &authority_payload[17..], + )?; + + authority.signature_odometer = counter; + Ok(()) +} + +/// Core Secp256r1 signature verification function. +/// +/// This function performs the actual signature verification by: +/// - Validating signature age +/// - Computing the message hash (including counter for replay protection) +/// - Finding and validating the secp256r1 precompile instruction +/// - Verifying the message hash matches what was passed to the precompile +/// - Verifying the public key matches +fn secp256r1_authenticate( + expected_key: &[u8; 33], + data_payload: &[u8], + authority_slot: u64, + current_slot: u64, + account_infos: &[AccountInfo], + instruction_account_index: usize, + counter: u32, + additional_paylaod: &[u8], +) -> Result<(), ProgramError> { + // Validate signature age + if current_slot < authority_slot || current_slot - authority_slot > MAX_SIGNATURE_AGE_IN_SLOTS { + return Err(SwigAuthenticateError::PermissionDeniedSecp256k1InvalidSignatureAge.into()); + } + + // Compute our expected message hash + let computed_hash = compute_message_hash(data_payload, account_infos, authority_slot, counter)?; + let mut message_buf: MaybeUninit<[u8; WEBAUTHN_AUTHENTICATOR_DATA_MAX_SIZE + 32]> = + MaybeUninit::uninit(); + + let message = if additional_paylaod.is_empty() { + &computed_hash + } else { + webauthn_message(additional_paylaod, computed_hash, unsafe { + &mut *message_buf.as_mut_ptr() + })? + }; + + // Get the sysvar instructions account + let sysvar_instructions = account_infos + .get(instruction_account_index) + .ok_or(SwigAuthenticateError::InvalidAuthorityPayload)?; + + // Verify this is the sysvar instructions account + + if sysvar_instructions.key().as_ref() != &INSTRUCTIONS_ID { + return Err(SwigAuthenticateError::PermissionDeniedSecp256r1InvalidInstruction.into()); + } + + 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; + if current_index == 0 { + return Err(SwigAuthenticateError::PermissionDeniedSecp256r1InvalidInstruction.into()); + } + let secpr1ix = unsafe { ixs.deserialize_instruction_unchecked(current_index - 1) }; + // Verify the instruction is calling the secp256r1 program + if secpr1ix.get_program_id() != &SECP256R1_PROGRAM_ID { + return Err(SwigAuthenticateError::PermissionDeniedSecp256r1InvalidInstruction.into()); + } + let instruction_data = secpr1ix.get_instruction_data(); + // Parse and verify the secp256r1 instruction data + verify_secp256r1_instruction_data(&instruction_data, expected_key, message)?; + Ok(()) +} + +/// Compute the message hash for secp256r1 authentication +fn compute_message_hash( + data_payload: &[u8], + account_infos: &[AccountInfo], + authority_slot: u64, + counter: u32, +) -> Result<[u8; 32], ProgramError> { + use super::secp256k1::AccountsPayload; + + let mut accounts_payload = [0u8; 64 * AccountsPayload::LEN]; + let mut cursor = 0; + for account in account_infos { + let offset = cursor + AccountsPayload::LEN; + accounts_payload[cursor..offset] + .copy_from_slice(AccountsPayload::from(account).into_bytes()?); + cursor = offset; + } + let mut hash = MaybeUninit::<[u8; 32]>::uninit(); + let data: &[&[u8]] = &[ + data_payload, + &accounts_payload[..cursor], + &authority_slot.to_le_bytes(), + &counter.to_le_bytes(), + ]; + + unsafe { + #[cfg(target_os = "solana")] + let res = pinocchio::syscalls::sol_keccak256( + data.as_ptr() as *const u8, + 4, + hash.as_mut_ptr() as *mut u8, + ); + #[cfg(not(target_os = "solana"))] + let res = 0; + if res != 0 { + return Err(SwigAuthenticateError::PermissionDeniedSecp256k1InvalidHash.into()); + } + + Ok(hash.assume_init()) + } +} + +fn webauthn_message<'a>( + auth_payload: &[u8], + computed_hash: [u8; 32], + message_buf: &'a mut [u8], +) -> Result<&'a [u8], ProgramError> { + // let _auth_type = u16::from_le_bytes(prefix[..2].try_into().unwrap()); + let auth_len = u16::from_le_bytes(auth_payload[2..4].try_into().unwrap()) as usize; + + if auth_len >= WEBAUTHN_AUTHENTICATOR_DATA_MAX_SIZE { + return Err(SwigAuthenticateError::PermissionDeniedSecp256r1InvalidMessage.into()); + } + + let auth_data = &auth_payload[4..4 + auth_len]; + + // Check if we have exactly 32 bytes after auth_data (SHA256 hash of + // clientDataJSON) + let remaining_bytes = auth_payload.len() - (4 + auth_len); + if remaining_bytes != 32 { + return Err(SwigAuthenticateError::PermissionDeniedSecp256r1InvalidMessage.into()); + } + + let client_data_json_hash = &auth_payload[4 + auth_len..4 + auth_len + 32]; + + // The client_data_json_hash is the SHA256 of clientDataJSON provided by the + // frontend We use this directly instead of computing it from the full JSON + message_buf[0..auth_len].copy_from_slice(auth_data); + message_buf[auth_len..auth_len + 32].copy_from_slice(client_data_json_hash); + + Ok(&message_buf[..auth_len + 32]) +} + +/// Verify the secp256r1 instruction data contains the expected signature and +/// public key +fn verify_secp256r1_instruction_data( + instruction_data: &[u8], + expected_pubkey: &[u8; 33], + expected_message: &[u8], +) -> Result<(), ProgramError> { + // Minimum check: must have at least the header and offsets + if instruction_data.len() < DATA_START { + return Err(SwigAuthenticateError::PermissionDeniedSecp256r1InvalidInstruction.into()); + } + let num_signatures = instruction_data[0] as usize; + if num_signatures == 0 || num_signatures > 1 { + return Err(SwigAuthenticateError::PermissionDeniedSecp256r1InvalidInstruction.into()); + } + + if instruction_data.len() < MESSAGE_DATA_OFFSET + MESSAGE_DATA_SIZE { + return Err(SwigAuthenticateError::PermissionDeniedSecp256r1InvalidInstruction.into()); + } + let pubkey_data = &instruction_data + [PUBKEY_DATA_OFFSET..PUBKEY_DATA_OFFSET + COMPRESSED_PUBKEY_SERIALIZED_SIZE]; + let message_data = + &instruction_data[MESSAGE_DATA_OFFSET..MESSAGE_DATA_OFFSET + expected_message.len()]; + + if pubkey_data != expected_pubkey { + return Err(SwigAuthenticateError::PermissionDeniedSecp256r1InvalidPubkey.into()); + } + if message_data != expected_message { + return Err(SwigAuthenticateError::PermissionDeniedSecp256r1InvalidMessageHash.into()); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Helper function to create real secp256r1 instruction data using the + /// official Solana secp256r1 program + fn create_test_secp256r1_instruction_data( + message: &[u8], + signature: &[u8; 64], + pubkey: &[u8; 33], + ) -> Vec { + use solana_secp256r1_program::new_secp256r1_instruction_with_signature; + + // Use the official Solana function to create the instruction data + // This ensures we match exactly what the Solana runtime expects + let instruction = new_secp256r1_instruction_with_signature(message, signature, pubkey); + + instruction.data + } + + /// Helper function to create a signature using OpenSSL for testing + fn create_test_signature_and_pubkey(message: &[u8]) -> ([u8; 64], [u8; 33]) { + use openssl::{ + bn::BigNumContext, + ec::{EcGroup, EcKey, PointConversionForm}, + nid::Nid, + }; + use solana_secp256r1_program::sign_message; + + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let signing_key = EcKey::generate(&group).unwrap(); + + let signature = sign_message(message, &signing_key.private_key_to_der().unwrap()).unwrap(); + + let mut ctx = BigNumContext::new().unwrap(); + let pubkey_bytes = signing_key + .public_key() + .to_bytes(&group, PointConversionForm::COMPRESSED, &mut ctx) + .unwrap(); + + assert_eq!(pubkey_bytes.len(), COMPRESSED_PUBKEY_SERIALIZED_SIZE); + + (signature, pubkey_bytes.try_into().unwrap()) + } + + #[test] + fn test_verify_secp256r1_instruction_data_single_signature() { + let test_message = [0u8; 32]; + let test_signature = [0xCD; 64]; // Test signature + let test_pubkey = [0x02; 33]; // Test compressed pubkey + + let instruction_data = + create_test_secp256r1_instruction_data(&test_message, &test_signature, &test_pubkey); + + // Should succeed with matching pubkey and message hash + let result = + verify_secp256r1_instruction_data(&instruction_data, &test_pubkey, &test_message); + assert!( + result.is_ok(), + "Verification should succeed with correct data. Error: {:?}", + result.err() + ); + } + + #[test] + fn test_verify_secp256r1_instruction_data_wrong_pubkey() { + let test_message = [0u8; 32]; + let test_pubkey = [0x02; 33]; + let wrong_pubkey = [0x03; 33]; // Different pubkey + let test_signature = [0xCD; 64]; + + let instruction_data = + create_test_secp256r1_instruction_data(&test_message, &test_signature, &test_pubkey); + + // Should fail with wrong pubkey + let result = + verify_secp256r1_instruction_data(&instruction_data, &wrong_pubkey, &test_message); + assert!( + result.is_err(), + "Verification should fail with wrong pubkey" + ); + assert_eq!( + result.unwrap_err(), + SwigAuthenticateError::PermissionDeniedSecp256r1InvalidPubkey.into() + ); + } + + #[test] + fn test_verify_secp256r1_instruction_data_wrong_message_hash() { + let test_message = [0u8; 32]; + let wrong_message = [1u8; 32]; // Different message + let test_pubkey = [0x02; 33]; + let test_signature = [0xCD; 64]; + + let instruction_data = + create_test_secp256r1_instruction_data(&test_message, &test_signature, &test_pubkey); + + // Should fail with wrong message hash + let result = + verify_secp256r1_instruction_data(&instruction_data, &test_pubkey, &wrong_message); + assert!( + result.is_err(), + "Verification should fail with wrong message hash" + ); + assert_eq!( + result.unwrap_err(), + SwigAuthenticateError::PermissionDeniedSecp256r1InvalidMessageHash.into() + ); + } + + #[test] + fn test_verify_secp256r1_instruction_data_insufficient_length() { + let short_data = vec![0x01, 0x00]; // Only 2 bytes + + let test_pubkey = [0x02; 33]; + let test_message_hash = [0xAB; 32]; + + let result = + verify_secp256r1_instruction_data(&short_data, &test_pubkey, &test_message_hash); + assert!( + result.is_err(), + "Verification should fail with insufficient data" + ); + assert_eq!( + result.unwrap_err(), + SwigAuthenticateError::PermissionDeniedSecp256r1InvalidInstruction.into() + ); + } + + #[test] + fn test_verify_secp256r1_instruction_data_zero_signatures() { + let mut instruction_data = Vec::new(); + instruction_data.push(0u8); // Zero signatures (1 byte, not 2) + instruction_data.push(0u8); // Padding + + let test_pubkey = [0x02; 33]; + let test_message_hash = [0xAB; 32]; + + let result = + verify_secp256r1_instruction_data(&instruction_data, &test_pubkey, &test_message_hash); + assert!( + result.is_err(), + "Verification should fail with zero signatures" + ); + assert_eq!( + result.unwrap_err(), + SwigAuthenticateError::PermissionDeniedSecp256r1InvalidInstruction.into() + ); + } + + #[test] + fn test_verify_secp256r1_instruction_data_cross_instruction_reference() { + let mut instruction_data = Vec::new(); + + // Number of signature sets (1 byte) and padding (1 byte) + instruction_data.push(1u8); // Number of signature sets + instruction_data.push(0u8); // Padding + + // Signature offsets with cross-instruction reference + instruction_data.extend_from_slice(&16u16.to_le_bytes()); // signature_offset + instruction_data.extend_from_slice(&1u16.to_le_bytes()); // signature_instruction_index (different instruction) + instruction_data.extend_from_slice(&80u16.to_le_bytes()); // public_key_offset + instruction_data.extend_from_slice(&0u16.to_le_bytes()); // public_key_instruction_index + instruction_data.extend_from_slice(&113u16.to_le_bytes()); // message_data_offset + instruction_data.extend_from_slice(&32u16.to_le_bytes()); // message_data_size + instruction_data.extend_from_slice(&0u16.to_le_bytes()); // message_instruction_index + + let test_pubkey = [0x02; 33]; + let test_message_hash = [0xAB; 32]; + + let result = + verify_secp256r1_instruction_data(&instruction_data, &test_pubkey, &test_message_hash); + assert!( + result.is_err(), + "Verification should fail with cross-instruction reference" + ); + assert_eq!( + result.unwrap_err(), + SwigAuthenticateError::PermissionDeniedSecp256r1InvalidInstruction.into() + ); + } + + #[test] + fn test_verify_secp256r1_with_real_crypto() { + // Create a test message 32 bytes + let test_message = b"Hello, secp256r1 world! dddddddd"; + + // Generate real cryptographic signature and pubkey using OpenSSL + let (signature_bytes, pubkey_bytes) = create_test_signature_and_pubkey(test_message); + + // Create instruction data using the official Solana function + let instruction_data = + create_test_secp256r1_instruction_data(test_message, &signature_bytes, &pubkey_bytes); + + // Should succeed with real cryptographic data + let result = + verify_secp256r1_instruction_data(&instruction_data, &pubkey_bytes, test_message); + assert!( + result.is_ok(), + "Verification should succeed with real cryptographic data" + ); + + // Should fail with wrong message + let wrong_message = b"Different message"; + let result = + verify_secp256r1_instruction_data(&instruction_data, &pubkey_bytes, wrong_message); + assert!( + result.is_err(), + "Verification should fail with wrong message" + ); + + // Should fail with wrong public key + let wrong_pubkey = [0xFF; 33]; + let result = + verify_secp256r1_instruction_data(&instruction_data, &wrong_pubkey, test_message); + assert!( + result.is_err(), + "Verification should fail with wrong public key" + ); + } +} diff --git a/state-x/src/lib.rs b/state-x/src/lib.rs index 5f2b16d0..00483a22 100644 --- a/state-x/src/lib.rs +++ b/state-x/src/lib.rs @@ -97,6 +97,14 @@ pub enum SwigStateError { RoleNotFound, /// Error loading permissions PermissionLoadError, + /// Adding an authority requires at least one action + InvalidAuthorityMustHaveAtLeastOneAction, + /// Oracle not available for mint + InvalidOracleTokenMint, + /// Feed Id non hex char + FeedIdNonHexCharacter, + /// Feed id must be 32 bytes + FeedIdMustBe32Bytes, } /// Error types related to authentication operations. @@ -137,6 +145,8 @@ pub enum SwigAuthenticateError { PermissionDeniedSecp256k1SignatureReused, /// Invalid Secp256k1 hash PermissionDeniedSecp256k1InvalidHash, + /// Secp256r1 signature has been reused + PermissionDeniedSecp256r1SignatureReused, /// Stake account is in an invalid state PermissionDeniedStakeAccountInvalidState, /// Cannot reuse session key @@ -145,6 +155,18 @@ pub enum SwigAuthenticateError { InvalidSessionDuration, /// Token account authority is not the Swig account PermissionDeniedTokenAccountAuthorityNotSwig, + /// Invalid Secp256r1 instruction + PermissionDeniedSecp256r1InvalidInstruction, + /// Invalid Secp256r1 public key + PermissionDeniedSecp256r1InvalidPubkey, + /// Invalid Secp256r1 message hash + PermissionDeniedSecp256r1InvalidMessageHash, + /// Invalid Secp256r1 message + PermissionDeniedSecp256r1InvalidMessage, + /// Missing oracle account + PermissionDeniedOracleLimitReached, + /// Invalid oracle price data + InvalidOraclePriceData, } impl From for ProgramError { diff --git a/state-x/src/swig.rs b/state-x/src/swig.rs index 311e67e8..a0b73326 100644 --- a/state-x/src/swig.rs +++ b/state-x/src/swig.rs @@ -15,6 +15,7 @@ use crate::{ authority::{ ed25519::{ED25519Authority, Ed25519SessionAuthority}, secp256k1::{Secp256k1Authority, Secp256k1SessionAuthority}, + secp256r1::{Secp256r1Authority, Secp256r1SessionAuthority}, Authority, AuthorityInfo, AuthorityType, }, role::{Position, Role, RoleMut}, @@ -226,14 +227,68 @@ impl<'a> SwigBuilder<'a> { Err(SwigStateError::RoleNotFound.into()) } + /// Calculates the actual number of actions in the provided actions data. + /// + /// This function iterates through the actions data and counts the number of + /// valid actions by parsing action headers and their boundaries. + /// + /// # Arguments + /// * `actions_data` - Raw bytes containing action data + /// + /// # Returns + /// * `Result` - The number of actions found, or error if invalid data + fn calculate_num_actions(actions_data: &[u8]) -> Result { + let mut cursor = 0; + let mut count = 0u8; + + while cursor < actions_data.len() { + if cursor + Action::LEN > actions_data.len() { + break; + } + + let action_header = + unsafe { Action::load_unchecked(&actions_data[cursor..cursor + Action::LEN])? }; + cursor += Action::LEN; + + let action_len = action_header.length() as usize; + if cursor + action_len > actions_data.len() { + return Err(SwigStateError::InvalidAuthorityMustHaveAtLeastOneAction.into()); + } + + cursor += action_len; + count += 1; + + // Prevent overflow + if count == u8::MAX { + return Err(ProgramError::InvalidInstructionData); + } + } + + if count == 0 { + return Err(SwigStateError::InvalidAuthorityMustHaveAtLeastOneAction.into()); + } + + Ok(count) + } + /// Adds a new role to the Swig account. + /// + /// # Arguments + /// * `authority_type` - The type of authority for this role + /// * `authority_data` - Raw bytes containing the authority data + /// * `actions_data` - Raw bytes containing the actions data + /// + /// # Returns + /// * `Result<(), ProgramError>` - Success or error status pub fn add_role( &mut self, authority_type: AuthorityType, authority_data: &[u8], - num_actions: u8, actions_data: &'a [u8], ) -> Result<(), ProgramError> { + // Calculate the actual number of actions from the actions data + let num_actions = Self::calculate_num_actions(actions_data)?; + // check number of roles and iterate to last boundary let mut cursor = 0; // iterate and transmute each position to get boundary if not the last then jump @@ -275,6 +330,21 @@ impl<'a> SwigBuilder<'a> { )?; Secp256k1SessionAuthority::LEN }, + AuthorityType::Secp256r1 => { + Secp256r1Authority::set_into_bytes( + authority_data, + &mut self.role_buffer[auth_offset..auth_offset + Secp256r1Authority::LEN], + )?; + Secp256r1Authority::LEN + }, + AuthorityType::Secp256r1Session => { + Secp256r1SessionAuthority::set_into_bytes( + authority_data, + &mut self.role_buffer + [auth_offset..auth_offset + Secp256r1SessionAuthority::LEN], + )?; + Secp256r1SessionAuthority::LEN + }, _ => return Err(SwigStateError::InvalidAuthorityData.into()), }; let size = authority_length + actions_data.len(); @@ -395,6 +465,12 @@ impl Swig { AuthorityType::Secp256k1Session => unsafe { Secp256k1SessionAuthority::load_mut_unchecked(authority)? }, + AuthorityType::Secp256r1 => unsafe { + Secp256r1Authority::load_mut_unchecked(authority)? + }, + AuthorityType::Secp256r1Session => unsafe { + Secp256r1SessionAuthority::load_mut_unchecked(authority)? + }, _ => return Err(ProgramError::InvalidAccountData), }; @@ -482,7 +558,16 @@ impl<'a> SwigWithRoles<'a> { self.roles.get_unchecked(offset..offset + auth_len), )? }, - + AuthorityType::Secp256r1 => unsafe { + Secp256r1Authority::load_unchecked( + self.roles.get_unchecked(offset..offset + auth_len), + )? + }, + AuthorityType::Secp256r1Session => unsafe { + Secp256r1SessionAuthority::load_unchecked( + self.roles.get_unchecked(offset..offset + auth_len), + )? + }, _ => return Err(ProgramError::InvalidAccountData), }; @@ -530,6 +615,16 @@ impl<'a> SwigWithRoles<'a> { offset..offset + position.authority_length() as usize, ))? }, + AuthorityType::Secp256r1 => unsafe { + Secp256r1Authority::load_unchecked(self.roles.get_unchecked( + offset..offset + position.authority_length() as usize, + ))? + }, + AuthorityType::Secp256r1Session => unsafe { + Secp256r1SessionAuthority::load_unchecked(self.roles.get_unchecked( + offset..offset + position.authority_length() as usize, + ))? + }, _ => return Err(ProgramError::InvalidAccountData), }; @@ -719,7 +814,6 @@ mod tests { .add_role( AuthorityType::Ed25519, authority.into_bytes().unwrap(), - 1, &actions_data, ) .unwrap(); @@ -768,7 +862,6 @@ mod tests { .add_role( AuthorityType::Ed25519, authority.into_bytes().unwrap(), - 1, &actions_data, ) .unwrap(); @@ -823,7 +916,6 @@ mod tests { .add_role( AuthorityType::Ed25519, authority1.into_bytes().unwrap(), - 1, &actions_data, ) .unwrap(); @@ -835,7 +927,6 @@ mod tests { .add_role( AuthorityType::Ed25519, authority2.into_bytes().unwrap(), - 1, &actions_data, ) .unwrap(); @@ -885,7 +976,6 @@ mod tests { .add_role( AuthorityType::Ed25519, authority.into_bytes().unwrap(), - 1, &actions_data, ) .unwrap(); @@ -1000,7 +1090,6 @@ mod tests { .add_role( AuthorityType::Ed25519, authority.into_bytes().unwrap(), - 2, &actions_data, ) .unwrap(); @@ -1141,7 +1230,6 @@ mod tests { .add_role( AuthorityType::Ed25519, authority1.into_bytes().unwrap(), - 1, &all_actions_data, ) .unwrap(); @@ -1151,7 +1239,6 @@ mod tests { .add_role( AuthorityType::Ed25519, authority2.into_bytes().unwrap(), - 1, &sol_limit_actions_data, ) .unwrap(); @@ -1203,7 +1290,6 @@ mod tests { .add_role( AuthorityType::Ed25519, authority1.into_bytes().unwrap(), - 1, &all_actions_data, ) .unwrap(); @@ -1211,7 +1297,6 @@ mod tests { .add_role( AuthorityType::Ed25519, authority1.into_bytes().unwrap(), - 1, &sol_limit_actions_data, ) .unwrap(); @@ -1267,7 +1352,6 @@ mod tests { .add_role( AuthorityType::Ed25519, authority1.into_bytes().unwrap(), - 1, &all_actions, ) .unwrap(); @@ -1275,7 +1359,6 @@ mod tests { .add_role( AuthorityType::Ed25519, authority2.into_bytes().unwrap(), - 1, &all_actions, ) .unwrap(); @@ -1404,7 +1487,6 @@ mod tests { .add_role( AuthorityType::Ed25519, authority.into_bytes().unwrap(), - 1, &actions_data, ) .unwrap(); @@ -1471,7 +1553,6 @@ mod tests { .add_role( AuthorityType::Ed25519, authority.into_bytes().unwrap(), - 1, &actions_data, ) .unwrap(); @@ -1533,7 +1614,6 @@ mod tests { .add_role( AuthorityType::Ed25519, authority1.into_bytes().unwrap(), - 1, &all_actions, ) .unwrap(); @@ -1541,7 +1621,6 @@ mod tests { .add_role( AuthorityType::Ed25519, authority2.into_bytes().unwrap(), - 1, &all_actions, ) .unwrap(); @@ -1666,12 +1745,11 @@ mod tests { .add_role( AuthorityType::Ed25519, authority1.into_bytes().unwrap(), - 1, &all_actions, ) .unwrap(); builder - .add_role(AuthorityType::Secp256k1, &authority2, 1, &ma_actions) + .add_role(AuthorityType::Secp256k1, &authority2, &ma_actions) .unwrap(); // Scan for assigned role IDs @@ -1791,12 +1869,11 @@ mod tests { .add_role( AuthorityType::Ed25519, authority1.into_bytes().unwrap(), - 1, &all_actions, ) .unwrap(); builder - .add_role(AuthorityType::Secp256k1, &authority2, 1, &all_actions) + .add_role(AuthorityType::Secp256k1, &authority2, &all_actions) .unwrap(); // Verify two roles exist