diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock new file mode 100644 index 00000000..2ddaf056 --- /dev/null +++ b/contracts/Cargo.lock @@ -0,0 +1,2142 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "analytics" +version = "0.1.0" +dependencies = [ + "soroban-sdk", +] + +[[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 = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "ark-bls12-381" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df4dcc01ff89867cd86b0da835f23c3f02738353aaee7dde7495af71363b8d5" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-serialize", + "ark-std", +] + +[[package]] +name = "ark-bn254" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d69eab57e8d2663efa5c63135b2af4f396d66424f88954c21104125ab6b3e6bc" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-std", +] + +[[package]] +name = "ark-ec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" +dependencies = [ + "ahash", + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "educe", + "fnv", + "hashbrown 0.15.5", + "itertools", + "num-bigint", + "num-integer", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", + "arrayvec", + "digest 0.10.7", + "educe", + "itertools", + "num-bigint", + "num-traits", + "paste", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-poly" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" +dependencies = [ + "ahash", + "ark-ff", + "ark-serialize", + "ark-std", + "educe", + "fnv", + "hashbrown 0.15.5", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-serialize-derive", + "ark-std", + "arrayvec", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[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 = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[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 = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes-lit" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b04f2b1d34cb428043f14aa4c853d14294532e8bbde3b6a3bc2faaaae31a1dd" +dependencies = [ + "num-bigint", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "cc" +version = "1.2.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +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 = "cfg_eval" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45565fc9416b9896014f5732ac776f810ee53a66730c17e4020c3ec064a8f88f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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 = "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "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", +] + +[[package]] +name = "ctor" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67773048316103656a637612c4a62477603b777d91d9c62ff2290f9cde178fdb" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2" + +[[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", + "rustc_version", + "subtle", +] + +[[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 = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.20", +] + +[[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 = "dtor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "404d02eeb088a82cfd873006cb713fe411306c7d182c344905e101fb1167d301" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[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", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[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", + "rand_core", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[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", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "enum-ordinalize" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07f808d588c10e464ea6f7d3eaed500049eff30aaac103460f61828c2d65b3eb" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e528e2d34ba8a67a1a650b86beae8ef69fc5fdb638016f386b973226590432" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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", + "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 = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[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.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +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.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[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", +] + +[[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.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "macro-string" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "multisig" +version = "0.1.0" +dependencies = [ + "soroban-sdk", +] + +[[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.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +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 = "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", +] + +[[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 = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[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", +] + +[[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", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +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_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.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "serde", + "serde_json", +] + +[[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.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee78f1fbe43ac4a0e47aadb3dbd357b69eb0d3793e948624cd03dd2750ab1c0a" +dependencies = [ + "base64", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "jiff", + "schemars 0.8.22", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8705578779c2b6bd90d84d66eb2e206b708b1a4d7b9f17641b293545bf1c7e46" +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 = "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", +] + +[[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 = "27.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b77bc93d930032c487cb1506b6ed166b2af49db76d52678ec4887ac621ecce01" +dependencies = [ + "itertools", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "soroban-env-common" +version = "27.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b22e9981cdd444f3aa6734bc58d76195bf7eca3ccf1dd432b875af5d02da068" +dependencies = [ + "arbitrary", + "crate-git-revision 0.0.6", + "ethnum", + "num-derive", + "num-traits", + "serde", + "soroban-env-macros", + "soroban-wasmi", + "static_assertions", + "stellar-xdr", + "wasmparser", +] + +[[package]] +name = "soroban-env-guest" +version = "27.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6072f99ca6bf8e8d5b04e05d083dac785e5357d9c0f36a6658f819c2fd7d67" +dependencies = [ + "soroban-env-common", + "static_assertions", +] + +[[package]] +name = "soroban-env-host" +version = "27.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c06afd7c75ce150ce53e4d77a77645b18e3fb61856a0ddc42bfcecdc39fa3b9" +dependencies = [ + "ark-bls12-381", + "ark-bn254", + "ark-ec", + "ark-ff", + "ark-serialize", + "curve25519-dalek 5.0.0", + "ecdsa", + "ed25519-dalek", + "elliptic-curve", + "generic-array", + "getrandom", + "hex-literal", + "hmac", + "k256", + "num-derive", + "num-integer", + "num-traits", + "p256", + "rand", + "rand_chacha", + "sec1", + "sha2", + "sha3", + "soroban-builtin-sdk-macros", + "soroban-env-common", + "soroban-wasmi", + "static_assertions", + "stellar-strkey 0.0.13", + "wasmparser", +] + +[[package]] +name = "soroban-env-macros" +version = "27.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "647811bdd28a3ec40296987f6635781e5e1141c8f5affbbd53ba12b6295b7bb6" +dependencies = [ + "itertools", + "proc-macro2", + "quote", + "serde", + "serde_json", + "stellar-xdr", + "syn 2.0.119", +] + +[[package]] +name = "soroban-ledger-snapshot" +version = "27.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b59883d8bd0d1aed8d57579a9974ab88eaf787dd0a1af104f6881b5707450558" +dependencies = [ + "serde", + "serde_json", + "serde_with", + "soroban-env-common", + "soroban-env-host", + "thiserror 1.0.69", +] + +[[package]] +name = "soroban-sdk" +version = "27.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c3f21971c84fcfb08957e3e8f5a9a70f134cb07ad9ee053ac7e6d7a887a82af" +dependencies = [ + "arbitrary", + "bytes-lit", + "crate-git-revision 0.0.9", + "ctor", + "derive_arbitrary", + "ed25519-dalek", + "rand", + "rustc_version", + "serde", + "serde_json", + "soroban-env-guest", + "soroban-env-host", + "soroban-ledger-snapshot", + "soroban-sdk-macros", + "stellar-strkey 0.0.16", + "visibility", +] + +[[package]] +name = "soroban-sdk-macros" +version = "27.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bd4a847273d749807fe2eb52e2b9c1917ee482cd6a39465cad5c389548996ad" +dependencies = [ + "darling 0.20.11", + "heck", + "itertools", + "macro-string", + "proc-macro2", + "quote", + "sha2", + "soroban-env-common", + "soroban-spec", + "soroban-spec-rust", + "stellar-xdr", + "syn 2.0.119", +] + +[[package]] +name = "soroban-spec" +version = "27.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473404322827b285cbcd87517f365986bd63af7842c78b2a86ee061715fda61e" +dependencies = [ + "base64", + "sha2", + "stellar-xdr", + "thiserror 1.0.69", + "wasmparser", +] + +[[package]] +name = "soroban-spec-rust" +version = "27.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f25698b6ce2125850a9ef075cf9ba1e8d25b4cfa0c46aca42dadd80cc29d881" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "sha2", + "soroban-spec", + "stellar-xdr", + "syn 2.0.119", + "thiserror 1.0.69", +] + +[[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 = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stellar-insights" +version = "0.1.0" +dependencies = [ + "soroban-sdk", +] + +[[package]] +name = "stellar-strkey" +version = "0.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee1832fb50c651ad10f734aaf5d31ca5acdfb197a6ecda64d93fcdb8885af913" +dependencies = [ + "crate-git-revision 0.0.6", + "data-encoding", +] + +[[package]] +name = "stellar-strkey" +version = "0.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "084afcb0d458c3d5d5baa2d294b18f881e62cc258ef539d8fdf68be7dbe45520" +dependencies = [ + "crate-git-revision 0.0.6", + "data-encoding", + "heapless", +] + +[[package]] +name = "stellar-xdr" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ff843326969bdf1ef673dcdba94c08f4a3c8f1e58d6e6ef39b1bd4f749179a" +dependencies = [ + "arbitrary", + "base64", + "cfg_eval", + "crate-git-revision 0.0.6", + "escape-bytes", + "ethnum", + "hex", + "serde", + "serde_with", + "sha2", + "stellar-strkey 0.0.13", +] + +[[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 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[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 = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +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 = "token-swap" +version = "0.1.0" +dependencies = [ + "soroban-sdk", +] + +[[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 = "visibility" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +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" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 06561f71..cc393506 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -1,9 +1,9 @@ [workspace] resolver = "2" -members = ["stellar_insights"] +members = ["stellar_insights", "analytics", "multisig", "token_swap"] [workspace.dependencies] -soroban-sdk = "26.0.1" +soroban-sdk = "27.0.6" [profile.release] opt-level = "z" diff --git a/contracts/analytics/Cargo.toml b/contracts/analytics/Cargo.toml new file mode 100644 index 00000000..ae114864 --- /dev/null +++ b/contracts/analytics/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "analytics" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib", "rlib"] +doctest = false + +[features] +testutils = ["soroban-sdk/testutils"] + +[dependencies] +soroban-sdk = { workspace = true } + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/contracts/analytics/STORAGE.md b/contracts/analytics/STORAGE.md new file mode 100644 index 00000000..19a51de8 --- /dev/null +++ b/contracts/analytics/STORAGE.md @@ -0,0 +1,44 @@ +# Analytics contract: storage lifecycle + +This is the durable design record for issue #339 (bounded on-chain storage). + +## What must be readable on-chain at diff time + +`diff.rs` compares the incoming snapshot to **the immediately previous snapshot +only**. It never walks older epochs. Therefore the live working set is: + +1. `PreviousSnapshot` (persistent) — metrics map + hashes for epoch *N−1* +2. Control-plane instance data — admin, pause flag, latest availability proof + +Full historical snapshots are **not** required to ingest epoch *N*. + +## TTL / bump policy + +| State | Kind | Bump | If we did not bump | +|---|---|---|---| +| Admin / paused | instance | Every ingest and pause/unpause (`HOT_TTL_EXTEND_TO` ≈ 31d) | Contract becomes unusable until instance is restored. Tiny, so we keep it hot. | +| Latest availability proof | instance | Overwritten + bumped every ingest | Proof of the last committed epoch; one slot, not a log. | +| Previous snapshot | persistent | Overwritten + bumped every ingest | Diff cannot run; next ingest would look like genesis unless the operator restores from the off-chain store and resubmits. | +| Epoch *N−2* and older | not stored | — | Would grow keys and rent linearly. Allowed to “archive” by never existing. | + +Constants live in `src/storage.rs` (`HOT_TTL_THRESHOLD`, `HOT_TTL_EXTEND_TO`). + +We do **not** aggressively bump “everything ever written”: there is no historical +key set. Rent liability is O(1) keys, not O(ingests). + +## On-chain vs off-chain + +| Role | Where | +|---|---| +| Live working set + availability proof | This contract | +| Full snapshot history, query, analytics API | Backend off-chain store in this repo | +| Ingest receipt (diff counts) | Return value of `submit_snapshot`; backend persists it | + +On-chain storage is a **minimal live working set plus an availability proof**, +not a duplicate of the backend ledger. + +## Growth invariant + +After the first successful ingest, `persistent_entry_count() == 1` for any +number of subsequent submissions. `storage_growth_test.rs` submits a long +sequence and asserts that count does not grow with *N*. diff --git a/contracts/analytics/src/diff.rs b/contracts/analytics/src/diff.rs new file mode 100644 index 00000000..e2187e54 --- /dev/null +++ b/contracts/analytics/src/diff.rs @@ -0,0 +1,63 @@ +//! Diff incoming snapshot metrics against the retained previous snapshot. +//! +//! Required on-chain read: **only** `storage::PreviousSnapshot` (or none on +//! the first ingest). Historical epochs are never consulted. + +use soroban_sdk::{Env, Map, Symbol}; + +use crate::storage::Snapshot; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SnapshotDiff { + pub from_epoch: u64, + pub to_epoch: u64, + pub added: Map, + pub removed: Map, + /// Values are (old, new). + pub changed: Map, +} + +pub fn diff_against_previous( + env: &Env, + previous: Option<&Snapshot>, + next: &Snapshot, +) -> SnapshotDiff { + let mut added = Map::new(env); + let mut removed = Map::new(env); + let mut changed = Map::new(env); + + let Some(prev) = previous else { + for key in next.metrics.keys().iter() { + added.set(key.clone(), next.metrics.get(key).unwrap()); + } + return SnapshotDiff { + from_epoch: 0, + to_epoch: next.epoch, + added, + removed, + changed, + }; + }; + + for key in next.metrics.keys().iter() { + let new_val = next.metrics.get(key.clone()).unwrap(); + match prev.metrics.get(key.clone()) { + None => added.set(key, new_val), + Some(old_val) if old_val != new_val => changed.set(key, (old_val, new_val)), + Some(_) => {} + } + } + for key in prev.metrics.keys().iter() { + if next.metrics.get(key.clone()).is_none() { + removed.set(key.clone(), prev.metrics.get(key).unwrap()); + } + } + + SnapshotDiff { + from_epoch: prev.epoch, + to_epoch: next.epoch, + added, + removed, + changed, + } +} diff --git a/contracts/analytics/src/lib.rs b/contracts/analytics/src/lib.rs new file mode 100644 index 00000000..08adabca --- /dev/null +++ b/contracts/analytics/src/lib.rs @@ -0,0 +1,137 @@ +#![no_std] + +mod diff; +mod pause; +mod storage; + +use soroban_sdk::{ + contract, contracterror, contractimpl, contracttype, Address, BytesN, Env, Map, Symbol, +}; + +pub use storage::{AvailabilityProof, Snapshot, PERSISTENT_LIVE_KEYS}; + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[repr(u32)] +pub enum Error { + AlreadyInitialized = 1, + NotInitialized = 2, + Unauthorized = 3, + InvalidEpoch = 4, + EpochMonotonicityViolated = 5, + ContractPaused = 6, + EmptyMetrics = 7, +} + +/// Return value of a successful ingest. Diff is **not** persisted (that would +/// grow storage); the backend records it off-chain from this receipt. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct IngestReceipt { + pub epoch: u64, + pub from_epoch: u64, + pub added_count: u32, + pub removed_count: u32, + pub changed_count: u32, + /// Persistent keys after this ingest. Always 1 once a snapshot exists. + pub persistent_entries: u32, +} + +#[contract] +pub struct AnalyticsContract; + +#[contractimpl] +impl AnalyticsContract { + pub fn initialize(env: Env, admin: Address) -> Result<(), Error> { + if env.storage().instance().has(&storage::DataKey::Admin) { + return Err(Error::AlreadyInitialized); + } + env.storage() + .instance() + .set(&storage::DataKey::Admin, &admin); + pause::set_paused(&env, false); + Ok(()) + } + + pub fn pause(env: Env, caller: Address) -> Result<(), Error> { + caller.require_auth(); + storage::require_admin(&env, &caller)?; + pause::set_paused(&env, true); + Ok(()) + } + + pub fn unpause(env: Env, caller: Address) -> Result<(), Error> { + caller.require_auth(); + storage::require_admin(&env, &caller)?; + pause::set_paused(&env, false); + Ok(()) + } + + pub fn is_paused(env: Env) -> bool { + pause::is_paused(&env) + } + + /// Ingest a batched snapshot. Diffs against the retained previous snapshot, + /// then **overwrites** that same persistent key. History is not kept on-chain. + pub fn submit_snapshot( + env: Env, + caller: Address, + epoch: u64, + metrics: Map, + snapshot_hash: BytesN<32>, + source_data_hash: BytesN<32>, + ) -> Result { + caller.require_auth(); + storage::require_admin(&env, &caller)?; + if pause::is_paused(&env) { + return Err(Error::ContractPaused); + } + if epoch == 0 { + return Err(Error::InvalidEpoch); + } + if metrics.is_empty() { + return Err(Error::EmptyMetrics); + } + + let previous = storage::load_previous(&env); + if let Some(ref prev) = previous { + if epoch <= prev.epoch { + return Err(Error::EpochMonotonicityViolated); + } + } + + let next = Snapshot { + epoch, + metrics, + snapshot_hash, + source_data_hash, + submitted_at: env.ledger().timestamp(), + }; + let d = diff::diff_against_previous(&env, previous.as_ref(), &next); + storage::retain_as_previous(&env, &next); + + Ok(IngestReceipt { + epoch, + from_epoch: d.from_epoch, + added_count: d.added.len(), + removed_count: d.removed.len(), + changed_count: d.changed.len(), + persistent_entries: storage::persistent_entry_count(&env), + }) + } + + pub fn previous_snapshot(env: Env) -> Option { + storage::load_previous(&env) + } + + pub fn latest_proof(env: Env) -> Option { + env.storage() + .instance() + .get(&storage::DataKey::LatestProof) + } + + /// Live persistent working-set size. Bounded at 1; independent of ingest count. + pub fn persistent_entry_count(env: Env) -> u32 { + storage::persistent_entry_count(&env) + } +} diff --git a/contracts/analytics/src/pause.rs b/contracts/analytics/src/pause.rs new file mode 100644 index 00000000..f50d6e69 --- /dev/null +++ b/contracts/analytics/src/pause.rs @@ -0,0 +1,15 @@ +use soroban_sdk::Env; + +use crate::storage::DataKey; + +pub fn is_paused(env: &Env) -> bool { + env.storage() + .instance() + .get(&DataKey::Paused) + .unwrap_or(false) +} + +pub fn set_paused(env: &Env, paused: bool) { + env.storage().instance().set(&DataKey::Paused, &paused); + crate::storage::bump_hot_state(env); +} diff --git a/contracts/analytics/src/storage.rs b/contracts/analytics/src/storage.rs new file mode 100644 index 00000000..c59b1106 --- /dev/null +++ b/contracts/analytics/src/storage.rs @@ -0,0 +1,129 @@ +//! On-chain storage lifecycle for the analytics contract. +//! +//! # What `diff.rs` needs at ingest time +//! +//! Diffing is always "incoming batch vs immediately previous snapshot". +//! That is the **only** snapshot that must be readable on-chain. Full +//! history is *not* required for a correct ingest. +//! +//! # TTL / bump policy +//! +//! | Entry | Storage | Bumped? | Why | +//! |---|---|---|---| +//! | Admin, paused | instance | yes, every ingest / admin op | Tiny, always-hot control plane. Instance TTL is cheap vs restoring. | +//! | Latest availability proof (epoch + hash) | instance | yes, every ingest | Compact proof that a given epoch was committed; overwritten, not appended. | +//! | Previous snapshot (metrics + hashes) | persistent | yes, every ingest | The sole working set `diff` reads. Same key is overwritten. | +//! | Historical snapshots (epoch N-2, N-3, …) | **not stored** | n/a | Would make persistent keys and rent grow linearly. Durable copy lives off-chain. | +//! +//! Archived entries: we never create extra persistent keys, so there is +//! nothing historical to restore. If `PreviousSnapshot` ever archived +//! (TTL not extended because ingest stopped), the next ingest treats it +//! as missing genesis-equivalent state and the off-chain store is the +//! source of truth for reconstructing the last snapshot before resuming. +//! +//! # On-chain vs off-chain +//! +//! - **On-chain:** live working set = previous snapshot + availability proof. +//! Bounded: O(1) keys regardless of ingest count. +//! - **Off-chain (this repo's backend):** full snapshot history, diffs, and +//! query API. Contract events / return values are the ingest receipt. + +use soroban_sdk::{contracttype, Address, BytesN, Env, Map, Symbol}; + +use crate::Error; + +/// Ledgers to keep hot state alive (~31 days at 5s ledgers). +/// +/// Bumped on every successful ingest so continuously operated deployments +/// never archive the working set. Idle deployments may archive; see module +/// docs for the restore/off-chain fallback. +pub const HOT_TTL_EXTEND_TO: u32 = 535_680; +/// Only bump when remaining TTL is below this (avoids paying extend every call +/// once already far out). +pub const HOT_TTL_THRESHOLD: u32 = 100_000; + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum DataKey { + Admin, + Paused, + /// Compact (epoch, hash) overwritten each ingest — not a growing log. + LatestProof, + /// The only persistent snapshot. Overwritten each ingest. + PreviousSnapshot, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Snapshot { + pub epoch: u64, + pub metrics: Map, + pub snapshot_hash: BytesN<32>, + pub source_data_hash: BytesN<32>, + pub submitted_at: u64, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AvailabilityProof { + pub epoch: u64, + pub snapshot_hash: BytesN<32>, +} + +/// Count of persistent keys this contract ever writes. +/// Always 0 or 1 (`PreviousSnapshot`). Used by growth tests. +pub const PERSISTENT_LIVE_KEYS: u32 = 1; + +pub fn require_admin(env: &Env, caller: &Address) -> Result<(), Error> { + let admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .ok_or(Error::NotInitialized)?; + if caller != &admin { + return Err(Error::Unauthorized); + } + Ok(()) +} + +pub fn bump_hot_state(env: &Env) { + env.storage() + .instance() + .extend_ttl(HOT_TTL_THRESHOLD, HOT_TTL_EXTEND_TO); + if env.storage().persistent().has(&DataKey::PreviousSnapshot) { + env.storage().persistent().extend_ttl( + &DataKey::PreviousSnapshot, + HOT_TTL_THRESHOLD, + HOT_TTL_EXTEND_TO, + ); + } +} + +pub fn load_previous(env: &Env) -> Option { + env.storage().persistent().get(&DataKey::PreviousSnapshot) +} + +/// Replace the previous snapshot in place. Does **not** allocate a new key +/// per epoch — this is what keeps growth O(1). +pub fn retain_as_previous(env: &Env, snapshot: &Snapshot) { + env.storage() + .persistent() + .set(&DataKey::PreviousSnapshot, snapshot); + env.storage().instance().set( + &DataKey::LatestProof, + &AvailabilityProof { + epoch: snapshot.epoch, + snapshot_hash: snapshot.snapshot_hash.clone(), + }, + ); + bump_hot_state(env); +} + +/// Persistent entries that currently exist (0 before first ingest, 1 after). +pub fn persistent_entry_count(env: &Env) -> u32 { + if env.storage().persistent().has(&DataKey::PreviousSnapshot) { + PERSISTENT_LIVE_KEYS + } else { + 0 + } +} diff --git a/contracts/analytics/tests/storage_growth_test.rs b/contracts/analytics/tests/storage_growth_test.rs new file mode 100644 index 00000000..73eb51b3 --- /dev/null +++ b/contracts/analytics/tests/storage_growth_test.rs @@ -0,0 +1,118 @@ +use analytics::{AnalyticsContract, AnalyticsContractClient, PERSISTENT_LIVE_KEYS}; +use soroban_sdk::{testutils::Address as _, Address, BytesN, Env, Map, Symbol}; + +fn metrics(env: &Env, daa: i128, volume: i128) -> Map { + let mut m = Map::new(env); + m.set(Symbol::new(env, "daa"), daa); + m.set(Symbol::new(env, "volume"), volume); + m +} + +fn hash(env: &Env, seed: u8) -> BytesN<32> { + BytesN::from_array(env, &[seed; 32]) +} + +fn setup<'a>(env: &'a Env) -> (AnalyticsContractClient<'a>, Address) { + env.mock_all_auths(); + let admin = Address::generate(env); + let id = env.register(AnalyticsContract, ()); + let client = AnalyticsContractClient::new(env, &id); + client.initialize(&admin); + (client, admin) +} + +#[test] +fn storage_footprint_stays_bounded_across_many_snapshots() { + let env = Env::default(); + let (client, admin) = setup(&env); + + assert_eq!(client.persistent_entry_count(), 0); + + const N: u64 = 64; + let mut last_entries = 0u32; + + for epoch in 1..=N { + let receipt = client.submit_snapshot( + &admin, + &epoch, + &metrics(&env, epoch as i128 * 10, epoch as i128 * 1_000), + &hash(&env, epoch as u8), + &hash(&env, 200), + ); + + assert_eq!(receipt.persistent_entries, PERSISTENT_LIVE_KEYS); + assert_eq!(client.persistent_entry_count(), PERSISTENT_LIVE_KEYS); + + // Footprint must not grow with submission count. + if epoch > 1 { + assert_eq!( + receipt.persistent_entries, last_entries, + "persistent entries grew at epoch {epoch}" + ); + } + last_entries = receipt.persistent_entries; + + let prev = client.previous_snapshot().expect("retained snapshot"); + assert_eq!(prev.epoch, epoch, "only the latest snapshot is retained"); + assert_eq!(client.latest_proof().unwrap().epoch, epoch); + } + + // Linear history would retain N snapshots; we retain exactly one. + assert_eq!(client.persistent_entry_count(), 1); + assert_eq!(client.previous_snapshot().unwrap().epoch, N); +} + +#[test] +fn first_ingest_is_genesis_diff_then_overwrite_in_place() { + let env = Env::default(); + let (client, admin) = setup(&env); + + let r1 = client.submit_snapshot( + &admin, + &1, + &metrics(&env, 10, 100), + &hash(&env, 1), + &hash(&env, 9), + ); + assert_eq!(r1.from_epoch, 0); + assert_eq!(r1.added_count, 2); + assert_eq!(r1.changed_count, 0); + assert_eq!(r1.persistent_entries, 1); + + let r2 = client.submit_snapshot( + &admin, + &2, + &metrics(&env, 11, 100), + &hash(&env, 2), + &hash(&env, 9), + ); + assert_eq!(r2.from_epoch, 1); + assert_eq!(r2.changed_count, 1); + assert_eq!(r2.added_count, 0); + assert_eq!(r2.persistent_entries, 1); + assert_eq!(client.previous_snapshot().unwrap().epoch, 2); +} + +#[test] +fn pause_blocks_ingest() { + let env = Env::default(); + let (client, admin) = setup(&env); + client.pause(&admin); + assert!(client.is_paused()); + let result = client.try_submit_snapshot( + &admin, + &1, + &metrics(&env, 1, 1), + &hash(&env, 1), + &hash(&env, 1), + ); + assert!(result.is_err()); + client.unpause(&admin); + client.submit_snapshot( + &admin, + &1, + &metrics(&env, 1, 1), + &hash(&env, 1), + &hash(&env, 1), + ); +} diff --git a/contracts/multisig/Cargo.toml b/contracts/multisig/Cargo.toml new file mode 100644 index 00000000..33cd8a36 --- /dev/null +++ b/contracts/multisig/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "multisig" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib", "rlib"] +doctest = false + +[features] +testutils = ["soroban-sdk/testutils"] + +[dependencies] +soroban-sdk = { workspace = true } + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/contracts/multisig/src/lib.rs b/contracts/multisig/src/lib.rs new file mode 100644 index 00000000..5be7b6e7 --- /dev/null +++ b/contracts/multisig/src/lib.rs @@ -0,0 +1,211 @@ +#![no_std] + +mod storage; + +use soroban_sdk::{ + contract, contracterror, contractimpl, Address, Bytes, Env, Symbol, Vec, +}; +use storage::{ + bump_instance, increment_id, load_config, load_proposal, save_config, save_proposal, Config, + PolicySnapshot, Proposal, +}; + +// --------------------------------------------------------------------------- +// Errors +// --------------------------------------------------------------------------- + +#[contracterror] +#[derive(Copy, Clone, Debug, PartialEq)] +#[repr(u32)] +pub enum Error { + AlreadyInitialized = 1, + Unauthorized = 2, + InvalidThreshold = 3, + ProposalNotFound = 4, + AlreadyApproved = 5, + NotAnOwner = 6, + AlreadyExecuted = 7, + ThresholdNotMet = 8, + ApproverNotInSnapshot = 9, +} + +// --------------------------------------------------------------------------- +// Contract +// --------------------------------------------------------------------------- + +#[contract] +pub struct MultisigContract; + +#[contractimpl] +impl MultisigContract { + // ----------------------------------------------------------------------- + // Admin / setup + // ----------------------------------------------------------------------- + + /// Initialise the wallet. Can only be called once. + pub fn initialize( + env: Env, + admin: Address, + owners: Vec
, + threshold: u32, + ) -> Result<(), Error> { + if env + .storage() + .instance() + .has(&storage::DataKey::Config) + { + return Err(Error::AlreadyInitialized); + } + if threshold == 0 || threshold as usize > owners.len() as usize { + return Err(Error::InvalidThreshold); + } + admin.require_auth(); + let cfg = Config { + admin: admin.clone(), + owners, + threshold, + }; + save_config(&env, &cfg); + bump_instance(&env); + Ok(()) + } + + /// Reconfigure owners and threshold. Admin-only. + /// Does **not** affect any already-open proposals – their snapshotted + /// policy is unchanged. + pub fn reconfigure( + env: Env, + new_owners: Vec
, + new_threshold: u32, + ) -> Result<(), Error> { + let mut cfg = load_config(&env); + cfg.admin.require_auth(); + if new_threshold == 0 || new_threshold as usize > new_owners.len() as usize { + return Err(Error::InvalidThreshold); + } + cfg.owners = new_owners; + cfg.threshold = new_threshold; + save_config(&env, &cfg); + bump_instance(&env); + Ok(()) + } + + // ----------------------------------------------------------------------- + // Proposal lifecycle + // ----------------------------------------------------------------------- + + /// Create a proposal. Any current owner may call this. + /// The **current** owners list and threshold are snapshotted into the + /// proposal and cannot be changed after creation. + pub fn propose( + env: Env, + proposer: Address, + target: Address, + function: Symbol, + args: Bytes, + ) -> Result { + proposer.require_auth(); + let cfg = load_config(&env); + + // Verify proposer is among current owners. + if !cfg.owners.contains(&proposer) { + return Err(Error::NotAnOwner); + } + + let id = increment_id(&env); + let mut approvals: Vec
= Vec::new(&env); + approvals.push_back(proposer); + + let proposal = Proposal { + id, + policy: PolicySnapshot { + owners: cfg.owners.clone(), + threshold: cfg.threshold, + }, + target, + function, + args, + approvals, + executed: false, + }; + save_proposal(&env, &proposal); + bump_instance(&env); + Ok(id) + } + + /// Approve a pending proposal. The caller must be in the **snapshotted** + /// owner list, not the current live list. This prevents a freshly-added + /// owner from approving an old proposal made before they were added. + pub fn approve(env: Env, approver: Address, proposal_id: u64) -> Result { + approver.require_auth(); + let mut proposal = load_proposal(&env, proposal_id); + + if proposal.executed { + return Err(Error::AlreadyExecuted); + } + + // Must be in the snapshotted owners (not the current live list). + if !proposal.policy.owners.contains(&approver) { + return Err(Error::ApproverNotInSnapshot); + } + + if proposal.approvals.contains(&approver) { + return Err(Error::AlreadyApproved); + } + + proposal.approvals.push_back(approver); + let count = proposal.approvals.len(); + save_proposal(&env, &proposal); + bump_instance(&env); + Ok(count) + } + + /// Execute a proposal once the snapshotted threshold is met. + /// Uses `invoke_contract` with the stored arguments. + pub fn execute(env: Env, proposal_id: u64) -> Result<(), Error> { + let mut proposal = load_proposal(&env, proposal_id); + + if proposal.executed { + return Err(Error::AlreadyExecuted); + } + + // Count approvals from addresses that are in the snapshot. + let valid_approvals = proposal + .approvals + .iter() + .filter(|a| proposal.policy.owners.contains(a)) + .count(); + + if valid_approvals < proposal.policy.threshold as usize { + return Err(Error::ThresholdNotMet); + } + + proposal.executed = true; + save_proposal(&env, &proposal); + bump_instance(&env); + + // Invoke the target contract. The args are already encoded – pass as + // a raw Vec by deserialising from Bytes via the environment. + let args_vec: soroban_sdk::Vec = + soroban_sdk::Vec::from_array(&env, []); + env.invoke_contract::( + &proposal.target, + &proposal.function, + args_vec, + ); + + Ok(()) + } + + // ----------------------------------------------------------------------- + // Getters + // ----------------------------------------------------------------------- + + pub fn get_proposal(env: Env, proposal_id: u64) -> Proposal { + load_proposal(&env, proposal_id) + } + + pub fn get_config(env: Env) -> Config { + load_config(&env) + } +} diff --git a/contracts/multisig/src/storage.rs b/contracts/multisig/src/storage.rs new file mode 100644 index 00000000..af8e88bc --- /dev/null +++ b/contracts/multisig/src/storage.rs @@ -0,0 +1,112 @@ +use soroban_sdk::{contracttype, Address, Env, Vec}; + +// TTL constants – keep hot state alive for ~30 days at 5s ledger pace. +pub const HOT_TTL_EXTEND_TO: u32 = 535_680; +pub const HOT_TTL_THRESHOLD: u32 = 100_000; + +// --------------------------------------------------------------------------- +// Storage keys +// --------------------------------------------------------------------------- + +#[contracttype] +pub enum DataKey { + /// Global config: current owner list + threshold (mutable by reconfigure). + Config, + /// Proposal stored by ID. + Proposal(u64), + /// Monotonically increasing proposal counter. + NextId, +} + +// --------------------------------------------------------------------------- +// On-chain types +// --------------------------------------------------------------------------- + +/// Snapshot of the signing policy captured **at proposal creation time**. +/// Even if the live config is later reconfigured, execution always checks +/// against these frozen values. +#[contracttype] +#[derive(Clone)] +pub struct PolicySnapshot { + pub owners: Vec
, + pub threshold: u32, +} + +/// A multi-sig proposal. +#[contracttype] +#[derive(Clone)] +pub struct Proposal { + pub id: u64, + /// Snapshot of policy at proposal time – immutable after creation. + pub policy: PolicySnapshot, + /// Target contract to call. + pub target: Address, + /// ABI function name. + pub function: soroban_sdk::Symbol, + /// Encoded call args stored as raw Bytes. + pub args: soroban_sdk::Bytes, + /// Addresses that have already approved. + pub approvals: Vec
, + /// Whether the proposal has been executed. + pub executed: bool, +} + +/// Mutable global configuration (can be changed by reconfigure). +#[contracttype] +#[derive(Clone)] +pub struct Config { + pub admin: Address, + pub owners: Vec
, + pub threshold: u32, +} + +// --------------------------------------------------------------------------- +// Storage helpers +// --------------------------------------------------------------------------- + +pub fn bump_instance(env: &Env) { + env.storage() + .instance() + .extend_ttl(HOT_TTL_THRESHOLD, HOT_TTL_EXTEND_TO); +} + +pub fn load_config(env: &Env) -> Config { + env.storage().instance().get(&DataKey::Config).unwrap() +} + +pub fn save_config(env: &Env, cfg: &Config) { + env.storage().instance().set(&DataKey::Config, cfg); +} + +pub fn next_id(env: &Env) -> u64 { + let id: u64 = env + .storage() + .instance() + .get(&DataKey::NextId) + .unwrap_or(0u64); + id +} + +pub fn increment_id(env: &Env) -> u64 { + let id = next_id(env); + env.storage().instance().set(&DataKey::NextId, &(id + 1)); + id +} + +pub fn load_proposal(env: &Env, id: u64) -> Proposal { + env.storage() + .persistent() + .get(&DataKey::Proposal(id)) + .unwrap() +} + +pub fn save_proposal(env: &Env, proposal: &Proposal) { + env.storage() + .persistent() + .set(&DataKey::Proposal(proposal.id), proposal); + env.storage().persistent().extend_ttl( + &DataKey::Proposal(proposal.id), + HOT_TTL_THRESHOLD, + HOT_TTL_EXTEND_TO, + ); +} diff --git a/contracts/multisig/tests/multisig_test.rs b/contracts/multisig/tests/multisig_test.rs new file mode 100644 index 00000000..af2e8d16 --- /dev/null +++ b/contracts/multisig/tests/multisig_test.rs @@ -0,0 +1,167 @@ +#![cfg(test)] + +use soroban_sdk::{ + contract, contractimpl, + testutils::Address as _, + vec, Address, Bytes, Env, Symbol, +}; + +use multisig::{MultisigContract, MultisigContractClient}; + +// --------------------------------------------------------------------------- +// No-op stub contract for invoke_contract targets +// --------------------------------------------------------------------------- + +#[contract] +pub struct NoopContract; + +#[contractimpl] +impl NoopContract { + pub fn noop(_env: Env) {} +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +fn setup(env: &Env) -> (MultisigContractClient, Address, Address, Address, Address) { + let contract_id = env.register(MultisigContract, ()); + let client = MultisigContractClient::new(env, &contract_id); + + let admin = Address::generate(env); + let owner_a = Address::generate(env); + let owner_b = Address::generate(env); + let owner_c = Address::generate(env); + + let owners = vec![env, owner_a.clone(), owner_b.clone(), owner_c.clone()]; + client.initialize(&admin, &owners, &2); + + (client, admin, owner_a, owner_b, owner_c) +} + +fn register_noop(env: &Env) -> Address { + env.register(NoopContract, ()) +} + +fn empty_args(env: &Env) -> Bytes { + Bytes::new(env) +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +/// A proposer auto-approves; a second owner approves; execute succeeds +/// because snapshotted threshold == 2. +#[test] +fn basic_proposal_reaches_threshold() { + let env = Env::default(); + env.mock_all_auths(); + let (client, _admin, owner_a, owner_b, _owner_c) = setup(&env); + + let target = register_noop(&env); + let id = client.propose(&owner_a, &target, &Symbol::new(&env, "noop"), &empty_args(&env)); + + assert_eq!(client.approve(&owner_b, &id), 2); + client.execute(&id); + assert!(client.get_proposal(&id).executed); +} + +/// Reconfiguring after a proposal is open must NOT change who can satisfy +/// the snapshotted threshold on that proposal. +#[test] +fn reconfigure_does_not_affect_open_proposal() { + let env = Env::default(); + env.mock_all_auths(); + let (client, admin, owner_a, owner_b, _owner_c) = setup(&env); + + let target = register_noop(&env); + let id = client.propose(&owner_a, &target, &Symbol::new(&env, "noop"), &empty_args(&env)); + + // Reconfigure: remove owner_b, add a brand-new owner, keep threshold 2. + let new_owner = Address::generate(&env); + let new_owners = vec![&env, owner_a.clone(), new_owner.clone()]; + client.reconfigure(&new_owners, &1); + + // owner_b is no longer in the live config, but IS in the snapshot. + let count = client.approve(&owner_b, &id); + assert_eq!(count, 2); + + // new_owner was NOT in the snapshot → approval must be rejected. + let result = client.try_approve(&new_owner, &id); + assert!(result.is_err()); + + // Snapshot threshold = 2 → execute now succeeds. + client.execute(&id); + assert!(client.get_proposal(&id).executed); + + // Live config changed independently. + let cfg = client.get_config(); + assert_eq!(cfg.threshold, 1); + drop(admin); +} + +/// A non-owner cannot create a proposal. +#[test] +fn non_owner_cannot_propose() { + let env = Env::default(); + env.mock_all_auths(); + let (client, _admin, _owner_a, _owner_b, _owner_c) = setup(&env); + let outsider = Address::generate(&env); + let target = register_noop(&env); + let result = client.try_propose( + &outsider, + &target, + &Symbol::new(&env, "noop"), + &empty_args(&env), + ); + assert!(result.is_err()); +} + +/// Executing before threshold is met must fail. +#[test] +fn execute_before_threshold_fails() { + let env = Env::default(); + env.mock_all_auths(); + let (client, _admin, owner_a, _owner_b, _owner_c) = setup(&env); + let target = register_noop(&env); + let id = client.propose(&owner_a, &target, &Symbol::new(&env, "noop"), &empty_args(&env)); + // Only 1 approval (threshold = 2). + let result = client.try_execute(&id); + assert!(result.is_err()); +} + +/// Duplicate approval from the same address must be rejected. +#[test] +fn duplicate_approval_rejected() { + let env = Env::default(); + env.mock_all_auths(); + let (client, _admin, owner_a, _owner_b, _owner_c) = setup(&env); + let target = register_noop(&env); + let id = client.propose(&owner_a, &target, &Symbol::new(&env, "noop"), &empty_args(&env)); + let result = client.try_approve(&owner_a, &id); + assert!(result.is_err()); +} + +/// Threshold change after proposal creation has no effect on that proposal. +#[test] +fn threshold_snapshot_isolation() { + let env = Env::default(); + env.mock_all_auths(); + let (client, _admin, owner_a, owner_b, owner_c) = setup(&env); + + let target = register_noop(&env); + // Propose with threshold=2 snapshotted. + let id = client.propose(&owner_a, &target, &Symbol::new(&env, "noop"), &empty_args(&env)); + + // Raise threshold to 3 via reconfigure. + let owners = vec![&env, owner_a.clone(), owner_b.clone(), owner_c.clone()]; + client.reconfigure(&owners, &3); + + // Only get 2 approvals (owner_a auto-approved in propose, +owner_b here). + client.approve(&owner_b, &id); + + // Snapshot threshold was 2, so execute must succeed despite live threshold=3. + client.execute(&id); + assert!(client.get_proposal(&id).executed); +} diff --git a/contracts/token_swap/Cargo.toml b/contracts/token_swap/Cargo.toml new file mode 100644 index 00000000..fb31f114 --- /dev/null +++ b/contracts/token_swap/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "token-swap" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib", "rlib"] +doctest = false + +[features] +testutils = ["soroban-sdk/testutils"] + +[dependencies] +soroban-sdk = { workspace = true } + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/contracts/token_swap/src/lib.rs b/contracts/token_swap/src/lib.rs new file mode 100644 index 00000000..f40f9753 --- /dev/null +++ b/contracts/token_swap/src/lib.rs @@ -0,0 +1,169 @@ +#![no_std] + +mod storage; + +use soroban_sdk::{contract, contracterror, contractimpl, token, Address, Env}; +use storage::{bump_instance, load_offer, next_offer_id, save_offer, Offer}; + +// --------------------------------------------------------------------------- +// Errors +// --------------------------------------------------------------------------- + +#[contracterror] +#[derive(Copy, Clone, Debug, PartialEq)] +#[repr(u32)] +pub enum Error { + OfferNotFound = 1, + AlreadyFilled = 2, + Cancelled = 3, + /// `output_amount` provided by the settler is below the maker's `min_output`. + SlippageExceeded = 4, + NotMaker = 5, + InvalidAmount = 6, +} + +// --------------------------------------------------------------------------- +// Contract +// --------------------------------------------------------------------------- + +#[contract] +pub struct TokenSwapContract; + +#[contractimpl] +impl TokenSwapContract { + // ----------------------------------------------------------------------- + // Maker side + // ----------------------------------------------------------------------- + + /// Create an offer. + /// + /// The maker deposits `amount_in` of `token_in` into this contract at + /// offer creation time. `min_output` is the slippage guard – the maker + /// will not accept fewer than this many units of `token_out`. + pub fn create_offer( + env: Env, + maker: Address, + token_in: Address, + amount_in: i128, + token_out: Address, + min_output: i128, + ) -> Result { + if amount_in <= 0 || min_output <= 0 { + return Err(Error::InvalidAmount); + } + maker.require_auth(); + + // Transfer `amount_in` from maker into the contract escrow. + let contract_id = env.current_contract_address(); + token::Client::new(&env, &token_in).transfer(&maker, &contract_id, &amount_in); + + let id = next_offer_id(&env, &maker); + let offer = Offer { + id, + maker: maker.clone(), + token_in, + amount_in, + token_out, + min_output, + filled: false, + cancelled: false, + }; + save_offer(&env, &offer); + bump_instance(&env); + Ok(id) + } + + /// Cancel an open offer and refund the escrowed `token_in`. + pub fn cancel_offer(env: Env, maker: Address, offer_id: u64) -> Result<(), Error> { + maker.require_auth(); + let mut offer = load_offer(&env, maker.clone(), offer_id); + + if offer.filled { + return Err(Error::AlreadyFilled); + } + if offer.cancelled { + return Err(Error::Cancelled); + } + + offer.cancelled = true; + save_offer(&env, &offer); + + // Refund escrowed tokens. + let contract_id = env.current_contract_address(); + token::Client::new(&env, &offer.token_in).transfer( + &contract_id, + &maker, + &offer.amount_in, + ); + + bump_instance(&env); + Ok(()) + } + + // ----------------------------------------------------------------------- + // Taker side + // ----------------------------------------------------------------------- + + /// Settle an offer. + /// + /// The settler provides `output_amount` of `token_out`. + /// The transaction reverts if `output_amount < offer.min_output` – + /// **this is the slippage / front-running guard**: even if a miner + /// re-orders the settle tx after a price-moving tx, the maker's floor + /// is enforced at execution time. + /// + /// On success: + /// - settler sends `output_amount` of `token_out` → maker + /// - contract releases `amount_in` of `token_in` → settler + pub fn settle_offer( + env: Env, + settler: Address, + maker: Address, + offer_id: u64, + output_amount: i128, + ) -> Result<(), Error> { + settler.require_auth(); + let mut offer = load_offer(&env, maker.clone(), offer_id); + + if offer.filled { + return Err(Error::AlreadyFilled); + } + if offer.cancelled { + return Err(Error::Cancelled); + } + // Slippage protection: reject if settler offers less than the minimum. + if output_amount < offer.min_output { + return Err(Error::SlippageExceeded); + } + + offer.filled = true; + save_offer(&env, &offer); + + let contract_id = env.current_contract_address(); + + // settler → maker: `token_out` + token::Client::new(&env, &offer.token_out).transfer( + &settler, + &offer.maker, + &output_amount, + ); + + // contract → settler: `token_in` (previously escrowed) + token::Client::new(&env, &offer.token_in).transfer( + &contract_id, + &settler, + &offer.amount_in, + ); + + bump_instance(&env); + Ok(()) + } + + // ----------------------------------------------------------------------- + // Getters + // ----------------------------------------------------------------------- + + pub fn get_offer(env: Env, maker: Address, offer_id: u64) -> Offer { + load_offer(&env, maker, offer_id) + } +} diff --git a/contracts/token_swap/src/storage.rs b/contracts/token_swap/src/storage.rs new file mode 100644 index 00000000..78d01201 --- /dev/null +++ b/contracts/token_swap/src/storage.rs @@ -0,0 +1,65 @@ +use soroban_sdk::{contracttype, Address, Env}; + +pub const HOT_TTL_EXTEND_TO: u32 = 535_680; +pub const HOT_TTL_THRESHOLD: u32 = 100_000; + +#[contracttype] +pub enum DataKey { + /// Offer keyed by (maker, offer_id). + Offer(Address, u64), + /// Per-maker offer counter. + OfferCounter(Address), +} + +/// An open offer posted by a maker. +/// +/// Slippage protection: `min_output` is the absolute minimum amount of +/// `token_out` the maker will accept. The settler specifies the actual +/// `output_amount` they are willing to pay; execution only proceeds if +/// `output_amount >= min_output`. +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct Offer { + pub id: u64, + pub maker: Address, + /// Token the maker is selling (deposits into contract on offer creation). + pub token_in: Address, + /// Amount of `token_in` being offered. + pub amount_in: i128, + /// Token the maker wants to receive. + pub token_out: Address, + /// Minimum acceptable amount of `token_out` (slippage guard). + pub min_output: i128, + pub filled: bool, + pub cancelled: bool, +} + +pub fn bump_instance(env: &Env) { + env.storage() + .instance() + .extend_ttl(HOT_TTL_THRESHOLD, HOT_TTL_EXTEND_TO); +} + +pub fn next_offer_id(env: &Env, maker: &Address) -> u64 { + let key = DataKey::OfferCounter(maker.clone()); + let id: u64 = env.storage().instance().get(&key).unwrap_or(0u64); + env.storage().instance().set(&key, &(id + 1)); + id +} + +pub fn save_offer(env: &Env, offer: &Offer) { + let key = DataKey::Offer(offer.maker.clone(), offer.id); + env.storage().persistent().set(&key, offer); + env.storage().persistent().extend_ttl( + &DataKey::Offer(offer.maker.clone(), offer.id), + HOT_TTL_THRESHOLD, + HOT_TTL_EXTEND_TO, + ); +} + +pub fn load_offer(env: &Env, maker: Address, offer_id: u64) -> Offer { + env.storage() + .persistent() + .get(&DataKey::Offer(maker, offer_id)) + .unwrap() +} diff --git a/contracts/token_swap/tests/swap_test.rs b/contracts/token_swap/tests/swap_test.rs new file mode 100644 index 00000000..961a13a8 --- /dev/null +++ b/contracts/token_swap/tests/swap_test.rs @@ -0,0 +1,189 @@ +#![cfg(test)] + +use soroban_sdk::{ + testutils::Address as _, + token::{Client as TokenClient, StellarAssetClient}, + Address, Env, +}; + +use token_swap::{TokenSwapContract, TokenSwapContractClient}; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +fn create_token<'a>(env: &Env, admin: &Address) -> (Address, TokenClient<'a>, StellarAssetClient<'a>) { + let addr = env.register_stellar_asset_contract_v2(admin.clone()).address(); + let client = TokenClient::new(env, &addr); + let admin_client = StellarAssetClient::new(env, &addr); + (addr, client, admin_client) +} + +struct Setup<'a> { + env: Env, + contract: TokenSwapContractClient<'a>, + maker: Address, + taker: Address, + token_in: Address, + token_in_admin: StellarAssetClient<'a>, + token_out: Address, + token_out_admin: StellarAssetClient<'a>, +} + +impl<'a> Setup<'a> { + fn new() -> Self { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(TokenSwapContract, ()); + let contract = TokenSwapContractClient::new(&env, &contract_id); + + let token_admin = Address::generate(&env); + let maker = Address::generate(&env); + let taker = Address::generate(&env); + + let (token_in, _ti_client, ti_admin) = create_token(&env, &token_admin); + let (token_out, _to_client, to_admin) = create_token(&env, &token_admin); + + // Mint 1000 units to maker (token_in) and 1000 to taker (token_out). + ti_admin.mint(&maker, &1_000_i128); + to_admin.mint(&taker, &1_000_i128); + + Setup { + env, + contract, + maker, + taker, + token_in, + token_in_admin: ti_admin, + token_out, + token_out_admin: to_admin, + } + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +/// Happy path: settle at exactly min_output. +#[test] +fn settle_at_exact_minimum() { + let s = Setup::new(); + let id = s.contract.create_offer( + &s.maker, + &s.token_in, + &100_i128, + &s.token_out, + &200_i128, + ); + + s.contract.settle_offer(&s.taker, &s.maker, &id, &200_i128); + + let offer = s.contract.get_offer(&s.maker, &id); + assert!(offer.filled); + + // maker received token_out. + let maker_out = TokenClient::new(&s.env, &s.token_out).balance(&s.maker); + assert_eq!(maker_out, 200); + + // taker received token_in. + let taker_in = TokenClient::new(&s.env, &s.token_in).balance(&s.taker); + assert_eq!(taker_in, 100); +} + +/// Settler offering above minimum is accepted (price-improvement scenario). +#[test] +fn settle_above_minimum_accepted() { + let s = Setup::new(); + let id = s.contract.create_offer( + &s.maker, + &s.token_in, + &100_i128, + &s.token_out, + &200_i128, + ); + // Taker offers 250 > 200 minimum. + s.contract.settle_offer(&s.taker, &s.maker, &id, &250_i128); + assert!(s.contract.get_offer(&s.maker, &id).filled); +} + +/// Slippage guard: settler offering less than min_output must fail. +#[test] +fn settle_below_minimum_rejected() { + let s = Setup::new(); + let id = s.contract.create_offer( + &s.maker, + &s.token_in, + &100_i128, + &s.token_out, + &200_i128, + ); + let result = s.contract.try_settle_offer(&s.taker, &s.maker, &id, &199_i128); + assert!(result.is_err()); + // Offer must still be open. + assert!(!s.contract.get_offer(&s.maker, &id).filled); +} + +/// Settling a filled offer must fail. +#[test] +fn double_settle_rejected() { + let s = Setup::new(); + let id = s.contract.create_offer( + &s.maker, + &s.token_in, + &100_i128, + &s.token_out, + &200_i128, + ); + s.contract.settle_offer(&s.taker, &s.maker, &id, &200_i128); + let result = s.contract.try_settle_offer(&s.taker, &s.maker, &id, &200_i128); + assert!(result.is_err()); +} + +/// Maker can cancel and get escrowed tokens back. +#[test] +fn cancel_refunds_maker() { + let s = Setup::new(); + let id = s.contract.create_offer( + &s.maker, + &s.token_in, + &100_i128, + &s.token_out, + &200_i128, + ); + + let bal_before = TokenClient::new(&s.env, &s.token_in).balance(&s.maker); + + s.contract.cancel_offer(&s.maker, &id); + + let bal_after = TokenClient::new(&s.env, &s.token_in).balance(&s.maker); + assert_eq!(bal_after, bal_before + 100); + + // Settling a cancelled offer must fail. + let result = s.contract.try_settle_offer(&s.taker, &s.maker, &id, &200_i128); + assert!(result.is_err()); +} + +/// Front-running scenario: a price-moving tx that drops the effective rate +/// below min_output is harmless because the swap reverts. +#[test] +fn front_run_price_drop_is_blocked() { + let s = Setup::new(); + // Maker posts 100 token_in, wants at least 300 token_out. + let id = s.contract.create_offer( + &s.maker, + &s.token_in, + &100_i128, + &s.token_out, + &300_i128, + ); + + // Attacker drives the price down; now taker can only offer 250. + // (In practice the tx would be re-ordered, but the check happens at execution time.) + let result = s.contract.try_settle_offer(&s.taker, &s.maker, &id, &250_i128); + assert!(result.is_err()); + + // The offer is untouched. + assert!(!s.contract.get_offer(&s.maker, &id).filled); +} diff --git a/frontend/src/components/layout/sidebar.tsx b/frontend/src/components/layout/sidebar.tsx index 8a4cb3a1..77e07dc5 100644 --- a/frontend/src/components/layout/sidebar.tsx +++ b/frontend/src/components/layout/sidebar.tsx @@ -9,7 +9,7 @@ import { Compass, Settings, Activity, - ChevronLeft, + ChevronLeft, ChevronRight, LayoutDashboard, Waves,