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 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..6a8a60f3 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::{ + Address, BytesN, ContractExecutable, Env, Symbol, contract, contractimpl, symbol_short, +}; mod error; mod xlm; @@ -14,33 +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 + /// Set the admin. Call reset after deployment to fund and start the game. pub fn __constructor(env: &Env, admin: Address) { - // Require auth from the admin to make the transfer + // Require the chosen admin to authorize setting up the game. 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); - // Send the contract an amount of XLM to play with + // Remember the admin so later calls can check who is allowed to reset. + env.storage().instance().set(ADMIN_KEY, &admin); + } + + /// 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(); + // 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); } @@ -69,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_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 c4f75001..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,75 +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 contract_id = env.register_at(&contract_id, GuessTheNumber, (admin,)); - 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