diff --git a/Cargo.lock b/Cargo.lock index 8b89db3c61..e4e1d3c69e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -206,7 +206,9 @@ dependencies = [ "chrono", "clap 4.5.17", "clap_complete", + "console 0.15.11", "dirs", + "ed25519-dalek", "flate2", "heck 0.4.1", "pathdiff", @@ -218,20 +220,29 @@ dependencies = [ "serde_json", "shellexpand", "solana-cli-config", + "solana-client", "solana-clock", "solana-commitment-config", "solana-compute-budget-interface", "solana-faucet", "solana-instruction", "solana-keypair", + "solana-loader-v3-interface", + "solana-message", + "solana-packet", "solana-pubkey 4.0.0", + "solana-pubsub-client", "solana-rpc-client", + "solana-rpc-client-api", + "solana-sdk-ids", "solana-signature", "solana-signer", "solana-system-interface", "solana-transaction", "syn 1.0.109", "tar", + "tempfile", + "tiny-bip39", "toml 0.7.8", "walkdir", ] @@ -462,6 +473,12 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "arrayref" version = "0.3.9" @@ -480,6 +497,45 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" +[[package]] +name = "asn1-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 1.0.66", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure 0.12.6", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "async-compression" version = "0.4.3" @@ -494,6 +550,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-lock" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -551,6 +618,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.7" @@ -589,6 +662,9 @@ name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] [[package]] name = "bitvec" @@ -804,6 +880,18 @@ name = "bytes" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +dependencies = [ + "serde", +] + +[[package]] +name = "caps" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd1ddba47aba30b6a889298ad0109c3b8dcb0e8fc993b459daa7067d46f865e0" +dependencies = [ + "libc", +] [[package]] name = "cargo_toml" @@ -826,6 +914,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.4" @@ -956,6 +1050,25 @@ dependencies = [ "unreachable", ] +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "console" version = "0.15.11" @@ -1014,11 +1127,21 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" @@ -1047,6 +1170,25 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.19" @@ -1178,6 +1320,29 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der-parser" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint 0.4.6", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + [[package]] name = "derivation-path" version = "0.2.0" @@ -1260,6 +1425,29 @@ dependencies = [ "syn 2.0.107", ] +[[package]] +name = "dlopen2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b4f5f101177ff01b8ec4ecc81eead416a8aa42819a2869311b3420fa114ffa" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cbae11b3de8fce2a456e8ea3dada226b35fe791f0dc1d360c0941f0bb681f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + [[package]] name = "eager" version = "0.1.0" @@ -1427,6 +1615,39 @@ dependencies = [ "libc", ] +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastbloom" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18c1ddb9231d8554c2d6bdf4cfaabf0c59251658c68b6c95cd52dd0c513a912a" +dependencies = [ + "getrandom 0.3.4", + "libm", + "rand 0.9.2", + "siphasher", +] + [[package]] name = "fastrand" version = "2.0.1" @@ -1603,6 +1824,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.31" @@ -1662,9 +1889,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasip2", + "wasm-bindgen", ] [[package]] @@ -1673,6 +1902,26 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "governor" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a7f542ee6b35af73b06abc0dad1c1bae89964e4e253bc4b587b91c9637867b" +dependencies = [ + "cfg-if", + "dashmap", + "futures", + "futures-timer", + "no-std-compat", + "nonzero_ext", + "parking_lot", + "portable-atomic", + "quanta", + "rand 0.8.5", + "smallvec", + "spinning_top", +] + [[package]] name = "group" version = "0.13.0" @@ -1737,9 +1986,9 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.15.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" @@ -1777,6 +2026,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "histogram" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cb882ccb290b8646e554b157ab0b71e64e8d5bef775cd66b6531e52d302669" + [[package]] name = "hmac" version = "0.12.1" @@ -1928,7 +2183,7 @@ dependencies = [ "http 1.3.1", "hyper 1.7.0", "hyper-util", - "rustls 0.23.23", + "rustls 0.23.35", "rustls-pki-types", "tokio", "tokio-rustls 0.26.2", @@ -2108,12 +2363,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.6.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.15.0", + "hashbrown 0.16.1", ] [[package]] @@ -2199,6 +2454,28 @@ dependencies = [ "syn 2.0.107", ] +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine 4.6.7", + "jni-sys", + "log", + "thiserror 1.0.66", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "jobserver" version = "0.1.32" @@ -2283,6 +2560,12 @@ version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + [[package]] name = "linux-raw-sys" version = "0.4.8" @@ -2311,6 +2594,12 @@ version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "memchr" version = "2.6.4" @@ -2354,6 +2643,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -2402,6 +2697,28 @@ dependencies = [ "memoffset", ] +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + [[package]] name = "num" version = "0.2.1" @@ -2447,6 +2764,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-derive" version = "0.3.3" @@ -2542,6 +2865,15 @@ dependencies = [ "syn 2.0.107", ] +[[package]] +name = "oid-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.20.2" @@ -2554,6 +2886,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + [[package]] name = "pairing" version = "0.23.0" @@ -2563,6 +2901,12 @@ dependencies = [ "group", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.1" @@ -2611,6 +2955,15 @@ dependencies = [ "hmac", ] +[[package]] +name = "pem" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +dependencies = [ + "base64 0.13.1", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2699,6 +3052,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -2747,39 +3106,62 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "quanta" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + [[package]] name = "quinn" -version = "0.11.5" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", + "cfg_aliases", "pin-project-lite", "quinn-proto", "quinn-udp", "rustc-hash 2.0.0", - "rustls 0.23.23", - "socket2 0.5.7", - "thiserror 1.0.66", + "rustls 0.23.35", + "socket2 0.6.1", + "thiserror 2.0.17", "tokio", "tracing", + "web-time", ] [[package]] name = "quinn-proto" -version = "0.11.8" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes", - "rand 0.8.5", + "fastbloom", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", "ring 0.17.8", "rustc-hash 2.0.0", - "rustls 0.23.23", + "rustls 0.23.35", + "rustls-pki-types", + "rustls-platform-verifier", "slab", - "thiserror 1.0.66", + "thiserror 2.0.17", "tinyvec", "tracing", + "web-time", ] [[package]] @@ -2885,6 +3267,35 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -3008,7 +3419,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.23", + "rustls 0.23.35", "rustls-pki-types", "serde", "serde_json", @@ -3130,6 +3541,15 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" version = "0.38.15" @@ -3157,18 +3577,30 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.23" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "once_cell", "ring 0.17.8", "rustls-pki-types", - "rustls-webpki 0.102.8", + "rustls-webpki 0.103.8", "subtle", "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "1.0.3" @@ -3180,9 +3612,40 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls 0.23.35", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki 0.103.8", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" @@ -3196,9 +3659,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.8" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "ring 0.17.8", "rustls-pki-types", @@ -3226,6 +3689,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -3256,6 +3728,29 @@ dependencies = [ "zeroize", ] +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.27" @@ -3372,7 +3867,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.6.0", + "indexmap 2.12.1", "itoa", "ryu", "serde", @@ -3467,6 +3962,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.9" @@ -3478,9 +3979,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" @@ -3806,6 +4307,75 @@ dependencies = [ "spl-memo-interface", ] +[[package]] +name = "solana-client" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf0eb4758181553a716c054a655e4dd61fa782bf98671b570b346b1a93d12e2" +dependencies = [ + "async-trait", + "bincode", + "dashmap", + "futures", + "futures-util", + "indexmap 2.12.1", + "indicatif", + "log", + "quinn", + "rayon", + "solana-account", + "solana-client-traits", + "solana-commitment-config", + "solana-connection-cache", + "solana-epoch-info", + "solana-hash", + "solana-instruction", + "solana-keypair", + "solana-measure", + "solana-message", + "solana-net-utils", + "solana-pubkey 3.0.0", + "solana-pubsub-client", + "solana-quic-client", + "solana-quic-definitions", + "solana-rpc-client", + "solana-rpc-client-api", + "solana-rpc-client-nonce-utils", + "solana-signature", + "solana-signer", + "solana-streamer", + "solana-time-utils", + "solana-tpu-client", + "solana-transaction", + "solana-transaction-error", + "solana-transaction-status-client-types", + "solana-udp-client", + "thiserror 2.0.17", + "tokio", + "tokio-util", +] + +[[package]] +name = "solana-client-traits" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08618ed587e128105510c54ae3e456b9a06d674d8640db75afe66dad65cb4e02" +dependencies = [ + "solana-account", + "solana-commitment-config", + "solana-epoch-info", + "solana-hash", + "solana-instruction", + "solana-keypair", + "solana-message", + "solana-pubkey 3.0.0", + "solana-signature", + "solana-signer", + "solana-system-interface", + "solana-transaction", + "solana-transaction-error", +] + [[package]] name = "solana-clock" version = "3.0.0" @@ -3865,6 +4435,29 @@ dependencies = [ "solana-system-interface", ] +[[package]] +name = "solana-connection-cache" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bfbc84abd020a0c4f075d11b0ba61f0e570631c3150df33477e364d53603d45" +dependencies = [ + "async-trait", + "bincode", + "crossbeam-channel", + "futures-util", + "indexmap 2.12.1", + "log", + "rand 0.8.5", + "rayon", + "solana-keypair", + "solana-measure", + "solana-metrics", + "solana-time-utils", + "solana-transaction-error", + "thiserror 2.0.17", + "tokio", +] + [[package]] name = "solana-cpi" version = "3.0.0" @@ -4198,6 +4791,12 @@ dependencies = [ "solana-system-interface", ] +[[package]] +name = "solana-measure" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "314b1ce76798ebe250b0587fbd4cd6f7557fabd61f0acc78cd91063e13938e3b" + [[package]] name = "solana-message" version = "3.0.1" @@ -4307,7 +4906,45 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6edf2f25743c95229ac0fdc32f8f5893ef738dbf332c669e9861d33ddb0f469d" dependencies = [ + "bincode", "bitflags 2.10.0", + "cfg_eval", + "serde", + "serde_derive", + "serde_with", +] + +[[package]] +name = "solana-perf" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec5d863ecde764f82d71411178e2d5247b5c684af8986a023ff3c9fda079063" +dependencies = [ + "ahash", + "bincode", + "bv", + "bytes", + "caps", + "curve25519-dalek", + "dlopen2", + "fnv", + "libc", + "log", + "nix", + "rand 0.8.5", + "rayon", + "serde", + "solana-hash", + "solana-message", + "solana-metrics", + "solana-packet", + "solana-pubkey 3.0.0", + "solana-rayon-threadlimit", + "solana-sdk-ids", + "solana-short-vec", + "solana-signature", + "solana-time-utils", + "solana-transaction-context", ] [[package]] @@ -4515,6 +5152,55 @@ dependencies = [ "url", ] +[[package]] +name = "solana-quic-client" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3c5ae8583a155ee390ce8907c6bdcd7a81b1b390d80c65c2dc2f78c6adcd4ad" +dependencies = [ + "async-lock", + "async-trait", + "futures", + "itertools", + "log", + "quinn", + "quinn-proto", + "rustls 0.23.35", + "solana-connection-cache", + "solana-keypair", + "solana-measure", + "solana-metrics", + "solana-net-utils", + "solana-pubkey 3.0.0", + "solana-quic-definitions", + "solana-rpc-client-api", + "solana-signer", + "solana-streamer", + "solana-tls-utils", + "solana-transaction-error", + "thiserror 2.0.17", + "tokio", +] + +[[package]] +name = "solana-quic-definitions" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15319accf7d3afd845817aeffa6edd8cc185f135cefbc6b985df29cfd8c09609" +dependencies = [ + "solana-keypair", +] + +[[package]] +name = "solana-rayon-threadlimit" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84cc3d9f64e36b38d2ad214a832cbf575b702f924a25a72986bffb5247627122" +dependencies = [ + "log", + "num_cpus", +] + [[package]] name = "solana-remote-wallet" version = "3.1.2" @@ -4622,6 +5308,23 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "solana-rpc-client-nonce-utils" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55a6c6578e34c6c5d0f5e981d1ab4cafe3e584c7fe6210a4fb9abc65cab1bdc3" +dependencies = [ + "solana-account", + "solana-commitment-config", + "solana-hash", + "solana-message", + "solana-nonce", + "solana-pubkey 3.0.0", + "solana-rpc-client", + "solana-sdk-ids", + "thiserror 2.0.17", +] + [[package]] name = "solana-rpc-client-types" version = "3.1.2" @@ -4662,7 +5365,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15b079e08471a9dbfe1e48b2c7439c85aa2a055cbd54eddd8bd257b0a7dbb29" dependencies = [ "byteorder", - "combine", + "combine 3.8.1", "hash32", "libc", "log", @@ -4854,6 +5557,54 @@ dependencies = [ "solana-sysvar-id", ] +[[package]] +name = "solana-streamer" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b3f294e632713839c61135dd402c9d073cc8397141e96dc2c359ecda75db7a0" +dependencies = [ + "arc-swap", + "bytes", + "crossbeam-channel", + "dashmap", + "futures", + "futures-util", + "governor", + "histogram", + "indexmap 2.12.1", + "itertools", + "libc", + "log", + "nix", + "num_cpus", + "pem", + "percentage", + "quinn", + "quinn-proto", + "rand 0.8.5", + "rustls 0.23.35", + "smallvec", + "socket2 0.6.1", + "solana-keypair", + "solana-measure", + "solana-metrics", + "solana-net-utils", + "solana-packet", + "solana-perf", + "solana-pubkey 3.0.0", + "solana-quic-definitions", + "solana-signature", + "solana-signer", + "solana-time-utils", + "solana-tls-utils", + "solana-transaction-error", + "solana-transaction-metrics-tracker", + "thiserror 2.0.17", + "tokio", + "tokio-util", + "x509-parser", +] + [[package]] name = "solana-svm-callback" version = "3.1.2" @@ -5001,6 +5752,53 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ced92c60aa76ec4780a9d93f3bd64dfa916e1b998eacc6f1c110f3f444f02c9" +[[package]] +name = "solana-tls-utils" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade309227e0bafaf81abef2f95efaaa68c32a0bac31f64fe953393b87a7a000d" +dependencies = [ + "rustls 0.23.35", + "solana-keypair", + "solana-pubkey 3.0.0", + "solana-signer", + "x509-parser", +] + +[[package]] +name = "solana-tpu-client" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56338e5f3b2a1a50548ffe54ef592fdc1e4224cef98f41b0033ffa26713329d5" +dependencies = [ + "async-trait", + "bincode", + "futures-util", + "indexmap 2.12.1", + "indicatif", + "log", + "rayon", + "solana-client-traits", + "solana-clock", + "solana-commitment-config", + "solana-connection-cache", + "solana-epoch-schedule", + "solana-measure", + "solana-message", + "solana-net-utils", + "solana-pubkey 3.0.0", + "solana-pubsub-client", + "solana-quic-definitions", + "solana-rpc-client", + "solana-rpc-client-api", + "solana-signature", + "solana-signer", + "solana-transaction", + "solana-transaction-error", + "thiserror 2.0.17", + "tokio", +] + [[package]] name = "solana-transaction" version = "3.0.1" @@ -5052,6 +5850,22 @@ dependencies = [ "solana-sanitize", ] +[[package]] +name = "solana-transaction-metrics-tracker" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d69375554c6dbe0458ceae1adef20ba16d6b07e223a43def364aeca06601f8e" +dependencies = [ + "base64 0.22.1", + "bincode", + "log", + "rand 0.8.5", + "solana-packet", + "solana-perf", + "solana-short-vec", + "solana-signature", +] + [[package]] name = "solana-transaction-status" version = "3.1.2" @@ -5119,6 +5933,22 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "solana-udp-client" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dc2dcf4c824afde36a2ce841f3b682fe9e611893898084f51324a003463ede2" +dependencies = [ + "async-trait", + "solana-connection-cache", + "solana-keypair", + "solana-net-utils", + "solana-streamer", + "solana-transaction-error", + "thiserror 2.0.17", + "tokio", +] + [[package]] name = "solana-version" version = "3.1.2" @@ -5240,6 +6070,15 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "spinning_top" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" +dependencies = [ + "lock_api", +] + [[package]] name = "spki" version = "0.7.3" @@ -5531,6 +6370,18 @@ dependencies = [ "futures-core", ] +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -5549,7 +6400,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.3", "system-configuration-sys", ] @@ -5651,6 +6502,37 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tiny-bip39" version = "2.0.0" @@ -5737,7 +6619,7 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls 0.23.23", + "rustls 0.23.35", "tokio", ] @@ -5760,7 +6642,7 @@ checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" dependencies = [ "futures-util", "log", - "rustls 0.23.23", + "rustls 0.23.35", "rustls-pki-types", "tokio", "tokio-rustls 0.26.2", @@ -5770,16 +6652,16 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.9" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", "futures-sink", + "futures-util", "pin-project-lite", "tokio", - "tracing", ] [[package]] @@ -5830,7 +6712,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.6.0", + "indexmap 2.12.1", "serde", "serde_spanned", "toml_datetime", @@ -5843,7 +6725,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ - "indexmap 2.6.0", + "indexmap 2.12.1", "serde", "serde_spanned", "toml_datetime", @@ -5856,7 +6738,7 @@ version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" dependencies = [ - "indexmap 2.6.0", + "indexmap 2.12.1", "toml_datetime", "winnow", ] @@ -5913,6 +6795,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", + "log", "pin-project-lite", "tracing-core", ] @@ -5944,7 +6827,7 @@ dependencies = [ "httparse", "log", "rand 0.9.2", - "rustls 0.23.23", + "rustls 0.23.35", "rustls-pki-types", "sha1", "thiserror 2.0.17", @@ -6000,6 +6883,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unit-prefix" version = "0.5.2" @@ -6226,6 +7115,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee3e3b5f5e80bc89f30ce8d0343bf4e5f12341c51f3e26cbeecbc7c85443e85b" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "0.25.2" @@ -6296,6 +7194,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -6341,6 +7248,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -6389,6 +7311,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -6407,6 +7335,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -6425,6 +7359,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -6455,6 +7395,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -6473,6 +7419,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -6491,6 +7443,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -6509,6 +7467,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -6567,6 +7531,24 @@ dependencies = [ "tap", ] +[[package]] +name = "x509-parser" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0ecbeb7b67ce215e40e3cc7f2ff902f94a223acf44995934763467e7b1febc8" +dependencies = [ + "asn1-rs", + "base64 0.13.1", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 1.0.66", + "time", +] + [[package]] name = "xattr" version = "1.0.1" @@ -6597,7 +7579,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.107", - "synstructure", + "synstructure 0.13.2", ] [[package]] @@ -6638,7 +7620,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.107", - "synstructure", + "synstructure 0.13.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 890ecff98b..e7f7eea0df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,9 @@ solana-instructions-sysvar = "3.0.0" solana-invoke = "0.5.0" solana-keypair = "3.0.0" solana-loader-v3-interface = "6.0.0" +solana-message = "3.0.0" solana-msg = "3.0.0" +solana-packet = "3.0.0" solana-program = "3.0.0" solana-program-entrypoint = "3.0.0" solana-program-error = "3.0.0" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index fd24d10684..e4f5919efd 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -25,7 +25,9 @@ cargo_toml = "0.19.2" chrono = "0.4.19" clap = { version = "4.5.17", features = ["derive"] } clap_complete = "4.5.26" +console = "0.15" dirs = "4.0" +ed25519-dalek = "2" flate2 = "1.0.19" heck = "0.4.0" pathdiff = "0.2.0" @@ -37,19 +39,28 @@ serde = { version = "1.0.130", features = ["derive"] } serde_json = "1.0" shellexpand = "2.1.0" solana-cli-config.workspace = true +solana-client = "3.0.0" solana-clock.workspace = true solana-commitment-config.workspace = true solana-compute-budget-interface.workspace = true solana-faucet = { workspace = true, features = ["agave-unstable-api"] } solana-instruction.workspace = true solana-keypair.workspace = true +solana-loader-v3-interface.workspace = true +solana-message.workspace = true +solana-packet.workspace = true solana-pubkey.workspace = true +solana-sdk-ids.workspace = true solana-signature.workspace = true solana-signer.workspace = true solana-system-interface.workspace = true solana-transaction.workspace = true solana-rpc-client.workspace = true +solana-rpc-client-api.workspace = true +solana-pubsub-client.workspace = true syn = { version = "1.0.60", features = ["full", "extra-traits"] } tar = "0.4.35" +tempfile = "3" +tiny-bip39 = "2.0" toml = "0.7.6" walkdir = "2.3.2" diff --git a/cli/src/account.rs b/cli/src/account.rs new file mode 100644 index 0000000000..b50b5d6dea --- /dev/null +++ b/cli/src/account.rs @@ -0,0 +1,153 @@ +use anyhow::{anyhow, Result}; +use clap::Parser; +use solana_commitment_config::CommitmentConfig; +use solana_pubkey::Pubkey; +use solana_rpc_client::rpc_client::RpcClient; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; + +use crate::config::{Config, ConfigOverride}; + +#[derive(Debug, Parser)] +pub struct ShowAccountCommand { + /// Account address to show + pub account_address: Pubkey, + /// Display balance in lamports instead of SOL + #[clap(long)] + pub lamports: bool, + /// Write the account data to this file + #[clap(short = 'o', long)] + pub output_file: Option, + /// Return information in specified output format + #[clap(long, value_parser = ["json", "json-compact"])] + pub output: Option, +} + +pub fn show_account(cfg_override: &ConfigOverride, cmd: ShowAccountCommand) -> Result<()> { + let config = Config::discover(cfg_override)?; + let url = match config { + Some(ref cfg) => cfg.provider.cluster.url().to_string(), + None => { + // If not in workspace, use cluster override or default to localhost + if let Some(ref cluster) = cfg_override.cluster { + cluster.url().to_string() + } else { + "https://api.mainnet-beta.solana.com".to_string() + } + } + }; + + let rpc_client = RpcClient::new_with_commitment(url, CommitmentConfig::confirmed()); + + // Fetch the account + let account = rpc_client + .get_account(&cmd.account_address) + .map_err(|e| anyhow!("Unable to fetch account {}: {}", cmd.account_address, e))?; + + // Handle JSON output + if let Some(format) = cmd.output { + use base64::{engine::general_purpose::STANDARD, Engine}; + + let json_output = serde_json::json!({ + "pubkey": cmd.account_address.to_string(), + "account": { + "lamports": account.lamports, + "owner": account.owner.to_string(), + "executable": account.executable, + "rentEpoch": account.rent_epoch, + "data": STANDARD.encode(&account.data), + } + }); + + let output_str = match format.as_str() { + "json" => serde_json::to_string_pretty(&json_output)?, + "json-compact" => serde_json::to_string(&json_output)?, + _ => unreachable!(), + }; + + if let Some(output_file) = cmd.output_file { + let mut file = File::create(&output_file)?; + file.write_all(output_str.as_bytes())?; + println!("Wrote account to {}", output_file.display()); + } else { + println!("{}", output_str); + } + + return Ok(()); + } + + // Text output + println!("Public Key: {}", cmd.account_address); + + if cmd.lamports { + println!("Balance: {} lamports", account.lamports); + } else { + println!("Balance: {} SOL", account.lamports as f64 / 1_000_000_000.0); + } + + println!("Owner: {}", account.owner); + println!("Executable: {}", account.executable); + println!("Rent Epoch: {}", account.rent_epoch); + + // Display account data + let data_len = account.data.len(); + println!("Length: {} (0x{:x}) bytes", data_len, data_len); + + if !account.data.is_empty() { + // Write to output file if specified + if let Some(output_file) = cmd.output_file { + let mut file = File::create(&output_file)?; + file.write_all(&account.data)?; + println!("Wrote account data to {}", output_file.display()); + } + + // Display hex dump + print_hex_dump(&account.data); + } + + Ok(()) +} + +fn print_hex_dump(data: &[u8]) { + const BYTES_PER_LINE: usize = 16; + + for (i, chunk) in data.chunks(BYTES_PER_LINE).enumerate() { + let offset = i * BYTES_PER_LINE; + + // Print offset + print!("{:04x}: ", offset); + + // Print hex values + for (j, byte) in chunk.iter().enumerate() { + if j > 0 && j % 4 == 0 { + print!(" "); + } + print!("{:02x} ", byte); + } + + // Pad if this is the last line and it's not complete + if chunk.len() < BYTES_PER_LINE { + for j in chunk.len()..BYTES_PER_LINE { + if j > 0 && j % 4 == 0 { + print!(" "); + } + print!(" "); + } + } + + print!(" "); + + // Print ASCII representation + for byte in chunk { + let c = *byte as char; + if c.is_ascii_graphic() || c == ' ' { + print!("{}", c); + } else { + print!("."); + } + } + + println!(); + } +} diff --git a/cli/src/config.rs b/cli/src/config.rs index 554d96dba0..731d72f061 100644 --- a/cli/src/config.rs +++ b/cli/src/config.rs @@ -11,6 +11,7 @@ use serde::ser::SerializeMap; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use solana_cli_config::{Config as SolanaConfig, CONFIG_FILE}; use solana_clock::Slot; +use solana_commitment_config::CommitmentLevel; use solana_keypair::Keypair; use solana_pubkey::Pubkey; use solana_signer::Signer; @@ -27,6 +28,32 @@ use std::str::FromStr; use std::{fmt, io}; use walkdir::WalkDir; +/// Wrapper around CommitmentLevel to support case-insensitive parsing +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CaseInsensitiveCommitmentLevel(pub CommitmentLevel); + +impl FromStr for CaseInsensitiveCommitmentLevel { + type Err = String; + + fn from_str(s: &str) -> Result { + // Convert to lowercase for case-insensitive matching + let lowercase = s.to_lowercase(); + let commitment = CommitmentLevel::from_str(&lowercase).map_err(|_| { + format!( + "Invalid commitment level '{}'. Valid values are: processed, confirmed, finalized", + s + ) + })?; + Ok(CaseInsensitiveCommitmentLevel(commitment)) + } +} + +impl From for CommitmentLevel { + fn from(val: CaseInsensitiveCommitmentLevel) -> Self { + val.0 + } +} + pub trait Merge: Sized { fn merge(&mut self, _other: Self) {} } @@ -39,6 +66,9 @@ pub struct ConfigOverride { /// Wallet override. #[clap(global = true, long = "provider.wallet")] pub wallet: Option, + /// Commitment override (valid values: processed, confirmed, finalized). + #[clap(global = true, long = "commitment")] + pub commitment: Option, } #[derive(Debug)] diff --git a/cli/src/keygen.rs b/cli/src/keygen.rs new file mode 100644 index 0000000000..46db4b50ee --- /dev/null +++ b/cli/src/keygen.rs @@ -0,0 +1,517 @@ +use std::{ + fs, + io::{self, Write}, + path::Path, +}; + +use anyhow::{anyhow, bail, Result}; +use bip39::{Language, Mnemonic, MnemonicType, Seed}; +use console::{Key, Term}; +use dirs::home_dir; +use solana_instruction::{AccountMeta, Instruction}; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::{EncodableKey, Signer}; +use solana_transaction::Message; + +use crate::{config::ConfigOverride, get_keypair, KeygenCommand}; + +/// Secure password input with asterisk visual feedback +/// - show_spaces: if true, spaces are visible (for seed phrases); if false, all characters are asterisks (for passphrases) +fn secure_input(prompt: &str, show_spaces: bool) -> Result { + print!("{}", prompt); + io::stdout().flush()?; + + let term = Term::stdout(); + let mut input = String::new(); + + loop { + let key = term.read_key()?; + match key { + Key::Enter => { + println!(); + break; + } + Key::Backspace => { + if !input.is_empty() { + input.pop(); + // Move cursor back, print space, move cursor back again + print!("\x08 \x08"); + io::stdout().flush()?; + } + } + Key::Char(c) => { + input.push(c); + // Display spaces as spaces if show_spaces is true, otherwise all asterisks + if show_spaces && c == ' ' { + print!(" "); + } else { + print!("*"); + } + io::stdout().flush()?; + } + Key::Escape => { + println!(); + bail!("Input cancelled"); + } + _ => {} + } + } + + Ok(input) +} + +/// Print a progress step with checkmark +fn print_step(step: &str) { + println!("āœ“ {}", step); +} + +pub fn keygen(_cfg_override: &ConfigOverride, cmd: KeygenCommand) -> Result<()> { + match cmd { + KeygenCommand::New { + outfile, + force, + no_passphrase, + silent, + word_count, + } => keygen_new(outfile, force, no_passphrase, silent, word_count), + KeygenCommand::Pubkey { keypair } => keygen_pubkey(keypair), + KeygenCommand::Recover { + outfile, + force, + skip_seed_phrase_validation, + no_passphrase, + } => keygen_recover(outfile, force, skip_seed_phrase_validation, no_passphrase), + KeygenCommand::Verify { pubkey, keypair } => keygen_verify(pubkey, keypair), + } +} + +fn keygen_new( + outfile: Option, + force: bool, + no_passphrase: bool, + silent: bool, + word_count: usize, +) -> Result<()> { + // Determine output file path + let outfile_path = outfile.unwrap_or_else(|| { + let mut path = home_dir().expect("home directory"); + path.push(".config"); + path.push("solana"); + path.push("id.json"); + path.to_str().unwrap().to_string() + }); + + // Check for overwrite + if Path::new(&outfile_path).exists() { + if !force { + bail!( + "Refusing to overwrite {} without --force flag", + outfile_path + ); + } + println!( + "āš ļø Warning: Overwriting existing keypair at {}", + outfile_path + ); + } + + println!("\nšŸ”‘ Generating a new keypair"); + println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + + // Convert word count to MnemonicType + let mnemonic_type = match word_count { + 12 => MnemonicType::Words12, + 15 => MnemonicType::Words15, + 18 => MnemonicType::Words18, + 21 => MnemonicType::Words21, + 24 => MnemonicType::Words24, + _ => bail!( + "Invalid word count: {}. Must be 12, 15, 18, 21, or 24", + word_count + ), + }; + + // Generate mnemonic with specified word count + print_step(&format!("Generating {}-word mnemonic", word_count)); + let mnemonic = Mnemonic::new(mnemonic_type, Language::English); + + // Get passphrase + let passphrase = if no_passphrase { + print_step("No passphrase required"); + String::new() + } else { + println!("\nšŸ” BIP39 Passphrase (optional)"); + let pass = secure_input("Enter BIP39 passphrase (leave empty for none): ", false)?; + if !pass.is_empty() { + print_step("Passphrase set"); + } + pass + }; + + // Generate seed from mnemonic and passphrase + print_step("Deriving keypair from seed"); + let seed = Seed::new(&mnemonic, &passphrase); + + // Create keypair from seed (use first 32 bytes as secret key) + // Ed25519 keypair derivation: use the first 32 bytes of the seed as the secret key + let secret_key_bytes: [u8; 32] = seed.as_bytes()[0..32].try_into().unwrap(); + let keypair = Keypair::new_from_array(secret_key_bytes); + + // Write keypair to file + if let Some(outdir) = Path::new(&outfile_path).parent() { + fs::create_dir_all(outdir)?; + } + keypair + .write_to_file(&outfile_path) + .map_err(|e| anyhow!("Failed to write keypair to {}: {}", outfile_path, e))?; + + // Set restrictive permissions (owner read/write only) + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&outfile_path)?.permissions(); + perms.set_mode(0o600); + fs::set_permissions(&outfile_path, perms)?; + } + + print_step(&format!("Keypair saved to {}", outfile_path)); + + let phrase: &str = mnemonic.phrase(); + let divider = "━".repeat(phrase.len().max(60)); + let passphrase_msg = if passphrase.is_empty() { + String::new() + } else { + " and your BIP39 passphrase".to_string() + }; + + // Always show the seed phrase - it's critical for recovery + println!("\n{}", divider); + if !silent { + println!("šŸ“‹ Public Key: {}", keypair.pubkey()); + println!("{}", divider); + } + println!( + "\nāš ļø IMPORTANT: Save this seed phrase{} to recover your keypair:", + passphrase_msg + ); + println!("\n{}\n", phrase); + println!("{}", divider); + + Ok(()) +} + +fn keygen_pubkey(keypair_path: Option) -> Result<()> { + let path = keypair_path.unwrap_or_else(|| { + let mut p = home_dir().expect("home directory"); + p.push(".config"); + p.push("solana"); + p.push("id.json"); + p.to_str().unwrap().to_string() + }); + + let keypair = get_keypair(&path)?; + println!("{}", keypair.pubkey()); + Ok(()) +} + +fn keygen_recover( + outfile: Option, + force: bool, + _skip_seed_phrase_validation: bool, + no_passphrase: bool, +) -> Result<()> { + println!("\nšŸ”“ Recover keypair from seed phrase"); + println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + + // Determine output file path + let outfile_path = outfile.unwrap_or_else(|| { + let mut path = home_dir().expect("home directory"); + path.push(".config"); + path.push("solana"); + path.push("id.json"); + path.to_str().unwrap().to_string() + }); + + // Check for overwrite + if Path::new(&outfile_path).exists() { + if !force { + bail!( + "Refusing to overwrite {} without --force flag", + outfile_path + ); + } + println!( + "āš ļø Warning: Overwriting existing keypair at {}", + outfile_path + ); + } + + // Prompt for seed phrase (secure input with spaces visible) + println!("\n🌱 Enter Recovery Seed Phrase"); + let seed_phrase = secure_input("Seed phrase: ", true)?; + + // Parse mnemonic from seed phrase + let mnemonic = Mnemonic::from_phrase(&seed_phrase, Language::English) + .map_err(|e| anyhow!("Invalid seed phrase: {:?}", e))?; + print_step("Seed phrase validated"); + + // Get passphrase + let passphrase = if no_passphrase { + print_step("No passphrase required"); + String::new() + } else { + println!("\nšŸ” BIP39 Passphrase (optional)"); + let pass = secure_input("Passphrase (leave empty for none): ", false)?; + if !pass.is_empty() { + print_step("Passphrase accepted"); + } + pass + }; + + // Generate seed from mnemonic and passphrase + print_step("Deriving keypair from seed"); + let seed = Seed::new(&mnemonic, &passphrase); + + // Create keypair from seed (use first 32 bytes as secret key) + let secret_key_bytes: [u8; 32] = seed.as_bytes()[0..32].try_into().unwrap(); + let keypair = Keypair::new_from_array(secret_key_bytes); + + // Write keypair to file + if let Some(outdir) = Path::new(&outfile_path).parent() { + fs::create_dir_all(outdir)?; + } + keypair + .write_to_file(&outfile_path) + .map_err(|e| anyhow!("Failed to write keypair to {}: {}", outfile_path, e))?; + + // Set restrictive permissions (owner read/write only) + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&outfile_path)?.permissions(); + perms.set_mode(0o600); + fs::set_permissions(&outfile_path, perms)?; + } + + print_step(&format!("Keypair recovered to {}", outfile_path)); + + println!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + println!("šŸ“‹ Public Key: {}", keypair.pubkey()); + println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); + + Ok(()) +} + +fn keygen_verify(pubkey: Pubkey, keypair_path: Option) -> Result<()> { + println!("\nšŸ” Verifying keypair"); + println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + + let path = keypair_path.unwrap_or_else(|| { + let mut p = home_dir().expect("home directory"); + p.push(".config"); + p.push("solana"); + p.push("id.json"); + p.to_str().unwrap().to_string() + }); + + print_step(&format!("Loading keypair from {}", path)); + let keypair = get_keypair(&path)?; + + // Create a simple message to sign + print_step("Creating test message"); + let message = Message::new( + &[Instruction::new_with_bincode( + Pubkey::default(), + &0, + vec![AccountMeta::new(keypair.pubkey(), true)], + )], + Some(&keypair.pubkey()), + ); + + // Sign the message + print_step("Signing message with keypair"); + let signature = keypair.sign_message(message.serialize().as_slice()); + + // Verify the signature + print_step("Verifying signature"); + if signature.verify(pubkey.as_ref(), message.serialize().as_slice()) { + println!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + println!("āœ… Verification Success"); + println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + println!("Public key {} matches the keypair\n", pubkey); + Ok(()) + } else { + println!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + println!("āŒ Verification Failed"); + println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + bail!("Public key {} does not match the keypair", pubkey); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::{tempdir, TempDir}; + + fn tmp_outfile_path(out_dir: &TempDir, name: &str) -> String { + let path = out_dir.path().join(name); + path.into_os_string().into_string().unwrap() + } + + fn read_keypair_file(path: &str) -> Result { + get_keypair(path) + } + + #[test] + fn test_keygen_new() { + let outfile_dir = tempdir().unwrap(); + let outfile_path = tmp_outfile_path(&outfile_dir, "test-keypair.json"); + + // Test: successful keypair generation with default word count (12) + keygen_new(Some(outfile_path.clone()), false, true, true, 12).unwrap(); + + // Verify the keypair file was created + assert!(Path::new(&outfile_path).exists()); + + // Verify we can read the keypair back + let keypair = read_keypair_file(&outfile_path).unwrap(); + assert_ne!(keypair.pubkey(), Pubkey::default()); + + // Test: refuse to overwrite without --force + let result = keygen_new(Some(outfile_path.clone()), false, true, true, 12); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("Refusing to overwrite")); + + // Test: overwrite with --force flag + keygen_new(Some(outfile_path.clone()), true, true, true, 12).unwrap(); + assert!(Path::new(&outfile_path).exists()); + } + + #[test] + fn test_keygen_pubkey() { + let keypair_dir = tempdir().unwrap(); + let keypair_path = tmp_outfile_path(&keypair_dir, "test-keypair.json"); + + // Create a test keypair + let test_keypair = Keypair::new(); + test_keypair.write_to_file(&keypair_path).unwrap(); + + // Test: reading pubkey from file + let result = keygen_pubkey(Some(keypair_path)); + // Since keygen_pubkey prints to stdout, we just verify it doesn't error + assert!(result.is_ok()); + } + + #[test] + fn test_keygen_verify() { + let keypair_dir = tempdir().unwrap(); + let keypair_path = tmp_outfile_path(&keypair_dir, "test-keypair.json"); + + // Create a test keypair + let test_keypair = Keypair::new(); + test_keypair.write_to_file(&keypair_path).unwrap(); + let correct_pubkey = test_keypair.pubkey(); + + // Test: verify with correct pubkey + let result = keygen_verify(correct_pubkey, Some(keypair_path.clone())); + assert!(result.is_ok()); + + // Test: verify with incorrect pubkey + let incorrect_pubkey = Pubkey::new_unique(); + let result = keygen_verify(incorrect_pubkey, Some(keypair_path)); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains(&format!( + "Public key {} does not match the keypair", + incorrect_pubkey + ))); + } + + #[test] + fn test_keypair_from_seed_consistency() { + // Test that the same seed phrase produces the same keypair + let test_phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + + let mnemonic = Mnemonic::from_phrase(test_phrase, Language::English).unwrap(); + let seed1 = Seed::new(&mnemonic, ""); + let secret_key_bytes1: [u8; 32] = seed1.as_bytes()[0..32].try_into().unwrap(); + let keypair1 = Keypair::new_from_array(secret_key_bytes1); + + // Generate again with same phrase + let mnemonic2 = Mnemonic::from_phrase(test_phrase, Language::English).unwrap(); + let seed2 = Seed::new(&mnemonic2, ""); + let secret_key_bytes2: [u8; 32] = seed2.as_bytes()[0..32].try_into().unwrap(); + let keypair2 = Keypair::new_from_array(secret_key_bytes2); + + // Should produce the same pubkey + assert_eq!(keypair1.pubkey(), keypair2.pubkey()); + assert_eq!(keypair1.to_bytes(), keypair2.to_bytes()); + } + + #[test] + fn test_keypair_with_passphrase() { + // Test that different passphrases produce different keypairs + let test_phrase = + "park remain person kitchen mule spell knee armed position rail grid ankle"; + + let mnemonic = Mnemonic::from_phrase(test_phrase, Language::English).unwrap(); + + // Without passphrase + let seed_no_pass = Seed::new(&mnemonic, ""); + let secret_key_bytes_no_pass: [u8; 32] = seed_no_pass.as_bytes()[0..32].try_into().unwrap(); + let keypair_no_pass = Keypair::new_from_array(secret_key_bytes_no_pass); + + // With passphrase + let seed_with_pass = Seed::new(&mnemonic, "test_passphrase"); + let secret_key_bytes_with_pass: [u8; 32] = + seed_with_pass.as_bytes()[0..32].try_into().unwrap(); + let keypair_with_pass = Keypair::new_from_array(secret_key_bytes_with_pass); + + // Should produce different pubkeys + assert_ne!(keypair_no_pass.pubkey(), keypair_with_pass.pubkey()); + } + + #[test] + fn test_word_count_variations() { + // Test all supported word counts + let word_counts = [12, 15, 18, 21, 24]; + + for word_count in word_counts { + let outfile_dir = tempdir().unwrap(); + let outfile_path = + tmp_outfile_path(&outfile_dir, &format!("test-keypair-{}.json", word_count)); + + // Test: successful keypair generation with different word counts + let result = keygen_new(Some(outfile_path.clone()), false, true, true, word_count); + assert!( + result.is_ok(), + "Failed to generate keypair with {} words", + word_count + ); + + // Verify the keypair file was created + assert!(Path::new(&outfile_path).exists()); + + // Verify we can read the keypair back + let keypair = read_keypair_file(&outfile_path).unwrap(); + assert_ne!(keypair.pubkey(), Pubkey::default()); + } + } + + #[test] + fn test_invalid_word_count() { + let outfile_dir = tempdir().unwrap(); + let outfile_path = tmp_outfile_path(&outfile_dir, "test-invalid-wordcount.json"); + + // Test: invalid word count should fail + let result = keygen_new(Some(outfile_path), false, true, true, 9); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("Invalid word count")); + } +} diff --git a/cli/src/lib.rs b/cli/src/lib.rs index bcc971d2a0..cf866543df 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -18,10 +18,16 @@ use regex::{Regex, RegexBuilder}; use rust_template::{ProgramTemplate, TestTemplate}; use semver::{Version, VersionReq}; use serde_json::{json, Map, Value as JsonValue}; +use solana_cli_config::Config as SolanaCliConfig; use solana_commitment_config::CommitmentConfig; +use solana_compute_budget_interface::ComputeBudgetInstruction; +use solana_instruction::Instruction; use solana_keypair::Keypair; use solana_pubkey::Pubkey; +use solana_pubsub_client::pubsub_client::{PubsubClient, PubsubClientSubscription}; use solana_rpc_client::rpc_client::RpcClient; +use solana_rpc_client_api::config::{RpcTransactionLogsConfig, RpcTransactionLogsFilter}; +use solana_rpc_client_api::response::{Response as RpcResponse, RpcLogsResponse}; use solana_signer::{EncodableKey, Signer}; use std::collections::BTreeMap; use std::collections::HashMap; @@ -34,8 +40,11 @@ use std::process::{Child, Command as ProcessCommand, Stdio}; use std::string::ToString; use std::sync::LazyLock; +mod account; mod checks; pub mod config; +mod keygen; +mod program; pub mod rust_template; // Version of the docker image. @@ -45,6 +54,9 @@ pub const DOCKER_BUILDER_VERSION: &str = VERSION; /// Default RPC port pub const DEFAULT_RPC_PORT: u16 = 8899; +/// WebSocket port offset for solana-test-validator (RPC port + 1) +pub const WEBSOCKET_PORT_OFFSET: u16 = 1; + pub static AVM_HOME: LazyLock = LazyLock::new(|| { if let Ok(avm_home) = std::env::var("AVM_HOME") { PathBuf::from(avm_home) @@ -238,6 +250,8 @@ pub enum Command { /// Remove all artifacts from the generated directories except program keypairs. Clean, /// Deploys each program in the workspace. + #[clap(hide = true)] + #[deprecated(since = "0.32.0", note = "use `anchor program deploy` instead")] Deploy { /// Only deploy this program #[clap(short, long)] @@ -260,6 +274,8 @@ pub enum Command { /// Deploys, initializes an IDL, and migrates all in one command. /// Upgrades a single program. The configured wallet must be the upgrade /// authority. + #[clap(hide = true)] + #[deprecated(since = "0.32.0", note = "use `anchor program upgrade` instead")] Upgrade { /// The program to upgrade. #[clap(short, long)] @@ -273,17 +289,23 @@ pub enum Command { #[clap(required = false, last = true)] solana_args: Vec, }, - #[cfg(feature = "dev")] - /// Runs an airdrop loop, continuously funding the configured wallet. + /// Request an airdrop of SOL Airdrop { - #[clap(short, long)] - url: Option, + /// Amount of SOL to airdrop + amount: f64, + /// Recipient address (defaults to configured wallet) + pubkey: Option, }, /// Cluster commands. Cluster { #[clap(subcommand)] subcmd: ClusterCommand, }, + /// Configuration management commands. + Config { + #[clap(subcommand)] + subcmd: ConfigCommand, + }, /// Starts a node shell with an Anchor client setup according to the local /// config. Shell, @@ -334,7 +356,7 @@ pub enum Command { }, /// Fetch and deserialize an account using the IDL provided. Account { - /// Account struct to deserialize + /// Account struct to deserialize (format: .) account_type: String, /// Address of the account to deserialize address: Pubkey, @@ -347,6 +369,94 @@ pub enum Command { #[clap(value_enum)] shell: clap_complete::Shell, }, + /// Get your public key + Address, + /// Get your balance + Balance { + /// Account to check balance for (defaults to configured wallet) + pubkey: Option, + /// Display balance in lamports instead of SOL + #[clap(long)] + lamports: bool, + }, + /// Get current epoch + Epoch, + /// Get information about the current epoch + #[clap(name = "epoch-info")] + EpochInfo, + /// Stream transaction logs + Logs { + /// Include vote transactions when monitoring all transactions + #[clap(long)] + include_votes: bool, + /// Addresses to filter logs by + #[clap(long)] + address: Option>, + }, + /// Show the contents of an account + ShowAccount { + #[clap(flatten)] + cmd: account::ShowAccountCommand, + }, + /// Keypair generation and management + Keygen { + #[clap(subcommand)] + subcmd: KeygenCommand, + }, + /// Program deployment and management commands + Program { + #[clap(subcommand)] + subcmd: ProgramCommand, + }, +} + +#[derive(Debug, Parser)] +pub enum KeygenCommand { + /// Generate a new keypair + New { + /// Path to generated keypair file + #[clap(short = 'o', long)] + outfile: Option, + /// Overwrite the output file if it exists + #[clap(short, long)] + force: bool, + /// Do not prompt for a passphrase + #[clap(long)] + no_passphrase: bool, + /// Do not display the generated pubkey + #[clap(long)] + silent: bool, + /// Number of words in the mnemonic phrase [possible values: 12, 15, 18, 21, 24] + #[clap(short = 'w', long, default_value = "12")] + word_count: usize, + }, + /// Display the pubkey for a given keypair + Pubkey { + /// Keypair filepath + keypair: Option, + }, + /// Recover a keypair from a seed phrase + Recover { + /// Path to recovered keypair file + #[clap(short = 'o', long)] + outfile: Option, + /// Overwrite the output file if it exists + #[clap(short, long)] + force: bool, + /// Skip seed phrase validation + #[clap(long)] + skip_seed_phrase_validation: bool, + /// Do not prompt for a passphrase + #[clap(long)] + no_passphrase: bool, + }, + /// Verify a keypair can sign and verify a message + Verify { + /// Public key to verify + pubkey: Pubkey, + /// Keypair filepath (defaults to configured wallet) + keypair: Option, + }, } #[derive(Debug, Parser)] @@ -361,6 +471,163 @@ pub enum KeysCommand { }, } +#[derive(Debug, Parser)] +pub enum ProgramCommand { + /// Deploy an upgradeable program + Deploy { + /// Program filepath (e.g., target/deploy/my_program.so). + /// If not provided, discovers programs from workspace + program_filepath: Option, + /// Program name to deploy (from workspace). Used when program_filepath is not provided + #[clap(short, long)] + program_name: Option, + /// Program keypair filepath (defaults to target/deploy/{program_name}-keypair.json) + #[clap(long)] + program_keypair: Option, + /// Upgrade authority keypair (defaults to configured wallet) + #[clap(long)] + upgrade_authority: Option, + /// Program id to deploy to (derived from program-keypair if not specified) + #[clap(long)] + program_id: Option, + /// Buffer account to use for deployment + #[clap(long)] + buffer: Option, + /// Maximum transaction length (BPF loader upgradeable limit) + #[clap(long)] + max_len: Option, + /// Don't upload IDL during deployment (IDL is uploaded by default) + #[clap(long)] + no_idl: bool, + /// Make the program immutable after deployment (cannot be upgraded) + #[clap(long = "final")] + make_final: bool, + /// Additional arguments to configure deployment (e.g., --with-compute-unit-price 1000) + #[clap(required = false, last = true)] + solana_args: Vec, + }, + /// Write a program into a buffer account + WriteBuffer { + /// Program filepath (e.g., target/deploy/my_program.so). + /// If not provided, discovers program from workspace using program_name + program_filepath: Option, + /// Program name to write (from workspace). Used when program_filepath is not provided + #[clap(short, long)] + program_name: Option, + /// Buffer account keypair (defaults to new keypair) + #[clap(long)] + buffer: Option, + /// Buffer authority (defaults to configured wallet) + #[clap(long)] + buffer_authority: Option, + /// Maximum transaction length + #[clap(long)] + max_len: Option, + }, + /// Set a new buffer authority + SetBufferAuthority { + /// Buffer account address + buffer: Pubkey, + /// New buffer authority + new_buffer_authority: Pubkey, + }, + /// Set a new program authority + SetUpgradeAuthority { + /// Program id + program_id: Pubkey, + /// New upgrade authority pubkey + #[clap(long)] + new_upgrade_authority: Option, + /// New upgrade authority signer (keypair file). Required unless --skip-new-upgrade-authority-signer-check is used. + /// When provided, both current and new authority will sign (checked mode, recommended) + #[clap(long)] + new_upgrade_authority_signer: Option, + /// Skip new upgrade authority signer check. Allows setting authority with only current authority signature. + /// WARNING: Less safe - use only if you're confident the pubkey is correct + #[clap(long)] + skip_new_upgrade_authority_signer_check: bool, + /// Make the program immutable (cannot be upgraded) + #[clap(long = "final")] + make_final: bool, + /// Current upgrade authority keypair (defaults to configured wallet) + #[clap(long)] + upgrade_authority: Option, + }, + /// Display information about a buffer or program + Show { + /// Account address (buffer or program) + account: Pubkey, + /// Get account information from the Solana config file + #[clap(long)] + get_programs: bool, + /// Get account information from the Solana config file + #[clap(long)] + get_buffers: bool, + /// Show all accounts + #[clap(long)] + all: bool, + }, + /// Upgrade an upgradeable program + Upgrade { + /// Program id to upgrade + program_id: Pubkey, + /// Program filepath (e.g., target/deploy/my_program.so). If not provided, discovers from workspace + #[clap(long)] + program_filepath: Option, + /// Program name to upgrade (from workspace). Used when program_filepath is not provided + #[clap(short, long)] + program_name: Option, + /// Existing buffer account to upgrade from. If not provided, auto-discovers program from workspace + #[clap(long)] + buffer: Option, + /// Upgrade authority (defaults to configured wallet) + #[clap(long)] + upgrade_authority: Option, + /// Max times to retry on failure + #[clap(long, default_value = "0")] + max_retries: u32, + /// Additional arguments to configure deployment (e.g., --with-compute-unit-price 1000) + #[clap(required = false, last = true)] + solana_args: Vec, + }, + /// Write the program data to a file + Dump { + /// Program account address + account: Pubkey, + /// Output file path + output_file: String, + }, + /// Close a program or buffer account and withdraw all lamports + Close { + /// Account address to close (buffer or program). + /// If not provided, discovers program from workspace using program_name + account: Option, + /// Program name to close (from workspace). Used when account is not provided + #[clap(short, long)] + program_name: Option, + /// Authority keypair (defaults to configured wallet) + #[clap(long)] + authority: Option, + /// Recipient address for reclaimed lamports (defaults to authority) + #[clap(long)] + recipient: Option, + /// Bypass warning prompts + #[clap(long)] + bypass_warning: bool, + }, + /// Extend the length of an upgradeable program + Extend { + /// Program id to extend. + /// If not provided, discovers program from workspace using program_name + program_id: Option, + /// Program name to extend (from workspace). Used when program_id is not provided + #[clap(short, long)] + program_name: Option, + /// Additional bytes to allocate + additional_bytes: usize, + }, +} + #[derive(Debug, Parser)] pub enum IdlCommand { /// Initializes a program's IDL account. Can only be run once. @@ -491,11 +758,136 @@ pub enum ClusterCommand { List, } +#[derive(Debug, Parser)] +pub enum ConfigCommand { + /// Get configuration settings from the local Anchor.toml + Get, + /// Set configuration settings in the local Anchor.toml + Set { + /// Cluster to connect to (custom URL). Use -um, -ud, -ut, -ul for standard clusters + #[clap(short = 'u', long = "url")] + url: Option, + /// Path to wallet keypair file to update the Anchor.toml file with + #[clap(short = 'k', long = "keypair")] + keypair: Option, + }, +} + fn get_keypair(path: &str) -> Result { solana_keypair::read_keypair_file(path) .map_err(|_| anyhow!("Unable to read keypair file ({path})")) } +/// Format lamports as SOL with trailing zeros removed +fn format_sol(lamports: u64) -> String { + let sol = lamports as f64 / 1_000_000_000.0; + let formatted = format!("{:.8}", sol); + + // Remove trailing zeros and decimal point if not needed + let trimmed = formatted.trim_end_matches('0').trim_end_matches('.'); + format!("{} SOL", trimmed) +} + +/// Get cluster URL and wallet path from Anchor config, CLI overrides, or Solana CLI config +fn get_cluster_and_wallet(cfg_override: &ConfigOverride) -> Result<(String, String)> { + // Try to get from Anchor workspace config first + if let Ok(Some(cfg)) = Config::discover(cfg_override) { + return Ok(( + cfg.provider.cluster.url().to_string(), + cfg.provider.wallet.to_string(), + )); + } + + // Try to load Solana CLI config + let (cluster_url, wallet_path) = + if let Some(config_file) = solana_cli_config::CONFIG_FILE.as_ref() { + match SolanaCliConfig::load(config_file) { + Ok(cli_config) => ( + cli_config.json_rpc_url.clone(), + cli_config.keypair_path.clone(), + ), + Err(_) => { + // Fallback to defaults if Solana CLI config doesn't exist + ( + "https://api.mainnet-beta.solana.com".to_string(), + dirs::home_dir() + .map(|home| { + home.join(".config/solana/id.json") + .to_string_lossy() + .to_string() + }) + .unwrap_or_else(|| "~/.config/solana/id.json".to_string()), + ) + } + } + } else { + // If CONFIG_FILE is None, use defaults + ( + "https://api.mainnet-beta.solana.com".to_string(), + dirs::home_dir() + .map(|home| { + home.join(".config/solana/id.json") + .to_string_lossy() + .to_string() + }) + .unwrap_or_else(|| "~/.config/solana/id.json".to_string()), + ) + }; + + // Apply cluster override if provided + let final_cluster = if let Some(cluster) = &cfg_override.cluster { + cluster.url().to_string() + } else { + cluster_url + }; + + Ok((final_cluster, wallet_path)) +} + +/// Get the recommended priority fee from the RPC client +pub fn get_recommended_micro_lamport_fee(client: &RpcClient) -> Result { + let mut fees = client.get_recent_prioritization_fees(&[])?; + if fees.is_empty() { + // Fees may be empty, e.g. on localnet + return Ok(0); + } + + // Get the median fee from the most recent 150 slots' prioritization fee + fees.sort_unstable_by_key(|fee| fee.prioritization_fee); + let median_index = fees.len() / 2; + + let median_priority_fee = if fees.len() % 2 == 0 { + (fees[median_index - 1].prioritization_fee + fees[median_index].prioritization_fee) / 2 + } else { + fees[median_index].prioritization_fee + }; + + Ok(median_priority_fee) +} + +/// Prepend a compute unit ix, if the priority fee is greater than 0. +pub fn prepend_compute_unit_ix( + instructions: Vec, + client: &RpcClient, + priority_fee: Option, +) -> Result> { + let priority_fee = match priority_fee { + Some(fee) => fee, + None => get_recommended_micro_lamport_fee(client)?, + }; + + if priority_fee > 0 { + let mut instructions_appended = instructions.clone(); + instructions_appended.insert( + 0, + ComputeBudgetInstruction::set_compute_unit_price(priority_fee), + ); + Ok(instructions_appended) + } else { + Ok(instructions) + } +} + pub fn entry(opts: Opts) -> Result<()> { let restore_cbs = override_toolchain(&opts.cfg_override)?; let result = process_command(opts); @@ -802,36 +1194,48 @@ fn process_command(opts: Opts) -> Result<()> { args, ), Command::Clean => clean(&opts.cfg_override), + #[allow(deprecated)] Command::Deploy { program_name, program_keypair, verifiable, no_idl, solana_args, - } => deploy( - &opts.cfg_override, - program_name, - program_keypair, - verifiable, - no_idl, - solana_args, - ), + } => { + eprintln!( + "Warning: 'anchor deploy' is deprecated. Use 'anchor program deploy' instead." + ); + deploy( + &opts.cfg_override, + program_name, + program_keypair, + verifiable, + no_idl, + solana_args, + ) + } Command::Expand { program_name, cargo_args, } => expand(&opts.cfg_override, program_name, &cargo_args), + #[allow(deprecated)] Command::Upgrade { program_id, program_filepath, max_retries, solana_args, - } => upgrade( - &opts.cfg_override, - program_id, - program_filepath, - max_retries, - solana_args, - ), + } => { + eprintln!( + "Warning: 'anchor upgrade' is deprecated. Use 'anchor program upgrade' instead." + ); + upgrade( + &opts.cfg_override, + program_id, + program_filepath, + max_retries, + solana_args, + ) + } Command::Idl { subcmd } => idl(&opts.cfg_override, subcmd), Command::Migrate => migrate(&opts.cfg_override), Command::Test { @@ -862,9 +1266,9 @@ fn process_command(opts: Opts) -> Result<()> { cargo_args, arch, ), - #[cfg(feature = "dev")] - Command::Airdrop { .. } => airdrop(&opts.cfg_override), + Command::Airdrop { amount, pubkey } => airdrop(&opts.cfg_override, amount, pubkey), Command::Cluster { subcmd } => cluster(subcmd), + Command::Config { subcmd } => config_cmd(&opts.cfg_override, subcmd), Command::Shell => shell(&opts.cfg_override), Command::Run { script, @@ -904,6 +1308,17 @@ fn process_command(opts: Opts) -> Result<()> { ); Ok(()) } + Command::Address => address(&opts.cfg_override), + Command::Balance { pubkey, lamports } => balance(&opts.cfg_override, pubkey, lamports), + Command::Epoch => epoch(&opts.cfg_override), + Command::EpochInfo => epoch_info(&opts.cfg_override), + Command::Logs { + include_votes, + address, + } => logs_subscribe(&opts.cfg_override, include_votes, address), + Command::ShowAccount { cmd } => account::show_account(&opts.cfg_override, cmd), + Command::Keygen { subcmd } => keygen::keygen(&opts.cfg_override, subcmd), + Command::Program { subcmd } => program::program(&opts.cfg_override, subcmd), } } @@ -1076,7 +1491,7 @@ fn new( template: ProgramTemplate, force: bool, ) -> Result<()> { - with_workspace(cfg_override, |cfg| { + with_workspace(cfg_override, |cfg| -> Result<()> { match cfg.path().parent() { None => { println!("Unable to make new program"); @@ -1113,7 +1528,7 @@ fn new( } }; Ok(()) - }) + })? } /// Array of (path, content) tuple. @@ -1185,7 +1600,8 @@ pub fn expand( cd_member(cfg_override, program_name)?; } - let workspace_cfg = Config::discover(cfg_override)?.expect("Not in workspace."); + let workspace_cfg = Config::discover(cfg_override)? + .ok_or_else(|| anyhow!("The 'anchor expand' command requires an Anchor workspace."))?; let cfg_parent = workspace_cfg.path().parent().expect("Invalid Anchor.toml"); let cargo = Manifest::discover()?; @@ -1294,7 +1710,8 @@ pub fn build( if let Some(program_name) = program_name.as_ref() { cd_member(cfg_override, program_name)?; } - let cfg = Config::discover(cfg_override)?.expect("Not in workspace."); + let cfg = Config::discover(cfg_override)? + .ok_or_else(|| anyhow!("The 'anchor build' command requires an Anchor workspace."))?; let cfg_parent = cfg.path().parent().expect("Invalid Anchor.toml"); // Require overflow checks @@ -1939,10 +2356,10 @@ pub fn verify( } fn cd_member(cfg_override: &ConfigOverride, program_name: &str) -> Result<()> { - // Change directories to the given `program_name`, if given. - let cfg = Config::discover(cfg_override)?.expect("Not in workspace."); + // Change directories to the given `program_name`, using either Anchor or Cargo workspace + let programs = program::get_programs_from_workspace(cfg_override, None)?; - for program in cfg.read_all_programs()? { + for program in programs { let cargo_toml = program.path.join("Cargo.toml"); if !cargo_toml.exists() { return Err(anyhow!( @@ -1953,8 +2370,7 @@ fn cd_member(cfg_override: &ConfigOverride, program_name: &str) -> Result<()> { let manifest = Manifest::from_path(&cargo_toml)?; let pkg_name = manifest.package().name(); - let lib_name = manifest.lib_name()?; - if program_name == pkg_name || program_name == lib_name { + if program_name == pkg_name || program_name == program.lib_name { std::env::set_current_dir(&program.path)?; return Ok(()); } @@ -2052,15 +2468,22 @@ fn idl_init( priority_fee: Option, non_canonical: bool, ) -> Result<()> { - let url = rpc_url(cfg_override)?; + // Get cluster URL and wallet path from Anchor config + let (cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?; + + // Skip IDL initialization on localnet + let is_localnet = cluster_url.contains("localhost") || cluster_url.contains("127.0.0.1"); + if is_localnet { + println!("Skipping IDL initialization on localnet"); + return Ok(()); + } let program_id_str = program_id.to_string(); - let mut args = vec!["write", "idl", &program_id_str, &idl_filepath]; - if non_canonical { - args.push("--non-canonical"); - } + // Build args with global options first, then command and command args + let mut args = vec!["--keypair", &wallet_path, "--rpc", &cluster_url]; + // Global option: priority fees let priority_fee_str; if let Some(priority_fee) = priority_fee { priority_fee_str = priority_fee.to_string(); @@ -2068,8 +2491,16 @@ fn idl_init( args.push(&priority_fee_str); } - args.push("--rpc"); - args.push(&url); + // Command: write + args.push("write"); + args.push("idl"); + args.push(&program_id_str); + args.push(&idl_filepath); + + // Command option: non-canonical + if non_canonical { + args.push("--non-canonical"); + } let status = ProcessCommand::new("npx") .arg("@solana-program/program-metadata") @@ -2092,11 +2523,22 @@ fn idl_upgrade( idl_filepath: String, priority_fee: Option, ) -> Result<()> { - let url = rpc_url(cfg_override)?; + // Get cluster URL and wallet path from Anchor config + let (cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?; + + // Skip IDL upgrade on localnet + let is_localnet = cluster_url.contains("localhost") || cluster_url.contains("127.0.0.1"); + if is_localnet { + println!("Skipping IDL upgrade on localnet"); + return Ok(()); + } let program_id_str = program_id.to_string(); - let mut args = vec!["write", "idl", &program_id_str, &idl_filepath]; + // Build args with global options first, then command and command args + let mut args = vec!["--keypair", &wallet_path, "--rpc", &cluster_url]; + + // Global option: priority fees let priority_fee_str; if let Some(priority_fee) = priority_fee { priority_fee_str = priority_fee.to_string(); @@ -2104,8 +2546,11 @@ fn idl_upgrade( args.push(&priority_fee_str); } - args.push("--rpc"); - args.push(&url); + // Command: write + args.push("write"); + args.push("idl"); + args.push(&program_id_str); + args.push(&idl_filepath); let status = ProcessCommand::new("npx") .arg("@solana-program/program-metadata") @@ -2131,7 +2576,8 @@ fn idl_build( skip_lint: bool, cargo_args: Vec, ) -> Result<()> { - let cfg = Config::discover(cfg_override)?.expect("Not in workspace"); + let cfg = Config::discover(cfg_override)? + .ok_or_else(|| anyhow!("The 'anchor idl build' command requires an Anchor workspace."))?; let current_dir = std::env::current_dir()?; let program_path = match program_name { Some(name) => cfg.get_program(&name)?.path, @@ -2466,9 +2912,8 @@ fn account( let idl = idl_filepath.map_or_else( || { - Config::discover(cfg_override) - .expect("Error when detecting workspace.") - .expect("Not in workspace.") + Config::discover(cfg_override)? + .ok_or_else(|| anyhow!("The 'anchor account' command requires an Anchor workspace with Anchor.toml for IDL type generation."))? .read_all_programs() .expect("Workspace must contain atleast one program.") .into_iter() @@ -2734,7 +3179,7 @@ fn test( }) .collect::, _>>()?; - with_workspace(cfg_override, |cfg| { + with_workspace(cfg_override, |cfg| -> Result<()> { // Build if needed. if !skip_build { build( @@ -2844,7 +3289,7 @@ fn test( } cfg.run_hooks(HookType::PostTest)?; Ok(()) - }) + })? } #[allow(clippy::too_many_arguments)] @@ -2883,8 +3328,15 @@ fn run_test_suite( get_node_dns_option()?, ); - // Setup log reader. - let log_streams = stream_logs(cfg, &url); + // Setup log reader - kept alive until end of scope + let log_streams = match stream_logs(cfg, &url) { + Ok(streams) => Some(streams), + Err(e) => { + eprintln!("Warning: Failed to setup program log streaming: {:#}", e); + eprintln!("Program logs will still be visible in the test output."); + None + } + }; // Run the tests. let test_result = { @@ -2918,9 +3370,11 @@ fn run_test_suite( println!("Failed to kill subprocess {}: {}", child.id(), err); } } - for mut child in log_streams? { - if let Err(err) = child.kill() { - println!("Failed to kill subprocess {}: {}", child.id(), err); + + // Explicitly shutdown log streams - closes WebSocket subscriptions + if let Some(log_streams) = log_streams { + for handle in log_streams { + handle.shutdown(); } } @@ -3112,58 +3566,163 @@ fn validator_flags( Ok(flags) } -fn stream_logs(config: &WithPath, rpc_url: &str) -> Result> { +/// Handle for a log streaming thread. +/// +/// Manages a WebSocket subscription and its associated receiver thread. +/// Call `shutdown()` to cleanly stop the thread. +struct LogStreamHandle { + subscription: PubsubClientSubscription>, +} + +impl LogStreamHandle { + /// Explicitly shutdown the log stream + fn shutdown(self) { + // Send unsubscribe in a background thread to avoid blocking + // PubsubClientSubscription::send_unsubscribe() can block indefinitely if WebSocket is stuck + // The receiver threads will exit when the subscription closes + std::thread::spawn(move || { + let _ = self.subscription.send_unsubscribe(); + }); + } +} + +/// Spawns a thread to receive logs from a subscription and write them to a file +fn spawn_log_receiver_thread(receiver: R, log_file_path: PathBuf) +where + R: IntoIterator> + Send + 'static, +{ + std::thread::spawn(move || { + if let Ok(mut file) = File::create(&log_file_path) { + for response in receiver { + let _ = writeln!( + file, + "Transaction executed in slot {}:", + response.context.slot + ); + let _ = writeln!(file, " Signature: {}", response.value.signature); + let _ = writeln!( + file, + " Status: {}", + response + .value + .err + .map(|err| err.to_string()) + .unwrap_or_else(|| "Ok".to_string()) + ); + let _ = writeln!(file, " Log Messages:"); + for log in response.value.logs { + let _ = writeln!(file, " {}", log); + } + let _ = writeln!(file); // Empty line between transactions + let _ = file.flush(); + } + } else { + eprintln!("Failed to create log file: {:?}", log_file_path); + } + }); +} + +fn stream_logs(config: &WithPath, rpc_url: &str) -> Result> { let program_logs_dir = Path::new(".anchor").join("program-logs"); if program_logs_dir.exists() { - fs::remove_dir_all(&program_logs_dir) - .with_context(|| format!("Failed to remove dir {}", program_logs_dir.display()))?; + fs::remove_dir_all(&program_logs_dir)?; } - fs::create_dir_all(&program_logs_dir) - .with_context(|| format!("Failed to create dir {}", program_logs_dir.display()))?; + fs::create_dir_all(&program_logs_dir)?; + + // For solana-test-validator, the WebSocket port is RPC port + WEBSOCKET_PORT_OFFSET + // Extract port from rpc_url and construct WebSocket URL + let ws_url = if rpc_url.contains("127.0.0.1") || rpc_url.contains("localhost") { + // Local validator: increment port by 1 for WebSocket + let rpc_port = rpc_url + .rsplit_once(':') + .and_then(|(_, port)| port.parse::().ok()) + .unwrap_or(DEFAULT_RPC_PORT); + + let ws_port = rpc_port + WEBSOCKET_PORT_OFFSET; + let url = format!("ws://127.0.0.1:{}", ws_port); + url + } else { + // Remote cluster: use same URL but replace http(s) with ws(s) + rpc_url + .replace("https://", "wss://") + .replace("http://", "ws://") + }; + + // Give the WebSocket endpoint a moment to be ready (especially for local validators) + std::thread::sleep(std::time::Duration::from_millis(1500)); let mut handles = vec![]; + + // Subscribe to logs for all workspace programs for program in config.read_all_programs()? { let idl_path = Path::new("target") .join("idl") .join(&program.lib_name) .with_extension("json"); - let idl = fs::read(&idl_path) - .with_context(|| format!("Failed to read IDL file {}", idl_path.display()))?; + let idl = fs::read(&idl_path)?; let idl = convert_idl(&idl)?; - let log_path = program_logs_dir.join(format!("{}.{}.log", &idl.address, program.lib_name)); - let log_file = File::create(&log_path) - .with_context(|| format!("Failed to create log file {}", log_path.display()))?; - let stdio = std::process::Stdio::from(log_file); - let child = std::process::Command::new("solana") - .arg("logs") - .arg(&idl.address) - .arg("--url") - .arg(rpc_url) - .stdout(stdio) - .spawn() - .with_context(|| format!("Failed to spawn 'solana logs' for {}", &idl.address))?; - handles.push(child); + let log_file_path = + program_logs_dir.join(format!("{}.{}.log", idl.address, program.lib_name)); + let program_address = idl.address.clone(); + + // Subscribe to logs using PubsubClient + let (client, receiver) = match PubsubClient::logs_subscribe( + &ws_url, + RpcTransactionLogsFilter::Mentions(vec![program_address.clone()]), + RpcTransactionLogsConfig { + commitment: Some(CommitmentConfig::confirmed()), + }, + ) { + Ok(result) => result, + Err(e) => { + eprintln!( + "Warning: Failed to subscribe to logs for program {}: {}", + program.lib_name, e + ); + continue; + } + }; + + // Spawn thread to write logs to file + spawn_log_receiver_thread(receiver, log_file_path); + + handles.push(LogStreamHandle { + subscription: client, + }); } + + // Also subscribe to logs for genesis programs if let Some(test) = config.test_validator.as_ref() { if let Some(genesis) = &test.genesis { for entry in genesis { - let genesis_log_path = program_logs_dir.join(&entry.address).with_extension("log"); - let log_file = File::create(&genesis_log_path).with_context(|| { - format!("Failed to create log file {}", genesis_log_path.display()) - })?; - let stdio = std::process::Stdio::from(log_file); - let child = std::process::Command::new("solana") - .arg("logs") - .arg(entry.address.clone()) - .arg("--url") - .arg(rpc_url) - .stdout(stdio) - .spawn() - .with_context(|| { - "Failed to spawn 'solana logs' for genesis entry".to_string() - })?; - handles.push(child); + let log_file_path = program_logs_dir.join(&entry.address).with_extension("log"); + let address = entry.address.clone(); + + // Subscribe to logs using PubsubClient + let (client, receiver) = match PubsubClient::logs_subscribe( + &ws_url, + RpcTransactionLogsFilter::Mentions(vec![address.clone()]), + RpcTransactionLogsConfig { + commitment: Some(CommitmentConfig::confirmed()), + }, + ) { + Ok(result) => result, + Err(e) => { + eprintln!( + "Warning: Failed to subscribe to logs for genesis program {}: {}", + &entry.address, e + ); + continue; + } + }; + + // Spawn thread to write logs to file + spawn_log_receiver_thread(receiver, log_file_path); + + handles.push(LogStreamHandle { + subscription: client, + }); } } } @@ -3319,10 +3878,19 @@ fn cluster_url(cfg: &Config, test_validator: &Option) -> String { } fn clean(cfg_override: &ConfigOverride) -> Result<()> { - let cfg = Config::discover(cfg_override)?.expect("Not in workspace."); - let cfg_parent = cfg.path().parent().expect("Invalid Anchor.toml"); - let dot_anchor_dir = cfg_parent.join(".anchor"); - let target_dir = cfg_parent.join("target"); + // Get workspace root - either from Anchor.toml or use current directory + let workspace_root = if let Ok(Some(cfg)) = Config::discover(cfg_override) { + cfg.path() + .parent() + .expect("Invalid Anchor.toml") + .to_path_buf() + } else { + // No Anchor.toml - use current directory for Cargo workspace + std::env::current_dir()? + }; + + let dot_anchor_dir = workspace_root.join(".anchor"); + let target_dir = workspace_root.join("target"); let deploy_dir = target_dir.join("deploy"); if dot_anchor_dir.exists() { @@ -3369,7 +3937,7 @@ fn deploy( solana_args: Vec, ) -> Result<()> { // Execute the code within the workspace - with_workspace(cfg_override, |cfg| { + with_workspace(cfg_override, |cfg| -> Result<()> { let url = cluster_url(cfg, &cfg.test_validator); let keypair = cfg.provider.wallet.to_string(); @@ -3382,119 +3950,38 @@ fn deploy( println!("Deploying cluster: {url}"); println!("Upgrade authority: {keypair}"); - for mut program in cfg.get_programs(program_name)? { + for program in cfg.get_programs(program_name)? { let binary_path = program.binary_path(verifiable).display().to_string(); println!("Deploying program {:?}...", program.lib_name); println!("Program path: {binary_path}..."); - let (program_keypair_filepath, program_id) = match &program_keypair { - Some(path) => (path.clone(), get_keypair(path)?.pubkey()), - None => ( - program.keypair_file()?.path().display().to_string(), - program.pubkey()?, - ), + let program_keypair_filepath = match &program_keypair { + Some(path) => path.clone(), + None => program.keypair_file()?.path().display().to_string(), }; - // Send deploy transactions using the Solana CLI - let exit = std::process::Command::new("solana") - .arg("program") - .arg("deploy") - .arg("--url") - .arg(&url) - .arg("--keypair") - .arg(&keypair) - .arg("--program-id") - .arg(strip_workspace_prefix(program_keypair_filepath)) - .arg(strip_workspace_prefix(binary_path)) - .args(&solana_args) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .output() - .expect("Must deploy"); - - // Check if deployment was successful - if !exit.status.success() { - println!("There was a problem deploying: {exit:?}."); - std::process::exit(exit.status.code().unwrap_or(1)); - } - - // Get the IDL filepath - let idl_filepath = Path::new("target") - .join("idl") - .join(&program.lib_name) - .with_extension("json"); - - if let Some(idl) = program.idl.as_mut() { - // Add program address to the IDL. - idl.address = program_id.to_string(); - - // Persist it. - write_idl(idl, OutFile::File(idl_filepath.clone()))?; - - // Upload the IDL to the cluster by default (unless no_idl is set) - if !no_idl { - // Wait for the program to be confirmed before initializing IDL to prevent - // race condition where the program isn't yet available in validator cache - let client = create_client(&url); - let max_retries = 5; - let retry_delay = std::time::Duration::from_millis(500); - let cache_delay = std::time::Duration::from_secs(2); - - println!("Waiting for program {program_id} to be confirmed..."); - - for attempt in 0..max_retries { - if let Ok(account) = client.get_account(&program_id) { - if account.executable { - println!("Program confirmed on-chain"); - std::thread::sleep(cache_delay); - break; - } - } - - if attempt == max_retries - 1 { - return Err(anyhow!( - "Timeout waiting for program {} to be confirmed", - program_id - )); - } - - std::thread::sleep(retry_delay); - } - - // Check if IDL account already exists - let (base, _) = Pubkey::find_program_address(&[], &program_id); - let idl_address = Pubkey::create_with_seed(&base, "anchor:idl", &program_id) - .expect("Seed is always valid"); - let idl_account_exists = client.get_account(&idl_address).is_ok(); - - if idl_account_exists { - // IDL account exists, upgrade it - idl_upgrade( - cfg_override, - program_id, - idl_filepath.display().to_string(), - None, - )?; - } else { - // IDL account doesn't exist, create it - idl_init( - cfg_override, - program_id, - idl_filepath.display().to_string(), - None, - false, - )?; - } - } - } + // Deploy using our native implementation + program::program_deploy( + cfg_override, + Some(strip_workspace_prefix(binary_path)), + None, // program_name - not needed since we have filepath + Some(strip_workspace_prefix(program_keypair_filepath)), + None, // upgrade_authority - uses wallet from config + None, // program_id - derived from program_keypair + None, // buffer + None, // max_len + no_idl, + false, // make_final + solana_args.clone(), + )?; } println!("Deploy success"); cfg.run_hooks(HookType::PostDeploy)?; Ok(()) - }) + })? } fn upgrade( @@ -3504,47 +3991,21 @@ fn upgrade( max_retries: u32, solana_args: Vec, ) -> Result<()> { - let path: PathBuf = program_filepath.parse().unwrap(); - let program_filepath = path.canonicalize()?.display().to_string(); - - with_workspace(cfg_override, |cfg| { - let url = cluster_url(cfg, &cfg.test_validator); - let client = create_client(&url); - let solana_args = add_recommended_deployment_solana_args(&client, solana_args)?; - - for retry in 0..(1 + max_retries) { - let exit = std::process::Command::new("solana") - .arg("program") - .arg("deploy") - .arg("--url") - .arg(url.clone()) - .arg("--keypair") - .arg(cfg.provider.wallet.to_string()) - .arg("--program-id") - .arg(program_id.to_string()) - .arg(strip_workspace_prefix(program_filepath.clone())) - .args(&solana_args) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .output() - .expect("Must deploy"); - if exit.status.success() { - break; - } - - println!("There was a problem deploying: {exit:?}."); - if retry < max_retries { - println!("Retrying {} more time(s)...", max_retries - retry); - } else { - std::process::exit(exit.status.code().unwrap_or(1)); - } - } - Ok(()) - }) + // Use our native upgrade implementation + program::program_upgrade( + cfg_override, + program_id, + Some(program_filepath), + None, // program_name - not needed since we have filepath + None, // buffer + None, // upgrade_authority - uses wallet from config + max_retries, + solana_args, + ) } fn migrate(cfg_override: &ConfigOverride) -> Result<()> { - with_workspace(cfg_override, |cfg| { + with_workspace(cfg_override, |cfg| -> Result<()> { println!("Running migration deploy script"); let url = cluster_url(cfg, &cfg.test_validator); @@ -3602,10 +4063,11 @@ fn migrate(cfg_override: &ConfigOverride) -> Result<()> { println!("Deploy complete."); Ok(()) - }) + })? } fn set_workspace_dir_or_exit() { + // First try to find Anchor workspace let d = match Config::discover(&ConfigOverride::default()) { Err(err) => { println!("Workspace configuration error: {err}"); @@ -3613,19 +4075,45 @@ fn set_workspace_dir_or_exit() { } Ok(d) => d, }; + match d { None => { - println!("Not in anchor workspace."); - std::process::exit(1); + // No Anchor.toml found - check for Cargo workspace with Solana programs + let current_dir = match std::env::current_dir() { + Ok(dir) => dir, + Err(_) => { + println!("Unable to determine current directory"); + std::process::exit(1); + } + }; + + let cargo_toml_path = current_dir.join("Cargo.toml"); + if !cargo_toml_path.exists() { + println!("Not in a Solana workspace. This command requires either Anchor.toml or a Cargo workspace with Solana programs."); + std::process::exit(1); + } + + // Check if this is a workspace and has Solana programs + match program::discover_solana_programs(None) { + Ok(programs) if !programs.is_empty() => { + // Found Solana programs in Cargo workspace - stay in current directory + // (already in the right place) + } + _ => { + println!("Not in a Solana workspace. This command requires either Anchor.toml or a Cargo workspace with Solana programs."); + std::process::exit(1); + } + } } Some(cfg) => { + // Found Anchor.toml - change to workspace root match cfg.path().parent() { None => { println!("Unable to make new program"); } Some(parent) => { if std::env::set_current_dir(parent).is_err() { - println!("Not in anchor workspace."); + println!("Not in a Solana workspace. This command requires either Anchor.toml or a Cargo workspace with Solana programs."); std::process::exit(1); } } @@ -3634,29 +4122,45 @@ fn set_workspace_dir_or_exit() { } } -#[cfg(feature = "dev")] -fn airdrop(cfg_override: &ConfigOverride) -> Result<()> { - let url = cfg_override - .cluster - .as_ref() - .unwrap_or(&Cluster::Devnet) - .url(); - loop { - let exit = std::process::Command::new("solana") - .arg("airdrop") - .arg("10") - .arg("--url") - .arg(url) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .output() - .expect("Must airdrop"); - if !exit.status.success() { - println!("There was a problem airdropping: {:?}.", exit); - std::process::exit(exit.status.code().unwrap_or(1)); - } - std::thread::sleep(std::time::Duration::from_millis(10000)); - } +fn airdrop(cfg_override: &ConfigOverride, amount: f64, pubkey: Option) -> Result<()> { + // Get cluster URL and wallet path + let (cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?; + + // Create RPC client + let client = RpcClient::new(cluster_url); + + // Determine recipient + let recipient_pubkey = if let Some(pubkey) = pubkey { + pubkey + } else { + // Load keypair from wallet path and get pubkey + let keypair = Keypair::read_from_file(&wallet_path) + .map_err(|e| anyhow!("Failed to read keypair from {}: {}", wallet_path, e))?; + keypair.pubkey() + }; + + // Convert SOL to lamports + let lamports = (amount * 1_000_000_000.0) as u64; + + // Request airdrop + println!("Requesting airdrop of {} SOL...", amount); + let signature = client + .request_airdrop(&recipient_pubkey, lamports) + .map_err(|e| anyhow!("Airdrop request failed: {}", e))?; + + println!("Signature: {}", signature); + println!("Waiting for confirmation..."); + + // Wait for confirmation + client + .confirm_transaction(&signature) + .map_err(|e| anyhow!("Transaction confirmation failed: {}", e))?; + + // Get and display the new balance + let balance = client.get_balance(&recipient_pubkey)?; + println!("{}", format_sol(balance)); + + Ok(()) } fn cluster(_cmd: ClusterCommand) -> Result<()> { @@ -3667,8 +4171,96 @@ fn cluster(_cmd: ClusterCommand) -> Result<()> { Ok(()) } +fn config_cmd(cfg_override: &ConfigOverride, cmd: ConfigCommand) -> Result<()> { + match cmd { + ConfigCommand::Get => config_get(cfg_override), + ConfigCommand::Set { url, keypair } => config_set(cfg_override, url, keypair), + } +} + +fn config_get(cfg_override: &ConfigOverride) -> Result<()> { + with_workspace(cfg_override, |cfg| -> Result<()> { + println!("Anchor Configuration:"); + println!(); + println!("Cluster: {}", cfg.provider.cluster.url()); + println!("Wallet: {}", cfg.provider.wallet); + Ok(()) + })? +} + +fn config_set( + cfg_override: &ConfigOverride, + url: Option, + keypair: Option, +) -> Result<()> { + // Find the Anchor.toml file + let anchor_toml_path = match Config::discover(cfg_override)? { + Some(cfg) => cfg.path().parent().unwrap().join("Anchor.toml"), + None => bail!("Not in an Anchor workspace"), + }; + + // Read the current Anchor.toml + let mut toml_content = + fs::read_to_string(&anchor_toml_path).context("Failed to read Anchor.toml")?; + let mut toml_doc: toml::Value = + toml::from_str(&toml_content).context("Failed to parse Anchor.toml")?; + + let mut updated = false; + + // Update cluster URL if provided + if let Some(cluster_url) = url { + let expanded_url = match cluster_url.as_str() { + "m" => "https://api.mainnet-beta.solana.com".to_string(), + "d" => "https://api.devnet.solana.com".to_string(), + "t" => "https://api.testnet.solana.com".to_string(), + "l" => "http://127.0.0.1:8899".to_string(), + _ => cluster_url, + }; + + if let Some(provider) = toml_doc.get_mut("provider").and_then(|v| v.as_table_mut()) { + provider.insert( + "cluster".to_string(), + toml::Value::String(expanded_url.clone()), + ); + println!("Updated cluster to: {}", expanded_url); + updated = true; + } + } + + // Update wallet path if provided + if let Some(keypair_path) = keypair { + let expanded_path = shellexpand::tilde(&keypair_path).to_string(); + + // Check if the wallet file exists + if !Path::new(&expanded_path).exists() { + eprintln!("Warning: Wallet file does not exist: {}", expanded_path); + } + + if let Some(provider) = toml_doc.get_mut("provider").and_then(|v| v.as_table_mut()) { + provider.insert( + "wallet".to_string(), + toml::Value::String(expanded_path.clone()), + ); + println!("Updated wallet to: {}", expanded_path); + updated = true; + } + } + + if updated { + // Write the updated config back to Anchor.toml + toml_content = + toml::to_string_pretty(&toml_doc).context("Failed to serialize Anchor.toml")?; + fs::write(&anchor_toml_path, toml_content).context("Failed to write Anchor.toml")?; + println!("\nConfiguration updated successfully!"); + } else { + println!("No changes made. Use --url or --keypair to update settings."); + } + + Ok(()) +} + fn shell(cfg_override: &ConfigOverride) -> Result<()> { - with_workspace(cfg_override, |cfg| { + with_workspace(cfg_override, |cfg| -> Result<()> { let programs = { // Create idl map from all workspace programs. let mut idls: HashMap = cfg @@ -3729,11 +4321,11 @@ fn shell(cfg_override: &ConfigOverride) -> Result<()> { return Ok(()); } Ok(()) - }) + })? } fn run(cfg_override: &ConfigOverride, script: String, script_args: Vec) -> Result<()> { - with_workspace(cfg_override, |cfg| { + with_workspace(cfg_override, |cfg| -> Result<()> { let url = cluster_url(cfg, &cfg.test_validator); let script = cfg .scripts @@ -3753,7 +4345,7 @@ fn run(cfg_override: &ConfigOverride, script: String, script_args: Vec) std::process::exit(exit.status.code().unwrap_or(1)); } Ok(()) - }) + })? } fn login(_cfg_override: &ConfigOverride, token: String) -> Result<()> { @@ -3780,18 +4372,18 @@ fn keys(cfg_override: &ConfigOverride, cmd: KeysCommand) -> Result<()> { } fn keys_list(cfg_override: &ConfigOverride) -> Result<()> { - with_workspace(cfg_override, |cfg| { + with_workspace(cfg_override, |cfg| -> Result<()> { for program in cfg.read_all_programs()? { let pubkey = program.pubkey()?; println!("{}: {}", program.lib_name, pubkey); } Ok(()) - }) + })? } /// Sync program `declare_id!` pubkeys with the pubkey from `target/deploy/.json`. fn keys_sync(cfg_override: &ConfigOverride, program_name: Option) -> Result<()> { - with_workspace(cfg_override, |cfg| { + with_workspace(cfg_override, |cfg| -> Result<()> { let declare_id_regex = RegexBuilder::new(r#"^(([\w]+::)*)declare_id!\("(\w*)"\)"#) .multi_line(true) .build() @@ -3865,7 +4457,7 @@ fn keys_sync(cfg_override: &ConfigOverride, program_name: Option) -> Res } Ok(()) - }) + })? } /// Check if there's a mismatch between the program keypair and the `declare_id!` in the source code. @@ -3924,7 +4516,7 @@ fn localnet( cargo_args: Vec, arch: ProgramArch, ) -> Result<()> { - with_workspace(cfg_override, |cfg| { + with_workspace(cfg_override, |cfg| -> Result<()> { // Build if needed. if !skip_build { build( @@ -3955,9 +4547,22 @@ fn localnet( let validator_handle = &mut start_test_validator(cfg, &cfg.test_validator, flags, false)?; - // Setup log reader. + // Setup log reader - kept alive until end of scope let url = test_validator_rpc_url(&cfg.test_validator); - let log_streams = stream_logs(cfg, &url); + let log_streams = match stream_logs(cfg, &url) { + Ok(streams) => { + println!( + "Log streams set up successfully ({} streams)", + streams.len() + ); + Some(streams) + } + Err(e) => { + eprintln!("Warning: Failed to setup program log streaming: {:#}", e); + eprintln!(" Program logs will still be visible in the validator output."); + None + } + }; std::io::stdin().lock().lines().next().unwrap().unwrap(); @@ -3970,14 +4575,15 @@ fn localnet( ); } - for mut child in log_streams? { - if let Err(err) = child.kill() { - println!("Failed to kill subprocess {}: {}", child.id(), err); + // Explicitly shutdown log streams - closes WebSocket subscriptions + if let Some(log_streams) = log_streams { + for handle in log_streams { + handle.shutdown(); } } Ok(()) - }) + })? } // with_workspace ensures the current working directory is always the top level @@ -3989,18 +4595,18 @@ fn localnet( fn with_workspace( cfg_override: &ConfigOverride, f: impl FnOnce(&mut WithPath) -> R, -) -> R { +) -> Result { set_workspace_dir_or_exit(); let mut cfg = Config::discover(cfg_override) - .expect("Previously set the workspace dir") - .expect("Anchor.toml must always exist"); + .map_err(|e| anyhow!("Workspace configuration error: {}", e))? + .ok_or_else(|| anyhow!("This command requires an Anchor workspace."))?; let r = f(&mut cfg); set_workspace_dir_or_exit(); - r + Ok(r) } fn is_hidden(entry: &walkdir::DirEntry) -> bool { @@ -4064,26 +4670,6 @@ fn add_recommended_deployment_solana_args( Ok(augmented_args) } -fn get_recommended_micro_lamport_fee(client: &RpcClient) -> Result { - let mut fees = client.get_recent_prioritization_fees(&[])?; - if fees.is_empty() { - // Fees may be empty, e.g. on localnet - return Ok(0); - } - - // Get the median fee from the most recent 150 slots' prioritization fee - fees.sort_unstable_by_key(|fee| fee.prioritization_fee); - let median_index = fees.len() / 2; - - let median_priority_fee = if fees.len() % 2 == 0 { - (fees[median_index - 1].prioritization_fee + fees[median_index].prioritization_fee) / 2 - } else { - fees[median_index].prioritization_fee - }; - - Ok(median_priority_fee) -} - fn get_node_dns_option() -> Result<&'static str> { let version = get_node_version()?; let req = VersionReq::parse(">=16.4.0").unwrap(); @@ -4114,6 +4700,255 @@ fn create_client(url: U) -> RpcClient { RpcClient::new_with_commitment(url, CommitmentConfig::confirmed()) } +fn address(cfg_override: &ConfigOverride) -> Result<()> { + let (_cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?; + + // Load keypair and get pubkey + let keypair = Keypair::read_from_file(&wallet_path) + .map_err(|e| anyhow!("Failed to read keypair from {}: {}", wallet_path, e))?; + + // Print the public key + println!("{}", keypair.pubkey()); + + Ok(()) +} + +fn balance(cfg_override: &ConfigOverride, pubkey: Option, lamports: bool) -> Result<()> { + let (cluster_url, wallet_path) = get_cluster_and_wallet(cfg_override)?; + + // Create RPC client + let client = RpcClient::new(cluster_url); + + // Determine which account to check + let account_pubkey = if let Some(pubkey) = pubkey { + pubkey + } else { + // Load keypair from wallet path and get pubkey + let keypair = Keypair::read_from_file(&wallet_path) + .map_err(|e| anyhow!("Failed to read keypair from {}: {}", wallet_path, e))?; + keypair.pubkey() + }; + + // Get balance + let balance = client.get_balance(&account_pubkey)?; + + // Format and display output + if lamports { + println!("{}", balance); + } else { + println!("{}", format_sol(balance)); + } + + Ok(()) +} + +fn epoch(cfg_override: &ConfigOverride) -> Result<()> { + let (cluster_url, _wallet_path) = get_cluster_and_wallet(cfg_override)?; + + // Create RPC client + let client = RpcClient::new(cluster_url); + + // Get epoch info + let epoch_info = client.get_epoch_info()?; + + // Print just the epoch number + println!("{}", epoch_info.epoch); + + Ok(()) +} + +fn epoch_info(cfg_override: &ConfigOverride) -> Result<()> { + let (cluster_url, _wallet_path) = get_cluster_and_wallet(cfg_override)?; + + // Create RPC client + let client = RpcClient::new(cluster_url); + + // Get epoch info + let epoch_info = client.get_epoch_info()?; + + // Calculate epoch slot range + let first_slot_in_epoch = epoch_info.absolute_slot - epoch_info.slot_index; + let last_slot_in_epoch = first_slot_in_epoch + epoch_info.slots_in_epoch; + + // Calculate completion stats + let epoch_completed_percent = + epoch_info.slot_index as f64 / epoch_info.slots_in_epoch as f64 * 100.0; + let remaining_slots = epoch_info.slots_in_epoch - epoch_info.slot_index; + + // Display epoch information (matching Solana CLI format) + println!("Block height: {}", epoch_info.block_height); + println!("Slot: {}", epoch_info.absolute_slot); + println!("Epoch: {}", epoch_info.epoch); + + if let Some(tx_count) = epoch_info.transaction_count { + println!("Transaction Count: {}", tx_count); + } + + println!( + "Epoch Slot Range: [{}..{})", + first_slot_in_epoch, last_slot_in_epoch + ); + println!("Epoch Completed Percent: {:>3.3}%", epoch_completed_percent); + println!( + "Epoch Completed Slots: {}/{} ({} remaining)", + epoch_info.slot_index, epoch_info.slots_in_epoch, remaining_slots + ); + + // Try to calculate epoch completed time + // Get average slot time from performance samples (aggregate up to 60 samples) + if let Ok(samples) = client.get_recent_performance_samples(Some(60)) { + // Aggregate all samples to calculate average slot time + let (total_slots, total_secs) = + samples.iter().fold((0u64, 0u64), |(slots, secs), sample| { + ( + slots.saturating_add(sample.num_slots), + secs.saturating_add(sample.sample_period_secs as u64), + ) + }); + + if total_slots > 0 { + let avg_slot_time_ms = (total_secs * 1000) / total_slots; + + // Calculate time_remaining using average slot time (always estimated) + let remaining_secs = (remaining_slots * avg_slot_time_ms) / 1000; + + // Calculate time_elapsed - try actual block times first, then estimate + // Get the first actual block in the epoch and adjust for slot differences + let start_block_time = client + .get_blocks_with_limit(first_slot_in_epoch, 1) + .ok() + .and_then(|slots| slots.first().cloned()) + .and_then(|first_actual_block| { + client.get_block_time(first_actual_block).ok().map(|time| { + // Adjust backwards if first actual block is after expected start + let slot_diff = first_actual_block.saturating_sub(first_slot_in_epoch); + let time_adjustment = (slot_diff * avg_slot_time_ms / 1000) as i64; + time.saturating_sub(time_adjustment) + }) + }); + + let current_block_time = client.get_block_time(epoch_info.absolute_slot).ok(); + + let (elapsed_secs, is_estimated) = if let (Some(start_time), Some(current_time)) = + (start_block_time, current_block_time) + { + // Use actual block times for elapsed + ((current_time - start_time) as u64, false) + } else { + // Estimate elapsed using average slot time + ((epoch_info.slot_index * avg_slot_time_ms) / 1000, true) + }; + + // Total time = elapsed + remaining + let total_secs = elapsed_secs + remaining_secs; + + let estimated_marker = if is_estimated { "*" } else { "" }; + println!( + "Epoch Completed Time: {}{}/{} ({} remaining)", + format_duration_secs(elapsed_secs), + estimated_marker, + format_duration_secs(total_secs), + format_duration_secs(remaining_secs) + ); + } + } + + Ok(()) +} + +/// Format seconds into human-readable duration (e.g., "1day 5h 49m 8s") +fn format_duration_secs(total_seconds: u64) -> String { + let seconds = total_seconds % 60; + let total_minutes = total_seconds / 60; + let minutes = total_minutes % 60; + let total_hours = total_minutes / 60; + let hours = total_hours % 24; + let days = total_hours / 24; + + let mut parts = Vec::new(); + if days > 0 { + parts.push(format!("{}day", days)); + } + if hours > 0 { + parts.push(format!("{}h", hours)); + } + if minutes > 0 { + parts.push(format!("{}m", minutes)); + } + if seconds > 0 || parts.is_empty() { + parts.push(format!("{}s", seconds)); + } + + parts.join(" ") +} + +fn logs_subscribe( + cfg_override: &ConfigOverride, + include_votes: bool, + address: Option>, +) -> Result<()> { + let (cluster_url, _wallet_path) = get_cluster_and_wallet(cfg_override)?; + + // Convert HTTP(S) URL to WebSocket URL + let ws_url = if cluster_url.contains("localhost") || cluster_url.contains("127.0.0.1") { + // Parse the URL to extract and increment the port + cluster_url + .replace("https://", "wss://") + .replace("http://", "ws://") + .replace(":8899", ":8900") // Default test validator ports + } else { + cluster_url + .replace("https://", "wss://") + .replace("http://", "ws://") + }; + + println!("Connecting to {}", ws_url); + + let filter = match (include_votes, address) { + (true, Some(address)) => { + RpcTransactionLogsFilter::Mentions(address.iter().map(|p| p.to_string()).collect()) + } + (true, None) => RpcTransactionLogsFilter::AllWithVotes, + (false, Some(address)) => { + RpcTransactionLogsFilter::Mentions(address.iter().map(|p| p.to_string()).collect()) + } + (false, None) => RpcTransactionLogsFilter::All, + }; + + let (_client, receiver) = PubsubClient::logs_subscribe( + &ws_url, + filter, + RpcTransactionLogsConfig { + commitment: cfg_override.commitment.map(|c| CommitmentConfig { + commitment: c.into(), + }), + }, + )?; + + loop { + match receiver.recv() { + Ok(logs) => { + println!("Transaction executed in slot {}:", logs.context.slot); + println!(" Signature: {}", logs.value.signature); + println!( + " Status: {}", + logs.value + .err + .map(|err| err.to_string()) + .unwrap_or_else(|| "Ok".to_string()) + ); + println!(" Log Messages:"); + for log in logs.value.logs { + println!(" {log}"); + } + } + Err(err) => { + return Err(anyhow!("Disconnected: {err}")); + } + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -4125,6 +4960,7 @@ mod tests { &ConfigOverride { cluster: None, wallet: None, + commitment: None, }, "await".to_string(), true, @@ -4145,6 +4981,7 @@ mod tests { &ConfigOverride { cluster: None, wallet: None, + commitment: None, }, "fn".to_string(), true, @@ -4165,6 +5002,7 @@ mod tests { &ConfigOverride { cluster: None, wallet: None, + commitment: None, }, "1project".to_string(), true, diff --git a/cli/src/program.rs b/cli/src/program.rs new file mode 100644 index 0000000000..d8c87e78d0 --- /dev/null +++ b/cli/src/program.rs @@ -0,0 +1,1987 @@ +use anchor_lang_idl::types::Idl; +use anyhow::{anyhow, bail, Result}; +use solana_client::send_and_confirm_transactions_in_parallel::{ + send_and_confirm_transactions_in_parallel_blocking_v2, SendAndConfirmConfigV2, +}; +use solana_commitment_config::CommitmentConfig; +use solana_keypair::Keypair; +use solana_loader_v3_interface::{ + instruction as loader_v3_instruction, state::UpgradeableLoaderState, +}; +use solana_message::{Hash, Message}; +use solana_packet::PACKET_DATA_SIZE; +use solana_pubkey::Pubkey; +use solana_rpc_client::rpc_client::RpcClient; +use solana_rpc_client_api::config::RpcSendTransactionConfig; +use solana_sdk_ids::bpf_loader_upgradeable as bpf_loader_upgradeable_id; +use solana_signature::Signature; +use solana_signer::{EncodableKey, Signer}; +use solana_transaction::Transaction; +use std::{ + fs::{self, File}, + io::Write, + path::Path, + sync::Arc, + thread, + time::Duration, +}; + +use crate::{ + config::{Config, Manifest, Program, WithPath}, + ConfigOverride, ProgramCommand, +}; + +/// Parse priority fee from solana args +fn parse_priority_fee_from_args(args: &[String]) -> Option { + args.windows(2) + .find(|pair| pair[0] == "--with-compute-unit-price") + .and_then(|pair| pair[1].parse().ok()) +} + +/// Calculate the IDL account address for a program +fn idl_account_address(program_id: &Pubkey) -> Pubkey { + let program_signer = Pubkey::find_program_address(&[], program_id).0; + Pubkey::create_with_seed(&program_signer, "anchor:idl", program_id) + .expect("Seed is always valid") +} + +/// Discover Solana programs from a non-Anchor Cargo workspace +pub fn discover_solana_programs(program_name: Option) -> Result> { + let current_dir = std::env::current_dir()?; + let mut program_paths = Vec::new(); + + // Check if current directory has Cargo.toml + let cargo_toml_path = current_dir.join("Cargo.toml"); + if cargo_toml_path.exists() { + let cargo_content = fs::read_to_string(&cargo_toml_path)?; + let cargo_toml: toml::Value = toml::from_str(&cargo_content)?; + + // Check if it's a workspace Cargo.toml + if let Some(workspace) = cargo_toml.get("workspace") { + // It's a workspace - iterate over members + if let Some(members) = workspace.get("members").and_then(|m| m.as_array()) { + for member in members { + if let Some(member_path) = member.as_str() { + let full_path = current_dir.join(member_path); + if full_path.is_dir() && full_path.join("Cargo.toml").exists() { + program_paths.push(full_path); + } + } + } + } + } else if is_solana_program(¤t_dir)? { + // It's a single program Cargo.toml with cdylib - use current directory + program_paths.push(current_dir.clone()); + } + } + + // If no programs found yet, fallback to looking in programs/ directory + if program_paths.is_empty() { + let programs_dir = current_dir.join("programs"); + if programs_dir.is_dir() { + for entry in fs::read_dir(programs_dir)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() && path.join("Cargo.toml").exists() { + program_paths.push(path); + } + } + } + } + + // Filter to only Solana programs and build Program structs + let mut programs = Vec::new(); + for path in program_paths { + if !is_solana_program(&path)? { + continue; + } + + let cargo = Manifest::from_path(path.join("Cargo.toml"))?; + let lib_name = cargo.lib_name()?; + + // Check if this is the program we're looking for (if name specified) + if let Some(ref name) = program_name { + let matches = *name == lib_name || *name == path.file_name().unwrap().to_str().unwrap(); + if !matches { + continue; + } + } + + // Try to read IDL if it exists (will be None for non-Anchor programs) + let idl_filepath = current_dir + .join("target") + .join("idl") + .join(&lib_name) + .with_extension("json"); + let idl = fs::read(idl_filepath) + .ok() + .and_then(|bytes| serde_json::from_reader(&*bytes).ok()); + + programs.push(Program { + lib_name, + path: path.canonicalize()?, + idl, + }); + } + + Ok(programs) +} + +/// Check if a given Cargo project is a Solana program +/// A deployable Solana program must have crate-type = ["cdylib", ...] +fn is_solana_program(path: &Path) -> Result { + let cargo_path = path.join("Cargo.toml"); + if !cargo_path.exists() { + return Ok(false); + } + + let cargo_content = fs::read_to_string(&cargo_path)?; + let cargo_toml: toml::Value = toml::from_str(&cargo_content)?; + + // Check if it has cdylib (required for deployable Solana programs) + // This is the definitive marker - libraries and client tools won't have this + if let Some(lib) = cargo_toml.get("lib") { + if let Some(crate_type) = lib.get("crate-type").and_then(|ct| ct.as_array()) { + if crate_type.iter().any(|ct| ct.as_str() == Some("cdylib")) { + return Ok(true); + } + } + } + + Ok(false) +} + +/// Get programs from workspace (Anchor or non-Anchor) +pub fn get_programs_from_workspace( + cfg_override: &ConfigOverride, + program_name: Option, +) -> Result> { + // First try Anchor workspace + if let Some(cfg) = Config::discover(cfg_override)? { + return cfg.get_programs(program_name); + } + + // Fallback to non-Anchor Solana workspace + let programs = discover_solana_programs(program_name.clone())?; + + if programs.is_empty() { + if let Some(name) = program_name { + return Err(anyhow!( + "Program '{}' not found. Make sure you're in a Solana workspace (Anchor or non-Anchor) with programs in the programs/ directory, or provide a program filepath.", + name + )); + } else { + return Err(anyhow!( + "No Solana programs found. Make sure you're in a Solana workspace (Anchor or non-Anchor) with programs in the programs/ directory, or provide a program filepath." + )); + } + } + + Ok(programs) +} + +/// Public entry point for deploying programs - validates and routes to appropriate handler +#[allow(clippy::too_many_arguments)] +pub fn process_deploy( + cfg_override: &ConfigOverride, + program_filepath: Option, + program_name: Option, + program_keypair: Option, + upgrade_authority: Option, + program_id: Option, + buffer: Option, + max_len: Option, + verifiable: bool, + no_idl: bool, + make_final: bool, + solana_args: Vec, +) -> Result<()> { + // If explicit filepath provided, deploy single program + if program_filepath.is_some() { + return program_deploy( + cfg_override, + program_filepath, + program_name, + program_keypair, + upgrade_authority, + program_id, + buffer, + max_len, + no_idl, + make_final, + solana_args, + ); + } + + // Discover from workspace (Anchor or non-Anchor) + let programs = get_programs_from_workspace(cfg_override, program_name.clone())?; + + // Multiple programs and no specific program requested -> deploy all + if programs.len() > 1 && program_name.is_none() { + // Validate that single-program options aren't used + if program_id.is_some() { + return Err(anyhow!( + "Cannot specify --program-id when deploying multiple programs. Use --program-name to deploy a specific program." + )); + } + if buffer.is_some() { + return Err(anyhow!( + "Cannot specify --buffer when deploying multiple programs. Use --program-name to deploy a specific program." + )); + } + if upgrade_authority.is_some() { + return Err(anyhow!( + "Cannot specify --upgrade-authority when deploying multiple programs. Use --program-name to deploy a specific program." + )); + } + if max_len.is_some() { + return Err(anyhow!( + "Cannot specify --max-len when deploying multiple programs. Use --program-name to deploy a specific program." + )); + } + + // Delegate to deploy_workspace + return deploy_workspace( + cfg_override, + None, // program_name - deploy all + program_keypair, + verifiable, + no_idl, + make_final, + solana_args, + ); + } + + // Single program or specific program requested -> deploy single + program_deploy( + cfg_override, + program_filepath, + program_name, + program_keypair, + upgrade_authority, + program_id, + buffer, + max_len, + no_idl, + make_final, + solana_args, + ) +} + +/// Deploy all programs in workspace using native implementation +fn deploy_workspace( + cfg_override: &ConfigOverride, + program_name: Option, + program_keypair: Option, + verifiable: bool, + no_idl: bool, + make_final: bool, + solana_args: Vec, +) -> Result<()> { + // Get programs from workspace (Anchor or non-Anchor) + let programs = get_programs_from_workspace(cfg_override, program_name.clone())?; + + // For Cargo workspaces, we don't have cluster/wallet in config, so just print basic info + if let Ok(Some(cfg)) = Config::discover(cfg_override) { + // Anchor workspace - we have cluster/wallet config + let url = crate::cluster_url(&cfg, &cfg.test_validator); + let keypair = cfg.provider.wallet.to_string(); + println!("Deploying cluster: {url}"); + println!("Upgrade authority: {keypair}"); + } else { + // Cargo workspace - cluster/wallet will come from Solana CLI config or flags + println!("Deploying programs from Cargo workspace"); + } + + for program in programs { + let binary_path = program.binary_path(verifiable); + + println!("\nDeploying program: {}", program.lib_name); + + let program_keypair_filepath = match &program_keypair { + Some(path) => Some(path.clone()), + None => { + // Try to find program keypair + let keypair_path = program + .keypair_file() + .ok() + .map(|kp| kp.path().display().to_string()); + keypair_path + } + }; + + // Use the native program_deploy implementation + program_deploy( + cfg_override, + Some(binary_path.display().to_string()), + None, // program_name - not needed since we have filepath + program_keypair_filepath, + None, // upgrade_authority - uses wallet + None, // program_id - derived from keypair + None, // buffer + None, // max_len + no_idl, + make_final, + solana_args.clone(), + )?; + } + + println!("\nDeploy success"); + Ok(()) +} + +// Main entry point for all program commands +pub fn program(cfg_override: &ConfigOverride, cmd: ProgramCommand) -> Result<()> { + match cmd { + ProgramCommand::Deploy { + program_filepath, + program_name, + program_keypair, + upgrade_authority, + program_id, + buffer, + max_len, + no_idl, + make_final, + solana_args, + } => process_deploy( + cfg_override, + program_filepath, + program_name, + program_keypair, + upgrade_authority, + program_id, + buffer, + max_len, + false, // verifiable + no_idl, + make_final, + solana_args, + ), + ProgramCommand::WriteBuffer { + program_filepath, + program_name, + buffer, + buffer_authority, + max_len, + } => program_write_buffer( + cfg_override, + program_filepath, + program_name, + buffer, + buffer_authority, + max_len, + ), + ProgramCommand::SetBufferAuthority { + buffer, + new_buffer_authority, + } => program_set_buffer_authority(cfg_override, buffer, new_buffer_authority), + ProgramCommand::SetUpgradeAuthority { + program_id, + new_upgrade_authority, + new_upgrade_authority_signer, + skip_new_upgrade_authority_signer_check, + make_final, + upgrade_authority, + } => program_set_upgrade_authority( + cfg_override, + program_id, + new_upgrade_authority, + new_upgrade_authority_signer, + skip_new_upgrade_authority_signer_check, + make_final, + upgrade_authority, + ), + ProgramCommand::Show { + account, + get_programs, + get_buffers, + all, + } => program_show(cfg_override, account, get_programs, get_buffers, all), + ProgramCommand::Upgrade { + program_id, + program_filepath, + program_name, + buffer, + upgrade_authority, + max_retries, + solana_args, + } => program_upgrade( + cfg_override, + program_id, + program_filepath, + program_name, + buffer, + upgrade_authority, + max_retries, + solana_args, + ), + ProgramCommand::Dump { + account, + output_file, + } => program_dump(cfg_override, account, output_file), + ProgramCommand::Close { + account, + program_name, + authority, + recipient, + bypass_warning, + } => program_close( + cfg_override, + account, + program_name, + authority, + recipient, + bypass_warning, + ), + ProgramCommand::Extend { + program_id, + program_name, + additional_bytes, + } => program_extend(cfg_override, program_id, program_name, additional_bytes), + } +} + +fn get_rpc_client_and_config( + cfg_override: &ConfigOverride, +) -> Result<(RpcClient, Option>)> { + // Try to discover Anchor config first + let config = Config::discover(cfg_override)?; + + let (url, _wallet_path) = crate::get_cluster_and_wallet(cfg_override)?; + let rpc_client = RpcClient::new_with_commitment(url, CommitmentConfig::confirmed()); + + Ok((rpc_client, config)) +} + +/// Get payer keypair from either Anchor config or Solana CLI config +fn get_payer_keypair( + cfg_override: &ConfigOverride, + config: &Option>, +) -> Result { + if let Some(cfg) = config { + cfg.wallet_kp() + } else { + // No Anchor config - get wallet from Solana CLI config + let (_url, wallet_path) = crate::get_cluster_and_wallet(cfg_override)?; + Keypair::read_from_file(&wallet_path) + .map_err(|e| anyhow!("Failed to read wallet keypair from {}: {}", wallet_path, e)) + } +} + +/// Deploy a single program (either from explicit filepath or workspace) - private implementation +#[allow(clippy::too_many_arguments)] +pub fn program_deploy( + cfg_override: &ConfigOverride, + program_filepath: Option, + program_name: Option, + program_keypair: Option, + upgrade_authority: Option, + program_id: Option, + buffer: Option, + max_len: Option, + no_idl: bool, + make_final: bool, + solana_args: Vec, +) -> Result<()> { + let (rpc_client, config) = get_rpc_client_and_config(cfg_override)?; + let payer = get_payer_keypair(cfg_override, &config)?; + + // Determine the program filepath + let program_filepath = if let Some(filepath) = program_filepath { + // Explicit filepath provided + filepath + } else { + // Discover from workspace (Anchor or non-Anchor) + let programs = get_programs_from_workspace(cfg_override, program_name.clone())?; + + let program = &programs[0]; + let binary_path = program.binary_path(false); // false = not verifiable build + + println!("Deploying program: {}", program.lib_name); + + binary_path.display().to_string() + }; + + // Augment solana_args with recommended defaults (priority fees, max sign attempts, buffer) + let solana_args = crate::add_recommended_deployment_solana_args(&rpc_client, solana_args)?; + + // Parse priority fee from solana_args + let priority_fee = parse_priority_fee_from_args(&solana_args); + + // Read program data + let program_data = fs::read(&program_filepath) + .map_err(|e| anyhow!("Failed to read program file {}: {}", program_filepath, e))?; + + // Determine program keypair + let loaded_program_keypair = if let Some(keypair_path) = program_keypair { + // Load from specified keypair file + Keypair::read_from_file(&keypair_path).map_err(|e| { + anyhow!( + "Failed to read program keypair from {}: {}", + keypair_path, + e + ) + })? + } else if let Some(_program_id) = program_id { + return Err(anyhow!( + "When --program-id is specified, --program-keypair must also be provided" + )); + } else { + // Auto-detect from target/deploy/{program_name}-keypair.json + let program_name = Path::new(&program_filepath) + .file_stem() + .and_then(|s| s.to_str()) + .ok_or_else(|| anyhow!("Invalid program filepath"))?; + + let keypair_path = format!("target/deploy/{}-keypair.json", program_name); + Keypair::read_from_file(&keypair_path).map_err(|e| { + anyhow!( + "Failed to read program keypair from {}: {}. \ + Use --program-keypair to specify a custom location.", + keypair_path, + e + ) + })? + }; + + let program_id = loaded_program_keypair.pubkey(); + + // Determine upgrade authority + let upgrade_authority = if let Some(auth_path) = upgrade_authority { + let authority_keypair = Keypair::read_from_file(&auth_path) + .map_err(|e| anyhow!("Failed to read upgrade authority keypair: {}", e))?; + println!( + "Using custom upgrade authority: {}", + authority_keypair.pubkey() + ); + authority_keypair + } else { + payer.insecure_clone() + }; + + // Check if program already exists + let program_account = rpc_client.get_account(&program_id); + + if program_account.is_ok() { + // Program exists - validate it can be upgraded BEFORE writing buffer + println!("Program already exists, upgrading..."); + + // Verify program can be upgraded before doing expensive buffer write + verify_program_can_be_upgraded(&rpc_client, &program_id, &upgrade_authority)?; + + // Write to buffer + let buffer_pubkey = if let Some(buffer) = buffer { + buffer + } else { + let buffer_keypair = Keypair::new(); + write_program_buffer( + &rpc_client, + &payer, + &program_data, + &upgrade_authority.pubkey(), + &buffer_keypair, + max_len, + CommitmentConfig::confirmed(), + RpcSendTransactionConfig { + skip_preflight: false, + preflight_commitment: Some(CommitmentConfig::confirmed().commitment), + encoding: None, + max_retries: None, + min_context_slot: None, + }, + )? + }; + + // Upgrade the program (skip verification - already done above at line 324) + upgrade_program( + &rpc_client, + &payer, + &program_id, + &buffer_pubkey, + &upgrade_authority, + priority_fee, + true, // skip_program_verification + )?; + } else { + // New deployment + + let buffer_pubkey = if let Some(buffer) = buffer { + buffer + } else { + let buffer_keypair = Keypair::new(); + write_program_buffer( + &rpc_client, + &payer, + &program_data, + &upgrade_authority.pubkey(), + &buffer_keypair, + max_len, + CommitmentConfig::confirmed(), + RpcSendTransactionConfig { + skip_preflight: false, + preflight_commitment: Some(CommitmentConfig::confirmed().commitment), + encoding: None, + max_retries: None, + min_context_slot: None, + }, + )? + }; + + // Deploy from buffer + let max_data_len = max_len.unwrap_or(program_data.len()); + deploy_program( + &rpc_client, + &payer, + &buffer_pubkey, + &loaded_program_keypair, + &upgrade_authority, + max_data_len, + priority_fee, + )?; + } + + // Print the program ID + println!("Program ID: {}", program_id); + + // Deploy IDL if not skipped + if !no_idl { + // Extract program name from filepath + let program_name = Path::new(&program_filepath) + .file_stem() + .and_then(|s| s.to_str()) + .ok_or_else(|| anyhow!("Invalid program filepath"))?; + + // Look for IDL file in target/idl/{program_name}.json + let idl_filepath = format!("target/idl/{}.json", program_name); + + if Path::new(&idl_filepath).exists() { + // Read and update the IDL with the program address + let idl_content = fs::read_to_string(&idl_filepath) + .map_err(|e| anyhow!("Failed to read IDL file {}: {}", idl_filepath, e))?; + + let mut idl: Idl = serde_json::from_str(&idl_content) + .map_err(|e| anyhow!("Failed to parse IDL file {}: {}", idl_filepath, e))?; + + // Update the IDL with the program address + idl.address = program_id.to_string(); + + // Write the updated IDL back to the file + let idl_json = serde_json::to_string_pretty(&idl) + .map_err(|e| anyhow!("Failed to serialize IDL: {}", e))?; + fs::write(&idl_filepath, idl_json) + .map_err(|e| anyhow!("Failed to write IDL file {}: {}", idl_filepath, e))?; + + // Wait for the program to be confirmed before initializing IDL to prevent + // race condition where the program isn't yet available in validator cache + let max_retries = 5; + let retry_delay = Duration::from_millis(500); + let cache_delay = Duration::from_secs(2); + + for attempt in 0..max_retries { + if let Ok(account) = rpc_client.get_account(&program_id) { + if account.executable { + thread::sleep(cache_delay); + break; + } + } + + if attempt == max_retries - 1 { + println!("Failed"); + return Err(anyhow!( + "Timeout waiting for program {} to be confirmed", + program_id + )); + } + + thread::sleep(retry_delay); + } + + // Check if we're on localnet - skip IDL operations on localnet + let cluster_url = rpc_client.url(); + let is_localnet = + cluster_url.contains("localhost") || cluster_url.contains("127.0.0.1"); + + if is_localnet { + println!("Skipping IDL deployment on localnet"); + } else { + // Check if IDL account already exists + let idl_address = idl_account_address(&program_id); + let idl_account_exists = rpc_client.get_account(&idl_address).is_ok(); + + if idl_account_exists { + // IDL account exists, upgrade it + crate::idl_upgrade(cfg_override, program_id, idl_filepath, None)?; + } else { + // IDL account doesn't exist, create it + crate::idl_init(cfg_override, program_id, idl_filepath, None, false)?; + } + + println!("āœ“ Idl account created: {}", idl_address); + } + } else { + println!( + "Warning: IDL file not found at {}, skipping IDL deployment", + idl_filepath + ); + } + } + + // Make program immutable if --final flag is set + if make_final { + println!("\nMaking program immutable..."); + + let set_authority_ix = loader_v3_instruction::set_upgrade_authority( + &program_id, + &upgrade_authority.pubkey(), + None, // None = remove upgrade authority = immutable + ); + + let recent_blockhash = rpc_client.get_latest_blockhash()?; + let tx = Transaction::new_signed_with_payer( + &[set_authority_ix], + Some(&payer.pubkey()), + &[&payer, &upgrade_authority], + recent_blockhash, + ); + + rpc_client + .send_and_confirm_transaction(&tx) + .map_err(|e| anyhow!("Failed to make program immutable: {}", e))?; + + println!("āœ“ Program is now immutable (cannot be upgraded)"); + } + + Ok(()) +} + +/// Verify that a buffer account is valid for upgrading +fn verify_buffer_account( + rpc_client: &RpcClient, + buffer_pubkey: &Pubkey, + buffer_authority: &Pubkey, +) -> Result<()> { + let buffer_account = rpc_client + .get_account(buffer_pubkey) + .map_err(|e| anyhow!("Buffer account {} not found: {}", buffer_pubkey, e))?; + + // Check if buffer is owned by BPF Upgradeable Loader + if buffer_account.owner != bpf_loader_upgradeable_id::id() { + return Err(anyhow!( + "Buffer account {} is not owned by the BPF Upgradeable Loader", + buffer_pubkey + )); + } + + // Verify it's actually a Buffer account + match bincode::deserialize::(&buffer_account.data) { + Ok(UpgradeableLoaderState::Buffer { authority_address }) => { + // Check if buffer is immutable + if authority_address.is_none() { + return Err(anyhow!("Buffer {} is immutable", buffer_pubkey)); + } + // Verify the authority matches + if authority_address != Some(*buffer_authority) { + return Err(anyhow!( + "Buffer's authority {:?} does not match authority provided {}", + authority_address, + buffer_authority + )); + } + } + Ok(_) => { + return Err(anyhow!("Account {} is not a Buffer account", buffer_pubkey)); + } + Err(e) => { + return Err(anyhow!( + "Failed to deserialize buffer account {}: {}", + buffer_pubkey, + e + )); + } + } + + Ok(()) +} + +/// Verify that a program exists, is upgradeable, and the authority matches +/// This should be called BEFORE doing expensive operations like buffer writes +fn verify_program_can_be_upgraded( + rpc_client: &RpcClient, + program_id: &Pubkey, + upgrade_authority: &Keypair, +) -> Result<()> { + // Verify the program exists + let program_account = rpc_client + .get_account(program_id) + .map_err(|e| anyhow!("Failed to get program account {}: {}", program_id, e))?; + + if program_account.owner != bpf_loader_upgradeable_id::id() { + return Err(anyhow!( + "Program {} is not an upgradeable program", + program_id + )); + } + + // Check if this is a valid program and get the ProgramData address + let programdata_address = + match bincode::deserialize::(&program_account.data) { + Ok(UpgradeableLoaderState::Program { + programdata_address, + }) => programdata_address, + _ => { + return Err(anyhow!( + "{} is not an upgradeable program account", + program_id + )); + } + }; + + // Verify the ProgramData account exists and is valid + let programdata_account = rpc_client.get_account(&programdata_address).map_err(|e| { + anyhow!( + "Failed to get ProgramData account: {}. The program may have been closed.", + e + ) + })?; + + // Verify it's a valid ProgramData account + match bincode::deserialize::(&programdata_account.data) { + Ok(UpgradeableLoaderState::ProgramData { + upgrade_authority_address, + .. + }) => { + // Check if the program is immutable + if upgrade_authority_address.is_none() { + return Err(anyhow!( + "Program {} is immutable and cannot be upgraded", + program_id + )); + } + // Verify the authority matches + if upgrade_authority_address != Some(upgrade_authority.pubkey()) { + return Err(anyhow!( + "Upgrade authority mismatch. Expected {:?}, but ProgramData has {:?}", + Some(upgrade_authority.pubkey()), + upgrade_authority_address + )); + } + } + _ => { + return Err(anyhow!( + "Program {} has been closed or is in an invalid state", + program_id + )); + } + } + + Ok(()) +} + +#[allow(deprecated)] +fn deploy_program( + rpc_client: &RpcClient, + payer: &Keypair, + buffer: &Pubkey, + program_keypair: &Keypair, + upgrade_authority: &Keypair, + max_data_len: usize, + priority_fee: Option, +) -> Result<()> { + let program_id = program_keypair.pubkey(); + let mut deploy_ixs = loader_v3_instruction::deploy_with_max_program_len( + &payer.pubkey(), + &program_id, + buffer, + &upgrade_authority.pubkey(), + rpc_client + .get_minimum_balance_for_rent_exemption(UpgradeableLoaderState::size_of_program())?, + max_data_len, + ) + .map_err(|e| anyhow!("Failed to create deploy instruction: {}", e))?; + + // Add priority fee if specified + deploy_ixs = crate::prepend_compute_unit_ix(deploy_ixs, rpc_client, priority_fee)?; + + let recent_blockhash = rpc_client.get_latest_blockhash()?; + let deploy_tx = Transaction::new_signed_with_payer( + &deploy_ixs, + Some(&payer.pubkey()), + &[payer, program_keypair, upgrade_authority], + recent_blockhash, + ); + + rpc_client + .send_and_confirm_transaction(&deploy_tx) + .map_err(|e| anyhow!("Failed to deploy program: {}", e))?; + + Ok(()) +} + +fn upgrade_program( + rpc_client: &RpcClient, + payer: &Keypair, + program_id: &Pubkey, + buffer: &Pubkey, + upgrade_authority: &Keypair, + priority_fee: Option, + skip_program_verification: bool, +) -> Result<()> { + // Verify program can be upgraded (unless caller already verified) + if !skip_program_verification { + verify_program_can_be_upgraded(rpc_client, program_id, upgrade_authority)?; + } + + // Verify the buffer account is valid + verify_buffer_account(rpc_client, buffer, &upgrade_authority.pubkey())?; + + println!("Sending upgrade transaction..."); + + let upgrade_ix = loader_v3_instruction::upgrade( + program_id, + buffer, + &upgrade_authority.pubkey(), + &payer.pubkey(), + ); + + // Add priority fee if specified + let upgrade_ixs = crate::prepend_compute_unit_ix(vec![upgrade_ix], rpc_client, priority_fee)?; + + let recent_blockhash = rpc_client.get_latest_blockhash()?; + let upgrade_tx = Transaction::new_signed_with_payer( + &upgrade_ixs, + Some(&payer.pubkey()), + &[payer, upgrade_authority], + recent_blockhash, + ); + + let signature = rpc_client + .send_and_confirm_transaction(&upgrade_tx) + .map_err(|e| anyhow!("Failed to upgrade program: {}", e))?; + println!("Signature: {}", signature); + Ok(()) +} + +fn program_write_buffer( + cfg_override: &ConfigOverride, + program_filepath: Option, + program_name: Option, + _buffer: Option, + buffer_authority: Option, + max_len: Option, +) -> Result<()> { + let (rpc_client, config) = get_rpc_client_and_config(cfg_override)?; + let payer = get_payer_keypair(cfg_override, &config)?; + + // Determine the program filepath + let program_filepath = if let Some(filepath) = program_filepath { + filepath + } else { + // Discover from workspace (Anchor or non-Anchor) + let programs = get_programs_from_workspace(cfg_override, program_name.clone())?; + + if programs.len() > 1 && program_name.is_none() { + let program_names: Vec<_> = programs.iter().map(|p| p.lib_name.as_str()).collect(); + return Err(anyhow!( + "Multiple programs found: {}. Use --program-name to specify which one to write", + program_names.join(", ") + )); + } + + let program = &programs[0]; + let binary_path = program.binary_path(false); + + println!("Writing buffer for program: {}", program.lib_name); + + binary_path.display().to_string() + }; + + // Read program data + let program_data = fs::read(&program_filepath) + .map_err(|e| anyhow!("Failed to read program file {}: {}", program_filepath, e))?; + + // Determine buffer authority + let buffer_authority_keypair = if let Some(auth_path) = buffer_authority { + Keypair::read_from_file(&auth_path) + .map_err(|e| anyhow!("Failed to read buffer authority keypair: {}", e))? + } else { + payer.insecure_clone() + }; + + let buffer_keypair = Keypair::new(); + let buffer_pubkey = write_program_buffer( + &rpc_client, + &payer, + &program_data, + &buffer_authority_keypair.pubkey(), + &buffer_keypair, + max_len, + CommitmentConfig::confirmed(), + RpcSendTransactionConfig { + skip_preflight: false, + preflight_commitment: Some(CommitmentConfig::confirmed().commitment), + encoding: None, + max_retries: None, + min_context_slot: None, + }, + )?; + + println!("Buffer: {}", buffer_pubkey); + Ok(()) +} + +fn program_set_buffer_authority( + cfg_override: &ConfigOverride, + buffer: Pubkey, + new_buffer_authority: Pubkey, +) -> Result<()> { + let (rpc_client, config) = get_rpc_client_and_config(cfg_override)?; + let payer = get_payer_keypair(cfg_override, &config)?; + + println!("Setting buffer authority..."); + println!("Buffer: {}", buffer); + println!("New authority: {}", new_buffer_authority); + + let set_authority_ixs = loader_v3_instruction::set_buffer_authority( + &buffer, + &payer.pubkey(), + &new_buffer_authority, + ); + + let recent_blockhash = rpc_client.get_latest_blockhash()?; + let tx = Transaction::new_signed_with_payer( + &[set_authority_ixs], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + + rpc_client + .send_and_confirm_transaction(&tx) + .map_err(|e| anyhow!("Failed to set buffer authority: {}", e))?; + + println!("Buffer authority updated successfully!"); + Ok(()) +} + +fn program_set_upgrade_authority( + cfg_override: &ConfigOverride, + program_id: Pubkey, + new_upgrade_authority: Option, + new_upgrade_authority_signer: Option, + skip_new_upgrade_authority_signer_check: bool, + make_final: bool, + current_upgrade_authority: Option, +) -> Result<()> { + let (rpc_client, config) = get_rpc_client_and_config(cfg_override)?; + let payer = get_payer_keypair(cfg_override, &config)?; + + // Validate that this is a Program account, not ProgramData + let program_account = rpc_client + .get_account(&program_id) + .map_err(|e| anyhow!("Failed to get account {}: {}", program_id, e))?; + + if program_account.owner != bpf_loader_upgradeable_id::id() { + return Err(anyhow!( + "Account {} is not owned by the BPF Upgradeable Loader", + program_id + )); + } + + // Ensure this is a Program account, not ProgramData or Buffer + match bincode::deserialize::(&program_account.data) { + Ok(UpgradeableLoaderState::Program { .. }) => { + // Valid program account + } + Ok(UpgradeableLoaderState::ProgramData { .. }) => { + return Err(anyhow!( + "Error: {} is a ProgramData account, not a Program account.\n\n\ + To set the upgrade authority, you must provide the Program ID, not the ProgramData address.\n\ + Use 'anchor program show {}' to find the associated Program ID.", + program_id, + program_id + )); + } + Ok(UpgradeableLoaderState::Buffer { .. }) => { + return Err(anyhow!( + "{} is a Buffer account, not a Program account. Use set-buffer-authority for buffers.", + program_id + )); + } + _ => { + return Err(anyhow!("{} is not a valid upgradeable program", program_id)); + } + } + + println!("Setting upgrade authority..."); + println!("Program ID: {}", program_id); + + if make_final { + println!("Making program immutable (cannot be upgraded)"); + } else if let Some(new_auth) = new_upgrade_authority { + println!("New upgrade authority: {}", new_auth); + } else { + bail!("Must provide either --new-upgrade-authority or --final"); + } + + // Determine current authority keypair (must be a signer) + let current_authority_keypair = if let Some(auth_path) = current_upgrade_authority { + let keypair = Keypair::read_from_file(&auth_path) + .map_err(|e| anyhow!("Failed to read current upgrade authority keypair: {}", e))?; + println!("Using custom current authority: {}", keypair.pubkey()); + keypair + } else { + payer.insecure_clone() + }; + + // Validate signer requirements and load keypair + let new_auth_keypair_opt = if let Some(signer_path) = new_upgrade_authority_signer { + // Signer provided - use checked mode + let keypair = Keypair::read_from_file(&signer_path) + .map_err(|e| anyhow!("Failed to read new upgrade authority signer keypair: {}", e))?; + + // Verify the pubkey matches if both are provided + if let Some(pubkey) = new_upgrade_authority { + if pubkey != keypair.pubkey() { + return Err(anyhow!( + "New upgrade authority pubkey mismatch: --new-upgrade-authority ({}) \ + doesn't match --new-upgrade-authority-signer keypair ({})", + pubkey, + keypair.pubkey() + )); + } + } + + println!("Using CHECKED mode - both current and new authority will sign (recommended)"); + Some(keypair) + } else if new_upgrade_authority.is_some() && !make_final { + // No signer provided but new authority specified + if skip_new_upgrade_authority_signer_check { + // User explicitly allowed unchecked mode + println!("WARNING: Using UNCHECKED mode - only current authority will sign"); + println!(" This is less safe! The new authority won't verify ownership."); + None + } else { + // By default, require the signer for safety + return Err(anyhow!( + "New upgrade authority signer is required for safety.\n\ + Please provide --new-upgrade-authority-signer (recommended),\n\ + or use --skip-new-upgrade-authority-signer-check if you're confident the pubkey is correct." + )); + } + } else { + // Making program final or no new authority - no signer needed + None + }; + + // Build instruction based on mode + let set_authority_ixs = if let Some(ref new_auth_keypair) = new_auth_keypair_opt { + // Checked mode: both current and new authority sign (safer) + loader_v3_instruction::set_upgrade_authority_checked( + &program_id, + ¤t_authority_keypair.pubkey(), + &new_auth_keypair.pubkey(), + ) + } else { + // Unchecked mode or final mode: only current authority signs + loader_v3_instruction::set_upgrade_authority( + &program_id, + ¤t_authority_keypair.pubkey(), + new_upgrade_authority.as_ref(), + ) + }; + + let recent_blockhash = rpc_client.get_latest_blockhash()?; + + let signature = if let Some(ref new_auth_keypair) = new_auth_keypair_opt { + // Checked mode with 3 signers + let tx = Transaction::new_signed_with_payer( + &[set_authority_ixs], + Some(&payer.pubkey()), + &[&payer, ¤t_authority_keypair, new_auth_keypair], + recent_blockhash, + ); + rpc_client + .send_and_confirm_transaction(&tx) + .map_err(|e| anyhow!("Failed to set upgrade authority: {}", e))? + } else { + // Unchecked mode or final mode with 2 signers + let tx = Transaction::new_signed_with_payer( + &[set_authority_ixs], + Some(&payer.pubkey()), + &[&payer, ¤t_authority_keypair], + recent_blockhash, + ); + rpc_client + .send_and_confirm_transaction(&tx) + .map_err(|e| anyhow!("Failed to set upgrade authority: {}", e))? + }; + + println!(); + println!("Upgrade authority updated successfully!"); + println!("Signature: {}", signature); + Ok(()) +} + +fn program_show( + cfg_override: &ConfigOverride, + account: Pubkey, + _get_programs: bool, + _get_buffers: bool, + _all: bool, +) -> Result<()> { + let (rpc_client, _config) = get_rpc_client_and_config(cfg_override)?; + + let account_data = rpc_client + .get_account(&account) + .map_err(|e| anyhow!("Failed to get account {}: {}", account, e))?; + + println!("Account: {}", account); + println!("Owner: {}", account_data.owner); + println!("Balance: {} lamports", account_data.lamports); + println!("Data length: {} bytes", account_data.data.len()); + println!("Executable: {}", account_data.executable); + + // Try to parse as upgradeable loader state + if account_data.owner == bpf_loader_upgradeable_id::id() { + match bincode::deserialize::(&account_data.data) { + Ok(state) => match state { + UpgradeableLoaderState::Uninitialized => { + println!("Type: Uninitialized"); + } + UpgradeableLoaderState::Buffer { authority_address } => { + println!("Type: Buffer"); + if let Some(authority) = authority_address { + println!("Authority: {}", authority); + } else { + println!("Authority: None (immutable)"); + } + } + UpgradeableLoaderState::Program { + programdata_address, + } => { + println!("Type: Program"); + println!("Program Data Address: {}", programdata_address); + + // Fetch program data account + if let Ok(programdata_account) = rpc_client.get_account(&programdata_address) { + if let Ok(UpgradeableLoaderState::ProgramData { + slot, + upgrade_authority_address, + }) = bincode::deserialize::( + &programdata_account.data, + ) { + println!("Slot: {}", slot); + if let Some(authority) = upgrade_authority_address { + println!("Upgrade Authority: {}", authority); + } else { + println!("Upgrade Authority: None (immutable)"); + } + } + } + } + UpgradeableLoaderState::ProgramData { + slot, + upgrade_authority_address, + } => { + println!("Type: Program Data"); + println!("Slot: {}", slot); + if let Some(authority) = upgrade_authority_address { + println!("Upgrade Authority: {}", authority); + } else { + println!("Upgrade Authority: None (immutable)"); + } + } + }, + Err(e) => { + println!("Failed to parse as upgradeable loader state: {}", e); + } + } + } + + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +pub fn program_upgrade( + cfg_override: &ConfigOverride, + program_id: Pubkey, + program_filepath: Option, + program_name: Option, + buffer: Option, + upgrade_authority: Option, + max_retries: u32, + solana_args: Vec, +) -> Result<()> { + let (rpc_client, config) = get_rpc_client_and_config(cfg_override)?; + let payer = get_payer_keypair(cfg_override, &config)?; + + // Augment solana_args with recommended defaults if provided + let solana_args = if !solana_args.is_empty() { + crate::add_recommended_deployment_solana_args(&rpc_client, solana_args)? + } else { + solana_args + }; + + // Parse priority fee from solana_args + let priority_fee = parse_priority_fee_from_args(&solana_args); + + // Determine upgrade authority + let upgrade_authority_keypair = if let Some(auth_path) = upgrade_authority { + let keypair = Keypair::read_from_file(&auth_path) + .map_err(|e| anyhow!("Failed to read upgrade authority keypair: {}", e))?; + println!("Using custom upgrade authority: {}", keypair.pubkey()); + keypair + } else { + payer.insecure_clone() + }; + + // Verify the program can be upgraded BEFORE doing expensive operations + // This prevents wasting time/money on buffer writes if the program is closed or immutable + verify_program_can_be_upgraded(&rpc_client, &program_id, &upgrade_authority_keypair)?; + + // Case 1: Using existing buffer (no retries needed) + if let Some(buffer_pubkey) = buffer { + return upgrade_program( + &rpc_client, + &payer, + &program_id, + &buffer_pubkey, + &upgrade_authority_keypair, + priority_fee, + true, // skip_program_verification - already done above + ); + } + + // Case 2: Creating buffer from program file (with retries) + let program_filepath = if let Some(filepath) = program_filepath { + // Explicit filepath provided + filepath + } else { + // Discover from workspace (Anchor or non-Anchor) + let programs = get_programs_from_workspace(cfg_override, program_name.clone())?; + + let program = &programs[0]; + let binary_path = program.binary_path(false); // false = not verifiable build + + println!("Upgrading program: {}", program.lib_name); + + binary_path.display().to_string() + }; + + let program_data = fs::read(&program_filepath) + .map_err(|e| anyhow!("Failed to read program file {}: {}", program_filepath, e))?; + + // Retry loop for buffer creation and upgrade + for retry in 0..(1 + max_retries) { + if max_retries > 0 { + println!("\nAttempt {}/{}", retry + 1, max_retries + 1); + } + + // Create a new buffer for each attempt + let buffer_keypair = Keypair::new(); + + // Write to buffer + let result = write_program_buffer( + &rpc_client, + &payer, + &program_data, + &upgrade_authority_keypair.pubkey(), + &buffer_keypair, + None, // max_len + CommitmentConfig::confirmed(), + RpcSendTransactionConfig { + skip_preflight: false, + preflight_commitment: Some(CommitmentConfig::confirmed().commitment), + encoding: None, + max_retries: None, + min_context_slot: None, + }, + ); + + let buffer_pubkey = match result { + Ok(pubkey) => pubkey, + Err(e) => { + println!("Buffer write failed: {}", e); + if retry < max_retries { + println!("Retrying {} more time(s)...", max_retries - retry); + continue; + } else { + return Err(e); + } + } + }; + + // Upgrade the program (skip verification - already done before retry loop) + let result = upgrade_program( + &rpc_client, + &payer, + &program_id, + &buffer_pubkey, + &upgrade_authority_keypair, + priority_fee, + true, // skip_program_verification + ); + + match result { + Ok(_) => { + if max_retries > 0 { + println!("\nUpgrade success"); + } + return Ok(()); + } + Err(e) => { + println!("Upgrade failed: {}", e); + if retry < max_retries { + println!("Retrying {} more time(s)...", max_retries - retry); + } else { + return Err(e); + } + } + } + } + + Ok(()) +} + +fn program_dump(cfg_override: &ConfigOverride, account: Pubkey, output_file: String) -> Result<()> { + let (rpc_client, _config) = get_rpc_client_and_config(cfg_override)?; + + println!("Fetching program data..."); + + let account_data = rpc_client + .get_account(&account) + .map_err(|e| anyhow!("Failed to get account {}: {}", account, e))?; + + // Check if this is a program account + let program_data = if account_data.owner == bpf_loader_upgradeable_id::id() { + match bincode::deserialize::(&account_data.data) { + Ok(UpgradeableLoaderState::Program { + programdata_address, + }) => { + // Fetch the program data account + let programdata_account = rpc_client + .get_account(&programdata_address) + .map_err(|e| anyhow!("Failed to get program data account: {}", e))?; + + // Skip the UpgradeableLoaderState header + let data_offset = UpgradeableLoaderState::size_of_programdata_metadata(); + programdata_account.data[data_offset..].to_vec() + } + Ok(UpgradeableLoaderState::Buffer { .. }) => { + // Buffer account - skip the header + let data_offset = UpgradeableLoaderState::size_of_buffer_metadata(); + account_data.data[data_offset..].to_vec() + } + Ok(UpgradeableLoaderState::ProgramData { .. }) => { + // Program data account - skip the header + let data_offset = UpgradeableLoaderState::size_of_programdata_metadata(); + account_data.data[data_offset..].to_vec() + } + _ => account_data.data, + } + } else { + // Regular program or other account + account_data.data + }; + + println!("Writing {} bytes to {}...", program_data.len(), output_file); + + let mut file = + File::create(&output_file).map_err(|e| anyhow!("Failed to create output file: {}", e))?; + + file.write_all(&program_data) + .map_err(|e| anyhow!("Failed to write program data: {}", e))?; + + println!("Program dumped to {}", output_file); + Ok(()) +} + +fn program_close( + cfg_override: &ConfigOverride, + account: Option, + program_name: Option, + authority: Option, + recipient: Option, + bypass_warning: bool, +) -> Result<()> { + let (rpc_client, config) = get_rpc_client_and_config(cfg_override)?; + let payer = get_payer_keypair(cfg_override, &config)?; + + // Determine the account to close + let account = if let Some(acc) = account { + acc + } else if let Some(name) = program_name { + // Discover from workspace (Anchor or non-Anchor) + let programs = get_programs_from_workspace(cfg_override, Some(name.clone()))?; + + let program = &programs[0]; + + // Get the program keypair to derive program ID + let keypair_path = program.keypair_file()?.path().display().to_string(); + let program_keypair = Keypair::read_from_file(&keypair_path).map_err(|e| { + anyhow!( + "Failed to read program keypair from {}: {}", + keypair_path, + e + ) + })?; + + let program_id = program_keypair.pubkey(); + println!("Closing program: {} ({})", program.lib_name, program_id); + program_id + } else { + return Err(anyhow!( + "Must provide either account address or --program-name" + )); + }; + + // Fetch the account to determine its type + let account_data = rpc_client + .get_account(&account) + .map_err(|e| anyhow!("Failed to get account {}: {}", account, e))?; + + // Check if this is a BPF Loader Upgradeable account + if account_data.owner != bpf_loader_upgradeable_id::id() { + return Err(anyhow!( + "Account {} is not owned by the BPF Loader Upgradeable program", + account + )); + } + + // Determine which account to actually close + let (account_to_close, account_type, program_account) = + match bincode::deserialize::(&account_data.data) { + Ok(UpgradeableLoaderState::Program { + programdata_address, + }) => (programdata_address, "ProgramData", Some(account)), + Ok(UpgradeableLoaderState::Buffer { .. }) => (account, "Buffer", None), + Ok(UpgradeableLoaderState::ProgramData { .. }) => (account, "ProgramData", None), + _ => { + return Err(anyhow!( + "Account {} is not a Buffer, Program, or ProgramData account", + account + )); + } + }; + + // Determine authority + let authority_keypair = if let Some(auth_path) = authority { + Keypair::read_from_file(&auth_path) + .map_err(|e| anyhow!("Failed to read authority keypair: {}", e))? + } else { + payer.insecure_clone() + }; + + // Determine recipient + let recipient_pubkey = recipient.unwrap_or_else(|| authority_keypair.pubkey()); + + if !bypass_warning { + println!(); + println!( + "WARNING: This will close the {} account and reclaim all lamports.", + account_type + ); + + if account_type == "ProgramData" { + println!(); + println!( + "IMPORTANT: Closing the ProgramData account will make the program non-upgradeable" + ); + println!("and the program will become immutable. This action cannot be undone!"); + } + + println!(); + print!("Continue? (y/n): "); + std::io::Write::flush(&mut std::io::stdout())?; + + let mut input = String::new(); + std::io::stdin().read_line(&mut input)?; + + if !input.trim().eq_ignore_ascii_case("y") { + println!("Cancelled"); + return Ok(()); + } + } + + println!("Closing {} account...", account_type); + + let close_ixs = loader_v3_instruction::close_any( + &account_to_close, + &recipient_pubkey, + Some(&authority_keypair.pubkey()), + program_account.as_ref(), + ); + + let recent_blockhash = rpc_client.get_latest_blockhash()?; + let tx = Transaction::new_signed_with_payer( + &[close_ixs], + Some(&payer.pubkey()), + &[&payer, &authority_keypair], + recent_blockhash, + ); + + rpc_client + .send_and_confirm_transaction(&tx) + .map_err(|e| anyhow!("Failed to close account: {}", e))?; + + println!("{} account closed", account_type); + println!("Reclaimed lamports sent to: {}", recipient_pubkey); + Ok(()) +} + +fn program_extend( + cfg_override: &ConfigOverride, + program_id: Option, + program_name: Option, + additional_bytes: usize, +) -> Result<()> { + let (rpc_client, config) = get_rpc_client_and_config(cfg_override)?; + let payer = get_payer_keypair(cfg_override, &config)?; + + if additional_bytes == 0 { + return Err(anyhow!("Additional bytes must be greater than zero")); + } + + // Determine the program ID + let program_id = if let Some(id) = program_id { + id + } else if let Some(name) = program_name { + // Discover from workspace (Anchor or non-Anchor) + let programs = get_programs_from_workspace(cfg_override, Some(name.clone()))?; + + let program = &programs[0]; + + // Get the program keypair to derive program ID + let keypair_path = program.keypair_file()?.path().display().to_string(); + let program_keypair = Keypair::read_from_file(&keypair_path).map_err(|e| { + anyhow!( + "Failed to read program keypair from {}: {}", + keypair_path, + e + ) + })?; + + let program_id = program_keypair.pubkey(); + println!("Extending program: {} ({})", program.lib_name, program_id); + program_id + } else { + return Err(anyhow!("Must provide either program ID or --program-name")); + }; + + println!("Extending program data..."); + println!("Program ID: {}", program_id); + println!("Additional bytes: {}", additional_bytes); + + // Get the program account to find the ProgramData address + let program_account = rpc_client + .get_account(&program_id) + .map_err(|e| anyhow!("Failed to get program account {}: {}", program_id, e))?; + + if program_account.owner != bpf_loader_upgradeable_id::id() { + return Err(anyhow!( + "Account {} is not an upgradeable program", + program_id + )); + } + + // Get the ProgramData address + let programdata_address = + match bincode::deserialize::(&program_account.data) { + Ok(UpgradeableLoaderState::Program { + programdata_address, + }) => programdata_address, + _ => { + return Err(anyhow!( + "{} is not an upgradeable program account", + program_id + )); + } + }; + + // Get the ProgramData account to verify upgrade authority + let programdata_account = rpc_client + .get_account(&programdata_address) + .map_err(|e| anyhow!("Program {} is closed: {}", program_id, e))?; + + // Get the upgrade authority address + let upgrade_authority_address = + match bincode::deserialize::(&programdata_account.data) { + Ok(UpgradeableLoaderState::ProgramData { + upgrade_authority_address, + .. + }) => upgrade_authority_address, + _ => { + return Err(anyhow!("Program {} is closed", program_id)); + } + }; + + let upgrade_authority_address = upgrade_authority_address + .ok_or_else(|| anyhow!("Program {} is not upgradeable", program_id))?; + + // Verify the payer is the upgrade authority + if upgrade_authority_address != payer.pubkey() { + return Err(anyhow!( + "Upgrade authority mismatch. Expected {}, but ProgramData has {}", + payer.pubkey(), + upgrade_authority_address + )); + } + + // Use the checked version which requires upgrade authority signature + let extend_ix = loader_v3_instruction::extend_program_checked( + &program_id, + &upgrade_authority_address, + Some(&payer.pubkey()), + additional_bytes as u32, + ); + + let recent_blockhash = rpc_client.get_latest_blockhash()?; + let tx = Transaction::new_signed_with_payer( + &[extend_ix], + Some(&payer.pubkey()), + &[&payer], // payer is also the upgrade authority + recent_blockhash, + ); + + rpc_client + .send_and_confirm_transaction(&tx) + .map_err(|e| anyhow!("Failed to extend program: {}", e))?; + + println!("Program extended succesfully!"); + Ok(()) +} + +// ========== Agave's core parallel deployment functions ========== + +pub fn calculate_max_chunk_size(baseline_msg: Message) -> usize { + let tx_size = bincode::serialized_size(&Transaction { + signatures: vec![ + Signature::default(); + baseline_msg.header.num_required_signatures as usize + ], + message: baseline_msg, + }) + .unwrap() as usize; + // add 1 byte buffer to account for shortvec encoding + PACKET_DATA_SIZE.saturating_sub(tx_size).saturating_sub(1) +} + +#[allow(clippy::too_many_arguments)] +pub fn send_deploy_messages( + rpc_client: &RpcClient, + initial_message: Option, + write_messages: Vec, + final_message: Option, + fee_payer_signer: &dyn Signer, + initial_signer: Option<&dyn Signer>, + write_signer: Option<&dyn Signer>, + final_signers: Option<&[&dyn Signer]>, + max_sign_attempts: usize, + commitment: CommitmentConfig, + send_transaction_config: RpcSendTransactionConfig, +) -> Result> { + // Handle initial message (e.g., buffer creation) + if let Some(message) = initial_message { + if let Some(initial_signer) = initial_signer { + let mut initial_transaction = Transaction::new_unsigned(message.clone()); + let blockhash = rpc_client.get_latest_blockhash()?; + + // Sign based on number of required signatures + if message.header.num_required_signatures == 3 { + initial_transaction.try_sign( + &[fee_payer_signer, initial_signer, write_signer.unwrap()], + blockhash, + )?; + } else if message.header.num_required_signatures == 2 { + initial_transaction.try_sign(&[fee_payer_signer, initial_signer], blockhash)?; + } else { + initial_transaction.try_sign(&[fee_payer_signer], blockhash)?; + } + + rpc_client + .send_and_confirm_transaction_with_spinner_and_config( + &initial_transaction, + commitment, + send_transaction_config, + ) + .map_err(|err| anyhow!("Account allocation failed: {}", err))?; + } else { + return Err(anyhow!( + "Buffer account not created yet, must provide a key pair" + )); + } + } + + if !write_messages.is_empty() { + if let Some(write_signer) = write_signer { + send_messages_in_batches( + rpc_client, + &write_messages, + &[fee_payer_signer, write_signer], + max_sign_attempts, + commitment, + send_transaction_config, + )?; + } + } + + if let Some(message) = final_message { + if let Some(final_signers) = final_signers { + let mut final_tx = Transaction::new_unsigned(message); + let blockhash = rpc_client.get_latest_blockhash()?; + let mut signers = final_signers.to_vec(); + signers.push(fee_payer_signer); + final_tx.try_sign(&signers, blockhash)?; + + return Ok(Some( + rpc_client + .send_and_confirm_transaction_with_spinner_and_config( + &final_tx, + commitment, + send_transaction_config, + ) + .map_err(|e| anyhow!("Deploying program failed: {}", e))?, + )); + } + } + + Ok(None) +} + +/// Complete buffer writing implementation +#[allow(clippy::too_many_arguments)] +pub fn write_program_buffer( + rpc_client: &RpcClient, + payer: &dyn Signer, + program_data: &[u8], + buffer_authority: &Pubkey, + buffer_keypair: &dyn Signer, + max_len: Option, + commitment: CommitmentConfig, + send_transaction_config: RpcSendTransactionConfig, +) -> Result { + let buffer_pubkey = buffer_keypair.pubkey(); + + let program_len = program_data.len(); + let buffer_len = max_len.unwrap_or(program_len); + + // Calculate required lamports for buffer + let buffer_data_len = UpgradeableLoaderState::size_of_buffer(buffer_len); + let min_balance = rpc_client + .get_minimum_balance_for_rent_exemption(buffer_data_len) + .map_err(|e| anyhow!("Failed to get rent exemption: {}", e))?; + + // Get blockhash for all messages + let blockhash = rpc_client.get_latest_blockhash()?; + + // Create buffer initialization message + let initial_instructions = loader_v3_instruction::create_buffer( + &payer.pubkey(), + &buffer_pubkey, + buffer_authority, + min_balance, + buffer_len, + ) + .map_err(|e| anyhow!("Failed to create buffer instruction: {}", e))?; + + let initial_message = Some(Message::new_with_blockhash( + &initial_instructions, + Some(&payer.pubkey()), + &blockhash, + )); + + // Prepare all write messages upfront + let write_messages = prepare_write_messages( + program_data, + &buffer_pubkey, + buffer_authority, + &payer.pubkey(), + &blockhash, + ); + + const MAX_SIGN_ATTEMPTS: usize = 5; + send_deploy_messages( + rpc_client, + initial_message, + write_messages, + None, + payer, + Some(buffer_keypair), + Some(payer), + None, + MAX_SIGN_ATTEMPTS, + commitment, + send_transaction_config, + )?; + Ok(buffer_pubkey) +} + +/// Prepare write messages +fn prepare_write_messages( + program_data: &[u8], + buffer_pubkey: &Pubkey, + buffer_authority: &Pubkey, + fee_payer: &Pubkey, + blockhash: &Hash, +) -> Vec { + let create_msg = |offset: u32, bytes: Vec| { + let instruction = + loader_v3_instruction::write(buffer_pubkey, buffer_authority, offset, bytes); + Message::new_with_blockhash(&[instruction], Some(fee_payer), blockhash) + }; + + let mut write_messages = Vec::new(); + let chunk_size = calculate_max_chunk_size(create_msg(0, Vec::new())); + + for (chunk, i) in program_data.chunks(chunk_size).zip(0usize..) { + let offset = i.saturating_mul(chunk_size); + write_messages.push(create_msg(offset as u32, chunk.to_vec())); + } + + write_messages +} + +/// Send messages in parallel +fn send_messages_in_batches( + rpc_client: &RpcClient, + messages: &[Message], + signers: &[&dyn Signer], + max_sign_attempts: usize, + commitment: CommitmentConfig, + send_config: RpcSendTransactionConfig, +) -> Result<()> { + // Use parallel send and confirm function + // Create a new RpcClient with the same URL and wrap in Arc for parallel processing + let url = rpc_client.url(); + let new_rpc_client = RpcClient::new_with_commitment(url, commitment); + let rpc_client_arc = Arc::new(new_rpc_client); + + let transaction_errors = send_and_confirm_transactions_in_parallel_blocking_v2( + rpc_client_arc, + None, + messages, + signers, + SendAndConfirmConfigV2 { + resign_txs_count: Some(max_sign_attempts), + with_spinner: true, + rpc_send_transaction_config: send_config, + }, + ) + .map_err(|err| anyhow!("Data writes to account failed: {}", err))? + .into_iter() + .flatten() + .collect::>(); + + if !transaction_errors.is_empty() { + for transaction_error in &transaction_errors { + eprintln!("{:?}", transaction_error); + } + return Err(anyhow!( + "{} write transactions failed", + transaction_errors.len() + )); + } + + Ok(()) +}