From d3c6ef7c563ed4f3aa269de03b7a7556a749abf3 Mon Sep 17 00:00:00 2001 From: portableDD <65081431+portableDD@users.noreply.github.com> Date: Mon, 27 Jul 2026 10:57:52 +0100 Subject: [PATCH] Align crate naming, build all WASM in CI, upgrade SDK, type events SC-33: crate naming - git mv multisig_transfer -> multisig-transfer so the directory matches the package name. `cargo test -p ` now works for all five crates. - Normalize the workspace members list: bare paths, alphabetical, no ./ prefix. - Record the convention alongside the members list. SC-34: WASM coverage in CI - build-wasm now builds the whole workspace for wasm32-unknown-unknown rather than only `contrib`. All five crates declare a cdylib crate-type and a #[contract] entrypoint, so all five are deployable and are now verified. - Report per-contract WASM sizes to the job summary and upload the .wasm artifacts. SC-35: soroban-sdk 22 -> 23 - Bumps to 23.5.3, which resolves and passes the suite with no source changes needed for the upgrade itself. - Not 27: it removes ContractEvents::len, breaking the test API, and raises 70 clippy errors. 23 is also the version the sibling ManageHub project runs and is protocol-aligned. Moving further should be its own change. SC-36: standardize event emission - SDK 23 deprecates env.events().publish() in favour of #[contractevent], so all 62 call sites warned and CI runs clippy with -D warnings. This made the two issues one piece of work, which is the order they were sequenced in. - Every event is now a typed #[contractevent] struct in a per-crate events.rs, the only place events are published from. Struct names are , the indexable identifier is a #[topic], and the types land in the contract spec so clients can generate bindings instead of decoding positional tuples. - Replaces a mix of symbol_short!, Symbol::new, and bare string-tuple topics in CamelCase and snake_case with one convention. - Adds events to entrypoints that changed state silently, including every initialize, the registrar allowlist changes, the multisig propose/confirm flow, and set_daily_limit. - Splits asset-maintenance's shared create/update paths so an update is distinguishable from a create off-chain. - Publishes the catalogue in contracts/EVENTS.md for the backend bridge. - Adds 13 event-emission tests. The existing contrib event test compared event counts before and after a call. Under SDK 23 env.events().all() reports only the most recent invocation, so that comparison was meaningless; it now asserts the event name and indexed topic. This behaviour is documented in EVENTS.md. Two findings recorded while inventorying events, both out of scope here: contrib's escrow, KYC, staking, oracle, tokenization, detokenization, restrictions and error modules are not declared in lib.rs and so are not compiled (~1,670 lines), and assetsup's register/update/transfer/retire check a caller-supplied address without calling require_auth on it. Closes #1197 Closes #1198 Closes #1199 Closes #1200 --- .github/workflows/CI.yaml | 29 +- contracts/Cargo.lock | 198 ++++-- contracts/Cargo.toml | 11 +- contracts/EVENTS.md | 222 +++++++ contracts/asset-maintenance/src/events.rs | 246 ++++++++ contracts/asset-maintenance/src/lib.rs | 99 +-- contracts/assetsup/src/detokenization.rs | 5 +- contracts/assetsup/src/dividends.rs | 8 +- contracts/assetsup/src/events.rs | 580 ++++++++++++++++++ contracts/assetsup/src/lease.rs | 20 +- contracts/assetsup/src/lib.rs | 50 +- contracts/assetsup/src/tests/events.rs | 162 +++++ contracts/assetsup/src/tests/mod.rs | 1 + contracts/assetsup/src/tokenization.rs | 31 +- .../assetsup/src/transfer_restrictions.rs | 11 +- contracts/assetsup/src/voting.rs | 5 +- contracts/contrib/src/events.rs | 331 ++++++++++ contracts/contrib/src/insurance.rs | 26 +- contracts/contrib/src/lease.rs | 17 +- contracts/contrib/src/lib.rs | 30 +- contracts/contrib/src/pause.rs | 4 +- contracts/contrib/src/tests/mod.rs | 33 +- .../Cargo.toml | 0 .../src/approvals.rs | 0 .../src/errors.rs | 0 contracts/multisig-transfer/src/events.rs | 224 +++++++ .../src/lib.rs | 2 + .../src/registry.rs | 0 .../src/rules.rs | 0 .../src/storage.rs | 0 .../src/types.rs | 0 .../src/utils.rs | 0 contracts/multisig-wallet/src/event_tests.rs | 162 +++++ contracts/multisig-wallet/src/events.rs | 308 ++++++++++ contracts/multisig-wallet/src/lib.rs | 81 +-- contracts/multisig_transfer/src/events.rs | 84 --- 36 files changed, 2599 insertions(+), 381 deletions(-) create mode 100644 contracts/EVENTS.md create mode 100644 contracts/asset-maintenance/src/events.rs create mode 100644 contracts/assetsup/src/events.rs create mode 100644 contracts/assetsup/src/tests/events.rs create mode 100644 contracts/contrib/src/events.rs rename contracts/{multisig_transfer => multisig-transfer}/Cargo.toml (100%) rename contracts/{multisig_transfer => multisig-transfer}/src/approvals.rs (100%) rename contracts/{multisig_transfer => multisig-transfer}/src/errors.rs (100%) create mode 100644 contracts/multisig-transfer/src/events.rs rename contracts/{multisig_transfer => multisig-transfer}/src/lib.rs (99%) rename contracts/{multisig_transfer => multisig-transfer}/src/registry.rs (100%) rename contracts/{multisig_transfer => multisig-transfer}/src/rules.rs (100%) rename contracts/{multisig_transfer => multisig-transfer}/src/storage.rs (100%) rename contracts/{multisig_transfer => multisig-transfer}/src/types.rs (100%) rename contracts/{multisig_transfer => multisig-transfer}/src/utils.rs (100%) create mode 100644 contracts/multisig-wallet/src/event_tests.rs create mode 100644 contracts/multisig-wallet/src/events.rs delete mode 100644 contracts/multisig_transfer/src/events.rs diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index c4e53b555..c793cf8cf 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -117,8 +117,33 @@ jobs: restore-keys: | ${{ runner.os }}-cargo-wasm- ${{ runner.os }}-cargo- - - name: Build contrib contract to WASM - run: cargo build --package contrib --target wasm32-unknown-unknown --release --verbose + # Every crate in the workspace is a deployable contract: each declares a + # `cdylib` crate-type and a `#[contract]` entrypoint. Building the whole + # workspace for wasm32 therefore fails the job if any contract cannot be + # deployed, which native compilation alone does not guarantee. + - name: Build all contracts to WASM + run: cargo build --workspace --target wasm32-unknown-unknown --release --verbose + + - name: Report WASM artifact sizes + run: | + echo "### WASM artifact sizes" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "| Contract | Size (bytes) |" >> "$GITHUB_STEP_SUMMARY" + echo "|---|---:|" >> "$GITHUB_STEP_SUMMARY" + for wasm in target/wasm32-unknown-unknown/release/*.wasm; do + name=$(basename "$wasm" .wasm) + size=$(wc -c < "$wasm" | tr -d ' ') + printf '| %s | %s |\n' "$name" "$size" >> "$GITHUB_STEP_SUMMARY" + done + cat "$GITHUB_STEP_SUMMARY" + + - name: Upload WASM artifacts + uses: actions/upload-artifact@v4 + with: + name: contract-wasm + path: contracts/target/wasm32-unknown-unknown/release/*.wasm + if-no-files-found: error + retention-days: 14 # ───────────────────────────────────────────── # BACKEND — NestJS diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index d697505f6..03925589c 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -176,12 +176,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - [[package]] name = "base64" version = "0.22.1" @@ -209,6 +203,12 @@ version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +[[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.5" @@ -237,6 +237,17 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +[[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.106", +] + [[package]] name = "chrono" version = "0.4.42" @@ -313,14 +324,20 @@ dependencies = [ [[package]] name = "ctor" -version = "0.2.9" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +checksum = "67773048316103656a637612c4a62477603b777d91d9c62ff2290f9cde178fdb" dependencies = [ - "quote", - "syn 2.0.106", + "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" @@ -484,6 +501,21 @@ 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" @@ -633,6 +665,15 @@ dependencies = [ "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" @@ -654,6 +695,22 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +[[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" @@ -801,6 +858,17 @@ version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +[[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.106", +] + [[package]] name = "memchr" version = "2.7.6" @@ -1033,6 +1101,17 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[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" @@ -1125,11 +1204,12 @@ version = "3.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e" dependencies = [ - "base64 0.22.1", + "base64", "chrono", "hex", "indexmap 1.9.3", "indexmap 2.11.4", + "schemars 0.8.22", "schemars 0.9.0", "schemars 1.0.4", "serde", @@ -1196,9 +1276,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "soroban-builtin-sdk-macros" -version = "22.1.3" +version = "23.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf2e42bf80fcdefb3aae6ff3c7101a62cf942e95320ed5b518a1705bc11c6b2f" +checksum = "d9336adeabcd6f636a4e0889c8baf494658ef5a3c4e7e227569acd2ce9091e85" dependencies = [ "itertools", "proc-macro2", @@ -1208,9 +1288,9 @@ dependencies = [ [[package]] name = "soroban-env-common" -version = "22.1.3" +version = "23.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "027cd856171bfd6ad2c0ffb3b7dfe55ad7080fb3050c36ad20970f80da634472" +checksum = "00067f52e8bbf1abf0de03fe3e2fbb06910893cfbe9a7d9093d6425658833ff3" dependencies = [ "arbitrary", "crate-git-revision", @@ -1227,9 +1307,9 @@ dependencies = [ [[package]] name = "soroban-env-guest" -version = "22.1.3" +version = "23.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a07dda1ae5220d975979b19ad4fd56bc86ec7ec1b4b25bc1c5d403f934e592e" +checksum = "ccd1e40963517b10963a8e404348d3fe6caf9c278ac47a6effd48771297374d6" dependencies = [ "soroban-env-common", "static_assertions", @@ -1237,9 +1317,9 @@ dependencies = [ [[package]] name = "soroban-env-host" -version = "22.1.3" +version = "23.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66e8b03a4191d485eab03f066336112b2a50541a7553179553dc838b986b94dd" +checksum = "b9766c5ad78e9d8ae10afbc076301f7d610c16407a1ebb230766dbe007a48725" dependencies = [ "ark-bls12-381", "ark-ec", @@ -1267,15 +1347,15 @@ dependencies = [ "soroban-env-common", "soroban-wasmi", "static_assertions", - "stellar-strkey", + "stellar-strkey 0.0.13", "wasmparser", ] [[package]] name = "soroban-env-macros" -version = "22.1.3" +version = "23.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00eff744764ade3bc480e4909e3a581a240091f3d262acdce80b41f7069b2bd9" +checksum = "b0e6a1c5844257ce96f5f54ef976035d5bd0ee6edefaf9f5e0bcb8ea4b34228c" dependencies = [ "itertools", "proc-macro2", @@ -1288,9 +1368,9 @@ dependencies = [ [[package]] name = "soroban-ledger-snapshot" -version = "22.0.10" +version = "23.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f420c93b0e44c6d08207adf86b6c608a95b272440f55737f20dcb3812e1980f" +checksum = "fea7299402f5f445089fde192cc68587baf0cc6432be300bce99d997fd85b4cb" dependencies = [ "serde", "serde_json", @@ -1302,12 +1382,13 @@ dependencies = [ [[package]] name = "soroban-sdk" -version = "22.0.10" +version = "23.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d702069aa16d36dee1d6b3c75e4bfae8e2692f2cbc16496652757d18d8533f7e" +checksum = "dd1e11f9fd537f9df29ec1a66c1b78d666094df56e196565d292ebf9a72732b4" dependencies = [ "arbitrary", "bytes-lit", + "crate-git-revision", "ctor", "derive_arbitrary", "ed25519-dalek", @@ -1319,21 +1400,22 @@ dependencies = [ "soroban-env-host", "soroban-ledger-snapshot", "soroban-sdk-macros", - "stellar-strkey", + "stellar-strkey 0.0.16", + "visibility", ] [[package]] name = "soroban-sdk-macros" -version = "22.0.10" +version = "23.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "965b977c52d16dd8dfd9074702583e5a5778f230cda0fa78b2ea743e9149ac8d" +checksum = "caa7114e2f031b6fbd30376e844f3c55f6daf56f6f9d33ce309f846ffced316d" dependencies = [ - "crate-git-revision", "darling 0.20.11", + "heck", "itertools", + "macro-string", "proc-macro2", "quote", - "rustc_version", "sha2", "soroban-env-common", "soroban-spec", @@ -1344,11 +1426,11 @@ dependencies = [ [[package]] name = "soroban-spec" -version = "22.0.10" +version = "23.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4ead5de5373f3278707c06142a12bd6bcf4cbe211a89a74904ae238b3aaabc5" +checksum = "1303589e67e7c76d0571b8d797b75f9fa33a1ceb1b4361a981570a3ebf00ac19" dependencies = [ - "base64 0.13.1", + "base64", "stellar-xdr", "thiserror", "wasmparser", @@ -1356,9 +1438,9 @@ dependencies = [ [[package]] name = "soroban-spec-rust" -version = "22.0.10" +version = "23.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9dd041c94ce48aa9f7f115e748ee9fa5b9f263df4a70178f060dd0876e15d11" +checksum = "956188f28b750b80a2bb4cd5698038746edae1be51ec19a29c7efc6dcd922e5f" dependencies = [ "prettyplease", "proc-macro2", @@ -1399,6 +1481,12 @@ dependencies = [ "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" @@ -1407,29 +1495,42 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "stellar-strkey" -version = "0.0.9" +version = "0.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e3aa3ed00e70082cb43febc1c2afa5056b9bb3e348bbb43d0cd0aa88a611144" +checksum = "ee1832fb50c651ad10f734aaf5d31ca5acdfb197a6ecda64d93fcdb8885af913" dependencies = [ "crate-git-revision", "data-encoding", - "thiserror", +] + +[[package]] +name = "stellar-strkey" +version = "0.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "084afcb0d458c3d5d5baa2d294b18f881e62cc258ef539d8fdf68be7dbe45520" +dependencies = [ + "crate-git-revision", + "data-encoding", + "heapless", ] [[package]] name = "stellar-xdr" -version = "22.1.0" +version = "23.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ce69db907e64d1e70a3dce8d4824655d154749426a6132b25395c49136013e4" +checksum = "89d2848e1694b0c8db81fd812bfab5ea71ee28073e09ccc45620ef3cf7a75a9b" dependencies = [ "arbitrary", - "base64 0.13.1", + "base64", + "cfg_eval", "crate-git-revision", "escape-bytes", + "ethnum", "hex", "serde", "serde_with", - "stellar-strkey", + "sha2", + "stellar-strkey 0.0.13", ] [[package]] @@ -1535,6 +1636,17 @@ 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.106", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 62be99f9c..e309bd3aa 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -1,15 +1,20 @@ [workspace] resolver = "2" + +# Naming convention: crate (package) names use hyphens, matching Rust ecosystem +# norms, and every directory name matches its package name. That keeps +# `cargo -p ` working for all five crates. Member paths are +# listed bare and alphabetically — no `./` prefixes, no mixed separators. members = [ - "./asset-maintenance", + "asset-maintenance", "assetsup", "contrib", + "multisig-transfer", "multisig-wallet", - "multisig_transfer", ] [workspace.dependencies] -soroban-sdk = "22" +soroban-sdk = "23" [profile.release] opt-level = "z" diff --git a/contracts/EVENTS.md b/contracts/EVENTS.md new file mode 100644 index 000000000..d467d2414 --- /dev/null +++ b/contracts/EVENTS.md @@ -0,0 +1,222 @@ +# Contract event catalogue + +Every event emitted by the AssetsUp contracts, for consumers of the backend +Soroban bridge. + +## Convention + +All events are **typed** `#[contractevent]` structs, introduced with the +`soroban-sdk` 23 upgrade. Each crate declares them in its own `events.rs`, which +is the only place events are published from. + +The rules: + +1. **Name.** The struct is named `` — `AssetRegistered`, + `TransferExecuted`, `LeaseCancelled`. The SDK derives **topic 0** from the + struct name in `lower_snake_case`, so `AssetRegistered` publishes under the + topic `asset_registered`. +2. **Topics.** The identifier a consumer would filter on — an asset id, a + request id, an owner address — is marked `#[topic]` and becomes topic 1. + Events that are not about a single entity have no extra topic. +3. **Data.** Every remaining field goes into the data map, keyed by field name. +4. **Timestamp.** Events carry a `timestamp` field where the emitting entrypoint + has one meaningful to record. + +Because `#[contractevent]` types are emitted into the **contract spec**, clients +can generate bindings for them instead of hand-decoding positional tuples. This +is the main practical gain over the previous approach, where topics were an +inconsistent mix of `symbol_short!`, `Symbol::new`, and bare string tuples, and +payloads were untyped tuples whose shape had to be read out of the source. + +### Reading events in tests + +`env.events().all()` returns the events of the **most recent contract +invocation**, not every event since the test began. Two consequences worth +knowing before writing an event test: + +- Comparing a count before and after a call does not work. Both readings see one + invocation's worth of events, so the difference is meaningless. +- **Any** invocation resets the view, including a read-only entrypoint. Calling + `client.get_threshold()` after the call under test will leave + `events().all()` empty. Read the events first, assert, then make other calls. + +Assert against the event you just triggered: + +```rust +let events = env.events().all(); +assert_eq!(events.len(), 1); + +let (_contract, topics, _data) = events.last().unwrap(); +let name: Symbol = topics.get(0).unwrap().try_into_val(&env).unwrap(); +assert_eq!(name, Symbol::new(&env, "asset_registered")); +``` + +--- + +## `assetsup` + +### Registry + +| Event (topic 0) | Topic 1 | Data | Emitted by | +|---|---|---|---| +| `contract_initialized` | `admin` | `timestamp` | `initialize` | +| `asset_registered` | `asset_id` | `owner`, `timestamp` | `register_asset` | +| `asset_updated` | `asset_id` | `caller`, `timestamp` | `update_asset_metadata` | +| `asset_transferred` | `asset_id` | `old_owner`, `new_owner`, `timestamp` | `transfer_asset_ownership` | +| `asset_retired` | `asset_id` | `caller`, `timestamp` | `retire_asset` | +| `admin_changed` | `new_admin` | `old_admin`, `timestamp` | `update_admin` | +| `registrar_added` | `registrar` | `timestamp` | `add_authorized_registrar` | +| `registrar_removed` | `registrar` | `timestamp` | `remove_authorized_registrar` | +| `contract_paused` | `admin` | `timestamp` | `pause_contract` | +| `contract_unpaused` | `admin` | `timestamp` | `unpause_contract` | + +### Tokenization + +| Event | Topic 1 | Data | Emitted by | +|---|---|---|---| +| `asset_tokenized` | `asset_id` | `total_supply`, `symbol`, `decimals`, `tokenizer` | `tokenize_asset` | +| `tokens_minted` | `asset_id` | `amount`, `total_supply` | `mint_tokens` | +| `tokens_burned` | `asset_id` | `amount`, `total_supply` | `burn_tokens` | +| `tokens_transferred` | `asset_id` | `from`, `to`, `amount` | `transfer_tokens` | +| `tokens_locked` | `asset_id` | `holder`, `until_timestamp` | `lock_tokens` | +| `tokens_unlocked` | `asset_id` | `holder`, `timestamp` | `unlock_tokens` | +| `valuation_updated` | `asset_id` | `new_valuation` | `update_valuation` | +| `asset_detokenized` | `asset_id` | `proposal_id`, `total_supply` | `execute_detokenization` | + +### Dividends and voting + +| Event | Topic 1 | Data | Emitted by | +|---|---|---|---| +| `dividend_distributed` | `asset_id` | `total_amount`, `holder_count` | `distribute_dividends` | +| `dividend_claimed` | `asset_id` | `holder`, `amount` | `claim_dividends` | +| `vote_cast` | `asset_id` | `proposal_id`, `voter`, `voting_power` | `cast_vote` | + +### Transfer restrictions + +| Event | Topic 1 | Data | Emitted by | +|---|---|---|---| +| `restriction_set` | `asset_id` | `require_accredited` | `set_transfer_restriction` | +| `whitelist_added` | `asset_id` | `address` | `add_to_whitelist` | +| `whitelist_removed` | `asset_id` | `address` | `remove_from_whitelist` | + +### Leasing + +| Event | Topic 1 | Data | Emitted by | +|---|---|---|---| +| `lease_created` | `lease_id` | `asset_id`, `lessor`, `lessee`, `timestamp` | `create_lease` | +| `lease_returned` | `lease_id` | `caller`, `timestamp` | `return_leased_asset` | +| `lease_cancelled` | `lease_id` | `caller`, `timestamp` | `cancel_lease` | +| `lease_expired` | `lease_id` | `timestamp` | `expire_lease` | + +--- + +## `contrib` + +> Only the modules `contrib/src/lib.rs` declares are compiled: `audit`, +> `pause`, `types`, `insurance`, `lease`. The escrow, KYC, staking, oracle, +> tokenization, detokenization, and restrictions files exist in the crate +> directory but are **not part of the crate**, so they emit nothing. See the +> crate README. + +| Event | Topic 1 | Data | Emitted by | +|---|---|---|---| +| `contract_initialized` | `admin` | `timestamp` | `initialize` | +| `asset_registered` | `asset_id` | `owner`, `timestamp` | `register_asset` | +| `asset_transferred` | `asset_id` | `old_owner`, `new_owner`, `timestamp` | `transfer_asset` | +| `asset_retired` | `asset_id` | `caller`, `timestamp` | `retire_asset` | +| `registrar_added` | `registrar` | `caller`, `timestamp` | `add_authorized_registrar` | +| `registrar_removed` | `registrar` | `caller`, `timestamp` | `remove_authorized_registrar` | +| `contract_paused` | `caller` | `timestamp` | `pause_contract` | +| `contract_unpaused` | `caller` | `timestamp` | `unpause_contract` | +| `policy_created` | `policy_id` | `asset_id`, `insurer`, `holder`, `timestamp` | `create_policy` | +| `policy_cancelled` | `policy_id` | `caller`, `timestamp` | `cancel_policy` | +| `claim_submitted` | `claim_id` | `policy_id`, `claimant`, `amount`, `timestamp` | `submit_claim` | +| `claim_status_updated` | `claim_id` | `status`, `timestamp` | `update_claim_status` | +| `lease_created` | `lease_id` | `asset_id`, `lessor`, `lessee`, `timestamp` | `create_lease` | +| `lease_checked_in` | `lease_id` | `timestamp` | `check_in_lease` | +| `lease_cancelled` | `lease_id` | `caller`, `timestamp` | `cancel_lease` | + +--- + +## `multisig-wallet` + +| Event | Topic 1 | Data | Emitted by | +|---|---|---|---| +| `wallet_initialized` | `admin` | `owners`, `threshold`, `timestamp` | `initialize` | +| `transaction_submitted` | `tx_id` | `initiator`, `tx_type`, `timestamp` | `submit_transaction` | +| `transaction_confirmed` | `tx_id` | `confirmer`, `confirmations_count`, `timestamp` | `confirm_transaction` | +| `confirmation_revoked` | `tx_id` | `revoker`, `timestamp` | `revoke_confirmation` | +| `transaction_executed` | `tx_id` | `initiator`, `timestamp` | `execute_transaction` | +| `transaction_cancelled` | `tx_id` | `caller`, `timestamp` | `cancel_transaction` | +| `proposal_submitted` | `proposal_id` | `proposer`, `proposal_type`, `timestamp` | `propose_add_owner`, `propose_remove_owner`, `propose_change_threshold` | +| `proposal_confirmed` | `proposal_id` | `confirmer`, `confirmations_count`, `timestamp` | `confirm_proposal` | +| `owner_added` | `owner` | `proposer`, `timestamp` | `execute_proposal` (AddOwner) | +| `owner_removed` | `owner` | `proposer`, `timestamp` | `execute_proposal` (RemoveOwner) | +| `threshold_changed` | — | `old_threshold`, `new_threshold`, `timestamp` | `execute_proposal` (ChangeThreshold) | +| `wallet_frozen` | `caller` | `timestamp` | `emergency_freeze` | +| `wallet_unfrozen` | `caller` | `timestamp` | `emergency_unfreeze` | +| `daily_limit_changed` | `caller` | `limit`, `timestamp` | `set_daily_limit` | +| `daily_limit_reached` | — | `limit`, `attempted_total`, `timestamp` | daily-limit check, before `DailyLimitExceeded` | + +`daily_limit_reached` is emitted on a **rejected** operation, not a state change. +It is the one deliberate exception to "events mark state changes"; it exists so +operators can see limit pressure. + +--- + +## `multisig-transfer` + +| Event | Topic 1 | Data | Emitted by | +|---|---|---|---| +| `contract_initialized` | `admin` | `asset_registry`, `timestamp` | `initialize` | +| `transfer_requested` | `request_id` | `asset_id`, `from_owner`, `to_owner`, `timestamp` | `create_transfer_request` | +| `transfer_approved` | `request_id` | `approver`, `approval_count`, `timestamp` | `approve_transfer_request` | +| `transfer_rejected` | `request_id` | `rejector`, `reason_hash`, `timestamp` | `reject_transfer_request` | +| `transfer_executed` | `request_id` | `asset_id`, `new_owner`, `timestamp` | `execute_transfer` | +| `transfer_cancelled` | `request_id` | `cancelled_by`, `timestamp` | `cancel_transfer_request` | +| `approval_rule_updated` | `category` | `required_approvals`, `timestamp` | `configure_approval_rule` | +| `approver_added` | `approver` | `added_by`, `timestamp` | *defined, not yet emitted* | +| `approver_removed` | `approver` | `removed_by`, `timestamp` | *defined, not yet emitted* | + +`approver_added` and `approver_removed` are declared because the approver set is +part of the contract's model, but no entrypoint currently mutates it. They are +marked `#[allow(dead_code)]`. + +--- + +## `asset-maintenance` + +| Event | Topic 1 | Data | Emitted by | +|---|---|---|---| +| `contract_initialized` | `admin` | `registry`, `timestamp` | `init` | +| `provider_registered` | `provider` | `timestamp` | `register_provider` | +| `provider_deactivated` | `provider` | `timestamp` | `deactivate_provider` | +| `maintenance_recorded` | `asset_id` | `record_id`, `provider`, `timestamp` | `add_maintenance_record` | +| `maintenance_scheduled` | `asset_id` | `next_service_due`, `timestamp` | `schedule_maintenance` | +| `maintenance_schedule_updated` | `asset_id` | `next_service_due`, `timestamp` | `update_maintenance_schedule` | +| `maintenance_completed` | `asset_id` | `record_id`, `provider`, `timestamp` | `complete_scheduled_maintenance` | +| `warranty_added` | `asset_id` | `end_date`, `timestamp` | `add_warranty_information` | +| `warranty_updated` | `asset_id` | `end_date`, `timestamp` | `update_warranty_information` | +| `warranty_claim_filed` | `asset_id` | `claim_amount`, `timestamp` | `file_warranty_claim` | +| `alert_created` | `asset_id` | `alert_type`, `severity`, `timestamp` | `create_maintenance_alert` | +| `alert_acknowledged` | `asset_id` | `alert_index`, `acknowledged_by`, `timestamp` | `acknowledge_maintenance_alert` | + +Previously `add_warranty_information` and `schedule_maintenance` were shared by +both the create and update entrypoints, so an update was indistinguishable from +a create off-chain. Persistence is now factored into private helpers so each +entrypoint emits its own event. + +--- + +## Coverage + +Every state-changing entrypoint in every compiled module emits an event. The +following were **silent before** this catalogue existed and now emit: + +| Contract | Entrypoint | +|---|---| +| `assetsup` | `initialize`, `add_authorized_registrar`, `remove_authorized_registrar` | +| `contrib` | `initialize`, `add_authorized_registrar`, `remove_authorized_registrar` | +| `multisig-wallet` | `initialize`, `propose_*`, `confirm_proposal`, `set_daily_limit` | +| `multisig-transfer` | `initialize` | +| `asset-maintenance` | `init`, `register_provider`, `deactivate_provider`, `update_maintenance_schedule`, `update_warranty_information`, `acknowledge_maintenance_alert` | diff --git a/contracts/asset-maintenance/src/events.rs b/contracts/asset-maintenance/src/events.rs new file mode 100644 index 000000000..c3b69a28d --- /dev/null +++ b/contracts/asset-maintenance/src/events.rs @@ -0,0 +1,246 @@ +//! Typed contract events for `asset-maintenance`. +//! +//! See `contracts/EVENTS.md` for the workspace-wide convention. Struct names +//! are ``; the SDK derives topic 0 from the name in +//! `lower_snake_case`. `asset_id` is always a `#[topic]` so consumers can index +//! per asset. + +use soroban_sdk::{contractevent, Address, Env}; + +use crate::{AlertSeverity, AlertType, MaintenanceAlert, MaintenanceRecord, ScheduledMaintenance}; + +/// The contract was initialized with an admin and an asset registry. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ContractInitialized { + #[topic] + pub admin: Address, + pub registry: Address, + pub timestamp: u64, +} + +/// A maintenance provider was registered. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProviderRegistered { + #[topic] + pub provider: Address, + pub timestamp: u64, +} + +/// A maintenance provider was deactivated and can no longer file records. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProviderDeactivated { + #[topic] + pub provider: Address, + pub timestamp: u64, +} + +/// A maintenance record was appended to an asset's history. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct MaintenanceRecorded { + #[topic] + pub asset_id: u64, + pub record_id: u64, + pub provider: Address, + pub timestamp: u64, +} + +/// Maintenance was scheduled for an asset. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct MaintenanceScheduled { + #[topic] + pub asset_id: u64, + pub next_service_due: u64, + pub timestamp: u64, +} + +/// An existing maintenance schedule was changed. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct MaintenanceScheduleUpdated { + #[topic] + pub asset_id: u64, + pub next_service_due: u64, + pub timestamp: u64, +} + +/// A scheduled maintenance was completed and recorded. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct MaintenanceCompleted { + #[topic] + pub asset_id: u64, + pub record_id: u64, + pub provider: Address, + pub timestamp: u64, +} + +/// Warranty information was attached to an asset. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct WarrantyAdded { + #[topic] + pub asset_id: u64, + pub end_date: u64, + pub timestamp: u64, +} + +/// Warranty information for an asset was changed. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct WarrantyUpdated { + #[topic] + pub asset_id: u64, + pub end_date: u64, + pub timestamp: u64, +} + +/// A claim was filed against an asset's warranty. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct WarrantyClaimFiled { + #[topic] + pub asset_id: u64, + pub claim_amount: i128, + pub timestamp: u64, +} + +/// A maintenance alert was raised against an asset. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AlertCreated { + #[topic] + pub asset_id: u64, + pub alert_type: AlertType, + pub severity: AlertSeverity, + pub timestamp: u64, +} + +/// A maintenance alert was acknowledged. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AlertAcknowledged { + #[topic] + pub asset_id: u64, + pub alert_index: u32, + pub acknowledged_by: Address, + pub timestamp: u64, +} + +// --------------------------------------------------------------------------- +// Emission helpers +// --------------------------------------------------------------------------- + +pub fn contract_initialized(env: &Env, admin: &Address, registry: &Address) { + ContractInitialized { + admin: admin.clone(), + registry: registry.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn provider_registered(env: &Env, provider: &Address) { + ProviderRegistered { + provider: provider.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn provider_deactivated(env: &Env, provider: &Address) { + ProviderDeactivated { + provider: provider.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn maintenance_recorded(env: &Env, record: &MaintenanceRecord) { + MaintenanceRecorded { + asset_id: record.asset_id, + record_id: record.record_id, + provider: record.provider.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn maintenance_scheduled(env: &Env, schedule: &ScheduledMaintenance) { + MaintenanceScheduled { + asset_id: schedule.asset_id, + next_service_due: schedule.next_service_due, + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn maintenance_schedule_updated(env: &Env, schedule: &ScheduledMaintenance) { + MaintenanceScheduleUpdated { + asset_id: schedule.asset_id, + next_service_due: schedule.next_service_due, + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn maintenance_completed(env: &Env, asset_id: u64, record: &MaintenanceRecord) { + MaintenanceCompleted { + asset_id, + record_id: record.record_id, + provider: record.provider.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn warranty_added(env: &Env, asset_id: u64, end_date: u64) { + WarrantyAdded { + asset_id, + end_date, + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn warranty_updated(env: &Env, asset_id: u64, end_date: u64) { + WarrantyUpdated { + asset_id, + end_date, + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn warranty_claim_filed(env: &Env, asset_id: u64, claim_amount: i128) { + WarrantyClaimFiled { + asset_id, + claim_amount, + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn alert_created(env: &Env, alert: &MaintenanceAlert) { + AlertCreated { + asset_id: alert.asset_id, + alert_type: alert.alert_type.clone(), + severity: alert.severity.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn alert_acknowledged(env: &Env, asset_id: u64, alert_index: u32, by: &Address) { + AlertAcknowledged { + asset_id, + alert_index, + acknowledged_by: by.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} diff --git a/contracts/asset-maintenance/src/lib.rs b/contracts/asset-maintenance/src/lib.rs index f8b11a27f..35e22506f 100644 --- a/contracts/asset-maintenance/src/lib.rs +++ b/contracts/asset-maintenance/src/lib.rs @@ -1,6 +1,7 @@ #![no_std] -use soroban_sdk::{contract, contractimpl, contracttype, symbol_short, Address, Env, String, Vec}; +use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, String, Vec}; +pub mod events; mod test; #[contracttype] @@ -170,6 +171,8 @@ impl AssetMaintenanceContract { env.storage() .persistent() .set(&DataKey::AssetRegistry, ®istry); + + events::contract_initialized(&env, &admin, ®istry); } pub fn register_provider(env: Env, provider: ProviderProfile) { @@ -179,6 +182,8 @@ impl AssetMaintenanceContract { env.storage() .persistent() .set(&DataKey::Provider(provider.address.clone()), &provider); + + events::provider_registered(&env, &provider.address); } pub fn deactivate_provider(env: Env, provider_address: Address) { @@ -193,7 +198,9 @@ impl AssetMaintenanceContract { provider.is_active = false; env.storage() .persistent() - .set(&DataKey::Provider(provider_address), &provider); + .set(&DataKey::Provider(provider_address.clone()), &provider); + + events::provider_deactivated(&env, &provider_address); } } @@ -271,10 +278,7 @@ impl AssetMaintenanceContract { .set(&DataKey::AssetStats(record.asset_id), &stats); // 6. Emit Event - env.events().publish( - (symbol_short!("MaintRec"), record.asset_id), - (record.record_id, record.provider, env.ledger().timestamp()), - ); + events::maintenance_recorded(&env, &record); } pub fn get_maintenance_history(env: Env, asset_id: u64) -> Vec { @@ -286,23 +290,13 @@ impl AssetMaintenanceContract { pub fn schedule_maintenance(env: Env, owner: Address, schedule: ScheduledMaintenance) { owner.require_auth(); - // Asset owner check (placeholder - assuming caller is owner or admin) - // In a real system, we'd check if owner is truly the owner via Registry - if schedule.frequency_days == 0 { - panic!("frequency must be positive"); - } - - env.storage() - .persistent() - .set(&DataKey::MaintenanceSchedule(schedule.asset_id), &schedule); - - env.events().publish( - (symbol_short!("MaintSch"), schedule.asset_id), - (schedule.next_service_due, env.ledger().timestamp()), - ); + Self::store_schedule(&env, &schedule); + events::maintenance_scheduled(&env, &schedule); } pub fn update_maintenance_schedule(env: Env, owner: Address, schedule: ScheduledMaintenance) { + owner.require_auth(); + // Update existing schedule if !env .storage() @@ -311,7 +305,23 @@ impl AssetMaintenanceContract { { panic!("no schedule exists for asset"); } - Self::schedule_maintenance(env, owner, schedule); + + Self::store_schedule(&env, &schedule); + events::maintenance_schedule_updated(&env, &schedule); + } + + /// Validates and persists a schedule without emitting an event, so that + /// the create and update entrypoints each emit their own. + fn store_schedule(env: &Env, schedule: &ScheduledMaintenance) { + // Asset owner check (placeholder - assuming caller is owner or admin) + // In a real system, we'd check if owner is truly the owner via Registry + if schedule.frequency_days == 0 { + panic!("frequency must be positive"); + } + + env.storage() + .persistent() + .set(&DataKey::MaintenanceSchedule(schedule.asset_id), schedule); } pub fn get_upcoming_maintenance(env: Env, asset_id: u64) -> Option { @@ -340,24 +350,12 @@ impl AssetMaintenanceContract { } } - env.events().publish( - (symbol_short!("MaintCmp"), asset_id), - (record.record_id, record.provider, env.ledger().timestamp()), - ); + events::maintenance_completed(&env, asset_id, &record); } pub fn add_warranty_information(env: Env, warranty: WarrantyInfo) { - if warranty.end_date <= warranty.start_date { - panic!("warranty dates invalid"); - } - env.storage() - .persistent() - .set(&DataKey::Warranty(warranty.asset_id), &warranty); - - env.events().publish( - (symbol_short!("WarrAdd"), warranty.asset_id), - (warranty.end_date, env.ledger().timestamp()), - ); + Self::store_warranty(&env, &warranty); + events::warranty_added(&env, warranty.asset_id, warranty.end_date); } pub fn update_warranty_information(env: Env, warranty: WarrantyInfo) { @@ -368,7 +366,20 @@ impl AssetMaintenanceContract { { panic!("no warranty exists for asset"); } - Self::add_warranty_information(env, warranty); + + Self::store_warranty(&env, &warranty); + events::warranty_updated(&env, warranty.asset_id, warranty.end_date); + } + + /// Validates and persists warranty terms without emitting an event, so + /// that the add and update entrypoints each emit their own. + fn store_warranty(env: &Env, warranty: &WarrantyInfo) { + if warranty.end_date <= warranty.start_date { + panic!("warranty dates invalid"); + } + env.storage() + .persistent() + .set(&DataKey::Warranty(warranty.asset_id), warranty); } pub fn get_provider_details(env: Env, provider_address: Address) -> Option { @@ -403,10 +414,7 @@ impl AssetMaintenanceContract { .persistent() .set(&DataKey::Warranty(asset_id), &warranty); - env.events().publish( - (symbol_short!("WarrClm"), asset_id), - (claim_amount, env.ledger().timestamp()), - ); + events::warranty_claim_filed(&env, asset_id, claim_amount); } pub fn create_maintenance_alert(env: Env, alert: MaintenanceAlert) { @@ -420,10 +428,7 @@ impl AssetMaintenanceContract { .persistent() .set(&DataKey::Alerts(alert.asset_id), &alerts); - env.events().publish( - (symbol_short!("AlertCr"), alert.asset_id), - (alert.alert_type, alert.severity, env.ledger().timestamp()), - ); + events::alert_created(&env, &alert); } pub fn acknowledge_maintenance_alert(env: Env, asset_id: u64, alert_index: u32, by: Address) { @@ -436,11 +441,13 @@ impl AssetMaintenanceContract { if let Some(mut alert) = alerts.get(alert_index) { alert.acknowledged = true; - alert.acknowledged_by = by; + alert.acknowledged_by = by.clone(); alerts.set(alert_index, alert); env.storage() .persistent() .set(&DataKey::Alerts(asset_id), &alerts); + + events::alert_acknowledged(&env, asset_id, alert_index, &by); } } diff --git a/contracts/assetsup/src/detokenization.rs b/contracts/assetsup/src/detokenization.rs index d297ce5f5..cb2e0de82 100644 --- a/contracts/assetsup/src/detokenization.rs +++ b/contracts/assetsup/src/detokenization.rs @@ -134,10 +134,7 @@ pub fn execute_detokenization(env: &Env, asset_id: u64, proposal_id: u64) -> Res store.set(&proposal_key, &executed_proposal); // Emit event: (asset_id, proposal_id, total_supply_removed) - env.events().publish( - ("detokenization", "asset_detokenized"), - (asset_id, proposal_id, total_supply), - ); + crate::events::asset_detokenized(env, asset_id, proposal_id, total_supply); Ok(()) } diff --git a/contracts/assetsup/src/dividends.rs b/contracts/assetsup/src/dividends.rs index 9ba0819ee..b72b3a016 100644 --- a/contracts/assetsup/src/dividends.rs +++ b/contracts/assetsup/src/dividends.rs @@ -37,10 +37,7 @@ pub fn distribute_dividends(env: &Env, asset_id: u64, total_amount: i128) -> Res } // Emit event: (asset_id, total_amount, holder_count) - env.events().publish( - ("dividend", "distributed"), - (asset_id, total_amount, holders.len()), - ); + crate::events::dividend_distributed(env, asset_id, total_amount, holders.len()); Ok(()) } @@ -69,8 +66,7 @@ pub fn claim_dividends(env: &Env, asset_id: u64, holder: Address) -> Result`; the SDK derives topic 0 from the name in +//! `lower_snake_case`. The primary entity identifier is marked `#[topic]` so +//! downstream systems can index on it. +//! +//! Note that the registry keys assets by `BytesN<32>` while tokenization, +//! dividends, and voting key them by `u64`; the event types reflect whichever +//! id space the emitting entrypoint uses. + +use soroban_sdk::{contractevent, Address, BytesN, Env, String}; + +// --------------------------------------------------------------------------- +// Registry +// --------------------------------------------------------------------------- + +/// The contract was initialized with an admin. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ContractInitialized { + #[topic] + pub admin: Address, + pub timestamp: u64, +} + +/// An asset was registered by an authorized registrar. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AssetRegistered { + #[topic] + pub asset_id: BytesN<32>, + pub owner: Address, + pub timestamp: u64, +} + +/// An asset's metadata was updated. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AssetUpdated { + #[topic] + pub asset_id: BytesN<32>, + pub caller: Address, + pub timestamp: u64, +} + +/// Ownership of an asset moved to a new owner. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AssetTransferred { + #[topic] + pub asset_id: BytesN<32>, + pub old_owner: Address, + pub new_owner: Address, + pub timestamp: u64, +} + +/// An asset was retired and can no longer be transferred or updated. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AssetRetired { + #[topic] + pub asset_id: BytesN<32>, + pub caller: Address, + pub timestamp: u64, +} + +/// The contract admin was changed. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AdminChanged { + #[topic] + pub new_admin: Address, + pub old_admin: Address, + pub timestamp: u64, +} + +/// An address was granted registrar rights. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RegistrarAdded { + #[topic] + pub registrar: Address, + pub timestamp: u64, +} + +/// An address had its registrar rights revoked. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RegistrarRemoved { + #[topic] + pub registrar: Address, + pub timestamp: u64, +} + +/// The contract was paused; mutating entrypoints now reject. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ContractPaused { + #[topic] + pub admin: Address, + pub timestamp: u64, +} + +/// The contract was unpaused and resumed normal operation. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ContractUnpaused { + #[topic] + pub admin: Address, + pub timestamp: u64, +} + +// --------------------------------------------------------------------------- +// Tokenization +// --------------------------------------------------------------------------- + +/// An asset was tokenized into fractional shares. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AssetTokenized { + #[topic] + pub asset_id: u64, + pub total_supply: i128, + pub symbol: String, + pub decimals: u32, + pub tokenizer: Address, +} + +/// New tokens were minted for a tokenized asset. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TokensMinted { + #[topic] + pub asset_id: u64, + pub amount: i128, + pub total_supply: i128, +} + +/// Tokens were burned, reducing the supply of a tokenized asset. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TokensBurned { + #[topic] + pub asset_id: u64, + pub amount: i128, + pub total_supply: i128, +} + +/// Tokens moved between two holders. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TokensTransferred { + #[topic] + pub asset_id: u64, + pub from: Address, + pub to: Address, + pub amount: i128, +} + +/// A holder's tokens were locked until a given timestamp. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TokensLocked { + #[topic] + pub asset_id: u64, + pub holder: Address, + pub until_timestamp: u64, +} + +/// A holder's tokens were unlocked. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TokensUnlocked { + #[topic] + pub asset_id: u64, + pub holder: Address, + pub timestamp: u64, +} + +/// A tokenized asset's valuation was updated. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ValuationUpdated { + #[topic] + pub asset_id: u64, + pub new_valuation: i128, +} + +/// A tokenized asset was fully detokenized. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AssetDetokenized { + #[topic] + pub asset_id: u64, + pub proposal_id: u64, + pub total_supply: i128, +} + +// --------------------------------------------------------------------------- +// Dividends and voting +// --------------------------------------------------------------------------- + +/// A dividend was distributed across an asset's token holders. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DividendDistributed { + #[topic] + pub asset_id: u64, + pub total_amount: i128, + pub holder_count: u32, +} + +/// A holder claimed their accrued dividends. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DividendClaimed { + #[topic] + pub asset_id: u64, + pub holder: Address, + pub amount: i128, +} + +/// A token holder cast a weighted vote on a proposal. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct VoteCast { + #[topic] + pub asset_id: u64, + pub proposal_id: u64, + pub voter: Address, + pub voting_power: i128, +} + +// --------------------------------------------------------------------------- +// Transfer restrictions +// --------------------------------------------------------------------------- + +/// A transfer restriction was set on an asset. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RestrictionSet { + #[topic] + pub asset_id: u64, + pub require_accredited: bool, +} + +/// An address was added to an asset's transfer whitelist. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct WhitelistAdded { + #[topic] + pub asset_id: u64, + pub address: Address, +} + +/// An address was removed from an asset's transfer whitelist. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct WhitelistRemoved { + #[topic] + pub asset_id: u64, + pub address: Address, +} + +// --------------------------------------------------------------------------- +// Leasing +// --------------------------------------------------------------------------- + +/// A lease was created over an asset. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LeaseCreated { + #[topic] + pub lease_id: BytesN<32>, + pub asset_id: BytesN<32>, + pub lessor: Address, + pub lessee: Address, + pub timestamp: u64, +} + +/// A leased asset was returned. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LeaseReturned { + #[topic] + pub lease_id: BytesN<32>, + pub caller: Address, + pub timestamp: u64, +} + +/// A lease was cancelled before its end date. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LeaseCancelled { + #[topic] + pub lease_id: BytesN<32>, + pub caller: Address, + pub timestamp: u64, +} + +/// A lease reached its end date and expired. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LeaseExpired { + #[topic] + pub lease_id: BytesN<32>, + pub timestamp: u64, +} + +// --------------------------------------------------------------------------- +// Emission helpers +// --------------------------------------------------------------------------- + +pub fn contract_initialized(env: &Env, admin: &Address) { + ContractInitialized { + admin: admin.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn asset_registered(env: &Env, asset_id: &BytesN<32>, owner: &Address) { + AssetRegistered { + asset_id: asset_id.clone(), + owner: owner.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn asset_updated(env: &Env, asset_id: &BytesN<32>, caller: &Address) { + AssetUpdated { + asset_id: asset_id.clone(), + caller: caller.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn asset_transferred( + env: &Env, + asset_id: &BytesN<32>, + old_owner: &Address, + new_owner: &Address, +) { + AssetTransferred { + asset_id: asset_id.clone(), + old_owner: old_owner.clone(), + new_owner: new_owner.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn asset_retired(env: &Env, asset_id: &BytesN<32>, caller: &Address) { + AssetRetired { + asset_id: asset_id.clone(), + caller: caller.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn admin_changed(env: &Env, old_admin: &Address, new_admin: &Address) { + AdminChanged { + new_admin: new_admin.clone(), + old_admin: old_admin.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn registrar_added(env: &Env, registrar: &Address) { + RegistrarAdded { + registrar: registrar.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn registrar_removed(env: &Env, registrar: &Address) { + RegistrarRemoved { + registrar: registrar.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn contract_paused(env: &Env, admin: &Address) { + ContractPaused { + admin: admin.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn contract_unpaused(env: &Env, admin: &Address) { + ContractUnpaused { + admin: admin.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn asset_tokenized( + env: &Env, + asset_id: u64, + total_supply: i128, + symbol: String, + decimals: u32, + tokenizer: Address, +) { + AssetTokenized { + asset_id, + total_supply, + symbol, + decimals, + tokenizer, + } + .publish(env); +} + +pub fn tokens_minted(env: &Env, asset_id: u64, amount: i128, total_supply: i128) { + TokensMinted { + asset_id, + amount, + total_supply, + } + .publish(env); +} + +pub fn tokens_burned(env: &Env, asset_id: u64, amount: i128, total_supply: i128) { + TokensBurned { + asset_id, + amount, + total_supply, + } + .publish(env); +} + +pub fn tokens_transferred(env: &Env, asset_id: u64, from: &Address, to: &Address, amount: i128) { + TokensTransferred { + asset_id, + from: from.clone(), + to: to.clone(), + amount, + } + .publish(env); +} + +pub fn tokens_locked(env: &Env, asset_id: u64, holder: &Address, until_timestamp: u64) { + TokensLocked { + asset_id, + holder: holder.clone(), + until_timestamp, + } + .publish(env); +} + +pub fn tokens_unlocked(env: &Env, asset_id: u64, holder: &Address) { + TokensUnlocked { + asset_id, + holder: holder.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn valuation_updated(env: &Env, asset_id: u64, new_valuation: i128) { + ValuationUpdated { + asset_id, + new_valuation, + } + .publish(env); +} + +pub fn asset_detokenized(env: &Env, asset_id: u64, proposal_id: u64, total_supply: i128) { + AssetDetokenized { + asset_id, + proposal_id, + total_supply, + } + .publish(env); +} + +pub fn dividend_distributed(env: &Env, asset_id: u64, total_amount: i128, holder_count: u32) { + DividendDistributed { + asset_id, + total_amount, + holder_count, + } + .publish(env); +} + +pub fn dividend_claimed(env: &Env, asset_id: u64, holder: &Address, amount: i128) { + DividendClaimed { + asset_id, + holder: holder.clone(), + amount, + } + .publish(env); +} + +pub fn vote_cast(env: &Env, asset_id: u64, proposal_id: u64, voter: &Address, voting_power: i128) { + VoteCast { + asset_id, + proposal_id, + voter: voter.clone(), + voting_power, + } + .publish(env); +} + +pub fn restriction_set(env: &Env, asset_id: u64, require_accredited: bool) { + RestrictionSet { + asset_id, + require_accredited, + } + .publish(env); +} + +pub fn whitelist_added(env: &Env, asset_id: u64, address: &Address) { + WhitelistAdded { + asset_id, + address: address.clone(), + } + .publish(env); +} + +pub fn whitelist_removed(env: &Env, asset_id: u64, address: &Address) { + WhitelistRemoved { + asset_id, + address: address.clone(), + } + .publish(env); +} + +pub fn lease_created( + env: &Env, + lease_id: &BytesN<32>, + asset_id: &BytesN<32>, + lessor: &Address, + lessee: &Address, +) { + LeaseCreated { + lease_id: lease_id.clone(), + asset_id: asset_id.clone(), + lessor: lessor.clone(), + lessee: lessee.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn lease_returned(env: &Env, lease_id: &BytesN<32>, caller: &Address) { + LeaseReturned { + lease_id: lease_id.clone(), + caller: caller.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn lease_cancelled(env: &Env, lease_id: &BytesN<32>, caller: &Address) { + LeaseCancelled { + lease_id: lease_id.clone(), + caller: caller.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn lease_expired(env: &Env, lease_id: &BytesN<32>) { + LeaseExpired { + lease_id: lease_id.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} diff --git a/contracts/assetsup/src/lease.rs b/contracts/assetsup/src/lease.rs index 559284f19..64f80fd98 100644 --- a/contracts/assetsup/src/lease.rs +++ b/contracts/assetsup/src/lease.rs @@ -129,10 +129,7 @@ pub fn create_lease( set_asset_active_lease(env, &asset_id, &lease_id); append_lessee_lease(env, &lessee, &lease_id); - env.events().publish( - (soroban_sdk::symbol_short!("lease_new"),), - (lease_id, asset_id, lessor, lessee, env.ledger().timestamp()), - ); + crate::events::lease_created(env, &lease_id, &asset_id, &lessor, &lessee); Ok(()) } @@ -152,10 +149,7 @@ pub fn return_leased_asset(env: &Env, lease_id: BytesN<32>, caller: Address) -> save_lease(env, &lease); clear_asset_active_lease(env, &lease.asset_id); - env.events().publish( - (soroban_sdk::symbol_short!("lease_ret"),), - (lease_id, caller, env.ledger().timestamp()), - ); + crate::events::lease_returned(env, &lease_id, &caller); Ok(()) } @@ -179,10 +173,7 @@ pub fn cancel_lease(env: &Env, lease_id: BytesN<32>, caller: Address) -> Result< save_lease(env, &lease); clear_asset_active_lease(env, &lease.asset_id); - env.events().publish( - (soroban_sdk::symbol_short!("lease_can"),), - (lease_id, caller, env.ledger().timestamp()), - ); + crate::events::lease_cancelled(env, &lease_id, &caller); Ok(()) } @@ -202,10 +193,7 @@ pub fn expire_lease(env: &Env, lease_id: BytesN<32>) -> Result<(), Error> { save_lease(env, &lease); clear_asset_active_lease(env, &lease.asset_id); - env.events().publish( - (soroban_sdk::symbol_short!("lease_exp"),), - (lease_id, env.ledger().timestamp()), - ); + crate::events::lease_expired(env, &lease_id); Ok(()) } diff --git a/contracts/assetsup/src/lib.rs b/contracts/assetsup/src/lib.rs index 7c220273f..a9ace2fba 100644 --- a/contracts/assetsup/src/lib.rs +++ b/contracts/assetsup/src/lib.rs @@ -2,9 +2,7 @@ #![allow(clippy::too_many_arguments)] use crate::error::{handle_error, Error}; -use soroban_sdk::{ - contract, contractimpl, contracttype, symbol_short, Address, BytesN, Env, String, Vec, -}; +use soroban_sdk::{contract, contractimpl, contracttype, Address, BytesN, Env, String, Vec}; pub(crate) mod asset; pub(crate) mod audit; @@ -12,6 +10,7 @@ pub(crate) mod branch; pub(crate) mod detokenization; pub(crate) mod dividends; pub(crate) mod error; +pub mod events; pub(crate) mod insurance; pub(crate) mod lease; pub(crate) mod tokenization; @@ -73,6 +72,8 @@ impl AssetUpContract { .persistent() .set(&DataKey::AuthorizedRegistrar(admin.clone()), &true); + events::contract_initialized(&env, &admin); + Ok(()) } @@ -168,10 +169,7 @@ impl AssetUpContract { ); // Emit event - env.events().publish( - (symbol_short!("asset_reg"),), - (asset.owner, asset.id, env.ledger().timestamp()), - ); + events::asset_registered(&env, &asset.id, &asset.owner); Ok(()) } @@ -267,10 +265,7 @@ impl AssetUpContract { ); // Emit event - env.events().publish( - (symbol_short!("asset_upd"),), - (asset_id, caller, env.ledger().timestamp()), - ); + events::asset_updated(&env, &asset_id, &caller); Ok(()) } @@ -342,10 +337,7 @@ impl AssetUpContract { ); // Emit event - env.events().publish( - (symbol_short!("asset_tx"),), - (asset_id, old_owner, new_owner, env.ledger().timestamp()), - ); + events::asset_transferred(&env, &asset_id, &old_owner, &new_owner); Ok(()) } @@ -383,10 +375,7 @@ impl AssetUpContract { ); // Emit event - env.events().publish( - (symbol_short!("asset_ret"),), - (asset_id, caller, env.ledger().timestamp()), - ); + events::asset_retired(&env, &asset_id, &caller); Ok(()) } @@ -466,10 +455,7 @@ impl AssetUpContract { .set(&DataKey::AuthorizedRegistrar(new_admin.clone()), &true); // Emit event - env.events().publish( - (symbol_short!("admin_chg"),), - (old_admin, new_admin, env.ledger().timestamp()), - ); + events::admin_changed(&env, &old_admin, &new_admin); Ok(()) } @@ -480,7 +466,9 @@ impl AssetUpContract { env.storage() .persistent() - .set(&DataKey::AuthorizedRegistrar(registrar), &true); + .set(&DataKey::AuthorizedRegistrar(registrar.clone()), &true); + + events::registrar_added(&env, ®istrar); Ok(()) } @@ -495,7 +483,9 @@ impl AssetUpContract { env.storage() .persistent() - .set(&DataKey::AuthorizedRegistrar(registrar), &false); + .set(&DataKey::AuthorizedRegistrar(registrar.clone()), &false); + + events::registrar_removed(&env, ®istrar); Ok(()) } @@ -506,10 +496,7 @@ impl AssetUpContract { env.storage().persistent().set(&DataKey::Paused, &true); // Emit event - env.events().publish( - (symbol_short!("c_pause"),), - (admin, env.ledger().timestamp()), - ); + events::contract_paused(&env, &admin); Ok(()) } @@ -521,10 +508,7 @@ impl AssetUpContract { env.storage().persistent().set(&DataKey::Paused, &false); // Emit event - env.events().publish( - (symbol_short!("c_unpause"),), - (admin, env.ledger().timestamp()), - ); + events::contract_unpaused(&env, &admin); Ok(()) } diff --git a/contracts/assetsup/src/tests/events.rs b/contracts/assetsup/src/tests/events.rs new file mode 100644 index 000000000..3cb46809b --- /dev/null +++ b/contracts/assetsup/src/tests/events.rs @@ -0,0 +1,162 @@ +//! Event emission tests. +//! +//! `env.events().all()` reports the events of the **most recent contract +//! invocation**, not every event since the test began, so each assertion here +//! looks at exactly what the entrypoint under test published. +//! +//! See `contracts/EVENTS.md` for the catalogue these assertions pin down. + +use soroban_sdk::testutils::{Address as _, Events as _}; +use soroban_sdk::{Address, BytesN, Env, Symbol, TryIntoVal, Val, Vec}; + +use super::helpers::{create_env, create_test_asset, initialize_contract}; +use crate::{AssetUpContract, AssetUpContractClient}; + +fn asset_id(env: &Env, seed: u8) -> BytesN<32> { + BytesN::from_array(env, &[seed; 32]) +} + +/// Returns the topics and data of the single event published by the most +/// recent invocation, failing if the entrypoint did not emit exactly one. +fn only_event(env: &Env) -> (Vec, Val) { + let events = env.events().all(); + assert_eq!( + events.len(), + 1, + "expected exactly one event from the last invocation" + ); + let (_contract, topics, data) = events.last().unwrap(); + (topics, data) +} + +fn assert_event_name(env: &Env, topics: &Vec, expected: &str) { + let name: Symbol = topics + .get(0) + .expect("event must have a name topic") + .try_into_val(env) + .expect("topic 0 must decode as a Symbol"); + assert_eq!( + name, + Symbol::new(env, expected), + "topic 0 should name the event" + ); +} + +#[test] +fn initialize_emits_contract_initialized() { + let env = create_env(); + let admin = Address::generate(&env); + let contract_id = env.register(AssetUpContract, ()); + let client = AssetUpContractClient::new(&env, &contract_id); + env.mock_all_auths(); + + client.initialize(&admin); + + let (topics, _) = only_event(&env); + assert_event_name(&env, &topics, "contract_initialized"); + let emitted: Address = topics.get(1).unwrap().try_into_val(&env).unwrap(); + assert_eq!(emitted, admin); +} + +#[test] +fn register_asset_emits_asset_registered_keyed_by_asset_id() { + let env = create_env(); + let admin = Address::generate(&env); + let client = initialize_contract(&env, &admin); + let owner = Address::generate(&env); + let id = asset_id(&env, 1); + + client.register_asset(&create_test_asset(&env, &owner, id.clone()), &admin); + + let (topics, _) = only_event(&env); + assert_event_name(&env, &topics, "asset_registered"); + let emitted: BytesN<32> = topics.get(1).unwrap().try_into_val(&env).unwrap(); + assert_eq!(emitted, id, "asset id must be indexable as topic 1"); +} + +#[test] +fn transfer_asset_ownership_emits_asset_transferred() { + let env = create_env(); + let admin = Address::generate(&env); + let client = initialize_contract(&env, &admin); + let owner = Address::generate(&env); + let new_owner = Address::generate(&env); + let id = asset_id(&env, 2); + client.register_asset(&create_test_asset(&env, &owner, id.clone()), &admin); + + client.transfer_asset_ownership(&id, &new_owner, &owner); + + let (topics, _) = only_event(&env); + assert_event_name(&env, &topics, "asset_transferred"); + let emitted: BytesN<32> = topics.get(1).unwrap().try_into_val(&env).unwrap(); + assert_eq!(emitted, id); +} + +#[test] +fn retire_asset_emits_asset_retired() { + let env = create_env(); + let admin = Address::generate(&env); + let client = initialize_contract(&env, &admin); + let owner = Address::generate(&env); + let id = asset_id(&env, 3); + client.register_asset(&create_test_asset(&env, &owner, id.clone()), &admin); + + client.retire_asset(&id, &owner); + + let (topics, _) = only_event(&env); + assert_event_name(&env, &topics, "asset_retired"); + let emitted: BytesN<32> = topics.get(1).unwrap().try_into_val(&env).unwrap(); + assert_eq!(emitted, id); +} + +#[test] +fn update_admin_emits_admin_changed_keyed_by_new_admin() { + let env = create_env(); + let admin = Address::generate(&env); + let client = initialize_contract(&env, &admin); + let new_admin = Address::generate(&env); + + client.update_admin(&new_admin); + + let (topics, _) = only_event(&env); + assert_event_name(&env, &topics, "admin_changed"); + let emitted: Address = topics.get(1).unwrap().try_into_val(&env).unwrap(); + assert_eq!(emitted, new_admin); +} + +#[test] +fn registrar_changes_are_observable() { + // These two entrypoints were silent before the event catalogue: an + // off-chain consumer could not tell that the registrar allowlist changed. + let env = create_env(); + let admin = Address::generate(&env); + let client = initialize_contract(&env, &admin); + let registrar = Address::generate(&env); + + client.add_authorized_registrar(®istrar); + let (topics, _) = only_event(&env); + assert_event_name(&env, &topics, "registrar_added"); + let emitted: Address = topics.get(1).unwrap().try_into_val(&env).unwrap(); + assert_eq!(emitted, registrar); + + client.remove_authorized_registrar(®istrar); + let (topics, _) = only_event(&env); + assert_event_name(&env, &topics, "registrar_removed"); + let emitted: Address = topics.get(1).unwrap().try_into_val(&env).unwrap(); + assert_eq!(emitted, registrar); +} + +#[test] +fn pause_and_unpause_emit_distinct_events() { + let env = create_env(); + let admin = Address::generate(&env); + let client = initialize_contract(&env, &admin); + + client.pause_contract(); + let (topics, _) = only_event(&env); + assert_event_name(&env, &topics, "contract_paused"); + + client.unpause_contract(); + let (topics, _) = only_event(&env); + assert_event_name(&env, &topics, "contract_unpaused"); +} diff --git a/contracts/assetsup/src/tests/mod.rs b/contracts/assetsup/src/tests/mod.rs index a44a81fdf..6a555823b 100644 --- a/contracts/assetsup/src/tests/mod.rs +++ b/contracts/assetsup/src/tests/mod.rs @@ -5,6 +5,7 @@ mod helpers; mod admin; mod asset; mod audit_trail; +mod events; mod initialization; // Tokenization and ownership tests diff --git a/contracts/assetsup/src/tokenization.rs b/contracts/assetsup/src/tokenization.rs index 7402eb8d0..0c3cf6a38 100644 --- a/contracts/assetsup/src/tokenization.rs +++ b/contracts/assetsup/src/tokenization.rs @@ -94,10 +94,7 @@ pub fn tokenize_asset( ); // Emit event: (asset_id, supply, symbol, decimals, tokenizer) - env.events().publish( - ("token", "asset_tokenized"), - (asset_id, total_supply, symbol, decimals, tokenizer), - ); + crate::events::asset_tokenized(env, asset_id, total_supply, symbol, decimals, tokenizer); Ok(tokenized_asset) } @@ -154,10 +151,7 @@ pub fn mint_tokens( ); // Emit event: (asset_id, amount, new_supply) - env.events().publish( - ("token", "tokens_minted"), - (asset_id, amount, tokenized_asset.total_supply), - ); + crate::events::tokens_minted(env, asset_id, amount, tokenized_asset.total_supply); Ok(tokenized_asset) } @@ -218,10 +212,7 @@ pub fn burn_tokens( ); // Emit event: (asset_id, amount, new_supply) - env.events().publish( - ("token", "tokens_burned"), - (asset_id, amount, tokenized_asset.total_supply), - ); + crate::events::tokens_burned(env, asset_id, amount, tokenized_asset.total_supply); Ok(tokenized_asset) } @@ -320,10 +311,7 @@ pub fn transfer_tokens( ); // Emit event: (asset_id, from, to, amount) - env.events().publish( - ("token", "tokens_transferred"), - (asset_id, from.clone(), to.clone(), amount), - ); + crate::events::tokens_transferred(env, asset_id, &from, &to, amount); Ok(()) } @@ -372,10 +360,7 @@ pub fn lock_tokens( store.set(&lock_key, &until_timestamp); // Emit event: (asset_id, holder, until_timestamp) - env.events().publish( - ("token", "tokens_locked"), - (asset_id, holder, until_timestamp), - ); + crate::events::tokens_locked(env, asset_id, &holder, until_timestamp); Ok(()) } @@ -396,8 +381,7 @@ pub fn unlock_tokens(env: &Env, asset_id: u64, holder: Address) -> Result<(), Er } // Emit event: (asset_id, holder) - env.events() - .publish(("token", "tokens_unlocked"), (asset_id, holder)); + crate::events::tokens_unlocked(env, asset_id, &holder); Ok(()) } @@ -468,8 +452,7 @@ pub fn update_valuation(env: &Env, asset_id: u64, new_valuation: i128) -> Result store.set(&key, &tokenized_asset); // Emit event: (asset_id, new_valuation) - env.events() - .publish(("token", "valuation_updated"), (asset_id, new_valuation)); + crate::events::valuation_updated(env, asset_id, new_valuation); Ok(()) } diff --git a/contracts/assetsup/src/transfer_restrictions.rs b/contracts/assetsup/src/transfer_restrictions.rs index 16a89d5ee..5dcda1fa4 100644 --- a/contracts/assetsup/src/transfer_restrictions.rs +++ b/contracts/assetsup/src/transfer_restrictions.rs @@ -15,10 +15,7 @@ pub fn set_transfer_restriction( store.set(&key, &restriction); // Emit event: (asset_id, require_accredited) - env.events().publish( - ("transfer", "restriction_set"), - (asset_id, restriction.require_accredited), - ); + crate::events::restriction_set(env, asset_id, restriction.require_accredited); Ok(()) } @@ -39,8 +36,7 @@ pub fn add_to_whitelist(env: &Env, asset_id: u64, address: Address) -> Result<() store.set(&key, &whitelist); // Emit event: (asset_id, address) - env.events() - .publish(("transfer", "whitelist_added"), (asset_id, address)); + crate::events::whitelist_added(env, asset_id, &address); Ok(()) } @@ -58,8 +54,7 @@ pub fn remove_from_whitelist(env: &Env, asset_id: u64, address: Address) -> Resu store.set(&key, &whitelist); // Emit event: (asset_id, address) - env.events() - .publish(("transfer", "whitelist_removed"), (asset_id, address)); + crate::events::whitelist_removed(env, asset_id, &address); } Ok(()) diff --git a/contracts/assetsup/src/voting.rs b/contracts/assetsup/src/voting.rs index c76d622be..aa89ca101 100644 --- a/contracts/assetsup/src/voting.rs +++ b/contracts/assetsup/src/voting.rs @@ -36,10 +36,7 @@ pub fn cast_vote(env: &Env, asset_id: u64, proposal_id: u64, voter: Address) -> store.set(&tally_key, &new_tally); // Emit event: (asset_id, proposal_id, voter, weight) - env.events().publish( - ("voting", "vote_cast"), - (asset_id, proposal_id, voter, ownership.balance), - ); + crate::events::vote_cast(env, asset_id, proposal_id, &voter, ownership.balance); Ok(()) } diff --git a/contracts/contrib/src/events.rs b/contracts/contrib/src/events.rs new file mode 100644 index 000000000..ab0447c8e --- /dev/null +++ b/contracts/contrib/src/events.rs @@ -0,0 +1,331 @@ +//! Typed contract events for `contrib`. +//! +//! See `contracts/EVENTS.md` for the workspace-wide convention. Struct names +//! are ``; the SDK derives topic 0 from the name in +//! `lower_snake_case`. The primary entity identifier is marked `#[topic]`. +//! +//! Only the modules `contrib/src/lib.rs` actually declares are covered here — +//! `audit`, `pause`, `types`, `insurance`, and `lease`. The other files in +//! `contrib/src/` are not compiled into the crate; see the crate README. + +use soroban_sdk::{contractevent, Address, BytesN, Env}; + +use crate::insurance::ClaimStatus; + +/// The contract was initialized with an admin. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ContractInitialized { + #[topic] + pub admin: Address, + pub timestamp: u64, +} + +/// An asset was registered by an authorized registrar. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AssetRegistered { + #[topic] + pub asset_id: BytesN<32>, + pub owner: Address, + pub timestamp: u64, +} + +/// Ownership of an asset moved to a new owner. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AssetTransferred { + #[topic] + pub asset_id: BytesN<32>, + pub old_owner: Address, + pub new_owner: Address, + pub timestamp: u64, +} + +/// An asset was retired and can no longer be transferred. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AssetRetired { + #[topic] + pub asset_id: BytesN<32>, + pub caller: Address, + pub timestamp: u64, +} + +/// An address was granted registrar rights. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RegistrarAdded { + #[topic] + pub registrar: Address, + pub caller: Address, + pub timestamp: u64, +} + +/// An address had its registrar rights revoked. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RegistrarRemoved { + #[topic] + pub registrar: Address, + pub caller: Address, + pub timestamp: u64, +} + +/// The contract was paused; mutating entrypoints now reject. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ContractPaused { + #[topic] + pub caller: Address, + pub timestamp: u64, +} + +/// The contract was unpaused and resumed normal operation. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ContractUnpaused { + #[topic] + pub caller: Address, + pub timestamp: u64, +} + +/// An insurance policy was created for an asset. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PolicyCreated { + #[topic] + pub policy_id: BytesN<32>, + pub asset_id: BytesN<32>, + pub insurer: Address, + pub holder: Address, + pub timestamp: u64, +} + +/// An insurance policy was cancelled. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PolicyCancelled { + #[topic] + pub policy_id: BytesN<32>, + pub caller: Address, + pub timestamp: u64, +} + +/// A claim was submitted against an insurance policy. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ClaimSubmitted { + #[topic] + pub claim_id: BytesN<32>, + pub policy_id: BytesN<32>, + pub claimant: Address, + pub amount: i128, + pub timestamp: u64, +} + +/// The status of an insurance claim was changed by the insurer. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ClaimStatusUpdated { + #[topic] + pub claim_id: BytesN<32>, + pub status: ClaimStatus, + pub timestamp: u64, +} + +/// A lease was created over an asset. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LeaseCreated { + #[topic] + pub lease_id: BytesN<32>, + pub asset_id: BytesN<32>, + pub lessor: Address, + pub lessee: Address, + pub timestamp: u64, +} + +/// A leased asset was checked back in by the lessor. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LeaseCheckedIn { + #[topic] + pub lease_id: BytesN<32>, + pub timestamp: u64, +} + +/// A lease was cancelled by the lessor or admin. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LeaseCancelled { + #[topic] + pub lease_id: BytesN<32>, + pub caller: Address, + pub timestamp: u64, +} + +// --------------------------------------------------------------------------- +// Emission helpers +// --------------------------------------------------------------------------- + +pub fn contract_initialized(env: &Env, admin: &Address) { + ContractInitialized { + admin: admin.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn asset_registered(env: &Env, asset_id: &BytesN<32>, owner: &Address) { + AssetRegistered { + asset_id: asset_id.clone(), + owner: owner.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn asset_transferred( + env: &Env, + asset_id: &BytesN<32>, + old_owner: &Address, + new_owner: &Address, +) { + AssetTransferred { + asset_id: asset_id.clone(), + old_owner: old_owner.clone(), + new_owner: new_owner.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn asset_retired(env: &Env, asset_id: &BytesN<32>, caller: &Address) { + AssetRetired { + asset_id: asset_id.clone(), + caller: caller.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn registrar_added(env: &Env, registrar: &Address, caller: &Address) { + RegistrarAdded { + registrar: registrar.clone(), + caller: caller.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn registrar_removed(env: &Env, registrar: &Address, caller: &Address) { + RegistrarRemoved { + registrar: registrar.clone(), + caller: caller.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn contract_paused(env: &Env, caller: &Address) { + ContractPaused { + caller: caller.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn contract_unpaused(env: &Env, caller: &Address) { + ContractUnpaused { + caller: caller.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn policy_created( + env: &Env, + policy_id: &BytesN<32>, + asset_id: &BytesN<32>, + insurer: &Address, + holder: &Address, +) { + PolicyCreated { + policy_id: policy_id.clone(), + asset_id: asset_id.clone(), + insurer: insurer.clone(), + holder: holder.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn policy_cancelled(env: &Env, policy_id: &BytesN<32>, caller: &Address) { + PolicyCancelled { + policy_id: policy_id.clone(), + caller: caller.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn claim_submitted( + env: &Env, + claim_id: &BytesN<32>, + policy_id: &BytesN<32>, + claimant: &Address, + amount: i128, +) { + ClaimSubmitted { + claim_id: claim_id.clone(), + policy_id: policy_id.clone(), + claimant: claimant.clone(), + amount, + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn claim_status_updated(env: &Env, claim_id: &BytesN<32>, status: ClaimStatus) { + ClaimStatusUpdated { + claim_id: claim_id.clone(), + status, + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn lease_created( + env: &Env, + lease_id: &BytesN<32>, + asset_id: &BytesN<32>, + lessor: &Address, + lessee: &Address, +) { + LeaseCreated { + lease_id: lease_id.clone(), + asset_id: asset_id.clone(), + lessor: lessor.clone(), + lessee: lessee.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn lease_checked_in(env: &Env, lease_id: &BytesN<32>) { + LeaseCheckedIn { + lease_id: lease_id.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn lease_cancelled(env: &Env, lease_id: &BytesN<32>, caller: &Address) { + LeaseCancelled { + lease_id: lease_id.clone(), + caller: caller.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} diff --git a/contracts/contrib/src/insurance.rs b/contracts/contrib/src/insurance.rs index 9fd0b6dec..5f1db4ac5 100644 --- a/contracts/contrib/src/insurance.rs +++ b/contracts/contrib/src/insurance.rs @@ -1,5 +1,5 @@ use crate::DataKey as GlobalDataKey; -use soroban_sdk::{contracttype, symbol_short, Address, BytesN, Env, String, Vec}; +use soroban_sdk::{contracttype, Address, BytesN, Env, String, Vec}; #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] @@ -71,9 +71,12 @@ pub fn create_policy(env: Env, asset_id: BytesN<32>, policy_data: InsurancePolic store.set(&key, &policy_data); - env.events().publish( - (symbol_short!("pol_cre"), policy_data.policy_id.clone()), - (asset_id, policy_data.insurer, policy_data.holder), + crate::events::policy_created( + &env, + &policy_data.policy_id, + &asset_id, + &policy_data.insurer, + &policy_data.holder, ); } @@ -97,10 +100,7 @@ pub fn cancel_policy(env: Env, policy_id: BytesN<32>, caller: Address) { policy.status = PolicyStatus::Cancelled; store.set(&key, &policy); - env.events().publish( - (symbol_short!("pol_can"), policy_id), - (caller, env.ledger().timestamp()), - ); + crate::events::policy_cancelled(&env, &policy_id, &caller); } pub fn is_policy_active(env: Env, policy_id: BytesN<32>) -> bool { @@ -159,10 +159,7 @@ pub fn submit_claim( claims.push_back(claim.claim_id.clone()); store.set(&list_key, &claims); - env.events().publish( - (symbol_short!("clm_sub"), claim.claim_id.clone()), - (claim.policy_id, claimant, amount), - ); + crate::events::claim_submitted(&env, &claim.claim_id, &claim.policy_id, &claimant, amount); } pub fn update_claim_status( @@ -184,10 +181,7 @@ pub fn update_claim_status( claim.status = new_status; store.set(&claim_key, &claim); - env.events().publish( - (symbol_short!("clm_upd"), claim_id), - (claim.status, env.ledger().timestamp()), - ); + crate::events::claim_status_updated(&env, &claim_id, claim.status); } pub fn get_claim(env: Env, claim_id: BytesN<32>) -> InsuranceClaim { diff --git a/contracts/contrib/src/lease.rs b/contracts/contrib/src/lease.rs index 27522c733..f37671ae0 100644 --- a/contracts/contrib/src/lease.rs +++ b/contracts/contrib/src/lease.rs @@ -1,5 +1,5 @@ use crate::{Asset, DataKey as GlobalDataKey}; -use soroban_sdk::{contracttype, symbol_short, Address, BytesN, Env, Vec}; +use soroban_sdk::{contracttype, Address, BytesN, Env, Vec}; #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] @@ -82,10 +82,7 @@ pub fn create_lease( leases.push_back(lease_id.clone()); store.set(&list_key, &leases); - env.events().publish( - (symbol_short!("lease_cr"), lease_id.clone()), - (asset_id, lessor, lessee), - ); + crate::events::lease_created(&env, &lease_id, &asset_id, &lessor, &lessee); lease_id } @@ -103,10 +100,7 @@ pub fn check_in_lease(env: Env, lease_id: BytesN<32>, caller: Address) { lease.status = LeaseStatus::Returned; store.set(&key, &lease); - env.events().publish( - (symbol_short!("lease_in"), lease_id), - (env.ledger().timestamp(),), - ); + crate::events::lease_checked_in(&env, &lease_id); } pub fn cancel_lease(env: Env, lease_id: BytesN<32>, caller: Address) { @@ -124,10 +118,7 @@ pub fn cancel_lease(env: Env, lease_id: BytesN<32>, caller: Address) { lease.status = LeaseStatus::Cancelled; store.set(&key, &lease); - env.events().publish( - (symbol_short!("lease_can"), lease_id), - (caller, env.ledger().timestamp()), - ); + crate::events::lease_cancelled(&env, &lease_id, &caller); } pub fn get_active_leases(env: Env, asset_id: BytesN<32>) -> Vec> { diff --git a/contracts/contrib/src/lib.rs b/contracts/contrib/src/lib.rs index 539797f3d..ce2009295 100644 --- a/contracts/contrib/src/lib.rs +++ b/contracts/contrib/src/lib.rs @@ -2,6 +2,7 @@ #![allow(clippy::too_many_arguments)] mod audit; +pub mod events; mod pause; mod types; @@ -12,9 +13,7 @@ mod lease; mod tests; use crate::types::AssetStatus; -use soroban_sdk::{ - contract, contractimpl, contracttype, symbol_short, Address, BytesN, Env, String, Vec, -}; +use soroban_sdk::{contract, contractimpl, contracttype, Address, BytesN, Env, String, Vec}; #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] @@ -71,6 +70,8 @@ impl ContribContract { env.storage() .persistent() .set(&DataKey::AuthorizedRegistrar(admin.clone()), &true); + + events::contract_initialized(&env, &admin); } pub fn get_admin(env: Env) -> Address { @@ -92,7 +93,9 @@ impl ContribContract { } env.storage() .persistent() - .set(&DataKey::AuthorizedRegistrar(registrar), &true); + .set(&DataKey::AuthorizedRegistrar(registrar.clone()), &true); + + events::registrar_added(&env, ®istrar, &caller); } pub fn remove_authorized_registrar(env: Env, caller: Address, registrar: Address) { @@ -107,7 +110,9 @@ impl ContribContract { } env.storage() .persistent() - .set(&DataKey::AuthorizedRegistrar(registrar), &false); + .set(&DataKey::AuthorizedRegistrar(registrar.clone()), &false); + + events::registrar_removed(&env, ®istrar, &caller); } pub fn is_authorized_registrar(env: Env, address: Address) -> bool { @@ -172,10 +177,7 @@ impl ContribContract { String::from_str(&env, "Asset registered"), ); - env.events().publish( - (symbol_short!("asset_reg"), asset.id.clone()), - (asset.owner, env.ledger().timestamp()), - ); + events::asset_registered(&env, &asset.id, &asset.owner); } pub fn transfer_asset(env: Env, asset_id: BytesN<32>, new_owner: Address, caller: Address) { @@ -213,10 +215,7 @@ impl ContribContract { String::from_str(&env, "Asset transferred"), ); - env.events().publish( - (symbol_short!("asset_tra"), asset_id.clone()), - (old_owner, new_owner, env.ledger().timestamp()), - ); + events::asset_transferred(&env, &asset_id, &old_owner, &new_owner); } pub fn retire_asset(env: Env, asset_id: BytesN<32>, caller: Address) { @@ -247,10 +246,7 @@ impl ContribContract { String::from_str(&env, "Asset retired"), ); - env.events().publish( - (symbol_short!("asset_ret"), asset_id), - (caller, env.ledger().timestamp()), - ); + events::asset_retired(&env, &asset_id, &caller); } pub fn get_asset(env: Env, asset_id: BytesN<32>) -> Option { diff --git a/contracts/contrib/src/pause.rs b/contracts/contrib/src/pause.rs index 655f0c590..d11152dbe 100644 --- a/contracts/contrib/src/pause.rs +++ b/contracts/contrib/src/pause.rs @@ -14,7 +14,7 @@ pub fn pause(env: &Env, caller: Address) { panic!("Only admin can call this function"); } env.storage().persistent().set(&DataKey::Paused, &true); - env.events().publish(("pause",), ()); + crate::events::contract_paused(env, &caller); } pub fn unpause(env: &Env, caller: Address) { @@ -28,7 +28,7 @@ pub fn unpause(env: &Env, caller: Address) { panic!("Only admin can call this function"); } env.storage().persistent().set(&DataKey::Paused, &false); - env.events().publish(("unpause",), ()); + crate::events::contract_unpaused(env, &caller); } pub fn is_paused(env: &Env) -> bool { diff --git a/contracts/contrib/src/tests/mod.rs b/contracts/contrib/src/tests/mod.rs index ca24b9628..362a93afc 100644 --- a/contracts/contrib/src/tests/mod.rs +++ b/contracts/contrib/src/tests/mod.rs @@ -5,7 +5,10 @@ mod dividends; mod tokenization; mod voting; use crate::{Asset, AssetStatus, ContribContract, ContribContractClient}; -use soroban_sdk::{testutils::Address as _, testutils::Events as _, Address, BytesN, Env, String}; +use soroban_sdk::{ + testutils::Address as _, testutils::Events as _, Address, BytesN, Env, String, Symbol, + TryIntoVal, +}; fn create_env() -> Env { Env::default() @@ -120,15 +123,31 @@ fn test_register_asset_emits_event() { let asset_id = generate_asset_id(&env, 1); let asset = create_test_asset(&env, &owner, asset_id.clone()); - let initial_events = env.events().all().len(); - env.mock_all_auths(); client.register_asset(&admin, &asset); - let final_events = env.events().all().len(); - assert!( - final_events > initial_events, - "Expected asset registered event to be emitted" + // `events().all()` reports the events of the most recent invocation, so + // this is exactly what register_asset published. + let events = env.events().all(); + assert_eq!( + events.len(), + 1, + "register_asset should emit exactly one event" + ); + + let (_contract, topics, _data) = events.last().unwrap(); + + let name: Symbol = topics.get(0).unwrap().try_into_val(&env).unwrap(); + assert_eq!( + name, + Symbol::new(&env, "asset_registered"), + "topic 0 should name the event" + ); + + let emitted_id: BytesN<32> = topics.get(1).unwrap().try_into_val(&env).unwrap(); + assert_eq!( + emitted_id, asset_id, + "topic 1 should be the asset id so consumers can index on it" ); } diff --git a/contracts/multisig_transfer/Cargo.toml b/contracts/multisig-transfer/Cargo.toml similarity index 100% rename from contracts/multisig_transfer/Cargo.toml rename to contracts/multisig-transfer/Cargo.toml diff --git a/contracts/multisig_transfer/src/approvals.rs b/contracts/multisig-transfer/src/approvals.rs similarity index 100% rename from contracts/multisig_transfer/src/approvals.rs rename to contracts/multisig-transfer/src/approvals.rs diff --git a/contracts/multisig_transfer/src/errors.rs b/contracts/multisig-transfer/src/errors.rs similarity index 100% rename from contracts/multisig_transfer/src/errors.rs rename to contracts/multisig-transfer/src/errors.rs diff --git a/contracts/multisig-transfer/src/events.rs b/contracts/multisig-transfer/src/events.rs new file mode 100644 index 000000000..50704476d --- /dev/null +++ b/contracts/multisig-transfer/src/events.rs @@ -0,0 +1,224 @@ +//! Typed contract events for `multisig-transfer`. +//! +//! Every event is a `#[contractevent]` struct following the workspace +//! convention documented in `contracts/EVENTS.md`: +//! +//! - The struct name is ``; the SDK derives topic 0 +//! from it in `lower_snake_case` (`TransferRequested` -> `transfer_requested`). +//! - The primary entity identifier is marked `#[topic]` so downstream systems +//! can index on it. +//! - Remaining fields form the data map, and every event carries `timestamp`. +//! +//! Because these types are emitted into the contract spec, clients can +//! generate bindings for them rather than hand-decoding tuples. + +use soroban_sdk::{contractevent, Address, BytesN, Env}; + +/// A transfer request was created and is awaiting approvals. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TransferRequested { + #[topic] + pub request_id: u64, + pub asset_id: BytesN<32>, + pub from_owner: Address, + pub to_owner: Address, + pub timestamp: u64, +} + +/// An authorized approver approved a pending transfer request. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TransferApproved { + #[topic] + pub request_id: u64, + pub approver: Address, + pub approval_count: u32, + pub timestamp: u64, +} + +/// An authorized approver rejected a pending transfer request. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TransferRejected { + #[topic] + pub request_id: u64, + pub rejector: Address, + pub reason_hash: BytesN<32>, + pub timestamp: u64, +} + +/// A fully approved transfer was executed and ownership moved in the registry. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TransferExecuted { + #[topic] + pub request_id: u64, + pub asset_id: BytesN<32>, + pub new_owner: Address, + pub timestamp: u64, +} + +/// A pending transfer request was cancelled by its requester or the admin. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TransferCancelled { + #[topic] + pub request_id: u64, + pub cancelled_by: Address, + pub timestamp: u64, +} + +/// The approval rule for an asset category was created or changed. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ApprovalRuleUpdated { + #[topic] + pub category: BytesN<32>, + pub required_approvals: u32, + pub timestamp: u64, +} + +/// An address was granted approver rights. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ApproverAdded { + #[topic] + pub approver: Address, + pub added_by: Address, + pub timestamp: u64, +} + +/// An address had its approver rights revoked. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ApproverRemoved { + #[topic] + pub approver: Address, + pub removed_by: Address, + pub timestamp: u64, +} + +/// The contract was initialized with an admin and a registry address. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ContractInitialized { + #[topic] + pub admin: Address, + pub asset_registry: Address, + pub timestamp: u64, +} + +// --------------------------------------------------------------------------- +// Emission helpers +// +// These keep call sites in `lib.rs` terse and ensure every event is published +// from exactly one place. +// --------------------------------------------------------------------------- + +pub fn contract_initialized(e: &Env, admin: &Address, asset_registry: &Address, timestamp: u64) { + ContractInitialized { + admin: admin.clone(), + asset_registry: asset_registry.clone(), + timestamp, + } + .publish(e); +} + +pub fn transfer_requested( + e: &Env, + request_id: u64, + asset_id: &BytesN<32>, + from_owner: &Address, + to_owner: &Address, + timestamp: u64, +) { + TransferRequested { + request_id, + asset_id: asset_id.clone(), + from_owner: from_owner.clone(), + to_owner: to_owner.clone(), + timestamp, + } + .publish(e); +} + +pub fn transfer_approved(e: &Env, request_id: u64, approver: &Address, count: u32, timestamp: u64) { + TransferApproved { + request_id, + approver: approver.clone(), + approval_count: count, + timestamp, + } + .publish(e); +} + +pub fn transfer_rejected( + e: &Env, + request_id: u64, + rejector: &Address, + reason_hash: &BytesN<32>, + timestamp: u64, +) { + TransferRejected { + request_id, + rejector: rejector.clone(), + reason_hash: reason_hash.clone(), + timestamp, + } + .publish(e); +} + +pub fn transfer_executed( + e: &Env, + request_id: u64, + asset_id: &BytesN<32>, + new_owner: &Address, + timestamp: u64, +) { + TransferExecuted { + request_id, + asset_id: asset_id.clone(), + new_owner: new_owner.clone(), + timestamp, + } + .publish(e); +} + +pub fn transfer_cancelled(e: &Env, request_id: u64, cancelled_by: &Address, timestamp: u64) { + TransferCancelled { + request_id, + cancelled_by: cancelled_by.clone(), + timestamp, + } + .publish(e); +} + +pub fn approval_rule_updated(e: &Env, category: &BytesN<32>, required: u32, timestamp: u64) { + ApprovalRuleUpdated { + category: category.clone(), + required_approvals: required, + timestamp, + } + .publish(e); +} + +#[allow(dead_code)] +pub fn approver_added(e: &Env, approver: &Address, added_by: &Address, timestamp: u64) { + ApproverAdded { + approver: approver.clone(), + added_by: added_by.clone(), + timestamp, + } + .publish(e); +} + +#[allow(dead_code)] +pub fn approver_removed(e: &Env, approver: &Address, removed_by: &Address, timestamp: u64) { + ApproverRemoved { + approver: approver.clone(), + removed_by: removed_by.clone(), + timestamp, + } + .publish(e); +} diff --git a/contracts/multisig_transfer/src/lib.rs b/contracts/multisig-transfer/src/lib.rs similarity index 99% rename from contracts/multisig_transfer/src/lib.rs rename to contracts/multisig-transfer/src/lib.rs index 5cb3fc176..8aa3a7398 100644 --- a/contracts/multisig_transfer/src/lib.rs +++ b/contracts/multisig-transfer/src/lib.rs @@ -29,6 +29,8 @@ impl MultiSigTransferContract { e.storage() .persistent() .set(&storage::DataKey::NextRequestId, &1u64); + + events::contract_initialized(&e, &admin, &asset_registry, utils::now(&e)); } // ---------------------------- diff --git a/contracts/multisig_transfer/src/registry.rs b/contracts/multisig-transfer/src/registry.rs similarity index 100% rename from contracts/multisig_transfer/src/registry.rs rename to contracts/multisig-transfer/src/registry.rs diff --git a/contracts/multisig_transfer/src/rules.rs b/contracts/multisig-transfer/src/rules.rs similarity index 100% rename from contracts/multisig_transfer/src/rules.rs rename to contracts/multisig-transfer/src/rules.rs diff --git a/contracts/multisig_transfer/src/storage.rs b/contracts/multisig-transfer/src/storage.rs similarity index 100% rename from contracts/multisig_transfer/src/storage.rs rename to contracts/multisig-transfer/src/storage.rs diff --git a/contracts/multisig_transfer/src/types.rs b/contracts/multisig-transfer/src/types.rs similarity index 100% rename from contracts/multisig_transfer/src/types.rs rename to contracts/multisig-transfer/src/types.rs diff --git a/contracts/multisig_transfer/src/utils.rs b/contracts/multisig-transfer/src/utils.rs similarity index 100% rename from contracts/multisig_transfer/src/utils.rs rename to contracts/multisig-transfer/src/utils.rs diff --git a/contracts/multisig-wallet/src/event_tests.rs b/contracts/multisig-wallet/src/event_tests.rs new file mode 100644 index 000000000..0e9757e59 --- /dev/null +++ b/contracts/multisig-wallet/src/event_tests.rs @@ -0,0 +1,162 @@ +//! Event emission tests. +//! +//! `env.events().all()` reports the events of the **most recent contract +//! invocation**, not every event since the test began, so each assertion here +//! looks at exactly what the entrypoint under test published. +//! +//! See `contracts/EVENTS.md` for the catalogue these assertions pin down. + +use soroban_sdk::testutils::{Address as _, Events as _}; +use soroban_sdk::{Address, Env, Symbol, TryIntoVal, Val, Vec}; + +use crate::{MultisigWallet, MultisigWalletClient}; + +fn setup(env: &Env) -> (MultisigWalletClient<'_>, Address, Address, Address) { + let admin = Address::generate(env); + let alice = Address::generate(env); + let bob = Address::generate(env); + let contract_id = env.register(MultisigWallet, ()); + let client = MultisigWalletClient::new(env, &contract_id); + env.mock_all_auths(); + client.initialize( + &admin, + &Vec::from_array(env, [alice.clone(), bob.clone()]), + &2, + ); + (client, admin, alice, bob) +} + +/// Topics of the last event published by the most recent invocation. +fn last_topics(env: &Env) -> Vec { + let events = env.events().all(); + assert!(!events.is_empty(), "expected at least one event"); + let (_contract, topics, _data) = events.last().unwrap(); + topics +} + +/// Names of every event published by the most recent invocation, in order. +fn event_names(env: &Env) -> Vec { + let mut names = Vec::new(env); + for (_contract, topics, _data) in env.events().all().iter() { + names.push_back(event_name(env, &topics)); + } + names +} + +fn event_name(env: &Env, topics: &Vec) -> Symbol { + topics + .get(0) + .expect("event must have a name topic") + .try_into_val(env) + .expect("topic 0 must decode as a Symbol") +} + +#[test] +fn initialize_emits_wallet_initialized() { + // initialize was silent before the event catalogue. + let env = Env::default(); + let (_client, admin, _alice, _bob) = setup(&env); + + let topics = last_topics(&env); + assert_eq!( + event_name(&env, &topics), + Symbol::new(&env, "wallet_initialized") + ); + let emitted: Address = topics.get(1).unwrap().try_into_val(&env).unwrap(); + assert_eq!(emitted, admin); +} + +#[test] +fn propose_change_threshold_emits_proposal_submitted() { + // The propose_* entrypoints were silent before the event catalogue, so a + // pending governance change was invisible off-chain until it executed. + let env = Env::default(); + let (client, _admin, alice, _bob) = setup(&env); + + let proposal_id = client.propose_change_threshold(&alice, &2); + + let topics = last_topics(&env); + assert_eq!( + event_name(&env, &topics), + Symbol::new(&env, "proposal_submitted") + ); + let emitted: u64 = topics.get(1).unwrap().try_into_val(&env).unwrap(); + assert_eq!(emitted, proposal_id, "proposal id must be indexable"); +} + +#[test] +fn confirm_proposal_emits_proposal_confirmed() { + let env = Env::default(); + let (client, _admin, alice, _bob) = setup(&env); + let proposal_id = client.propose_add_owner(&alice, &Address::generate(&env)); + + client.confirm_proposal(&alice, &proposal_id); + + let topics = last_topics(&env); + assert_eq!( + event_name(&env, &topics), + Symbol::new(&env, "proposal_confirmed") + ); +} + +#[test] +fn threshold_change_reaching_quorum_emits_threshold_changed() { + let env = Env::default(); + let (client, _admin, alice, bob) = setup(&env); + let proposal_id = client.propose_change_threshold(&alice, &1); + + client.confirm_proposal(&alice, &proposal_id); + // Second confirmation reaches the 2-of-2 threshold and auto-executes. + client.confirm_proposal(&bob, &proposal_id); + + // Read the events before any other call: `events().all()` only reports the + // most recent invocation, and even a read entrypoint like get_threshold() + // is an invocation that would clear this view. + let names = event_names(&env); + assert!( + names.contains(Symbol::new(&env, "proposal_confirmed")), + "the confirming call should emit proposal_confirmed" + ); + assert!( + names.contains(Symbol::new(&env, "threshold_changed")), + "reaching quorum should auto-execute and emit threshold_changed" + ); + + assert_eq!(client.get_threshold(), 1, "proposal should have executed"); +} + +#[test] +fn freeze_and_unfreeze_emit_distinct_events() { + let env = Env::default(); + let (client, _admin, alice, bob) = setup(&env); + + client.emergency_freeze(&alice); + let topics = last_topics(&env); + assert_eq!( + event_name(&env, &topics), + Symbol::new(&env, "wallet_frozen") + ); + + // Unfreezing requires an owner, not the admin. + client.emergency_unfreeze(&bob); + let topics = last_topics(&env); + assert_eq!( + event_name(&env, &topics), + Symbol::new(&env, "wallet_unfrozen") + ); +} + +#[test] +fn set_daily_limit_emits_daily_limit_changed() { + // set_daily_limit was silent before the event catalogue. + let env = Env::default(); + let (client, _admin, alice, _bob) = setup(&env); + + client.set_daily_limit(&alice, &1_000u128); + + let topics = last_topics(&env); + assert_eq!( + event_name(&env, &topics), + Symbol::new(&env, "daily_limit_changed") + ); +} diff --git a/contracts/multisig-wallet/src/events.rs b/contracts/multisig-wallet/src/events.rs new file mode 100644 index 000000000..81df159c1 --- /dev/null +++ b/contracts/multisig-wallet/src/events.rs @@ -0,0 +1,308 @@ +//! Typed contract events for `multisig-wallet`. +//! +//! See `contracts/EVENTS.md` for the workspace-wide convention. Struct names +//! are ``; the SDK derives topic 0 from the name in +//! `lower_snake_case`. The identifier a consumer would filter on (`tx_id`, +//! `proposal_id`, an owner address) is marked `#[topic]`. + +use soroban_sdk::{contractevent, Address, Env, Vec}; + +use crate::types::{ProposalType, TransactionType}; + +/// The wallet was initialized with its owner set and threshold. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct WalletInitialized { + #[topic] + pub admin: Address, + pub owners: Vec
, + pub threshold: u32, + pub timestamp: u64, +} + +/// An owner submitted a transaction for confirmation. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TransactionSubmitted { + #[topic] + pub tx_id: u64, + pub initiator: Address, + pub tx_type: TransactionType, + pub timestamp: u64, +} + +/// An owner confirmed a pending transaction. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TransactionConfirmed { + #[topic] + pub tx_id: u64, + pub confirmer: Address, + pub confirmations_count: u32, + pub timestamp: u64, +} + +/// An owner withdrew a previously given confirmation. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ConfirmationRevoked { + #[topic] + pub tx_id: u64, + pub revoker: Address, + pub timestamp: u64, +} + +/// A transaction reached its threshold and was executed. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TransactionExecuted { + #[topic] + pub tx_id: u64, + pub initiator: Address, + pub timestamp: u64, +} + +/// A pending transaction was cancelled before execution. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TransactionCancelled { + #[topic] + pub tx_id: u64, + pub caller: Address, + pub timestamp: u64, +} + +/// An owner raised a governance proposal. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProposalSubmitted { + #[topic] + pub proposal_id: u64, + pub proposer: Address, + pub proposal_type: ProposalType, + pub timestamp: u64, +} + +/// An owner confirmed a governance proposal. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProposalConfirmed { + #[topic] + pub proposal_id: u64, + pub confirmer: Address, + pub confirmations_count: u32, + pub timestamp: u64, +} + +/// An owner was added to the wallet by an executed proposal. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct OwnerAdded { + #[topic] + pub owner: Address, + pub proposer: Address, + pub timestamp: u64, +} + +/// An owner was removed from the wallet by an executed proposal. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct OwnerRemoved { + #[topic] + pub owner: Address, + pub proposer: Address, + pub timestamp: u64, +} + +/// The confirmation threshold was changed by an executed proposal. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ThresholdChanged { + pub old_threshold: u32, + pub new_threshold: u32, + pub timestamp: u64, +} + +/// The wallet was frozen; all mutating operations now reject. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct WalletFrozen { + #[topic] + pub caller: Address, + pub timestamp: u64, +} + +/// The wallet was unfrozen and resumed normal operation. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct WalletUnfrozen { + #[topic] + pub caller: Address, + pub timestamp: u64, +} + +/// The daily spend limit was changed. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DailyLimitChanged { + #[topic] + pub caller: Address, + pub limit: u128, + pub timestamp: u64, +} + +/// A transaction was rejected because it would exceed the daily spend limit. +#[contractevent] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DailyLimitReached { + pub limit: u128, + pub attempted_total: u128, + pub timestamp: u64, +} + +// --------------------------------------------------------------------------- +// Emission helpers +// --------------------------------------------------------------------------- + +pub fn wallet_initialized(env: &Env, admin: &Address, owners: &Vec
, threshold: u32) { + WalletInitialized { + admin: admin.clone(), + owners: owners.clone(), + threshold, + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn transaction_submitted(env: &Env, tx_id: u64, initiator: &Address, tx_type: TransactionType) { + TransactionSubmitted { + tx_id, + initiator: initiator.clone(), + tx_type, + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn transaction_confirmed(env: &Env, tx_id: u64, confirmer: &Address, count: u32) { + TransactionConfirmed { + tx_id, + confirmer: confirmer.clone(), + confirmations_count: count, + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn confirmation_revoked(env: &Env, tx_id: u64, revoker: &Address) { + ConfirmationRevoked { + tx_id, + revoker: revoker.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn transaction_executed(env: &Env, tx_id: u64, initiator: &Address) { + TransactionExecuted { + tx_id, + initiator: initiator.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn transaction_cancelled(env: &Env, tx_id: u64, caller: &Address) { + TransactionCancelled { + tx_id, + caller: caller.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn proposal_submitted( + env: &Env, + proposal_id: u64, + proposer: &Address, + proposal_type: ProposalType, +) { + ProposalSubmitted { + proposal_id, + proposer: proposer.clone(), + proposal_type, + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn proposal_confirmed(env: &Env, proposal_id: u64, confirmer: &Address, count: u32) { + ProposalConfirmed { + proposal_id, + confirmer: confirmer.clone(), + confirmations_count: count, + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn owner_added(env: &Env, owner: &Address, proposer: &Address) { + OwnerAdded { + owner: owner.clone(), + proposer: proposer.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn owner_removed(env: &Env, owner: &Address, proposer: &Address) { + OwnerRemoved { + owner: owner.clone(), + proposer: proposer.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn threshold_changed(env: &Env, old_threshold: u32, new_threshold: u32) { + ThresholdChanged { + old_threshold, + new_threshold, + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn wallet_frozen(env: &Env, caller: &Address) { + WalletFrozen { + caller: caller.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn wallet_unfrozen(env: &Env, caller: &Address) { + WalletUnfrozen { + caller: caller.clone(), + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn daily_limit_changed(env: &Env, caller: &Address, limit: u128) { + DailyLimitChanged { + caller: caller.clone(), + limit, + timestamp: env.ledger().timestamp(), + } + .publish(env); +} + +pub fn daily_limit_reached(env: &Env, limit: u128, attempted_total: u128) { + DailyLimitReached { + limit, + attempted_total, + timestamp: env.ledger().timestamp(), + } + .publish(env); +} diff --git a/contracts/multisig-wallet/src/lib.rs b/contracts/multisig-wallet/src/lib.rs index 3533ed84c..ba5ebbeb6 100644 --- a/contracts/multisig-wallet/src/lib.rs +++ b/contracts/multisig-wallet/src/lib.rs @@ -1,9 +1,12 @@ #![no_std] -use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env, Symbol, Val, Vec}; +use soroban_sdk::{contract, contractimpl, Address, Env, Symbol, Val, Vec}; mod errors; #[cfg(test)] +mod event_tests; +pub mod events; +#[cfg(test)] mod tests; mod types; @@ -63,6 +66,8 @@ impl MultisigWallet { .set(&DataKey::OwnerProfile(owner), &profile); } + events::wallet_initialized(&env, &admin, &owners, threshold); + Ok(()) } @@ -109,10 +114,7 @@ impl MultisigWallet { .persistent() .set(&DataKey::Transaction(tx_id), &tx); - env.events().publish( - (symbol_short!("tx_sub"), tx_id), - (initiator, tx.tx_type, env.ledger().timestamp()), - ); + events::transaction_submitted(&env, tx_id, &initiator, tx.tx_type); Ok(tx_id) } @@ -169,10 +171,7 @@ impl MultisigWallet { .persistent() .set(&DataKey::OwnerProfile(confirmer.clone()), &profile); - env.events().publish( - (symbol_short!("tx_conf"), tx_id), - (confirmer, tx.confirmations_count, env.ledger().timestamp()), - ); + events::transaction_confirmed(&env, tx_id, &confirmer, tx.confirmations_count); // Auto-execute if threshold reached if tx.confirmations_count >= tx.required_confirmations { @@ -214,10 +213,7 @@ impl MultisigWallet { .persistent() .set(&DataKey::Transaction(tx_id), &tx); - env.events().publish( - (symbol_short!("tx_rev"), tx_id), - (revoker, env.ledger().timestamp()), - ); + events::confirmation_revoked(&env, tx_id, &revoker); Ok(()) } @@ -264,10 +260,7 @@ impl MultisigWallet { let _result: Val = env.invoke_contract(&tx.target, &tx.function_name, tx.parameters.clone()); - env.events().publish( - (symbol_short!("tx_exec"), tx_id), - (tx.initiator, _result, env.ledger().timestamp()), - ); + events::transaction_executed(&env, tx_id, &tx.initiator); Ok(()) } @@ -297,10 +290,7 @@ impl MultisigWallet { .persistent() .set(&DataKey::Transaction(tx_id), &tx); - env.events().publish( - (symbol_short!("tx_can"), tx_id), - (caller, env.ledger().timestamp()), - ); + events::transaction_cancelled(&env, tx_id, &caller); Ok(()) } @@ -405,6 +395,13 @@ impl MultisigWallet { .persistent() .set(&DataKey::Proposal(proposal_id), &proposal); + events::proposal_confirmed( + &env, + proposal_id, + &confirmer, + proposal.confirmations_received, + ); + let threshold: u32 = env.storage().instance().get(&DataKey::Threshold).unwrap(); if proposal.confirmations_received >= threshold { Self::execute_proposal(env, proposal_id)?; @@ -451,14 +448,7 @@ impl MultisigWallet { .persistent() .set(&DataKey::OwnerProfile(new_owner.clone()), &profile); - env.events().publish( - (symbol_short!("own_add"),), - ( - new_owner, - proposal.proposer.clone(), - env.ledger().timestamp(), - ), - ); + events::owner_added(&env, &new_owner, &proposal.proposer); } ProposalType::RemoveOwner => { let owner_to_remove = proposal.target_address.clone().unwrap(); @@ -472,14 +462,7 @@ impl MultisigWallet { .persistent() .remove(&DataKey::OwnerProfile(owner_to_remove.clone())); - env.events().publish( - (symbol_short!("own_rem"),), - ( - owner_to_remove, - proposal.proposer.clone(), - env.ledger().timestamp(), - ), - ); + events::owner_removed(&env, &owner_to_remove, &proposal.proposer); } ProposalType::ChangeThreshold => { let new_threshold = proposal.new_threshold.unwrap(); @@ -488,10 +471,7 @@ impl MultisigWallet { .instance() .set(&DataKey::Threshold, &new_threshold); - env.events().publish( - (symbol_short!("thr_chg"),), - (old_threshold, new_threshold, env.ledger().timestamp()), - ); + events::threshold_changed(&env, old_threshold, new_threshold); } } @@ -525,10 +505,7 @@ impl MultisigWallet { // For now, let's just set it. env.storage().instance().set(&DataKey::Frozen, &true); - env.events().publish( - (symbol_short!("frozen"),), - (caller, env.ledger().timestamp()), - ); + events::wallet_frozen(&env, &caller); Ok(()) } @@ -538,10 +515,7 @@ impl MultisigWallet { env.storage().instance().set(&DataKey::Frozen, &false); - env.events().publish( - (symbol_short!("unfrozen"),), - (caller, env.ledger().timestamp()), - ); + events::wallet_unfrozen(&env, &caller); Ok(()) } @@ -551,6 +525,8 @@ impl MultisigWallet { // Should probably be a proposal too. env.storage().instance().set(&DataKey::DailyLimit, &limit); + + events::daily_limit_changed(&env, &caller, limit); Ok(()) } @@ -642,10 +618,7 @@ impl MultisigWallet { .unwrap_or(0); if spent + amount > limit { - env.events().publish( - (symbol_short!("lim_rch"),), - (limit, spent + amount, env.ledger().timestamp()), - ); + events::daily_limit_reached(env, limit, spent + amount); return Err(Error::DailyLimitExceeded); } @@ -686,6 +659,8 @@ impl MultisigWallet { .persistent() .set(&DataKey::Proposal(id), &proposal); + events::proposal_submitted(env, id, &proposer, proposal.proposal_type); + // Auto-confirm for proposer // Self::confirm_proposal(env.clone(), proposer, id)?; diff --git a/contracts/multisig_transfer/src/events.rs b/contracts/multisig_transfer/src/events.rs deleted file mode 100644 index 1c0536e8f..000000000 --- a/contracts/multisig_transfer/src/events.rs +++ /dev/null @@ -1,84 +0,0 @@ -use soroban_sdk::{Address, BytesN, Env}; - -pub fn transfer_requested( - e: &Env, - request_id: u64, - asset_id: &BytesN<32>, - from_owner: &Address, - to_owner: &Address, - timestamp: u64, -) { - e.events().publish( - ("TransferRequested",), - ( - request_id, - asset_id.clone(), - from_owner.clone(), - to_owner.clone(), - timestamp, - ), - ); -} - -pub fn transfer_approved(e: &Env, request_id: u64, approver: &Address, count: u32, timestamp: u64) { - e.events().publish( - ("TransferApproved",), - (request_id, approver.clone(), count, timestamp), - ); -} - -pub fn transfer_rejected( - e: &Env, - request_id: u64, - rejector: &Address, - reason_hash: &BytesN<32>, - timestamp: u64, -) { - e.events().publish( - ("TransferRejected",), - (request_id, rejector.clone(), reason_hash.clone(), timestamp), - ); -} - -pub fn transfer_executed( - e: &Env, - request_id: u64, - asset_id: &BytesN<32>, - new_owner: &Address, - timestamp: u64, -) { - e.events().publish( - ("TransferExecuted",), - (request_id, asset_id.clone(), new_owner.clone(), timestamp), - ); -} - -pub fn transfer_cancelled(e: &Env, request_id: u64, cancelled_by: &Address, timestamp: u64) { - e.events().publish( - ("TransferCancelled",), - (request_id, cancelled_by.clone(), timestamp), - ); -} - -pub fn approval_rule_updated(e: &Env, category: &BytesN<32>, required: u32, timestamp: u64) { - e.events().publish( - ("ApprovalRuleUpdated",), - (category.clone(), required, timestamp), - ); -} - -#[allow(dead_code)] -pub fn approver_added(e: &Env, approver: &Address, added_by: &Address, timestamp: u64) { - e.events().publish( - ("ApproverAdded",), - (approver.clone(), added_by.clone(), timestamp), - ); -} - -#[allow(dead_code)] -pub fn approver_removed(e: &Env, approver: &Address, removed_by: &Address, timestamp: u64) { - e.events().publish( - ("ApproverRemoved",), - (approver.clone(), removed_by.clone(), timestamp), - ); -}