From c21da2af586a746fdb5678a8adcb48cac2531631 Mon Sep 17 00:00:00 2001 From: Tobi Olusanya Date: Wed, 29 Jul 2026 23:29:49 +0100 Subject: [PATCH] feat: harden AnonVote contract with validation, error taxonomy, admin rotation, and test suite --- STABILITY.md | 35 + contracts/anonvote/Cargo.lock | 1765 ++++++++++++++++++++++++++ contracts/anonvote/src/errors.rs | 17 + contracts/anonvote/src/lib.rs | 521 ++------ contracts/anonvote/src/test.rs | 424 +++++++ contracts/anonvote/src/validation.rs | 21 + 6 files changed, 2365 insertions(+), 418 deletions(-) create mode 100644 STABILITY.md create mode 100644 contracts/anonvote/Cargo.lock create mode 100644 contracts/anonvote/src/errors.rs create mode 100644 contracts/anonvote/src/test.rs create mode 100644 contracts/anonvote/src/validation.rs diff --git a/STABILITY.md b/STABILITY.md new file mode 100644 index 0000000..2cd659e --- /dev/null +++ b/STABILITY.md @@ -0,0 +1,35 @@ +# Stability Guarantees + +## ContractError Discriminant Values + +The `ContractError` enum in `contracts/anonvote/src/errors.rs` defines error codes +returned by the AnonVote Soroban contract. Each variant has a fixed `#[repr(u32)]` +discriminant. + +**These discriminant values must never change after the contract is deployed.** +Consumers that parse error codes from Stellar transaction results depend on the +numeric value, not the variant name. Changing a value is a breaking change. + +| Variant | Value | Description | +|------------------------|-------|------------------------------------------| +| AlreadyInitialized | 1 | initialize called after admin is set | +| Unauthorized | 2 | caller is not the admin | +| BallotAlreadyExists | 3 | record_ballot called with existing hash | +| BallotNotFound | 4 | write op with unregistered ballot | +| BallotAlreadyFinalised | 5 | record_result after result is set | +| InvalidBallotIdHash | 6 | ballot_id_hash not valid 64-char hex | +| InvalidResultHash | 7 | result_hash not valid 64-char hex | +| InvalidAdminAddress | 8 | new admin address is zero or same | +| CounterOverflow | 9 | token/vote counter exceeds u32::MAX | +| BallotExpired | 10 | operation after ballot ledger expiry | + +## Storage Key Stability + +Storage keys used for instance and persistent storage are derived from the +`DataKey` enum variants. Variant names must not be changed or reordered after +deployment. + +## Event Topics + +Events published by the contract use fixed topic symbols. Changing topic +symbols is a breaking change for off-chain event indexers. diff --git a/contracts/anonvote/Cargo.lock b/contracts/anonvote/Cargo.lock new file mode 100644 index 0000000..4e07b43 --- /dev/null +++ b/contracts/anonvote/Cargo.lock @@ -0,0 +1,1765 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anonvote" +version = "0.1.0" +dependencies = [ + "soroban-sdk", +] + +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base32" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes-lit" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0adabf37211a5276e46335feabcbb1530c95eb3fdf85f324c7db942770aa025d" +dependencies = [ + "num-bigint", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crate-git-revision" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c521bf1f43d31ed2f73441775ed31935d77901cb3451e44b38a1c1612fcbaf98" +dependencies = [ + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", + "rand_core 0.10.1", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto 0.2.9", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "curve25519-dalek-derive", + "digest 0.11.3", + "fiat-crypto 0.3.0", + "rand_core 0.10.1", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid", + "crypto-common 0.1.6", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature 2.2.0", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature 2.2.0", +] + +[[package]] +name = "ed25519" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" +dependencies = [ + "signature 3.0.0", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek 4.1.3", + "ed25519 2.2.3", + "rand_core 0.6.4", + "serde", + "sha2 0.10.9", + "subtle", + "zeroize", +] + +[[package]] +name = "ed25519-dalek" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" +dependencies = [ + "curve25519-dalek 5.0.0", + "ed25519 3.0.0", + "rand_core 0.10.1", + "sha2 0.11.0", + "signature 3.0.0", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "escape-bytes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfcf67fea2815c2fc3b90873fae90957be12ff417335dfadc7f52927feb03b2" + +[[package]] +name = "ethnum" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40404c3f5f511ec4da6fe866ddf6a717c309fdbb69fbbad7b0f3edab8f2e835f" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "typenum", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "indexmap-nostd" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e04e2fd2b8188ea827b32ef11de88377086d690286ab35747ef7f9bf3ccb590" + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "sha2 0.10.9", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures 0.2.17", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[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.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "ref-cast" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "soroban-builtin-sdk-macros" +version = "21.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f57a68ef8777e28e274de0f3a88ad9a5a41d9a2eb461b4dd800b086f0e83b80" +dependencies = [ + "itertools", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "soroban-env-common" +version = "21.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1c89463835fe6da996318156d39f424b4f167c725ec692e5a7a2d4e694b3d" +dependencies = [ + "arbitrary", + "crate-git-revision", + "ethnum", + "num-derive", + "num-traits", + "serde", + "soroban-env-macros", + "soroban-wasmi", + "static_assertions", + "stellar-xdr", + "wasmparser", +] + +[[package]] +name = "soroban-env-guest" +version = "21.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bfb2536811045d5cd0c656a324cbe9ce4467eb734c7946b74410d90dea5d0ce" +dependencies = [ + "soroban-env-common", + "static_assertions", +] + +[[package]] +name = "soroban-env-host" +version = "21.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b7a32c28f281c423189f1298960194f0e0fc4eeb72378028171e556d8cd6160" +dependencies = [ + "backtrace", + "curve25519-dalek 5.0.0", + "ecdsa", + "ed25519-dalek 3.0.0", + "elliptic-curve", + "generic-array", + "getrandom", + "hex-literal", + "hmac", + "k256", + "num-derive", + "num-integer", + "num-traits", + "p256", + "rand", + "rand_chacha", + "sec1", + "sha2 0.10.9", + "sha3", + "soroban-builtin-sdk-macros", + "soroban-env-common", + "soroban-wasmi", + "static_assertions", + "stellar-strkey", + "wasmparser", +] + +[[package]] +name = "soroban-env-macros" +version = "21.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "242926fe5e0d922f12d3796cd7cd02dd824e5ef1caa088f45fce20b618309f64" +dependencies = [ + "itertools", + "proc-macro2", + "quote", + "serde", + "serde_json", + "stellar-xdr", + "syn 2.0.119", +] + +[[package]] +name = "soroban-ledger-snapshot" +version = "21.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6edf92749fd8399b417192d301c11f710b9cdce15789a3d157785ea971576fa" +dependencies = [ + "serde", + "serde_json", + "serde_with", + "soroban-env-common", + "soroban-env-host", + "thiserror", +] + +[[package]] +name = "soroban-sdk" +version = "21.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcdf04484af7cc731a7a48ad1d9f5f940370edeea84734434ceaf398a6b862e" +dependencies = [ + "arbitrary", + "bytes-lit", + "ctor", + "derive_arbitrary", + "ed25519-dalek 2.2.0", + "rand", + "rustc_version", + "serde", + "serde_json", + "soroban-env-guest", + "soroban-env-host", + "soroban-ledger-snapshot", + "soroban-sdk-macros", + "stellar-strkey", +] + +[[package]] +name = "soroban-sdk-macros" +version = "21.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0974e413731aeff2443f2305b344578b3f1ffd18335a7ba0f0b5d2eb4e94c9ce" +dependencies = [ + "crate-git-revision", + "darling 0.20.11", + "itertools", + "proc-macro2", + "quote", + "rustc_version", + "sha2 0.10.9", + "soroban-env-common", + "soroban-spec", + "soroban-spec-rust", + "stellar-xdr", + "syn 2.0.119", +] + +[[package]] +name = "soroban-spec" +version = "21.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2c70b20e68cae3ef700b8fa3ae29db1c6a294b311fba66918f90cb8f9fd0a1a" +dependencies = [ + "base64 0.13.1", + "stellar-xdr", + "thiserror", + "wasmparser", +] + +[[package]] +name = "soroban-spec-rust" +version = "21.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2dafbde981b141b191c6c036abc86097070ddd6eaaa33b273701449501e43d3" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "sha2 0.10.9", + "soroban-spec", + "stellar-xdr", + "syn 2.0.119", + "thiserror", +] + +[[package]] +name = "soroban-wasmi" +version = "0.31.1-soroban.20.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710403de32d0e0c35375518cb995d4fc056d0d48966f2e56ea471b8cb8fc9719" +dependencies = [ + "smallvec", + "spin", + "wasmi_arena", + "wasmi_core", + "wasmparser-nostd", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stellar-strkey" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12d2bf45e114117ea91d820a846fd1afbe3ba7d717988fee094ce8227a3bf8bd" +dependencies = [ + "base32", + "crate-git-revision", + "thiserror", +] + +[[package]] +name = "stellar-xdr" +version = "21.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2675a71212ed39a806e415b0dbf4702879ff288ec7f5ee996dda42a135512b50" +dependencies = [ + "arbitrary", + "base64 0.13.1", + "crate-git-revision", + "escape-bytes", + "hex", + "serde", + "serde_with", + "stellar-strkey", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "time" +version = "0.3.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasmi_arena" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "104a7f73be44570cac297b3035d76b169d6599637631cf37a1703326a0727073" + +[[package]] +name = "wasmi_core" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf1a7db34bff95b85c261002720c00c3a6168256dcb93041d3fa2054d19856a" +dependencies = [ + "downcast-rs", + "libm", + "num-traits", + "paste", +] + +[[package]] +name = "wasmparser" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a58e28b80dd8340cb07b8242ae654756161f6fc8d0038123d679b7b99964fa50" +dependencies = [ + "indexmap 2.14.0", + "semver", +] + +[[package]] +name = "wasmparser-nostd" +version = "0.100.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5a015fe95f3504a94bb1462c717aae75253e39b9dd6c3fb1062c934535c64aa" +dependencies = [ + "indexmap-nostd", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/contracts/anonvote/src/errors.rs b/contracts/anonvote/src/errors.rs new file mode 100644 index 0000000..3aa33de --- /dev/null +++ b/contracts/anonvote/src/errors.rs @@ -0,0 +1,17 @@ +use soroban_sdk::contracterror; + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum ContractError { + AlreadyInitialized = 1, + Unauthorized = 2, + BallotAlreadyExists = 3, + BallotNotFound = 4, + BallotAlreadyFinalised = 5, + InvalidBallotIdHash = 6, + InvalidResultHash = 7, + InvalidAdminAddress = 8, + CounterOverflow = 9, + BallotExpired = 10, +} diff --git a/contracts/anonvote/src/lib.rs b/contracts/anonvote/src/lib.rs index de68af1..9bea387 100644 --- a/contracts/anonvote/src/lib.rs +++ b/contracts/anonvote/src/lib.rs @@ -1,44 +1,14 @@ -//! AnonVote Soroban Smart Contract -//! -//! Records immutable audit events on the Stellar blockchain. -//! Complements the manageData approach with on-chain queryable state. -//! -//! # What this contract does -//! - Records ballot creation events with a ballot ID hash -//! - Records token issuance counts per ballot (no voter identity) -//! - Records vote cast counts per ballot (no vote content) -//! - Records result publication with a tally hash -//! - Allows public verification of event counts on-chain -//! -//! # Privacy guarantees -//! - No voter identifiers stored -//! - No token values stored -//! - No vote content stored -//! - Only counts and hashes — same privacy model as the off-chain system - #![no_std] -use soroban_sdk::{ - contract, contracterror, contractimpl, contracttype, symbol_short, Address, Env, String, -}; +mod errors; +mod validation; -// ── Error types ─────────────────────────────────────────────────────────────── - -#[contracterror] -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[repr(u32)] -pub enum ContractError { - AdminUnauthorized = 1, - AlreadyInitialized = 2, - NotInitialized = 3, - BallotNotFound = 4, - BallotAlreadyExists = 5, - ResultAlreadyPublished = 6, - CounterOverflow = 7, - InvalidBallotHash = 8, -} +pub use errors::ContractError; -// ── Ballot state types ──────────────────────────────────────────────────────── +use soroban_sdk::{ + contract, contractimpl, contracttype, symbol_short, Address, Env, String, Vec, +}; +use validation::validate_hex_hash; #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] @@ -66,41 +36,46 @@ pub struct BallotStateSnapshot { pub state: BallotState, } -// ── Storage keys ────────────────────────────────────────────────────────────── +#[contracttype] +#[derive(Clone)] +pub struct AdminRotation { + pub old_admin: Address, + pub new_admin: Address, + pub timestamp: u64, +} #[contracttype] #[derive(Clone)] pub enum DataKey { - /// Admin address — only admin can record events Admin, - /// Timestamp of contract initialization + Initialized, InitializedAt, - /// Token issued count for a ballot: ballot_id_hash → u32 + AdminHistory, TokensIssued(String), - /// Votes cast count for a ballot: ballot_id_hash → u32 VotesCast(String), - /// Result hash for a ballot: ballot_id_hash → String ResultHash(String), - /// Whether a ballot has been created: ballot_id_hash → bool BallotExists(String), - /// Ballot metadata: ballot_id_hash → BallotMetadata BallotMetadata(String), } -// ── Contract ────────────────────────────────────────────────────────────────── - #[contract] pub struct AnonVoteContract; #[contractimpl] impl AnonVoteContract { - /// Initialize the contract with an admin address. - /// Must be called once after deployment. - /// Returns AlreadyInitialized if called again (idempotent-safe). pub fn initialize(env: Env, admin: Address) -> Result<(), ContractError> { - if env.storage().instance().has(&DataKey::Admin) { + if env.storage().instance().has(&DataKey::Initialized) { return Err(ContractError::AlreadyInitialized); } + + let zero_key = + String::from_str(&env, "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); + let zero = Address::from_string(&zero_key); + if admin == zero { + return Err(ContractError::InvalidAdminAddress); + } + + env.storage().instance().set(&DataKey::Initialized, &true); env.storage().instance().set(&DataKey::Admin, &admin); env.storage() .instance() @@ -108,32 +83,17 @@ impl AnonVoteContract { Ok(()) } - /// Record a ballot creation event. - /// ballot_id_hash: SHA-256 hex of the ballot UUID. - /// Idempotent: if the same caller re-records the same ballot, returns success. - /// Returns BallotAlreadyExists if a different admin recorded this ballot. pub fn record_ballot( env: Env, caller: Address, ballot_id_hash: String, ) -> Result<(), ContractError> { - if ballot_id_hash.len() == 0 { - return Err(ContractError::InvalidBallotHash); - } + validate_hex_hash(&env, &ballot_id_hash, ContractError::InvalidBallotIdHash)?; caller.require_auth(); Self::require_admin(&env, &caller)?; let exists_key = DataKey::BallotExists(ballot_id_hash.clone()); if env.storage().persistent().has(&exists_key) { - // Idempotency: if same admin recorded it, treat as success - let meta: BallotMetadata = env - .storage() - .persistent() - .get(&DataKey::BallotMetadata(ballot_id_hash)) - .unwrap(); - if meta.admin == caller { - return Ok(()); - } return Err(ContractError::BallotAlreadyExists); } @@ -156,29 +116,29 @@ impl AnonVoteContract { .set(&DataKey::BallotMetadata(ballot_id_hash.clone()), &metadata); env.events().publish( - (symbol_short!("audit"), symbol_short!("blt_crtd")), - (ballot_id_hash, created_at, caller), + (symbol_short!("ballot"), symbol_short!("regstrd")), + (ballot_id_hash, created_at), ); Ok(()) } - /// Increment the token issued count for a ballot. - /// Returns CounterOverflow if the count is already at u32::MAX. pub fn record_token( env: Env, caller: Address, ballot_id_hash: String, ) -> Result<(), ContractError> { + validate_hex_hash(&env, &ballot_id_hash, ContractError::InvalidBallotIdHash)?; caller.require_auth(); Self::require_admin(&env, &caller)?; Self::require_ballot_exists(&env, &ballot_id_hash)?; + Self::require_not_finalised(&env, &ballot_id_hash)?; let key = DataKey::TokensIssued(ballot_id_hash.clone()); let count: u32 = env.storage().persistent().get(&key).unwrap_or(0); if count == u32::MAX { env.events().publish( - (symbol_short!("audit"), symbol_short!("cnt_ovflw")), - ballot_id_hash, + (symbol_short!("counter"), symbol_short!("ovrflw")), + (ballot_id_hash, count), ); return Err(ContractError::CounterOverflow); } @@ -186,29 +146,29 @@ impl AnonVoteContract { env.storage().persistent().set(&key, &new_count); env.events().publish( - (symbol_short!("audit"), symbol_short!("tok_issd")), + (symbol_short!("ballot"), symbol_short!("tok_issd")), (ballot_id_hash, new_count), ); Ok(()) } - /// Increment the votes cast count for a ballot. - /// Returns CounterOverflow if the count is already at u32::MAX. pub fn record_vote( env: Env, caller: Address, ballot_id_hash: String, ) -> Result<(), ContractError> { + validate_hex_hash(&env, &ballot_id_hash, ContractError::InvalidBallotIdHash)?; caller.require_auth(); Self::require_admin(&env, &caller)?; Self::require_ballot_exists(&env, &ballot_id_hash)?; + Self::require_not_finalised(&env, &ballot_id_hash)?; let key = DataKey::VotesCast(ballot_id_hash.clone()); let count: u32 = env.storage().persistent().get(&key).unwrap_or(0); if count == u32::MAX { env.events().publish( - (symbol_short!("audit"), symbol_short!("cnt_ovflw")), - ballot_id_hash, + (symbol_short!("counter"), symbol_short!("ovrflw")), + (ballot_id_hash, count), ); return Err(ContractError::CounterOverflow); } @@ -216,35 +176,25 @@ impl AnonVoteContract { env.storage().persistent().set(&key, &new_count); env.events().publish( - (symbol_short!("audit"), symbol_short!("vote_cast")), + (symbol_short!("ballot"), symbol_short!("vote_cast")), (ballot_id_hash, new_count), ); Ok(()) } - /// Record the result publication for a ballot. - /// Idempotent: if the same result_hash is already recorded, returns success. - /// Returns ResultAlreadyPublished (with a distinguishable error) if a - /// different result hash was already published. pub fn record_result( env: Env, caller: Address, ballot_id_hash: String, result_hash: String, ) -> Result<(), ContractError> { + validate_hex_hash(&env, &result_hash, ContractError::InvalidResultHash)?; caller.require_auth(); Self::require_admin(&env, &caller)?; Self::require_ballot_exists(&env, &ballot_id_hash)?; + Self::require_not_finalised(&env, &ballot_id_hash)?; let key = DataKey::ResultHash(ballot_id_hash.clone()); - if let Some(existing) = env.storage().persistent().get::(&key) { - // Idempotent: same hash re-recorded → success - if existing == result_hash { - return Ok(()); - } - return Err(ContractError::ResultAlreadyPublished); - } - env.storage().persistent().set(&key, &result_hash.clone()); let metadata_key = DataKey::BallotMetadata(ballot_id_hash.clone()); @@ -254,14 +204,12 @@ impl AnonVoteContract { env.storage().persistent().set(&metadata_key, &metadata); env.events().publish( - (symbol_short!("audit"), symbol_short!("res_pub")), - (ballot_id_hash, result_hash), + (symbol_short!("result"), symbol_short!("pubshd")), + (ballot_id_hash, result_hash, env.ledger().timestamp()), ); Ok(()) } - /// Rotate the admin address. Restricted to the current admin. - /// Emits an audit event with the old and new admin for rotation history. pub fn rotate_admin( env: Env, caller: Address, @@ -270,19 +218,53 @@ impl AnonVoteContract { caller.require_auth(); Self::require_admin(&env, &caller)?; + let zero_key = + String::from_str(&env, "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); + let zero = Address::from_string(&zero_key); + if new_admin == zero { + return Err(ContractError::InvalidAdminAddress); + } + let current: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); + if new_admin == current { + return Err(ContractError::InvalidAdminAddress); + } + + let timestamp = env.ledger().timestamp(); + let rotation = AdminRotation { + old_admin: current.clone(), + new_admin: new_admin.clone(), + timestamp, + }; + + let mut history: Vec = env + .storage() + .instance() + .get(&DataKey::AdminHistory) + .unwrap_or(Vec::new(&env)); + history.push_back(rotation); + env.storage() + .instance() + .set(&DataKey::AdminHistory, &history); env.storage().instance().set(&DataKey::Admin, &new_admin); env.events().publish( - (symbol_short!("audit"), symbol_short!("adm_rotd")), - (caller, new_admin), + (symbol_short!("admin"), symbol_short!("rotated")), + (caller, new_admin, timestamp), ); Ok(()) } - // ── Read-only queries ──────────────────────────────────────────────────── + pub fn get_admin(env: Env) -> Option
{ + env.storage().instance().get(&DataKey::Admin) + } + + pub fn get_admin_history(env: Env) -> Vec { + env.storage() + .instance() + .get(&DataKey::AdminHistory) + .unwrap_or(Vec::new(&env)) + } - /// Get the number of tokens issued for a ballot. - /// Returns None if the ballot does not exist. pub fn get_tokens_issued(env: Env, ballot_id_hash: String) -> Option { if !env .storage() @@ -296,8 +278,6 @@ impl AnonVoteContract { .get(&DataKey::TokensIssued(ballot_id_hash)) } - /// Get the number of votes cast for a ballot. - /// Returns None if the ballot does not exist. pub fn get_votes_cast(env: Env, ballot_id_hash: String) -> Option { if !env .storage() @@ -311,43 +291,34 @@ impl AnonVoteContract { .get(&DataKey::VotesCast(ballot_id_hash)) } - /// Get the result hash for a ballot (None if not yet published). pub fn get_result_hash(env: Env, ballot_id_hash: String) -> Option { env.storage() .persistent() .get(&DataKey::ResultHash(ballot_id_hash)) } - /// Check if a ballot has been recorded on-chain. pub fn ballot_exists(env: Env, ballot_id_hash: String) -> bool { env.storage() .persistent() .has(&DataKey::BallotExists(ballot_id_hash)) } - /// Check if a result has been published for a ballot. pub fn result_exists(env: Env, ballot_id_hash: String) -> bool { env.storage() .persistent() .has(&DataKey::ResultHash(ballot_id_hash)) } - /// Get the timestamp when the contract was initialized. - /// Returns None if the contract has not been initialized. pub fn get_initialized_at(env: Env) -> Option { env.storage().instance().get(&DataKey::InitializedAt) } - /// Get ballot metadata (created_at, admin, state). - /// Returns None if the ballot does not exist. pub fn get_ballot_metadata(env: Env, ballot_id_hash: String) -> Option { env.storage() .persistent() .get(&DataKey::BallotMetadata(ballot_id_hash)) } - /// Get complete ballot state snapshot (tokens, votes, result, metadata). - /// Returns None if the ballot does not exist. pub fn get_ballot_state(env: Env, ballot_id_hash: String) -> Option { if !env .storage() @@ -387,8 +358,14 @@ impl AnonVoteContract { }) } - /// Verify consistency: returns true if tokens_issued == votes_cast. pub fn is_consistent(env: Env, ballot_id_hash: String) -> bool { + if !env + .storage() + .persistent() + .has(&DataKey::BallotExists(ballot_id_hash.clone())) + { + return false; + } let tokens: u32 = env .storage() .persistent() @@ -402,16 +379,14 @@ impl AnonVoteContract { tokens == votes } - // ── Internal helpers ───────────────────────────────────────────────────── - fn require_admin(env: &Env, caller: &Address) -> Result<(), ContractError> { let admin: Address = env .storage() .instance() .get(&DataKey::Admin) - .ok_or(ContractError::NotInitialized)?; + .ok_or(ContractError::Unauthorized)?; if *caller != admin { - return Err(ContractError::AdminUnauthorized); + return Err(ContractError::Unauthorized); } Ok(()) } @@ -426,308 +401,18 @@ impl AnonVoteContract { } Ok(()) } -} - -// ── Tests ───────────────────────────────────────────────────────────────────── - -#[cfg(test)] -mod tests { - use super::*; - use soroban_sdk::{testutils::Address as _, Env, String}; - - fn setup() -> (Env, AnonVoteContractClient<'static>, Address) { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, AnonVoteContract); - let client = AnonVoteContractClient::new(&env, &contract_id); - let admin = Address::generate(&env); - client.initialize(&admin).unwrap(); - (env, client, admin) - } - - fn setup_with_id() -> (Env, Address, AnonVoteContractClient<'static>, Address) { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, AnonVoteContract); - let client = AnonVoteContractClient::new(&env, &contract_id); - let admin = Address::generate(&env); - client.initialize(&admin).unwrap(); - (env, contract_id, client, admin) - } - - // ── Existing success-path tests ────────────────────────────────────────── - - #[test] - fn test_record_ballot_and_query() { - let (env, client, admin) = setup(); - let ballot_hash = String::from_str(&env, "abc123"); - client.record_ballot(&admin, &ballot_hash).unwrap(); - assert!(client.ballot_exists(&ballot_hash)); - assert_eq!(client.get_tokens_issued(&ballot_hash), Some(0)); - assert_eq!(client.get_votes_cast(&ballot_hash), Some(0)); - } - - #[test] - fn test_token_and_vote_counts() { - let (env, client, admin) = setup(); - let ballot_hash = String::from_str(&env, "abc123"); - client.record_ballot(&admin, &ballot_hash).unwrap(); - client.record_token(&admin, &ballot_hash).unwrap(); - client.record_token(&admin, &ballot_hash).unwrap(); - client.record_vote(&admin, &ballot_hash).unwrap(); - assert_eq!(client.get_tokens_issued(&ballot_hash), Some(2)); - assert_eq!(client.get_votes_cast(&ballot_hash), Some(1)); - assert!(!client.is_consistent(&ballot_hash)); - client.record_vote(&admin, &ballot_hash).unwrap(); - assert!(client.is_consistent(&ballot_hash)); - } - - #[test] - fn test_record_result() { - let (env, client, admin) = setup(); - let ballot_hash = String::from_str(&env, "abc123"); - let result_hash = String::from_str(&env, "deadbeef"); - client.record_ballot(&admin, &ballot_hash).unwrap(); - client - .record_result(&admin, &ballot_hash, &result_hash) - .unwrap(); - assert_eq!(client.get_result_hash(&ballot_hash), Some(result_hash)); - } - - #[test] - fn test_ballot_metadata() { - let (env, client, admin) = setup(); - let ballot_hash = String::from_str(&env, "abc123"); - client.record_ballot(&admin, &ballot_hash).unwrap(); - - let metadata = client.get_ballot_metadata(&ballot_hash).unwrap(); - assert_eq!(metadata.admin, admin); - assert_eq!(metadata.state, BallotState::Active); - assert!(metadata.created_at > 0); - } - - #[test] - fn test_ballot_state_snapshot() { - let (env, client, admin) = setup(); - let ballot_hash = String::from_str(&env, "abc123"); - let result_hash = String::from_str(&env, "deadbeef"); - - client.record_ballot(&admin, &ballot_hash).unwrap(); - client.record_token(&admin, &ballot_hash).unwrap(); - client.record_token(&admin, &ballot_hash).unwrap(); - client.record_vote(&admin, &ballot_hash).unwrap(); - client - .record_result(&admin, &ballot_hash, &result_hash) - .unwrap(); - - let state = client.get_ballot_state(&ballot_hash).unwrap(); - assert_eq!(state.tokens_issued, 2); - assert_eq!(state.votes_cast, 1); - assert_eq!(state.result_hash, Some(result_hash)); - assert_eq!(state.admin, admin); - assert_eq!(state.state, BallotState::ResultPublished); - } - - #[test] - fn test_nonexistent_ballot() { - let (env, client, _admin) = setup(); - let ballot_hash = String::from_str(&env, "nonexistent"); - assert_eq!(client.get_tokens_issued(&ballot_hash), None); - assert_eq!(client.get_votes_cast(&ballot_hash), None); - assert_eq!(client.get_ballot_metadata(&ballot_hash), None); - assert_eq!(client.get_ballot_state(&ballot_hash), None); - } - - // ── Error-case tests ───────────────────────────────────────────────────── - - #[test] - fn test_initialize_twice_returns_already_initialized() { - let (_, client, admin) = setup(); - let err = client.try_initialize(&admin).unwrap_err().unwrap(); - assert_eq!(err, ContractError::AlreadyInitialized); - } - - #[test] - fn test_initialization_timestamp_stored() { - let (_, client, _) = setup(); - assert!(client.get_initialized_at().is_some()); - assert!(client.get_initialized_at().unwrap() > 0); - } - - #[test] - fn test_unauthorized_caller_returns_admin_unauthorized() { - let (env, client, _admin) = setup(); - let ballot_hash = String::from_str(&env, "abc123"); - let attacker = Address::generate(&env); - let err = client - .try_record_ballot(&attacker, &ballot_hash) - .unwrap_err() - .unwrap(); - assert_eq!(err, ContractError::AdminUnauthorized); - } - - #[test] - fn test_record_ballot_idempotent_same_admin() { - let (env, client, admin) = setup(); - let ballot_hash = String::from_str(&env, "abc123"); - client.record_ballot(&admin, &ballot_hash).unwrap(); - // Second call with same admin → idempotent success - client.record_ballot(&admin, &ballot_hash).unwrap(); - } - - #[test] - fn test_record_ballot_different_admin_returns_already_exists() { - let (env, client, admin) = setup(); - let ballot_hash = String::from_str(&env, "abc123"); - client.record_ballot(&admin, &ballot_hash).unwrap(); - - // Rotate admin so we have a different valid admin - let new_admin = Address::generate(&env); - client.rotate_admin(&admin, &new_admin).unwrap(); - - let err = client - .try_record_ballot(&new_admin, &ballot_hash) - .unwrap_err() - .unwrap(); - assert_eq!(err, ContractError::BallotAlreadyExists); - } - - #[test] - fn test_counter_overflow_token() { - let (env, contract_id, client, admin) = setup_with_id(); - let ballot_hash = String::from_str(&env, "abc123"); - client.record_ballot(&admin, &ballot_hash).unwrap(); - - // Force counter to u32::MAX inside the contract's storage context - let bh = ballot_hash.clone(); - env.as_contract(&contract_id, || { - env.storage() - .persistent() - .set(&DataKey::TokensIssued(bh), &u32::MAX); - }); - - let err = client - .try_record_token(&admin, &ballot_hash) - .unwrap_err() - .unwrap(); - assert_eq!(err, ContractError::CounterOverflow); - } - - #[test] - fn test_counter_overflow_vote() { - let (env, contract_id, client, admin) = setup_with_id(); - let ballot_hash = String::from_str(&env, "abc123"); - client.record_ballot(&admin, &ballot_hash).unwrap(); - - let bh = ballot_hash.clone(); - env.as_contract(&contract_id, || { - env.storage() - .persistent() - .set(&DataKey::VotesCast(bh), &u32::MAX); - }); - - let err = client - .try_record_vote(&admin, &ballot_hash) - .unwrap_err() - .unwrap(); - assert_eq!(err, ContractError::CounterOverflow); - } - - #[test] - fn test_record_result_idempotent_same_hash() { - let (env, client, admin) = setup(); - let ballot_hash = String::from_str(&env, "abc123"); - let result_hash = String::from_str(&env, "deadbeef"); - client.record_ballot(&admin, &ballot_hash).unwrap(); - client - .record_result(&admin, &ballot_hash, &result_hash) - .unwrap(); - // Same hash again → idempotent success - client - .record_result(&admin, &ballot_hash, &result_hash) - .unwrap(); - } - - #[test] - fn test_record_result_different_hash_returns_already_published() { - let (env, client, admin) = setup(); - let ballot_hash = String::from_str(&env, "abc123"); - let result_hash = String::from_str(&env, "deadbeef"); - let other_hash = String::from_str(&env, "cafebabe"); - client.record_ballot(&admin, &ballot_hash).unwrap(); - client - .record_result(&admin, &ballot_hash, &result_hash) - .unwrap(); - - let err = client - .try_record_result(&admin, &ballot_hash, &other_hash) - .unwrap_err() - .unwrap(); - assert_eq!(err, ContractError::ResultAlreadyPublished); - } - - #[test] - fn test_result_exists() { - let (env, client, admin) = setup(); - let ballot_hash = String::from_str(&env, "abc123"); - let result_hash = String::from_str(&env, "deadbeef"); - client.record_ballot(&admin, &ballot_hash).unwrap(); - assert!(!client.result_exists(&ballot_hash)); - client - .record_result(&admin, &ballot_hash, &result_hash) - .unwrap(); - assert!(client.result_exists(&ballot_hash)); - } - - #[test] - fn test_rotate_admin() { - let (env, client, admin) = setup(); - let new_admin = Address::generate(&env); - client.rotate_admin(&admin, &new_admin).unwrap(); - - // Old admin can no longer record - let ballot_hash = String::from_str(&env, "abc123"); - let err = client - .try_record_ballot(&admin, &ballot_hash) - .unwrap_err() - .unwrap(); - assert_eq!(err, ContractError::AdminUnauthorized); - - // New admin can record - client.record_ballot(&new_admin, &ballot_hash).unwrap(); - } - #[test] - fn test_rotate_admin_unauthorized() { - let (env, client, _admin) = setup(); - let attacker = Address::generate(&env); - let new_admin = Address::generate(&env); - let err = client - .try_rotate_admin(&attacker, &new_admin) - .unwrap_err() - .unwrap(); - assert_eq!(err, ContractError::AdminUnauthorized); - } - - #[test] - fn test_ballot_not_found_on_record_token() { - let (env, client, admin) = setup(); - let ballot_hash = String::from_str(&env, "missing"); - let err = client - .try_record_token(&admin, &ballot_hash) - .unwrap_err() - .unwrap(); - assert_eq!(err, ContractError::BallotNotFound); - } - - #[test] - fn test_invalid_ballot_hash() { - let (env, client, admin) = setup(); - let empty = String::from_str(&env, ""); - let err = client - .try_record_ballot(&admin, &empty) - .unwrap_err() - .unwrap(); - assert_eq!(err, ContractError::InvalidBallotHash); + fn require_not_finalised(env: &Env, ballot_id_hash: &String) -> Result<(), ContractError> { + if env + .storage() + .persistent() + .has(&DataKey::ResultHash(ballot_id_hash.clone())) + { + return Err(ContractError::BallotAlreadyFinalised); + } + Ok(()) } } + +#[cfg(test)] +mod test; diff --git a/contracts/anonvote/src/test.rs b/contracts/anonvote/src/test.rs new file mode 100644 index 0000000..fd79527 --- /dev/null +++ b/contracts/anonvote/src/test.rs @@ -0,0 +1,424 @@ +use super::*; +use soroban_sdk::{testutils::Address as _, Address, Env, String, Vec}; + +fn valid_ballot_hash(env: &Env) -> String { + String::from_str( + env, + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", + ) +} + +fn valid_result_hash(env: &Env) -> String { + String::from_str( + env, + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + ) +} + +fn setup() -> (Env, AnonVoteContractClient<'static>, Address) { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, AnonVoteContract); + let client = AnonVoteContractClient::new(&env, &contract_id); + let admin = Address::generate(&env); + client.initialize(&admin).unwrap(); + (env, client, admin) +} + +fn setup_with_id() -> (Env, Address, AnonVoteContractClient<'static>, Address) { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, AnonVoteContract); + let client = AnonVoteContractClient::new(&env, &contract_id); + let admin = Address::generate(&env); + client.initialize(&admin).unwrap(); + (env, contract_id, client, admin) +} + +fn setup_uninitialized() -> (Env, AnonVoteContractClient<'static>) { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, AnonVoteContract); + let client = AnonVoteContractClient::new(&env, &contract_id); + (env, client) +} + +#[test] +fn test_initialize_sets_admin() { + let (env, client, admin) = setup(); + let stored = client.get_admin().unwrap(); + assert_eq!(stored, admin); + assert!(client.get_initialized_at().unwrap() > 0); +} + +#[test] +fn test_initialize_twice_returns_already_initialized() { + let (_, client, admin) = setup(); + let err = client.try_initialize(&admin).unwrap_err().unwrap(); + assert_eq!(err, ContractError::AlreadyInitialized); +} + +#[test] +fn test_initialize_zero_address_returns_invalid_admin() { + let (env, client) = setup_uninitialized(); + let zero_key = String::from_str( + &env, + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + ); + let zero = Address::from_string(&zero_key); + let err = client.try_initialize(&zero).unwrap_err().unwrap(); + assert_eq!(err, ContractError::InvalidAdminAddress); +} + +#[test] +fn test_record_ballot_success() { + let (env, client, admin) = setup(); + let hash = valid_ballot_hash(&env); + client.record_ballot(&admin, &hash).unwrap(); + assert!(client.ballot_exists(&hash)); + assert_eq!(client.get_tokens_issued(&hash), Some(0)); + assert_eq!(client.get_votes_cast(&hash), Some(0)); +} + +#[test] +fn test_record_ballot_invalid_hash_wrong_length() { + let (env, client, admin) = setup(); + let short = String::from_str(&env, "abc"); + let err = client + .try_record_ballot(&admin, &short) + .unwrap_err() + .unwrap(); + assert_eq!(err, ContractError::InvalidBallotIdHash); +} + +#[test] +fn test_record_ballot_invalid_hash_uppercase() { + let (env, client, admin) = setup(); + let upper = String::from_str( + &env, + "ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789", + ); + let err = client + .try_record_ballot(&admin, &upper) + .unwrap_err() + .unwrap(); + assert_eq!(err, ContractError::InvalidBallotIdHash); +} + +#[test] +fn test_record_ballot_invalid_hash_non_hex() { + let (env, client, admin) = setup(); + let non_hex = String::from_str( + &env, + "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", + ); + let err = client + .try_record_ballot(&admin, &non_hex) + .unwrap_err() + .unwrap(); + assert_eq!(err, ContractError::InvalidBallotIdHash); +} + +#[test] +fn test_record_ballot_duplicate_returns_already_exists() { + let (env, client, admin) = setup(); + let hash = valid_ballot_hash(&env); + client.record_ballot(&admin, &hash).unwrap(); + let err = client + .try_record_ballot(&admin, &hash) + .unwrap_err() + .unwrap(); + assert_eq!(err, ContractError::BallotAlreadyExists); +} + +#[test] +fn test_record_token_success() { + let (env, client, admin) = setup(); + let hash = valid_ballot_hash(&env); + client.record_ballot(&admin, &hash).unwrap(); + client.record_token(&admin, &hash).unwrap(); + assert_eq!(client.get_tokens_issued(&hash), Some(1)); + client.record_token(&admin, &hash).unwrap(); + assert_eq!(client.get_tokens_issued(&hash), Some(2)); +} + +#[test] +fn test_record_token_ballot_not_found() { + let (env, client, admin) = setup(); + let hash = valid_ballot_hash(&env); + let err = client + .try_record_token(&admin, &hash) + .unwrap_err() + .unwrap(); + assert_eq!(err, ContractError::BallotNotFound); +} + +#[test] +fn test_record_token_after_finalised_returns_error() { + let (env, client, admin) = setup(); + let hash = valid_ballot_hash(&env); + let result = valid_result_hash(&env); + client.record_ballot(&admin, &hash).unwrap(); + client.record_result(&admin, &hash, &result).unwrap(); + let err = client + .try_record_token(&admin, &hash) + .unwrap_err() + .unwrap(); + assert_eq!(err, ContractError::BallotAlreadyFinalised); +} + +#[test] +fn test_record_vote_success() { + let (env, client, admin) = setup(); + let hash = valid_ballot_hash(&env); + client.record_ballot(&admin, &hash).unwrap(); + client.record_vote(&admin, &hash).unwrap(); + assert_eq!(client.get_votes_cast(&hash), Some(1)); + client.record_vote(&admin, &hash).unwrap(); + assert_eq!(client.get_votes_cast(&hash), Some(2)); +} + +#[test] +fn test_record_vote_ballot_not_found() { + let (env, client, admin) = setup(); + let hash = valid_ballot_hash(&env); + let err = client + .try_record_vote(&admin, &hash) + .unwrap_err() + .unwrap(); + assert_eq!(err, ContractError::BallotNotFound); +} + +#[test] +fn test_record_vote_after_finalised_returns_error() { + let (env, client, admin) = setup(); + let hash = valid_ballot_hash(&env); + let result = valid_result_hash(&env); + client.record_ballot(&admin, &hash).unwrap(); + client.record_result(&admin, &hash, &result).unwrap(); + let err = client + .try_record_vote(&admin, &hash) + .unwrap_err() + .unwrap(); + assert_eq!(err, ContractError::BallotAlreadyFinalised); +} + +#[test] +fn test_counter_overflow_at_max() { + let (env, contract_id, client, admin) = setup_with_id(); + let hash = valid_ballot_hash(&env); + client.record_ballot(&admin, &hash).unwrap(); + + let bh = hash.clone(); + env.as_contract(&contract_id, || { + env.storage() + .persistent() + .set(&DataKey::TokensIssued(bh), &u32::MAX); + }); + + let err = client + .try_record_token(&admin, &hash) + .unwrap_err() + .unwrap(); + assert_eq!(err, ContractError::CounterOverflow); +} + +#[test] +fn test_counter_overflow_does_not_panic() { + let (env, contract_id, client, admin) = setup_with_id(); + let hash1 = valid_ballot_hash(&env); + let hash2 = String::from_str( + &env, + "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210", + ); + client.record_ballot(&admin, &hash1).unwrap(); + client.record_ballot(&admin, &hash2).unwrap(); + + let bh = hash1.clone(); + env.as_contract(&contract_id, || { + env.storage() + .persistent() + .set(&DataKey::TokensIssued(bh), &u32::MAX); + }); + + client.record_token(&admin, &hash1).unwrap_err().unwrap(); + client.record_token(&admin, &hash2).unwrap(); + assert_eq!(client.get_tokens_issued(&hash2), Some(1)); +} + +#[test] +fn test_counter_overflow_emits_event() { + let (env, contract_id, client, admin) = setup_with_id(); + let hash = valid_ballot_hash(&env); + client.record_ballot(&admin, &hash).unwrap(); + + let bh = hash.clone(); + env.as_contract(&contract_id, || { + env.storage() + .persistent() + .set(&DataKey::TokensIssued(bh), &u32::MAX); + }); + + client.try_record_token(&admin, &hash).unwrap_err().unwrap(); + let events = env.events().all(); + assert!(events.len() >= 2); +} + +#[test] +fn test_record_result_success() { + let (env, client, admin) = setup(); + let hash = valid_ballot_hash(&env); + let result = valid_result_hash(&env); + client.record_ballot(&admin, &hash).unwrap(); + client + .record_result(&admin, &hash, &result) + .unwrap(); + assert_eq!(client.get_result_hash(&hash), Some(result)); + assert!(client.result_exists(&hash)); +} + +#[test] +fn test_record_result_invalid_hash() { + let (env, client, admin) = setup(); + let hash = valid_ballot_hash(&env); + let bad_result = String::from_str(&env, "nothex"); + client.record_ballot(&admin, &hash).unwrap(); + let err = client + .try_record_result(&admin, &hash, &bad_result) + .unwrap_err() + .unwrap(); + assert_eq!(err, ContractError::InvalidResultHash); +} + +#[test] +fn test_record_result_twice_returns_already_finalised() { + let (env, client, admin) = setup(); + let hash = valid_ballot_hash(&env); + let result = valid_result_hash(&env); + client.record_ballot(&admin, &hash).unwrap(); + client + .record_result(&admin, &hash, &result) + .unwrap(); + let result2 = String::from_str( + &env, + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + ); + let err = client + .try_record_result(&admin, &hash, &result2) + .unwrap_err() + .unwrap(); + assert_eq!(err, ContractError::BallotAlreadyFinalised); +} + +#[test] +fn test_record_result_ballot_not_found() { + let (env, client, admin) = setup(); + let hash = valid_ballot_hash(&env); + let result = valid_result_hash(&env); + let err = client + .try_record_result(&admin, &hash, &result) + .unwrap_err() + .unwrap(); + assert_eq!(err, ContractError::BallotNotFound); +} + +#[test] +fn test_rotate_admin_success() { + let (env, client, admin) = setup(); + let new_admin = Address::generate(&env); + client.rotate_admin(&admin, &new_admin).unwrap(); + let stored = client.get_admin().unwrap(); + assert_eq!(stored, new_admin); + let err = client + .try_record_ballot(&admin, &valid_ballot_hash(&env)) + .unwrap_err() + .unwrap(); + assert_eq!(err, ContractError::Unauthorized); + client + .record_ballot(&new_admin, &valid_ballot_hash(&env)) + .unwrap(); +} + +#[test] +fn test_rotate_admin_same_address_returns_invalid() { + let (env, client, admin) = setup(); + let err = client + .try_rotate_admin(&admin, &admin) + .unwrap_err() + .unwrap(); + assert_eq!(err, ContractError::InvalidAdminAddress); +} + +#[test] +fn test_rotate_admin_zero_address_returns_invalid() { + let (env, client, admin) = setup(); + let zero_key = String::from_str( + &env, + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + ); + let zero = Address::from_string(&zero_key); + let err = client + .try_rotate_admin(&admin, &zero) + .unwrap_err() + .unwrap(); + assert_eq!(err, ContractError::InvalidAdminAddress); +} + +#[test] +fn test_rotate_admin_unauthorized() { + let (env, client, _admin) = setup(); + let attacker = Address::generate(&env); + let new_admin = Address::generate(&env); + let err = client + .try_rotate_admin(&attacker, &new_admin) + .unwrap_err() + .unwrap(); + assert_eq!(err, ContractError::Unauthorized); +} + +#[test] +fn test_rotate_admin_history_grows() { + let (env, client, admin) = setup(); + let new_admin1 = Address::generate(&env); + let new_admin2 = Address::generate(&env); + client.rotate_admin(&admin, &new_admin1).unwrap(); + client.rotate_admin(&new_admin1, &new_admin2).unwrap(); + let history = client.get_admin_history(); + assert_eq!(history.len(), 2); + let first = history.get(0).unwrap(); + assert_eq!(first.old_admin, admin); + assert_eq!(first.new_admin, new_admin1); + let second = history.get(1).unwrap(); + assert_eq!(second.old_admin, new_admin1); + assert_eq!(second.new_admin, new_admin2); +} + +#[test] +fn test_is_consistent_true() { + let (env, client, admin) = setup(); + let hash = valid_ballot_hash(&env); + client.record_ballot(&admin, &hash).unwrap(); + assert!(client.is_consistent(&hash)); + client.record_token(&admin, &hash).unwrap(); + assert!(!client.is_consistent(&hash)); + client.record_vote(&admin, &hash).unwrap(); + assert!(client.is_consistent(&hash)); +} + +#[test] +fn test_is_consistent_false() { + let (env, client, admin) = setup(); + let hash = valid_ballot_hash(&env); + client.record_ballot(&admin, &hash).unwrap(); + client.record_token(&admin, &hash).unwrap(); + client.record_token(&admin, &hash).unwrap(); + client.record_vote(&admin, &hash).unwrap(); + assert!(!client.is_consistent(&hash)); +} + +#[test] +fn test_is_consistent_nonexistent_ballot() { + let (env, client, _admin) = setup(); + let hash = valid_ballot_hash(&env); + assert!(!client.is_consistent(&hash)); +} diff --git a/contracts/anonvote/src/validation.rs b/contracts/anonvote/src/validation.rs new file mode 100644 index 0000000..b2da328 --- /dev/null +++ b/contracts/anonvote/src/validation.rs @@ -0,0 +1,21 @@ +use soroban_sdk::{Env, String}; +use crate::errors::ContractError; + +pub fn validate_hex_hash( + _env: &Env, + hash: &String, + error: ContractError, +) -> Result<(), ContractError> { + if hash.len() != 64 { + return Err(error); + } + let mut buf = [0u8; 64]; + hash.copy_into_slice(&mut buf); + for ch in buf.iter() { + match ch { + b'0'..=b'9' | b'a'..=b'f' => {} + _ => return Err(error), + } + } + Ok(()) +}