From 07381430e2748738418f04d9864bb467d45a768d Mon Sep 17 00:00:00 2001 From: Rob Knight Date: Mon, 24 Aug 2026 21:24:55 +0000 Subject: [PATCH] Add a two-party trade demo over iroh Run the joint-tx swap protocol across two processes with serialized plan, offer, and acceptance messages over iroh. The initiator assembles, finalizes, and posts through the relayer. Load local plugins and objects, persist projected received states before endorsing, and reconcile custody after confirmation. Include mock mode and end-to-end engine tests. --- CLAUDE.md | 1 + Cargo.lock | 1748 ++++++++++++++++++++++++++++-- Cargo.toml | 1 + interfaces/trade/Cargo.toml | 29 + interfaces/trade/src/engine.rs | 614 +++++++++++ interfaces/trade/src/lib.rs | 14 + interfaces/trade/src/local.rs | 312 ++++++ interfaces/trade/src/main.rs | 701 ++++++++++++ interfaces/trade/src/net.rs | 119 ++ interfaces/trade/src/post.rs | 55 + interfaces/trade/src/protocol.rs | 166 +++ interfaces/trade/src/tests.rs | 175 +++ interfaces/trade/src/ui.rs | 86 ++ libs/driver/src/clients/mod.rs | 14 +- libs/driver/src/lib.rs | 6 +- 15 files changed, 3927 insertions(+), 114 deletions(-) create mode 100644 interfaces/trade/Cargo.toml create mode 100644 interfaces/trade/src/engine.rs create mode 100644 interfaces/trade/src/lib.rs create mode 100644 interfaces/trade/src/local.rs create mode 100644 interfaces/trade/src/main.rs create mode 100644 interfaces/trade/src/net.rs create mode 100644 interfaces/trade/src/post.rs create mode 100644 interfaces/trade/src/protocol.rs create mode 100644 interfaces/trade/src/tests.rs create mode 100644 interfaces/trade/src/ui.rs diff --git a/CLAUDE.md b/CLAUDE.md index 8885276d..36d3edfa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,6 +24,7 @@ The workspace is declared in `Cargo.toml`. Crate-by-crate: | `sdk` | Rhai engine + two-phase Loader/Executor that compiles plugin scripts into pod2 modules. | | `txlib` | Transaction state machine: `StateHeader`, `GroundingWitness`, `Tx`, `TxBuilder` + `TxFinalized` rule. Accepts local object dictionaries or statements supplied by another party; does not model party identities. | | `joint-tx` | Multi-party transaction planning: validates participants and custody, derives the shared `TxPlan` and proving schedule, and exchanges proof contributions. Depends on `txlib`. | +| `interfaces/trade` | Two-party swap CLI over iroh and `joint-tx`: loads local objects and plugins, proves in process, posts through the relayer, and imports the received object through dobjd. | | `synchronizer` | Long-running service: ingests Ethereum blobs, maintains Merkle state, serves HTTP queries. | | `relayer` | HTTP service that wraps proofs as EIP-4844 blob txs and submits them. | | `archiver` | Service that follows beacon blocks and archives blobs filtered by destination address to the filesystem (no DB). Serves them via a beacon-compatible HTTP API; the synchronizer reads blobs from it. | diff --git a/Cargo.lock b/Cargo.lock index a8526184..4873742f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,41 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.8.12" @@ -254,7 +289,7 @@ dependencies = [ "either", "serde", "serde_with", - "sha2", + "sha2 0.10.9", "thiserror 2.0.18", ] @@ -399,7 +434,7 @@ dependencies = [ "either", "futures", "futures-utils-wasm", - "lru", + "lru 0.16.3", "parking_lot", "pin-project", "reqwest 0.12.28", @@ -946,6 +981,15 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + [[package]] name = "archiver" version = "0.1.0" @@ -1412,6 +1456,18 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "attohttpc" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" +dependencies = [ + "base64 0.22.1", + "http", + "log", + "url", +] + [[package]] name = "auto_impl" version = "1.3.0" @@ -1532,12 +1588,29 @@ dependencies = [ "tokio", ] +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + [[package]] name = "base16ct" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + [[package]] name = "base64" version = "0.21.7" @@ -1658,6 +1731,19 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake3" +version = "1.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae" +dependencies = [ + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] + [[package]] name = "block" version = "0.1.6" @@ -1673,6 +1759,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -1941,9 +2036,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "chacha20" @@ -1970,6 +2065,16 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -2045,6 +2150,21 @@ dependencies = [ "wire-types", ] +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -2088,6 +2208,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "const-random" version = "0.1.18" @@ -2128,6 +2254,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + [[package]] name = "convert_case" version = "0.4.0" @@ -2153,6 +2285,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "cordyceps" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9ab7e0ca1d179628fa0172b2b97203c7fa0cd81be2448bd446fb9559ca9261" +dependencies = [ + "loom", + "tracing", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -2245,6 +2387,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -2316,6 +2464,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", + "rand_core 0.10.0", +] + [[package]] name = "cssparser" version = "0.29.6" @@ -2372,6 +2530,53 @@ version = "0.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "curve25519-dalek" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "curve25519-dalek-derive", + "digest 0.11.3", + "fiat-crypto", + "rand_core 0.10.0", + "rustc_version 0.4.1", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "darling" version = "0.20.11" @@ -2493,9 +2698,29 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.10.0" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "data-encoding-macro" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6a127ecbb3c4632e1525380e04c0c3fcf8dcb44d32a79ea290d8a36906edcd8" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +checksum = "c54e03a951783e8b327515db3f2a2fd0e3bed362a96b066f341ce66ed49b4ead" +dependencies = [ + "data-encoding", + "syn 2.0.117", +] [[package]] name = "dbus" @@ -2514,8 +2739,19 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", - "pem-rfc7468", + "const-oid 0.9.6", + "pem-rfc7468 0.7.0", + "zeroize", +] + +[[package]] +name = "der" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" +dependencies = [ + "const-oid 0.10.2", + "pem-rfc7468 1.0.0", "zeroize", ] @@ -2618,6 +2854,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "diatomic-waker" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c" + [[package]] name = "digest" version = "0.9.0" @@ -2633,12 +2875,22 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", +] + [[package]] name = "directories" version = "6.0.0" @@ -2878,13 +3130,40 @@ version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "der", + "der 0.7.10", "digest 0.10.7", "elliptic-curve", "rfc6979", - "serdect", - "signature", - "spki", + "serdect 0.2.0", + "signature 2.2.0", + "spki 0.7.3", +] + +[[package]] +name = "ed25519" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" +dependencies = [ + "pkcs8 0.11.0", + "serdect 0.4.3", + "signature 3.0.0", +] + +[[package]] +name = "ed25519-dalek" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.10.0", + "serde", + "sha2 0.11.0", + "signature 3.0.0", + "subtle", + "zeroize", ] [[package]] @@ -2914,16 +3193,16 @@ version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ - "base16ct", + "base16ct 0.2.0", "crypto-bigint", "digest 0.10.7", "ff", "generic-array", "group", - "pkcs8", + "pkcs8 0.10.2", "rand_core 0.6.4", "sec1", - "serdect", + "serdect 0.2.0", "subtle", "zeroize", ] @@ -2948,6 +3227,18 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -2963,6 +3254,17 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" +[[package]] +name = "enum-assoc" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0590c4a94da3372e83493b956755a6e2266830b6e4e3b101afe66e3f39477b91" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "enum-ordinalize" version = "4.3.2" @@ -3164,6 +3466,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + [[package]] name = "field-offset" version = "0.3.6" @@ -3235,7 +3543,7 @@ checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ "futures-core", "futures-sink", - "spin", + "spin 0.9.8", ] [[package]] @@ -3338,6 +3646,19 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-buffered" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4421cb78ee172b6b06080093479d3c50f058e7c81b7d577bbb8d118d551d4cd5" +dependencies = [ + "cordyceps", + "diatomic-waker", + "futures-core", + "pin-project-lite", + "spin 0.10.1", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -3555,6 +3876,21 @@ dependencies = [ "x11", ] +[[package]] +name = "generator" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link 0.2.1", + "windows-result 0.4.1", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -3611,11 +3947,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", "rand_core 0.10.0", "wasip2", "wasip3", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", ] [[package]] @@ -3703,6 +4051,18 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "gobject-sys" version = "0.18.0" @@ -3856,8 +4216,19 @@ dependencies = [ ] [[package]] -name = "hashlink" -version = "0.10.0" +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashlink" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ @@ -3897,6 +4268,83 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "hickory-net" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" +dependencies = [ + "async-trait", + "bytes", + "cfg-if", + "data-encoding", + "futures-channel", + "futures-io", + "futures-util", + "h2", + "hickory-proto", + "http", + "idna", + "ipnet", + "jni 0.22.4", + "rand 0.10.0", + "rustls", + "thiserror 2.0.18", + "tinyvec", + "tokio", + "tokio-rustls", + "tracing", + "url", +] + +[[package]] +name = "hickory-proto" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" +dependencies = [ + "data-encoding", + "idna", + "ipnet", + "jni 0.22.4", + "once_cell", + "prefix-trie", + "rand 0.10.0", + "ring", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-net", + "hickory-proto", + "ipconfig", + "ipnet", + "jni 0.22.4", + "moka", + "ndk-context", + "once_cell", + "parking_lot", + "rand 0.10.0", + "resolv-conf", + "rustls", + "smallvec", + "system-configuration", + "thiserror 2.0.18", + "tokio", + "tokio-rustls", + "tracing", +] + [[package]] name = "hkdf" version = "0.12.4" @@ -3991,6 +4439,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.8.1" @@ -4085,7 +4542,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.61.2", ] [[package]] @@ -4200,6 +4657,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "identity-hash" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfdd7caa900436d8f13b2346fe10257e0c05c1f1f9e351f4f5d57c03bd5f45da" + [[package]] name = "idna" version = "1.1.0" @@ -4221,6 +4684,26 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "igd-next" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de7238d487a9aff61f81b5ab41c0a841532a115a398b5fa92a2fadd0885e2581" +dependencies = [ + "attohttpc", + "bytes", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "rand 0.10.0", + "tokio", + "url", + "xmltree", +] + [[package]] name = "impl-codec" version = "0.6.0" @@ -4273,6 +4756,15 @@ dependencies = [ "cfb", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.13" @@ -4297,11 +4789,27 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ipconfig" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" +dependencies = [ + "socket2", + "widestring", + "windows-registry", + "windows-result 0.4.1", + "windows-sys 0.61.2", +] + [[package]] name = "ipnet" version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +dependencies = [ + "serde", +] [[package]] name = "iri-string" @@ -4313,6 +4821,172 @@ dependencies = [ "serde", ] +[[package]] +name = "iroh" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460de6bc52163b41b1646931f2897e5ab986f0966ade444467fec25024751a72" +dependencies = [ + "backon", + "blake3", + "bytes", + "cfg_aliases", + "ctutils", + "data-encoding", + "derive_more 2.1.1", + "ed25519-dalek", + "futures-util", + "getrandom 0.4.2", + "hickory-resolver", + "http", + "ipnet", + "iroh-base", + "iroh-dns", + "iroh-metrics", + "iroh-relay", + "n0-error", + "n0-future", + "n0-watcher", + "netwatch", + "noq", + "noq-proto", + "noq-udp", + "papaya", + "pin-project", + "portable-atomic", + "portmapper", + "rand 0.10.0", + "reqwest 0.13.2", + "rustc-hash", + "rustls", + "rustls-pki-types", + "serde", + "smallvec", + "strum 0.28.0", + "time", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "url", + "wasm-bindgen-futures", +] + +[[package]] +name = "iroh-base" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6be73e16ee21c923aca9b3121aaa0db936f7c7ecc156ff47b8dac944c68d59a8" +dependencies = [ + "curve25519-dalek", + "data-encoding", + "data-encoding-macro", + "derive_more 2.1.1", + "ed25519-dalek", + "getrandom 0.4.2", + "n0-error", + "rand 0.10.0", + "serde", + "url", + "zeroize", +] + +[[package]] +name = "iroh-dns" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46f6a9b39d18e6345f5c151afd299f2488e2cb5c520fe41b107b6bd3dc4c3349" +dependencies = [ + "arc-swap", + "cfg_aliases", + "derive_more 2.1.1", + "hickory-resolver", + "iroh-base", + "n0-error", + "n0-future", + "ndk-context", + "portable-atomic", + "rand 0.10.0", + "rustls", + "simple-dns", + "strum 0.28.0", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "iroh-metrics" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291065721ad7c477b972e581bbc528df031dc8eb5e39fe1ff3300ae5dfb157ef" +dependencies = [ + "iroh-metrics-derive", + "itoa", + "n0-error", + "portable-atomic", + "ryu", + "serde", + "tracing", +] + +[[package]] +name = "iroh-metrics-derive" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae5f0c4405d1fbc9fb16ff422ca40620e93dc36c30ecaba0c2aee3992b7bd48" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "iroh-relay" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24bd586cf927f7b700f56ec3639b53cb5fa901ce284784051ff71092bfbf8193" +dependencies = [ + "blake3", + "bytes", + "cfg_aliases", + "data-encoding", + "derive_more 2.1.1", + "getrandom 0.4.2", + "hickory-resolver", + "http", + "http-body-util", + "hyper", + "hyper-util", + "iroh-base", + "iroh-dns", + "iroh-metrics", + "lru 0.18.2", + "n0-error", + "n0-future", + "noq", + "noq-proto", + "num_enum", + "pin-project", + "postcard", + "rand 0.10.0", + "reqwest 0.13.2", + "rustls", + "rustls-pki-types", + "serde", + "serde_bytes", + "strum 0.28.0", + "tokio", + "tokio-rustls", + "tokio-util", + "tokio-websockets", + "tracing", + "url", + "webpki-roots 1.0.6", + "ws_stream_wasm", +] + [[package]] name = "is-docker" version = "0.2.0" @@ -4436,19 +5110,68 @@ dependencies = [ "cesu8", "cfg-if", "combine", - "jni-sys", + "jni-sys 0.3.0", "log", "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version 0.4.1", + "simd_cesu8", + "syn 2.0.117", +] + [[package]] name = "jni-sys" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -4513,8 +5236,8 @@ dependencies = [ "ecdsa", "elliptic-curve", "once_cell", - "serdect", - "sha2", + "serdect 0.2.0", + "sha2 0.10.9", ] [[package]] @@ -4575,7 +5298,7 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin", + "spin 0.9.8", ] [[package]] @@ -4727,6 +5450,19 @@ dependencies = [ "value-bag", ] +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + [[package]] name = "lru" version = "0.16.3" @@ -4736,6 +5472,15 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "lru" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d2f2f9b4ba7e6b24d95e7e899329d35be83bcded72c8540cdd5368932d1d90a" +dependencies = [ + "hashbrown 0.17.1", +] + [[package]] name = "lru-slab" version = "0.1.2" @@ -4774,6 +5519,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "mac-addr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d25b0e0b648a86960ac23b7ad4abb9717601dec6f66c165f5b037f3f03065f" + [[package]] name = "macro-string" version = "0.1.4" @@ -4939,6 +5690,23 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "moka" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + [[package]] name = "muda" version = "0.19.2" @@ -4960,6 +5728,59 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "n0-error" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c37e81176a83a77d2514528b91bdafc70ef88aab428f0e1b91aebb8d99888895" +dependencies = [ + "n0-error-macros", + "spez", +] + +[[package]] +name = "n0-error-macros" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2acd8b070213b0299282f884b4beba4e7b52d624fdcd504a3ad3665390c11e1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "n0-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2ab99dfb861450e68853d34ae665243a88b8c493d01ba957321a1e9b2312bbe" +dependencies = [ + "cfg_aliases", + "derive_more 2.1.1", + "futures-buffered", + "futures-lite", + "futures-util", + "js-sys", + "pin-project", + "send_wrapper", + "tokio", + "tokio-util", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-time", +] + +[[package]] +name = "n0-watcher" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc618745ad0b7414b149d0517ad8b5573b2fb4d4e2717add3d2446ce1fdd826" +dependencies = [ + "derive_more 2.1.1", + "n0-error", + "n0-future", +] + [[package]] name = "native-tls" version = "0.2.18" @@ -4978,27 +5799,146 @@ dependencies = [ ] [[package]] -name = "ndk" -version = "0.9.0" +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.0", + "jni-sys 0.3.0", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.0", +] + +[[package]] +name = "netdev" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "569dfbdd2efd771b24ec9bb57f956e04d4fbfc72f62b2f11961723f9b3f4b020" +dependencies = [ + "block2", + "dispatch2", + "dlopen2", + "ipnet", + "jni 0.21.1", + "libc", + "mac-addr", + "ndk-context", + "netlink-packet-core", + "netlink-packet-route", + "netlink-sys", + "objc2", + "objc2-core-foundation", + "objc2-core-wlan", + "objc2-foundation", + "objc2-system-configuration", + "once_cell", + "plist", + "windows-sys 0.61.2", +] + +[[package]] +name = "netlink-packet-core" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b897d7bd4f0af82e68d40d0344cf37e97f9c97ddf74a098de3e4da05e96ca395" +dependencies = [ + "paste", +] + +[[package]] +name = "netlink-packet-route" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2288fcb784eb3defd5fb16f4c4160d5f477de192eac730f43e1d11c24d9a007" +dependencies = [ + "bitflags 2.11.0", + "libc", + "log", + "netlink-packet-core", +] + +[[package]] +name = "netlink-proto" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93af8261786086024cd5e96e0a991dd65ced07bbf7c233a487bbc96b971d5539" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "log", + "netlink-packet-core", + "netlink-sys", + "thiserror 2.0.18", +] + +[[package]] +name = "netlink-sys" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae" dependencies = [ - "bitflags 2.11.0", - "jni-sys", + "bytes", + "futures-util", + "libc", "log", - "ndk-sys", - "num_enum", - "raw-window-handle", - "thiserror 1.0.69", + "tokio", ] [[package]] -name = "ndk-sys" -version = "0.6.0+11769913" +name = "netwatch" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +checksum = "4d9cbe01741347ef750d743d6690603f5eed8341e679fb51c8e629337aa11976" dependencies = [ - "jni-sys", + "atomic-waker", + "bytes", + "cfg_aliases", + "derive_more 2.1.1", + "ipnet", + "js-sys", + "libc", + "n0-error", + "n0-future", + "n0-watcher", + "netdev", + "netlink-packet-core", + "netlink-packet-route", + "netlink-proto", + "netlink-sys", + "noq-udp", + "objc2-core-foundation", + "objc2-system-configuration", + "pin-project-lite", + "serde", + "socket2", + "time", + "tokio", + "tokio-util", + "tracing", + "web-sys", + "windows 0.62.2", + "windows-result 0.4.1", + "wmi", ] [[package]] @@ -5036,6 +5976,68 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "noq" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e4bb6601fa543c110d8957813267d5a8d775a0f8fbaccf1f615d06ba9b10da" +dependencies = [ + "bytes", + "cfg_aliases", + "derive_more 2.1.1", + "noq-proto", + "noq-udp", + "pin-project-lite", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tracing", + "web-time", +] + +[[package]] +name = "noq-proto" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa7b5ccd819a9c68a0d955e67a881032d09b1a17219b1f90b0997a0888e1a15" +dependencies = [ + "aes-gcm", + "bytes", + "derive_more 2.1.1", + "enum-assoc", + "getrandom 0.4.2", + "identity-hash", + "lru-slab", + "rand 0.10.0", + "rand_pcg 0.10.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "sorted-index-buffer", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "noq-udp" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bba20e097a5a16cd0ad14ec882fae1e80a092a124e9422fc4dddd92e96a647" +dependencies = [ + "cfg_aliases", + "libc", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + [[package]] name = "ntapi" version = "0.4.3" @@ -5278,7 +6280,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.11.0", + "block2", "dispatch2", + "libc", "objc2", ] @@ -5327,6 +6331,20 @@ dependencies = [ "objc2-core-graphics", ] +[[package]] +name = "objc2-core-wlan" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e34919aba0d701380d911702455038a8a3587467fe0141d6a71501e7ffe48" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-security", + "objc2-security-foundation", +] + [[package]] name = "objc2-encode" version = "4.1.0" @@ -5350,6 +6368,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.11.0", "block2", + "libc", "objc2", "objc2-core-foundation", ] @@ -5387,6 +6406,41 @@ dependencies = [ "objc2-foundation", ] +[[package]] +name = "objc2-security" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-security-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef76382e9cedd18123099f17638715cc3d81dba3637d4c0d39ab69df2ef345a5" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "libc", + "objc2", + "objc2-core-foundation", + "objc2-security", +] + [[package]] name = "objc2-ui-kit" version = "0.3.2" @@ -5447,6 +6501,7 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" dependencies = [ + "critical-section", "portable-atomic", ] @@ -5456,6 +6511,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "open" version = "5.3.3" @@ -5553,6 +6614,16 @@ dependencies = [ "system-deps", ] +[[package]] +name = "papaya" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "997ee03cd38c01469a7046643714f0ad28880bcb9e6679ff0666e24817ca19b7" +dependencies = [ + "equivalent", + "seize", +] + [[package]] name = "parity-scale-codec" version = "3.7.5" @@ -5657,6 +6728,15 @@ dependencies = [ "base64ct", ] +[[package]] +name = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -5703,7 +6783,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" dependencies = [ "pest", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -5974,9 +7054,9 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ - "der", - "pkcs8", - "spki", + "der 0.7.10", + "pkcs8 0.10.2", + "spki 0.7.3", ] [[package]] @@ -5985,8 +7065,18 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der 0.8.1", + "spki 0.8.0", ] [[package]] @@ -6138,7 +7228,7 @@ dependencies = [ "serde_arrays", "serde_bytes", "serde_json", - "sha2", + "sha2 0.10.9", "strum 0.26.3", "strum_macros 0.26.4", "thiserror 2.0.18", @@ -6180,11 +7270,26 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +dependencies = [ + "serde", +] [[package]] name = "portable-atomic-util" @@ -6195,6 +7300,59 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "portmapper" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3713e4977408279158444a18c1a01ac9bf2e7eaf1fbfd1a19ac9cd18d90721" +dependencies = [ + "base64 0.22.1", + "bytes", + "derive_more 2.1.1", + "hyper-util", + "igd-next", + "iroh-metrics", + "libc", + "n0-error", + "n0-future", + "netwatch", + "num_enum", + "rand 0.10.0", + "serde", + "smallvec", + "socket2", + "time", + "tokio", + "tokio-util", + "tower-layer", + "tracing", + "url", +] + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "postcard-derive", + "serde", +] + +[[package]] +name = "postcard-derive" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0232bd009a197ceec9cc881ba46f727fcd8060a2d8d6a9dde7a69030a6fe2bb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -6225,6 +7383,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "prefix-trie" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7" +dependencies = [ + "either", + "ipnet", + "num-traits", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -6473,7 +7642,7 @@ dependencies = [ "rand_chacha 0.2.2", "rand_core 0.5.1", "rand_hc", - "rand_pcg", + "rand_pcg 0.2.1", ] [[package]] @@ -6592,6 +7761,15 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.0", +] + [[package]] name = "rand_xorshift" version = "0.4.0" @@ -6808,15 +7986,20 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", "sync_wrapper", "tokio", + "tokio-rustls", "tokio-util", "tower", "tower-http", @@ -6844,6 +8027,12 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + [[package]] name = "rfc6979" version = "0.4.0" @@ -6989,16 +8178,16 @@ version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ - "const-oid", + "const-oid 0.9.6", "digest 0.10.7", "num-bigint-dig", "num-integer", "num-traits", "pkcs1", - "pkcs8", + "pkcs8 0.10.2", "rand_core 0.6.4", - "signature", - "spki", + "signature 2.2.0", + "spki 0.7.3", "subtle", "zeroize", ] @@ -7086,23 +8275,63 @@ version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni 0.21.1", + "log", "once_cell", - "ring", - "rustls-pki-types", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", "rustls-webpki", - "subtle", - "zeroize", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", ] [[package]] -name = "rustls-pki-types" -version = "1.14.0" +name = "rustls-platform-verifier-android" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" -dependencies = [ - "web-time", - "zeroize", -] +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" @@ -7222,6 +8451,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -7260,11 +8495,11 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ - "base16ct", - "der", + "base16ct 0.2.0", + "der 0.7.10", "generic-array", - "pkcs8", - "serdect", + "pkcs8 0.10.2", + "serdect 0.2.0", "subtle", "zeroize", ] @@ -7313,6 +8548,16 @@ dependencies = [ "libc", ] +[[package]] +name = "seize" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "selectors" version = "0.24.0" @@ -7571,7 +8816,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" dependencies = [ - "base16ct", + "base16ct 0.2.0", + "serde", +] + +[[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct 1.0.0", "serde", ] @@ -7627,6 +8882,12 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.10.9" @@ -7638,6 +8899,17 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + [[package]] name = "sha3" version = "0.10.8" @@ -7693,12 +8965,46 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "rand_core 0.10.0", +] + [[package]] name = "simd-adler32" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version 0.4.1", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "simple-dns" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a75cbde1bf934313596a004973e462f9a82caa814dcf1a5f507bdf51597eeb4" +dependencies = [ + "bitflags 2.11.0", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -7769,6 +9075,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "sorted-index-buffer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea06cc588e43c632923a55450401b8f25e628131571d4e1baea1bdfdb2b5ed06" + [[package]] name = "soup3" version = "0.5.0" @@ -7795,6 +9107,17 @@ dependencies = [ "system-deps", ] +[[package]] +name = "spez" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87e960f4dca2788eeb86bbdde8dd246be8948790b7618d656e68f9b720a86e8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "spin" version = "0.9.8" @@ -7804,6 +9127,12 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spin" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" + [[package]] name = "spki" version = "0.7.3" @@ -7811,7 +9140,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.10", +] + +[[package]] +name = "spki" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der 0.8.1", ] [[package]] @@ -7853,7 +9192,7 @@ dependencies = [ "rustls", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "thiserror 2.0.18", "tokio", @@ -7891,7 +9230,7 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "sqlx-core", "sqlx-mysql", "sqlx-postgres", @@ -7934,7 +9273,7 @@ dependencies = [ "rsa", "serde", "sha1", - "sha2", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", @@ -7971,7 +9310,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", @@ -8110,6 +9449,15 @@ dependencies = [ "strum_macros 0.27.2", ] +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros 0.28.0", +] + [[package]] name = "strum_macros" version = "0.26.4" @@ -8135,6 +9483,18 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "subtle" version = "2.6.1" @@ -8174,6 +9534,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn-solidity" version = "1.5.7" @@ -8251,7 +9622,7 @@ dependencies = [ "ntapi", "objc2-core-foundation", "objc2-io-kit", - "windows", + "windows 0.61.3", ] [[package]] @@ -8288,6 +9659,12 @@ dependencies = [ "version-compare", ] +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + [[package]] name = "tao" version = "0.35.3" @@ -8306,7 +9683,7 @@ dependencies = [ "gdkwayland-sys", "gdkx11-sys", "gtk", - "jni", + "jni 0.21.1", "libc", "log", "ndk", @@ -8322,8 +9699,8 @@ dependencies = [ "tao-macros", "unicode-segmentation", "url", - "windows", - "windows-core", + "windows 0.61.3", + "windows-core 0.61.2", "windows-version", "x11-dl", ] @@ -8379,7 +9756,7 @@ dependencies = [ "gtk", "heck 0.5.0", "http", - "jni", + "jni 0.21.1", "libc", "log", "mime", @@ -8410,7 +9787,7 @@ dependencies = [ "webkit2gtk", "webview2-com", "window-vibrancy", - "windows", + "windows 0.61.3", ] [[package]] @@ -8451,7 +9828,7 @@ dependencies = [ "semver 1.0.27", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "syn 2.0.117", "tauri-utils", "thiserror 2.0.18", @@ -8510,7 +9887,7 @@ dependencies = [ "tauri-plugin", "thiserror 2.0.18", "url", - "windows", + "windows 0.61.3", "zbus 5.14.0", ] @@ -8524,7 +9901,7 @@ dependencies = [ "dpi", "gtk", "http", - "jni", + "jni 0.21.1", "objc2", "objc2-ui-kit", "objc2-web-kit", @@ -8536,7 +9913,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.61.3", ] [[package]] @@ -8547,7 +9924,7 @@ checksum = "b83849ee63ecb27a8e8d0fe51915ca215076914aca43f96db1179f0f415f6cd9" dependencies = [ "gtk", "http", - "jni", + "jni 0.21.1", "log", "objc2", "objc2-app-kit", @@ -8561,7 +9938,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.61.3", "wry", ] @@ -8722,6 +10099,7 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", + "js-sys", "libc", "num-conv", "num_threads", @@ -8876,10 +10254,34 @@ dependencies = [ "bytes", "futures-core", "futures-sink", + "futures-util", "pin-project-lite", "tokio", ] +[[package]] +name = "tokio-websockets" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52efb639344a7c6adb8e62c6f3d2c19c001ff1b79a5041ba1c6ed42e19c6aa5" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-sink", + "getrandom 0.4.2", + "http", + "httparse", + "rand 0.10.0", + "ring", + "rustls-pki-types", + "sha1_smol", + "simdutf8", + "tokio", + "tokio-rustls", + "tokio-util", +] + [[package]] name = "toml" version = "0.8.2" @@ -9134,6 +10536,31 @@ dependencies = [ "tracing-log 0.2.0", ] +[[package]] +name = "trade" +version = "0.1.0" +dependencies = [ + "anyhow", + "bs58", + "clap", + "driver", + "env_logger", + "hex", + "iroh", + "joint-tx", + "payload", + "pexe", + "pod2", + "pod2utils", + "reqwest 0.12.28", + "sdk", + "serde", + "serde_json", + "tokio", + "txlib", + "wire-types", +] + [[package]] name = "tray-icon" version = "0.23.1" @@ -9329,6 +10756,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + [[package]] name = "unroll" version = "0.1.5" @@ -9793,6 +11230,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "0.26.11" @@ -9819,8 +11265,8 @@ checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows", - "windows-core", + "windows 0.61.3", + "windows-core 0.61.2", "windows-implement", "windows-interface", ] @@ -9843,8 +11289,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ "thiserror 2.0.18", - "windows", - "windows-core", + "windows 0.61.3", + "windows-core 0.61.2", ] [[package]] @@ -9915,11 +11361,23 @@ version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-collections", - "windows-core", - "windows-future", + "windows-collections 0.2.0", + "windows-core 0.61.2", + "windows-future 0.2.1", "windows-link 0.1.3", - "windows-numerics", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", ] [[package]] @@ -9928,7 +11386,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-core", + "windows-core 0.61.2", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", ] [[package]] @@ -9944,15 +11411,39 @@ dependencies = [ "windows-strings 0.4.2", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-future" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ - "windows-core", + "windows-core 0.61.2", "windows-link 0.1.3", - "windows-threading", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", ] [[package]] @@ -9995,10 +11486,20 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-core", + "windows-core 0.61.2", "windows-link 0.1.3", ] +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + [[package]] name = "windows-registry" version = "0.6.1" @@ -10172,6 +11673,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-version" version = "0.1.7" @@ -10493,6 +12003,21 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wmi" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c81b85c57a57500e56669586496bf2abd5cf082b9d32995251185d105208b64" +dependencies = [ + "chrono", + "futures", + "log", + "serde", + "thiserror 2.0.18", + "windows 0.61.3", + "windows-core 0.61.2", +] + [[package]] name = "writeable" version = "0.6.2" @@ -10517,7 +12042,7 @@ dependencies = [ "gtk", "http", "javascriptcore-rs", - "jni", + "jni 0.21.1", "libc", "ndk", "objc2", @@ -10529,7 +12054,7 @@ dependencies = [ "once_cell", "percent-encoding", "raw-window-handle", - "sha2", + "sha2 0.10.9", "soup3", "tao-macros", "thiserror 2.0.18", @@ -10537,8 +12062,8 @@ dependencies = [ "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows", - "windows-core", + "windows 0.61.3", + "windows-core 0.61.2", "windows-version", "x11-dl", ] @@ -10612,6 +12137,21 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "xml-rs" +version = "0.8.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" + +[[package]] +name = "xmltree" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +dependencies = [ + "xml-rs", +] + [[package]] name = "yoke" version = "0.8.1" @@ -10801,18 +12341,18 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 88a08832..181c7130 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "interfaces/cli", "interfaces/gui/src-tauri", "interfaces/mcp", + "interfaces/trade", "libs/driver", "libs/sdk", "libs/txlib", diff --git a/interfaces/trade/Cargo.toml b/interfaces/trade/Cargo.toml new file mode 100644 index 00000000..a112aae6 --- /dev/null +++ b/interfaces/trade/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "trade" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "dobj-trade" +path = "src/main.rs" + +[dependencies] +anyhow = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +pod2 = { workspace = true } +pod2utils = { path = "../../libs/pod2utils" } +txlib = { path = "../../libs/txlib" } +joint-tx = { path = "../../libs/joint-tx" } +sdk = { path = "../../libs/sdk" } +payload = { path = "../../libs/payload", features = ["test-utils"] } +driver = { path = "../../libs/driver" } +pexe = { path = "../../libs/pexe" } +wire-types = { workspace = true } +hex = { workspace = true } +iroh = "1.0" +tokio = { version = "1", features = ["macros", "rt-multi-thread", "io-util", "time"] } +clap = { version = "4", features = ["derive", "env"] } +bs58 = "0.5" +env_logger = { workspace = true } +reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] } diff --git a/interfaces/trade/src/engine.rs b/interfaces/trade/src/engine.rs new file mode 100644 index 00000000..ba3e1ab2 --- /dev/null +++ b/interfaces/trade/src/engine.rs @@ -0,0 +1,614 @@ +//! Transport-independent initiator and accepter state machines. +//! +//! Each transition consumes its stage and returns the next, preventing +//! message replay and out-of-order protocol steps. + +use std::sync::Arc; + +use anyhow::{Context as _, Result, anyhow, ensure}; +use joint_tx::{ + InputState, JointEvent, JointTransaction, TransferAcceptance, TransferOffer, TxPlan, +}; +use pod2::{ + backends::plonky2::{basetypes::DEFAULT_VD_SET, mainpod::Prover, mock::mainpod::MockProver}, + frontend::{MainPod, MultiPodBuilder}, + lang::Module, + middleware::{Hash, MainPodProver, Params, Statement, VDSet, Value, containers::Dictionary}, +}; +use pod2utils::{macros::BuildContext, map, rand_raw_value}; +use txlib::{GroundingWitness, StateHeader, Tx, TxBuilder, compute_nullifier, obj_with_key}; + +use crate::protocol::{ + ACCEPTER, AcceptMsg, AcceptanceMsg, INITIATOR, LegDisclosure, OfferMsg, PlanAckMsg, PlanDataMsg, +}; + +/// Return the new-state commitment for transfer action `index`. +fn transfer_new(transaction: &JointTransaction, index: usize) -> Result { + if let Some(JointEvent::Action(leg)) = transaction.events().get(index) + && let [JointEvent::Transfer { new, .. }] = leg.as_slice() + { + return Ok(*new); + } + Err(anyhow!( + "counterparty deal does not have the swap's two-leg shape" + )) +} + +/// Class-guard predicate name and Rekey branch index. +#[derive(Clone, Debug)] +pub struct ClassGuardInfo { + pub guard_name: String, + pub rekey_branch: usize, +} + +/// Guard metadata keyed by the class hash stored in object `type`. +#[derive(Clone, Debug, Default)] +pub struct ClassDirectory { + classes: std::collections::HashMap, +} + +impl ClassDirectory { + pub fn from_sdk_module(module: &sdk::SdkModule) -> Self { + let mut classes = std::collections::HashMap::new(); + for class in module.classes() { + let hash = module + .class_hash(&class.name) + .expect("loaded module resolves its own classes"); + classes.insert( + hash, + ClassGuardInfo { + guard_name: format!("Is{}", class.name), + rekey_branch: class.actions.len(), + }, + ); + } + Self { classes } + } + + /// Merge another directory in; later entries win on collision. + pub fn absorb(&mut self, other: ClassDirectory) { + self.classes.extend(other.classes); + } + + pub fn get(&self, class_hash: Hash) -> Result<&ClassGuardInfo> { + self.classes + .get(&class_hash) + .ok_or_else(|| anyhow!("no installed class with guard hash {class_hash:#}")) + } +} + +/// Shared proving dependencies. +#[derive(Clone)] +pub struct SwapDeps { + pub modules: Vec>, + pub classes: ClassDirectory, + pub mock: bool, +} + +impl SwapDeps { + fn vd_set(&self) -> VDSet { + if self.mock { + VDSet::new(&[]) + } else { + DEFAULT_VD_SET.clone() + } + } + + fn build_ctx(&self) -> BuildContext { + BuildContext { + builder: MultiPodBuilder::new(&Params::default(), &self.vd_set()), + modules: self.modules.clone(), + } + } + + fn prove_session(&self, builder: MultiPodBuilder) -> Result { + let solution = builder + .solve() + .map_err(|err| anyhow!("proving session does not solve: {err}"))?; + let prover: Box = if self.mock { + Box::new(MockProver {}) + } else { + Box::new(Prover {}) + }; + let pod = solution + .prove(prover.as_ref()) + .map_err(|err| anyhow!("proving session failed: {err}"))? + .output_pod() + .clone(); + pod.pod.verify().context("own pod fails verification")?; + Ok(pod) + } + + /// Reject pods produced in a different proving mode. + fn check_pod_mode(&self, pod: &MainPod, whose: &str) -> Result<()> { + let pod_is_mock = pod.pod.is_mock(); + ensure!( + pod_is_mock == self.mock, + "{whose} pod is {} but this side runs {}; both sides must use the same mode", + if pod_is_mock { "mock" } else { "real" }, + if self.mock { "--mock" } else { "real proving" }, + ); + Ok(()) + } + + /// Prove `Is{class}` with `st_rekey` in the guard's Rekey branch. + fn prove_guard( + &self, + ctx: &mut BuildContext, + class_hash: Hash, + header: &StateHeader, + st_rekey: Statement, + ) -> Result { + let info = self.classes.get(class_hash)?; + let mut premises = vec![Statement::None; info.rekey_branch + 1]; + premises[info.rekey_branch] = st_rekey; + ctx.apply_custom_pred( + false, + &info.guard_name, + map!({"state_header" => header.array()}), + premises, + ) + .map_err(|err| anyhow!("guard {} does not apply: {err}", info.guard_name)) + } +} + +/// Deal independently derived by both engines. +/// Event 0 transfers to the initiator; event 1 transfers to the accepter. +struct AgreedPlan { + transaction: JointTransaction, + context: Hash, + accepter_object: LegPlan, + initiator_object: LegPlan, +} + +struct LegPlan { + old: Hash, + new: Hash, + nullifier: Hash, +} + +impl AgreedPlan { + fn derive( + accepter_object: LegPlan, + initiator_object: LegPlan, + header: &StateHeader, + ) -> Result { + let transaction = JointTransaction::new( + vec![INITIATOR.to_string(), ACCEPTER.to_string()], + INITIATOR.to_string(), + vec![ + InputState { + commitment: accepter_object.old, + holder: ACCEPTER.to_string(), + }, + InputState { + commitment: initiator_object.old, + holder: INITIATOR.to_string(), + }, + ], + vec![ + JointEvent::transfer_leg( + ACCEPTER, + INITIATOR, + accepter_object.old, + accepter_object.new, + accepter_object.nullifier, + ), + JointEvent::transfer_leg( + INITIATOR, + ACCEPTER, + initiator_object.old, + initiator_object.new, + initiator_object.nullifier, + ), + ], + ) + .map_err(|err| anyhow!("deal does not validate: {err}"))?; + let context = transaction.plan().context(header.hash()); + Ok(Self { + transaction, + context, + accepter_object, + initiator_object, + }) + } + + fn plan(&self) -> &TxPlan { + self.transaction.plan() + } + + fn new_commitments(&self) -> Vec { + vec![self.accepter_object.new, self.initiator_object.new] + } + + fn nullifiers(&self) -> Vec { + vec![ + self.accepter_object.nullifier, + self.initiator_object.nullifier, + ] + } +} + +/// Received state and transaction effect expected by one party. +pub struct SwapExpectation { + pub received: Dictionary, + pub tx_final: Hash, + pub new_commitments: Vec, + pub nullifiers: Vec, +} + +/// Finalized pod, transaction, and expected effect. +pub struct SwapOutcome { + pub pod: MainPod, + pub tx: Tx, + pub expectation: SwapExpectation, +} + +// ---------------------------------------------------------------- // +// Initiator // +// ---------------------------------------------------------------- // + +pub struct Initiator { + deps: SwapDeps, + outgoing: Dictionary, + want_class: Hash, + new_key: Value, +} + +impl Initiator { + /// Start with the outgoing state and desired class hash. + pub fn new(deps: SwapDeps, outgoing: Dictionary, want_class: Hash) -> Self { + Self { + deps, + outgoing, + want_class, + new_key: Value::from(rand_raw_value()), + } + } + + /// Validate the accepter's disclosure and ground both inputs. + pub fn on_accept( + self, + msg: &AcceptMsg, + witness: Arc, + ) -> Result<(InitiatorNegotiated, PlanDataMsg)> { + msg.accepter_object.validate(self.want_class)?; + for commitment in [ + msg.accepter_object.old_commitment, + self.outgoing.commitment(), + ] { + ensure!( + witness.created_proofs.contains_key(&commitment), + "grounding witness has no proof for input {commitment:#}" + ); + } + let incoming_new = obj_with_key(&msg.accepter_object.mid, self.new_key.clone()); + let reply = PlanDataMsg { + initiator_object: LegDisclosure::of(&self.outgoing), + header: witness.state_header.clone(), + accepter_object_new: incoming_new.commitment(), + }; + let next = InitiatorNegotiated { + deps: self.deps, + outgoing: self.outgoing, + want_class: self.want_class, + new_key: self.new_key, + incoming: msg.accepter_object.clone(), + incoming_new, + witness, + }; + Ok((next, reply)) + } +} + +pub struct InitiatorNegotiated { + deps: SwapDeps, + outgoing: Dictionary, + want_class: Hash, + new_key: Value, + incoming: LegDisclosure, + incoming_new: Dictionary, + witness: Arc, +} + +impl InitiatorNegotiated { + /// Projected received state, including its new private key. + /// Persist this state before exporting an endorsement. + pub fn projected_received(&self) -> &Dictionary { + &self.incoming_new + } + + /// Require the accepter's deal to match, then prove the initiator's offer. + pub fn on_plan_ack(self, msg: &PlanAckMsg) -> Result<(InitiatorOffered, OfferMsg)> { + let agreed = AgreedPlan::derive( + LegPlan { + old: self.incoming.old_commitment, + new: self.incoming_new.commitment(), + nullifier: self.incoming.nullifier, + }, + LegPlan { + old: self.outgoing.commitment(), + new: transfer_new(&msg.transaction, 1)?, + nullifier: compute_nullifier(&self.outgoing), + }, + &self.witness.state_header, + )?; + ensure!( + agreed.transaction == msg.transaction, + "deals diverge: this side derives tx_final {:#}, counterparty {:#}", + agreed.plan().tx_final(), + msg.transaction.plan().tx_final() + ); + + let mut session = self.deps.build_ctx(); + let offer = TransferOffer::prove(&mut session, agreed.context, &self.outgoing); + let pod = self.deps.prove_session(session.builder)?; + let reply = OfferMsg { + offer: offer.clone(), + pod, + }; + let next = InitiatorOffered { + deps: self.deps, + outgoing: self.outgoing, + want_class: self.want_class, + new_key: self.new_key, + incoming: self.incoming, + incoming_new: self.incoming_new, + witness: self.witness, + agreed, + }; + Ok((next, reply)) + } +} + +pub struct InitiatorOffered { + deps: SwapDeps, + outgoing: Dictionary, + want_class: Hash, + new_key: Value, + incoming: LegDisclosure, + incoming_new: Dictionary, + witness: Arc, + agreed: AgreedPlan, +} + +impl InitiatorOffered { + /// Validate the accepter's session, assemble both legs, and finalize. + pub fn on_acceptance(self, msg: AcceptanceMsg) -> Result { + self.deps.check_pod_mode(&msg.pod, "the accepter's")?; + msg.pod + .pod + .verify() + .context("accepter's pod fails verification")?; + msg.offer + .validate(&msg.pod, self.agreed.context, self.incoming.old_commitment) + .context("accepter's offer does not validate")?; + msg.acceptance + .validate(&msg.pod, self.agreed.initiator_object.new) + .context("accepter's acceptance does not validate")?; + msg.acceptance + .validate_guard(&msg.pod, &msg.guard) + .context("accepter's class guard does not validate")?; + + let mut session = self.deps.build_ctx(); + session + .builder + .add_pod(msg.pod) + .map_err(|err| anyhow!("cannot import accepter's pod: {err}"))?; + let mut tx = TxBuilder::new_from_commitments( + &mut session, + &[self.incoming.old_commitment, self.outgoing.commitment()], + self.witness.clone(), + ); + ensure!( + self.agreed.plan().chain_start() == tx.chain_start, + "plan and builder disagree on chain start" + ); + + let header = &self.witness.state_header; + + // Apply the incoming leg. + let scope = tx.begin_action(); + let (received, st_rekey, handle) = tx.rekey_apply( + &mut session, + &msg.offer.spend_facts(), + msg.offer.st_key_erasure.clone(), + &self.incoming.mid, + self.new_key.clone(), + ); + let guard = self + .deps + .prove_guard(&mut session, self.want_class, header, st_rekey)?; + tx.set_guard(handle, guard); + tx.end_action(scope); + ensure!( + received.commitment() == self.incoming_new.commitment(), + "received state diverges from the planned projection" + ); + + // Record the outgoing leg against the accepter's class guard. + let scope = tx.begin_action(); + let handle = tx.rekey_record(&mut session, &self.outgoing, &msg.acceptance.state_facts()); + tx.set_guard(handle, msg.guard); + tx.end_action(scope); + + let (st_finalized, tx_out, _stats) = tx.finalize(&mut session); + session + .builder + .reveal(&st_finalized) + .map_err(|err| anyhow!("cannot reveal TxFinalized: {err}"))?; + ensure!( + tx_out.dict().commitment() == self.agreed.plan().tx_final(), + "finalized transaction diverges from the agreed plan" + ); + let pod = self.deps.prove_session(session.builder)?; + + Ok(SwapOutcome { + pod, + tx: tx_out, + expectation: SwapExpectation { + received, + tx_final: self.agreed.plan().tx_final(), + new_commitments: self.agreed.new_commitments(), + nullifiers: self.agreed.nullifiers(), + }, + }) + } +} + +// ---------------------------------------------------------------- // +// Accepter // +// ---------------------------------------------------------------- // + +pub struct Accepter { + deps: SwapDeps, + give: Dictionary, + incoming_class: Hash, + new_key: Value, +} + +impl Accepter { + /// Start with the outgoing state and invitation's offered class. + pub fn new(deps: SwapDeps, give: Dictionary, incoming_class: Hash) -> Self { + Self { + deps, + give, + incoming_class, + new_key: Value::from(rand_raw_value()), + } + } + + /// Disclose the object this party gives. + pub fn accept(self) -> (AccepterDisclosed, AcceptMsg) { + let msg = AcceptMsg { + accepter_object: LegDisclosure::of(&self.give), + }; + ( + AccepterDisclosed { + deps: self.deps, + give: self.give, + incoming_class: self.incoming_class, + new_key: self.new_key, + }, + msg, + ) + } +} + +pub struct AccepterDisclosed { + deps: SwapDeps, + give: Dictionary, + incoming_class: Hash, + new_key: Value, +} + +impl AccepterDisclosed { + /// Complete the plan and return the independently derived deal. + pub fn on_plan_data(self, msg: &PlanDataMsg) -> Result<(AccepterPlanned, PlanAckMsg)> { + msg.initiator_object.validate(self.incoming_class)?; + let incoming_new = obj_with_key(&msg.initiator_object.mid, self.new_key.clone()); + let agreed = AgreedPlan::derive( + LegPlan { + old: self.give.commitment(), + new: msg.accepter_object_new, + nullifier: compute_nullifier(&self.give), + }, + LegPlan { + old: msg.initiator_object.old_commitment, + new: incoming_new.commitment(), + nullifier: msg.initiator_object.nullifier, + }, + &msg.header, + )?; + let reply = PlanAckMsg { + transaction: agreed.transaction.clone(), + }; + let next = AccepterPlanned { + deps: self.deps, + give: self.give, + incoming_class: self.incoming_class, + new_key: self.new_key, + incoming: msg.initiator_object.clone(), + incoming_new, + header: msg.header.clone(), + agreed, + }; + Ok((next, reply)) + } +} + +pub struct AccepterPlanned { + deps: SwapDeps, + give: Dictionary, + incoming_class: Hash, + new_key: Value, + incoming: LegDisclosure, + incoming_new: Dictionary, + header: StateHeader, + agreed: AgreedPlan, +} + +impl AccepterPlanned { + /// Projected received state, including its new private key. + pub fn projected_received(&self) -> &Dictionary { + &self.incoming_new + } + + /// Validate the initiator's offer, then prove this side's offer, + /// acceptance, and class guard in one session. + pub fn on_offer(self, msg: OfferMsg) -> Result<(AcceptanceMsg, SwapExpectation)> { + self.deps.check_pod_mode(&msg.pod, "the initiator's")?; + msg.pod + .pod + .verify() + .context("initiator's pod fails verification")?; + msg.offer + .validate(&msg.pod, self.agreed.context, self.incoming.old_commitment) + .context("initiator's offer does not validate")?; + + let mut session = self.deps.build_ctx(); + session + .builder + .add_pod(msg.pod) + .map_err(|err| anyhow!("cannot import initiator's pod: {err}"))?; + let my_offer = TransferOffer::prove(&mut session, self.agreed.context, &self.give); + let (prev_chain, chain) = self.agreed.plan().event_range(1); + let (acceptance, received) = TransferAcceptance::prove( + &mut session, + &msg.offer, + &self.incoming.mid, + self.new_key.clone(), + prev_chain, + chain, + ); + ensure!( + received.commitment() == self.incoming_new.commitment(), + "received state diverges from the planned projection" + ); + let guard = self.deps.prove_guard( + &mut session, + self.incoming_class, + &self.header, + acceptance.st_rekey.clone(), + )?; + session + .builder + .reveal(&guard) + .map_err(|err| anyhow!("cannot reveal the class guard: {err}"))?; + let pod = self.deps.prove_session(session.builder)?; + + let reply = AcceptanceMsg { + offer: my_offer, + acceptance, + guard, + pod, + }; + let expectation = SwapExpectation { + received, + tx_final: self.agreed.plan().tx_final(), + new_commitments: self.agreed.new_commitments(), + nullifiers: self.agreed.nullifiers(), + }; + Ok((reply, expectation)) + } +} diff --git a/interfaces/trade/src/lib.rs b/interfaces/trade/src/lib.rs new file mode 100644 index 00000000..2290948f --- /dev/null +++ b/interfaces/trade/src/lib.rs @@ -0,0 +1,14 @@ +//! Two-party swap demo over the joint-transaction machinery. +//! +//! One negotiation exchange completes the plan, then two exchanges carry +//! three proving sessions. The initiator assembles, finalizes, and posts. + +pub mod engine; +pub mod local; +pub mod net; +pub mod post; +pub mod protocol; +pub mod ui; + +#[cfg(test)] +mod tests; diff --git a/interfaces/trade/src/local.rs b/interfaces/trade/src/local.rs new file mode 100644 index 00000000..b3e33a41 --- /dev/null +++ b/interfaces/trade/src/local.rs @@ -0,0 +1,312 @@ +//! Local objects, plugins, service settings, and received-state import. +//! +//! Full dictionaries and plugin sources are read from `$DOBJ_HOME`. +//! Received objects are written through dobjd for validation and canonical +//! storage. + +use std::collections::HashMap; +use std::sync::Arc; + +use anyhow::{Context as _, Result, anyhow}; +use driver::{ObjectRecord, parse_object_record_file, paths}; +use pod2::{lang::Module, middleware::Hash}; +use wire_types::{DriverSettings, ObjectStatus, QualifiedName}; + +use crate::engine::ClassDirectory; + +pub struct ClassDisplay { + pub qualified: QualifiedName, + pub emoji: String, +} + +pub struct PluginCatalog { + /// Txlib batches and installed plugin modules for `BuildContext`. + pub modules: Vec>, + pub classes: ClassDirectory, + pub display: HashMap, + by_name: HashMap, +} + +impl PluginCatalog { + pub fn resolve_class(&self, name: &str) -> Result { + self.by_name.get(name).copied().ok_or_else(|| { + let mut known: Vec<_> = self.by_name.keys().cloned().collect(); + known.sort(); + anyhow!( + "no installed class named {name}; installed: {}", + known.join(", ") + ) + }) + } + + pub fn class_label(&self, hash: Hash) -> String { + match self.display.get(&hash) { + Some(display) => format!("{} {}", display.emoji, display.qualified), + None => format!("(unknown class {hash:#})"), + } + } +} + +pub struct Local { + pub paths: driver::DriverPaths, + pub settings: DriverSettings, + pub catalog: PluginCatalog, + pub dobjd_url: String, +} + +impl Local { + pub fn open() -> Result { + let paths = paths::default_paths()?; + let settings = read_settings(&paths)?; + let catalog = load_plugins(&paths)?; + let port = match std::env::var("DOBJD_PORT") { + Ok(raw) => raw + .parse::() + .map_err(|err| anyhow!("invalid DOBJD_PORT env: {err}"))?, + Err(_) => 7717, + }; + Ok(Self { + paths, + settings, + catalog, + dobjd_url: format!("http://127.0.0.1:{port}"), + }) + } + + /// Every live object in the store, full dictionaries included. + pub fn live_objects(&self) -> Result> { + let mut records = Vec::new(); + let entries = match std::fs::read_dir(&self.paths.objects_dir) { + Ok(entries) => entries, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(records), + Err(err) => { + return Err(anyhow!( + "cannot read objects dir {}: {err}", + self.paths.objects_dir.display() + )); + } + }; + for entry in entries { + let path = entry?.path(); + if !paths::is_dobj_file(&path) { + continue; + } + let record = parse_object_record_file(&path) + .with_context(|| format!("unreadable object file {}", path.display()))?; + if record.status == ObjectStatus::Live { + records.push(record); + } + } + Ok(records) + } + + /// Live objects of one class, by guard hash. + pub fn live_objects_of_class(&self, class_hash: Hash) -> Result> { + let class_value = pod2::middleware::Value::from(class_hash); + Ok(self + .live_objects()? + .into_iter() + .filter(|record| { + record + .obj + .get(&pod2::middleware::StrKey::from("type")) + .ok() + .flatten() + .is_some_and(|value| value == class_value) + }) + .collect()) + } + + /// Import a received object through dobjd. + pub fn import_received( + &self, + obj: &pod2::middleware::containers::Dictionary, + class_hash: Hash, + tx_final: Hash, + ) -> Result<()> { + let display = self + .catalog + .display + .get(&class_hash) + .ok_or_else(|| anyhow!("received object's class is not installed"))?; + let record = serde_json::json!({ + "contentHash": obj.commitment(), + "class": display.qualified, + "status": "live", + "obj": obj, + "txFinal": tx_final, + }); + let body = serde_json::json!({ "dobj": record.to_string() }); + let url = format!("{}/objects/import", self.dobjd_url); + let response = reqwest::blocking::Client::new() + .post(&url) + .json(&body) + .send() + .with_context(|| format!("dobjd unreachable at {url}"))?; + let status = response.status(); + if !status.is_success() { + let text = response.text().unwrap_or_default(); + anyhow::bail!("dobjd import failed with {status}: {text}"); + } + Ok(()) + } +} + +/// Build a synthetic grounding witness for a `--mock` dry run. +pub fn mock_grounding_witness(commitments: &[Hash]) -> Arc { + let mut state = payload::test_state::TestState::default(); + state.apply_tx(commitments.to_vec(), Vec::new()); + state.build_grounding_witness( + commitments, + |block_meta, created_root, nullifiers_root, prior_state_history_root, created_proofs| { + Arc::new(txlib::GroundingWitness::new( + txlib::StateHeader::new( + block_meta.number as i64, + block_meta.timestamp as i64, + block_meta.hash, + created_root, + nullifiers_root, + prior_state_history_root, + ), + created_proofs, + )) + }, + ) +} + +/// Create an ungrounded local object for `--mock` development. +pub fn dev_spawn(local: &Local, class_hash: Hash) -> Result { + let display = local + .catalog + .display + .get(&class_hash) + .ok_or_else(|| anyhow!("class is not installed"))?; + let initial = pod2utils::dict!({ + "type" => pod2::middleware::Value::from(class_hash), + "key" => pod2utils::rand_raw_value() + }); + let obj = txlib::with_stable_identifier(&initial); + let record = ObjectRecord { + content_hash: obj.commitment(), + class: display.qualified.clone(), + status: ObjectStatus::Live, + tx_hash: None, + obj, + tx_final: Hash::default(), + }; + std::fs::create_dir_all(&local.paths.objects_dir)?; + let file_name = format!( + "{}_{}.dobj", + record.class.file_prefix(), + format!("{:#}", record.content_hash).to_ascii_lowercase() + ); + let path = local.paths.objects_dir.join(file_name); + std::fs::write(&path, serde_json::to_string_pretty(&record)?)?; + Ok(record) +} + +impl Local { + /// Fetch and reconcile object statuses through dobjd. + pub fn object_statuses(&self) -> Result> { + let url = format!("{}/objects", self.dobjd_url); + let summaries: Vec = reqwest::blocking::Client::new() + .get(&url) + .send() + .with_context(|| format!("dobjd unreachable at {url}"))? + .error_for_status() + .context("dobjd objects fetch failed")? + .json() + .context("unreadable dobjd objects response")?; + let mut statuses = HashMap::new(); + for summary in summaries { + if let Ok(hash) = payload::decode_hash_hex(&summary.content_hash) { + statuses.insert(hash, summary.status); + } + } + Ok(statuses) + } +} + +fn read_settings(paths: &driver::DriverPaths) -> Result { + match std::fs::read_to_string(&paths.settings_path) { + Ok(raw) => serde_json::from_str(&raw).map_err(|err| { + anyhow!( + "unreadable settings file {}: {err}", + paths.settings_path.display() + ) + }), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(DriverSettings { + synchronizer_api_url: "http://127.0.0.1:3000".to_string(), + relayer_api_url: "http://127.0.0.1:3200".to_string(), + mcp_enabled: false, + }), + Err(err) => Err(anyhow!( + "cannot read settings file {}: {err}", + paths.settings_path.display() + )), + } +} + +fn load_plugins(paths: &driver::DriverPaths) -> Result { + let mut modules = vec![ + Arc::new(txlib::predicates::events_module()), + Arc::new(txlib::predicates::rekey_module()), + Arc::new(txlib::predicates::module()), + ]; + let mut classes = ClassDirectory::default(); + let mut display = HashMap::new(); + let mut by_name = HashMap::new(); + + let entries = match std::fs::read_dir(&paths.actions_dir) { + Ok(entries) => entries, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + anyhow::bail!( + "no plugins installed under {} (run `just ensure-plugins`)", + paths.actions_dir.display() + ); + } + Err(err) => { + return Err(anyhow!( + "cannot read actions dir {}: {err}", + paths.actions_dir.display() + )); + } + }; + let sdk = sdk::Sdk::default(); + for entry in entries { + let path = entry?.path(); + if path.extension().and_then(|ext| ext.to_str()) != Some(pexe::PEXE_EXTENSION) { + continue; + } + let bytes = pexe::read_pexe_file(&path) + .with_context(|| format!("unreadable plugin {}", path.display()))?; + let (manifest, script) = pexe::unpack(&bytes) + .with_context(|| format!("cannot unpack plugin {}", path.display()))?; + let module = sdk + .load_module_from_src_manifest(&script, &manifest) + .map_err(|err| anyhow!("plugin {} does not load: {err}", manifest.plugin.name))?; + for class in module.classes() { + let hash = module + .class_hash(&class.name) + .expect("loaded module resolves its own classes"); + let emoji = manifest + .classes + .iter() + .find(|c| c.name == class.name) + .map(|c| c.emoji.clone()) + .unwrap_or_default(); + let qualified = QualifiedName::new(&manifest.plugin.name, &class.name); + by_name.insert(class.name.clone(), hash); + by_name.insert(qualified.to_string(), hash); + display.insert(hash, ClassDisplay { qualified, emoji }); + } + classes.absorb(ClassDirectory::from_sdk_module(&module)); + modules.push(module.module().clone()); + } + Ok(PluginCatalog { + modules, + classes, + display, + by_name, + }) +} diff --git a/interfaces/trade/src/main.rs b/interfaces/trade/src/main.rs new file mode 100644 index 00000000..f4f9d23f --- /dev/null +++ b/interfaces/trade/src/main.rs @@ -0,0 +1,701 @@ +//! `dobj-trade`: two-party swaps over iroh and joint transactions. +//! The offerer shares an invitation; the accepter connects, and the offerer +//! assembles, finalizes, and posts the agreed swap. + +use std::sync::Arc; + +use anyhow::{Result, anyhow}; +use clap::{Parser, Subcommand}; +use driver::{ + HttpSynchronizerClient, ObjectRecord, SYNCHRONIZER_POLL_INTERVAL_MS, + SYNCHRONIZER_POLL_TIMEOUT_SECS, SynchronizerClient, +}; +use joint_tx::{JointTransaction, graph::NodeKind}; +use pod2::middleware::Hash; +use trade::engine::{Accepter, Initiator, SwapDeps, SwapExpectation}; +use trade::protocol::{ACCEPTER, INITIATOR, Invitation, WireMsg}; +use trade::{local::Local, net, post, ui}; + +#[derive(Parser)] +#[command(name = "dobj-trade", about = "swap digital objects with another user")] +struct Cli { + #[command(subcommand)] + cmd: Cmd, +} + +#[derive(Subcommand)] +enum Cmd { + /// Offer a trade: prints an invitation to hand to the counterparty, + /// then waits for them to connect. + Offer { + /// Class you are giving (e.g. Wood or craft-basics::Wood). + #[arg(long)] + give: String, + /// Class you want in return. + #[arg(long)] + want: String, + /// Commitment prefix selecting which of your objects to give. + #[arg(long)] + object: Option, + /// Prove with the mock prover and skip posting (local dry run). + #[arg(long)] + mock: bool, + }, + /// Accept a trade from a pasted invitation. + Accept { + /// The invitation blob; prompted for if omitted. + blob: Option, + /// Commitment prefix selecting which of your objects to give. + #[arg(long)] + object: Option, + /// Prove with the mock prover and skip watching (local dry run). + #[arg(long)] + mock: bool, + /// Answer yes to every prompt (for scripted runs). + #[arg(long)] + yes: bool, + }, + /// List your live objects. + Objects, + /// Fabricate a live object for mock runs (dev scaffolding). + #[command(hide = true)] + DevSpawn { + /// Class to fabricate (e.g. Wood). + class: String, + }, +} + +#[tokio::main] +async fn main() -> Result<()> { + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init(); + match Cli::parse().cmd { + Cmd::Offer { + give, + want, + object, + mock, + } => run_offer(give, want, object, mock).await, + Cmd::Accept { + blob, + object, + mock, + yes, + } => run_accept(blob, object, mock, yes).await, + Cmd::Objects => run_objects(), + Cmd::DevSpawn { class } => run_dev_spawn(class), + } +} + +fn blocking(f: impl FnOnce() -> T) -> T { + tokio::task::block_in_place(f) +} + +fn run_dev_spawn(class: String) -> Result<()> { + let local = Local::open()?; + let class_hash = local.catalog.resolve_class(&class)?; + let record = trade::local::dev_spawn(&local, class_hash)?; + println!( + "spawned {} {}", + local.catalog.class_label(class_hash), + ui::short(&record.content_hash) + ); + Ok(()) +} + +fn run_objects() -> Result<()> { + let local = Local::open()?; + let objects = local.live_objects()?; + if objects.is_empty() { + println!("no live objects"); + return Ok(()); + } + for record in objects { + let class_label = local.catalog.class_label(class_hash_of(&record)?); + println!( + " {} {} {}", + ui::short(&record.obj.commitment()), + class_label, + ui::describe_object(&record.obj) + ); + } + Ok(()) +} + +fn class_hash_of(record: &ObjectRecord) -> Result { + let value = record + .obj + .get(&pod2::middleware::StrKey::from("type")) + .ok() + .flatten() + .ok_or_else(|| anyhow!("object {} has no type field", record.content_hash))?; + Ok(Hash(value.raw().0)) +} + +/// Pick which of the user's live objects of `class_hash` to give. +fn pick_object(local: &Local, class_hash: Hash, prefix: Option<&str>) -> Result { + let mut candidates = local.live_objects_of_class(class_hash)?; + if let Some(prefix) = prefix { + let prefix = prefix.trim_start_matches("0x").to_ascii_lowercase(); + candidates.retain(|record| { + format!("{:#}", record.obj.commitment()) + .trim_start_matches("0x") + .to_ascii_lowercase() + .starts_with(&prefix) + }); + } + match candidates.len() { + 0 => anyhow::bail!( + "you hold no live object of class {}", + local.catalog.class_label(class_hash) + ), + 1 => Ok(candidates.remove(0)), + _ => { + println!(" you hold several; pick one with --object :"); + for record in &candidates { + ui::object_line("candidate", &record.obj); + } + anyhow::bail!("ambiguous object choice"); + } + } +} + +/// Render the derived schedule with viewer-relative party and object labels. +fn narrate_schedule(transaction: &JointTransaction, my_object: Hash, my_role: &str) { + let graph = transaction.graph(); + let labels: std::collections::HashMap<&str, String> = graph + .nodes() + .iter() + .map(|node| { + let whose = |object: &Hash| { + if *object == my_object { + "mine" + } else { + "theirs" + } + }; + let label = match node.kind() { + NodeKind::TransferOffer { object } => format!("offer:{}", whose(object)), + NodeKind::TransferAcceptance { object } => format!("accept:{}", whose(object)), + NodeKind::Finalize => "finalize".to_string(), + }; + (node.id(), label) + }) + .collect(); + let relabel = |ids: &[String]| { + ids.iter() + .map(|id| labels[id.as_str()].clone()) + .collect::>() + .join(", ") + }; + + ui::heading("the schedule (this is the whole protocol)"); + for (round, sessions) in graph.schedule().rounds.iter().enumerate() { + for session in sessions { + let side = if session.party == my_role { + "this side" + } else { + "their side" + }; + let mut line = format!( + "round {round}: {side} proves [{}]", + relabel(&session.statements) + ); + if !session.imports.is_empty() { + line.push_str(&format!(" importing [{}]", relabel(&session.imports))); + } + ui::note(&line); + } + } +} + +fn confirm(question: &str, auto_yes: bool) -> bool { + if auto_yes { + println!(" {question} [y/N] y (auto)"); + return true; + } + ui::confirm(question) +} + +async fn run_offer(give: String, want: String, object: Option, mock: bool) -> Result<()> { + let local = blocking(Local::open)?; + let give_class = local.catalog.resolve_class(&give)?; + let want_class = local.catalog.resolve_class(&want)?; + let record = pick_object(&local, give_class, object.as_deref())?; + let give_label = local.catalog.class_label(give_class); + let want_label = local.catalog.class_label(want_class); + + ui::say("you", &format!("offering {give_label} for {want_label}")); + ui::object_line("your object", &record.obj); + + ui::heading("opening the door"); + ui::say("you", "binding an iroh endpoint and coming online..."); + let (endpoint, addr) = net::listen().await?; + let invitation = Invitation { + node: addr, + offers: give_class, + wants: want_class, + mock, + }; + ui::key_moment("hand this invitation to your counterparty (any channel works)"); + ui::note("whoever holds this invitation can claim the trade; hand it only to your"); + ui::note("intended counterparty."); + println!("{}", invitation.encode()); + println!(); + ui::say("you", "waiting for them to paste it and connect..."); + + let (connection, mut channel) = net::accept_peer(&endpoint).await?; + ui::say("them", "connected over iroh"); + + let accept = match channel.recv().await? { + WireMsg::Accept(msg) => msg, + other => anyhow::bail!("expected their disclosure, got {other:?}"), + }; + ui::heading("data round"); + ui::say( + "them", + "disclosing the object they give (fields, commitment, nullifier; never the key):", + ); + ui::object_line("their object", &accept.accepter_object.mid); + ui::note(&format!( + "nullifier {} -- spendable only with their key", + ui::short(&accept.accepter_object.nullifier) + )); + ui::note("disclosing your side is irrevocable: they will recognize this object's"); + ui::note("later spend even if the deal dies here."); + + let commitments = [ + accept.accepter_object.old_commitment, + record.obj.commitment(), + ]; + let witness = if mock { + ui::say( + "you", + "mock run: grounding both inputs in a synthetic local state", + ); + trade::local::mock_grounding_witness(&commitments) + } else { + ui::say( + "you", + "fetching one grounding witness for both inputs from the synchronizer...", + ); + let sync_url = local.settings.synchronizer_api_url.clone(); + Arc::new(blocking(|| { + HttpSynchronizerClient::new().fetch_grounding_witness(&sync_url, &commitments) + })?) + }; + ui::note(&format!( + "grounded against state root {} (block {})", + ui::short(&witness.state_header.hash()), + witness.state_header.block_number + )); + + let deps = SwapDeps { + modules: local.catalog.modules.clone(), + classes: local.catalog.classes.clone(), + mock, + }; + let initiator = Initiator::new(deps, record.obj.clone(), want_class); + let (initiator, plan_data) = blocking(|| initiator.on_accept(&accept, witness.clone()))?; + ui::say( + "you -> them", + "your disclosure plus the grounding header and your projected new state", + ); + channel.send(&WireMsg::PlanData(plan_data)).await?; + + let plan_ack = match channel.recv().await? { + WireMsg::PlanAck(msg) => msg, + other => anyhow::bail!("expected their plan ack, got {other:?}"), + }; + let deal_tx_final = plan_ack.transaction.plan().tx_final(); + ui::say( + "them", + &format!( + "agreed; sent their full deal document (tx_final {})", + ui::short(&deal_tx_final) + ), + ); + ui::note("this side re-derives the deal and proves only if the documents match exactly."); + ui::note("endorsements bind this exact effect: it lands exactly as read, or not at all."); + + narrate_schedule(&plan_ack.transaction, record.obj.commitment(), INITIATOR); + + if !mock { + file_projection( + &local, + initiator.projected_received(), + want_class, + deal_tx_final, + &mut channel, + ) + .await?; + } + + ui::heading("round 0: your offer"); + ui::say( + "you", + "proving the offer of your object (openings, key erasure, spend endorsement)...", + ); + let (initiator, offer) = blocking(|| initiator.on_plan_ack(&plan_ack))?; + ui::say("you -> them", "offer pod"); + channel.send(&WireMsg::Offer(Box::new(offer))).await?; + + ui::say( + "you", + "waiting for their combined offer-plus-acceptance session...", + ); + let acceptance = match channel.recv().await? { + WireMsg::Acceptance(msg) => *msg, + other => anyhow::bail!("expected their acceptance, got {other:?}"), + }; + ui::say( + "them", + "their offer, their acceptance of your object, and its class guard, in one pod", + ); + + ui::heading("round 2: assembly and finalize"); + channel + .send(&WireMsg::Progress { + note: "validating your pod, assembling both legs, finalizing, proving".to_string(), + }) + .await?; + ui::say( + "you", + "validating their artifacts against the pod, then assembling...", + ); + if !mock { + ui::note("real proving now; expect roughly half a minute of silence."); + } + let outcome = blocking(|| initiator.on_acceptance(acceptance))?; + ui::say( + "you", + &format!( + "TxFinalized proven; tx_final {}", + ui::short(&outcome.expectation.tx_final) + ), + ); + + if mock { + channel + .send(&WireMsg::Posted { + tx_hash: None, + block_number: None, + }) + .await?; + ui::key_moment("mock run complete: nothing was posted"); + summarize(&outcome.expectation, &local); + } else { + channel + .send(&WireMsg::Progress { + note: "shrinking the proof into a blob payload and posting via the relayer" + .to_string(), + }) + .await?; + ui::say("you", "shrinking the proof and posting via the relayer..."); + let state_root = witness.state_header.hash(); + let relayer_url = local.settings.relayer_api_url.clone(); + let pod = outcome.pod.clone(); + let confirmation = blocking(move || -> Result<_> { + let bytes = post::build_payload_bytes(state_root, &outcome.tx, pod)?; + post::post_and_confirm(&relayer_url, &bytes) + })?; + ui::say( + "you", + &format!( + "posted: eth tx {}", + confirmation.tx_hash.as_deref().unwrap_or("(pending)") + ), + ); + channel + .send(&WireMsg::Posted { + tx_hash: confirmation.tx_hash.clone(), + block_number: confirmation.block_number, + }) + .await?; + watch_and_reconcile(&local, &outcome.expectation, record.obj.commitment()).await?; + } + + // Let the final message drain before closing the endpoint. + channel.finish(); + let _ = tokio::time::timeout(std::time::Duration::from_secs(30), connection.closed()).await; + endpoint.close().await; + Ok(()) +} + +async fn run_accept( + blob: Option, + object: Option, + mock: bool, + yes: bool, +) -> Result<()> { + let local = blocking(Local::open)?; + let blob = match blob { + Some(blob) => blob, + None => blocking(|| -> Result { + println!("paste the invitation blob:"); + let mut line = String::new(); + std::io::stdin().read_line(&mut line)?; + Ok(line) + })?, + }; + let invitation = Invitation::decode(&blob)?; + if mock && !invitation.mock { + anyhow::bail!( + "this invitation is for a real trade; drop --mock or ask for a mock invitation" + ); + } + let mock = invitation.mock; + let offers_label = local.catalog.class_label(invitation.offers); + let wants_label = local.catalog.class_label(invitation.wants); + ui::banner(); + println!(); + ui::field("their iroh id", &invitation.node.id.to_string()); + ui::field( + "they give", + &format!("{offers_label} (guard {})", ui::short(&invitation.offers)), + ); + ui::field( + "they want", + &format!("{wants_label} (guard {})", ui::short(&invitation.wants)), + ); + if mock { + ui::field("mode", "MOCK DRY RUN: mock proofs, nothing will be posted"); + } + + let record = pick_object(&local, invitation.wants, object.as_deref())?; + ui::object_line("you would give", &record.obj); + ui::note("accepting discloses this object's fields, commitment, and nullifier to them,"); + ui::note("irrevocably even if the deal dies. this is your decision point: everything"); + ui::note("after runs to completion."); + if !blocking(|| confirm("accept the trade?", yes)) { + anyhow::bail!("trade declined"); + } + + ui::say("you", "dialing their endpoint over iroh..."); + let (endpoint, connection, mut channel) = net::connect(invitation.node.clone()).await?; + ui::say("you", "connected"); + + let deps = SwapDeps { + modules: local.catalog.modules.clone(), + classes: local.catalog.classes.clone(), + mock, + }; + let accepter = Accepter::new(deps, record.obj.clone(), invitation.offers); + let (accepter, accept_msg) = accepter.accept(); + ui::heading("data round"); + ui::say( + "you -> them", + "your disclosure (fields, commitment, nullifier; never the key)", + ); + let my_commitment = record.obj.commitment(); + channel.send(&WireMsg::Accept(accept_msg)).await?; + + let plan_data = match channel.recv().await? { + WireMsg::PlanData(msg) => msg, + other => anyhow::bail!("expected their plan data, got {other:?}"), + }; + ui::say( + "them", + "their disclosure, the grounding header, and their projected new state:", + ); + ui::object_line("their object", &plan_data.initiator_object.mid); + ui::note(&format!( + "grounding block {}, state root {}", + plan_data.header.block_number, + ui::short(&plan_data.header.hash()) + )); + let (accepter, plan_ack) = blocking(|| accepter.on_plan_data(&plan_data))?; + let deal_tx_final = plan_ack.transaction.plan().tx_final(); + if !mock { + file_projection( + &local, + accepter.projected_received(), + invitation.offers, + deal_tx_final, + &mut channel, + ) + .await?; + } + ui::say( + "you -> them", + &format!( + "plan agreed; sending the full deal document (tx_final {})", + ui::short(&deal_tx_final) + ), + ); + ui::note( + "your endorsement will bind this exact effect: it lands exactly as read, or not at all.", + ); + narrate_schedule(&plan_ack.transaction, my_commitment, ACCEPTER); + channel.send(&WireMsg::PlanAck(plan_ack)).await?; + + ui::say("you", "waiting for their offer pod (round 0)..."); + let offer = match channel.recv().await? { + WireMsg::Offer(msg) => *msg, + other => anyhow::bail!("expected their offer, got {other:?}"), + }; + ui::say("them", "offer pod for the object they give"); + + ui::heading("round 1: your combined session"); + ui::say( + "you", + "validating their offer, then proving your offer, your acceptance, and the class guard...", + ); + if !mock { + ui::note("real proving now; this side takes a while too."); + } + let (acceptance, expectation) = blocking(|| accepter.on_offer(offer))?; + ui::say("you -> them", "combined offer-plus-acceptance pod"); + channel + .send(&WireMsg::Acceptance(Box::new(acceptance))) + .await?; + + ui::heading("round 2 is theirs: they assemble, finalize, and post"); + loop { + match channel.recv().await? { + WireMsg::Progress { note } => ui::say("them", ¬e), + WireMsg::Posted { + tx_hash, + block_number, + } => { + match (&tx_hash, mock) { + (Some(hash), _) => ui::say( + "them", + &format!( + "posted: eth tx {hash}{}", + block_number + .map(|block| format!(" (block {block})")) + .unwrap_or_default() + ), + ), + (None, true) => ui::say("them", "mock run complete: nothing was posted"), + (None, false) => ui::say("them", "posted (no tx hash reported)"), + } + break; + } + other => anyhow::bail!("unexpected message while watching: {other:?}"), + } + } + + if mock { + summarize(&expectation, &local); + } else { + watch_and_reconcile(&local, &expectation, record.obj.commitment()).await?; + } + + connection.close(0u32.into(), b"done"); + endpoint.close().await; + Ok(()) +} + +/// Persist the projected received state before exporting an endorsement. +/// Abort if its new key cannot be stored durably. +async fn file_projection( + local: &Local, + projection: &pod2::middleware::containers::Dictionary, + received_class: Hash, + tx_final: Hash, + channel: &mut trade::net::Channel, +) -> Result<()> { + ui::say( + "you", + "filing your projected new object (with its key) in dobjd before endorsing...", + ); + let result = blocking(|| local.import_received(projection, received_class, tx_final)); + if let Err(err) = result { + net::abort(channel, "could not durably file the projected object").await; + return Err(err.context( + "dobjd must be reachable to file the projected object before endorsing; aborted safely", + )); + } + ui::note("it shows as an unknown object until the transaction lands; if the deal"); + ui::note("dies instead, delete it from dobjd."); + Ok(()) +} + +/// Wait for the complete effect, then reconcile local object statuses. +async fn watch_and_reconcile( + local: &Local, + expectation: &SwapExpectation, + given: Hash, +) -> Result<()> { + ui::heading("watching the synchronizer"); + ui::say( + "you", + &format!( + "waiting until every new commitment and nullifier of tx_final {} is on-chain...", + ui::short(&expectation.tx_final) + ), + ); + let sync_url = local.settings.synchronizer_api_url.clone(); + let created = expectation.new_commitments.clone(); + let nullifiers = expectation.nullifiers.clone(); + let head = blocking(move || { + HttpSynchronizerClient::new().wait_for_tx( + &sync_url, + &created, + &nullifiers, + SYNCHRONIZER_POLL_TIMEOUT_SECS, + SYNCHRONIZER_POLL_INTERVAL_MS, + ) + })?; + ui::say( + "you", + &format!( + "landed; synchronizer head is now {}", + ui::short(&head.current_state_root) + ), + ); + + ui::say("you", "asking dobjd to reconcile object statuses..."); + match blocking(|| local.object_statuses()) { + Ok(statuses) => { + let received = expectation.received.commitment(); + match statuses.get(&received) { + Some(wire_types::ObjectStatus::Live) => { + ui::note("your new object is live in the object store.") + } + Some(status) => ui::note(&format!( + "your new object is filed but still {}; the next sync should settle it.", + status.as_str() + )), + None => ui::note("your new object is not in dobjd's store; check the daemon."), + } + match statuses.get(&given) { + Some(wire_types::ObjectStatus::Nullified) | None => { + ui::note("the object you gave is spent and moved to the nullified store.") + } + Some(status) => ui::note(&format!( + "the object you gave still shows as {}; the next sync should settle it.", + status.as_str() + )), + } + } + Err(err) => { + ui::note(&format!( + "dobjd is unreachable ({err:#}); your new object was filed before endorsing, so" + )); + ui::note("nothing is lost: statuses settle on the daemon's next sync."); + } + } + summarize(expectation, local); + Ok(()) +} + +fn summarize(expectation: &SwapExpectation, local: &Local) { + let class = class_label_of_dict(local, &expectation.received); + ui::key_moment("trade complete"); + ui::say("you", &format!("you now control {class}")); + ui::object_line("received", &expectation.received); + ui::note(&format!( + "under tx_final {}", + ui::short(&expectation.tx_final) + )); +} + +fn class_label_of_dict(local: &Local, obj: &pod2::middleware::containers::Dictionary) -> String { + obj.get(&pod2::middleware::StrKey::from("type")) + .ok() + .flatten() + .map(|value| local.catalog.class_label(Hash(value.raw().0))) + .unwrap_or_else(|| "(unknown class)".to_string()) +} diff --git a/interfaces/trade/src/net.rs b/interfaces/trade/src/net.rs new file mode 100644 index 00000000..b7660521 --- /dev/null +++ b/interfaces/trade/src/net.rs @@ -0,0 +1,119 @@ +//! JSON-line messages over one bidirectional iroh stream. + +use anyhow::{Context as _, Result, anyhow}; +use iroh::{ + Endpoint, EndpointAddr, + endpoint::{Connection, RecvStream, SendStream, presets}, +}; +use tokio::io::{AsyncBufReadExt, BufReader}; + +use crate::protocol::WireMsg; + +pub const ALPN: &[u8] = b"zk-craft/trade/0"; + +pub struct Channel { + send: SendStream, + recv: BufReader, +} + +impl Channel { + pub async fn send(&mut self, msg: &WireMsg) -> Result<()> { + let mut line = serde_json::to_vec(msg)?; + line.push(b'\n'); + self.send + .write_all(&line) + .await + .map_err(|err| anyhow!("send failed: {err}"))?; + Ok(()) + } + + pub async fn recv(&mut self) -> Result { + let mut line = String::new(); + let n = self + .recv + .read_line(&mut line) + .await + .map_err(|err| anyhow!("receive failed: {err}"))?; + if n == 0 { + anyhow::bail!("counterparty closed the connection"); + } + let msg: WireMsg = serde_json::from_str(&line) + .map_err(|err| anyhow!("unreadable message from counterparty: {err}"))?; + if let WireMsg::Abort { reason } = &msg { + anyhow::bail!("counterparty aborted: {reason}"); + } + Ok(msg) + } +} + +impl Channel { + /// Finish sending after queued messages have been written. + pub fn finish(&mut self) { + let _ = self.send.finish(); + } +} + +/// Bind the initiator's endpoint, come online, and return it with the +/// dialable address for the invitation. +pub async fn listen() -> Result<(Endpoint, EndpointAddr)> { + let endpoint = Endpoint::builder(presets::N0) + .alpns(vec![ALPN.to_vec()]) + .bind() + .await + .map_err(|err| anyhow!("cannot bind iroh endpoint: {err}"))?; + endpoint.online().await; + let addr = endpoint.addr(); + Ok((endpoint, addr)) +} + +/// Wait for the accepter to dial in and open the message stream. +pub async fn accept_peer(endpoint: &Endpoint) -> Result<(Connection, Channel)> { + let incoming = endpoint + .accept() + .await + .ok_or_else(|| anyhow!("endpoint closed while waiting for the counterparty"))?; + let connection = incoming + .await + .map_err(|err| anyhow!("incoming connection failed: {err}"))?; + let (send, recv) = connection + .accept_bi() + .await + .map_err(|err| anyhow!("counterparty opened no stream: {err}"))?; + let channel = Channel { + send, + recv: BufReader::new(recv), + }; + Ok((connection, channel)) +} + +/// Dial the initiator from the invitation and open the message stream. +pub async fn connect(addr: EndpointAddr) -> Result<(Endpoint, Connection, Channel)> { + let endpoint = Endpoint::bind(presets::N0) + .await + .map_err(|err| anyhow!("cannot bind iroh endpoint: {err}"))?; + let connection = endpoint + .connect(addr, ALPN) + .await + .map_err(|err| anyhow!("cannot reach the initiator: {err}"))?; + let (send, mut recv) = connection + .open_bi() + .await + .map_err(|err| anyhow!("cannot open message stream: {err}"))?; + // The protocol sends first, so the peer will observe the stream. + let _ = &mut recv; + let channel = Channel { + send, + recv: BufReader::new(recv), + }; + Ok((endpoint, connection, channel)) +} + +/// Send a best-effort abort notice. +pub async fn abort(channel: &mut Channel, reason: &str) { + let _ = channel + .send(&WireMsg::Abort { + reason: reason.to_string(), + }) + .await + .context("abort notice failed"); +} diff --git a/interfaces/trade/src/post.rs b/interfaces/trade/src/post.rs new file mode 100644 index 00000000..131c4775 --- /dev/null +++ b/interfaces/trade/src/post.rs @@ -0,0 +1,55 @@ +//! Build, submit, and confirm a relayer payload for a joint transaction. + +use anyhow::{Result, anyhow}; +use driver::{ + HttpRelayerClient, RELAYER_CONFIRM_TIMEOUT_SECS, RELAYER_POLL_INTERVAL_MS, + RELAYER_TX_HASH_TIMEOUT_SECS, RelayerClient, RelayerConfirmation, +}; +use payload::{ + payload::{Payload, PayloadProof}, + shrink::{ShrunkMainPodSetup, shrink_compress_pod}, +}; +use pod2::{ + frontend::MainPod, + middleware::{Hash, Params}, +}; +use txlib::Tx; + +pub fn build_payload_bytes(state_root: Hash, tx: &Tx, tx_pod: MainPod) -> Result> { + let params = Params::default(); + let setup = ShrunkMainPodSetup::new(¶ms) + .build() + .map_err(|err| anyhow!("cannot build the shrink circuit: {err}"))?; + let compressed = shrink_compress_pod(&setup, tx_pod) + .map_err(|err| anyhow!("cannot shrink the transaction proof: {err}"))?; + let payload = Payload { + proof: PayloadProof::Plonky2(Box::new(compressed)), + tx_final: tx.dict().commitment(), + state_root, + nullifiers: tx.nullifier_hashes()?, + live: tx.live_commitments()?, + }; + Ok(payload.to_bytes()) +} + +pub fn post_and_confirm( + relayer_api_url: &str, + payload_bytes: &[u8], +) -> Result { + let client = HttpRelayerClient::new(); + let submitted = client.submit_proof(relayer_api_url, payload_bytes, None)?; + let tx_hash = client.wait_for_tx_hash( + relayer_api_url, + &submitted.job_id, + RELAYER_TX_HASH_TIMEOUT_SECS, + RELAYER_POLL_INTERVAL_MS, + )?; + let mut confirmation = client.wait_for_confirmation( + relayer_api_url, + &submitted.job_id, + RELAYER_CONFIRM_TIMEOUT_SECS, + RELAYER_POLL_INTERVAL_MS, + )?; + confirmation.tx_hash.get_or_insert(tx_hash); + Ok(confirmation) +} diff --git a/interfaces/trade/src/protocol.rs b/interfaces/trade/src/protocol.rs new file mode 100644 index 00000000..0c2acbed --- /dev/null +++ b/interfaces/trade/src/protocol.rs @@ -0,0 +1,166 @@ +//! Wire messages for the swap protocol. +//! +//! Messages carry negotiated plan data or proven artifacts. Receive-side +//! checks validate disclosures, pods, and grounding data. Fields name the +//! accepter and initiator objects consistently: event 0 transfers the +//! accepter's object; event 1 transfers the initiator's. + +use joint_tx::{JointTransaction, TransferAcceptance, TransferOffer}; +use pod2::{ + frontend::MainPod, + middleware::{EMPTY_VALUE, Hash, Statement, StrKey, Value, containers::Dictionary}, +}; +use serde::{Deserialize, Serialize}; +use txlib::{StateHeader, compute_nullifier, erased_key_state}; + +/// Seat names embedded in the shared `JointTransaction`. +pub const INITIATOR: &str = "initiator"; +pub const ACCEPTER: &str = "accepter"; + +/// Erased-key state, original commitment, and nullifier for one swap leg. +/// +/// These values define the negotiated plan; subsequent proofs enforce it. +/// Disclosure lets the counterparty recognize a later spend of this state, +/// even if the trade is abandoned. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LegDisclosure { + pub mid: Dictionary, + pub old_commitment: Hash, + pub nullifier: Hash, +} + +impl LegDisclosure { + pub fn of(obj: &Dictionary) -> Self { + Self { + mid: erased_key_state(obj), + old_commitment: obj.commitment(), + nullifier: compute_nullifier(obj), + } + } + + /// Require an erased key and the negotiated class. + pub fn validate(&self, expected_class: Hash) -> anyhow::Result<()> { + let key = self + .mid + .get(&StrKey::from("key")) + .ok() + .flatten() + .ok_or_else(|| anyhow::anyhow!("disclosed state has no key entry"))?; + anyhow::ensure!( + key == Value::from(EMPTY_VALUE), + "disclosed state's key is not erased" + ); + let class = self + .mid + .get(&StrKey::from("type")) + .ok() + .flatten() + .ok_or_else(|| anyhow::anyhow!("disclosed state has no type entry"))?; + anyhow::ensure!( + class == Value::from(expected_class), + "disclosed object is of class {class}, negotiated class is {:#}", + expected_class + ); + Ok(()) + } +} + +/// Accepter -> initiator: accepts the invitation and discloses the +/// object it gives. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AcceptMsg { + pub accepter_object: LegDisclosure, +} + +/// Initiator -> accepter: initiator disclosure and remaining plan data. +/// `header` grounds the transaction; `accepter_object_new` commits to +/// the accepter's object under the initiator's private new key. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PlanDataMsg { + pub initiator_object: LegDisclosure, + pub header: StateHeader, + pub accepter_object_new: Hash, +} + +/// Accepter -> initiator: independently derived deal document. +/// The initiator requires exact equality before proving. Deserialization +/// validates the document. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PlanAckMsg { + pub transaction: JointTransaction, +} + +/// Initiator -> accepter: transfer offer and its pod. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OfferMsg { + pub offer: TransferOffer, + pub pod: MainPod, +} + +/// Accepter -> initiator: offer, acceptance, class guard, and shared pod. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AcceptanceMsg { + pub offer: TransferOffer, + pub acceptance: TransferAcceptance, + pub guard: Statement, + pub pod: MainPod, +} + +/// Base58 invitation exchanged out of band. +/// Contains the endpoint, class guard hashes, and proving mode, but no +/// grounding-bound transaction data. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Invitation { + pub node: iroh::EndpointAddr, + /// Guard hash of the class the initiator gives. + pub offers: Hash, + /// Guard hash of the class the initiator wants. + pub wants: Hash, + /// Proving mode selected by the initiator. Mock and real pods cannot mix. + pub mock: bool, +} + +impl Invitation { + pub fn encode(&self) -> String { + bs58::encode(serde_json::to_vec(self).expect("invitation serializes")).into_string() + } + + pub fn decode(blob: &str) -> anyhow::Result { + let bytes = bs58::decode(blob.trim()) + .into_vec() + .map_err(|err| anyhow::anyhow!("not a base58 invitation: {err}"))?; + serde_json::from_slice(&bytes) + .map_err(|err| anyhow::anyhow!("not a trade invitation: {err}")) + } +} + +/// One JSON-line message on the iroh stream. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "camelCase")] +pub enum WireMsg { + Accept(AcceptMsg), + PlanData(PlanDataMsg), + PlanAck(PlanAckMsg), + Offer(Box), + Acceptance(Box), + /// Progress text for the counterparty's screen. + Progress { + note: String, + }, + /// The transaction is with the relayer. + Posted { + tx_hash: Option, + block_number: Option, + }, + /// Either side aborts the trade. + Abort { + reason: String, + }, +} diff --git a/interfaces/trade/src/tests.rs b/interfaces/trade/src/tests.rs new file mode 100644 index 00000000..7252306f --- /dev/null +++ b/interfaces/trade/src/tests.rs @@ -0,0 +1,175 @@ +use std::sync::Arc; + +use payload::test_state::TestState; +use pod2::middleware::{Hash, StrKey, Value}; +use serde::{Serialize, de::DeserializeOwned}; +use txlib::{GroundingWitness, StateHeader, erased_key_state}; + +use crate::engine::{Accepter, ClassDirectory, Initiator, SwapDeps}; + +/// Cross a simulated wire boundary through JSON encoding. +fn wire(value: &T) -> T { + serde_json::from_str(&serde_json::to_string(value).unwrap()).unwrap() +} + +fn grounding_witness(state: &TestState, input_commitments: &[Hash]) -> Arc { + state.build_grounding_witness( + input_commitments, + |block_meta, created_root, nullifiers_root, prior_state_history_root, created_proofs| { + Arc::new(GroundingWitness::new( + StateHeader::new( + block_meta.number as i64, + block_meta.timestamp as i64, + block_meta.hash, + created_root, + nullifiers_root, + prior_state_history_root, + ), + created_proofs, + )) + }, + ) +} + +const CRAFT_SRC: &str = r#" + fn SpawnFoo(action) { + var foo = action.output("Foo"); + foo.set([["quality", 7]]); + } + + fn SpawnBar(action) { + var bar = action.output("Bar"); + bar.set([["weight", 3]]); + } +"#; + +struct Fixture { + state: TestState, + deps: SwapDeps, + foo: pod2::middleware::containers::Dictionary, + bar: pod2::middleware::containers::Dictionary, + foo_class: Hash, + bar_class: Hash, +} + +fn fixture() -> Fixture { + let sdk = sdk::Sdk::default(); + let module = sdk + .load_module_from_src_actions(CRAFT_SRC, &["SpawnFoo", "SpawnBar"]) + .unwrap(); + + let mut state = TestState::default(); + let executor = module.executor(true, grounding_witness(&state, &[])); + let res = executor.action("SpawnFoo", vec![]).unwrap(); + let [foo] = res.objs(); + state.apply_tx( + res.tx.live_commitments().unwrap(), + res.tx.nullifier_hashes().unwrap(), + ); + let executor = module.executor(true, grounding_witness(&state, &[])); + let res = executor.action("SpawnBar", vec![]).unwrap(); + let [bar] = res.objs(); + state.apply_tx( + res.tx.live_commitments().unwrap(), + res.tx.nullifier_hashes().unwrap(), + ); + + let deps = SwapDeps { + modules: vec![ + Arc::new(txlib::predicates::events_module()), + Arc::new(txlib::predicates::rekey_module()), + Arc::new(txlib::predicates::module()), + module.module().clone(), + ], + classes: ClassDirectory::from_sdk_module(&module), + mock: true, + }; + Fixture { + state, + deps, + foo: foo.obj.clone(), + bar: bar.obj.clone(), + foo_class: module.class_hash("Foo").unwrap(), + bar_class: module.class_hash("Bar").unwrap(), + } +} + +/// Swap two SDK-generated object classes over serialized messages. +#[test] +fn swap_engines_complete_a_trade() { + let fx = fixture(); + + let initiator = Initiator::new(fx.deps.clone(), fx.foo.clone(), fx.bar_class); + let accepter = Accepter::new(fx.deps.clone(), fx.bar.clone(), fx.foo_class); + + let (accepter, accept_msg) = accepter.accept(); + let accept_msg = wire(&accept_msg); + let witness = grounding_witness( + &fx.state, + &[ + accept_msg.accepter_object.old_commitment, + fx.foo.commitment(), + ], + ); + let (initiator, plan_data) = initiator.on_accept(&accept_msg, witness).unwrap(); + let (accepter, plan_ack) = accepter.on_plan_data(&wire(&plan_data)).unwrap(); + + let (initiator, offer_msg) = initiator.on_plan_ack(&wire(&plan_ack)).unwrap(); + let (acceptance_msg, accepter_expect) = accepter.on_offer(wire(&offer_msg)).unwrap(); + + let outcome = initiator.on_acceptance(wire(&acceptance_msg)).unwrap(); + + assert_eq!(outcome.expectation.tx_final, accepter_expect.tx_final); + assert_eq!(outcome.tx.dict().commitment(), outcome.expectation.tx_final); + for commitment in &outcome.expectation.new_commitments { + assert!(outcome.tx.live.contains(&Value::from(*commitment)).unwrap()); + } + for nullifier in &outcome.expectation.nullifiers { + assert!( + outcome + .tx + .nullifiers + .contains(&Value::from(*nullifier)) + .unwrap() + ); + } + + let initiator_received = &outcome.expectation.received; + assert_eq!( + initiator_received.get(&StrKey::from("type")).unwrap(), + Some(Value::from(fx.bar_class)) + ); + assert_eq!( + erased_key_state(initiator_received).commitment(), + erased_key_state(&fx.bar).commitment() + ); + let accepter_received = &accepter_expect.received; + assert_eq!( + accepter_received.get(&StrKey::from("type")).unwrap(), + Some(Value::from(fx.foo_class)) + ); + assert_eq!( + erased_key_state(accepter_received).commitment(), + erased_key_state(&fx.foo).commitment() + ); + assert_ne!( + initiator_received.get(&StrKey::from("key")).unwrap(), + fx.bar.get(&StrKey::from("key")).unwrap() + ); +} + +/// Reject a disclosure of the wrong class before proving. +#[test] +fn wrong_class_disclosure_is_rejected() { + let fx = fixture(); + + let initiator = Initiator::new(fx.deps.clone(), fx.foo.clone(), fx.bar_class); + let accepter = Accepter::new(fx.deps.clone(), fx.foo.clone(), fx.foo_class); + let (_accepter, accept_msg) = accepter.accept(); + let witness = grounding_witness(&fx.state, &[fx.foo.commitment()]); + let err = initiator + .on_accept(&wire(&accept_msg), witness) + .err() + .expect("wrong-class disclosure must be rejected"); + assert!(err.to_string().contains("class"), "unexpected error: {err}"); +} diff --git a/interfaces/trade/src/ui.rs b/interfaces/trade/src/ui.rs new file mode 100644 index 00000000..667bea1e --- /dev/null +++ b/interfaces/trade/src/ui.rs @@ -0,0 +1,86 @@ +//! Terminal narration for protocol messages and proof artifacts. + +use std::io::Write as _; + +use pod2::middleware::{Hash, StrKey, Value, containers::Dictionary}; + +pub fn banner() { + println!(); + println!(" +----------------------------------------------+"); + println!(" | ⚠️ TRADE OFFER ⚠️ |"); + println!(" +----------------------------------------------+"); +} + +/// A narration line: who is speaking, over which channel. +pub fn say(who: &str, text: &str) { + println!(" [{who}] {text}"); +} + +pub fn note(text: &str) { + println!(" {text}"); +} + +pub fn short(hash: &Hash) -> String { + let full = format!("{hash:#}"); + if full.len() > 14 { + format!("{}..{}", &full[..8], &full[full.len() - 4..]) + } else { + full + } +} + +/// Render an object's non-key fields compactly, key redacted. +pub fn describe_object(obj: &Dictionary) -> String { + let mut parts = Vec::new(); + let mut entries: Vec<(String, Value)> = obj.iter().filter_map(|kv| kv.ok()).collect(); + entries.sort_by(|a, b| a.0.cmp(&b.0)); + for (name, value) in entries { + match name.as_str() { + "key" => parts.push("key: (private)".to_string()), + "type" | "stable_identifier" => {} + _ => parts.push(format!("{name}: {value}")), + } + } + format!("{{ {} }}", parts.join(", ")) +} + +pub fn object_line(label: &str, obj: &Dictionary) { + println!( + " {label}: {} {}", + short(&obj.commitment()), + describe_object(obj) + ); +} + +/// Blocking yes/no prompt. +pub fn confirm(question: &str) -> bool { + print!(" {question} [y/N] "); + std::io::stdout().flush().ok(); + let mut answer = String::new(); + if std::io::stdin().read_line(&mut answer).is_err() { + return false; + } + matches!(answer.trim(), "y" | "Y" | "yes" | "YES") +} + +pub fn field(label: &str, value: &str) { + println!(" {label:<18} {value}"); +} + +pub fn heading(text: &str) { + println!(); + println!(" == {text} =="); +} + +pub fn key_moment(text: &str) { + println!(); + println!(" *** {text} ***"); + println!(); +} + +pub fn redact_key(obj: &Dictionary) -> String { + match obj.get(&StrKey::from("key")) { + Ok(Some(_)) => "held".to_string(), + _ => "missing".to_string(), + } +} diff --git a/libs/driver/src/clients/mod.rs b/libs/driver/src/clients/mod.rs index 5301e7fd..925471e4 100644 --- a/libs/driver/src/clients/mod.rs +++ b/libs/driver/src/clients/mod.rs @@ -3,16 +3,14 @@ use std::time::Duration; mod relayer_client; mod synchronizer_client; -#[cfg(test)] -pub(crate) use relayer_client::RelayerConfirmation; -pub(crate) use relayer_client::{HttpRelayerClient, RelayerClient}; pub use relayer_client::{ - RELAYER_CONFIRM_TIMEOUT_SECS, RELAYER_POLL_INTERVAL_MS, RELAYER_TX_HASH_TIMEOUT_SECS, + HttpRelayerClient, RELAYER_CONFIRM_TIMEOUT_SECS, RELAYER_POLL_INTERVAL_MS, + RELAYER_TX_HASH_TIMEOUT_SECS, RelayerClient, RelayerConfirmation, +}; +pub use synchronizer_client::{ + HttpSynchronizerClient, SYNCHRONIZER_POLL_INTERVAL_MS, SYNCHRONIZER_POLL_TIMEOUT_SECS, + SynchronizerClient, SynchronizerHead, SynchronizerMembership, }; -pub(crate) use synchronizer_client::{HttpSynchronizerClient, SynchronizerClient}; -pub use synchronizer_client::{SYNCHRONIZER_POLL_INTERVAL_MS, SYNCHRONIZER_POLL_TIMEOUT_SECS}; -#[cfg(test)] -pub(crate) use synchronizer_client::{SynchronizerHead, SynchronizerMembership}; /// Per-request ceiling for a single relayer/synchronizer HTTP call. The poll /// loops only re-check their overall deadline between iterations, so without a diff --git a/libs/driver/src/lib.rs b/libs/driver/src/lib.rs index 08381e9b..40efc2d3 100644 --- a/libs/driver/src/lib.rs +++ b/libs/driver/src/lib.rs @@ -16,8 +16,10 @@ mod tests; pub use crate::catalog::ActionCatalog as DriverActionCatalog; pub use crate::clients::{ - RELAYER_CONFIRM_TIMEOUT_SECS, RELAYER_POLL_INTERVAL_MS, RELAYER_TX_HASH_TIMEOUT_SECS, - SYNCHRONIZER_POLL_INTERVAL_MS, SYNCHRONIZER_POLL_TIMEOUT_SECS, + HttpRelayerClient, HttpSynchronizerClient, RELAYER_CONFIRM_TIMEOUT_SECS, + RELAYER_POLL_INTERVAL_MS, RELAYER_TX_HASH_TIMEOUT_SECS, RelayerClient, RelayerConfirmation, + SYNCHRONIZER_POLL_INTERVAL_MS, SYNCHRONIZER_POLL_TIMEOUT_SECS, SynchronizerClient, + SynchronizerHead, SynchronizerMembership, }; pub use crate::driver::{Driver, DriverDeps, PayloadBuilder}; pub use crate::error::DriverError;