From d66a8c29624ebb4ea98035d7441c52634a3dd636 Mon Sep 17 00:00:00 2001 From: Chad Ostrowski <221614+chadoh@users.noreply.github.com> Date: Thu, 22 Oct 2026 13:30:24 -0400 Subject: [PATCH 1/3] build: update deps: oz 0.8, soroban-sdk 28 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SDK 29/30 must wait: OpenZeppelin 0.8 requires SDK 28.x. The CLI’s SDK version isn’t itself a strict coupling because contracts compile separately, but 28 matches it anyway. Migrated the affected NFT API and guessing-game test setup. The deployed constructor’s arguments remain unchanged. Lockfile regeneration also reconciled the existing stellar-registry = "0.1.0" requirement to 0.1.1. Validation: • All 14 tests passed • All 3 Wasm contracts built • Clippy passed with existing style warnings • No whitespace errors Co-Authored-By: Warp --- Cargo.lock | 184 ++++++++++------------- Cargo.toml | 10 +- contracts/guess-the-number/Cargo.toml | 2 +- contracts/guess-the-number/src/lib.rs | 21 ++- contracts/guess-the-number/src/test.rs | 4 +- contracts/guess-the-number/src/xlm.rs | 11 +- contracts/nft-enumerable/src/contract.rs | 2 +- 7 files changed, 112 insertions(+), 122 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e69d1b47..c169fdbe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,15 +14,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "aho-corasick" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" -dependencies = [ - "memchr", -] - [[package]] name = "allocator-api2" version = "0.2.21" @@ -242,9 +233,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes-lit" -version = "0.0.5" +version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0adabf37211a5276e46335feabcbb1530c95eb3fdf85f324c7db942770aa025d" +checksum = "9b04f2b1d34cb428043f14aa4c853d14294532e8bbde3b6a3bc2faaaae31a1dd" dependencies = [ "num-bigint", "proc-macro2", @@ -355,6 +346,17 @@ dependencies = [ "serde_json", ] +[[package]] +name = "crate-git-revision" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54851b5b3f24621804b1cded2820975623c205e3055d2d44031cdb1237339ac8" +dependencies = [ + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -667,7 +669,7 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "fungible-allowlist-example" -version = "0.0.1" +version = "0.0.2" dependencies = [ "soroban-sdk", "stellar-access", @@ -712,11 +714,11 @@ dependencies = [ [[package]] name = "guess-the-number" -version = "0.0.1" +version = "0.0.2" dependencies = [ "soroban-sdk", "stellar-registry", - "stellar-xdr 26.0.1", + "stellar-xdr 28.0.0", ] [[package]] @@ -924,7 +926,7 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "nft-enumerable-example" -version = "0.0.1" +version = "0.0.2" dependencies = [ "soroban-sdk", "stellar-macros", @@ -1092,35 +1094,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "regex" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - [[package]] name = "rfc6979" version = "0.4.0" @@ -1304,9 +1277,9 @@ checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] name = "soroban-builtin-sdk-macros" -version = "26.1.4" +version = "28.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4a09f48a67357fdad682b601f4a2f3fdd8d897f3a97c2e1704fa22b0a474e0" +checksum = "86ffd329211a50add3965a8f4a8f70e24fc5c8268c945566ab963029b12dce40" dependencies = [ "itertools", "proc-macro2", @@ -1316,12 +1289,12 @@ dependencies = [ [[package]] name = "soroban-env-common" -version = "26.1.4" +version = "28.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ededa4063061c00886490bb3290e416ee0b03d7732a7378ccba7bb19211fb00c" +checksum = "610bb12ee937a56c2b629f8eb16efb7e47607cba4e79ab0c24132b9a53dd19a0" dependencies = [ "arbitrary", - "crate-git-revision", + "crate-git-revision 0.0.9", "ethnum", "num-derive", "num-traits", @@ -1329,15 +1302,15 @@ dependencies = [ "soroban-env-macros", "soroban-wasmi", "static_assertions", - "stellar-xdr 26.0.1", + "stellar-xdr 28.0.0", "wasmparser", ] [[package]] name = "soroban-env-guest" -version = "26.1.4" +version = "28.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ff2525f31e2453b52e90400a2952ce4618f8925bd03167938573e83c320c33f" +checksum = "7d383176b27bb286f718ab0ff5713bd9b01132089151266359bd9b20f38684cf" dependencies = [ "soroban-env-common", "static_assertions", @@ -1345,9 +1318,9 @@ dependencies = [ [[package]] name = "soroban-env-host" -version = "26.1.4" +version = "28.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "755806fedf2c948e882dff4bf69a8e844c9950b1d534abcadba4026150180274" +checksum = "36d7e5920c200939164cc58038224e2d90d7e2fabbfa505d6e2129d648dce7e2" dependencies = [ "ark-bls12-381", "ark-bn254", @@ -1382,24 +1355,24 @@ dependencies = [ [[package]] name = "soroban-env-macros" -version = "26.1.4" +version = "28.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e446ac7f005d89cb77cc4e2cc006377e1a8c970c9b05d11968a32f78e10e4ef4" +checksum = "077600bb175ea54592c1ac5983b8fb34ec9ab7a65f298af11d687b61e9d646a4" dependencies = [ "itertools", "proc-macro2", "quote", "serde", "serde_json", - "stellar-xdr 26.0.1", + "stellar-xdr 28.0.0", "syn 2.0.101", ] [[package]] name = "soroban-ledger-snapshot" -version = "26.1.1" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf43abc1e6219c45dd14e75e8abb848a6cf9830372febbe005fda1cbfb8c940f" +checksum = "b5caeb0a4c8af6d04dc1dfbf96199e0d50ae086c7d54c9001abe78bb891fc44f" dependencies = [ "serde", "serde_json", @@ -1411,13 +1384,13 @@ dependencies = [ [[package]] name = "soroban-sdk" -version = "26.1.1" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ce2a22afcbfc9407fe32ce789f441636d62cd078a4cc126191932b74b1e6c4" +checksum = "e4946477e75667215a25b652b0febb446485686c944d364c117c903e3fb98bab" dependencies = [ "arbitrary", "bytes-lit", - "crate-git-revision", + "crate-git-revision 0.0.9", "ctor", "derive_arbitrary", "ed25519-dalek", @@ -1435,9 +1408,9 @@ dependencies = [ [[package]] name = "soroban-sdk-macros" -version = "26.1.1" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6538ceb7d40c01499c2779b449a697c56c62a60bae6e5c5705d3a782075b5028" +checksum = "168f2d2c3fd4da57584c71e0a8315a8e656486495bc53e18c93b8ec4db085438" dependencies = [ "darling", "heck", @@ -1449,35 +1422,35 @@ dependencies = [ "soroban-env-common", "soroban-spec", "soroban-spec-rust", - "stellar-xdr 26.0.1", + "stellar-xdr 28.0.0", "syn 2.0.101", ] [[package]] name = "soroban-spec" -version = "26.1.1" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca18ced644f86e1e36a0fddb856d45a6a8478296e6b54941fb5e2daa96076bf7" +checksum = "2f48c8e183fc140ccd89e1fe1663cc605fb18641967c0bb591f64887599fde59" dependencies = [ "base64", "sha2", - "stellar-xdr 26.0.1", + "stellar-xdr 28.0.0", "thiserror 1.0.69", "wasmparser", ] [[package]] name = "soroban-spec-rust" -version = "26.1.1" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f3e3933a8ca588de7fb70460a48a5e0b043f43836aeb11b6aa0128143d95814" +checksum = "40cd0021887fd047e9f63cea36da35b202881bc0e0dbcc067a146137ca46f192" dependencies = [ "prettyplease", "proc-macro2", "quote", "sha2", "soroban-spec", - "stellar-xdr 26.0.1", + "stellar-xdr 28.0.0", "syn 2.0.101", "thiserror 1.0.69", ] @@ -1525,18 +1498,18 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "stellar-access" -version = "0.7.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aae4c50000565b08e97241a06cfb2cff4c3dfde12bdafa0b5303aca22139f38" +checksum = "8e5dc69bd3a6d260a5fba6541f0260d0bed3fbfcbb615d2fb00dc95bda94ceaf" dependencies = [ "soroban-sdk", ] [[package]] name = "stellar-build" -version = "0.0.7" +version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca65fe40f758802cff4bab7715e2e65b013cb196d715eda2180c5e3e9614f206" +checksum = "97fd5e3be902a9658fb9f642d4eef7837ab1d51e52dde9be1d9b218326499d97" dependencies = [ "cargo_metadata", "sha2", @@ -1546,27 +1519,27 @@ dependencies = [ [[package]] name = "stellar-contract-utils" -version = "0.7.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c45490c0ca2176bf9283445f59734ab304b17153f55fe9155de7f5cdffa4a85" +checksum = "642ca8d4cd845beebd617d51d0631dc3326914e33a4805d433d284d70e67015b" dependencies = [ "soroban-sdk", ] [[package]] name = "stellar-governance" -version = "0.7.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4828d07c8f3ae45483bdfd41e7e3aa43b4da3dbff1733d77c0f76bf247f01e6" +checksum = "4c341e8024a891522f4a61299cf7f714619a1b8c4e0c13ad61fae004f36a0f4a" dependencies = [ "soroban-sdk", ] [[package]] name = "stellar-macros" -version = "0.7.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a512f2975f47ab532537c7b3cc845bd5a39cab8f8c52c16388a26482f201883" +checksum = "a3b4e7bcf89602f2dd864df1b4ee43bf7b3321847f1ca82e382548f34a43bdc5" dependencies = [ "proc-macro2", "quote", @@ -1575,36 +1548,46 @@ dependencies = [ [[package]] name = "stellar-registry" -version = "0.0.11" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa6e1358b3d5ebdd0d835e5465c978646103a8c478594c716c273d6665ad3a1b" +checksum = "6f0f3bf39972776a5f538090604589e0dbd5c483c24ba1ca2ee10b3ca094c907" dependencies = [ - "stellar-scaffold-macro", + "stellar-registry-macro", ] [[package]] -name = "stellar-scaffold-macro" -version = "0.8.15" +name = "stellar-registry-macro" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41923f65b80378bfcec4124df5f560105e9e4d298872b2f03e2c7ba4eed26d48" +checksum = "7b5a33aefbd6f51ec5135369fca2746c7ecd59205f58f24badc195ce49952409" dependencies = [ "proc-macro2", "quote", - "regex", "sha2", "stellar-build", - "stellar-strkey 0.0.13", - "stellar-xdr 23.0.0", + "stellar-registry-name", + "stellar-strkey 0.0.16", + "stellar-xdr 27.0.0", "syn 2.0.101", ] +[[package]] +name = "stellar-registry-name" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb45025b1f475b3246651ecf4afef9e64c26c0adf27cc8a6d748e7a9c1f95169" +dependencies = [ + "semver", + "thiserror 2.0.19", +] + [[package]] name = "stellar-strkey" version = "0.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee1832fb50c651ad10f734aaf5d31ca5acdfb197a6ecda64d93fcdb8885af913" dependencies = [ - "crate-git-revision", + "crate-git-revision 0.0.6", "data-encoding", ] @@ -1614,16 +1597,16 @@ version = "0.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "084afcb0d458c3d5d5baa2d294b18f881e62cc258ef539d8fdf68be7dbe45520" dependencies = [ - "crate-git-revision", + "crate-git-revision 0.0.6", "data-encoding", "heapless", ] [[package]] name = "stellar-tokens" -version = "0.7.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c69421f52f418e0ebd343a6046aea3a3d1a4b0544debdf88780173d88c9d9b" +checksum = "f1e8c79407082324265a4334e7a8201ac4c5a04f25657fadbaed7116e68bb3b5" dependencies = [ "soroban-sdk", "stellar-contract-utils", @@ -1632,12 +1615,11 @@ dependencies = [ [[package]] name = "stellar-xdr" -version = "23.0.0" +version = "27.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d2848e1694b0c8db81fd812bfab5ea71ee28073e09ccc45620ef3cf7a75a9b" +checksum = "05ff843326969bdf1ef673dcdba94c08f4a3c8f1e58d6e6ef39b1bd4f749179a" dependencies = [ - "cfg_eval", - "crate-git-revision", + "crate-git-revision 0.0.6", "escape-bytes", "ethnum", "hex", @@ -1647,14 +1629,14 @@ dependencies = [ [[package]] name = "stellar-xdr" -version = "26.0.1" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6e29c7e1f071c2767916460d006668197843d5d93f0ec8893a26f72a14f595" +checksum = "f93d09ff8b9f919b084f664003c4c546ac66a76affd5429460dbe29f4b326f8e" dependencies = [ "arbitrary", "base64", "cfg_eval", - "crate-git-revision", + "crate-git-revision 0.0.9", "escape-bytes", "ethnum", "hex", diff --git a/Cargo.toml b/Cargo.toml index 801fa5df..b296dbe8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,12 +10,12 @@ repository = "https://github.com/stellar-scaffold/cli" version = "0.0.2" [workspace.dependencies] -soroban-sdk = "26.1.0" -stellar-access = "0.7.2" -stellar-macros = "0.7.2" -stellar-tokens = "0.7.2" +soroban-sdk = "28.0.0" +stellar-access = "0.8.0" +stellar-macros = "0.8.0" +stellar-tokens = "0.8.0" stellar-registry = "0.1.0" -stellar-xdr = "26.0.1" +stellar-xdr = "28.0.0" [profile.release] opt-level = "z" diff --git a/contracts/guess-the-number/Cargo.toml b/contracts/guess-the-number/Cargo.toml index ebf48d74..d529773c 100644 --- a/contracts/guess-the-number/Cargo.toml +++ b/contracts/guess-the-number/Cargo.toml @@ -20,5 +20,5 @@ soroban-sdk = { workspace = true } stellar-registry = { workspace = true } [dev-dependencies] -stellar-xdr = { workspace = true, features = ["curr", "serde"] } +stellar-xdr = { workspace = true, features = ["serde"] } soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/contracts/guess-the-number/src/lib.rs b/contracts/guess-the-number/src/lib.rs index d4558a4a..f4b232b2 100644 --- a/contracts/guess-the-number/src/lib.rs +++ b/contracts/guess-the-number/src/lib.rs @@ -1,5 +1,7 @@ #![no_std] -use soroban_sdk::{contract, contractimpl, symbol_short, Address, BytesN, Env, Symbol}; +use soroban_sdk::{ + contract, contractimpl, symbol_short, Address, BytesN, ContractExecutable, Env, Symbol, +}; mod error; mod xlm; @@ -15,11 +17,22 @@ pub const ADMIN_KEY: &Symbol = &symbol_short!("ADMIN"); #[contractimpl] impl GuessTheNumber { /// Constructor to initialize the contract with an admin and a random number + #[cfg(not(test))] pub fn __constructor(env: &Env, admin: Address) { - // Require auth from the admin to make the transfer admin.require_auth(); - // This is for testing purposes. Ensures that the XLM contract set up for unit testing and local network xlm::register(env, &admin); + Self::initialize(env, admin); + } + + // SDK test generators must run outside contract invocations. + #[cfg(test)] + pub fn __constructor(env: &Env, admin: Address, asset: Address) { + admin.require_auth(); + xlm::set_contract_id(env, &asset); + Self::initialize(env, admin); + } + + fn initialize(env: &Env, admin: Address) { // Send the contract an amount of XLM to play with xlm::token_client(env).transfer( &admin, @@ -81,7 +94,7 @@ impl GuessTheNumber { /// Upgrade the contract to new wasm. Only callable by admin. pub fn upgrade(env: &Env, new_wasm_hash: BytesN<32>) { Self::require_admin(env); - env.deployer().update_current_contract_wasm(new_wasm_hash); + env.deployer().update_current_contract(ContractExecutable::Wasm(new_wasm_hash)); } /// readonly function to get the current number diff --git a/contracts/guess-the-number/src/test.rs b/contracts/guess-the-number/src/test.rs index c4f75001..bc0ef6e7 100644 --- a/contracts/guess-the-number/src/test.rs +++ b/contracts/guess-the-number/src/test.rs @@ -130,7 +130,9 @@ fn reset_and_guess() { fn generate_client<'a>(env: &Env, admin: &Address) -> GuessTheNumberClient<'a> { let contract_id = Address::generate(env); env.mock_all_auths(); - let contract_id = env.register_at(&contract_id, GuessTheNumber, (admin,)); + let sac = env.register_stellar_asset_contract_v2(admin.clone()); + StellarAssetClient::new(env, &sac.address()).mint(admin, &xlm::to_stroops(10_000)); + let contract_id = env.register_at(&contract_id, GuessTheNumber, (admin, sac.address())); env.set_auths(&[]); // clear auths GuessTheNumberClient::new(env, &contract_id) } diff --git a/contracts/guess-the-number/src/xlm.rs b/contracts/guess-the-number/src/xlm.rs index 006693d9..b9cbaf1a 100644 --- a/contracts/guess-the-number/src/xlm.rs +++ b/contracts/guess-the-number/src/xlm.rs @@ -1,6 +1,5 @@ #[cfg(test)] mod xlm { - use super::*; const XLM_KEY: &soroban_sdk::Symbol = &soroban_sdk::symbol_short!("XLM"); pub fn contract_id(env: &soroban_sdk::Env) -> soroban_sdk::Address { @@ -10,14 +9,8 @@ mod xlm { .expect("XLM contract not initialized. Please deploy the XLM contract first.") } - pub fn register( - env: &soroban_sdk::Env, - admin: &soroban_sdk::Address, - ) -> soroban_sdk::testutils::StellarAssetContract { - let sac = env.register_stellar_asset_contract_v2(admin.clone()); - env.storage().instance().set(XLM_KEY, &sac.address()); - stellar_asset_client(env).mint(admin, &to_stroops(10_000)); - sac + pub fn set_contract_id(env: &soroban_sdk::Env, asset: &soroban_sdk::Address) { + env.storage().instance().set(XLM_KEY, asset); } #[allow(unused)] diff --git a/contracts/nft-enumerable/src/contract.rs b/contracts/nft-enumerable/src/contract.rs index 94b0a6e8..7aa66b09 100644 --- a/contracts/nft-enumerable/src/contract.rs +++ b/contracts/nft-enumerable/src/contract.rs @@ -30,7 +30,7 @@ impl ExampleContract { let owner: Address = e.storage().instance().get(&DataKey::Owner).expect("owner should be set"); owner.require_auth(); - Enumerable::sequential_mint(e, &to) + Enumerable::mint(e, &to) } } From 4dba15ad6fc54b969a98b67ef3da03573ed9b952 Mon Sep 17 00:00:00 2001 From: Chad Ostrowski <221614+chadoh@users.noreply.github.com> Date: Thu, 22 Oct 2026 15:14:43 -0400 Subject: [PATCH 2/3] refactor(contracts): fund guessing game rounds on reset Move the admin's 1 XLM contribution from construction to the admin-only reset method. Scaffold already calls reset after local deployment, so the normal flow still starts with a funded pot while subsequent resets replenish it. Remove add_funds to give this beginner template one clear way to fund a new round. SDK 28 prohibits test generators inside contract invocations. Deferring funding lets tests register the game first, then create and configure the test asset outside an invocation. This keeps the constructor interface identical in tests and production, without a test-only asset parameter. Choose the number during reset and fail explicitly before the first round. Restore teaching comments explaining no_std test setup, authorization, asset registration, contract-context storage, funding, and payouts. Readability for newcomers matters more than minimizing changed lines. Initialize the admin directly in the constructor, but retain set_admin as a useful administration-transfer endpoint instead of a setter that always panics after initialization. Require the currently stored admin's auth before replacing their address: the proposed new admin cannot appoint themselves, and the old admin loses authority once the transfer succeeds. Consolidate overlapping game tests while retaining pre-reset behavior, pot accumulation, and payouts across rounds. Check the public interface rather than private storage keys where possible. Keep explicit tests for nested-transfer authorization and insufficient-funds rollback, and add coverage that unauthorized admin transfers leave storage unchanged and reset permissions and funding move to the new admin. Rename and move the test asset helper module to eliminate the Clippy layout warnings. Validation: all 15 workspace tests pass on SDK 28/OpenZeppelin 0.8; workspace Clippy passes with -D warnings; the updated game Wasm builds. Co-Authored-By: Warp --- contracts/guess-the-number/src/lib.rs | 76 +++------ contracts/guess-the-number/src/test.rs | 219 +++++++++++++++---------- contracts/guess-the-number/src/xlm.rs | 82 ++++----- 3 files changed, 203 insertions(+), 174 deletions(-) diff --git a/contracts/guess-the-number/src/lib.rs b/contracts/guess-the-number/src/lib.rs index f4b232b2..6a8a60f3 100644 --- a/contracts/guess-the-number/src/lib.rs +++ b/contracts/guess-the-number/src/lib.rs @@ -1,6 +1,6 @@ #![no_std] use soroban_sdk::{ - contract, contractimpl, symbol_short, Address, BytesN, ContractExecutable, Env, Symbol, + Address, BytesN, ContractExecutable, Env, Symbol, contract, contractimpl, symbol_short, }; mod error; @@ -16,44 +16,30 @@ pub const ADMIN_KEY: &Symbol = &symbol_short!("ADMIN"); #[contractimpl] impl GuessTheNumber { - /// Constructor to initialize the contract with an admin and a random number - #[cfg(not(test))] + /// Set the admin. Call reset after deployment to fund and start the game. pub fn __constructor(env: &Env, admin: Address) { + // Require the chosen admin to authorize setting up the game. admin.require_auth(); - xlm::register(env, &admin); - Self::initialize(env, admin); + // Remember the admin so later calls can check who is allowed to reset. + env.storage().instance().set(ADMIN_KEY, &admin); } - // SDK test generators must run outside contract invocations. - #[cfg(test)] - pub fn __constructor(env: &Env, admin: Address, asset: Address) { + /// Add 1 XLM to the pot and choose a new number. Only callable by admin. + pub fn reset(env: &Env) { + let admin = Self::admin(env).expect("admin not set"); + // Only the admin can reset the game and authorize spending their XLM. admin.require_auth(); - xlm::set_contract_id(env, &asset); - Self::initialize(env, admin); - } - - fn initialize(env: &Env, admin: Address) { - // Send the contract an amount of XLM to play with + // Ensure the XLM contract exists on the network. Unit tests create + // their own asset in test setup, outside contract invocations. + #[cfg(not(test))] + xlm::register(env); + // Seed the pot with 1 XLM from the admin for players to win. xlm::token_client(env).transfer( &admin, env.current_contract_address(), &xlm::to_stroops(1), ); - // Set the admin in storage - Self::set_admin(env, admin); - // Set a random number between 1 and 10 - Self::reset_number(env); - } - - /// Update the number. Only callable by admin. - pub fn reset(env: &Env) { - Self::require_admin(env); - Self::reset_number(env); - } - - // Private function to reset the number to a new random value - // which doesn't require auth from the admin - fn reset_number(env: &Env) { + // Store a new number between 1 and 10 for this round. let new_number: u64 = env.prng().gen_range(1..=10); env.storage().instance().set(THE_NUMBER, &new_number); } @@ -82,41 +68,31 @@ impl GuessTheNumber { Ok(guessed_it) } - /// Admin can add more funds to the contract - pub fn add_funds(env: &Env, amount: i128) { - Self::require_admin(env); - let contract_address = env.current_contract_address(); - // unwrap here is safe because the admin was set in the constructor - let admin = Self::admin(env).unwrap(); - xlm::token_client(env).transfer(&admin, &contract_address, &amount); - } - /// Upgrade the contract to new wasm. Only callable by admin. pub fn upgrade(env: &Env, new_wasm_hash: BytesN<32>) { Self::require_admin(env); - env.deployer().update_current_contract(ContractExecutable::Wasm(new_wasm_hash)); + env.deployer() + .update_current_contract(ContractExecutable::Wasm(new_wasm_hash)); } /// readonly function to get the current number /// `pub(crate)` makes it accessible in the same crate, but not outside of it pub(crate) fn number(env: &Env) -> u64 { - // We can unwrap because the number is set in the constructor - // and then only reset by the admin - unsafe { env.storage().instance().get(THE_NUMBER).unwrap_unchecked() } + env.storage() + .instance() + .get(THE_NUMBER) + .expect("game not started; call reset first") } /// Get current admin pub fn admin(env: &Env) -> Option
{ env.storage().instance().get(ADMIN_KEY) } - - /// Set a new admin. Only callable by admin. - pub fn set_admin(env: &Env, admin: Address) { - // Check if admin is already set - if env.storage().instance().has(ADMIN_KEY) { - panic!("admin already set"); - } - env.storage().instance().set(ADMIN_KEY, &admin); + /// Transfer administration to a new address. Only callable by the current admin. + pub fn set_admin(env: &Env, new_admin: Address) { + // Check the stored admin's authorization before replacing their address. + Self::require_admin(env); + env.storage().instance().set(ADMIN_KEY, &new_admin); } /// Private helper function to require auth from the admin diff --git a/contracts/guess-the-number/src/test.rs b/contracts/guess-the-number/src/test.rs index bc0ef6e7..6e7cdabf 100644 --- a/contracts/guess-the-number/src/test.rs +++ b/contracts/guess-the-number/src/test.rs @@ -1,82 +1,135 @@ #![cfg(test)] -// This lets use reference types in the std library for testing +// Tests can use Rust's standard library even though the contract is no_std. extern crate std; use super::*; use soroban_sdk::{ + Address, Env, IntoVal, testutils::{Address as _, MockAuth, MockAuthInvoke}, - token::StellarAssetClient, - Address, Env, IntoVal, Val, Vec, + token::{StellarAssetClient, TokenClient}, }; fn init_test<'a>(env: &'a Env) -> (Address, StellarAssetClient<'a>, GuessTheNumberClient<'a>) { let admin = Address::generate(env); - let client = generate_client(env, &admin); - // This is needed because we want to call a function from within the context of the contract - // In this case we want to get the address of the XLM contract registered by the constructor - let sac_address = env.as_contract(&client.address, || xlm::contract_id(env)); - (admin, StellarAssetClient::new(env, &sac_address), client) + // Skip signature checks while preparing the test's accounts and balances. + // Tests that check authorization supply their own mocks afterward. + env.mock_all_auths(); + let contract_id = env.register(GuessTheNumber, (&admin,)); + let client = GuessTheNumberClient::new(env, &contract_id); + // Create the asset outside contract invocations, after the game exists. + let asset = xlm::register(env, &admin, &contract_id); + let sac = StellarAssetClient::new(env, &asset.address()); + // Mint a starting balance for the admin. On testnet, use Friendbot instead. + sac.mint(&admin, &xlm::to_stroops(10_000)); + // Clear setup's auth mocks so each test chooses what to authorize. + env.set_auths(&[]); + (admin, sac, client) } #[test] fn constructed_correctly() { let env = &Env::default(); let (admin, sac, client) = init_test(env); - // Check that the admin is set correctly assert_eq!(client.admin(), Some(admin.clone())); - // Check that the contract has a balance of 1 XLM - assert_eq!(sac.balance(&client.address), xlm::to_stroops(1)); - // Need to use `as_contract` to call a function in the context of the contract - // Since the method `number` is not in the client, but is visibile in the crate - let number = env.as_contract(&client.address, || GuessTheNumber::number(env)); - assert_eq!(number, 4); + // Deployment doesn't take funds or start a round; after-deploy calls reset. + assert_eq!(sac.balance(&admin), xlm::to_stroops(10_000)); + assert_eq!(sac.balance(&client.address), 0); + let alice = Address::generate(env); + assert!(client.try_guess(&4, &alice).is_err()); } #[test] fn only_admin_can_reset() { let env = &Env::default(); - let (admin, _, client) = init_test(env); + let (admin, sac, client) = init_test(env); let user = Address::generate(env); - set_caller(&client, "reset", &user, ()); + set_caller(&client, &user); assert!(client.try_reset().is_err()); + assert_eq!(sac.balance(&client.address), 0); + assert_eq!(sac.balance(&admin), xlm::to_stroops(10_000)); - set_caller(&client, "reset", &admin, ()); + set_caller(&client, &admin); assert!(client.try_reset().is_ok()); + assert_eq!(sac.balance(&client.address), xlm::to_stroops(1)); + assert_eq!(sac.balance(&admin), xlm::to_stroops(9_999)); +} + +#[test] +fn only_current_admin_can_transfer_administration() { + let env = &Env::default(); + let (admin, sac, client) = init_test(env); + let new_admin = Address::generate(env); + // Give the new admin enough XLM to fund a round after taking over. + env.mock_all_auths(); + sac.mint(&new_admin, &xlm::to_stroops(1)); + env.set_auths(&[]); + + // No authorization means no change to the stored admin. + assert!(client.try_set_admin(&new_admin).is_err()); + assert_eq!(client.admin(), Some(admin.clone())); + + let transfer = MockAuthInvoke { + contract: &client.address, + fn_name: "set_admin", + args: (&new_admin,).into_val(env), + sub_invokes: &[], + }; + // The proposed new admin cannot authorize their own appointment. + env.mock_auths(&[MockAuth { + address: &new_admin, + invoke: &transfer, + }]); + assert!(client.try_set_admin(&new_admin).is_err()); + assert_eq!(client.admin(), Some(admin.clone())); + + // Only the currently stored admin can authorize the transfer. + env.mock_auths(&[MockAuth { + address: &admin, + invoke: &transfer, + }]); + client.set_admin(&new_admin); + assert_eq!(client.admin(), Some(new_admin.clone())); + assert!(client.try_set_admin(&new_admin).is_err()); + assert_eq!(client.admin(), Some(new_admin.clone())); + + // Reset permissions move to the new admin, who now funds the pot. + set_caller(&client, &admin); + assert!(client.try_reset().is_err()); + set_caller(&client, &new_admin); + client.reset(); + assert_eq!(sac.balance(&client.address), xlm::to_stroops(1)); + assert_eq!(sac.balance(&new_admin), 0); + assert_eq!(sac.balance(&admin), xlm::to_stroops(10_000)); } #[test] fn guess() { let env = &Env::default(); let (_, sac, client) = init_test(env); - // This lets you mock all auth when they become complicated when making cross contract calls. + // Focus this test on gameplay, without specifying every nested token auth. env.mock_all_auths(); + client.reset(); - // Create a user to guess let alice = Address::generate(env); - // Mint tokens to the user. On testnet you use friendbot to fund the account. + // Give Alice enough to make a paid guess; Bob starts with no funds. sac.mint(&alice, &xlm::to_stroops(2)); - // Check that alice has the tokens - assert_eq!(sac.balance(&alice), xlm::to_stroops(2)); - - // Create another user with no funds let bob = Address::generate(env); - // In the testing enviroment the random seed is always the same initially. - // This tests a wrong guess so the balance should go down one XLM + // The first round's deterministic test number is 4. + // A wrong guess costs Alice 1 XLM, which is added to the prize pot. assert!(!client.guess(&3, &alice)); assert_eq!(sac.balance(&alice), xlm::to_stroops(1)); - - // Now we test a wrong guess but the user has no funds so we get an error + // Bob can't pay the fee, so the contract returns its custom transfer error. assert_eq!( client.try_guess(&3, &bob).unwrap_err(), Ok(Error::FailedToTransferFromGuesser) ); - // Now we test a correct guess, the balance should go up by the initial 1 XLM + the 1 XLM from the contract + // The winner receives the initial pot plus the previous wrong guess's fee. assert!(client.guess(&4, &alice)); assert_eq!(sac.balance(&alice), xlm::to_stroops(3)); - + // A second winning guess can't claim the same pot again. assert_eq!( client.try_guess(&4, &alice).unwrap_err(), Ok(Error::NoBalanceToTransfer) @@ -84,77 +137,71 @@ fn guess() { } #[test] -fn add_funds() { +fn reset_and_guess() { let env = &Env::default(); - let (_, sac, client) = init_test(env); - // This lets you mock all auth when they become complicated when making cross contract calls. + let (admin, sac, client) = init_test(env); env.mock_all_auths(); - // Create a user to guess + // Start the first round, just as scaffold.yml's after-deploy hook does. + client.reset(); let alice = Address::generate(env); - // Mint tokens to the user. On testnet you use friendbot to fund the account. - sac.mint(&alice, &xlm::to_stroops(2)); - // Now we test a correct guess, the balance should go up by the initial 1 XLM + the 1 XLM from the contract assert!(client.guess(&4, &alice)); - assert_eq!(sac.balance(&alice), xlm::to_stroops(3)); assert_eq!(sac.balance(&client.address), 0); - client.add_funds(&xlm::to_stroops(5)); - assert_eq!(sac.balance(&client.address), xlm::to_stroops(5)); - - // Since we didn't reset the number, the guess should still be correct - assert!(client.guess(&4, &alice)); - assert_eq!(sac.balance(&alice), xlm::to_stroops(8)); + // Reset replenishes the pot after a win and chooses a new number. + client.reset(); + assert_eq!(sac.balance(&client.address), xlm::to_stroops(1)); + assert_eq!(sac.balance(&admin), xlm::to_stroops(9_998)); + assert!(client.guess(&10, &alice)); + assert_eq!(sac.balance(&alice), xlm::to_stroops(2)); assert_eq!(sac.balance(&client.address), 0); + + // Resetting an unclaimed round adds to the pot instead of replacing it. + client.reset(); + client.reset(); + assert_eq!(sac.balance(&client.address), xlm::to_stroops(2)); + assert_eq!(sac.balance(&admin), xlm::to_stroops(9_996)); } #[test] -fn reset_and_guess() { +fn reset_without_funds_does_not_change_round() { let env = &Env::default(); - let (_, sac, client) = init_test(env); - // This lets you mock all auth when they become complicated when making cross contract calls. + let (admin, sac, client) = init_test(env); env.mock_all_auths(); - - // Create a user to guess - let alice = Address::generate(env); - // Mint tokens to the user. On testnet you use friendbot to fund the account. - sac.mint(&alice, &xlm::to_stroops(2)); - - // Reset the number client.reset(); + // as_contract lets test code read storage in the game's contract context. + let number = env.as_contract(&client.address, || GuessTheNumber::number(env)); + let recipient = Address::generate(env); + let token = TokenClient::new(env, &sac.address); + // Empty the admin's wallet so funding the next round will fail. + token.transfer(&admin, &recipient, &token.balance(&admin)); + // A failed transfer rolls back reset: the existing pot and number survive. - // Guess again, this should be correct now - assert!(client.guess(&10, &alice)); -} - -fn generate_client<'a>(env: &Env, admin: &Address) -> GuessTheNumberClient<'a> { - let contract_id = Address::generate(env); - env.mock_all_auths(); - let sac = env.register_stellar_asset_contract_v2(admin.clone()); - StellarAssetClient::new(env, &sac.address()).mint(admin, &xlm::to_stroops(10_000)); - let contract_id = env.register_at(&contract_id, GuessTheNumber, (admin, sac.address())); - env.set_auths(&[]); // clear auths - GuessTheNumberClient::new(env, &contract_id) + assert!(client.try_reset().is_err()); + assert_eq!(sac.balance(&client.address), xlm::to_stroops(1)); + assert_eq!( + env.as_contract(&client.address, || GuessTheNumber::number(env)), + number + ); } -// This lets you mock the auth context for a function call -fn set_caller(client: &GuessTheNumberClient, fn_name: &str, caller: &Address, args: T) -where - T: IntoVal>, -{ - // clear previous auth mocks - client.env.set_auths(&[]); - - let invoke = &MockAuthInvoke { - contract: &client.address, - fn_name, - args: args.into_val(&client.env), - sub_invokes: &[], - }; - - // mock auth as passed-in address - client.env.mock_auths(&[MockAuth { - address: &caller, - invoke, +// Authorize reset and its nested transfer, rather than mocking every caller. +fn set_caller(client: &GuessTheNumberClient, caller: &Address) { + let env = &client.env; + let asset = env.as_contract(&client.address, || xlm::contract_id(env)); + env.mock_auths(&[MockAuth { + address: caller, + invoke: &MockAuthInvoke { + contract: &client.address, + fn_name: "reset", + args: ().into_val(env), + // The admin authorizes both reset and the token transfer it calls. + sub_invokes: &[MockAuthInvoke { + contract: &asset, + fn_name: "transfer", + args: (caller, &client.address, xlm::to_stroops(1)).into_val(env), + sub_invokes: &[], + }], + }, }]); } diff --git a/contracts/guess-the-number/src/xlm.rs b/contracts/guess-the-number/src/xlm.rs index b9cbaf1a..18b85a68 100644 --- a/contracts/guess-the-number/src/xlm.rs +++ b/contracts/guess-the-number/src/xlm.rs @@ -1,5 +1,32 @@ +const ONE_XLM: i128 = 10_000_000; // 1 XLM in stroops + +pub const fn to_stroops(num: u64) -> i128 { + (num as i128) * ONE_XLM +} + +#[cfg(not(test))] +// Resolve the real network's XLM contract; tests use a separate asset below. +stellar_registry::import_asset!("xlm"); + +#[cfg(test)] +pub use test_asset::*; +#[cfg(not(test))] +pub use xlm::*; + +/// Ensure the native XLM asset contract exists before funding a round. +#[cfg(not(test))] +pub fn register(env: &soroban_sdk::Env) { + if token_client(env) + .try_balance(&env.current_contract_address()) + .is_err() + { + // XDR encoding of the native asset. + env.deployer().with_stellar_asset([0u8; 4]).deploy(); + } +} + #[cfg(test)] -mod xlm { +mod test_asset { const XLM_KEY: &soroban_sdk::Symbol = &soroban_sdk::symbol_short!("XLM"); pub fn contract_id(env: &soroban_sdk::Env) -> soroban_sdk::Address { @@ -9,45 +36,24 @@ mod xlm { .expect("XLM contract not initialized. Please deploy the XLM contract first.") } - pub fn set_contract_id(env: &soroban_sdk::Env, asset: &soroban_sdk::Address) { - env.storage().instance().set(XLM_KEY, asset); - } - - #[allow(unused)] - pub fn stellar_asset_client<'a>( + pub fn register( env: &soroban_sdk::Env, - ) -> soroban_sdk::token::StellarAssetClient<'a> { - soroban_sdk::token::StellarAssetClient::new(&env, &contract_id(env)) - } - /// Create a Stellar Asset Client for the asset which provides an admin interface - pub fn token_client<'a>(env: &soroban_sdk::Env) -> soroban_sdk::token::TokenClient<'a> { - soroban_sdk::token::TokenClient::new(&env, &contract_id(env)) + admin: &soroban_sdk::Address, + game: &soroban_sdk::Address, + ) -> soroban_sdk::testutils::StellarAssetContract { + // SDK test generators must run outside contract invocations. + // This is a mintable test asset, not native XLM. + let sac = env.register_stellar_asset_contract_v2(admin.clone()); + // Run the storage write in the game's context so its token client can + // find this asset when called from inside a contract method. + env.as_contract(game, || { + env.storage().instance().set(XLM_KEY, &sac.address()); + }); + sac } -} -const ONE_XLM: i128 = 1_000_000_0; // 1 XLM in stroops; - -pub const fn to_stroops(num: u64) -> i128 { - (num as i128) * ONE_XLM -} - -#[cfg(not(test))] -stellar_registry::import_asset!("xlm"); -#[allow(unused)] -pub const SERIALIZED_ASSET: [u8; 4] = [0, 0, 0, 0]; - -pub use xlm::*; -mod register { - - #[allow(unused)] - #[cfg(not(test))] - pub fn register(env: &soroban_sdk::Env, admin: &soroban_sdk::Address) { - let balance = super::token_client(env).try_balance(&env.current_contract_address()); - if balance.is_err() { - env.deployer().with_stellar_asset(super::SERIALIZED_ASSET).deploy(); - } + /// Create a client for the test asset stored in the game's instance. + pub fn token_client<'a>(env: &soroban_sdk::Env) -> soroban_sdk::token::TokenClient<'a> { + soroban_sdk::token::TokenClient::new(env, &contract_id(env)) } } - -#[allow(unused_imports)] -pub use register::*; \ No newline at end of file From ad87e1fbf6176ecd1ae6709441da84531a7286cf Mon Sep 17 00:00:00 2001 From: Chad Ostrowski <221614+chadoh@users.noreply.github.com> Date: Thu, 22 Oct 2026 16:08:22 -0400 Subject: [PATCH 3/3] fix(ci): pin a compatible Stellar local network Quickstart's floating latest image moved to Core/RPC 30 while its local network still defaults to ledger protocol 28. RPC 30's protocol-29/30 simulation hosts cannot simulate that ledger, causing deployment simulations for all three contracts to fail with Context/InternalError. Pin the Quickstart image and protocol to Scaffold CLI's v678-b1525.1-testing and protocol 29 settings. Scaffold cannot replace CI's already-running container on port 8000, so the workflow must start a compatible network itself. Log RPC version information and assert the ledger protocol before deployment to catch future drift early. Validated successful unsigned Wasm-upload simulations for all three contracts on protocol 29, preflight acceptance of protocol 29 and rejection of protocol 28, and workflow formatting and whitespace checks. Co-Authored-By: Warp --- .github/workflows/build.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 211f707c..d3dcbf8e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -119,6 +119,22 @@ jobs: uses: stellar/quickstart@main with: enable: "core,rpc" + # Match Scaffold CLI's local-network image and protocol. The floating + # latest image moved to RPC 30 but still defaults to ledger protocol + # 28, which its protocol-29/30 simulation hosts cannot simulate. + tag: v678-b1525.1-testing + protocol_version: "29" + - name: Check local network protocol + run: | + # A healthy RPC alone does not guarantee protocol compatibility. + curl --fail --silent --show-error \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","id":1,"method":"getVersionInfo"}' \ + http://localhost:8000/rpc | jq . + curl --fail --silent --show-error \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","id":1,"method":"getLatestLedger"}' \ + http://localhost:8000/rpc | jq -e '.result.protocolVersion == 29' - name: Add master account as default identity run: | mkdir -p .config/stellar/identity