From 6ae45f1430d6eb883181eb624ea802705c194143 Mon Sep 17 00:00:00 2001 From: shahnami Date: Wed, 4 Jun 2025 13:43:51 +0200 Subject: [PATCH 01/11] chore: Add midnight to supported networks --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1c2567b6d..adae8ea9a 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ This relayer service enables interaction with blockchain networks through transa - Solana - EVM (🚧 Partial support) - Stellar (🚧 Partial support) +- Midnight (🚧 Partial support) > For details about current development status and upcoming features, check our [Project Roadmap](https://docs.openzeppelin.com/relayer/roadmap). From 7a8165a4f1bc2864dbe10b8b6aa3358de9fab6e8 Mon Sep 17 00:00:00 2001 From: Nami Date: Tue, 8 Jul 2025 14:51:07 +0400 Subject: [PATCH 02/11] feat: Add models and configuration parsing (#258) * feat: Add network and address models * docs: Add reference in docstring for midnight address * feat: Add relayer, transaction and request draft models * fix: Tests * chore: Add Midnight network config * chore: Add Midnight transaction request and data field * test: Add address tests * chore: Create Midnight Transaction Builder (#267) * chore: Add types and scaffolding for transaction builder * chore: Use generic types for midnight_transaction * chore: Update builder and prover logic * chore: Update builder and prover logic * chore: Add typo * feat: Implement relayer model for Midnight (#321) * feat: Implement relayer model for Midnight * feat: Add wallet sync service (#335) * chore: Add indexer url to network config * feat: Add service sync manager * chore: Implement tx preparation scaffolding * chore: Implement get_balance from relayer service * chore: Replace repo w/ signertrait and clarify sync * feat: Implement relayer submission functionality (#337) * feat: Working implementation for transaction submission * feat: Use sync state repo to allow incremental sync * fix: Comments for token_type * feat: Implement status checker * chore: Upgrade package * feat: Implement signing for midnight * refactor: Replace dyn with EventHandlerType enum --- Cargo.lock | 3838 +++++++++++++++-- Cargo.toml | 128 +- config/networks/midnight.json | 21 + src/api/routes/relayer.rs | 5 +- src/bootstrap/config_processor.rs | 12 +- src/bootstrap/initialize_app_state.rs | 6 +- src/config/config_file/mod.rs | 5 +- src/config/config_file/network/collection.rs | 51 +- .../config_file/network/file_loading.rs | 4 +- src/config/config_file/network/inheritance.rs | 11 +- src/config/config_file/network/midnight.rs | 90 + src/config/config_file/network/mod.rs | 92 +- src/config/config_file/network/test_utils.rs | 47 + src/config/config_file/relayer.rs | 14 + src/constants/relayer.rs | 2 + src/constants/token.rs | 1 + .../relayer/midnight/midnight_relayer.rs | 939 ++++ src/domain/relayer/midnight/mod.rs | 2 + src/domain/relayer/mod.rs | 133 +- src/domain/relayer/solana/mod.rs | 1 + src/domain/relayer/util.rs | 2 + src/domain/transaction/midnight/builder.rs | 141 + .../midnight/midnight_transaction.rs | 802 ++++ src/domain/transaction/midnight/mod.rs | 12 + src/domain/transaction/midnight/types.rs | 245 ++ src/domain/transaction/mod.rs | 104 +- src/domain/transaction/util.rs | 2 + .../transaction_submission_handler.rs | 12 +- src/models/address/midnight/address.rs | 236 + src/models/address/midnight/mod.rs | 2 + src/models/{address.rs => address/mod.rs} | 23 + src/models/app_state.rs | 16 +- src/models/network/midnight/mod.rs | 3 + src/models/network/midnight/network.rs | 96 + src/models/network/mod.rs | 2 + src/models/network/repository.rs | 27 +- src/models/relayer/repository.rs | 26 +- src/models/relayer/response.rs | 20 + src/models/relayer/rpc_config.rs | 5 +- src/models/rpc/midnight/mod.rs | 14 + src/models/rpc/mod.rs | 5 + src/models/transaction/repository.rs | 61 +- src/models/transaction/request/midnight.rs | 63 + src/models/transaction/request/mod.rs | 6 + src/models/transaction/response.rs | 30 + src/repositories/mod.rs | 3 + src/repositories/network.rs | 15 +- src/repositories/relayer.rs | 14 +- src/repositories/sync_state.rs | 329 ++ src/services/mod.rs | 3 + src/services/provider/evm/mod.rs | 10 +- src/services/provider/midnight/mod.rs | 790 ++++ .../provider/midnight/remote_prover.rs | 107 + src/services/provider/mod.rs | 85 +- src/services/provider/retry.rs | 128 +- src/services/provider/solana/mod.rs | 32 +- src/services/signer/midnight/local_signer.rs | 228 + src/services/signer/midnight/mod.rs | 87 + src/services/signer/mod.rs | 32 +- src/services/sync/midnight/handler/events.rs | 263 ++ src/services/sync/midnight/handler/manager.rs | 344 ++ src/services/sync/midnight/handler/mod.rs | 25 + .../sync/midnight/handler/strategy.rs | 216 + src/services/sync/midnight/handler/tracker.rs | 72 + src/services/sync/midnight/indexer/client.rs | 583 +++ src/services/sync/midnight/indexer/mod.rs | 13 + src/services/sync/midnight/indexer/types.rs | 173 + src/services/sync/midnight/mod.rs | 35 + src/services/sync/midnight/utils/mod.rs | 95 + src/services/sync/mod.rs | 1 + src/utils/mod.rs | 3 + src/utils/token.rs | 112 + typos.toml | 1 + 73 files changed, 10661 insertions(+), 495 deletions(-) create mode 100644 config/networks/midnight.json create mode 100644 src/config/config_file/network/midnight.rs create mode 100644 src/domain/relayer/midnight/midnight_relayer.rs create mode 100644 src/domain/relayer/midnight/mod.rs create mode 100644 src/domain/transaction/midnight/builder.rs create mode 100644 src/domain/transaction/midnight/midnight_transaction.rs create mode 100644 src/domain/transaction/midnight/mod.rs create mode 100644 src/domain/transaction/midnight/types.rs create mode 100644 src/models/address/midnight/address.rs create mode 100644 src/models/address/midnight/mod.rs rename src/models/{address.rs => address/mod.rs} (60%) create mode 100644 src/models/network/midnight/mod.rs create mode 100644 src/models/network/midnight/network.rs create mode 100644 src/models/rpc/midnight/mod.rs create mode 100644 src/models/transaction/request/midnight.rs create mode 100644 src/repositories/sync_state.rs create mode 100644 src/services/provider/midnight/mod.rs create mode 100644 src/services/provider/midnight/remote_prover.rs create mode 100644 src/services/signer/midnight/local_signer.rs create mode 100644 src/services/signer/midnight/mod.rs create mode 100644 src/services/sync/midnight/handler/events.rs create mode 100644 src/services/sync/midnight/handler/manager.rs create mode 100644 src/services/sync/midnight/handler/mod.rs create mode 100644 src/services/sync/midnight/handler/strategy.rs create mode 100644 src/services/sync/midnight/handler/tracker.rs create mode 100644 src/services/sync/midnight/indexer/client.rs create mode 100644 src/services/sync/midnight/indexer/mod.rs create mode 100644 src/services/sync/midnight/indexer/types.rs create mode 100644 src/services/sync/midnight/mod.rs create mode 100644 src/services/sync/midnight/utils/mod.rs create mode 100644 src/services/sync/mod.rs create mode 100644 src/utils/token.rs diff --git a/Cargo.lock b/Cargo.lock index a73f59450..0717ca2d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + [[package]] name = "actix-codec" version = "0.5.2" @@ -76,7 +86,7 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -194,17 +204,26 @@ checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" dependencies = [ "actix-router", "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] +[[package]] +name = "addr2line" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +dependencies = [ + "gimli 0.27.3", +] + [[package]] name = "addr2line" version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ - "gimli", + "gimli 0.31.1", ] [[package]] @@ -255,7 +274,7 @@ version = "2.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "973a83d0d66d1f04647d1146a07736864f0742300b56bf2a5aadf5ce7b22fe47" dependencies = [ - "ahash", + "ahash 0.8.12", "solana-epoch-schedule", "solana-feature-set-interface", "solana-hash", @@ -263,6 +282,17 @@ dependencies = [ "solana-sha256-hasher", ] +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.12" @@ -565,9 +595,9 @@ dependencies = [ "proptest", "rand 0.8.5", "ruint", - "rustc-hash", + "rustc-hash 2.1.1", "serde", - "sha3", + "sha3 0.10.8", "tiny-keccak", ] @@ -637,7 +667,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f70d83b765fdc080dbcd4f4db70d8d23fe4761f2f02ebfa9146b833900634b4" dependencies = [ "alloy-rlp-derive", - "arrayvec", + "arrayvec 0.7.6", "bytes", ] @@ -648,7 +678,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64b728d511962dda67c1bc7ea7c03736ec275ed2cf4c35d9585298ac9ccf3b73" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -789,7 +819,7 @@ dependencies = [ "alloy-sol-macro-input", "proc-macro-error2", "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -806,7 +836,7 @@ dependencies = [ "indexmap 2.9.0", "proc-macro-error2", "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", "syn-solidity", "tiny-keccak", @@ -824,7 +854,7 @@ dependencies = [ "heck", "macro-string", "proc-macro2", - "quote", + "quote 1.0.40", "serde_json", "syn 2.0.101", "syn-solidity", @@ -933,7 +963,7 @@ checksum = "d95a94854e420f07e962f7807485856cde359ab99ab6413883e15235ad996e8b" dependencies = [ "alloy-primitives", "alloy-rlp", - "arrayvec", + "arrayvec 0.7.6", "derive_more 1.0.0", "nybbles", "serde", @@ -956,6 +986,15 @@ dependencies = [ "libc", ] +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "anstream" version = "0.6.18" @@ -1080,6 +1119,38 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "archery" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cd774058b1b415c4855d8b86436c04bf050c003156fe24bc326fb3fe75c343" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "ark-bls12-377" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb00293ba84f51ce3bd026bd0de55899c4e68f0a39a5728cebae3a73ffdc0a4f" +dependencies = [ + "ark-ec", + "ark-ff 0.4.2", + "ark-std 0.4.0", +] + +[[package]] +name = "ark-bls12-381" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c775f0d12169cba7aae4caeb547bb6a50781c7449a8aa53793827c9ec4abf488" +dependencies = [ + "ark-ec", + "ark-ff 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", +] + [[package]] name = "ark-bn254" version = "0.4.0" @@ -1152,7 +1223,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db02d390bf6643fb404d3d22d31aee1c4bc4459600aef9113833d17e786c6e44" dependencies = [ - "quote", + "quote 1.0.40", "syn 1.0.109", ] @@ -1162,7 +1233,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" dependencies = [ - "quote", + "quote 1.0.40", "syn 1.0.109", ] @@ -1174,7 +1245,7 @@ checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" dependencies = [ "num-bigint 0.4.6", "num-traits", - "quote", + "quote 1.0.40", "syn 1.0.109", ] @@ -1187,7 +1258,7 @@ dependencies = [ "num-bigint 0.4.6", "num-traits", "proc-macro2", - "quote", + "quote 1.0.40", "syn 1.0.109", ] @@ -1233,7 +1304,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae3281bc6d0fd7e549af32b52511e1302185bd688fd3359fa36423346ff682ea" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 1.0.109", ] @@ -1257,12 +1328,27 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "array-bytes" +version = "6.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5dde061bd34119e902bbb2d9b90c5692635cf59fb91d582c2b68043f1b8293" + [[package]] name = "arrayref" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" +[[package]] +name = "arrayvec" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" +dependencies = [ + "nodrop", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -1295,7 +1381,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 1.0.109", "synstructure 0.12.6", ] @@ -1307,7 +1393,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 1.0.109", ] @@ -1332,6 +1418,18 @@ dependencies = [ "futures-core", ] +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-compression" version = "0.4.23" @@ -1346,6 +1444,50 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-executor" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1237c0ae75a0f3765f58910ff9cdd0a12eeb39ab2f4c7de23262f337f0aacbb3" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.0.7", + "slab", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "async-lock" version = "3.4.0" @@ -1357,6 +1499,54 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde3f4e40e6021d7acffc90095cbd6dc54cb593903d1de5832f435eb274b85dc" +dependencies = [ + "async-channel 2.3.1", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener 5.4.0", + "futures-lite", + "rustix 1.0.7", + "tracing", +] + +[[package]] +name = "async-signal" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7605a4e50d4b06df3898d5a70bf5fde51ed9059b0434b73105193bc27acce0d" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.0.7", + "signal-hook-registry", + "slab", + "windows-sys 0.59.0", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -1375,10 +1565,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.88" @@ -1386,7 +1582,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -1401,6 +1597,12 @@ dependencies = [ "rustc_version 0.4.1", ] +[[package]] +name = "atomic-take" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8ab6b55fe97976e46f91ddbed8d147d966475dc29b2032757ba47e02376fbc3" + [[package]] name = "atomic-waker" version = "1.1.2" @@ -1425,7 +1627,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -1435,6 +1637,20 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "backoff" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" +dependencies = [ + "futures-core", + "getrandom 0.2.16", + "instant", + "pin-project-lite", + "rand 0.8.5", + "tokio", +] + [[package]] name = "backon" version = "1.5.1" @@ -1450,11 +1666,11 @@ version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ - "addr2line", + "addr2line 0.24.2", "cfg-if", "libc", "miniz_oxide", - "object", + "object 0.36.7", "rustc-demangle", "windows-targets 0.52.6", ] @@ -1465,6 +1681,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base58" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6107fe1be6682a68940da878d9e9f5e90ca5745b3dec9fd1bb393c8777d4f581" + [[package]] name = "base64" version = "0.12.3" @@ -1495,6 +1717,12 @@ version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" +[[package]] +name = "bech32" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" + [[package]] name = "beef" version = "0.5.2" @@ -1519,6 +1747,15 @@ dependencies = [ "serde", ] +[[package]] +name = "bip39" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d193de1f7487df1914d3a568b772458861d33f9c54249612cc2893d6915054" +dependencies = [ + "bitcoin_hashes", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -1534,6 +1771,22 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bitcoin-internals" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" + +[[package]] +name = "bitcoin_hashes" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" +dependencies = [ + "bitcoin-internals", + "hex-conservative", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -1561,6 +1814,36 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "blake2-rfc" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d530bdd2d52966a6d03b7a964add7ae1a288d25214066fd4b600f0f796400" +dependencies = [ + "arrayvec 0.4.12", + "constant_time_eq 0.1.5", +] + +[[package]] +name = "blake2b_simd" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e903a20b159e944f91ec8499fe1e55651480c541ea0a584f5d967c49ad9d99" +dependencies = [ + "arrayref", + "arrayvec 0.7.6", + "constant_time_eq 0.3.1", +] + [[package]] name = "blake3" version = "1.8.2" @@ -1568,10 +1851,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" dependencies = [ "arrayref", - "arrayvec", + "arrayvec 0.7.6", "cc", "cfg-if", - "constant_time_eq", + "constant_time_eq 0.3.1", "digest 0.10.7", ] @@ -1581,6 +1864,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ + "block-padding", "generic-array", ] @@ -1593,6 +1877,25 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel 2.3.1", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "blst" version = "0.3.14" @@ -1605,6 +1908,27 @@ dependencies = [ "zeroize", ] +[[package]] +name = "blstrs" +version = "0.7.1" +source = "git+https://github.com/davidnevadoc/blstrs?rev=3dfe5bf#3dfe5bf15d7b1fa4f898b1a286a9d43d5669b48e" +dependencies = [ + "bitvec", + "blst", + "byte-slice-cast", + "ff", + "getrandom 0.2.16", + "group", + "halo2curves", + "num-bigint 0.4.6", + "pairing", + "pasta_curves", + "rand 0.8.5", + "rand_core 0.6.4", + "serde", + "subtle", +] + [[package]] name = "bon" version = "3.6.3" @@ -1625,7 +1949,7 @@ dependencies = [ "ident_case", "prettyplease", "proc-macro2", - "quote", + "quote 1.0.40", "rustversion", "syn 2.0.101", ] @@ -1672,7 +1996,7 @@ dependencies = [ "once_cell", "proc-macro-crate 3.3.0", "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -1683,7 +2007,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65d6ba50644c98714aa2a70d13d7df3cd75cd2b523a2b452bf010443800976b3" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 1.0.109", ] @@ -1694,10 +2018,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "276691d96f063427be83e6692b86148e488ebba9f48f77788724ca027ba3b6d4" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 1.0.109", ] +[[package]] +name = "bounded-collections" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ad8a0bed7827f0b07a5d23cec2e58cc02038a99e4ca81616cb2bb2025f804d" +dependencies = [ + "log", + "parity-scale-codec", + "scale-info", + "serde", +] + [[package]] name = "brotli" version = "8.0.1" @@ -1735,7 +2071,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", - "regex-automata", + "regex-automata 0.4.9", "serde", ] @@ -1777,7 +2113,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ecc273b49b3205b83d648f0690daa588925572cc5063745bfe547fe7ec8e1a1" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -1858,7 +2194,7 @@ dependencies = [ "camino", "cargo-config2", "duct", - "fs-err", + "fs-err 3.1.0", "glob", "is_executable", "lcov2cobertura", @@ -1866,7 +2202,7 @@ dependencies = [ "opener", "regex", "rustc-demangle", - "ruzstd", + "ruzstd 0.8.1", "serde", "serde_derive", "serde_json", @@ -1912,10 +2248,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45565fc9416b9896014f5732ac776f810ee53a66730c17e4020c3ec064a8f88f" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "chrono" version = "0.4.41" @@ -1971,7 +2318,7 @@ checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" dependencies = [ "heck", "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -2037,6 +2384,12 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "common-path" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2382f75942f4b3be3690fe4f86365e9c853c1587d6ee58212cebf6e2a9ccd101" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -2114,25 +2467,72 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" dependencies = [ "proc-macro2", - "quote", - "unicode-xid", + "quote 1.0.40", + "unicode-xid 0.2.6", ] [[package]] -name = "constant_time_eq" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" - -[[package]] -name = "cookie" -version = "0.16.2" +name = "const_num_bigint" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +checksum = "5ddae0ac5d546ee2446704bc529e011d584921305af4f0ae27590accc8cf9251" dependencies = [ - "percent-encoding", - "time", - "version_check", + "const_num_bigint_derive", + "const_std_vec", + "num-bigint 0.4.6", +] + +[[package]] +name = "const_num_bigint_derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1b18965eb9d280ddde32faf948cfe5c6e5bf6885cbf98fadacc28278b8f331f" +dependencies = [ + "num-bigint 0.4.6", + "proc-macro2", + "quote 1.0.40", + "syn 1.0.109", +] + +[[package]] +name = "const_panic" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2459fc9262a1aa204eb4b5764ad4f189caec88aea9634389c0a25f8be7f6265e" + +[[package]] +name = "const_std_vec" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee6a4701b947a8608d1991b1db0cf5bab74164bf2627e5cecc9aa69dd33f172" + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time", + "version_check", ] [[package]] @@ -2161,6 +2561,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpp_demangle" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeaa953eaad386a53111e47172c2fedba671e5684c8dd601a5f474f4f118710f" +dependencies = [ + "cfg-if", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -2170,6 +2579,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cranelift-entity" +version = "0.95.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40099d38061b37e505e63f89bab52199037a72b931ad4868d9089ff7268660b0" +dependencies = [ + "serde", +] + [[package]] name = "crate-git-revision" version = "0.0.6" @@ -2229,6 +2647,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -2241,6 +2668,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +[[package]] +name = "crypto" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf1e6e5492f8f0830c37f301f6349e0dac8b2466e4fe89eef90e9eef906cd046" +dependencies = [ + "crypto-common", + "digest 0.10.7", +] + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -2321,7 +2758,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -2354,7 +2791,7 @@ dependencies = [ "fnv", "ident_case", "proc-macro2", - "quote", + "quote 1.0.40", "strsim 0.10.0", "syn 1.0.109", ] @@ -2368,7 +2805,7 @@ dependencies = [ "fnv", "ident_case", "proc-macro2", - "quote", + "quote 1.0.40", "strsim 0.11.1", "syn 2.0.101", ] @@ -2380,7 +2817,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" dependencies = [ "darling_core 0.14.4", - "quote", + "quote 1.0.40", "syn 1.0.109", ] @@ -2391,7 +2828,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -2494,10 +2931,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 1.0.109", ] +[[package]] +name = "derive-syn-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" +dependencies = [ + "proc-macro2", + "quote 1.0.40", + "syn 2.0.101", +] + +[[package]] +name = "derive-where" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e73f2692d4bd3cac41dca28934a39894200c9fabf49586d77d0e5954af1d7902" +dependencies = [ + "proc-macro2", + "quote 1.0.40", + "syn 2.0.101", +] + [[package]] name = "derive_builder" version = "0.12.0" @@ -2515,7 +2974,7 @@ checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" dependencies = [ "darling 0.14.4", "proc-macro2", - "quote", + "quote 1.0.40", "syn 1.0.109", ] @@ -2529,6 +2988,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote 1.0.40", + "rustc_version 0.4.1", + "syn 2.0.101", +] + [[package]] name = "derive_more" version = "1.0.0" @@ -2554,9 +3026,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", - "unicode-xid", + "unicode-xid 0.2.6", ] [[package]] @@ -2566,9 +3038,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", - "unicode-xid", + "unicode-xid 0.2.6", ] [[package]] @@ -2599,7 +3071,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -2622,8 +3094,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cbae11b3de8fce2a456e8ea3dada226b35fe791f0dc1d360c0941f0bb681f3" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", + "syn 2.0.101", +] + +[[package]] +name = "docify" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a772b62b1837c8f060432ddcc10b17aae1453ef17617a99bc07789252d2a5896" +dependencies = [ + "docify_macros", +] + +[[package]] +name = "docify_macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60e6be249b0a462a14784a99b19bf35a667bb5e09de611738bb7362fa4c95ff7" +dependencies = [ + "common-path", + "derive-syn-parse", + "once_cell", + "proc-macro2", + "quote 1.0.40", + "regex", "syn 2.0.101", + "termcolor", + "toml 0.8.22", + "walkdir", ] [[package]] @@ -2650,6 +3149,12 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "duct" version = "0.13.7" @@ -2668,6 +3173,33 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "dyn-clonable" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a36efbb9bfd58e1723780aa04b61aba95ace6a05d9ffabfdb0b43672552f0805" +dependencies = [ + "dyn-clonable-impl", + "dyn-clone", +] + +[[package]] +name = "dyn-clonable-impl" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8671d54058979a37a26f3511fbf8d198ba1aa35ffb202c42587d918d77213a" +dependencies = [ + "proc-macro2", + "quote 1.0.40", + "syn 2.0.101", +] + +[[package]] +name = "dyn-clone" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" + [[package]] name = "ecdsa" version = "0.16.9" @@ -2678,6 +3210,7 @@ dependencies = [ "digest 0.10.7", "elliptic-curve", "rfc6979", + "serdect", "signature 2.2.0", "spki", ] @@ -2741,6 +3274,35 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "ed25519-zebra" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c24f403d068ad0b359e577a77f92392118be3f3c927538f2bb544a5ecd828c6" +dependencies = [ + "curve25519-dalek 3.2.0", + "hashbrown 0.12.3", + "hex", + "rand_core 0.6.4", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "ed25519-zebra" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d9ce6874da5d4415896cd45ffbc4d1cfc0c4f9c079427bd870742c30f2f65a9" +dependencies = [ + "curve25519-dalek 4.1.3", + "ed25519 2.2.3", + "hashbrown 0.14.5", + "hex", + "rand_core 0.6.4", + "sha2 0.10.9", + "zeroize", +] + [[package]] name = "either" version = "1.15.0" @@ -2763,6 +3325,7 @@ dependencies = [ "pkcs8", "rand_core 0.6.4", "sec1", + "serdect", "subtle", "zeroize", ] @@ -2782,6 +3345,22 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum_index" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5532bdea562e7be83060c36185eecccba82fe16729d2eaad2891d65417656dd" + +[[package]] +name = "enum_index_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ab22c8085548bf06190113dca556e149ecdbb05ae5b972a2b9899f26b944ee4" +dependencies = [ + "quote 0.3.15", + "syn 0.11.11", +] + [[package]] name = "env_logger" version = "0.9.3" @@ -2795,6 +3374,12 @@ dependencies = [ "termcolor", ] +[[package]] +name = "environmental" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48c92028aaa870e83d51c64e5d4e0b6981b360c522198c23959f219a4e1b15b" + [[package]] name = "equivalent" version = "1.0.2" @@ -2828,13 +3413,13 @@ dependencies = [ "digest 0.10.7", "hex", "hmac 0.12.1", - "pbkdf2", + "pbkdf2 0.11.0", "rand 0.8.5", "scrypt", "serde", "serde_json", "sha2 0.10.9", - "sha3", + "sha3 0.10.8", "thiserror 1.0.69", "uuid 0.8.2", ] @@ -2845,6 +3430,16 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "event-listener" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" +dependencies = [ + "concurrent-queue", + "pin-project-lite", +] + [[package]] name = "event-listener" version = "5.4.0" @@ -2866,6 +3461,21 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "expander" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2c470c71d91ecbd179935b24170459e926382eaaa86b590b78814e180d8a8e2" +dependencies = [ + "blake2", + "file-guard", + "fs-err 2.11.0", + "prettyplease", + "proc-macro2", + "quote 1.0.40", + "syn 2.0.101", +] + [[package]] name = "eyre" version = "0.6.12" @@ -2876,6 +3486,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + [[package]] name = "fastbloom" version = "0.9.0" @@ -2900,7 +3516,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" dependencies = [ - "arrayvec", + "arrayvec 0.7.6", "auto_impl", "bytes", ] @@ -2911,7 +3527,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4" dependencies = [ - "arrayvec", + "arrayvec 0.7.6", "auto_impl", "bytes", ] @@ -2928,6 +3544,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ + "bitvec", "rand_core 0.6.4", "subtle", ] @@ -2938,6 +3555,16 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "file-guard" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21ef72acf95ec3d7dbf61275be556299490a245f017cf084bd23b4f68cf9407c" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "filetime" version = "0.2.25" @@ -2950,6 +3577,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "finito" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2384245d85162258a14b43567a9ee3598f5ae746a1581fb5d3d2cb780f0dbf95" +dependencies = [ + "futures-timer", + "pin-project", +] + [[package]] name = "five8_const" version = "0.1.4" @@ -3029,6 +3666,38 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" +[[package]] +name = "frame-metadata" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "878babb0b136e731cc77ec2fd883ff02745ff21e6fb662729953d44923df009c" +dependencies = [ + "cfg-if", + "parity-scale-codec", + "scale-info", +] + +[[package]] +name = "frame-metadata" +version = "16.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cf1549fba25a6fcac22785b61698317d958e96cac72a59102ea45b9ae64692" +dependencies = [ + "cfg-if", + "parity-scale-codec", + "scale-info", + "serde", +] + +[[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + [[package]] name = "fs-err" version = "3.1.0" @@ -3084,6 +3753,7 @@ dependencies = [ "futures-core", "futures-task", "futures-util", + "num_cpus", ] [[package]] @@ -3092,6 +3762,19 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.31" @@ -3099,7 +3782,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -3207,17 +3890,38 @@ dependencies = [ ] [[package]] -name = "gimli" -version = "0.31.1" +name = "getrandom_or_panic" +version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "6ea1015b5a70616b688dc230cfe50c8af89d972cb132d5a622814d29773b10b9" +dependencies = [ + "rand 0.8.5", + "rand_core 0.6.4", +] [[package]] -name = "glob" -version = "0.3.2" +name = "gimli" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" - +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" +dependencies = [ + "fallible-iterator", + "indexmap 1.9.3", + "stable_deref_trait", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + [[package]] name = "google-cloud-auth" version = "0.20.0" @@ -3336,7 +4040,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", + "rand 0.8.5", "rand_core 0.6.4", + "rand_xorshift", "subtle", ] @@ -3378,11 +4084,94 @@ dependencies = [ "tracing", ] +[[package]] +name = "halo2_proofs" +version = "0.3.0" +source = "git+https://github.com/input-output-hk/halo2?rev=04db20e#04db20e066385fdc1e370c8f257eea0475b3fa10" +dependencies = [ + "blake2b_simd", + "blstrs", + "ff", + "getrandom 0.2.16", + "group", + "halo2curves", + "num-bigint 0.4.6", + "rand_chacha 0.3.1", + "rand_core 0.6.4", + "rayon", + "serde", + "serde_derive", + "sha3 0.9.1", + "tracing", +] + +[[package]] +name = "halo2curves" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d380afeef3f1d4d3245b76895172018cfb087d9976a7cabcd5597775b2933e07" +dependencies = [ + "blake2", + "digest 0.10.7", + "ff", + "group", + "halo2derive", + "hex", + "lazy_static", + "num-bigint 0.4.6", + "num-integer", + "num-traits", + "pairing", + "pasta_curves", + "paste", + "rand 0.8.5", + "rand_core 0.6.4", + "rayon", + "serde", + "serde_arrays", + "sha2 0.10.9", + "static_assertions", + "subtle", + "unroll", +] + +[[package]] +name = "halo2derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb99e7492b4f5ff469d238db464131b86c2eaac814a78715acba369f64d2c76" +dependencies = [ + "num-bigint 0.4.6", + "num-integer", + "num-traits", + "proc-macro2", + "quote 1.0.40", + "syn 1.0.109", +] + +[[package]] +name = "hash-db" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e7d7786361d7425ae2fe4f9e407eb0efaa0840f5212d109cc018c40c35c6ab4" + +[[package]] +name = "hash256-std-hasher" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92c171d55b98633f4ed3860808f004099b36c1cc29c42cfc53aa8591b21efcf2" +dependencies = [ + "crunchy", +] + [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] [[package]] name = "hashbrown" @@ -3390,7 +4179,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash", + "ahash 0.8.12", ] [[package]] @@ -3398,6 +4187,11 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.8.12", + "allocator-api2", + "serde", +] [[package]] name = "hashbrown" @@ -3426,6 +4220,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "hermit-abi" version = "0.5.1" @@ -3441,6 +4241,18 @@ dependencies = [ "serde", ] +[[package]] +name = "hex-conservative" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212ab92002354b4819390025006c897e8140934349e8635c9b077f47b4dcbd20" + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + [[package]] name = "histogram" version = "0.6.9" @@ -3823,6 +4635,15 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" +[[package]] +name = "impl-serde" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc88fc67028ae3db0c853baa36269d398d5f45b6982f95549ff5def78c935cd" +dependencies = [ + "serde", +] + [[package]] name = "impl-trait-for-tuples" version = "0.2.3" @@ -3830,7 +4651,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -3862,6 +4683,12 @@ dependencies = [ "serde", ] +[[package]] +name = "indexmap-nostd" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e04e2fd2b8188ea827b32ef11de88377086d690286ab35747ef7f9bf3ccb590" + [[package]] name = "indicatif" version = "0.17.11" @@ -3884,6 +4711,24 @@ dependencies = [ "generic-array", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "integer-sqrt" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770" +dependencies = [ + "num-traits", +] + [[package]] name = "interprocess" version = "2.2.3" @@ -3899,6 +4744,38 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "introspection" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8aef96cc8e702a9107d35e7eb802f60c8f3babb94c5b1d5eac7c6b99116344ff" +dependencies = [ + "quote 0.3.15", + "syn 0.11.11", +] + +[[package]] +name = "introspection-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f40c436bdcb61b4dcd7c302029c522150b45e4f048cacd0ad845b48ba3b1cac" +dependencies = [ + "introspection", + "quote 0.3.15", + "syn 0.11.11", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -3924,6 +4801,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "is_sorted" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357376465c37db3372ef6a00585d336ed3d0f11d4345eef77ebcb05865392b21" + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -3957,21 +4840,26 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", +] + [[package]] name = "jni" version = "0.21.1" @@ -4029,6 +4917,73 @@ dependencies = [ "serde_json", ] +[[package]] +name = "jsonrpsee" +version = "0.22.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfdb12a2381ea5b2e68c3469ec604a007b367778cdb14d09612c8069ebd616ad" +dependencies = [ + "jsonrpsee-client-transport 0.22.5", + "jsonrpsee-core 0.22.5", + "jsonrpsee-http-client 0.22.5", + "jsonrpsee-types 0.22.5", +] + +[[package]] +name = "jsonrpsee" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b089779ad7f80768693755a031cc14a7766aba707cbe886674e3f79e9b7e47" +dependencies = [ + "jsonrpsee-core 0.23.2", + "jsonrpsee-types 0.23.2", + "jsonrpsee-ws-client", +] + +[[package]] +name = "jsonrpsee-client-transport" +version = "0.22.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4978087a58c3ab02efc5b07c5e5e2803024536106fd5506f558db172c889b3aa" +dependencies = [ + "futures-util", + "http 0.2.12", + "jsonrpsee-core 0.22.5", + "pin-project", + "rustls-native-certs 0.7.3", + "rustls-pki-types", + "soketto 0.7.1", + "thiserror 1.0.69", + "tokio", + "tokio-rustls 0.25.0", + "tokio-util", + "tracing", + "url", +] + +[[package]] +name = "jsonrpsee-client-transport" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08163edd8bcc466c33d79e10f695cdc98c00d1e6ddfb95cec41b6b0279dd5432" +dependencies = [ + "base64 0.22.1", + "futures-util", + "http 1.3.1", + "jsonrpsee-core 0.23.2", + "pin-project", + "rustls 0.23.27", + "rustls-pki-types", + "rustls-platform-verifier 0.3.4", + "soketto 0.8.1", + "thiserror 1.0.69", + "tokio", + "tokio-rustls 0.26.2", + "tokio-util", + "tracing", + "url", +] + [[package]] name = "jsonrpsee-core" version = "0.20.4" @@ -4040,7 +4995,7 @@ dependencies = [ "beef", "futures-util", "hyper 0.14.32", - "jsonrpsee-types", + "jsonrpsee-types 0.20.4", "serde", "serde_json", "thiserror 1.0.69", @@ -4049,62 +5004,167 @@ dependencies = [ ] [[package]] -name = "jsonrpsee-http-client" -version = "0.20.4" +name = "jsonrpsee-core" +version = "0.22.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c7b9f95208927653e7965a98525e7fc641781cab89f0e27c43fa2974405683" +checksum = "b4b257e1ec385e07b0255dde0b933f948b5c8b8c28d42afda9587c3a967b896d" dependencies = [ + "anyhow", "async-trait", + "beef", + "futures-timer", + "futures-util", "hyper 0.14.32", - "hyper-rustls 0.24.2", - "jsonrpsee-core", - "jsonrpsee-types", + "jsonrpsee-types 0.22.5", + "pin-project", + "rustc-hash 1.1.0", "serde", "serde_json", "thiserror 1.0.69", "tokio", - "tower 0.4.13", + "tokio-stream", "tracing", - "url", ] [[package]] -name = "jsonrpsee-types" -version = "0.20.4" +name = "jsonrpsee-core" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3264e339143fe37ed081953842ee67bfafa99e3b91559bdded6e4abd8fc8535e" +checksum = "79712302e737d23ca0daa178e752c9334846b08321d439fd89af9a384f8c830b" dependencies = [ "anyhow", + "async-trait", "beef", + "futures-timer", + "futures-util", + "jsonrpsee-types 0.23.2", + "pin-project", + "rustc-hash 1.1.0", "serde", "serde_json", "thiserror 1.0.69", + "tokio", + "tokio-stream", "tracing", ] [[package]] -name = "k256" -version = "0.13.4" +name = "jsonrpsee-http-client" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +checksum = "57c7b9f95208927653e7965a98525e7fc641781cab89f0e27c43fa2974405683" dependencies = [ - "cfg-if", - "ecdsa", - "elliptic-curve", - "once_cell", - "sha2 0.10.9", - "signature 2.2.0", + "async-trait", + "hyper 0.14.32", + "hyper-rustls 0.24.2", + "jsonrpsee-core 0.20.4", + "jsonrpsee-types 0.20.4", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tower 0.4.13", + "tracing", + "url", ] [[package]] -name = "keccak" -version = "0.1.5" +name = "jsonrpsee-http-client" +version = "0.22.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +checksum = "1ccf93fc4a0bfe05d851d37d7c32b7f370fe94336b52a2f0efc5f1981895c2e5" dependencies = [ - "cpufeatures", -] - + "async-trait", + "hyper 0.14.32", + "hyper-rustls 0.24.2", + "jsonrpsee-core 0.22.5", + "jsonrpsee-types 0.22.5", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tower 0.4.13", + "tracing", + "url", +] + +[[package]] +name = "jsonrpsee-types" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3264e339143fe37ed081953842ee67bfafa99e3b91559bdded6e4abd8fc8535e" +dependencies = [ + "anyhow", + "beef", + "serde", + "serde_json", + "thiserror 1.0.69", + "tracing", +] + +[[package]] +name = "jsonrpsee-types" +version = "0.22.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "150d6168405890a7a3231a3c74843f58b8959471f6df76078db2619ddee1d07d" +dependencies = [ + "anyhow", + "beef", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonrpsee-types" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c465fbe385238e861fdc4d1c85e04ada6c1fd246161d26385c1b311724d2af" +dependencies = [ + "beef", + "http 1.3.1", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonrpsee-ws-client" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c28759775f5cb2f1ea9667672d3fe2b0e701d1f4b7b67954e60afe7fd058b5e" +dependencies = [ + "http 1.3.1", + "jsonrpsee-client-transport 0.23.2", + "jsonrpsee-core 0.23.2", + "jsonrpsee-types 0.23.2", + "url", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "serdect", + "sha2 0.10.9", + "signature 2.2.0", +] + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + [[package]] name = "keccak-asm" version = "0.1.4" @@ -4115,6 +5175,33 @@ dependencies = [ "sha3-asm", ] +[[package]] +name = "konst" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4381b9b00c55f251f2ebe9473aef7c117e96828def1a7cb3bd3f0f903c6894e9" +dependencies = [ + "const_panic", + "konst_kernel", + "konst_proc_macros", + "typewit", +] + +[[package]] +name = "konst_kernel" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4b1eb7788f3824c629b1116a7a9060d6e898c358ebff59070093d51103dcc3c" +dependencies = [ + "typewit", +] + +[[package]] +name = "konst_proc_macros" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00af7901ba50898c9e545c24d5c580c96a982298134e8037d8978b6594782c07" + [[package]] name = "language-tags" version = "0.3.2" @@ -4126,6 +5213,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "lcov2cobertura" @@ -4178,15 +5268,34 @@ dependencies = [ "base64 0.12.3", "digest 0.9.0", "hmac-drbg", - "libsecp256k1-core", - "libsecp256k1-gen-ecmult", - "libsecp256k1-gen-genmult", + "libsecp256k1-core 0.2.2", + "libsecp256k1-gen-ecmult 0.2.1", + "libsecp256k1-gen-genmult 0.2.1", "rand 0.7.3", "serde", "sha2 0.9.9", "typenum", ] +[[package]] +name = "libsecp256k1" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79019718125edc905a079a70cfa5f3820bc76139fc91d6f9abc27ea2a887139" +dependencies = [ + "arrayref", + "base64 0.22.1", + "digest 0.9.0", + "hmac-drbg", + "libsecp256k1-core 0.3.0", + "libsecp256k1-gen-ecmult 0.3.0", + "libsecp256k1-gen-genmult 0.3.0", + "rand 0.8.5", + "serde", + "sha2 0.9.9", + "typenum", +] + [[package]] name = "libsecp256k1-core" version = "0.2.2" @@ -4198,13 +5307,33 @@ dependencies = [ "subtle", ] +[[package]] +name = "libsecp256k1-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + [[package]] name = "libsecp256k1-gen-ecmult" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccab96b584d38fac86a83f07e659f0deafd0253dc096dab5a36d53efe653c5c3" dependencies = [ - "libsecp256k1-core", + "libsecp256k1-core 0.2.2", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" +dependencies = [ + "libsecp256k1-core 0.3.0", ] [[package]] @@ -4213,7 +5342,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67abfe149395e3aa1c48a2beb32b068e2334402df8181f818d3aee2b304c4f5d" dependencies = [ - "libsecp256k1-core", + "libsecp256k1-core 0.2.2", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" +dependencies = [ + "libsecp256k1-core 0.3.0", ] [[package]] @@ -4228,6 +5366,18 @@ dependencies = [ "walkdir", ] +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.9.4" @@ -4288,6 +5438,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + [[package]] name = "macro-string" version = "0.1.4" @@ -4295,16 +5454,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] +[[package]] +name = "matchers" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f099785f7595cc4b4553a174ce30dd7589ef93391ff414dbb67f62392b9e0ce1" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memfd" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2cffa4ad52c6f791f4f8b15f0c05f9824b2ced1160e88cc393d64fff9a8ac64" +dependencies = [ + "rustix 0.38.44", +] + [[package]] name = "memmap2" version = "0.5.10" @@ -4314,6 +5491,15 @@ dependencies = [ "libc", ] +[[package]] +name = "memoffset" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +dependencies = [ + "autocfg", +] + [[package]] name = "memoffset" version = "0.9.1" @@ -4323,6 +5509,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memory-db" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808b50db46293432a45e63bc15ea51e0ab4c0a1647b8eb114e31a3e698dd6fbe" +dependencies = [ + "hash-db", +] + [[package]] name = "merlin" version = "3.0.0" @@ -4335,6 +5530,300 @@ dependencies = [ "zeroize", ] +[[package]] +name = "midnight-base-crypto" +version = "0.4.3" +source = "git+https://github.com/midnightntwrk/midnight-ledger-prototype?tag=ledger-4.0.0#b315c1d60d97c076e23fa3b6acf3329dde1aa4c4" +dependencies = [ + "anyhow", + "borsh 0.10.4", + "const-hex", + "ff", + "flate2", + "futures", + "group", + "k256", + "lazy_static", + "midnight-base-crypto-derive 0.4.3", + "midnight-serialize", + "paste", + "rand 0.8.5", + "reqwest 0.11.27", + "serde", + "serde_bytes", + "serde_json", + "sha2 0.10.9", + "signature 2.2.0", + "tracing", +] + +[[package]] +name = "midnight-base-crypto-derive" +version = "0.4.3-rc" +source = "git+https://github.com/input-output-hk/midnight-ledger-prototype#8d87336cf534e9a3c8a9b910994921562ec9814f" +dependencies = [ + "proc-macro2", + "quote 1.0.40", + "syn 1.0.109", +] + +[[package]] +name = "midnight-base-crypto-derive" +version = "0.4.3" +source = "git+https://github.com/midnightntwrk/midnight-ledger-prototype?tag=ledger-4.0.0#b315c1d60d97c076e23fa3b6acf3329dde1aa4c4" +dependencies = [ + "proc-macro2", + "quote 1.0.40", + "syn 1.0.109", +] + +[[package]] +name = "midnight-circuits" +version = "3.0.0" +source = "git+https://github.com/input-output-hk/midnight-circuits?tag=v3.0.0#44a7efe922e6b59b67d5429a286aff75a022ebbc" +dependencies = [ + "arrayvec 0.7.6", + "base64 0.13.1", + "bitvec", + "blake2b_simd", + "blstrs", + "const_num_bigint", + "ff", + "group", + "halo2_proofs", + "halo2curves", + "lazy_static", + "num-bigint 0.4.6", + "num-integer", + "num-traits", + "pasta_curves", + "rand 0.8.5", + "sha2 0.10.9", + "subtle", + "uint", +] + +[[package]] +name = "midnight-coin-structure" +version = "0.4.0" +source = "git+https://github.com/midnightntwrk/midnight-ledger-prototype?tag=ledger-4.0.0#b315c1d60d97c076e23fa3b6acf3329dde1aa4c4" +dependencies = [ + "lazy_static", + "midnight-storage", + "midnight-transient-crypto", + "rand 0.8.5", + "serde", +] + +[[package]] +name = "midnight-ledger" +version = "4.0.0" +source = "git+https://github.com/midnightntwrk/midnight-ledger-prototype?tag=ledger-4.0.0#b315c1d60d97c076e23fa3b6acf3329dde1aa4c4" +dependencies = [ + "derive-where", + "futures", + "introspection", + "introspection-derive", + "is_sorted", + "lazy_static", + "midnight-coin-structure", + "midnight-onchain-runtime", + "midnight-zswap", + "rand 0.8.5", + "rayon", + "reqwest 0.11.27", + "serde", + "sha2 0.10.9", + "tokio", + "tracing", + "tracing-subscriber 0.3.19", +] + +[[package]] +name = "midnight-node-ledger-helpers" +version = "0.1.0" +source = "git+https://github.com/midnightntwrk/midnight-node?tag=node-0.12.0#29935d2f0ef1cc6cba8830f652e814b9668ab11d" +dependencies = [ + "async-trait", + "futures", + "hex", + "itertools 0.12.1", + "lazy_static", + "log", + "midnight-ledger", + "rand 0.8.5", + "rand_chacha 0.3.1", + "tokio", +] + +[[package]] +name = "midnight-node-res" +version = "0.1.0" +source = "git+https://github.com/midnightntwrk/midnight-node?tag=node-0.12.0#29935d2f0ef1cc6cba8830f652e814b9668ab11d" +dependencies = [ + "hex", + "hex-literal", + "midnight-serialize", + "serde", + "serde_json", + "subxt", + "walkdir", +] + +[[package]] +name = "midnight-onchain-runtime" +version = "0.3.0" +source = "git+https://github.com/midnightntwrk/midnight-ledger-prototype?tag=ledger-4.0.0#b315c1d60d97c076e23fa3b6acf3329dde1aa4c4" +dependencies = [ + "derive-where", + "enum_index", + "enum_index_derive", + "hex", + "konst", + "lazy_static", + "midnight-coin-structure", + "midnight-onchain-state", + "midnight-onchain-vm", + "rand 0.8.5", + "serde", + "serde_bytes", + "tracing", +] + +[[package]] +name = "midnight-onchain-state" +version = "0.3.0" +source = "git+https://github.com/midnightntwrk/midnight-ledger-prototype?tag=ledger-4.0.0#b315c1d60d97c076e23fa3b6acf3329dde1aa4c4" +dependencies = [ + "derive-where", + "hex", + "midnight-coin-structure", + "rand 0.8.5", + "serde", + "serde_bytes", +] + +[[package]] +name = "midnight-onchain-vm" +version = "0.3.0" +source = "git+https://github.com/midnightntwrk/midnight-ledger-prototype?tag=ledger-4.0.0#b315c1d60d97c076e23fa3b6acf3329dde1aa4c4" +dependencies = [ + "derive-where", + "hex", + "konst", + "midnight-coin-structure", + "midnight-onchain-state", + "rand 0.8.5", + "rpds", + "serde", + "serde_bytes", +] + +[[package]] +name = "midnight-serialize" +version = "0.3.2" +source = "git+https://github.com/midnightntwrk/midnight-ledger-prototype?tag=ledger-4.0.0#b315c1d60d97c076e23fa3b6acf3329dde1aa4c4" +dependencies = [ + "borsh 0.10.4", + "crypto", + "konst", + "lazy_static", + "midnight-serialize-macros", + "serde", + "serde_bytes", +] + +[[package]] +name = "midnight-serialize-macros" +version = "0.1.0" +source = "git+https://github.com/midnightntwrk/midnight-ledger-prototype?tag=ledger-4.0.0#b315c1d60d97c076e23fa3b6acf3329dde1aa4c4" +dependencies = [ + "proc-macro2", + "quote 1.0.40", + "syn 1.0.109", +] + +[[package]] +name = "midnight-storage" +version = "0.4.0" +source = "git+https://github.com/midnightntwrk/midnight-ledger-prototype?tag=ledger-4.0.0#b315c1d60d97c076e23fa3b6acf3329dde1aa4c4" +dependencies = [ + "archery", + "crypto", + "derive-where", + "hex", + "konst", + "lru", + "midnight-base-crypto", + "midnight-storage-macros", + "parking_lot", + "rand 0.8.5", + "serde", + "sha2 0.10.9", + "tempfile", +] + +[[package]] +name = "midnight-storage-macros" +version = "0.1.1" +source = "git+https://github.com/midnightntwrk/midnight-ledger-prototype?tag=ledger-4.0.0#b315c1d60d97c076e23fa3b6acf3329dde1aa4c4" +dependencies = [ + "proc-macro2", + "quote 1.0.40", + "syn 2.0.101", +] + +[[package]] +name = "midnight-transient-crypto" +version = "0.5.0" +source = "git+https://github.com/midnightntwrk/midnight-ledger-prototype?tag=ledger-4.0.0#b315c1d60d97c076e23fa3b6acf3329dde1aa4c4" +dependencies = [ + "anyhow", + "blstrs", + "borsh 0.10.4", + "const-hex", + "derive-where", + "ff", + "flate2", + "group", + "halo2_proofs", + "k256", + "lazy_static", + "lru", + "midnight-base-crypto", + "midnight-base-crypto-derive 0.4.3-rc", + "midnight-circuits", + "midnight-serialize", + "midnight-storage", + "pasta_curves", + "paste", + "rand 0.8.5", + "serde", + "serde_bytes", + "serde_json", + "sha2 0.10.9", + "signature 2.2.0", + "tracing", +] + +[[package]] +name = "midnight-zswap" +version = "4.0.0" +source = "git+https://github.com/midnightntwrk/midnight-ledger-prototype?tag=ledger-4.0.0#b315c1d60d97c076e23fa3b6acf3329dde1aa4c4" +dependencies = [ + "derive-where", + "futures", + "is_sorted", + "lazy_static", + "midnight-base-crypto", + "midnight-coin-structure", + "midnight-onchain-runtime", + "midnight-transient-crypto", + "rand 0.8.5", + "serde", + "tracing", +] + [[package]] name = "mime" version = "0.3.17" @@ -4400,7 +5889,7 @@ checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898" dependencies = [ "cfg-if", "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -4468,7 +5957,7 @@ dependencies = [ "cfg-if", "cfg_aliases", "libc", - "memoffset", + "memoffset 0.9.1", ] [[package]] @@ -4477,6 +5966,24 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" +[[package]] +name = "no-std-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + [[package]] name = "nom" version = "7.1.3" @@ -4511,6 +6018,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num" version = "0.2.1" @@ -4521,7 +6038,7 @@ dependencies = [ "num-complex", "num-integer", "num-iter", - "num-rational", + "num-rational 0.2.4", "num-traits", ] @@ -4569,7 +6086,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 1.0.109", ] @@ -4580,10 +6097,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] +[[package]] +name = "num-format" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" +dependencies = [ + "arrayvec 0.7.6", + "itoa", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -4616,6 +6143,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint 0.4.6", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -4653,7 +6191,7 @@ checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ "proc-macro-crate 3.3.0", "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -4704,6 +6242,18 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "object" +version = "0.30.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b4680b86d9cfafba8fc491dc9b6df26b68cf40e9e6cd73909194759a63c385" +dependencies = [ + "crc32fast", + "hashbrown 0.13.2", + "indexmap 1.9.3", + "memchr", +] + [[package]] name = "object" version = "0.36.7" @@ -4773,7 +6323,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -4807,7 +6357,9 @@ dependencies = [ "apalis-cron", "apalis-redis", "async-trait", + "backoff", "base64 0.22.1", + "bech32", "bincode", "bs58", "bytes", @@ -4820,15 +6372,20 @@ dependencies = [ "ed25519-dalek 2.1.1", "eyre", "futures", + "futures-util", "google-cloud-auth", + "governor 0.8.1", "hex", "hmac 0.12.1", "http 1.3.1", - "itertools 0.14.0", + "itertools 0.12.1", "k256", "lazy_static", "libsodium-sys", "log", + "midnight-ledger", + "midnight-node-ledger-helpers", + "midnight-node-res", "mockall", "mockito", "mpl-token-metadata", @@ -4861,10 +6418,12 @@ dependencies = [ "strum 0.27.1", "strum_macros 0.27.1", "subtle", + "subxt", "sysinfo", "tempfile", "thiserror 2.0.12", "tokio", + "tokio-tungstenite 0.20.1", "tower 0.5.2", "utoipa", "uuid 1.17.0", @@ -4893,6 +6452,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "owo-colors" version = "4.2.1" @@ -4929,15 +6494,38 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "pairing" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" +dependencies = [ + "group", +] + +[[package]] +name = "parity-bip39" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e69bf016dc406eff7d53a7d3f7cf1c2e72c82b9088aac1118591e36dd2cd3e9" +dependencies = [ + "bitcoin_hashes", + "rand 0.7.3", + "rand_core 0.5.1", + "serde", + "unicode-normalization", +] + [[package]] name = "parity-scale-codec" version = "3.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" dependencies = [ - "arrayvec", + "arrayvec 0.7.6", "bitvec", "byte-slice-cast", + "bytes", "const_format", "impl-trait-for-tuples", "parity-scale-codec-derive", @@ -4953,7 +6541,7 @@ checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" dependencies = [ "proc-macro-crate 3.3.0", "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -4986,6 +6574,34 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pasta_curves" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e57598f73cc7e1b2ac63c79c517b31a0877cd7c402cdcaa311b5208de7a095" +dependencies = [ + "blake2b_simd", + "ff", + "group", + "hex", + "lazy_static", + "rand 0.8.5", + "serde", + "static_assertions", + "subtle", +] + [[package]] name = "paste" version = "1.0.15" @@ -5001,6 +6617,16 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "password-hash", +] + [[package]] name = "pem" version = "1.1.1" @@ -5081,7 +6707,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -5097,6 +6723,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -5113,6 +6750,106 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "polkavm-common" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92c99f7eee94e7be43ba37eef65ad0ee8cbaf89b7c00001c3f6d2be985cb1817" + +[[package]] +name = "polkavm-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9428a5cfcc85c5d7b9fc4b6a18c4b802d0173d768182a51cc7751640f08b92" + +[[package]] +name = "polkavm-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79fa916f7962348bd1bb1a65a83401675e6fc86c51a0fdbcf92a3108e58e6125" +dependencies = [ + "polkavm-derive-impl-macro 0.8.0", +] + +[[package]] +name = "polkavm-derive" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8c4bea6f3e11cd89bb18bcdddac10bd9a24015399bd1c485ad68a985a19606" +dependencies = [ + "polkavm-derive-impl-macro 0.9.0", +] + +[[package]] +name = "polkavm-derive-impl" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c10b2654a8a10a83c260bfb93e97b262cf0017494ab94a65d389e0eda6de6c9c" +dependencies = [ + "polkavm-common 0.8.0", + "proc-macro2", + "quote 1.0.40", + "syn 2.0.101", +] + +[[package]] +name = "polkavm-derive-impl" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c4fdfc49717fb9a196e74a5d28e0bc764eb394a2c803eb11133a31ac996c60c" +dependencies = [ + "polkavm-common 0.9.0", + "proc-macro2", + "quote 1.0.40", + "syn 2.0.101", +] + +[[package]] +name = "polkavm-derive-impl-macro" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e85319a0d5129dc9f021c62607e0804f5fb777a05cdda44d750ac0732def66" +dependencies = [ + "polkavm-derive-impl 0.8.0", + "syn 2.0.101", +] + +[[package]] +name = "polkavm-derive-impl-macro" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ba81f7b5faac81e528eb6158a6f3c9e0bb1008e0ffa19653bc8dea925ecb429" +dependencies = [ + "polkavm-derive-impl 0.9.0", + "syn 2.0.101", +] + +[[package]] +name = "polling" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.5.1", + "pin-project-lite", + "rustix 1.0.7", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "polyval" version = "0.6.2" @@ -5208,6 +6945,8 @@ checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" dependencies = [ "fixed-hash", "impl-codec", + "impl-serde", + "scale-info", "uint", ] @@ -5217,7 +6956,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" dependencies = [ - "toml", + "toml 0.5.11", ] [[package]] @@ -5229,6 +6968,30 @@ dependencies = [ "toml_edit", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote 1.0.40", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote 1.0.40", + "version_check", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -5236,7 +6999,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", ] [[package]] @@ -5247,7 +7010,7 @@ checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" dependencies = [ "proc-macro-error-attr2", "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -5289,7 +7052,7 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "rand_xorshift", - "regex-syntax", + "regex-syntax 0.8.5", "rusty-fork", "tempfile", "unarray", @@ -5301,6 +7064,15 @@ version = "2.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" +[[package]] +name = "psm" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f" +dependencies = [ + "cc", +] + [[package]] name = "qstring" version = "0.7.2" @@ -5351,7 +7123,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash", + "rustc-hash 2.1.1", "rustls 0.23.27", "socket2", "thiserror 2.0.12", @@ -5372,10 +7144,10 @@ dependencies = [ "lru-slab", "rand 0.9.1", "ring", - "rustc-hash", + "rustc-hash 2.1.1", "rustls 0.23.27", "rustls-pki-types", - "rustls-platform-verifier", + "rustls-platform-verifier 0.5.3", "slab", "thiserror 2.0.12", "tinyvec", @@ -5397,6 +7169,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "quote" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" + [[package]] name = "quote" version = "1.0.40" @@ -5557,6 +7335,22 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "reconnecting-jsonrpsee-ws-client" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06fa4f17e09edfc3131636082faaec633c7baa269396b4004040bc6c52f49f65" +dependencies = [ + "cfg_aliases", + "finito", + "futures", + "jsonrpsee 0.23.2", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tracing", +] + [[package]] name = "recvmsg" version = "1.0.0" @@ -5597,6 +7391,26 @@ dependencies = [ "bitflags 2.9.1", ] +[[package]] +name = "ref-cast" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +dependencies = [ + "proc-macro2", + "quote 1.0.40", + "syn 2.0.101", +] + [[package]] name = "regex" version = "1.11.1" @@ -5605,8 +7419,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -5617,7 +7440,7 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.5", ] [[package]] @@ -5626,6 +7449,12 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.8.5" @@ -5658,6 +7487,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rustls 0.21.12", + "rustls-native-certs 0.6.3", "rustls-pemfile 1.0.4", "serde", "serde_json", @@ -5671,6 +7501,7 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "webpki-roots 0.25.4", "winreg", @@ -5771,6 +7602,15 @@ dependencies = [ "rustc-hex", ] +[[package]] +name = "rpds" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd6ce569b15c331b1e5fd8cf6adb0bf240678b5f0cdc4d0f41e11683f6feba9" +dependencies = [ + "archery", +] + [[package]] name = "ruint" version = "1.15.0" @@ -5810,6 +7650,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.1" @@ -5876,13 +7722,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f07d43b2dbdbd99aaed648192098f0f413b762f0f352667153934ef3955f1793" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "regex", "serde_urlencoded", "syn 1.0.109", "synstructure 0.12.6", ] +[[package]] +name = "rustix" +version = "0.36.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "305efbd14fde4139eb501df5f136994bb520b033fa9fbdce287507dc23b8c7ed" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.1.4", + "windows-sys 0.45.0", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.52.0", +] + [[package]] name = "rustix" version = "1.0.7" @@ -5892,7 +7765,7 @@ dependencies = [ "bitflags 2.9.1", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.9.4", "windows-sys 0.59.0", ] @@ -5908,6 +7781,20 @@ dependencies = [ "sct", ] +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + [[package]] name = "rustls" version = "0.23.27" @@ -5935,6 +7822,19 @@ dependencies = [ "security-framework 2.11.1", ] +[[package]] +name = "rustls-native-certs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +dependencies = [ + "openssl-probe", + "rustls-pemfile 2.2.0", + "rustls-pki-types", + "schannel", + "security-framework 2.11.1", +] + [[package]] name = "rustls-native-certs" version = "0.8.1" @@ -5975,6 +7875,27 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afbb878bdfdf63a336a5e63561b1835e7a8c91524f51621db870169eac84b490" +dependencies = [ + "core-foundation 0.9.4", + "core-foundation-sys", + "jni 0.19.0", + "log", + "once_cell", + "rustls 0.23.27", + "rustls-native-certs 0.7.3", + "rustls-platform-verifier-android", + "rustls-webpki 0.102.8", + "security-framework 2.11.1", + "security-framework-sys", + "webpki-roots 0.26.11", + "winapi", +] + [[package]] name = "rustls-platform-verifier" version = "0.5.3" @@ -5983,7 +7904,7 @@ checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", - "jni", + "jni 0.21.1", "log", "once_cell", "rustls 0.23.27", @@ -6004,11 +7925,22 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.101.7" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ "ring", + "rustls-pki-types", "untrusted", ] @@ -6041,6 +7973,17 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "ruzstd" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c4eb8a81997cf040a091d1f7e1938aeab6749d3a0dfa73af43cdc32393483d" +dependencies = [ + "byteorder", + "derive_more 0.99.20", + "twox-hash", +] + [[package]] name = "ruzstd" version = "0.8.1" @@ -6080,6 +8023,143 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scale-bits" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57b1e7f6b65ed1f04e79a85a57d755ad56d76fdf1e9bddcc9ae14f71fcdcf54" +dependencies = [ + "parity-scale-codec", + "scale-info", + "scale-type-resolver", + "serde", +] + +[[package]] +name = "scale-decode" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e98f3262c250d90e700bb802eb704e1f841e03331c2eb815e46516c4edbf5b27" +dependencies = [ + "derive_more 0.99.20", + "parity-scale-codec", + "primitive-types", + "scale-bits", + "scale-decode-derive", + "scale-type-resolver", + "smallvec", +] + +[[package]] +name = "scale-decode-derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb22f574168103cdd3133b19281639ca65ad985e24612728f727339dcaf4021" +dependencies = [ + "darling 0.14.4", + "proc-macro2", + "quote 1.0.40", + "syn 1.0.109", +] + +[[package]] +name = "scale-encode" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528464e6ae6c8f98e2b79633bf79ef939552e795e316579dab09c61670d56602" +dependencies = [ + "derive_more 0.99.20", + "parity-scale-codec", + "primitive-types", + "scale-bits", + "scale-encode-derive", + "scale-type-resolver", + "smallvec", +] + +[[package]] +name = "scale-encode-derive" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef2618f123c88da9cd8853b69d766068f1eddc7692146d7dfe9b89e25ce2efd" +dependencies = [ + "darling 0.20.11", + "proc-macro-crate 3.3.0", + "proc-macro2", + "quote 1.0.40", + "syn 2.0.101", +] + +[[package]] +name = "scale-info" +version = "2.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346a3b32eba2640d17a9cb5927056b08f3de90f65b72fe09402c2ad07d684d0b" +dependencies = [ + "bitvec", + "cfg-if", + "derive_more 1.0.0", + "parity-scale-codec", + "scale-info-derive", + "serde", +] + +[[package]] +name = "scale-info-derive" +version = "2.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6630024bf739e2179b91fb424b28898baf819414262c5d376677dbff1fe7ebf" +dependencies = [ + "proc-macro-crate 3.3.0", + "proc-macro2", + "quote 1.0.40", + "syn 2.0.101", +] + +[[package]] +name = "scale-type-resolver" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0cded6518aa0bd6c1be2b88ac81bf7044992f0f154bfbabd5ad34f43512abcb" +dependencies = [ + "scale-info", + "smallvec", +] + +[[package]] +name = "scale-typegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498d1aecf2ea61325d4511787c115791639c0fd21ef4f8e11e49dd09eff2bbac" +dependencies = [ + "proc-macro2", + "quote 1.0.40", + "scale-info", + "syn 2.0.101", + "thiserror 1.0.69", +] + +[[package]] +name = "scale-value" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cd6ab090d823e75cfdb258aad5fe92e13f2af7d04b43a55d607d25fcc38c811" +dependencies = [ + "base58", + "blake2", + "derive_more 0.99.20", + "either", + "frame-metadata 15.1.0", + "parity-scale-codec", + "scale-bits", + "scale-decode", + "scale-encode", + "scale-info", + "scale-type-resolver", + "serde", + "yap", +] + [[package]] name = "scc" version = "2.3.4" @@ -6104,11 +8184,30 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "356285bbf17bea63d9e52e96bd18f039672ac92b55b8cb997d6162a2a37d1649" dependencies = [ - "ahash", + "ahash 0.8.12", "cfg-if", "hashbrown 0.13.2", ] +[[package]] +name = "schnorrkel" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de18f6d8ba0aad7045f5feae07ec29899c1112584a38509a84ad7b04451eaa0" +dependencies = [ + "aead", + "arrayref", + "arrayvec 0.7.6", + "curve25519-dalek 4.1.3", + "getrandom_or_panic", + "merlin", + "rand_core 0.6.4", + "serde_bytes", + "sha2 0.10.9", + "subtle", + "zeroize", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -6122,7 +8221,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f9e24d2b632954ded8ab2ef9fea0a0c769ea56ea98bddbafbad22caeeadf45d" dependencies = [ "hmac 0.12.1", - "pbkdf2", + "pbkdf2 0.11.0", "salsa20", "sha2 0.10.9", ] @@ -6153,10 +8252,38 @@ dependencies = [ "der", "generic-array", "pkcs8", + "serdect", "subtle", "zeroize", ] +[[package]] +name = "secp256k1" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" +dependencies = [ + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb" +dependencies = [ + "cc", +] + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "zeroize", +] + [[package]] name = "secrets" version = "1.2.0" @@ -6178,6 +8305,7 @@ dependencies = [ "core-foundation 0.9.4", "core-foundation-sys", "libc", + "num-bigint 0.4.6", "security-framework-sys", ] @@ -6274,6 +8402,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_arrays" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38636132857f68ec3d5f3eb121166d2af33cb55174c4d5ff645db6165cbef0fd" +dependencies = [ + "serde", +] + [[package]] name = "serde_bytes" version = "0.11.17" @@ -6290,7 +8427,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -6353,10 +8490,20 @@ checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" dependencies = [ "darling 0.20.11", "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "serial_test" version = "3.2.0" @@ -6378,10 +8525,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] +[[package]] +name = "sha-1" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + [[package]] name = "sha1" version = "0.10.6" @@ -6423,6 +8583,18 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha3" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" +dependencies = [ + "block-buffer 0.9.0", + "digest 0.9.0", + "keccak", + "opaque-debug", +] + [[package]] name = "sha3" version = "0.10.8" @@ -6515,6 +8687,12 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +[[package]] +name = "simple-mermaid" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "620a1d43d70e142b1d46a929af51d44f383db9c7a2ec122de2cd992ccfcf3c18" + [[package]] name = "simple_asn1" version = "0.6.3" @@ -6568,6 +8746,114 @@ dependencies = [ "serde", ] +[[package]] +name = "smol" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" +dependencies = [ + "async-channel 2.3.1", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-net", + "async-process", + "blocking", + "futures-lite", +] + +[[package]] +name = "smoldot" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d1eaa97d77be4d026a1e7ffad1bb3b78448763b357ea6f8188d3e6f736a9b9" +dependencies = [ + "arrayvec 0.7.6", + "async-lock", + "atomic-take", + "base64 0.21.7", + "bip39", + "blake2-rfc", + "bs58", + "chacha20", + "crossbeam-queue", + "derive_more 0.99.20", + "ed25519-zebra 4.0.3", + "either", + "event-listener 4.0.3", + "fnv", + "futures-lite", + "futures-util", + "hashbrown 0.14.5", + "hex", + "hmac 0.12.1", + "itertools 0.12.1", + "libm", + "libsecp256k1 0.7.2", + "merlin", + "no-std-net", + "nom", + "num-bigint 0.4.6", + "num-rational 0.4.2", + "num-traits", + "pbkdf2 0.12.2", + "pin-project", + "poly1305", + "rand 0.8.5", + "rand_chacha 0.3.1", + "ruzstd 0.5.0", + "schnorrkel", + "serde", + "serde_json", + "sha2 0.10.9", + "sha3 0.10.8", + "siphasher 1.0.1", + "slab", + "smallvec", + "soketto 0.7.1", + "twox-hash", + "wasmi", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "smoldot-light" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5496f2d116b7019a526b1039ec2247dd172b8670633b1a64a614c9ea12c9d8c7" +dependencies = [ + "async-channel 2.3.1", + "async-lock", + "base64 0.21.7", + "blake2-rfc", + "derive_more 0.99.20", + "either", + "event-listener 4.0.3", + "fnv", + "futures-channel", + "futures-lite", + "futures-util", + "hashbrown 0.14.5", + "hex", + "itertools 0.12.1", + "log", + "lru", + "no-std-net", + "parking_lot", + "pin-project", + "rand 0.8.5", + "rand_chacha 0.3.1", + "serde", + "serde_json", + "siphasher 1.0.1", + "slab", + "smol", + "smoldot", + "zeroize", +] + [[package]] name = "socket2" version = "0.5.10" @@ -6578,6 +8864,36 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "soketto" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d1c5305e39e09653383c2c7244f2f78b3bcae37cf50c64cb4789c9f5096ec2" +dependencies = [ + "base64 0.13.1", + "bytes", + "futures", + "httparse", + "log", + "rand 0.8.5", + "sha-1", +] + +[[package]] +name = "soketto" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures", + "httparse", + "log", + "rand 0.8.5", + "sha1", +] + [[package]] name = "solana-account" version = "2.2.1" @@ -7011,7 +9327,7 @@ version = "2.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93b93971e289d6425f88e6e3cb6668c4b05df78b3c518c249be55ced8efd6b6d" dependencies = [ - "ahash", + "ahash 0.8.12", "lazy_static", "solana-epoch-schedule", "solana-hash", @@ -7025,7 +9341,7 @@ version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02007757246e40f10aa936dae4fa27efbf8dbd6a59575a12ccc802c1aea6e708" dependencies = [ - "ahash", + "ahash 0.8.12", "solana-pubkey", ] @@ -7172,7 +9488,7 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7aeb957fbd42a451b99235df4942d96db7ef678e8d5061ef34c9b34cae12f79" dependencies = [ - "sha3", + "sha3 0.10.8", "solana-define-syscall", "solana-hash", "solana-sanitize", @@ -7413,7 +9729,7 @@ version = "2.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97222a3fda48570754ce114e43ca56af34741098c357cb8d3cb6695751e60330" dependencies = [ - "ahash", + "ahash 0.8.12", "bincode", "bv", "caps", @@ -7504,7 +9820,7 @@ dependencies = [ "getrandom 0.2.16", "lazy_static", "log", - "memoffset", + "memoffset 0.9.1", "num-bigint 0.4.6", "num-derive 0.4.2", "num-traits", @@ -7966,7 +10282,7 @@ checksum = "86280da8b99d03560f6ab5aca9de2e38805681df34e0bb8f238e69b29433b9df" dependencies = [ "bs58", "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -7978,10 +10294,10 @@ checksum = "a0a1caa972414cc78122c32bdae65ac5fe89df7db598585a5cde19d16a20280a" dependencies = [ "bincode", "digest 0.10.7", - "libsecp256k1", + "libsecp256k1 0.6.0", "serde", "serde_derive", - "sha3", + "sha3 0.10.8", "solana-feature-set", "solana-instruction", "solana-precompile-error", @@ -7995,7 +10311,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baa3120b6cdaa270f39444f5093a90a7b03d296d362878f7a6991d6de3bbe496" dependencies = [ "borsh 1.5.7", - "libsecp256k1", + "libsecp256k1 0.6.0", "solana-define-syscall", "thiserror 2.0.12", ] @@ -8036,7 +10352,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36187af2324f079f65a675ec22b31c24919cb4ac22c79472e85d819db9bbbc15" dependencies = [ "hmac 0.12.1", - "pbkdf2", + "pbkdf2 0.11.0", "sha2 0.10.9", ] @@ -8189,7 +10505,7 @@ version = "2.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1eaf5b216717d1d551716f3190878d028c689dabac40c8889767cead7e447481" dependencies = [ - "async-channel", + "async-channel 1.9.0", "bytes", "crossbeam-channel", "dashmap 5.5.3", @@ -8466,151 +10782,488 @@ dependencies = [ name = "solana-transaction-status-client-types" version = "2.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aaef59e8a54fc3a2dabfd85c32e35493c5e228f9d1efbcdcdc3c0819dddf7fd" +checksum = "4aaef59e8a54fc3a2dabfd85c32e35493c5e228f9d1efbcdcdc3c0819dddf7fd" +dependencies = [ + "base64 0.22.1", + "bincode", + "bs58", + "serde", + "serde_derive", + "serde_json", + "solana-account-decoder-client-types", + "solana-commitment-config", + "solana-message", + "solana-reward-info", + "solana-signature", + "solana-transaction", + "solana-transaction-context", + "solana-transaction-error", + "thiserror 2.0.12", +] + +[[package]] +name = "solana-udp-client" +version = "2.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d3e085a6adf81d51f678624934ffe266bd45a1c105849992b1af933c80bbf19" +dependencies = [ + "async-trait", + "solana-connection-cache", + "solana-keypair", + "solana-net-utils", + "solana-streamer", + "solana-transaction-error", + "thiserror 2.0.12", + "tokio", +] + +[[package]] +name = "solana-validator-exit" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bbf6d7a3c0b28dd5335c52c0e9eae49d0ae489a8f324917faf0ded65a812c1d" + +[[package]] +name = "solana-version" +version = "2.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a58e01912dc3d5ff4391fe49476461b3b9ebc4215f3713d2fe3ffcfeda7f8e2" +dependencies = [ + "agave-feature-set", + "semver 1.0.26", + "serde", + "serde_derive", + "solana-sanitize", + "solana-serde-varint", +] + +[[package]] +name = "solana-vote-interface" +version = "2.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4f08746f154458f28b98330c0d55cb431e2de64ee4b8efc98dcbe292e0672b" +dependencies = [ + "bincode", + "num-derive 0.4.2", + "num-traits", + "serde", + "serde_derive", + "solana-clock", + "solana-decode-error", + "solana-hash", + "solana-instruction", + "solana-pubkey", + "solana-rent", + "solana-sdk-ids", + "solana-serde-varint", + "solana-serialize-utils", + "solana-short-vec", + "solana-system-interface", +] + +[[package]] +name = "solana-zk-sdk" +version = "2.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15045540c315a9b8ea056323e73320e76098dfdaac9e65b1b33fe9c2f3c9b9e1" +dependencies = [ + "aes-gcm-siv", + "base64 0.22.1", + "bincode", + "bytemuck", + "bytemuck_derive", + "curve25519-dalek 4.1.3", + "itertools 0.12.1", + "js-sys", + "lazy_static", + "merlin", + "num-derive 0.4.2", + "num-traits", + "rand 0.8.5", + "serde", + "serde_derive", + "serde_json", + "sha3 0.10.8", + "solana-derivation-path", + "solana-instruction", + "solana-pubkey", + "solana-sdk-ids", + "solana-seed-derivable", + "solana-seed-phrase", + "solana-signature", + "solana-signer", + "subtle", + "thiserror 2.0.12", + "wasm-bindgen", + "zeroize", +] + +[[package]] +name = "soroban-rs" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790ecbbc2f864561c405f485383605960eb49353e2bd9f70b29b6ac9feb70b72" +dependencies = [ + "async-trait", + "ed25519-dalek 2.1.1", + "hex", + "rand 0.9.1", + "sha2 0.10.9", + "soroban-rs-macros", + "stellar-rpc-client", + "stellar-strkey 0.0.11", + "stellar-xdr", + "tokio", +] + +[[package]] +name = "soroban-rs-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e2fa346eb4b437b64abfb3a1972d1bdaa7e868bedfb352d5a4638f4ac586365" +dependencies = [ + "proc-macro2", + "quote 1.0.40", + "syn 2.0.101", +] + +[[package]] +name = "sp-application-crypto" +version = "33.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13ca6121c22c8bd3d1dce1f05c479101fd0d7b159bef2a3e8c834138d839c75c" +dependencies = [ + "parity-scale-codec", + "scale-info", + "serde", + "sp-core", + "sp-io", + "sp-std", +] + +[[package]] +name = "sp-arithmetic" +version = "25.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "910c07fa263b20bf7271fdd4adcb5d3217dfdac14270592e0780223542e7e114" +dependencies = [ + "integer-sqrt", + "num-traits", + "parity-scale-codec", + "scale-info", + "serde", + "sp-std", + "static_assertions", +] + +[[package]] +name = "sp-core" +version = "31.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d7a0fd8f16dcc3761198fc83be12872f823b37b749bc72a3a6a1f702509366" +dependencies = [ + "array-bytes", + "bitflags 1.3.2", + "blake2", + "bounded-collections", + "bs58", + "dyn-clonable", + "ed25519-zebra 3.1.0", + "futures", + "hash-db", + "hash256-std-hasher", + "impl-serde", + "itertools 0.10.5", + "k256", + "libsecp256k1 0.7.2", + "log", + "merlin", + "parity-bip39", + "parity-scale-codec", + "parking_lot", + "paste", + "primitive-types", + "rand 0.8.5", + "scale-info", + "schnorrkel", + "secp256k1", + "secrecy", + "serde", + "sp-crypto-hashing", + "sp-debug-derive", + "sp-externalities", + "sp-runtime-interface", + "sp-std", + "sp-storage", + "ss58-registry", + "substrate-bip39", + "thiserror 1.0.69", + "tracing", + "w3f-bls", + "zeroize", +] + +[[package]] +name = "sp-crypto-hashing" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9927a7f81334ed5b8a98a4a978c81324d12bd9713ec76b5c68fd410174c5eb" +dependencies = [ + "blake2b_simd", + "byteorder", + "digest 0.10.7", + "sha2 0.10.9", + "sha3 0.10.8", + "twox-hash", +] + +[[package]] +name = "sp-debug-derive" +version = "14.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d09fa0a5f7299fb81ee25ae3853d26200f7a348148aed6de76be905c007dbe" +dependencies = [ + "proc-macro2", + "quote 1.0.40", + "syn 2.0.101", +] + +[[package]] +name = "sp-externalities" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d6a4572eadd4a63cff92509a210bf425501a0c5e76574b30a366ac77653787" +dependencies = [ + "environmental", + "parity-scale-codec", + "sp-std", + "sp-storage", +] + +[[package]] +name = "sp-io" +version = "33.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e09bba780b55bd9e67979cd8f654a31e4a6cf45426ff371394a65953d2177f2" +dependencies = [ + "bytes", + "ed25519-dalek 2.1.1", + "libsecp256k1 0.7.2", + "log", + "parity-scale-codec", + "polkavm-derive 0.9.1", + "rustversion", + "secp256k1", + "sp-core", + "sp-crypto-hashing", + "sp-externalities", + "sp-keystore", + "sp-runtime-interface", + "sp-state-machine", + "sp-std", + "sp-tracing", + "sp-trie", + "tracing", + "tracing-core", +] + +[[package]] +name = "sp-keystore" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdbab8b61bd61d5f8625a0c75753b5d5a23be55d3445419acd42caf59cf6236b" +dependencies = [ + "parity-scale-codec", + "parking_lot", + "sp-core", + "sp-externalities", +] + +[[package]] +name = "sp-panic-handler" +version = "13.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8b52e69a577cbfdea62bfaf16f59eb884422ce98f78b5cd8d9bf668776bced1" +dependencies = [ + "backtrace", + "regex", +] + +[[package]] +name = "sp-runtime" +version = "34.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3cb126971e7db2f0fcf8053dce740684c438c7180cfca1959598230f342c58" +dependencies = [ + "docify", + "either", + "hash256-std-hasher", + "impl-trait-for-tuples", + "log", + "parity-scale-codec", + "paste", + "rand 0.8.5", + "scale-info", + "serde", + "simple-mermaid", + "sp-application-crypto", + "sp-arithmetic", + "sp-core", + "sp-io", + "sp-std", + "sp-weights", +] + +[[package]] +name = "sp-runtime-interface" +version = "26.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a675ea4858333d4d755899ed5ed780174aa34fec15953428d516af5452295" +dependencies = [ + "bytes", + "impl-trait-for-tuples", + "parity-scale-codec", + "polkavm-derive 0.8.0", + "primitive-types", + "sp-externalities", + "sp-runtime-interface-proc-macro", + "sp-std", + "sp-storage", + "sp-tracing", + "sp-wasm-interface", + "static_assertions", +] + +[[package]] +name = "sp-runtime-interface-proc-macro" +version = "18.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0195f32c628fee3ce1dfbbf2e7e52a30ea85f3589da9fe62a8b816d70fc06294" dependencies = [ - "base64 0.22.1", - "bincode", - "bs58", - "serde", - "serde_derive", - "serde_json", - "solana-account-decoder-client-types", - "solana-commitment-config", - "solana-message", - "solana-reward-info", - "solana-signature", - "solana-transaction", - "solana-transaction-context", - "solana-transaction-error", - "thiserror 2.0.12", + "Inflector", + "expander", + "proc-macro-crate 3.3.0", + "proc-macro2", + "quote 1.0.40", + "syn 2.0.101", ] [[package]] -name = "solana-udp-client" -version = "2.2.7" +name = "sp-state-machine" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d3e085a6adf81d51f678624934ffe266bd45a1c105849992b1af933c80bbf19" +checksum = "1eae0eac8034ba14437e772366336f579398a46d101de13dbb781ab1e35e67c5" dependencies = [ - "async-trait", - "solana-connection-cache", - "solana-keypair", - "solana-net-utils", - "solana-streamer", - "solana-transaction-error", - "thiserror 2.0.12", - "tokio", + "hash-db", + "log", + "parity-scale-codec", + "parking_lot", + "rand 0.8.5", + "smallvec", + "sp-core", + "sp-externalities", + "sp-panic-handler", + "sp-std", + "sp-trie", + "thiserror 1.0.69", + "tracing", + "trie-db", ] [[package]] -name = "solana-validator-exit" -version = "2.2.1" +name = "sp-std" +version = "14.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bbf6d7a3c0b28dd5335c52c0e9eae49d0ae489a8f324917faf0ded65a812c1d" +checksum = "12f8ee986414b0a9ad741776762f4083cd3a5128449b982a3919c4df36874834" [[package]] -name = "solana-version" -version = "2.2.7" +name = "sp-storage" +version = "20.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a58e01912dc3d5ff4391fe49476461b3b9ebc4215f3713d2fe3ffcfeda7f8e2" +checksum = "8dba5791cb3978e95daf99dad919ecb3ec35565604e88cd38d805d9d4981e8bd" dependencies = [ - "agave-feature-set", - "semver 1.0.26", + "impl-serde", + "parity-scale-codec", + "ref-cast", "serde", - "serde_derive", - "solana-sanitize", - "solana-serde-varint", + "sp-debug-derive", + "sp-std", ] [[package]] -name = "solana-vote-interface" -version = "2.2.5" +name = "sp-tracing" +version = "16.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4f08746f154458f28b98330c0d55cb431e2de64ee4b8efc98dcbe292e0672b" +checksum = "0351810b9d074df71c4514c5228ed05c250607cba131c1c9d1526760ab69c05c" dependencies = [ - "bincode", - "num-derive 0.4.2", - "num-traits", - "serde", - "serde_derive", - "solana-clock", - "solana-decode-error", - "solana-hash", - "solana-instruction", - "solana-pubkey", - "solana-rent", - "solana-sdk-ids", - "solana-serde-varint", - "solana-serialize-utils", - "solana-short-vec", - "solana-system-interface", + "parity-scale-codec", + "sp-std", + "tracing", + "tracing-core", + "tracing-subscriber 0.2.25", ] [[package]] -name = "solana-zk-sdk" -version = "2.2.15" +name = "sp-trie" +version = "32.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15045540c315a9b8ea056323e73320e76098dfdaac9e65b1b33fe9c2f3c9b9e1" +checksum = "f1aa91ad26c62b93d73e65f9ce7ebd04459c4bad086599348846a81988d6faa4" dependencies = [ - "aes-gcm-siv", - "base64 0.22.1", - "bincode", - "bytemuck", - "bytemuck_derive", - "curve25519-dalek 4.1.3", - "itertools 0.12.1", - "js-sys", + "ahash 0.8.12", + "hash-db", "lazy_static", - "merlin", - "num-derive 0.4.2", - "num-traits", + "memory-db", + "nohash-hasher", + "parity-scale-codec", + "parking_lot", "rand 0.8.5", - "serde", - "serde_derive", - "serde_json", - "sha3", - "solana-derivation-path", - "solana-instruction", - "solana-pubkey", - "solana-sdk-ids", - "solana-seed-derivable", - "solana-seed-phrase", - "solana-signature", - "solana-signer", - "subtle", - "thiserror 2.0.12", - "wasm-bindgen", - "zeroize", + "scale-info", + "schnellru", + "sp-core", + "sp-externalities", + "sp-std", + "thiserror 1.0.69", + "tracing", + "trie-db", + "trie-root", ] [[package]] -name = "soroban-rs" -version = "0.2.4" +name = "sp-wasm-interface" +version = "20.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790ecbbc2f864561c405f485383605960eb49353e2bd9f70b29b6ac9feb70b72" +checksum = "9ef97172c42eb4c6c26506f325f48463e9bc29b2034a587f1b9e48c751229bee" dependencies = [ - "async-trait", - "ed25519-dalek 2.1.1", - "hex", - "rand 0.9.1", - "sha2 0.10.9", - "soroban-rs-macros", - "stellar-rpc-client", - "stellar-strkey 0.0.11", - "stellar-xdr", - "tokio", + "anyhow", + "impl-trait-for-tuples", + "log", + "parity-scale-codec", + "sp-std", + "wasmtime", ] [[package]] -name = "soroban-rs-macros" -version = "0.2.3" +name = "sp-weights" +version = "30.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e2fa346eb4b437b64abfb3a1972d1bdaa7e868bedfb352d5a4638f4ac586365" +checksum = "9af6c661fe3066b29f9e1d258000f402ff5cc2529a9191972d214e5871d0ba87" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", + "bounded-collections", + "parity-scale-codec", + "scale-info", + "serde", + "smallvec", + "sp-arithmetic", + "sp-debug-derive", + "sp-std", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "spinning_top" version = "0.3.0" @@ -8674,7 +11327,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9e8418ea6269dcfb01c712f0444d2c75542c04448b480e87de59d2865edc750" dependencies = [ - "quote", + "quote 1.0.40", "spl-discriminator-syn", "syn 2.0.101", ] @@ -8686,7 +11339,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c1f05593b7ca9eac7caca309720f2eafb96355e037e6d373b909a80fe7b69b9" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "sha2 0.10.9", "syn 2.0.101", "thiserror 1.0.69", @@ -8797,7 +11450,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d375dd76c517836353e093c2dbb490938ff72821ab568b545fd30ab3256b3e" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "sha2 0.10.9", "syn 2.0.101", ] @@ -8809,7 +11462,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2539e259c66910d78593475540e8072f0b10f0f61d7607bbf7593899ed52d0" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "sha2 0.10.9", "syn 2.0.101", ] @@ -9219,6 +11872,21 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "ss58-registry" +version = "1.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19409f13998e55816d1c728395af0b52ec066206341d939e22e7766df9b494b8" +dependencies = [ + "Inflector", + "num-format", + "proc-macro2", + "quote 1.0.40", + "serde", + "serde_json", + "unicode-xid 0.2.6", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -9241,8 +11909,8 @@ dependencies = [ "hex", "http 1.3.1", "itertools 0.10.5", - "jsonrpsee-core", - "jsonrpsee-http-client", + "jsonrpsee-core 0.20.4", + "jsonrpsee-http-client 0.20.4", "serde", "serde-aux", "serde_json", @@ -9340,7 +12008,7 @@ checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ "heck", "proc-macro2", - "quote", + "quote 1.0.40", "rustversion", "syn 2.0.101", ] @@ -9353,17 +12021,172 @@ checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" dependencies = [ "heck", "proc-macro2", - "quote", + "quote 1.0.40", "rustversion", "syn 2.0.101", ] +[[package]] +name = "substrate-bip39" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b564c293e6194e8b222e52436bcb99f60de72043c7f845cf6c4406db4df121" +dependencies = [ + "hmac 0.12.1", + "pbkdf2 0.12.2", + "schnorrkel", + "sha2 0.10.9", + "zeroize", +] + [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "subxt" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a160cba1edbf3ec4fbbeaea3f1a185f70448116a6bccc8276bb39adb3b3053bd" +dependencies = [ + "async-trait", + "derive-where", + "either", + "frame-metadata 16.0.0", + "futures", + "hex", + "impl-serde", + "instant", + "jsonrpsee 0.22.5", + "parity-scale-codec", + "primitive-types", + "reconnecting-jsonrpsee-ws-client", + "scale-bits", + "scale-decode", + "scale-encode", + "scale-info", + "scale-value", + "serde", + "serde_json", + "sp-crypto-hashing", + "subxt-core", + "subxt-lightclient", + "subxt-macro", + "subxt-metadata", + "thiserror 1.0.69", + "tokio-util", + "tracing", + "url", +] + +[[package]] +name = "subxt-codegen" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d703dca0905cc5272d7cc27a4ac5f37dcaae7671acc7fef0200057cc8c317786" +dependencies = [ + "frame-metadata 16.0.0", + "heck", + "hex", + "jsonrpsee 0.22.5", + "parity-scale-codec", + "proc-macro2", + "quote 1.0.40", + "scale-info", + "scale-typegen", + "subxt-metadata", + "syn 2.0.101", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "subxt-core" +version = "0.37.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3af3b36405538a36b424d229dc908d1396ceb0994c90825ce928709eac1a159a" +dependencies = [ + "base58", + "blake2", + "derive-where", + "frame-metadata 16.0.0", + "hashbrown 0.14.5", + "hex", + "impl-serde", + "parity-scale-codec", + "primitive-types", + "scale-bits", + "scale-decode", + "scale-encode", + "scale-info", + "scale-value", + "serde", + "serde_json", + "sp-core", + "sp-crypto-hashing", + "sp-runtime", + "subxt-metadata", + "tracing", +] + +[[package]] +name = "subxt-lightclient" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d9406fbdb9548c110803cb8afa750f8b911d51eefdf95474b11319591d225d9" +dependencies = [ + "futures", + "futures-util", + "serde", + "serde_json", + "smoldot-light", + "thiserror 1.0.69", + "tokio", + "tokio-stream", + "tracing", +] + +[[package]] +name = "subxt-macro" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c195f803d70687e409aba9be6c87115b5da8952cd83c4d13f2e043239818fcd" +dependencies = [ + "darling 0.20.11", + "parity-scale-codec", + "proc-macro-error", + "quote 1.0.40", + "scale-typegen", + "subxt-codegen", + "syn 2.0.101", +] + +[[package]] +name = "subxt-metadata" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "738be5890fdeff899bbffff4d9c0f244fe2a952fb861301b937e3aa40ebb55da" +dependencies = [ + "frame-metadata 16.0.0", + "hashbrown 0.14.5", + "parity-scale-codec", + "scale-info", + "sp-crypto-hashing", +] + +[[package]] +name = "syn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" +dependencies = [ + "quote 0.3.15", + "synom", + "unicode-xid 0.0.4", +] + [[package]] name = "syn" version = "1.0.109" @@ -9371,7 +12194,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "unicode-ident", ] @@ -9382,7 +12205,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "unicode-ident", ] @@ -9394,7 +12217,7 @@ checksum = "4560533fbd6914b94a8fb5cc803ed6801c3455668db3b810702c57612bac9412" dependencies = [ "paste", "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -9413,6 +12236,15 @@ dependencies = [ "futures-core", ] +[[package]] +name = "synom" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" +dependencies = [ + "unicode-xid 0.0.4", +] + [[package]] name = "synstructure" version = "0.12.6" @@ -9420,9 +12252,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 1.0.109", - "unicode-xid", + "unicode-xid 0.2.6", ] [[package]] @@ -9432,7 +12264,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -9509,6 +12341,12 @@ dependencies = [ "xattr", ] +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "task-local-extensions" version = "0.1.4" @@ -9527,7 +12365,7 @@ dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", - "rustix", + "rustix 1.0.7", "windows-sys 0.59.0", ] @@ -9587,7 +12425,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -9598,7 +12436,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -9713,7 +12551,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -9737,6 +12575,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.4", + "rustls-pki-types", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.2" @@ -9767,8 +12616,10 @@ checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" dependencies = [ "futures-util", "log", + "native-tls", "rustls 0.21.12", "tokio", + "tokio-native-tls", "tokio-rustls 0.24.1", "tungstenite 0.20.1", "webpki-roots 0.25.4", @@ -9798,6 +12649,7 @@ checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", "pin-project-lite", "tokio", @@ -9812,6 +12664,18 @@ dependencies = [ "serde", ] +[[package]] +name = "toml" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + [[package]] name = "toml_datetime" version = "0.6.9" @@ -9831,9 +12695,16 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", + "toml_write", "winnow 0.7.10", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tower" version = "0.4.13" @@ -9915,7 +12786,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -9936,7 +12807,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" dependencies = [ "tracing", - "tracing-subscriber", + "tracing-subscriber 0.3.19", ] [[package]] @@ -9948,15 +12819,94 @@ dependencies = [ "tracing", ] +[[package]] +name = "tracing-log" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" +dependencies = [ + "ansi_term", + "chrono", + "lazy_static", + "matchers", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log 0.1.4", + "tracing-serde", +] + [[package]] name = "tracing-subscriber" version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ + "nu-ansi-term", "sharded-slab", + "smallvec", "thread_local", "tracing-core", + "tracing-log 0.2.0", +] + +[[package]] +name = "trie-db" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff28e0f815c2fea41ebddf148e008b077d2faddb026c9555b29696114d602642" +dependencies = [ + "hash-db", + "hashbrown 0.13.2", + "log", + "rustc-hex", + "smallvec", +] + +[[package]] +name = "trie-root" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ed310ef5ab98f5fa467900ed906cb9232dd5376597e00fd4cba2a449d06c0b" +dependencies = [ + "hash-db", ] [[package]] @@ -9977,6 +12927,7 @@ dependencies = [ "http 0.2.12", "httparse", "log", + "native-tls", "rand 0.8.5", "rustls 0.21.12", "sha1", @@ -10006,12 +12957,39 @@ dependencies = [ "utf-8", ] +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "digest 0.10.7", + "rand 0.8.5", + "static_assertions", +] + [[package]] name = "typenum" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "typewit" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb77c29baba9e4d3a6182d51fa75e3215c7fd1dab8f4ea9d107c716878e55fc0" +dependencies = [ + "typewit_proc_macros", +] + +[[package]] +name = "typewit_proc_macros" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" + [[package]] name = "ucd-trie" version = "0.1.7" @@ -10058,12 +13036,27 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-width" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +[[package]] +name = "unicode-xid" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -10080,6 +13073,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "unroll" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ad948c1cb799b1a70f836077721a92a35ac177d4daddf4c20a633786d4cf618" +dependencies = [ + "quote 1.0.40", + "syn 1.0.109", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -10144,7 +13147,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a77d306bc75294fd52f3e99b13ece67c02c1a2789190a6f31d32f736624326f7" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "regex", "syn 2.0.101", ] @@ -10196,7 +13199,7 @@ dependencies = [ "once_cell", "proc-macro-error2", "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -10238,6 +13241,28 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "w3f-bls" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6bfb937b3d12077654a9e43e32a4e9c20177dd9fea0f3aba673e7840bb54f32" +dependencies = [ + "ark-bls12-377", + "ark-bls12-381", + "ark-ec", + "ark-ff 0.4.2", + "ark-serialize 0.4.2", + "ark-serialize-derive", + "arrayref", + "digest 0.10.7", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_core 0.6.4", + "sha2 0.10.9", + "sha3 0.10.8", + "zeroize", +] + [[package]] name = "wait-timeout" version = "0.2.1" @@ -10308,7 +13333,7 @@ dependencies = [ "bumpalo", "log", "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", "wasm-bindgen-shared", ] @@ -10332,7 +13357,7 @@ version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ - "quote", + "quote 1.0.40", "wasm-bindgen-macro-support", ] @@ -10343,7 +13368,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", "wasm-bindgen-backend", "wasm-bindgen-shared", @@ -10358,6 +13383,201 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmi" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8281d1d660cdf54c76a3efa9ddd0c270cada1383a995db3ccb43d166456c7" +dependencies = [ + "smallvec", + "spin", + "wasmi_arena", + "wasmi_core", + "wasmparser-nostd", +] + +[[package]] +name = "wasmi_arena" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "104a7f73be44570cac297b3035d76b169d6599637631cf37a1703326a0727073" + +[[package]] +name = "wasmi_core" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf1a7db34bff95b85c261002720c00c3a6168256dcb93041d3fa2054d19856a" +dependencies = [ + "downcast-rs", + "libm", + "num-traits", + "paste", +] + +[[package]] +name = "wasmparser" +version = "0.102.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48134de3d7598219ab9eaf6b91b15d8e50d31da76b8519fe4ecfcec2cf35104b" +dependencies = [ + "indexmap 1.9.3", + "url", +] + +[[package]] +name = "wasmparser-nostd" +version = "0.100.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5a015fe95f3504a94bb1462c717aae75253e39b9dd6c3fb1062c934535c64aa" +dependencies = [ + "indexmap-nostd", +] + +[[package]] +name = "wasmtime" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f907fdead3153cb9bfb7a93bbd5b62629472dc06dee83605358c64c52ed3dda9" +dependencies = [ + "anyhow", + "bincode", + "cfg-if", + "indexmap 1.9.3", + "libc", + "log", + "object 0.30.4", + "once_cell", + "paste", + "psm", + "serde", + "target-lexicon", + "wasmparser", + "wasmtime-environ", + "wasmtime-jit", + "wasmtime-runtime", + "windows-sys 0.45.0", +] + +[[package]] +name = "wasmtime-asm-macros" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b9daa7c14cd4fa3edbf69de994408d5f4b7b0959ac13fa69d465f6597f810d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "wasmtime-environ" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a990198cee4197423045235bf89d3359e69bd2ea031005f4c2d901125955c949" +dependencies = [ + "anyhow", + "cranelift-entity", + "gimli 0.27.3", + "indexmap 1.9.3", + "log", + "object 0.30.4", + "serde", + "target-lexicon", + "thiserror 1.0.69", + "wasmparser", + "wasmtime-types", +] + +[[package]] +name = "wasmtime-jit" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de48df552cfca1c9b750002d3e07b45772dd033b0b206d5c0968496abf31244" +dependencies = [ + "addr2line 0.19.0", + "anyhow", + "bincode", + "cfg-if", + "cpp_demangle", + "gimli 0.27.3", + "log", + "object 0.30.4", + "rustc-demangle", + "serde", + "target-lexicon", + "wasmtime-environ", + "wasmtime-jit-icache-coherence", + "wasmtime-runtime", + "windows-sys 0.45.0", +] + +[[package]] +name = "wasmtime-jit-debug" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e0554b84c15a27d76281d06838aed94e13a77d7bf604bbbaf548aa20eb93846" +dependencies = [ + "once_cell", +] + +[[package]] +name = "wasmtime-jit-icache-coherence" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecae978b13f7f67efb23bd827373ace4578f2137ec110bbf6a4a7cde4121bbd" +dependencies = [ + "cfg-if", + "libc", + "windows-sys 0.45.0", +] + +[[package]] +name = "wasmtime-runtime" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658cf6f325232b6760e202e5255d823da5e348fdea827eff0a2a22319000b441" +dependencies = [ + "anyhow", + "cc", + "cfg-if", + "indexmap 1.9.3", + "libc", + "log", + "mach", + "memfd", + "memoffset 0.8.0", + "paste", + "rand 0.8.5", + "rustix 0.36.17", + "wasmtime-asm-macros", + "wasmtime-environ", + "wasmtime-jit-debug", + "windows-sys 0.45.0", +] + +[[package]] +name = "wasmtime-types" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f6fffd2a1011887d57f07654dd112791e872e3ff4a2e626aee8059ee17f06f" +dependencies = [ + "cranelift-entity", + "serde", + "thiserror 1.0.69", + "wasmparser", +] + [[package]] name = "wasmtimer" version = "0.4.1" @@ -10543,7 +13763,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -10554,7 +13774,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -10994,6 +14214,18 @@ dependencies = [ "tap", ] +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek 4.1.3", + "rand_core 0.6.4", + "serde", + "zeroize", +] + [[package]] name = "x509-parser" version = "0.14.0" @@ -11019,9 +14251,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e" dependencies = [ "libc", - "rustix", + "rustix 1.0.7", ] +[[package]] +name = "yap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff4524214bc4629eba08d78ceb1d6507070cc0bcbbed23af74e19e6e924a24cf" + [[package]] name = "yoke" version = "0.8.0" @@ -11041,7 +14279,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", "synstructure 0.13.2", ] @@ -11062,7 +14300,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -11082,7 +14320,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", "synstructure 0.13.2", ] @@ -11103,7 +14341,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] @@ -11136,7 +14374,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.40", "syn 2.0.101", ] diff --git a/Cargo.toml b/Cargo.toml index 755cd9a09..f4bfb3485 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,82 +15,104 @@ opt-level = 0 overflow-checks = true panic = "unwind" +[patch."https://github.com/input-output-hk/midnight-ledger-prototype"] +mn-ledger-storage = { git = "https://github.com/midnightntwrk/midnight-ledger-prototype", package = "midnight-storage", tag = "ledger-4.0.0" } +coin-structure = { git = "https://github.com/midnightntwrk/midnight-ledger-prototype", package = "midnight-coin-structure", tag = "ledger-4.0.0" } +onchain-runtime = { git = "https://github.com/midnightntwrk/midnight-ledger-prototype", package = "midnight-onchain-runtime", tag = "ledger-4.0.0" } +midnight-serialize = { git = "https://github.com/midnightntwrk/midnight-ledger-prototype", tag = "ledger-4.0.0" } +base-crypto = { git = "https://github.com/midnightntwrk/midnight-ledger-prototype", package = "midnight-base-crypto", tag = "ledger-4.0.0" } +transient-crypto = { git = "https://github.com/midnightntwrk/midnight-ledger-prototype", package = "midnight-transient-crypto", tag = "ledger-4.0.0" } + [dependencies] -actix-web = "4" -log = "0.4" -simplelog = "0.12" -prometheus = "0.13" -lazy_static = "1.5" -dotenvy = "0.15" -thiserror = "2" -async-trait = "0.1" +actix-governor = "0.8" actix-rt = "2.0.0" +actix-web = "4" alloy = { version = "0.9", features = ["full"] } -serde_json = "1" -strum = { version = "0.27", default-features = false, features = ["derive"] } -strum_macros = "0.27" -serde = { version = "1.0", features = ["derive", "alloc"] } -num_enum = { version = "0.7", default-features = false } -once_cell = "1.17" -regex = "1" -futures = "0.3" -uuid = { version = "1.11", features = ["v4"] } +apalis = { version = "0.7", features = [ + "limit", + "retry", + "catch-panic", + "timeout", +] } +apalis-cron = { version = "0.7" } +apalis-redis = { version = "0.7" } +async-trait = "0.1" +backoff = { version = "0.4.0", features = ["tokio"] } +base64 = { version = "0.22" } +bech32 = "0.11.0" +bincode = { version = "1.3.3" } +bs58 = "0.5" +bytes = { version = "1.9" } chrono = "0.4" -eyre = "0.6" color-eyre = "0.6" -apalis = { version = "0.7", features = ["limit", "retry", "catch-panic", "timeout"] } -apalis-redis = { version = "0.7" } -apalis-cron = { version = "0.7" } -redis = { version = "0.31" } -tokio = { version = "1.43", features = ["sync", "io-util", "time"] } -rand = "0.9" +dashmap = { version = "6.1" } +dotenvy = "0.15" +ed25519-dalek = "2.1" +eyre = "0.6" +futures = "0.3" +futures-util = "0.3" +google-cloud-auth = "0.20.0" +governor = "0.8.0" +hex = { version = "0.4" } +hmac = { version = "0.12" } +http = { version = "1.3.1" } +itertools = "0.12.0" # Required by midnight +k256 = { version = "0.13" } +lazy_static = "1.5" +libsodium-sys = "0.2.7" +log = "0.4" +midnight-ledger-prototype = { git = "https://github.com/midnightntwrk/midnight-ledger-prototype", package = "midnight-ledger", tag = "ledger-4.0.0" } +midnight-node-ledger-helpers = { git = "https://github.com/midnightntwrk/midnight-node", package = "midnight-node-ledger-helpers", tag = "node-0.12.0" } +midnight-node-res = { git = "https://github.com/midnightntwrk/midnight-node", package = "midnight-node-res", tag = "node-0.12.0" } +mpl-token-metadata = { version = "5.1" } +num_enum = { version = "0.7", default-features = false } +once_cell = "1.17" +oz-keystore = { version = "0.1.4" } +p256 = { version = "0.13.2" } parking_lot = "0.12" -tower = "0.5" -oz-keystore = { version = "0.1.4"} -hex = { version = "0.4"} -bytes = { version = "1.9" } +pem = { version = "3" } +prometheus = "0.13" +rand = "0.9" +redis = { version = "0.31" } +regex = "1" reqwest = { version = "0.12", features = ["json"] } -base64 = { version = "0.22" } -hmac = { version = "0.12" } +secrets = { version = "1.2" } +serde = { version = "1.0", features = ["derive", "alloc"] } +serde_json = "1" sha2 = { version = "0.10" } -dashmap = { version = "6.1" } -actix-governor = "0.8" -solana-sdk = { version = "2.2" } +simple_asn1 = { version = "0.6" } +simplelog = "0.12" solana-client = { version = "2.2" } +solana-sdk = { version = "2.2" } +soroban-rs = "0.2.4" +spl-associated-token-account = "6.0.0" spl-token = { version = "8" } spl-token-2022 = { version = "8" } -mpl-token-metadata = { version = "5.1" } +stellar-strkey = "0.0.12" +strum = { version = "0.27", default-features = false, features = ["derive"] } +strum_macros = "0.27" +subtle = "2.6" +subxt = { version = "0.37.0", features = ["substrate-compat"] } sysinfo = "0.35" -bincode = { version = "1.3" } -bs58 = "0.5" -spl-associated-token-account = "6.0.0" -itertools = "0.14.0" +thiserror = "2" +tokio = { version = "1.43", features = ["sync", "io-util", "time"] } +tokio-tungstenite = { version = "0.20", features = ["native-tls"] } +tower = "0.5" +utoipa = { version = "5.3", features = ["actix_extras"] } +uuid = { version = "1.11", features = ["v4"] } validator = { version = "0.20", features = ["derive"] } vaultrs = { version = "0.7.4" } -utoipa = { version = "5.3", features = ["actix_extras"] } -secrets = { version = "1.2"} -libsodium-sys = "0.2.7" zeroize = "1.8" -subtle = "2.6" -ed25519-dalek = "2.1" -stellar-strkey = "0.0.12" -soroban-rs = "0.2.4" -p256 = { version = "0.13.2" } -google-cloud-auth = "0.20.0" -http = { version = "1.3.1" } -pem = { version = "3" } -simple_asn1 = { version = "0.6" } -k256 = { version = "0.13" } [dev-dependencies] cargo-llvm-cov = "0.6" +clap = { version = "4.4", features = ["derive"] } mockall = { version = "0.13" } mockito = "1.6.1" proptest = "1.6.0" rand = "0.9.0" -tempfile = "3.2" serial_test = "3.2" -clap = { version = "4.4", features = ["derive"] } +tempfile = "3.2" wiremock = "0.6" [[bin]] diff --git a/config/networks/midnight.json b/config/networks/midnight.json new file mode 100644 index 000000000..e121d1387 --- /dev/null +++ b/config/networks/midnight.json @@ -0,0 +1,21 @@ +{ + "networks": [ + { + "type": "midnight", + "network": "testnet", + "rpc_urls": [ + "wss://rpc.testnet-02.midnight.network/" + ], + "explorer_urls": [ + "https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Frpc.testnet-02.midnight.network#/explorer" + ], + "indexer_urls": { + "ws": "wss://indexer.testnet-02.midnight.network/api/v1/graphql/ws", + "http": "https://indexer.testnet-02.midnight.network/api/v1/graphql" + }, + "prover_url": "http://localhost:6300", + "average_blocktime_ms": 6000, + "is_testnet": true + } + ] +} diff --git a/src/api/routes/relayer.rs b/src/api/routes/relayer.rs index b44339030..e15bb64d3 100644 --- a/src/api/routes/relayer.rs +++ b/src/api/routes/relayer.rs @@ -191,8 +191,8 @@ mod tests { jobs::{JobProducer, Queue}, repositories::{ InMemoryNetworkRepository, InMemoryNotificationRepository, InMemoryRelayerRepository, - InMemorySignerRepository, InMemoryTransactionCounter, InMemoryTransactionRepository, - RelayerRepositoryStorage, + InMemorySignerRepository, InMemorySyncState, InMemoryTransactionCounter, + InMemoryTransactionRepository, RelayerRepositoryStorage, }, }; use actix_web::{http::StatusCode, test, App}; @@ -209,6 +209,7 @@ mod tests { notification_repository: Arc::new(InMemoryNotificationRepository::new()), network_repository: Arc::new(InMemoryNetworkRepository::new()), transaction_counter_store: Arc::new(InMemoryTransactionCounter::new()), + sync_state_store: Arc::new(InMemorySyncState::new()), job_producer: Arc::new(JobProducer::new(Queue::setup().await.unwrap())), } } diff --git a/src/bootstrap/config_processor.rs b/src/bootstrap/config_processor.rs index ca553bb87..ce26de8b5 100644 --- a/src/bootstrap/config_processor.rs +++ b/src/bootstrap/config_processor.rs @@ -301,8 +301,9 @@ async fn process_relayers( .find(|s| s.id == repo_model.signer_id) .ok_or_else(|| eyre::eyre!("Signer not found"))?; let network_type = repo_model.network_type; - let signer_service = SignerFactory::create_signer(&network_type, signer_model) - .wrap_err("Failed to create signer service")?; + let signer_service = + SignerFactory::create_signer(&network_type, signer_model, &repo_model.network) + .wrap_err("Failed to create signer service")?; let address = signer_service.address().await?; repo_model.address = address.to_string(); @@ -353,8 +354,8 @@ mod tests { models::{NetworkType, PlainOrEnvValue, SecretString}, repositories::{ InMemoryNetworkRepository, InMemoryNotificationRepository, InMemoryRelayerRepository, - InMemorySignerRepository, InMemoryTransactionCounter, InMemoryTransactionRepository, - RelayerRepositoryStorage, + InMemorySignerRepository, InMemorySyncState, InMemoryTransactionCounter, + InMemoryTransactionRepository, RelayerRepositoryStorage, }, }; use serde_json::json; @@ -392,6 +393,7 @@ mod tests { notification_repository: Arc::new(InMemoryNotificationRepository::default()), network_repository: Arc::new(InMemoryNetworkRepository::default()), transaction_counter_store: Arc::new(InMemoryTransactionCounter::default()), + sync_state_store: Arc::new(InMemorySyncState::default()), job_producer: Arc::new(mock_job_producer), } } @@ -994,6 +996,7 @@ mod tests { let network_repo = Arc::new(InMemoryNetworkRepository::default()); let transaction_repo = Arc::new(InMemoryTransactionRepository::default()); let transaction_counter = Arc::new(InMemoryTransactionCounter::default()); + let sync_state_store = Arc::new(InMemorySyncState::default()); // Create a mock job producer let mut mock_job_producer = MockJobProducerTrait::new(); @@ -1019,6 +1022,7 @@ mod tests { network_repository: network_repo.clone(), transaction_repository: transaction_repo.clone(), transaction_counter_store: transaction_counter.clone(), + sync_state_store: sync_state_store.clone(), job_producer: job_producer.clone(), }); diff --git a/src/bootstrap/initialize_app_state.rs b/src/bootstrap/initialize_app_state.rs index 9d5fc4ff6..5b0711066 100644 --- a/src/bootstrap/initialize_app_state.rs +++ b/src/bootstrap/initialize_app_state.rs @@ -7,8 +7,8 @@ use crate::{ models::{AppState, DefaultAppState}, repositories::{ InMemoryNetworkRepository, InMemoryNotificationRepository, InMemoryRelayerRepository, - InMemorySignerRepository, InMemoryTransactionCounter, InMemoryTransactionRepository, - RelayerRepositoryStorage, + InMemorySignerRepository, InMemorySyncState, InMemoryTransactionCounter, + InMemoryTransactionRepository, RelayerRepositoryStorage, }, }; use actix_web::web; @@ -35,6 +35,7 @@ pub async fn initialize_app_state() -> Result> { let notification_repository = Arc::new(InMemoryNotificationRepository::new()); let network_repository = Arc::new(InMemoryNetworkRepository::new()); let transaction_counter_store = Arc::new(InMemoryTransactionCounter::new()); + let sync_state_store = Arc::new(InMemorySyncState::new()); let queue = Queue::setup().await?; let job_producer = Arc::new(jobs::JobProducer::new(queue.clone())); @@ -45,6 +46,7 @@ pub async fn initialize_app_state() -> Result> { notification_repository, network_repository, transaction_counter_store, + sync_state_store, job_producer, }); diff --git a/src/config/config_file/mod.rs b/src/config/config_file/mod.rs index 5bd0d6c15..d343f9692 100644 --- a/src/config/config_file/mod.rs +++ b/src/config/config_file/mod.rs @@ -39,8 +39,8 @@ pub use notification::*; pub mod network; pub use network::{ - EvmNetworkConfig, NetworkConfigCommon, NetworkFileConfig, NetworksFileConfig, - SolanaNetworkConfig, StellarNetworkConfig, + EvmNetworkConfig, MidnightNetworkConfig, NetworkConfigCommon, NetworkFileConfig, + NetworksFileConfig, SolanaNetworkConfig, StellarNetworkConfig, }; #[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)] @@ -49,6 +49,7 @@ pub enum ConfigFileNetworkType { Evm, Stellar, Solana, + Midnight, } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/src/config/config_file/network/collection.rs b/src/config/config_file/network/collection.rs index 4c823f61f..d4bc947ed 100644 --- a/src/config/config_file/network/collection.rs +++ b/src/config/config_file/network/collection.rs @@ -189,6 +189,11 @@ impl NetworksFileConfig { resolver.resolve_stellar_inheritance(config, network_name, parent_name)?; Ok(NetworkFileConfig::Stellar(resolved_config)) } + NetworkFileConfig::Midnight(config) => { + let resolved_config = + resolver.resolve_midnight_inheritance(config, network_name, parent_name)?; + Ok(NetworkFileConfig::Midnight(resolved_config)) + } } } @@ -368,13 +373,14 @@ mod tests { create_evm_network_wrapped("evm-1"), create_solana_network_wrapped("solana-1"), create_stellar_network_wrapped("stellar-1"), + create_midnight_network_wrapped("midnight-1"), ]; let config = NetworksFileConfig::new(networks); assert!(config.is_ok()); let config = config.unwrap(); - assert_eq!(config.networks.len(), 3); - assert_eq!(config.network_map.len(), 3); + assert_eq!(config.networks.len(), 4); + assert_eq!(config.network_map.len(), 4); assert!(config .network_map .contains_key(&(ConfigFileNetworkType::Evm, "evm-1".to_string()))); @@ -384,6 +390,9 @@ mod tests { assert!(config .network_map .contains_key(&(ConfigFileNetworkType::Stellar, "stellar-1".to_string()))); + assert!(config + .network_map + .contains_key(&(ConfigFileNetworkType::Midnight, "midnight-1".to_string()))); } #[test] @@ -642,13 +651,14 @@ mod tests { create_solana_network_wrapped("solana-parent"), create_solana_network_wrapped_with_parent("solana-child", "solana-parent"), create_stellar_network_wrapped("stellar-standalone"), + create_midnight_network_wrapped("midnight-standalone"), ]; let config = NetworksFileConfig::new(networks).unwrap(); let flattened = config.flatten(); assert!(flattened.is_ok()); let flattened = flattened.unwrap(); - assert_eq!(flattened.networks.len(), 5); + assert_eq!(flattened.networks.len(), 6); } #[test] @@ -678,9 +688,10 @@ mod tests { create_evm_network_wrapped("test1"), create_solana_network_wrapped("test2"), create_stellar_network_wrapped("test3"), + create_midnight_network_wrapped("test4"), ]; let config = NetworksFileConfig::new(networks).unwrap(); - assert_eq!(config.len(), 3); + assert_eq!(config.len(), 4); } #[test] @@ -700,6 +711,7 @@ mod tests { create_evm_network_wrapped("evm-2"), create_solana_network_wrapped("solana-1"), create_stellar_network_wrapped("stellar-1"), + create_midnight_network_wrapped("midnight-1"), ]; let config = NetworksFileConfig::new(networks).unwrap(); @@ -717,6 +729,11 @@ mod tests { .networks_by_type(ConfigFileNetworkType::Stellar) .collect(); assert_eq!(stellar_networks.len(), 1); + + let midnight_networks: Vec<_> = config + .networks_by_type(ConfigFileNetworkType::Midnight) + .collect(); + assert_eq!(midnight_networks.len(), 1); } #[test] @@ -736,14 +753,16 @@ mod tests { create_evm_network_wrapped("alpha"), create_solana_network_wrapped("beta"), create_stellar_network_wrapped("gamma"), + create_midnight_network_wrapped("delta"), ]; let config = NetworksFileConfig::new(networks).unwrap(); let names: Vec<_> = config.network_names().collect(); - assert_eq!(names.len(), 3); + assert_eq!(names.len(), 4); assert!(names.contains(&"alpha")); assert!(names.contains(&"beta")); assert!(names.contains(&"gamma")); + assert!(names.contains(&"delta")); } #[test] @@ -916,12 +935,13 @@ mod tests { create_evm_network_wrapped("测试网络"), create_solana_network_wrapped("тестовая-сеть"), create_stellar_network_wrapped("réseau-test"), + create_midnight_network_wrapped("❣⩪❅♒Ⱎ"), ]; let config = NetworksFileConfig::new(networks); assert!(config.is_ok()); let config = config.unwrap(); - assert_eq!(config.len(), 3); + assert_eq!(config.len(), 4); assert!(config .get_network(ConfigFileNetworkType::Evm, "测试网络") .is_some()); @@ -931,6 +951,9 @@ mod tests { assert!(config .get_network(ConfigFileNetworkType::Stellar, "réseau-test") .is_some()); + assert!(config + .get_network(ConfigFileNetworkType::Midnight, "❣⩪❅♒Ⱎ") + .is_some()); } #[test] @@ -939,12 +962,13 @@ mod tests { create_evm_network_wrapped("test-network_123"), create_solana_network_wrapped("test.network.with.dots"), create_stellar_network_wrapped("test@network#with$symbols"), + create_midnight_network_wrapped("test!network+123"), ]; let config = NetworksFileConfig::new(networks); assert!(config.is_ok()); let config = config.unwrap(); - assert_eq!(config.len(), 3); + assert_eq!(config.len(), 4); } #[test] @@ -992,13 +1016,14 @@ mod tests { create_evm_network_wrapped("mainnet"), create_solana_network_wrapped("mainnet"), create_stellar_network_wrapped("mainnet"), + create_midnight_network_wrapped("mainnet"), ]; let result = NetworksFileConfig::new(networks); assert!(result.is_ok()); let config = result.unwrap(); - assert_eq!(config.networks.len(), 3); - assert_eq!(config.network_map.len(), 3); + assert_eq!(config.networks.len(), 4); + assert_eq!(config.network_map.len(), 4); // Verify we can retrieve each network by type and name assert!(config @@ -1010,6 +1035,9 @@ mod tests { assert!(config .get_network(ConfigFileNetworkType::Stellar, "mainnet") .is_some()); + assert!(config + .get_network(ConfigFileNetworkType::Midnight, "mainnet") + .is_some()); } #[test] @@ -1123,6 +1151,7 @@ mod tests { create_evm_network_wrapped("network-0"), create_solana_network_wrapped("network-1"), create_stellar_network_wrapped("network-2"), + create_midnight_network_wrapped("network-3"), ]; let config = NetworksFileConfig::new(networks).unwrap(); @@ -1137,6 +1166,10 @@ mod tests { let network_2 = config.get(2); assert!(network_2.is_some()); assert_eq!(network_2.unwrap().network_name(), "network-2"); + + let network_3 = config.get(3); + assert!(network_3.is_some()); + assert_eq!(network_3.unwrap().network_name(), "network-3"); } #[test] diff --git a/src/config/config_file/network/file_loading.rs b/src/config/config_file/network/file_loading.rs index c87e739cb..55ec9af4c 100644 --- a/src/config/config_file/network/file_loading.rs +++ b/src/config/config_file/network/file_loading.rs @@ -880,9 +880,7 @@ mod tests { })); } - let large_json = json!({ - "networks": networks_array - }); + let large_json = json!({ "networks": networks_array }); create_temp_file(&dir, "large.json", &large_json.to_string()); diff --git a/src/config/config_file/network/inheritance.rs b/src/config/config_file/network/inheritance.rs index 24f1f10b9..bd91b6e27 100644 --- a/src/config/config_file/network/inheritance.rs +++ b/src/config/config_file/network/inheritance.rs @@ -18,8 +18,8 @@ //! 3. **Merging**: Combine child with resolved parent configuration use super::{ - ConfigFileNetworkType, EvmNetworkConfig, NetworkFileConfig, SolanaNetworkConfig, - StellarNetworkConfig, + ConfigFileNetworkType, EvmNetworkConfig, MidnightNetworkConfig, NetworkFileConfig, + SolanaNetworkConfig, StellarNetworkConfig, }; use crate::config::ConfigFileError; @@ -109,6 +109,13 @@ impl<'a> InheritanceResolver<'a> { Stellar, "Stellar" ); + impl_inheritance_resolver!( + resolve_midnight_inheritance, + MidnightNetworkConfig, + Midnight, + Midnight, + "Midnight" + ); } #[cfg(test)] diff --git a/src/config/config_file/network/midnight.rs b/src/config/config_file/network/midnight.rs new file mode 100644 index 000000000..108dcb054 --- /dev/null +++ b/src/config/config_file/network/midnight.rs @@ -0,0 +1,90 @@ +//! Midnight Network Configuration +//! +//! This module provides configuration support for Midnight blockchain networks. + +use super::common::NetworkConfigCommon; +use crate::config::ConfigFileError; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] +pub struct IndexerUrls { + pub http: String, + pub ws: String, +} + +/// Configuration specific to Midnight networks. +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(deny_unknown_fields)] +pub struct MidnightNetworkConfig { + /// Common network fields. + #[serde(flatten)] + pub common: NetworkConfigCommon, + // Midnight-specific fields + pub indexer_urls: IndexerUrls, // URL for the indexer server (ws, http) + pub prover_url: String, // URL for the prover server + pub commitment_tree_ttl: Option, // How long to cache Merkle roots +} + +impl MidnightNetworkConfig { + /// Validates the specific configuration fields for a Midnight network. + /// + /// # Returns + /// - `Ok(())` if the Midnight configuration is valid. + /// - `Err(ConfigFileError)` if validation fails (e.g., missing fields, invalid URLs). + pub fn validate(&self) -> Result<(), ConfigFileError> { + self.common.validate()?; + + // Validate indexer URLs + reqwest::Url::parse(&self.indexer_urls.http).map_err(|_| { + ConfigFileError::InvalidFormat(format!( + "Invalid indexer HTTP URL: {}", + self.indexer_urls.http + )) + })?; + + reqwest::Url::parse(&self.indexer_urls.ws).map_err(|_| { + ConfigFileError::InvalidFormat(format!( + "Invalid indexer WebSocket URL: {}", + self.indexer_urls.ws + )) + })?; + + // Validate prover URL if provided + reqwest::Url::parse(&self.prover_url).map_err(|_| { + ConfigFileError::InvalidFormat(format!("Invalid prover URL: {}", self.prover_url)) + })?; + + // Validate network_id if provided + match self.common.network.as_str() { + "mainnet" | "testnet" | "devnet" => {} + _ => { + return Err(ConfigFileError::InvalidFormat(format!( + "Invalid network_id: {}. Must be one of: mainnet, testnet, devnet", + self.common.network + ))) + } + } + + // Validate commitment_tree_ttl is reasonable if provided + if let Some(ttl) = self.commitment_tree_ttl { + if ttl == 0 { + return Err(ConfigFileError::InvalidFormat( + "commitment_tree_ttl must be greater than 0".to_string(), + )); + } + } + + Ok(()) + } + + /// Merges this Midnight configuration with a parent Midnight configuration. + /// Parent values are used as defaults, child values take precedence. + pub fn merge_with_parent(&self, parent: &Self) -> Self { + Self { + common: self.common.merge_with_parent(&parent.common), + indexer_urls: self.indexer_urls.clone(), + prover_url: self.prover_url.clone(), + commitment_tree_ttl: self.commitment_tree_ttl.or(parent.commitment_tree_ttl), + } + } +} diff --git a/src/config/config_file/network/mod.rs b/src/config/config_file/network/mod.rs index a6099d3e8..e99c22f3a 100644 --- a/src/config/config_file/network/mod.rs +++ b/src/config/config_file/network/mod.rs @@ -1,6 +1,6 @@ //! Network Configuration Module //! -//! This module provides network configuration support for EVM, Solana, and Stellar networks +//! This module provides network configuration support for EVM, Solana, Midnight, and Stellar networks //! with inheritance, validation, and flexible loading mechanisms. //! //! ## Key Features @@ -23,8 +23,10 @@ pub mod common; pub mod evm; pub mod file_loading; pub mod inheritance; +pub mod midnight; pub mod solana; pub mod stellar; + #[cfg(test)] pub mod test_utils; @@ -33,6 +35,7 @@ pub use common::*; pub use evm::*; pub use file_loading::*; pub use inheritance::*; +pub use midnight::*; pub use solana::*; pub use stellar::*; @@ -52,6 +55,8 @@ pub enum NetworkFileConfig { Solana(SolanaNetworkConfig), /// Configuration for a Stellar network. Stellar(StellarNetworkConfig), + /// Configuration for a Midnight network. + Midnight(MidnightNetworkConfig), } impl NetworkFileConfig { @@ -65,6 +70,7 @@ impl NetworkFileConfig { NetworkFileConfig::Evm(network) => network.validate(), NetworkFileConfig::Solana(network) => network.validate(), NetworkFileConfig::Stellar(network) => network.validate(), + NetworkFileConfig::Midnight(network) => network.validate(), } } @@ -77,6 +83,7 @@ impl NetworkFileConfig { NetworkFileConfig::Evm(network) => &network.common.network, NetworkFileConfig::Solana(network) => &network.common.network, NetworkFileConfig::Stellar(network) => &network.common.network, + NetworkFileConfig::Midnight(network) => &network.common.network, } } @@ -89,6 +96,7 @@ impl NetworkFileConfig { NetworkFileConfig::Evm(_) => ConfigFileNetworkType::Evm, NetworkFileConfig::Solana(_) => ConfigFileNetworkType::Solana, NetworkFileConfig::Stellar(_) => ConfigFileNetworkType::Stellar, + NetworkFileConfig::Midnight(_) => ConfigFileNetworkType::Midnight, } } @@ -102,6 +110,7 @@ impl NetworkFileConfig { NetworkFileConfig::Evm(network) => network.common.is_testnet.unwrap_or(false), NetworkFileConfig::Solana(network) => network.common.is_testnet.unwrap_or(false), NetworkFileConfig::Stellar(network) => network.common.is_testnet.unwrap_or(false), + NetworkFileConfig::Midnight(network) => network.common.is_testnet.unwrap_or(false), } } @@ -115,6 +124,7 @@ impl NetworkFileConfig { NetworkFileConfig::Evm(network) => network.common.from.as_deref(), NetworkFileConfig::Solana(network) => network.common.from.as_deref(), NetworkFileConfig::Stellar(network) => network.common.from.as_deref(), + NetworkFileConfig::Midnight(network) => network.common.from.as_deref(), } } } @@ -145,6 +155,13 @@ mod tests { assert!(result.is_ok()); } + #[test] + fn test_validate_midnight_network_success() { + let config = create_midnight_network_wrapped("test-midnight"); + let result = config.validate(); + assert!(result.is_ok()); + } + #[test] fn test_validate_evm_network_failure() { let mut config = create_evm_network_wrapped("test-evm"); @@ -190,6 +207,21 @@ mod tests { )); } + #[test] + fn test_validate_midnight_network_failure() { + let mut config = create_midnight_network_wrapped("test-midnight"); + if let NetworkFileConfig::Midnight(ref mut midnight_config) = config { + midnight_config.common.network = "".to_string(); // Invalid empty network name + } + + let result = config.validate(); + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + ConfigFileError::MissingField(_) + )); + } + #[test] fn test_validate_evm_network_missing_chain_id() { let mut config = create_evm_network_wrapped("test-evm"); @@ -254,6 +286,12 @@ mod tests { assert_eq!(config.network_name(), "test-stellar"); } + #[test] + fn test_network_name_midnight() { + let config = create_midnight_network_wrapped("test-midnight"); + assert_eq!(config.network_name(), "test-midnight"); + } + #[test] fn test_network_name_with_unicode() { let mut config = create_evm_network_wrapped("test-evm"); @@ -299,11 +337,18 @@ mod tests { assert_eq!(config.network_type(), ConfigFileNetworkType::Stellar); } + #[test] + fn test_network_type_midnight() { + let config = create_midnight_network_wrapped("test-midnight"); + assert_eq!(config.network_type(), ConfigFileNetworkType::Midnight); + } + #[test] fn test_network_type_consistency() { let evm_config = create_evm_network_wrapped("test-evm"); let solana_config = create_solana_network_wrapped("test-solana"); let stellar_config = create_stellar_network_wrapped("test-stellar"); + let midnight_config = create_midnight_network_wrapped("test-midnight"); // Ensure each type returns the correct enum variant assert!(matches!( @@ -318,6 +363,10 @@ mod tests { stellar_config.network_type(), ConfigFileNetworkType::Stellar )); + assert!(matches!( + midnight_config.network_type(), + ConfigFileNetworkType::Midnight + )); } #[test] @@ -350,6 +399,15 @@ mod tests { assert_eq!(config.inherits_from(), Some("parent-stellar")); } + #[test] + fn test_inherits_from_some_midnight() { + let mut config = create_midnight_network_wrapped("test-midnight"); + if let NetworkFileConfig::Midnight(ref mut midnight_config) = config { + midnight_config.common.from = Some("parent-midnight".to_string()); + } + assert_eq!(config.inherits_from(), Some("parent-midnight")); + } + #[test] fn test_inherits_from_empty_string() { let mut config = create_evm_network_wrapped("test-evm"); @@ -401,6 +459,17 @@ mod tests { assert_eq!(original.inherits_from(), deserialized.inherits_from()); } + #[test] + fn test_serialize_deserialize_midnight() { + let original = create_midnight_network_wrapped("test-midnight"); + let serialized = serde_json::to_string(&original).unwrap(); + let deserialized: NetworkFileConfig = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(original.network_name(), deserialized.network_name()); + assert_eq!(original.network_type(), deserialized.network_type()); + assert_eq!(original.inherits_from(), deserialized.inherits_from()); + } + #[test] fn test_deserialize_evm_from_json() { let json = r#"{ @@ -446,6 +515,19 @@ mod tests { assert_eq!(config.inherits_from(), None); } + #[test] + fn test_deserialize_midnight_from_json() { + let json = r#"{ + "type": "midnight", + "network": "test-midnight-json" + }"#; + + let config: NetworkFileConfig = serde_json::from_str(json).unwrap(); + assert_eq!(config.network_name(), "test-midnight-json"); + assert_eq!(config.network_type(), ConfigFileNetworkType::Midnight); + assert_eq!(config.inherits_from(), None); + } + #[test] fn test_deserialize_with_inheritance() { let json = r#"{ @@ -501,12 +583,14 @@ mod tests { create_evm_network_wrapped("test-evm"), create_solana_network_wrapped("test-solana"), create_stellar_network_wrapped("test-stellar"), + create_midnight_network_wrapped("test-midnight"), ]; let types: Vec = configs.iter().map(|c| c.network_type()).collect(); assert!(types.contains(&ConfigFileNetworkType::Evm)); assert!(types.contains(&ConfigFileNetworkType::Solana)); assert!(types.contains(&ConfigFileNetworkType::Stellar)); + assert!(types.contains(&ConfigFileNetworkType::Midnight)); } #[test] @@ -515,13 +599,15 @@ mod tests { create_evm_network_wrapped("test-evm"), create_solana_network_wrapped("test-solana"), create_stellar_network_wrapped("test-stellar"), + create_midnight_network_wrapped("test-midnight"), ]; let names: Vec<&str> = configs.iter().map(|c| c.network_name()).collect(); - assert_eq!(names.len(), 3); + assert_eq!(names.len(), 4); assert!(names.contains(&"test-evm")); assert!(names.contains(&"test-solana")); assert!(names.contains(&"test-stellar")); + assert!(names.contains(&"test-midnight")); } #[test] @@ -594,6 +680,7 @@ mod tests { create_evm_network_wrapped("test-evm"), create_solana_network_wrapped("test-solana"), create_stellar_network_wrapped("test-stellar"), + create_midnight_network_wrapped("test-midnight"), ]; // Ensure all methods work consistently across all network types @@ -608,6 +695,7 @@ mod tests { ConfigFileNetworkType::Evm | ConfigFileNetworkType::Solana | ConfigFileNetworkType::Stellar + | ConfigFileNetworkType::Midnight )); // All should validate successfully diff --git a/src/config/config_file/network/test_utils.rs b/src/config/config_file/network/test_utils.rs index 0e532596c..6a8c6c411 100644 --- a/src/config/config_file/network/test_utils.rs +++ b/src/config/config_file/network/test_utils.rs @@ -153,6 +153,48 @@ pub fn create_stellar_network_with_parent(network: &str, parent: &str) -> Stella } } +/// Creates a default valid Midnight network configuration. +pub fn create_midnight_network(network: &str) -> MidnightNetworkConfig { + MidnightNetworkConfig { + common: NetworkConfigCommon { + network: network.to_string(), + from: None, + rpc_urls: Some(vec![format!("https://rpc.{}.midnight.org", network)]), + explorer_urls: Some(vec!["https://explorer.example.com".to_string()]), + average_blocktime_ms: Some(5000), + is_testnet: Some(true), + tags: Some(vec!["stellar".to_string()]), + }, + indexer_urls: IndexerUrls { + http: "https://indexer.midnight.network".to_string(), + ws: "wss://indexer.midnight.network".to_string(), + }, + prover_url: "http://localhost:6300".to_string(), + commitment_tree_ttl: None, + } +} + +/// Creates a Midnight network configuration with inheritance. +pub fn create_midnight_network_with_parent(network: &str, parent: &str) -> MidnightNetworkConfig { + MidnightNetworkConfig { + common: NetworkConfigCommon { + network: network.to_string(), + from: Some(parent.to_string()), + rpc_urls: Some(vec![format!("https://rpc.{}.midnight.org", network)]), // Override parent's RPC URLs + explorer_urls: Some(vec!["https://explorer.example.com".to_string()]), + average_blocktime_ms: Some(6000), // Override parent's blocktime + is_testnet: None, // Will inherit from parent + tags: None, // Will inherit from parent + }, + indexer_urls: IndexerUrls { + http: "https://indexer.midnight.network".to_string(), + ws: "wss://indexer.midnight.network".to_string(), + }, + prover_url: "http://localhost:6300".to_string(), + commitment_tree_ttl: None, + } +} + // ============================================================================= // Wrapped Network Creation Functions (for NetworkFileConfig) // ============================================================================= @@ -187,6 +229,11 @@ pub fn create_stellar_network_wrapped(network: &str) -> NetworkFileConfig { NetworkFileConfig::Stellar(create_stellar_network(network)) } +/// Creates a wrapped Midnight network configuration. +pub fn create_midnight_network_wrapped(network: &str) -> NetworkFileConfig { + NetworkFileConfig::Midnight(create_midnight_network(network)) +} + // ============================================================================= // Temporary File Utilities // ============================================================================= diff --git a/src/config/config_file/relayer.rs b/src/config/config_file/relayer.rs index ed4f4d032..d439ab618 100644 --- a/src/config/config_file/relayer.rs +++ b/src/config/config_file/relayer.rs @@ -19,6 +19,7 @@ pub enum ConfigFileRelayerNetworkPolicy { Evm(ConfigFileRelayerEvmPolicy), Solana(ConfigFileRelayerSolanaPolicy), Stellar(ConfigFileRelayerStellarPolicy), + Midnight(ConfigFileRelayerMidnightPolicy), } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -140,6 +141,12 @@ pub struct ConfigFileRelayerStellarPolicy { pub min_balance: Option, } +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(deny_unknown_fields)] +pub struct ConfigFileRelayerMidnightPolicy { + pub min_balance: Option, +} + #[derive(Debug, Serialize, Clone)] pub struct RelayerFileConfig { pub id: String, @@ -232,6 +239,12 @@ impl<'de> Deserialize<'de> for RelayerFileConfig { .map(Some) .map_err(de::Error::custom) } + ConfigFileNetworkType::Midnight => { + serde_json::from_value::(policy_value.clone()) + .map(ConfigFileRelayerNetworkPolicy::Midnight) + .map(Some) + .map_err(de::Error::custom) + } } } else { Ok(None) // `policies` is optional @@ -427,6 +440,7 @@ impl RelayerFileConfig { } ConfigFileNetworkType::Evm => {} ConfigFileNetworkType::Stellar => {} + ConfigFileNetworkType::Midnight => {} } Ok(()) } diff --git a/src/constants/relayer.rs b/src/constants/relayer.rs index 5829e080f..8e72b950b 100644 --- a/src/constants/relayer.rs +++ b/src/constants/relayer.rs @@ -3,11 +3,13 @@ pub const DEFAULT_EVM_MIN_BALANCE: u128 = 1; // 0.001 ETH in wei pub const DEFAULT_STELLAR_MIN_BALANCE: u64 = 1_000_000; // 1 XLM pub const DEFAULT_SOLANA_MIN_BALANCE: u64 = 10_000_000; // 0.01 Lamport +pub const DEFAULT_MIDNIGHT_MIN_BALANCE: u64 = 1_000_000; // 1 DUST pub const MAX_SOLANA_TX_DATA_SIZE: u16 = 1232; pub const EVM_SMALLEST_UNIT_NAME: &str = "wei"; pub const ZERO_ADDRESS: &str = "0x0000000000000000000000000000000000000000"; #[allow(dead_code)] pub const STELLAR_SMALLEST_UNIT_NAME: &str = "stroop"; pub const SOLANA_SMALLEST_UNIT_NAME: &str = "lamport"; +pub const MIDNIGHT_SMALLEST_UNIT_NAME: &str = "dust"; // Dust (unlike NIGHT) doesn't have a smaller unit name pub const DEFAULT_RPC_WEIGHT: u8 = 100; diff --git a/src/constants/token.rs b/src/constants/token.rs index 74ae5bfad..8897e227d 100644 --- a/src/constants/token.rs +++ b/src/constants/token.rs @@ -3,4 +3,5 @@ pub const WRAPPED_SOL_MINT: &str = "So11111111111111111111111111111111111111112" pub const NATIVE_SOL: &str = "11111111111111111111111111111111"; pub const SYSTEM_PROGRAM_ID: &str = "11111111111111111111111111111111"; pub const SOLANA_DECIMALS: u8 = 9; +pub const MIDNIGHT_DECIMALS: u32 = 6; pub const JUPITER_BASE_API_URL: &str = "https://lite-api.jup.ag"; diff --git a/src/domain/relayer/midnight/midnight_relayer.rs b/src/domain/relayer/midnight/midnight_relayer.rs new file mode 100644 index 000000000..b3a4cc4a2 --- /dev/null +++ b/src/domain/relayer/midnight/midnight_relayer.rs @@ -0,0 +1,939 @@ +/// This module defines the `MidnightRelayer` struct and its associated functionality for +/// interacting with Midnight networks. The `MidnightRelayer` is responsible for managing +/// transactions, synchronizing sequence numbers, and ensuring the relayer's state is +/// consistent with the Midnight blockchain. +/// +/// # Components +/// +/// - `MidnightRelayer`: The main struct that encapsulates the relayer's state and operations for Midnight. +/// - `RelayerRepoModel`: Represents the relayer's data model. +/// - `MidnightProvider`: Provides blockchain interaction capabilities, such as fetching account details. +/// - `TransactionCounterService`: Manages the sequence number for transactions to ensure correct ordering. +/// - `JobProducer`: Produces jobs for processing transactions and sending notifications. +/// +/// # Error Handling +/// +/// The module uses the `RelayerError` enum to handle various errors that can occur during +/// operations, such as provider errors, sequence synchronization failures, and transaction failures. +/// +/// # Usage +/// +/// To use the `MidnightRelayer`, create an instance using the `new` method, providing the necessary +/// components. Then, call the appropriate methods to process transactions and manage the relayer's state. +use crate::{ + constants::MIDNIGHT_SMALLEST_UNIT_NAME, + domain::{ + next_sequence_u64, BalanceResponse, JsonRpcRequest, JsonRpcResponse, SignDataRequest, + SignDataResponse, SignTypedDataRequest, + }, + jobs::{JobProducer, JobProducerTrait, TransactionRequest}, + models::{ + produce_relayer_disabled_payload, MidnightNetwork, MidnightRpcResult, NetworkRpcRequest, + NetworkRpcResult, NetworkTransactionRequest, NetworkType, RelayerRepoModel, RelayerStatus, + RepositoryError, TransactionRepoModel, TransactionStatus, + }, + repositories::{ + InMemoryNetworkRepository, InMemoryRelayerRepository, InMemoryTransactionCounter, + InMemoryTransactionRepository, NetworkRepository, RelayerRepository, + RelayerRepositoryStorage, Repository, TransactionRepository, + }, + services::{ + sync::midnight::handler::{QuickSyncStrategy, SyncManager, SyncManagerTrait}, + MidnightProvider, MidnightProviderTrait, MidnightSigner, MidnightSignerTrait, + TransactionCounterService, TransactionCounterServiceTrait, + }, +}; +use async_trait::async_trait; +use eyre::Result; +use log::{info, warn}; +use std::sync::Arc; +use tokio::sync::Mutex; + +use crate::domain::relayer::{Relayer, RelayerError}; + +/// Dependencies container for `MidnightRelayer` construction. +pub struct MidnightRelayerDependencies +where + R: Repository + RelayerRepository + Send + Sync, + N: NetworkRepository + Send + Sync, + T: Repository + Send + Sync, + J: JobProducerTrait + Send + Sync, + C: TransactionCounterServiceTrait + Send + Sync, + S: SyncManagerTrait + Send + Sync, + SR: MidnightSignerTrait + Send + Sync, +{ + pub relayer_repository: Arc, + pub network_repository: Arc, + pub transaction_repository: Arc, + pub signer: Arc, + pub transaction_counter_service: Arc, + pub sync_service: Arc>, + pub job_producer: Arc, +} + +impl MidnightRelayerDependencies +where + R: Repository + RelayerRepository + Send + Sync, + N: NetworkRepository + Send + Sync, + T: Repository + Send + Sync, + J: JobProducerTrait + Send + Sync, + C: TransactionCounterServiceTrait + Send + Sync, + S: SyncManagerTrait + Send + Sync, + SR: MidnightSignerTrait + Send + Sync, +{ + /// Creates a new dependencies container for `MidnightRelayer`. + /// + /// # Arguments + /// + /// * `relayer_repository` - Repository for managing relayer model persistence + /// * `network_repository` - Repository for accessing network configuration data (RPC URLs, chain settings) + /// * `transaction_repository` - Repository for storing and retrieving transaction models + /// * `signer` - Transaction signer for signing transactions + /// * `transaction_counter_service` - Service for managing sequence numbers to ensure proper transaction ordering + /// * `sync_service` - Service for syncing wallet state with the blockchain + /// * `job_producer` - Service for creating background jobs for transaction processing and notifications + /// + /// # Returns + /// + /// Returns a new `MidnightRelayerDependencies` instance containing all provided dependencies. + pub fn new( + relayer_repository: Arc, + network_repository: Arc, + transaction_repository: Arc, + signer: Arc, + transaction_counter_service: Arc, + sync_service: Arc>, + job_producer: Arc, + ) -> Self { + Self { + relayer_repository, + network_repository, + transaction_repository, + signer, + transaction_counter_service, + sync_service, + job_producer, + } + } +} + +#[allow(dead_code)] +pub struct MidnightRelayer +where + P: MidnightProviderTrait + Send + Sync, + R: Repository + RelayerRepository + Send + Sync, + N: NetworkRepository + Send + Sync, + T: Repository + TransactionRepository + Send + Sync, + J: JobProducerTrait + Send + Sync, + C: TransactionCounterServiceTrait + Send + Sync, + S: SyncManagerTrait + Send + Sync, + SR: MidnightSignerTrait + Send + Sync, +{ + relayer: RelayerRepoModel, + network: MidnightNetwork, + provider: P, + relayer_repository: Arc, + network_repository: Arc, + transaction_repository: Arc, + signer: Arc, + transaction_counter_service: Arc, + sync_service: Arc>, + job_producer: Arc, +} + +pub type DefaultMidnightRelayer = MidnightRelayer< + MidnightProvider, + RelayerRepositoryStorage, + InMemoryNetworkRepository, + InMemoryTransactionRepository, + JobProducer, + TransactionCounterService, + SyncManager, + MidnightSigner, +>; + +impl MidnightRelayer +where + P: MidnightProviderTrait + Send + Sync, + R: Repository + RelayerRepository + Send + Sync, + N: NetworkRepository + Send + Sync, + T: Repository + TransactionRepository + Send + Sync, + J: JobProducerTrait + Send + Sync, + C: TransactionCounterServiceTrait + Send + Sync, + S: SyncManagerTrait + Send + Sync, + SR: MidnightSignerTrait + Send + Sync, +{ + /// Creates a new `MidnightRelayer` instance. + /// + /// This constructor initializes a new Midnight relayer with the provided configuration, + /// provider, and dependencies. It validates the network configuration and sets up + /// all necessary components for transaction processing. + /// + /// # Arguments + /// + /// * `relayer` - The relayer model containing configuration like ID, address, network name, and policies + /// * `provider` - The Midnight provider implementation for blockchain interactions + /// * `dependencies` - Container with all required repositories and services (see [`MidnightRelayerDependencies`]) + /// + /// # Returns + /// + /// * `Ok(MidnightRelayer)` - Successfully initialized relayer ready for operation + /// * `Err(RelayerError)` - If initialization fails due to configuration or validation errors + #[allow(clippy::too_many_arguments)] + pub async fn new( + relayer: RelayerRepoModel, + provider: P, + dependencies: MidnightRelayerDependencies, + ) -> Result { + let network_repo = dependencies + .network_repository + .get_by_name(NetworkType::Midnight, &relayer.network) + .await + .ok() + .flatten() + .ok_or_else(|| { + RelayerError::NetworkConfiguration(format!("Network {} not found", relayer.network)) + })?; + + let network = MidnightNetwork::try_from(network_repo)?; + + Ok(Self { + relayer, + network, + provider, + relayer_repository: dependencies.relayer_repository, + network_repository: dependencies.network_repository, + transaction_repository: dependencies.transaction_repository, + signer: dependencies.signer, + transaction_counter_service: dependencies.transaction_counter_service, + sync_service: dependencies.sync_service, + job_producer: dependencies.job_producer, + }) + } + + async fn sync_nonce(&self) -> Result<(), RelayerError> { + info!( + "Fetching nonce for relayer: {} ({})", + self.relayer.id, self.relayer.address + ); + + let nonce = self + .provider + .get_nonce(&self.relayer.address) + .await + .map_err(|e| RelayerError::ProviderError(format!("Failed to fetch account: {}", e)))?; + + let next = next_sequence_u64(nonce as i64)?; + + info!( + "Setting next nonce {} for relayer {}", + next, self.relayer.id + ); + self.transaction_counter_service + .set(next) + .await + .map_err(RelayerError::from)?; + Ok(()) + } + + async fn disable_relayer(&self, reasons: &[String]) -> Result<(), RelayerError> { + let reason = reasons.join(", "); + warn!("Disabling relayer {} due to: {}", self.relayer.id, reason); + + let updated = self + .relayer_repository + .disable_relayer(self.relayer.id.clone()) + .await?; + + if let Some(nid) = &self.relayer.notification_id { + self.job_producer + .produce_send_notification_job( + produce_relayer_disabled_payload(nid, &updated, &reason), + None, + ) + .await?; + } + Ok(()) + } +} + +#[async_trait] +impl Relayer for MidnightRelayer +where + P: MidnightProviderTrait + Send + Sync, + R: Repository + RelayerRepository + Send + Sync, + N: NetworkRepository + Send + Sync, + T: Repository + TransactionRepository + Send + Sync, + J: JobProducerTrait + Send + Sync, + C: TransactionCounterServiceTrait + Send + Sync, + S: SyncManagerTrait + Send + Sync, + SR: MidnightSignerTrait + Send + Sync, +{ + async fn process_transaction_request( + &self, + network_transaction: NetworkTransactionRequest, + ) -> Result { + let network_model = self + .network_repository + .get_by_name(NetworkType::Midnight, &self.relayer.network) + .await? + .ok_or_else(|| { + RelayerError::NetworkConfiguration(format!( + "Network {} not found", + self.relayer.network + )) + })?; + let transaction = + TransactionRepoModel::try_from((&network_transaction, &self.relayer, &network_model))?; + + self.transaction_repository + .create(transaction.clone()) + .await + .map_err(|e| RepositoryError::TransactionFailure(e.to_string()))?; + + self.job_producer + .produce_transaction_request_job( + TransactionRequest::new(transaction.id.clone(), transaction.relayer_id.clone()), + None, + ) + .await?; + + Ok(transaction) + } + + async fn get_balance(&self) -> Result { + // Get the ledger context from the sync service + let sync_service = self.sync_service.lock().await; + let context = sync_service.get_context(); + drop(sync_service); // Drop the lock early + + let wallet_seed = self.signer.wallet_seed(); + + let balance = self + .provider + .get_balance(wallet_seed, &context) + .await + .map_err(|e| RelayerError::ProviderError(format!("Failed to fetch balance: {}", e)))?; + + let balance_u128 = balance + .to_string() + .parse::() + .map_err(|e| RelayerError::ProviderError(format!("Failed to parse balance: {}", e)))?; + + Ok(BalanceResponse { + balance: balance_u128, + unit: MIDNIGHT_SMALLEST_UNIT_NAME.to_string(), + }) + } + + async fn get_status(&self) -> Result { + let relayer_model = &self.relayer; + + let nonce = self + .provider + .get_nonce(&relayer_model.address) + .await + .map_err(|e| { + RelayerError::ProviderError(format!("Failed to get account details: {}", e)) + })?; + + let nonce_str = nonce.to_string(); + + let balance_response = self.get_balance().await?; + + let pending_statuses = [TransactionStatus::Pending, TransactionStatus::Submitted]; + let pending_transactions = self + .transaction_repository + .find_by_status(&relayer_model.id, &pending_statuses[..]) + .await + .map_err(RelayerError::from)?; + let pending_transactions_count = pending_transactions.len() as u64; + + let confirmed_statuses = [TransactionStatus::Confirmed]; + let confirmed_transactions = self + .transaction_repository + .find_by_status(&relayer_model.id, &confirmed_statuses[..]) + .await + .map_err(RelayerError::from)?; + + let last_confirmed_transaction_timestamp = confirmed_transactions + .iter() + .filter_map(|tx| tx.confirmed_at.as_ref()) + .max() + .cloned(); + + Ok(RelayerStatus::Midnight { + balance: balance_response.balance.to_string(), + pending_transactions_count, + last_confirmed_transaction_timestamp, + system_disabled: relayer_model.system_disabled, + paused: relayer_model.paused, + nonce: nonce_str, + }) + } + + async fn delete_pending_transactions(&self) -> Result { + println!("Midnight delete_pending_transactions..."); + Ok(true) + } + + async fn sign_data(&self, _request: SignDataRequest) -> Result { + Err(RelayerError::NotSupported( + "Signing data not supported for Midnight".to_string(), + )) + } + + async fn sign_typed_data( + &self, + _request: SignTypedDataRequest, + ) -> Result { + Err(RelayerError::NotSupported( + "Signing typed data not supported for Midnight".to_string(), + )) + } + + async fn rpc( + &self, + _request: JsonRpcRequest, + ) -> Result, RelayerError> { + println!("Midnight rpc..."); + Ok(JsonRpcResponse { + id: Some(1), + jsonrpc: "2.0".to_string(), + result: Some(NetworkRpcResult::Midnight( + MidnightRpcResult::GenericRpcResult("".to_string()), + )), + error: None, + }) + } + + async fn validate_min_balance(&self) -> Result<(), RelayerError> { + Ok(()) + } + + async fn initialize_relayer(&self) -> Result<(), RelayerError> { + info!("Initializing Midnight relayer: {}", self.relayer.id); + + // First sync the wallet from genesis on initial setup + info!( + "Performing initial wallet sync from genesis for relayer: {}", + self.relayer.id + ); + + // Synchronises the LedgerContext (including ledger and wallet state) with the network + // This is required to get the latest merkle tree state before submitting a transaction to ensure the transaction is valid locally AND on-chain + // We use the QuickSyncStrategy by default which is a lightweight sync that uses the indexer to get the latest wallet-relevant states + // The main difference between QuickSyncStrategy and FullSyncStrategy is that QuickSyncStrategy only syncs the wallet state and the ledger state, while FullSyncStrategy syncs the entire ledger state + // This is useful because it's much faster and doesn't require downloading the entire ledger state from genesis + // However, it requires trusting the indexer with your wallet viewing key (which is read-only key used to identify transactions belonging to your wallet). + let mut sync_service = self.sync_service.lock().await; + sync_service + .sync(0) + .await + .map_err(|e| RelayerError::ProviderError(format!("Initial sync failed: {}", e)))?; + drop(sync_service); // Explicitly drop the lock + + info!("Initial wallet sync completed successfully"); + + let seq_res = self.sync_nonce().await.err(); + + let mut failures: Vec = Vec::new(); + if let Some(e) = seq_res { + failures.push(format!("Sequence sync failed: {}", e)); + } + + if !failures.is_empty() { + self.disable_relayer(&failures).await?; + return Ok(()); // same semantics as EVM + } + + info!( + "Midnight relayer initialized successfully: {}", + self.relayer.id + ); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + config::{network::IndexerUrls, MidnightNetworkConfig, NetworkConfigCommon}, + constants::MIDNIGHT_SMALLEST_UNIT_NAME, + jobs::MockJobProducerTrait, + models::{ + LocalSignerConfig, NetworkConfigData, NetworkRepoModel, NetworkType, + RelayerMidnightPolicy, RelayerNetworkPolicy, RelayerRepoModel, SignerConfig, + SignerRepoModel, + }, + repositories::{ + InMemoryNetworkRepository, InMemorySignerRepository, MockRelayerRepository, + MockTransactionRepository, + }, + services::{ + MidnightSignerFactory, MockMidnightProviderTrait, MockTransactionCounterServiceTrait, + ProviderError, + }, + }; + use midnight_node_ledger_helpers::{DefaultDB, LedgerContext, NetworkId, WalletSeed}; + use mockall::{mock, predicate::*}; + use secrets::SecretVec; + use std::future::ready; + use std::sync::Arc; + + // Mock for SyncManagerTrait - using a simple pointer cast for testing + mock! { + pub SyncManager {} + + #[async_trait] + impl SyncManagerTrait for SyncManager { + async fn sync(&mut self, start_height: u64) -> Result<(), crate::services::midnight::SyncError>; + fn get_context(&self) -> Arc>; + } + } + + /// Test context structure to manage test dependencies + struct TestCtx { + relayer_model: RelayerRepoModel, + network_repository: Arc, + signer_repository: Arc, + signer_model: SignerRepoModel, + } + + impl Default for TestCtx { + fn default() -> Self { + let network_repository = Arc::new(InMemoryNetworkRepository::new()); + let signer_repository = Arc::new(InMemorySignerRepository::new()); + + // Create a 32-byte test key + let signer_model = SignerRepoModel { + id: "signer-id".to_string(), + config: SignerConfig::Test(LocalSignerConfig { + raw_key: SecretVec::new(32, |buffer| { + buffer[0] = 1; // Make it non-zero + }), + }), + }; + + let relayer_model = RelayerRepoModel { + id: "test-relayer-id".to_string(), + name: "Test Relayer".to_string(), + network: "testnet".to_string(), + paused: false, + network_type: NetworkType::Midnight, + signer_id: "signer-id".to_string(), + policies: RelayerNetworkPolicy::Midnight(RelayerMidnightPolicy::default()), + address: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(), + notification_id: Some("notification-id".to_string()), + system_disabled: false, + custom_rpc_urls: None, + }; + + TestCtx { + relayer_model, + network_repository, + signer_repository, + signer_model, + } + } + } + + impl TestCtx { + async fn setup_network(&self) { + // Store the signer in repository + self.signer_repository + .create(self.signer_model.clone()) + .await + .unwrap(); + + let test_network = NetworkRepoModel { + id: "midnight:testnet".to_string(), + name: "testnet".to_string(), + network_type: NetworkType::Midnight, + config: NetworkConfigData::Midnight(MidnightNetworkConfig { + common: NetworkConfigCommon { + network: "testnet".to_string(), + from: None, + rpc_urls: Some(vec!["https://rpc.midnight.network".to_string()]), + explorer_urls: None, + average_blocktime_ms: Some(5000), + is_testnet: Some(true), + tags: None, + }, + prover_url: "https://prover.midnight.network".to_string(), + commitment_tree_ttl: Some(60), + indexer_urls: IndexerUrls { + http: "https://indexer.midnight.network".to_string(), + ws: "wss://indexer.midnight.network".to_string(), + }, + }), + }; + + self.network_repository.create(test_network).await.unwrap(); + } + + fn create_mock_sync_manager() -> MockSyncManager { + let mut sync_manager = MockSyncManager::new(); + // Note: This requires MIDNIGHT_LEDGER_TEST_STATIC_DIR=/path/to/midnightntwrk/midnight-node/static/contracts + // to be set in the environment when running tests + let wallet_seed = WalletSeed::from([1u8; 32]); + let context = Arc::new(LedgerContext::new_from_wallet_seeds(&[wallet_seed])); + sync_manager.expect_get_context().return_const(context); + sync_manager + } + } + + #[tokio::test] + async fn test_sync_nonce_success() { + let ctx = TestCtx::default(); + ctx.setup_network().await; + let relayer_model = ctx.relayer_model.clone(); + let mut provider = MockMidnightProviderTrait::new(); + provider + .expect_get_nonce() + .with(eq(relayer_model.address.clone())) + .returning(|_| Box::pin(async { Ok(5) })); + let mut counter = MockTransactionCounterServiceTrait::new(); + counter + .expect_set() + .with(eq(6u64)) + .returning(|_| Box::pin(async { Ok(()) })); + let relayer_repo = MockRelayerRepository::new(); + let tx_repo = MockTransactionRepository::new(); + let job_producer = MockJobProducerTrait::new(); + let sync_manager = TestCtx::create_mock_sync_manager(); + let signer = + MidnightSignerFactory::create_midnight_signer(&ctx.signer_model, NetworkId::TestNet) + .unwrap(); + + let relayer = MidnightRelayer::new( + relayer_model.clone(), + provider, + MidnightRelayerDependencies::new( + Arc::new(relayer_repo), + ctx.network_repository.clone(), + Arc::new(tx_repo), + Arc::new(signer), + Arc::new(counter), + Arc::new(Mutex::new(sync_manager)), + Arc::new(job_producer), + ), + ) + .await + .unwrap(); + + let result = relayer.sync_nonce().await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_sync_nonce_provider_error() { + let ctx = TestCtx::default(); + ctx.setup_network().await; + let relayer_model = ctx.relayer_model.clone(); + let mut provider = MockMidnightProviderTrait::new(); + provider + .expect_get_nonce() + .with(eq(relayer_model.address.clone())) + .returning(|_| Box::pin(async { Err(ProviderError::Other("fail".to_string())) })); + let counter = MockTransactionCounterServiceTrait::new(); + let relayer_repo = MockRelayerRepository::new(); + let tx_repo = MockTransactionRepository::new(); + let job_producer = MockJobProducerTrait::new(); + let sync_manager = TestCtx::create_mock_sync_manager(); + let signer = + MidnightSignerFactory::create_midnight_signer(&ctx.signer_model, NetworkId::TestNet) + .unwrap(); + let relayer = MidnightRelayer::new( + relayer_model.clone(), + provider, + MidnightRelayerDependencies::new( + Arc::new(relayer_repo), + ctx.network_repository.clone(), + Arc::new(tx_repo), + Arc::new(signer), + Arc::new(counter), + Arc::new(Mutex::new(sync_manager)), + Arc::new(job_producer), + ), + ) + .await + .unwrap(); + + let result = relayer.sync_nonce().await; + assert!(matches!(result, Err(RelayerError::ProviderError(_)))); + } + + #[tokio::test] + async fn test_disable_relayer() { + let ctx = TestCtx::default(); + ctx.setup_network().await; + let relayer_model = ctx.relayer_model.clone(); + let provider = MockMidnightProviderTrait::new(); + let mut relayer_repo = MockRelayerRepository::new(); + let mut updated_model = relayer_model.clone(); + updated_model.system_disabled = true; + relayer_repo + .expect_disable_relayer() + .with(eq(relayer_model.id.clone())) + .returning(move |_| Ok::(updated_model.clone())); + let mut job_producer = MockJobProducerTrait::new(); + job_producer + .expect_produce_send_notification_job() + .returning(|_, _| Box::pin(async { Ok(()) })); + let tx_repo = MockTransactionRepository::new(); + let counter = MockTransactionCounterServiceTrait::new(); + let sync_manager = TestCtx::create_mock_sync_manager(); + let signer = + MidnightSignerFactory::create_midnight_signer(&ctx.signer_model, NetworkId::TestNet) + .unwrap(); + let relayer = MidnightRelayer::new( + relayer_model.clone(), + provider, + MidnightRelayerDependencies::new( + Arc::new(relayer_repo), + ctx.network_repository.clone(), + Arc::new(tx_repo), + Arc::new(signer), + Arc::new(counter), + Arc::new(Mutex::new(sync_manager)), + Arc::new(job_producer), + ), + ) + .await + .unwrap(); + + let reasons = vec!["reason1".to_string(), "reason2".to_string()]; + let result = relayer.disable_relayer(&reasons).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_get_status_success_midnight() { + let ctx = TestCtx::default(); + ctx.setup_network().await; + let relayer_model = ctx.relayer_model.clone(); + let mut provider_mock = MockMidnightProviderTrait::new(); + let mut tx_repo_mock = MockTransactionRepository::new(); + let relayer_repo_mock = MockRelayerRepository::new(); + let job_producer_mock = MockJobProducerTrait::new(); + let counter_mock = MockTransactionCounterServiceTrait::new(); + let signer = + MidnightSignerFactory::create_midnight_signer(&ctx.signer_model, NetworkId::TestNet) + .unwrap(); + provider_mock + .expect_get_nonce() + .times(1) + .returning(|_| Box::pin(ready(Ok(12345)))); + + provider_mock + .expect_get_balance() + .times(1) + .returning(|_, _| Box::pin(ready(Ok(crate::models::U256::from(10000000u128))))); + + tx_repo_mock + .expect_find_by_status() + .withf(|relayer_id, statuses| { + relayer_id == "test-relayer-id" + && statuses == [TransactionStatus::Pending, TransactionStatus::Submitted] + }) + .returning(|_, _| Ok(vec![]) as Result, RepositoryError>) + .once(); + + let confirmed_tx = TransactionRepoModel { + id: "tx1_midnight".to_string(), + relayer_id: relayer_model.id.clone(), + status: TransactionStatus::Confirmed, + confirmed_at: Some("2023-02-01T12:00:00Z".to_string()), + ..TransactionRepoModel::default() + }; + tx_repo_mock + .expect_find_by_status() + .withf(|relayer_id, statuses| { + relayer_id == "test-relayer-id" && statuses == [TransactionStatus::Confirmed] + }) + .returning(move |_, _| { + Ok(vec![confirmed_tx.clone()]) as Result, RepositoryError> + }) + .once(); + + let sync_manager = TestCtx::create_mock_sync_manager(); + + let midnight_relayer = MidnightRelayer::new( + relayer_model.clone(), + provider_mock, + MidnightRelayerDependencies::new( + Arc::new(relayer_repo_mock), + ctx.network_repository.clone(), + Arc::new(tx_repo_mock), + Arc::new(signer), + Arc::new(counter_mock), + Arc::new(Mutex::new(sync_manager)), + Arc::new(job_producer_mock), + ), + ) + .await + .unwrap(); + + let status = midnight_relayer.get_status().await.unwrap(); + + match status { + RelayerStatus::Midnight { + balance, + pending_transactions_count, + last_confirmed_transaction_timestamp, + system_disabled, + paused, + nonce, + } => { + assert_eq!(balance, "10000000"); + assert_eq!(pending_transactions_count, 0); + assert_eq!( + last_confirmed_transaction_timestamp, + Some("2023-02-01T12:00:00Z".to_string()) + ); + assert_eq!(system_disabled, relayer_model.system_disabled); + assert_eq!(paused, relayer_model.paused); + assert_eq!(nonce, "12345"); + } + _ => panic!("Expected Midnight RelayerStatus"), + } + } + + #[tokio::test] + async fn test_get_status_midnight_provider_error() { + let ctx = TestCtx::default(); + ctx.setup_network().await; + let relayer_model = ctx.relayer_model.clone(); + let mut provider_mock = MockMidnightProviderTrait::new(); + let tx_repo_mock = MockTransactionRepository::new(); + let relayer_repo_mock = MockRelayerRepository::new(); + let job_producer_mock = MockJobProducerTrait::new(); + let counter_mock = MockTransactionCounterServiceTrait::new(); + let signer = + MidnightSignerFactory::create_midnight_signer(&ctx.signer_model, NetworkId::TestNet) + .unwrap(); + provider_mock + .expect_get_nonce() + .with(eq(relayer_model.address.clone())) + .returning(|_| { + Box::pin(async { Err(ProviderError::Other("Midnight provider down".to_string())) }) + }); + + let sync_manager = TestCtx::create_mock_sync_manager(); + + let midnight_relayer = MidnightRelayer::new( + relayer_model.clone(), + provider_mock, + MidnightRelayerDependencies::new( + Arc::new(relayer_repo_mock), + ctx.network_repository.clone(), + Arc::new(tx_repo_mock), + Arc::new(signer), + Arc::new(counter_mock), + Arc::new(Mutex::new(sync_manager)), + Arc::new(job_producer_mock), + ), + ) + .await + .unwrap(); + + let result = midnight_relayer.get_status().await; + assert!(result.is_err()); + match result.err().unwrap() { + RelayerError::ProviderError(msg) => { + assert!(msg.contains("Failed to get account details")) + } + _ => panic!("Expected ProviderError for get_account failure"), + } + } + + #[tokio::test] + async fn test_get_balance_success() { + let ctx = TestCtx::default(); + ctx.setup_network().await; + let relayer_model = ctx.relayer_model.clone(); + let mut provider = MockMidnightProviderTrait::new(); + let expected_balance = 100_000_000u128; + + provider.expect_get_balance().returning(move |_, _| { + Box::pin(async move { Ok(crate::models::U256::from(expected_balance)) }) + }); + + let relayer_repo = Arc::new(MockRelayerRepository::new()); + let tx_repo = Arc::new(MockTransactionRepository::new()); + let job_producer = Arc::new(MockJobProducerTrait::new()); + let counter = Arc::new(MockTransactionCounterServiceTrait::new()); + let sync_manager = TestCtx::create_mock_sync_manager(); + let signer = + MidnightSignerFactory::create_midnight_signer(&ctx.signer_model, NetworkId::TestNet) + .unwrap(); + let relayer = MidnightRelayer::new( + relayer_model, + provider, + MidnightRelayerDependencies::new( + relayer_repo, + ctx.network_repository.clone(), + tx_repo, + Arc::new(signer), + counter, + Arc::new(Mutex::new(sync_manager)), + job_producer, + ), + ) + .await + .unwrap(); + + let result = relayer.get_balance().await; + assert!(result.is_ok()); + let balance_response = result.unwrap(); + assert_eq!(balance_response.balance, expected_balance); + assert_eq!(balance_response.unit, MIDNIGHT_SMALLEST_UNIT_NAME); + } + + #[tokio::test] + async fn test_get_balance_provider_error() { + let ctx = TestCtx::default(); + ctx.setup_network().await; + let relayer_model = ctx.relayer_model.clone(); + let mut provider = MockMidnightProviderTrait::new(); + + provider.expect_get_balance().returning(|_, _| { + Box::pin(async { Err(ProviderError::Other("provider failed".to_string())) }) + }); + + let relayer_repo = Arc::new(MockRelayerRepository::new()); + let tx_repo = Arc::new(MockTransactionRepository::new()); + let job_producer = Arc::new(MockJobProducerTrait::new()); + let counter = Arc::new(MockTransactionCounterServiceTrait::new()); + let sync_manager = TestCtx::create_mock_sync_manager(); + let signer = + MidnightSignerFactory::create_midnight_signer(&ctx.signer_model, NetworkId::TestNet) + .unwrap(); + let relayer = MidnightRelayer::new( + relayer_model, + provider, + MidnightRelayerDependencies::new( + relayer_repo, + ctx.network_repository.clone(), + tx_repo, + Arc::new(signer), + counter, + Arc::new(Mutex::new(sync_manager)), + job_producer, + ), + ) + .await + .unwrap(); + + let result = relayer.get_balance().await; + assert!(result.is_err()); + match result.err().unwrap() { + RelayerError::ProviderError(msg) => { + assert!(msg.contains("Failed to fetch balance") && msg.contains("provider failed")); + } + _ => panic!("Unexpected error type"), + } + } +} diff --git a/src/domain/relayer/midnight/mod.rs b/src/domain/relayer/midnight/mod.rs new file mode 100644 index 000000000..833f07b02 --- /dev/null +++ b/src/domain/relayer/midnight/mod.rs @@ -0,0 +1,2 @@ +mod midnight_relayer; +pub use midnight_relayer::*; diff --git a/src/domain/relayer/mod.rs b/src/domain/relayer/mod.rs index 9f419347e..882ac0639 100644 --- a/src/domain/relayer/mod.rs +++ b/src/domain/relayer/mod.rs @@ -9,30 +9,41 @@ //! that share common interfaces for transaction handling and monitoring. use serde::{Deserialize, Serialize}; -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; use utoipa::ToSchema; #[cfg(test)] use mockall::automock; use crate::{ + domain::{ + relayer::midnight::{DefaultMidnightRelayer, MidnightRelayerDependencies}, + to_midnight_network_id, + }, jobs::JobProducer, models::{ - DecoratedSignature, EvmNetwork, EvmTransactionDataSignature, NetworkRpcRequest, - NetworkRpcResult, NetworkTransactionRequest, NetworkType, RelayerError, RelayerRepoModel, - RelayerStatus, SignerRepoModel, StellarNetwork, TransactionError, TransactionRepoModel, + DecoratedSignature, EvmNetwork, EvmTransactionDataSignature, MidnightNetwork, + NetworkRpcRequest, NetworkRpcResult, NetworkTransactionRequest, NetworkType, RelayerError, + RelayerRepoModel, RelayerStatus, SignerRepoModel, StellarNetwork, TransactionError, + TransactionRepoModel, }, repositories::{ - InMemoryNetworkRepository, InMemoryRelayerRepository, InMemoryTransactionCounter, - InMemoryTransactionRepository, RelayerRepositoryStorage, + InMemoryNetworkRepository, InMemoryRelayerRepository, InMemorySyncState, + InMemoryTransactionCounter, InMemoryTransactionRepository, RelayerRepositoryStorage, + }, + services::{ + get_network_provider, + sync::midnight::handler::{QuickSyncStrategy, SyncManager}, + EvmSignerFactory, MidnightProvider, MidnightProviderTrait, MidnightSignerFactory, + MidnightSignerTrait, TransactionCounterService, }, - services::{get_network_provider, EvmSignerFactory, TransactionCounterService}, }; use async_trait::async_trait; use eyre::Result; mod evm; +mod midnight; mod solana; mod stellar; mod util; @@ -205,6 +216,7 @@ pub enum NetworkRelayer { Evm(DefaultEvmRelayer), Solana(DefaultSolanaRelayer), Stellar(DefaultStellarRelayer), + Midnight(DefaultMidnightRelayer), } #[async_trait] @@ -219,6 +231,9 @@ impl Relayer for NetworkRelayer { NetworkRelayer::Stellar(relayer) => { relayer.process_transaction_request(tx_request).await } + NetworkRelayer::Midnight(relayer) => { + relayer.process_transaction_request(tx_request).await + } } } @@ -227,6 +242,7 @@ impl Relayer for NetworkRelayer { NetworkRelayer::Evm(relayer) => relayer.get_balance().await, NetworkRelayer::Solana(relayer) => relayer.get_balance().await, NetworkRelayer::Stellar(relayer) => relayer.get_balance().await, + NetworkRelayer::Midnight(relayer) => relayer.get_balance().await, } } @@ -235,6 +251,7 @@ impl Relayer for NetworkRelayer { NetworkRelayer::Evm(relayer) => relayer.delete_pending_transactions().await, NetworkRelayer::Solana(_) => solana_not_supported_relayer(), NetworkRelayer::Stellar(relayer) => relayer.delete_pending_transactions().await, + NetworkRelayer::Midnight(relayer) => relayer.delete_pending_transactions().await, } } @@ -243,6 +260,7 @@ impl Relayer for NetworkRelayer { NetworkRelayer::Evm(relayer) => relayer.sign_data(request).await, NetworkRelayer::Solana(_) => solana_not_supported_relayer(), NetworkRelayer::Stellar(relayer) => relayer.sign_data(request).await, + NetworkRelayer::Midnight(relayer) => relayer.sign_data(request).await, } } @@ -254,6 +272,7 @@ impl Relayer for NetworkRelayer { NetworkRelayer::Evm(relayer) => relayer.sign_typed_data(request).await, NetworkRelayer::Solana(_) => solana_not_supported_relayer(), NetworkRelayer::Stellar(relayer) => relayer.sign_typed_data(request).await, + NetworkRelayer::Midnight(relayer) => relayer.sign_typed_data(request).await, } } @@ -265,6 +284,7 @@ impl Relayer for NetworkRelayer { NetworkRelayer::Evm(relayer) => relayer.rpc(request).await, NetworkRelayer::Solana(relayer) => relayer.rpc(request).await, NetworkRelayer::Stellar(relayer) => relayer.rpc(request).await, + NetworkRelayer::Midnight(relayer) => relayer.rpc(request).await, } } @@ -273,6 +293,7 @@ impl Relayer for NetworkRelayer { NetworkRelayer::Evm(relayer) => relayer.get_status().await, NetworkRelayer::Solana(_) => solana_not_supported_relayer(), NetworkRelayer::Stellar(relayer) => relayer.get_status().await, + NetworkRelayer::Midnight(relayer) => relayer.get_status().await, } } @@ -281,6 +302,7 @@ impl Relayer for NetworkRelayer { NetworkRelayer::Evm(relayer) => relayer.validate_min_balance().await, NetworkRelayer::Solana(relayer) => relayer.validate_min_balance().await, NetworkRelayer::Stellar(relayer) => relayer.validate_min_balance().await, + NetworkRelayer::Midnight(relayer) => relayer.validate_min_balance().await, } } @@ -289,11 +311,13 @@ impl Relayer for NetworkRelayer { NetworkRelayer::Evm(relayer) => relayer.initialize_relayer().await, NetworkRelayer::Solana(relayer) => relayer.initialize_relayer().await, NetworkRelayer::Stellar(relayer) => relayer.initialize_relayer().await, + NetworkRelayer::Midnight(relayer) => relayer.initialize_relayer().await, } } } #[async_trait] +#[allow(clippy::too_many_arguments)] pub trait RelayerFactoryTrait { async fn create_relayer( relayer: RelayerRepoModel, @@ -302,6 +326,7 @@ pub trait RelayerFactoryTrait { networks_repository: Arc, transaction_repository: Arc, transaction_counter_store: Arc, + sync_state_store: Arc, job_producer: Arc, ) -> Result; } @@ -317,6 +342,7 @@ impl RelayerFactoryTrait for RelayerFactory { networks_repository: Arc, transaction_repository: Arc, transaction_counter_store: Arc, + sync_state_store: Arc, job_producer: Arc, ) -> Result { match relayer.network_type { @@ -335,7 +361,8 @@ impl RelayerFactoryTrait for RelayerFactory { let network = EvmNetwork::try_from(network_repo)?; - let evm_provider = get_network_provider(&network, relayer.custom_rpc_urls.clone())?; + let evm_provider = + get_network_provider(&network, relayer.custom_rpc_urls.clone(), None)?; let signer_service = EvmSignerFactory::create_evm_signer(&signer)?; let transaction_counter_service = Arc::new(TransactionCounterService::new( relayer.id.clone(), @@ -384,7 +411,7 @@ impl RelayerFactoryTrait for RelayerFactory { let network = StellarNetwork::try_from(network_repo)?; let stellar_provider = - get_network_provider(&network, relayer.custom_rpc_urls.clone()) + get_network_provider(&network, relayer.custom_rpc_urls.clone(), None) .map_err(|e| RelayerError::NetworkConfiguration(e.to_string()))?; let transaction_counter_service = Arc::new(TransactionCounterService::new( @@ -407,6 +434,88 @@ impl RelayerFactoryTrait for RelayerFactory { .await?; Ok(NetworkRelayer::Stellar(relayer)) } + NetworkType::Midnight => { + let network_repo = networks_repository + .get(NetworkType::Midnight, &relayer.network) + .await + .ok() + .flatten() + .ok_or_else(|| { + RelayerError::NetworkConfiguration(format!( + "Network {} not found", + relayer.network + )) + })?; + + let network = MidnightNetwork::try_from(network_repo)?; + let indexer_urls = network.indexer_urls.clone(); + let metadata = HashMap::from([ + ( + "network".to_string(), + format!("{:?}", to_midnight_network_id(&relayer.network)), + ), + ("http".to_string(), indexer_urls.http), + ("ws".to_string(), indexer_urls.ws), + ]); + + let midnight_provider: MidnightProvider = get_network_provider( + &network, + relayer.custom_rpc_urls.clone(), + Some(&metadata), + )?; + + // Convert network to NetworkId for sync manager + let network_id = to_midnight_network_id(&relayer.network); + + // Create the Midnight signer to get the wallet seed + let midnight_signer = MidnightSignerFactory::create_midnight_signer( + &signer, network_id, + ) + .map_err(|e| { + RelayerError::NetworkConfiguration(format!("Failed to create signer: {}", e)) + })?; + let wallet_seed = midnight_signer.wallet_seed(); + + // Get the indexer client from provider + let indexer_client = midnight_provider.get_indexer_client(); + + // Create the sync manager + let sync_manager = SyncManager::::new( + indexer_client, + wallet_seed, + network_id, + sync_state_store.clone(), + relayer.id.clone(), + ) + .map_err(|e| { + RelayerError::NetworkConfiguration(format!( + "Failed to create sync manager: {}", + e + )) + })?; + + // Signer repository is passed from the factory + + let transaction_counter_service = Arc::new(TransactionCounterService::new( + relayer.id.clone(), + relayer.address.clone(), + transaction_counter_store, + )); + + let dependencies = MidnightRelayerDependencies::new( + relayer_repository, + networks_repository, + transaction_repository, + Arc::new(midnight_signer), + transaction_counter_service, + Arc::new(tokio::sync::Mutex::new(sync_manager)), + job_producer, + ); + let relayer = + DefaultMidnightRelayer::new(relayer, midnight_provider, dependencies).await?; + + Ok(NetworkRelayer::Midnight(relayer)) + } } } } @@ -455,11 +564,17 @@ pub struct SignTransactionResponseStellar { pub signature: DecoratedSignature, } +#[derive(Debug, Serialize, Deserialize)] +pub struct SignTransactionResponseMidnight { + pub signature: String, +} + #[derive(Debug, Serialize, Deserialize)] pub enum SignTransactionResponse { Evm(SignTransactionResponseEvm), Solana(Vec), Stellar(SignTransactionResponseStellar), + Midnight(SignTransactionResponseMidnight), } impl SignTransactionResponse { diff --git a/src/domain/relayer/solana/mod.rs b/src/domain/relayer/solana/mod.rs index ba1b77c09..7bade83e2 100644 --- a/src/domain/relayer/solana/mod.rs +++ b/src/domain/relayer/solana/mod.rs @@ -46,6 +46,7 @@ pub async fn create_solana_relayer( let provider = Arc::new(get_network_provider( &network, relayer.custom_rpc_urls.clone(), + None, )?); let signer_service = Arc::new(SolanaSignerFactory::create_solana_signer(&signer)?); let jupiter_service = Arc::new(JupiterService::new_from_network(relayer.network.as_str())); diff --git a/src/domain/relayer/util.rs b/src/domain/relayer/util.rs index 6d09aa4c2..75e823f3a 100644 --- a/src/domain/relayer/util.rs +++ b/src/domain/relayer/util.rs @@ -75,6 +75,7 @@ pub async fn get_network_relayer( state.network_repository(), state.transaction_repository(), state.transaction_counter_store(), + state.sync_state_store(), state.job_producer(), ) .await @@ -108,6 +109,7 @@ pub async fn get_network_relayer_by_model( state.network_repository(), state.transaction_repository(), state.transaction_counter_store(), + state.sync_state_store(), state.job_producer(), ) .await diff --git a/src/domain/transaction/midnight/builder.rs b/src/domain/transaction/midnight/builder.rs new file mode 100644 index 000000000..0ed7017e7 --- /dev/null +++ b/src/domain/transaction/midnight/builder.rs @@ -0,0 +1,141 @@ +//! Midnight transaction builder +//! +//! This module provides a builder pattern for constructing Midnight transactions +//! following the midnight-node patterns. +//! +//! IMPORTANT: When creating transactions, always include a change output back to the sender +//! if there's any remaining value after accounting for the recipient amount and fees. +//! This ensures the indexer recognizes the transaction as relevant to the sender's wallet +//! during synchronization. + +use midnight_node_ledger_helpers::{ + FromContext, + IntentInfo, + LedgerContext, + OfferInfo, + Proof, + ProofProvider, + // StandardTrasactionInfo has a typo and may be changed in the future + StandardTrasactionInfo, + Transaction, + WellFormedStrictness, + DB, +}; +use std::sync::Arc; + +use crate::models::TransactionError; + +/// Builder for constructing Midnight transactions using midnight-node patterns +pub struct MidnightTransactionBuilder { + /// The ledger context containing wallet and network information + context: Option>>, + /// The proof provider for generating ZK proofs + proof_provider: Option>>, + /// Random seed for transaction building + rng_seed: Option<[u8; 32]>, + /// The guaranteed offer to be added + guaranteed_offer: Option>, + /// Intent info containing all fallible offers with segment information preserved + intent_info: Option>, +} + +impl MidnightTransactionBuilder { + /// Creates a new transaction builder + pub fn new() -> Self { + Self { + context: None, + proof_provider: None, + rng_seed: None, + guaranteed_offer: None, + intent_info: None, + } + } + + /// Sets the ledger context + pub fn with_context(mut self, context: std::sync::Arc>) -> Self { + self.context = Some(context); + self + } + + /// Sets the proof provider + pub fn with_proof_provider(mut self, proof_provider: Box>) -> Self { + self.proof_provider = Some(proof_provider); + self + } + + /// Sets the RNG seed + pub fn with_rng_seed(mut self, seed: [u8; 32]) -> Self { + self.rng_seed = Some(seed); + self + } + + /// Sets the entire guaranteed offer + pub fn with_guaranteed_offer(mut self, offer: OfferInfo) -> Self { + self.guaranteed_offer = Some(offer); + self + } + + /// Set the intent info + pub fn with_intent_info(mut self, intent: IntentInfo) -> Self { + self.intent_info = Some(intent); + self + } + + /// Builds the final transaction + pub async fn build(self) -> Result, TransactionError> { + let context_arc = self + .context + .ok_or_else(|| TransactionError::ValidationError("Context not provided".to_string()))?; + let proof_provider = self.proof_provider.ok_or_else(|| { + TransactionError::ValidationError("Proof provider not provided".to_string()) + })?; + let rng_seed = self.rng_seed.ok_or_else(|| { + TransactionError::ValidationError("RNG seed not provided".to_string()) + })?; + + // Create StandardTransactionInfo with the context + let mut tx_info = StandardTrasactionInfo::new_from_context( + context_arc.clone(), + proof_provider.into(), + Some(rng_seed), + ); + + // Set the guaranteed offer if present + if let Some(offer) = self.guaranteed_offer { + tx_info.set_guaranteed_coins(offer); + } + + // Set the intent info if present to preserve segment information + if let Some(intent) = self.intent_info { + tx_info.set_intents(vec![intent]); + } + + // Build transaction and generate proofs + let proven_tx = tx_info.prove().await; + + // Get the ledger state from the context + let ledger_state_guard = context_arc.ledger_state.lock().map_err(|e| { + TransactionError::UnexpectedError(format!("Failed to acquire ledger state lock: {}", e)) + })?; + + let ref_state = &*ledger_state_guard; + + // Perform well_formed validation + proven_tx + .well_formed(ref_state, WellFormedStrictness::default()) + .map_err(|e| { + TransactionError::ValidationError(format!( + "Transaction failed well_formed validation: {:?}", + e + )) + })?; + + Ok(proven_tx) + } +} + +impl Default for MidnightTransactionBuilder { + fn default() -> Self { + Self::new() + } +} diff --git a/src/domain/transaction/midnight/midnight_transaction.rs b/src/domain/transaction/midnight/midnight_transaction.rs new file mode 100644 index 000000000..4e73d0b0b --- /dev/null +++ b/src/domain/transaction/midnight/midnight_transaction.rs @@ -0,0 +1,802 @@ +//! Midnight transaction implementation +//! +//! This module provides the core transaction handling logic for Midnight network transactions. + +use crate::{ + domain::{ + to_midnight_network_id, MidnightTransactionBuilder, SignTransactionResponse, Transaction, + DUST_TOKEN_TYPE, + }, + jobs::{JobProducer, JobProducerTrait, TransactionSend, TransactionStatusCheck}, + models::{ + produce_transaction_update_notification_payload, MidnightNetwork, MidnightOfferRequest, + NetworkTransactionData, RelayerRepoModel, TransactionError, TransactionRepoModel, + TransactionStatus, TransactionUpdateRequest, + }, + repositories::{ + InMemoryRelayerRepository, InMemoryTransactionCounter, InMemoryTransactionRepository, + RelayerRepositoryStorage, Repository, TransactionCounterTrait, TransactionRepository, + }, + services::{ + midnight::handler::{QuickSyncStrategy, SyncManager}, + remote_prover::RemoteProofServer, + sync::midnight::indexer::ApplyStage, + MidnightProvider, MidnightProviderTrait, MidnightSigner, MidnightSignerTrait, + TransactionSubmissionResult, + }, +}; +use async_trait::async_trait; +use chrono::Utc; +use log::{debug, info}; +use midnight_node_ledger_helpers::{ + DefaultDB, InputInfo, LedgerContext, OfferInfo, OutputInfo, WalletSeed, +}; +use rand::Rng; +use std::sync::Arc; +use tokio::sync::Mutex; + +#[allow(dead_code)] +/// Midnight transaction handler with generic dependencies +pub struct MidnightTransaction +where + P: MidnightProviderTrait, + R: Repository, + T: TransactionRepository, + J: JobProducerTrait, + S: MidnightSignerTrait, + C: TransactionCounterTrait, +{ + relayer: RelayerRepoModel, + provider: Arc

, + relayer_repository: Arc, + transaction_repository: Arc, + job_producer: Arc, + signer: Arc, + transaction_counter_service: Arc, + sync_manager: Arc>>, + network: MidnightNetwork, +} + +#[allow(dead_code, clippy::too_many_arguments)] +impl MidnightTransaction +where + P: MidnightProviderTrait, + R: Repository, + T: TransactionRepository, + J: JobProducerTrait, + S: MidnightSignerTrait, + C: TransactionCounterTrait, +{ + /// Creates a new `MidnightTransaction`. + /// + /// # Arguments + /// + /// * `relayer` - The relayer model. + /// * `provider` - The Midnight provider. + /// * `relayer_repository` - Storage for relayer repository. + /// * `transaction_repository` - Storage for transaction repository. + /// * `job_producer` - Producer for job queue. + /// * `signer` - The signer service. + /// * `transaction_counter_service` - Service for managing transaction counters. + /// * `sync_manager` - Sync manager. + /// * `network` - The Midnight network configuration. + /// + /// # Returns + /// + /// A result containing the new `MidnightTransaction` or a `TransactionError`. + pub fn new( + relayer: RelayerRepoModel, + provider: Arc

, + relayer_repository: Arc, + transaction_repository: Arc, + job_producer: Arc, + signer: Arc, + transaction_counter_service: Arc, + sync_manager: Arc>>, + network: MidnightNetwork, + ) -> Result { + Ok(Self { + relayer, + provider, + relayer_repository, + transaction_repository, + job_producer, + signer, + transaction_counter_service, + sync_manager, + network, + }) + } + + /// Returns a reference to the provider. + pub fn provider(&self) -> &P { + &self.provider + } + + /// Returns a reference to the relayer model. + pub fn relayer(&self) -> &RelayerRepoModel { + &self.relayer + } + + /// Returns a reference to the job producer. + pub fn job_producer(&self) -> &J { + &self.job_producer + } + + /// Returns a reference to the transaction repository. + pub fn transaction_repository(&self) -> &T { + &self.transaction_repository + } + + /// Returns a reference to the network configuration. + pub fn network(&self) -> &MidnightNetwork { + &self.network + } + + /// Returns a reference to the sync manager. + pub fn sync_manager(&self) -> &Arc>> { + &self.sync_manager + } + + /// Enqueue a submit-transaction job for the given transaction. + pub async fn enqueue_submit(&self, tx: &TransactionRepoModel) -> Result<(), TransactionError> { + let job = TransactionSend::submit(tx.id.clone(), tx.relayer_id.clone()); + self.job_producer() + .produce_submit_transaction_job(job, None) + .await?; + Ok(()) + } + + /// Sends a transaction update notification if a notification ID is configured. + pub(super) async fn send_transaction_update_notification( + &self, + tx: &TransactionRepoModel, + ) -> Result<(), TransactionError> { + if let Some(notification_id) = &self.relayer().notification_id { + self.job_producer() + .produce_send_notification_job( + produce_transaction_update_notification_payload(notification_id, tx), + None, + ) + .await + .map_err(|e| { + TransactionError::UnexpectedError(format!("Failed to send notification: {}", e)) + })?; + } + Ok(()) + } + + /// Convert API offer request to Midnight's OfferInfo type + /// This method handles UTXO selection, fee calculation, and change output creation + async fn convert_offer_request_to_offer_info( + &self, + offer_request: &MidnightOfferRequest, + from_wallet_seed: WalletSeed, + context: &Arc>, + ) -> Result, TransactionError> { + // Validate we have at least one input + if offer_request.inputs.is_empty() { + return Err(TransactionError::ValidationError( + "At least one input is required".to_string(), + )); + } + + let output_requests = &offer_request.outputs; + + // Validate all inputs are from the relayer's wallet + for input_req in &offer_request.inputs { + // Parse the origin wallet seed + let origin_seed_bytes = hex::decode(&input_req.origin).map_err(|e| { + TransactionError::ValidationError(format!("Invalid origin wallet seed hex: {}", e)) + })?; + + if origin_seed_bytes.len() != 32 { + return Err(TransactionError::ValidationError( + "Wallet seed must be 32 bytes".to_string(), + )); + } + + let mut origin_array = [0u8; 32]; + origin_array.copy_from_slice(&origin_seed_bytes); + let origin_seed = WalletSeed(origin_array); + + // Verify the origin matches the relayer's wallet + if origin_seed != from_wallet_seed { + return Err(TransactionError::ValidationError( + "All input origins must match relayer wallet".to_string(), + )); + } + } + + // Calculate total output amount + let mut total_output_amount = 0u128; + let token_type = DUST_TOKEN_TYPE; // Only support DUST_TOKEN_TYPE + + for output_req in output_requests { + let amount = output_req.value.parse::().map_err(|e| { + TransactionError::ValidationError(format!("Invalid output value: {}", e)) + })?; + total_output_amount += amount; + } + + // Calculate fee based on transaction structure + // Use actual number of inputs and potentially outputs.len() + 1 outputs (including change) + let num_inputs = offer_request.inputs.len(); + let num_outputs = output_requests.len() + 1; // +1 for potential change output + + // // Use Wallet::calculate_fee to get the minimum fee + // let calculated_fee = midnight_node_ledger_helpers::Wallet::::calculate_fee( + // num_inputs, + // num_outputs + // ); + + // The Wallet::calculate_fee method seems to overestimate fees significantly + // Based on observed fees from actual transactions: + // - 1 input, 2 outputs = ~60,855 tDUST + // - Add ~20k tDUST for each additional input/output + let base_fee = 61000u128; + let per_input_fee = if num_inputs > 1 { + (num_inputs - 1) as u128 * 20000 + } else { + 0 + }; + let per_output_fee = if num_outputs > 2 { + (num_outputs - 2) as u128 * 20000 + } else { + 0 + }; + let calculated_fee = base_fee + per_input_fee + per_output_fee; + + debug!( + "Transaction with {} inputs, {} outputs: {} tDUST + {} tDUST fee = {} tDUST total", + num_inputs, + num_outputs, + total_output_amount, + calculated_fee, + total_output_amount + calculated_fee + ); + + // Get the wallet to find available UTXOs + let from_wallet = context.wallet_from_seed(from_wallet_seed); + + // Build the offer + let mut offer = OfferInfo::default(); + + // Process each input request + let mut total_input_amount = 0u128; + for input_req in &offer_request.inputs { + // Parse input value + let requested_value = input_req.value.parse::().map_err(|e| { + TransactionError::ValidationError(format!("Invalid input value: {}", e)) + })?; + + // Create a temporary input_info to find the minimum UTXO that can cover this input + let temp_input_info = InputInfo:: { + origin: from_wallet_seed, + token_type, + value: requested_value, + }; + + // Find the actual UTXO that will be selected for this input + let selected_coin = temp_input_info.min_match_coin(&from_wallet.state); + let actual_utxo_value = selected_coin.value; + + debug!( + "Selected UTXO with value: {} tDUST for requested value: {} tDUST", + actual_utxo_value, requested_value + ); + + // Create the actual input with the exact UTXO value + let input_info = InputInfo:: { + origin: from_wallet_seed, + token_type, + value: actual_utxo_value, // Use the exact value of the selected UTXO + }; + + offer.inputs.push(Box::new(input_info)); + total_input_amount += actual_utxo_value; + } + + // Verify total inputs can cover outputs + fees + if total_input_amount < total_output_amount + calculated_fee { + return Err(TransactionError::InsufficientBalance(format!( + "Total input value {} is insufficient for outputs {} + fee {} = {} tDUST", + total_input_amount, + total_output_amount, + calculated_fee, + total_output_amount + calculated_fee + ))); + } + + // Add all requested outputs + for output_req in output_requests { + // Parse destination wallet seed + let dest_seed_bytes = hex::decode(&output_req.destination).map_err(|e| { + TransactionError::ValidationError(format!( + "Invalid destination wallet seed hex: {}", + e + )) + })?; + + if dest_seed_bytes.len() != 32 { + return Err(TransactionError::ValidationError( + "Wallet seed must be 32 bytes".to_string(), + )); + } + + let mut dest_array = [0u8; 32]; + dest_array.copy_from_slice(&dest_seed_bytes); + let dest_seed = WalletSeed(dest_array); + + // Parse amount + let value = output_req.value.parse::().map_err(|e| { + TransactionError::ValidationError(format!("Invalid output value: {}", e)) + })?; + + // For now, we only support DUST_TOKEN_TYPE + let output_info = OutputInfo:: { + destination: dest_seed, + token_type, // Always DUST_TOKEN_TYPE + value, + }; + + offer.outputs.push(Box::new(output_info)); + debug!( + "Added output: {} tDUST to {:?}", + value, + hex::encode(dest_seed.0) + ); + } + + // Calculate change and create change output if needed + // This ensures the indexer recognizes the transaction as relevant to the sender + let change_amount = total_input_amount.saturating_sub(total_output_amount + calculated_fee); + if change_amount > 0 { + let change_output = OutputInfo:: { + destination: from_wallet_seed, // Send change back to sender + token_type, + value: change_amount, + }; + offer.outputs.push(Box::new(change_output)); + debug!( + "Added change output: {} tDUST back to sender", + change_amount + ); + } else { + debug!("No change output needed (exact amount)"); + } + + Ok(offer) + } + + /// Helper method to schedule a transaction status check job. + pub(super) async fn schedule_status_check( + &self, + tx: &TransactionRepoModel, + delay_seconds: Option, + ) -> Result<(), TransactionError> { + let delay = delay_seconds.map(|seconds| Utc::now().timestamp() + seconds); + self.job_producer() + .produce_check_transaction_status_job( + TransactionStatusCheck::new(tx.id.clone(), tx.relayer_id.clone()), + delay, + ) + .await + .map_err(|e| { + TransactionError::UnexpectedError(format!("Failed to schedule status check: {}", e)) + }) + } +} + +#[async_trait] +impl Transaction for MidnightTransaction +where + P: MidnightProviderTrait + Send + Sync, + R: Repository + Send + Sync, + T: TransactionRepository + Send + Sync, + J: JobProducerTrait + Send + Sync, + S: MidnightSignerTrait + Send + Sync, + C: TransactionCounterTrait + Send + Sync, +{ + async fn prepare_transaction( + &self, + tx: TransactionRepoModel, + ) -> Result { + log::debug!("Preparing Midnight transaction: {}", tx.id); + + // Extract Midnight-specific data + let midnight_data = tx.network_data.get_midnight_transaction_data()?; + let wallet_seed = self.signer.wallet_seed(); + + // Perform incremental sync - the sync manager will automatically + // read from the last synced blockchain index stored for this relayer + let mut sync_manager = self.sync_manager.lock().await; + sync_manager + .sync_incremental() + .await + .map_err(|e| TransactionError::UnexpectedError(e.to_string()))?; + + let context = sync_manager.get_context(); + drop(sync_manager); + + // Check balance + let balance = self.provider.get_balance(wallet_seed, &context).await?; + info!("Wallet balance: {} tDUST", balance); + + // Create proof provider + let proof_provider = Box::new(RemoteProofServer::new( + self.network.prover_url.clone(), + to_midnight_network_id(&self.network.network), + )); + + // Generate cryptographically secure random seed with timestamp for uniqueness + let mut rng_seed = [0u8; 32]; + rand::rng().fill(&mut rng_seed); + + // Mix in current timestamp to ensure uniqueness across transaction attempts + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() as u64; + let timestamp_bytes = timestamp.to_le_bytes(); + + // XOR the first 8 bytes of the seed with timestamp for uniqueness + for (i, &byte) in timestamp_bytes.iter().enumerate() { + rng_seed[i] ^= byte; + } + + // Build transaction based on request data + let mut builder = MidnightTransactionBuilder::::new() + .with_context(context.clone()) + .with_proof_provider(proof_provider) + .with_rng_seed(rng_seed); + + // Convert and add guaranteed offer if present + if let Some(offer_request) = &midnight_data.guaranteed_offer { + let offer = self + .convert_offer_request_to_offer_info(offer_request, *wallet_seed, &context) + .await?; + builder = builder.with_guaranteed_offer(offer); + } + + if !midnight_data.intents.is_empty() || !midnight_data.fallible_offers.is_empty() { + return Err(TransactionError::NotSupported( + "Contract interactions not yet supported".to_string(), + )); + } + + // Build and prove the transaction + let proven_transaction = builder.build().await?; + + // Serialize the transaction using Midnight's serialize function + let serialized_tx = midnight_node_ledger_helpers::serialize( + &proven_transaction, + to_midnight_network_id(&self.network.network), + ) + .map_err(|e| { + TransactionError::UnexpectedError(format!("Failed to serialize transaction: {:?}", e)) + })?; + + // Update transaction with prepared data + let mut updated_midnight_data = midnight_data.clone(); + updated_midnight_data.raw = Some(serialized_tx); + + let update = TransactionUpdateRequest { + status: Some(TransactionStatus::Sent), + network_data: Some(NetworkTransactionData::Midnight(updated_midnight_data)), + priced_at: Some(Utc::now().to_rfc3339()), + ..Default::default() + }; + + let updated_tx = self + .transaction_repository + .partial_update(tx.id.clone(), update) + .await?; + + self.enqueue_submit(&updated_tx).await?; + self.send_transaction_update_notification(&updated_tx) + .await?; + + Ok(updated_tx) + } + + async fn submit_transaction( + &self, + tx: TransactionRepoModel, + ) -> Result { + // Extract Midnight-specific data + let midnight_data = tx.network_data.get_midnight_transaction_data()?; + + // Check if we have serialized transaction data + let serialized_tx = midnight_data.raw.as_ref().ok_or_else(|| { + TransactionError::UnexpectedError( + "Transaction not prepared - missing serialized data".to_string(), + ) + })?; + + // Deserialize the transaction + let transaction = midnight_node_ledger_helpers::deserialize::< + midnight_node_ledger_helpers::Transaction< + midnight_node_ledger_helpers::Proof, + DefaultDB, + >, + _, + >( + &serialized_tx[..], + to_midnight_network_id(&self.network.network), + ) + .map_err(|e| { + TransactionError::UnexpectedError(format!("Failed to deserialize transaction: {:?}", e)) + })?; + + // Submit to the network + let result_json = self.provider.send_transaction(transaction).await?; + + // Parse the JSON response + let result: TransactionSubmissionResult = + serde_json::from_str(&result_json).map_err(|e| { + TransactionError::UnexpectedError(format!( + "Failed to parse transaction result: {}", + e + )) + })?; + + // Update transaction with hash and block hash + let mut updated_midnight_data = midnight_data.clone(); + updated_midnight_data.hash = Some(result.extrinsic_tx_hash.clone()); + updated_midnight_data.pallet_hash = Some(result.pallet_tx_hash.clone()); + + let update = TransactionUpdateRequest { + status: Some(TransactionStatus::Submitted), + network_data: Some(NetworkTransactionData::Midnight(updated_midnight_data)), + sent_at: Some(Utc::now().to_rfc3339()), + hashes: Some(vec![result.extrinsic_tx_hash]), + ..Default::default() + }; + + let updated_tx = self + .transaction_repository + .partial_update(tx.id.clone(), update) + .await?; + + // Schedule status check + let job = crate::jobs::TransactionStatusCheck::new( + updated_tx.id.clone(), + updated_tx.relayer_id.clone(), + ); + self.job_producer() + .produce_check_transaction_status_job(job, None) + .await?; + + self.send_transaction_update_notification(&updated_tx) + .await?; + + Ok(updated_tx) + } + + async fn resubmit_transaction( + &self, + tx: TransactionRepoModel, + ) -> Result { + // TODO: Implement transaction resubmission + // This might involve resubmitting only failed segments + + // For now, just return the transaction as-is + Ok(tx) + } + + async fn handle_transaction_status( + &self, + tx: TransactionRepoModel, + ) -> Result { + log::debug!("Handling Midnight transaction status: {}", tx.id); + + // If transaction is in a final state, return as-is + if matches!( + tx.status, + TransactionStatus::Confirmed | TransactionStatus::Failed | TransactionStatus::Expired + ) { + return Ok(tx); + } + + // Extract Midnight-specific data + let midnight_data = tx.network_data.get_midnight_transaction_data()?; + + // Check if we have a transaction hash + let pallet_tx_hash = midnight_data.pallet_hash.as_ref().ok_or_else(|| { + TransactionError::UnexpectedError("Transaction hash is missing".to_string()) + })?; + + match self.provider.get_transaction_by_hash(pallet_tx_hash).await { + Ok(Some(tx_data)) => { + // Check the applyStage field to determine success/failure + if let Some(apply_stage_value) = tx_data.get("applyStage") { + // Deserialize the applyStage value into the ApplyStage enum + let apply_stage: ApplyStage = serde_json::from_value(apply_stage_value.clone()) + .map_err(|e| { + TransactionError::UnexpectedError(format!( + "Failed to parse applyStage: {}", + e + )) + })?; + + match apply_stage { + ApplyStage::SucceedEntirely => { + // Transaction succeeded entirely + let update = TransactionUpdateRequest { + status: Some(TransactionStatus::Confirmed), + confirmed_at: Some(Utc::now().to_rfc3339()), + ..Default::default() + }; + + let updated_tx = self + .transaction_repository + .partial_update(tx.id.clone(), update) + .await?; + + self.send_transaction_update_notification(&updated_tx) + .await?; + + Ok(updated_tx) + } + ApplyStage::FailEntirely => { + // Transaction failed entirely + log::warn!("Transaction {} failed entirely", tx.id); + + let update = TransactionUpdateRequest { + status: Some(TransactionStatus::Failed), + ..Default::default() + }; + + let updated_tx = self + .transaction_repository + .partial_update(tx.id.clone(), update) + .await?; + + self.send_transaction_update_notification(&updated_tx) + .await?; + + Ok(updated_tx) + } + ApplyStage::SucceedPartially => { + // Partial success - could be expanded to handle segment-specific results + log::warn!("Transaction {} succeeded partially", tx.id); + + // For now, treat any partial success as a failure + // In the future, this could be more nuanced + let update = TransactionUpdateRequest { + status: Some(TransactionStatus::Failed), + ..Default::default() + }; + + let updated_tx = self + .transaction_repository + .partial_update(tx.id.clone(), update) + .await?; + + self.send_transaction_update_notification(&updated_tx) + .await?; + + Ok(updated_tx) + } + ApplyStage::Pending => { + // Still pending - schedule another check + log::info!("Transaction {} is still pending", tx.id); + + self.schedule_status_check(&tx, Some(5)).await?; + + Ok(tx) + } + } + } else { + // No applyStage field - this shouldn't happen + log::error!("Transaction {} missing applyStage field", tx.id); + + // Schedule another status check + self.schedule_status_check(&tx, Some(5)).await?; + + Ok(tx) + } + } + Ok(None) => { + // Transaction not found in indexer yet + log::info!("Transaction {} not found in indexer yet", tx.id); + + // Schedule another status check + self.schedule_status_check(&tx, Some(5)).await?; + + Ok(tx) + } + Err(e) => { + // Error querying indexer + log::error!("Error querying indexer for transaction {}: {}", tx.id, e); + + // Schedule another status check + self.schedule_status_check(&tx, Some(5)).await?; + + Ok(tx) + } + } + } + + async fn cancel_transaction( + &self, + tx: TransactionRepoModel, + ) -> Result { + // TODO: Implement transaction cancellation + // Note: Midnight transactions might not be cancellable once submitted + + log::debug!("Cancelling Midnight transaction: {}", tx.id); + + Err(TransactionError::NotSupported( + "Transaction cancellation is not supported for Midnight".to_string(), + )) + } + + async fn replace_transaction( + &self, + tx: TransactionRepoModel, + ) -> Result { + // TODO: Implement transaction replacement + // Note: Midnight transactions might not be replaceable + + log::debug!("Replacing Midnight transaction: {}", tx.id); + + Err(TransactionError::NotSupported( + "Transaction replacement is not supported for Midnight".to_string(), + )) + } + + async fn sign_transaction( + &self, + tx: TransactionRepoModel, + ) -> Result { + let signature_response = self + .signer + .sign_transaction(tx.network_data.clone()) + .await?; + + // Extract the Midnight signature from the response + let signature = match signature_response { + SignTransactionResponse::Midnight(midnight_sig) => midnight_sig, + _ => { + return Err(TransactionError::InvalidType( + "Expected Midnight signature response".to_string(), + )) + } + }; + + let mut updated_midnight_data = tx.network_data.get_midnight_transaction_data()?; + updated_midnight_data.signature = Some(signature.signature); + + let update = TransactionUpdateRequest { + network_data: Some(NetworkTransactionData::Midnight(updated_midnight_data)), + ..Default::default() + }; + + let updated_tx = self + .transaction_repository + .partial_update(tx.id.clone(), update) + .await?; + + Ok(updated_tx) + } + + async fn validate_transaction( + &self, + _tx: TransactionRepoModel, + ) -> Result { + // NOTE: This is already handled by the transaction builder in the prepare_transaction method + Ok(true) + } +} + +/// Default concrete type for Midnight transactions +pub type DefaultMidnightTransaction = MidnightTransaction< + MidnightProvider, + RelayerRepositoryStorage, + InMemoryTransactionRepository, + JobProducer, + MidnightSigner, + InMemoryTransactionCounter, +>; diff --git a/src/domain/transaction/midnight/mod.rs b/src/domain/transaction/midnight/mod.rs new file mode 100644 index 000000000..2769a8772 --- /dev/null +++ b/src/domain/transaction/midnight/mod.rs @@ -0,0 +1,12 @@ +//! Midnight transaction domain implementation +//! +//! This module provides the core transaction handling logic for the Midnight network, +//! including transaction building, signing, and submission. + +pub mod builder; +pub mod midnight_transaction; +pub mod types; + +pub use builder::MidnightTransactionBuilder; +pub use midnight_transaction::MidnightTransaction; +pub use types::*; diff --git a/src/domain/transaction/midnight/types.rs b/src/domain/transaction/midnight/types.rs new file mode 100644 index 000000000..3c0e4ce6b --- /dev/null +++ b/src/domain/transaction/midnight/types.rs @@ -0,0 +1,245 @@ +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; +use std::marker::PhantomData; + +use midnight_ledger_prototype::{ + coin_structure::coin::{Commitment, Nullifier, SecretKey}, + structure::ContractCall, + transient_crypto::curve::Fr, +}; + +use midnight_node_ledger_helpers::{ + CoinInfo, CoinPublicKey, ContractAction, ContractAddress, ContractCalls, ContractDeploy, + EncryptionPublicKey, HashOutput, Input, InputInfo, IntentInfo, NetworkId, Offer, OfferInfo, + Output, OutputInfo, ProofPreimage, Proofish, SecretKeys, TokenType, Transaction, + TransactionResult, Transcript, Transient, DB, NATIVE_TOKEN, +}; + +// Wrapper types for Midnight ZSwap types +#[derive(Debug, Clone)] +pub struct MidnightZSwapInput, D: DB> { + pub inner: Input, + _phantom: PhantomData, +} + +#[derive(Debug, Clone)] +pub struct MidnightZSwapOutput, D: DB> { + pub inner: Output, + _phantom: PhantomData, +} + +#[derive(Debug, Clone)] +pub struct MidnightZSwapOffer, D: DB> { + pub inner: Offer, + _phantom: PhantomData, +} + +impl, D: DB> MidnightZSwapOffer { + pub fn new( + inputs: Vec>, + outputs: Vec>, + transient: Vec>, + deltas: Vec<(TokenType, i128)>, + ) -> Self { + Self { + inner: Offer:: { + inputs, + outputs, + transient, + deltas, + }, + _phantom: PhantomData, + } + } +} + +#[derive(Debug, Clone)] +pub struct MidnightTransaction, D: DB> { + pub inner: Transaction, + _phantom: PhantomData, +} + +impl MidnightTransaction { + pub fn new( + guaranteed_offer: MidnightZSwapOffer, + fallible_offer: Option>, + contract_calls: Option>, + ) -> Self { + Self { + inner: Transaction::new( + guaranteed_offer.inner, + fallible_offer.map(|o| o.inner), + contract_calls, + ), + _phantom: PhantomData, + } + } +} + +pub struct MidnightZSwapIntent { + pub inner: IntentInfo, +} + +#[derive(Debug, Clone)] +pub struct MidnightZSwapTransient, D: DB> { + pub inner: Transient

, + _phantom: PhantomData, +} + +// Implement Deref for easy access to inner fields +impl, D: DB> std::ops::Deref for MidnightZSwapInput { + type Target = Input; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl, D: DB> std::ops::Deref for MidnightZSwapOutput { + type Target = Output; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl, D: DB> std::ops::Deref for MidnightZSwapOffer { + type Target = Offer; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl std::ops::Deref for MidnightZSwapIntent { + type Target = IntentInfo; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl, D: DB> std::ops::Deref for MidnightZSwapTransient { + type Target = Transient

; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl, D: DB> std::ops::Deref for MidnightTransaction { + type Target = Transaction; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +// Implement DerefMut for mutable access +impl, D: DB> std::ops::DerefMut for MidnightZSwapInput { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl, D: DB> std::ops::DerefMut for MidnightZSwapOutput { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl, D: DB> std::ops::DerefMut for MidnightZSwapOffer { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl std::ops::DerefMut for MidnightZSwapIntent { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl, D: DB> std::ops::DerefMut for MidnightZSwapTransient { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl, D: DB> std::ops::DerefMut for MidnightTransaction { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +pub type MidnightContractAction = ContractAction; +pub type MidnightTranscript = Transcript; +pub type MidnightContractDeploy = ContractDeploy; +pub type MidnightContractCall = ContractCall; +pub type MidnightTransactionResult = TransactionResult; +pub type MidnightSecretKeys = SecretKeys; +pub type MidnightCoinPublicKey = CoinPublicKey; +pub type MidnightEncryptionPublicKey = EncryptionPublicKey; +pub type MidnightNullifier = Nullifier; +pub type MidnightCommitment = Commitment; +pub type MidnightTokenType = TokenType; +pub type MidnightCoinInfo = CoinInfo; +pub type MidnightInputInfo = InputInfo; +pub type MidnightOutputInfo = OutputInfo; +pub type MidnightOfferInfo = OfferInfo; + +// Segment ID type +pub type SegmentId = u16; + +// Copied from midnight-ledger-prototype since it's not exported +// +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct Effects { + pub claimed_nullifiers: HashSet, + pub claimed_receives: HashSet, + pub claimed_spends: HashSet, + pub claimed_contract_calls: HashSet<(u64, ContractAddress, HashOutput, Fr)>, + pub mints: HashMap, +} + +// Either type for representing choices +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Either { + Left(L), + Right(R), +} + +// Proof request types for prover server +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum MidnightProofRequest { + InputProof { + segment: SegmentId, + coin: CoinInfo, + secret_key: Either, // Either + merkle_path: Vec<[u8; 32]>, + randomness: Fr, // Fr field element + }, + OutputProof { + segment: SegmentId, + coin: CoinInfo, + public_key: Either, // Either + randomness: Fr, // Fr field element + }, + BindingProof { + intent_hash: [u8; 32], + commitment: Commitment, // Pedersen commitment + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MidnightProofResponse { + pub proof: Vec, + pub public_inputs: Vec, +} + +// Constants +pub const SEGMENT_GUARANTEED: SegmentId = 0; +pub const DUST_TOKEN_TYPE: TokenType = NATIVE_TOKEN; + +pub fn to_midnight_network_id(network: &str) -> NetworkId { + match network.to_lowercase().as_str() { + "devnet" => NetworkId::DevNet, + "testnet" => NetworkId::TestNet, + "mainnet" => NetworkId::MainNet, + _ => NetworkId::Undeployed, + } +} diff --git a/src/domain/transaction/mod.rs b/src/domain/transaction/mod.rs index 75817b82b..f055e9cef 100644 --- a/src/domain/transaction/mod.rs +++ b/src/domain/transaction/mod.rs @@ -14,34 +14,41 @@ use crate::{ jobs::JobProducer, models::{ - EvmNetwork, NetworkType, RelayerRepoModel, SignerRepoModel, SolanaNetwork, StellarNetwork, - TransactionError, TransactionRepoModel, + EvmNetwork, MidnightNetwork, NetworkType, RelayerRepoModel, SignerRepoModel, SolanaNetwork, + StellarNetwork, TransactionError, TransactionRepoModel, }, repositories::{ - InMemoryNetworkRepository, InMemoryRelayerRepository, InMemoryTransactionCounter, - InMemoryTransactionRepository, RelayerRepositoryStorage, + InMemoryNetworkRepository, InMemoryRelayerRepository, InMemorySyncState, + InMemoryTransactionCounter, InMemoryTransactionRepository, RelayerRepositoryStorage, }, services::{ - get_network_extra_fee_calculator_service, get_network_provider, EvmGasPriceService, - EvmSignerFactory, StellarSignerFactory, + get_network_extra_fee_calculator_service, get_network_provider, + midnight::handler::{QuickSyncStrategy, SyncManager}, + EvmGasPriceService, EvmSignerFactory, MidnightProviderTrait, MidnightSignerFactory, + MidnightSignerTrait, StellarSignerFactory, }, }; use async_trait::async_trait; use eyre::Result; #[cfg(test)] use mockall::automock; -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; +use tokio::sync::Mutex; mod evm; +mod midnight; mod solana; mod stellar; mod util; pub use evm::*; +pub use midnight::*; pub use solana::*; pub use stellar::*; pub use util::*; +use self::midnight_transaction::DefaultMidnightTransaction; + /// A trait that defines the operations for handling transactions across different networks. #[cfg_attr(test, automock)] #[async_trait] @@ -167,6 +174,7 @@ pub enum NetworkTransaction { Evm(Box), Solana(SolanaRelayerTransaction), Stellar(DefaultStellarTransaction), + Midnight(DefaultMidnightTransaction), } #[async_trait] @@ -188,6 +196,7 @@ impl Transaction for NetworkTransaction { NetworkTransaction::Evm(relayer) => relayer.prepare_transaction(tx).await, NetworkTransaction::Solana(relayer) => relayer.prepare_transaction(tx).await, NetworkTransaction::Stellar(relayer) => relayer.prepare_transaction(tx).await, + NetworkTransaction::Midnight(relayer) => relayer.prepare_transaction(tx).await, } } @@ -208,6 +217,7 @@ impl Transaction for NetworkTransaction { NetworkTransaction::Evm(relayer) => relayer.submit_transaction(tx).await, NetworkTransaction::Solana(relayer) => relayer.submit_transaction(tx).await, NetworkTransaction::Stellar(relayer) => relayer.submit_transaction(tx).await, + NetworkTransaction::Midnight(relayer) => relayer.submit_transaction(tx).await, } } /// Resubmits a transaction with updated parameters based on the network type. @@ -227,6 +237,7 @@ impl Transaction for NetworkTransaction { NetworkTransaction::Evm(relayer) => relayer.resubmit_transaction(tx).await, NetworkTransaction::Solana(relayer) => relayer.resubmit_transaction(tx).await, NetworkTransaction::Stellar(relayer) => relayer.resubmit_transaction(tx).await, + NetworkTransaction::Midnight(relayer) => relayer.resubmit_transaction(tx).await, } } @@ -248,6 +259,7 @@ impl Transaction for NetworkTransaction { NetworkTransaction::Evm(relayer) => relayer.handle_transaction_status(tx).await, NetworkTransaction::Solana(relayer) => relayer.handle_transaction_status(tx).await, NetworkTransaction::Stellar(relayer) => relayer.handle_transaction_status(tx).await, + NetworkTransaction::Midnight(relayer) => relayer.handle_transaction_status(tx).await, } } @@ -268,6 +280,7 @@ impl Transaction for NetworkTransaction { NetworkTransaction::Evm(relayer) => relayer.cancel_transaction(tx).await, NetworkTransaction::Solana(_) => solana_not_supported_transaction(), NetworkTransaction::Stellar(relayer) => relayer.cancel_transaction(tx).await, + NetworkTransaction::Midnight(relayer) => relayer.cancel_transaction(tx).await, } } @@ -288,6 +301,7 @@ impl Transaction for NetworkTransaction { NetworkTransaction::Evm(relayer) => relayer.replace_transaction(tx).await, NetworkTransaction::Solana(_) => solana_not_supported_transaction(), NetworkTransaction::Stellar(relayer) => relayer.replace_transaction(tx).await, + NetworkTransaction::Midnight(relayer) => relayer.replace_transaction(tx).await, } } @@ -308,6 +322,7 @@ impl Transaction for NetworkTransaction { NetworkTransaction::Evm(relayer) => relayer.sign_transaction(tx).await, NetworkTransaction::Solana(relayer) => relayer.sign_transaction(tx).await, NetworkTransaction::Stellar(relayer) => relayer.sign_transaction(tx).await, + NetworkTransaction::Midnight(relayer) => relayer.sign_transaction(tx).await, } } @@ -329,6 +344,7 @@ impl Transaction for NetworkTransaction { NetworkTransaction::Evm(relayer) => relayer.validate_transaction(tx).await, NetworkTransaction::Solana(relayer) => relayer.validate_transaction(tx).await, NetworkTransaction::Stellar(relayer) => relayer.validate_transaction(tx).await, + NetworkTransaction::Midnight(relayer) => relayer.validate_transaction(tx).await, } } } @@ -369,11 +385,13 @@ impl RelayerTransactionFactory { /// * `relayer_repository` - An `Arc` to the `RelayerRepositoryStorage`. /// * `transaction_repository` - An `Arc` to the `InMemoryTransactionRepository`. /// * `transaction_counter_store` - An `Arc` to the `InMemoryTransactionCounter`. + /// * `sync_state_store` - An `Arc` to the `InMemorySyncState`. /// * `job_producer` - An `Arc` to the `JobProducer`. /// /// # Returns /// /// A `Result` containing the created `NetworkTransaction` or a `TransactionError`. + #[allow(clippy::too_many_arguments)] pub async fn create_transaction( relayer: RelayerRepoModel, signer: SignerRepoModel, @@ -381,6 +399,7 @@ impl RelayerTransactionFactory { network_repository: Arc, transaction_repository: Arc, transaction_counter_store: Arc, + sync_state_store: Arc, job_producer: Arc, ) -> Result { match relayer.network_type { @@ -400,7 +419,8 @@ impl RelayerTransactionFactory { let network = EvmNetwork::try_from(network_repo) .map_err(|e| TransactionError::NetworkConfiguration(e.to_string()))?; - let evm_provider = get_network_provider(&network, relayer.custom_rpc_urls.clone())?; + let evm_provider = + get_network_provider(&network, relayer.custom_rpc_urls.clone(), None)?; let signer_service = EvmSignerFactory::create_evm_signer(&signer)?; let network_extra_fee_calculator = get_network_extra_fee_calculator_service(network.clone(), evm_provider.clone()); @@ -442,6 +462,7 @@ impl RelayerTransactionFactory { let solana_provider = Arc::new(get_network_provider( &network, relayer.custom_rpc_urls.clone(), + None, )?); Ok(NetworkTransaction::Solana(SolanaRelayerTransaction::new( @@ -472,7 +493,7 @@ impl RelayerTransactionFactory { .map_err(|e| TransactionError::NetworkConfiguration(e.to_string()))?; let stellar_provider = - get_network_provider(&network, relayer.custom_rpc_urls.clone()) + get_network_provider(&network, relayer.custom_rpc_urls.clone(), None) .map_err(|e| TransactionError::NetworkConfiguration(e.to_string()))?; Ok(NetworkTransaction::Stellar(DefaultStellarTransaction::new( @@ -485,6 +506,71 @@ impl RelayerTransactionFactory { transaction_counter_store, )?)) } + NetworkType::Midnight => { + let signer_service = Arc::new(MidnightSignerFactory::create_midnight_signer( + &signer, + to_midnight_network_id(&relayer.network), + )?); + + let network_repo = network_repository + .get(NetworkType::Midnight, &relayer.network) + .await + .ok() + .flatten() + .ok_or_else(|| { + TransactionError::NetworkConfiguration(format!( + "Network {} not found", + relayer.network + )) + })?; + + let network = MidnightNetwork::try_from(network_repo) + .map_err(|e| TransactionError::NetworkConfiguration(e.to_string()))?; + + let network_id = to_midnight_network_id(&relayer.network); + let indexer_urls = network.indexer_urls.clone(); + + let midnight_provider = Arc::new(get_network_provider( + &network, + relayer.custom_rpc_urls.clone(), + Some(&HashMap::from([ + ("network".to_string(), format!("{:?}", network_id)), + ("http".to_string(), indexer_urls.http), + ("ws".to_string(), indexer_urls.ws), + ])), + )?); + + // Get wallet seed for the relayer + let wallet_seed = signer_service.wallet_seed(); + + // Sync wallet state with the network + let indexer_client = midnight_provider.get_indexer_client(); + + // This still requires `MIDNIGHT_LEDGER_TEST_STATIC_DIR` environment variable to be set (limitation by LedgerContext test resolver) + // TODO: We should check with the Midnight team if we can use a different constructor for LedgerContext + let sync_manager = Arc::new(Mutex::new( + SyncManager::::new( + indexer_client, + wallet_seed, + network_id, + sync_state_store.clone(), + relayer.id.clone(), + ) + .map_err(|e| TransactionError::NetworkConfiguration(e.to_string()))?, + )); + + Ok(NetworkTransaction::Midnight(MidnightTransaction::new( + relayer, + midnight_provider, + relayer_repository, + transaction_repository, + job_producer, + signer_service, + transaction_counter_store, + sync_manager, + network, + )?)) + } } } } diff --git a/src/domain/transaction/util.rs b/src/domain/transaction/util.rs index 5322e3eb8..37ba7ab86 100644 --- a/src/domain/transaction/util.rs +++ b/src/domain/transaction/util.rs @@ -63,6 +63,7 @@ pub async fn get_relayer_transaction( state.network_repository(), state.transaction_repository(), state.transaction_counter_store(), + state.sync_state_store(), state.job_producer(), ) .await @@ -95,6 +96,7 @@ pub async fn get_relayer_transaction_by_model( state.network_repository(), state.transaction_repository(), state.transaction_counter_store(), + state.sync_state_store(), state.job_producer(), ) .await diff --git a/src/jobs/handlers/transaction_submission_handler.rs b/src/jobs/handlers/transaction_submission_handler.rs index 91fe58c00..a88ea6448 100644 --- a/src/jobs/handlers/transaction_submission_handler.rs +++ b/src/jobs/handlers/transaction_submission_handler.rs @@ -8,7 +8,6 @@ use actix_web::web::ThinData; use apalis::prelude::{Attempt, Data, *}; use eyre::Result; -use log::info; use crate::{ constants::WORKER_DEFAULT_MAXIMUM_RETRIES, @@ -22,8 +21,6 @@ pub async fn transaction_submission_handler( state: Data>>, attempt: Attempt, ) -> Result<(), Error> { - info!("handling transaction submission: {:?}", job.data); - let result = handle_request(job.data, state).await; handle_result( @@ -41,30 +38,25 @@ async fn handle_request( let relayer_transaction = get_relayer_transaction(status_request.relayer_id.clone(), &state).await?; - let transaction = get_transaction_by_id(status_request.transaction_id, &state).await?; + let transaction = get_transaction_by_id(status_request.transaction_id.clone(), &state).await?; match status_request.command { TransactionCommand::Submit => { relayer_transaction.submit_transaction(transaction).await?; } - TransactionCommand::Cancel { reason } => { - info!("Cancelling transaction: {:?}", reason); + TransactionCommand::Cancel { reason: _ } => { relayer_transaction.submit_transaction(transaction).await?; } TransactionCommand::Resubmit => { - info!("Resubmitting transaction with updated parameters"); relayer_transaction .resubmit_transaction(transaction) .await?; } TransactionCommand::Resend => { - info!("Resending transaction"); relayer_transaction.submit_transaction(transaction).await?; } }; - info!("Transaction handled successfully"); - Ok(()) } diff --git a/src/models/address/midnight/address.rs b/src/models/address/midnight/address.rs new file mode 100644 index 000000000..d9e523720 --- /dev/null +++ b/src/models/address/midnight/address.rs @@ -0,0 +1,236 @@ +//! Midnight address handling module. +//! +//! This module provides functionality for encoding, decoding, and managing Midnight addresses. +//! It supports different network types and address formats. +//! +//! Majority of the code is directly copied from midnight-node repository (node-0.12.0): +//! . + +use bech32::Bech32m; +use rand::Rng; +use thiserror::Error; + +use midnight_node_ledger_helpers::{ + DefaultDB, NetworkId, Serializable, Wallet, WalletKind, WalletSeed, DB, +}; + +/// Errors that can occur during address operations +#[derive(Error, Debug)] +pub enum MidnightAddressError { + #[error("prefix first part != 'mn'")] + PrefixInvalidConstant, + #[error("prefix missing type")] + PrefixMissingType, +} + +/// Represents a Midnight address with its type, network, and data +#[derive(Debug, Clone, PartialEq)] +pub struct MidnightAddress { + /// The type of the address (e.g., "shield-addr") + pub type_: String, + /// The network identifier (e.g., "test", "dev", or None for mainnet) + pub network: Option, + /// The raw address data + pub data: Vec, +} + +impl MidnightAddress { + /// Decodes a bech32m-encoded Midnight address string into a MidnightAddress struct + /// + /// # Arguments + /// * `encoded_data` - The bech32m-encoded address string + /// + /// # Returns + /// * `Result` - The decoded address or an error + pub fn decode(encoded_data: &str) -> Result { + let (hrp, data) = bech32::decode(encoded_data).expect("Failed while bech32 decoding"); + let prefix_parts = hrp.as_str().split('_').collect::>(); + prefix_parts + .first() + .filter(|c| *c == &"mn") + .ok_or(MidnightAddressError::PrefixInvalidConstant)?; + let type_ = prefix_parts + .get(1) + .ok_or(MidnightAddressError::PrefixMissingType)? + .to_string(); + let network = prefix_parts.get(2).map(|s| s.to_string()); + + Ok(Self { + type_, + network, + data, + }) + } + + /// Encodes the MidnightAddress into a bech32m string + /// + /// # Returns + /// * `String` - The bech32m-encoded address string + pub fn encode(&self) -> String { + let network_str = match &self.network { + Some(network) => format!("_{}", network), + None => "".to_string(), + }; + + bech32::encode::( + bech32::Hrp::parse(&format!("mn_{}{}", self.type_, network_str)) + .expect("Failed while bech32 parsing"), + &self.data, + ) + .expect("Failed while bech32 encoding") + } + + /// Creates a MidnightAddress from a wallet and network ID + /// + /// # Arguments + /// * `wallet` - The wallet to create the address from + /// * `network` - The network ID to use + /// + /// # Returns + /// * `Self` - The created MidnightAddress + pub fn from_wallet(wallet: &Wallet, network: NetworkId) -> Self { + let network_str = match network { + NetworkId::MainNet => None, + NetworkId::DevNet => Some("dev".to_string()), + NetworkId::TestNet => Some("test".to_string()), + NetworkId::Undeployed => Some("undeployed".to_string()), + _ => None, + }; + + let coin_pub_key = wallet.secret_keys.coin_public_key().0 .0; + let mut enc_pub_key = Vec::new(); + Serializable::serialize(&wallet.secret_keys.enc_public_key(), &mut enc_pub_key) + .expect("Failed serializing secret keys"); + + Self { + type_: "shield-addr".to_string(), + network: network_str, + data: [&coin_pub_key[..], &enc_pub_key[..]].concat(), + } + } + + /// Creates a MidnightAddress from a seed string and network ID + /// + /// # Arguments + /// * `seed` - The hex-encoded seed string + /// * `network` - The network ID to use + /// + /// # Returns + /// * `Self` - The created MidnightAddress + pub fn from_seed(seed: String, network: NetworkId) -> Self { + let seed = hex::decode(seed).unwrap(); + let wallet: Wallet = Wallet::new( + WalletSeed(seed.try_into().unwrap()), + 0, + WalletKind::NoLegacy, + ); + Self::from_wallet(&wallet, network) + } + + /// Generates a random 32-byte seed and returns it as a hex string + /// + /// # Returns + /// * `String` - A hex-encoded random seed + pub fn generate_random_seed() -> String { + let mut seed = [0u8; 32]; + rand::rng().fill(&mut seed); + hex::encode(seed) + } +} + +impl TryFrom<&MidnightAddress> for NetworkId { + type Error = String; + + /// Attempts to convert a MidnightAddress into a NetworkId + /// + /// # Arguments + /// * `value` - The MidnightAddress to convert + /// + /// # Returns + /// * `Result` - The NetworkId or an error string + fn try_from(value: &MidnightAddress) -> Result { + match value.network { + Some(ref network) => match network.as_str() { + "dev" => Ok(NetworkId::DevNet), + "test" => Ok(NetworkId::TestNet), + "undeployed" => Ok(NetworkId::Undeployed), + _ => Err(network.to_string()), + }, + None => Ok(NetworkId::MainNet), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bech32::{Bech32m, Hrp}; + + #[test] + fn test_parse() { + let encoded_str = bech32::encode::( + Hrp::parse("mn_shield-addr_test").expect("Failed while bech32 parsing"), + &[1, 2, 3], + ) + .expect("Failed while bech32 encoding"); + let address = + MidnightAddress::decode(&encoded_str).expect("Failed while decoding `MidnightAddress"); + assert_eq!(address.type_, "shield-addr".to_string()); + assert_eq!(address.network, Some("test".to_string())); + assert_eq!(address.data, vec![1u8, 2u8, 3u8]); + } + + #[test] + fn test_from_seed() { + let seed = "b49408db310c043ab736fb57a98e15c8cedbed4c38450df3755ac9726ee14d0c"; + let address = MidnightAddress::from_seed(seed.to_string(), NetworkId::TestNet); + assert_eq!(address.type_, "shield-addr".to_string()); + assert_eq!(address.network, Some("test".to_string())); + assert_eq!(address.encode(), "mn_shield-addr_test1quc5snkchyepu6rpn5sn85cmnjfk2kzynwtf3lapt9t8q0qlw97sxqypw479uxdvf48386urhyndrty9vmpkjlydmdcur78rr3lw345kg5r4fgc2"); + let decoded_address = MidnightAddress::decode(&address.encode()).unwrap(); + assert_eq!(decoded_address.type_, "shield-addr".to_string()); + assert_eq!(decoded_address.network, Some("test".to_string())); + assert_eq!(decoded_address.data, address.data); + } + + #[test] + fn test_from_wallet() { + let wallet = Wallet::::new( + WalletSeed( + hex::decode("b49408db310c043ab736fb57a98e15c8cedbed4c38450df3755ac9726ee14d0c") + .unwrap() + .try_into() + .unwrap(), + ), + 0, + WalletKind::NoLegacy, + ); + let address = MidnightAddress::from_wallet(&wallet, NetworkId::TestNet); + assert_eq!(address.type_, "shield-addr".to_string()); + assert_eq!(address.network, Some("test".to_string())); + assert_eq!(address.encode(), "mn_shield-addr_test1quc5snkchyepu6rpn5sn85cmnjfk2kzynwtf3lapt9t8q0qlw97sxqypw479uxdvf48386urhyndrty9vmpkjlydmdcur78rr3lw345kg5r4fgc2"); + } + + #[test] + fn test_encode_decode() { + let address = + MidnightAddress::from_seed(MidnightAddress::generate_random_seed(), NetworkId::TestNet); + let encoded_address = address.encode(); + let decoded_address = MidnightAddress::decode(&encoded_address).unwrap(); + assert_eq!(address, decoded_address); + } + + #[test] + fn test_try_from() { + let address = + MidnightAddress::from_seed(MidnightAddress::generate_random_seed(), NetworkId::TestNet); + let network_id = NetworkId::try_from(&address).unwrap(); + assert_eq!(network_id, NetworkId::TestNet); + } + + #[test] + fn test_random_seed() { + let seed = MidnightAddress::generate_random_seed(); + assert_eq!(seed.len(), 64); + } +} diff --git a/src/models/address/midnight/mod.rs b/src/models/address/midnight/mod.rs new file mode 100644 index 000000000..0248475c0 --- /dev/null +++ b/src/models/address/midnight/mod.rs @@ -0,0 +1,2 @@ +mod address; +pub use address::*; diff --git a/src/models/address.rs b/src/models/address/mod.rs similarity index 60% rename from src/models/address.rs rename to src/models/address/mod.rs index a3d0f7ddf..5a8e8ed57 100644 --- a/src/models/address.rs +++ b/src/models/address/mod.rs @@ -1,5 +1,8 @@ use std::fmt; +mod midnight; +pub use midnight::*; + #[derive(Debug, Clone, PartialEq, Eq)] #[allow(dead_code)] pub enum Address { @@ -9,6 +12,8 @@ pub enum Address { Stellar(String), /// Solana address (Base58-encoded string) Solana(String), + /// Midnight address (bech32m-encoded string with type and network) + Midnight(String), } impl fmt::Display for Address { @@ -17,6 +22,7 @@ impl fmt::Display for Address { Address::Evm(addr) => write!(f, "0x{}", hex::encode(addr)), Address::Stellar(addr) => write!(f, "{}", addr), Address::Solana(addr) => write!(f, "{}", addr), + Address::Midnight(addr) => write!(f, "{}", addr), } } } @@ -33,6 +39,9 @@ impl Address { Address::Solana(addr) => { addr.len() <= 44 && addr.chars().all(|c| c.is_ascii_alphanumeric()) } + Address::Midnight(addr) => { + addr.len() <= 56 && addr.chars().all(|c| c.is_ascii_alphanumeric()) + } } } } @@ -40,6 +49,8 @@ impl Address { #[cfg(test)] mod tests { use super::*; + use crate::models::address::midnight::MidnightAddress; + use midnight_node_ledger_helpers::NetworkId; #[test] fn test_evm_address_display() { @@ -52,4 +63,16 @@ mod tests { "0xc834dcdc9a074dbbadcc71584789ae4b463db116" ); } + + #[test] + fn test_midnight_address_display() { + let seed = "b49408db310c043ab736fb57a98e15c8cedbed4c38450df3755ac9726ee14d0c"; + let midnight_addr = + MidnightAddress::from_seed(seed.to_string(), NetworkId::TestNet).encode(); + let address = Address::Midnight(midnight_addr); + assert_eq!( + address.to_string(), + "mn_shield-addr_test1quc5snkchyepu6rpn5sn85cmnjfk2kzynwtf3lapt9t8q0qlw97sxqypw479uxdvf48386urhyndrty9vmpkjlydmdcur78rr3lw345kg5r4fgc2" + ); + } } diff --git a/src/models/app_state.rs b/src/models/app_state.rs index b24087450..d8cd4db28 100644 --- a/src/models/app_state.rs +++ b/src/models/app_state.rs @@ -7,8 +7,8 @@ use crate::{ jobs::{JobProducer, JobProducerTrait}, repositories::{ InMemoryNetworkRepository, InMemoryNotificationRepository, InMemoryRelayerRepository, - InMemorySignerRepository, InMemoryTransactionCounter, InMemoryTransactionRepository, - RelayerRepositoryStorage, + InMemorySignerRepository, InMemorySyncState, InMemoryTransactionCounter, + InMemoryTransactionRepository, RelayerRepositoryStorage, }, }; @@ -28,6 +28,8 @@ pub struct AppState { pub network_repository: Arc, /// Store for managing transaction counters. pub transaction_counter_store: Arc, + /// Store for managing sync state. + pub sync_state_store: Arc, /// Producer for managing job creation and execution. pub job_producer: Arc, } @@ -89,6 +91,15 @@ impl AppState { Arc::clone(&self.transaction_counter_store) } + /// Returns a clone of the sync state store. + /// + /// # Returns + /// + /// An `Arc` pointing to the `InMemorySyncState`. + pub fn sync_state_store(&self) -> Arc { + Arc::clone(&self.sync_state_store) + } + /// Returns a clone of the job producer. /// /// # Returns @@ -136,6 +147,7 @@ mod tests { notification_repository: Arc::new(InMemoryNotificationRepository::default()), network_repository: Arc::new(InMemoryNetworkRepository::default()), transaction_counter_store: Arc::new(InMemoryTransactionCounter::default()), + sync_state_store: Arc::new(InMemorySyncState::default()), job_producer: Arc::new(mock_job_producer), } } diff --git a/src/models/network/midnight/mod.rs b/src/models/network/midnight/mod.rs new file mode 100644 index 000000000..a2ae55601 --- /dev/null +++ b/src/models/network/midnight/mod.rs @@ -0,0 +1,3 @@ +mod network; + +pub use network::*; diff --git a/src/models/network/midnight/network.rs b/src/models/network/midnight/network.rs new file mode 100644 index 000000000..dd38a4766 --- /dev/null +++ b/src/models/network/midnight/network.rs @@ -0,0 +1,96 @@ +use crate::{ + config::network::IndexerUrls, + models::{NetworkConfigData, NetworkRepoModel, RepositoryError}, +}; +use core::time::Duration; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Debug)] +pub struct MidnightNetwork { + /// Unique network identifier (e.g., "mainnet", "testnet", "devnet"). + pub network: String, + /// List of RPC endpoint URLs for connecting to the network. + pub rpc_urls: Vec, + /// List of Explorer endpoint URLs for connecting to the network. + pub explorer_urls: Option>, + /// Estimated average time between blocks in milliseconds. + pub average_blocktime_ms: u64, + /// Flag indicating if the network is a testnet. + pub is_testnet: bool, + /// List of arbitrary tags for categorizing or filtering networks. + pub tags: Vec, + /// List of Indexer endpoint URLs for connecting to the network. + pub indexer_urls: IndexerUrls, + /// URL of the prover server for generating proofs. + pub prover_url: String, +} + +impl TryFrom for MidnightNetwork { + type Error = RepositoryError; + + /// Converts a NetworkRepoModel to a MidnightNetwork. + /// + /// # Arguments + /// * `network_repo` - The repository model to convert + /// + /// # Returns + /// Result containing the MidnightNetwork if successful, or a RepositoryError + fn try_from(network_repo: NetworkRepoModel) -> Result { + match &network_repo.config { + NetworkConfigData::Midnight(midnight_config) => { + let common = &midnight_config.common; + + let rpc_urls = common.rpc_urls.clone().ok_or_else(|| { + RepositoryError::InvalidData(format!( + "Midnight network '{}' has no rpc_urls", + network_repo.name + )) + })?; + + let average_blocktime_ms = common.average_blocktime_ms.ok_or_else(|| { + RepositoryError::InvalidData(format!( + "Midnight network '{}' has no average_blocktime_ms", + network_repo.name + )) + })?; + + Ok(MidnightNetwork { + network: common.network.clone(), + rpc_urls, + explorer_urls: common.explorer_urls.clone(), + average_blocktime_ms, + is_testnet: common.is_testnet.unwrap_or(false), + tags: common.tags.clone().unwrap_or_default(), + indexer_urls: midnight_config.indexer_urls.clone(), + prover_url: midnight_config.prover_url.clone(), + }) + } + _ => Err(RepositoryError::InvalidData(format!( + "Network '{}' is not a Midnight network", + network_repo.name + ))), + } + } +} + +impl MidnightNetwork { + pub fn average_blocktime(&self) -> Option { + Some(Duration::from_millis(self.average_blocktime_ms)) + } + + pub fn public_rpc_urls(&self) -> Option<&[String]> { + if self.rpc_urls.is_empty() { + None + } else { + Some(&self.rpc_urls) + } + } + + pub fn explorer_urls(&self) -> Option<&[String]> { + self.explorer_urls.as_deref() + } + + pub fn is_testnet(&self) -> bool { + self.is_testnet + } +} diff --git a/src/models/network/mod.rs b/src/models/network/mod.rs index 50f824767..28ed13fa1 100644 --- a/src/models/network/mod.rs +++ b/src/models/network/mod.rs @@ -1,9 +1,11 @@ mod evm; +mod midnight; mod repository; mod solana; mod stellar; pub use evm::*; +pub use midnight::*; pub use repository::*; pub use solana::*; pub use stellar::*; diff --git a/src/models/network/repository.rs b/src/models/network/repository.rs index 7b67a86d5..b540f6daa 100644 --- a/src/models/network/repository.rs +++ b/src/models/network/repository.rs @@ -1,7 +1,7 @@ use crate::{ config::{ - EvmNetworkConfig, NetworkConfigCommon, NetworkFileConfig, SolanaNetworkConfig, - StellarNetworkConfig, + EvmNetworkConfig, MidnightNetworkConfig, NetworkConfigCommon, NetworkFileConfig, + SolanaNetworkConfig, StellarNetworkConfig, }, models::NetworkType, }; @@ -17,6 +17,8 @@ pub enum NetworkConfigData { Solana(SolanaNetworkConfig), /// Stellar network configuration Stellar(StellarNetworkConfig), + /// Midnight network configuration + Midnight(MidnightNetworkConfig), } impl NetworkConfigData { @@ -26,6 +28,7 @@ impl NetworkConfigData { NetworkConfigData::Evm(config) => &config.common, NetworkConfigData::Solana(config) => &config.common, NetworkConfigData::Stellar(config) => &config.common, + NetworkConfigData::Midnight(config) => &config.common, } } @@ -35,6 +38,7 @@ impl NetworkConfigData { NetworkConfigData::Evm(_) => NetworkType::Evm, NetworkConfigData::Solana(_) => NetworkType::Solana, NetworkConfigData::Stellar(_) => NetworkType::Stellar, + NetworkConfigData::Midnight(_) => NetworkType::Midnight, } } @@ -115,6 +119,24 @@ impl NetworkRepoModel { } } + /// Creates a new NetworkRepoModel with Midnight configuration. + /// + /// # Arguments + /// * `config` - The Midnight network configuration + /// + /// # Returns + /// A new NetworkRepoModel instance + pub fn new_midnight(config: MidnightNetworkConfig) -> Self { + let name = config.common.network.clone(); + let id = format!("midnight:{}", name).to_lowercase(); + Self { + id, + name, + network_type: NetworkType::Midnight, + config: NetworkConfigData::Midnight(config), + } + } + /// Creates an ID string from network type and name. /// /// # Arguments @@ -153,6 +175,7 @@ impl TryFrom for NetworkRepoModel { NetworkFileConfig::Evm(evm_config) => Ok(Self::new_evm(evm_config)), NetworkFileConfig::Solana(solana_config) => Ok(Self::new_solana(solana_config)), NetworkFileConfig::Stellar(stellar_config) => Ok(Self::new_stellar(stellar_config)), + NetworkFileConfig::Midnight(midnight_config) => Ok(Self::new_midnight(midnight_config)), } } } diff --git a/src/models/relayer/repository.rs b/src/models/relayer/repository.rs index b9a8ce19b..9541445ef 100644 --- a/src/models/relayer/repository.rs +++ b/src/models/relayer/repository.rs @@ -5,7 +5,8 @@ use utoipa::ToSchema; use crate::{ constants::{ DEFAULT_CONVERSION_SLIPPAGE_PERCENTAGE, DEFAULT_EVM_MIN_BALANCE, - DEFAULT_SOLANA_MIN_BALANCE, DEFAULT_STELLAR_MIN_BALANCE, MAX_SOLANA_TX_DATA_SIZE, + DEFAULT_MIDNIGHT_MIN_BALANCE, DEFAULT_SOLANA_MIN_BALANCE, DEFAULT_STELLAR_MIN_BALANCE, + MAX_SOLANA_TX_DATA_SIZE, }, models::RelayerError, }; @@ -18,6 +19,7 @@ pub enum NetworkType { Evm, Stellar, Solana, + Midnight, } #[derive(Debug, Serialize, Clone)] @@ -25,6 +27,7 @@ pub enum RelayerNetworkPolicy { Evm(RelayerEvmPolicy), Solana(RelayerSolanaPolicy), Stellar(RelayerStellarPolicy), + Midnight(RelayerMidnightPolicy), } impl RelayerNetworkPolicy { @@ -48,6 +51,13 @@ impl RelayerNetworkPolicy { _ => RelayerStellarPolicy::default(), } } + + pub fn get_midnight_policy(&self) -> RelayerMidnightPolicy { + match self { + Self::Midnight(policy) => policy.clone(), + _ => RelayerMidnightPolicy::default(), + } + } } #[derive(Debug, Serialize, Clone)] @@ -280,6 +290,20 @@ impl Default for RelayerStellarPolicy { } } +#[derive(Debug, Serialize, Clone)] +#[serde(deny_unknown_fields)] +pub struct RelayerMidnightPolicy { + pub min_balance: u64, +} + +impl Default for RelayerMidnightPolicy { + fn default() -> Self { + Self { + min_balance: DEFAULT_MIDNIGHT_MIN_BALANCE, + } + } +} + #[derive(Debug, Clone, Serialize)] pub struct RelayerRepoModel { pub id: String, diff --git a/src/models/relayer/response.rs b/src/models/relayer/response.rs index 4e9a7058c..76e0b211b 100644 --- a/src/models/relayer/response.rs +++ b/src/models/relayer/response.rs @@ -41,6 +41,15 @@ pub enum RelayerStatus { paused: bool, sequence_number: String, }, + #[serde(rename = "midnight")] + Midnight { + balance: String, + pending_transactions_count: u64, + last_confirmed_transaction_timestamp: Option, + system_disabled: bool, + paused: bool, + nonce: String, + }, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)] @@ -49,6 +58,7 @@ pub enum NetworkPolicyResponse { Evm(EvmPolicyResponse), Solana(SolanaPolicyResponse), Stellar(StellarPolicyResponse), + Midnight(MidnightPolicyResponse), } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)] @@ -104,6 +114,11 @@ pub struct StellarPolicyResponse { pub min_balance: u64, } +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)] +pub struct MidnightPolicyResponse { + pub min_balance: u64, +} + impl From for RelayerResponse { fn from(model: RelayerRepoModel) -> Self { let policies = match model.policies { @@ -135,6 +150,11 @@ impl From for RelayerResponse { min_balance: stellar.min_balance, }) } + RelayerNetworkPolicy::Midnight(midnight) => { + NetworkPolicyResponse::Midnight(MidnightPolicyResponse { + min_balance: midnight.min_balance, + }) + } }; Self { diff --git a/src/models/relayer/rpc_config.rs b/src/models/relayer/rpc_config.rs index efc66a43c..7d4e19497 100644 --- a/src/models/relayer/rpc_config.rs +++ b/src/models/relayer/rpc_config.rs @@ -74,9 +74,10 @@ impl RpcConfig { /// Validates that a URL has an HTTP or HTTPS scheme. /// Helper function, hence private. fn validate_url_scheme(url: &str) -> Result<()> { - if !url.starts_with("http://") && !url.starts_with("https://") { + let supported_protocols = ["http://", "https://", "wss://", "ws://"]; + if !supported_protocols.iter().any(|p| url.starts_with(p)) { return Err(eyre!( - "Invalid URL scheme for {}: Only HTTP and HTTPS are supported", + "Invalid URL scheme for {}: Only HTTP, HTTPS, WSS, and WS are supported", url )); } diff --git a/src/models/rpc/midnight/mod.rs b/src/models/rpc/midnight/mod.rs new file mode 100644 index 000000000..1db9b451c --- /dev/null +++ b/src/models/rpc/midnight/mod.rs @@ -0,0 +1,14 @@ +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Debug, Serialize, Deserialize, ToSchema, PartialEq)] +#[serde(untagged)] +pub enum MidnightRpcResult { + GenericRpcResult(String), +} + +#[derive(Debug, Serialize, Deserialize, ToSchema, PartialEq)] +#[serde(tag = "method", content = "params")] +pub enum MidnightRpcRequest { + GenericRpcRequest(String), +} diff --git a/src/models/rpc/mod.rs b/src/models/rpc/mod.rs index fe9933b6f..bb0e43d74 100644 --- a/src/models/rpc/mod.rs +++ b/src/models/rpc/mod.rs @@ -10,12 +10,16 @@ pub use stellar::*; mod evm; pub use evm::*; +mod midnight; +pub use midnight::*; + #[derive(Debug, Serialize, Deserialize, ToSchema, PartialEq)] #[serde(untagged)] pub enum NetworkRpcResult { Solana(SolanaRpcResult), Stellar(StellarRpcResult), Evm(EvmRpcResult), + Midnight(MidnightRpcResult), } #[derive(Debug, Serialize, Deserialize, ToSchema, PartialEq)] @@ -25,4 +29,5 @@ pub enum NetworkRpcRequest { Solana(SolanaRpcRequest), Stellar(StellarRpcRequest), Evm(EvmRpcRequest), + Midnight(MidnightRpcRequest), } diff --git a/src/models/transaction/repository.rs b/src/models/transaction/repository.rs index dc5da325e..aafd39fc7 100644 --- a/src/models/transaction/repository.rs +++ b/src/models/transaction/repository.rs @@ -2,7 +2,10 @@ use super::evm::Speed; use crate::{ domain::{PriceParams, SignTransactionResponseEvm}, models::{ - transaction::stellar_types::{MemoSpec, OperationSpec}, + transaction::{ + request::midnight::{MidnightIntentRequest, MidnightOfferRequest}, + stellar_types::{MemoSpec, OperationSpec}, + }, AddressError, EvmNetwork, NetworkRepoModel, NetworkTransactionRequest, NetworkType, RelayerError, RelayerRepoModel, SignerError, StellarNetwork, TransactionError, U256, }, @@ -15,7 +18,7 @@ use alloy::{ use chrono::Utc; use serde::{Deserialize, Serialize}; -use std::{convert::TryFrom, str::FromStr}; +use std::{collections::HashMap, convert::TryFrom, str::FromStr}; use utoipa::ToSchema; use uuid::Uuid; @@ -88,6 +91,7 @@ pub enum NetworkTransactionData { Evm(EvmTransactionData), Solana(SolanaTransactionData), Stellar(StellarTransactionData), + Midnight(MidnightTransactionData), } impl NetworkTransactionData { @@ -117,6 +121,17 @@ impl NetworkTransactionData { )), } } + + pub fn get_midnight_transaction_data( + &self, + ) -> Result { + match self { + NetworkTransactionData::Midnight(data) => Ok(data.clone()), + _ => Err(TransactionError::InvalidType( + "Expected Midnight transaction".to_string(), + )), + } + } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -295,6 +310,22 @@ impl StellarTransactionData { } } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MidnightTransactionData { + // Transaction lifecycle fields + pub hash: Option, // Extrinsic transaction hash + pub pallet_hash: Option, // Midnight pallet transaction hash + pub block_hash: Option, // Hash of the block containing the transaction + pub segment_results: Option>, // For partial success tracking + pub raw: Option>, + pub signature: Option, // Signature of the transaction + + // Request data stored for prepare_transaction + pub guaranteed_offer: Option, + pub intents: Vec, + pub fallible_offers: Vec<(u16, MidnightOfferRequest)>, +} + impl TryFrom<( &NetworkTransactionRequest, @@ -395,6 +426,32 @@ impl noop_count: None, is_canceled: Some(false), }), + NetworkTransactionRequest::Midnight(midnight_request) => Ok(Self { + id: Uuid::new_v4().to_string(), + relayer_id: relayer_model.id.clone(), + status: TransactionStatus::Pending, + status_reason: None, + created_at: now, + sent_at: None, + confirmed_at: None, + valid_until: midnight_request.ttl.clone(), + network_type: NetworkType::Midnight, + network_data: NetworkTransactionData::Midnight(MidnightTransactionData { + hash: None, + pallet_hash: None, + block_hash: None, + segment_results: None, + raw: None, + signature: None, + guaranteed_offer: midnight_request.guaranteed_offer.clone(), + intents: midnight_request.intents.clone(), + fallible_offers: midnight_request.fallible_offers.clone(), + }), + priced_at: None, + hashes: Vec::new(), + noop_count: None, + is_canceled: Some(false), + }), } } } diff --git a/src/models/transaction/request/midnight.rs b/src/models/transaction/request/midnight.rs new file mode 100644 index 000000000..73f7e6110 --- /dev/null +++ b/src/models/transaction/request/midnight.rs @@ -0,0 +1,63 @@ +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +/// Request structure for Midnight transactions +/// For simple transfers, use only the guaranteed_offer field +#[derive(Deserialize, Serialize, ToSchema, Debug, Clone)] +pub struct MidnightTransactionRequest { + /// The guaranteed offer (used for simple transfers and guaranteed operations) + pub guaranteed_offer: Option, + + /// Intents for complex contract interactions + pub intents: Vec, + + /// Fallible offers that may fail independently + pub fallible_offers: Vec<(u16, MidnightOfferRequest)>, // (segment_id, offer) + + /// Transaction time-to-live as ISO timestamp + pub ttl: Option, +} + +#[derive(Deserialize, Serialize, ToSchema, Debug, Clone)] +pub struct MidnightIntentRequest { + pub segment_id: u16, + pub actions: Vec, +} + +/// Offer request that will be converted to Midnight's OfferInfo +#[derive(Deserialize, Serialize, ToSchema, Debug, Clone)] +pub struct MidnightOfferRequest { + /// Inputs to be consumed + pub inputs: Vec, + /// Outputs to be created + pub outputs: Vec, +} + +#[derive(Deserialize, Serialize, ToSchema, Debug, Clone)] +pub struct MidnightContractAction { + // Contract-specific action data + // TODO: Define based on Midnight contract interaction patterns +} + +/// Input request that will be converted to Midnight's InputInfo +#[derive(Deserialize, Serialize, ToSchema, Debug, Clone)] +pub struct MidnightInputRequest { + /// Origin wallet seed (hex encoded) + pub origin: String, + /// Token type (e.g., "02000000000000000000000000000000000000000000000000000000000000000000") + pub token_type: String, + /// Amount to consume (in smallest unit) + pub value: String, +} + +/// Output request that will be converted to Midnight's OutputInfo +#[derive(Deserialize, Serialize, ToSchema, Debug, Clone)] +pub struct MidnightOutputRequest { + /// Destination wallet seed (hex encoded) + /// TODO: Change this to wallet address once updated by Midnight team + pub destination: String, + /// Token type (e.g., "02000000000000000000000000000000000000000000000000000000000000000000") + pub token_type: String, + /// Amount to send (in smallest unit) + pub value: String, +} diff --git a/src/models/transaction/request/mod.rs b/src/models/transaction/request/mod.rs index c16e65e7b..24e8a53f6 100644 --- a/src/models/transaction/request/mod.rs +++ b/src/models/transaction/request/mod.rs @@ -1,4 +1,5 @@ pub mod evm; +pub mod midnight; pub mod solana; pub mod stellar; @@ -6,6 +7,7 @@ use crate::models::{ApiError, NetworkType, RelayerRepoModel}; use serde::Serialize; pub use evm::EvmTransactionRequest; +pub use midnight::{MidnightOfferRequest, MidnightTransactionRequest}; pub use solana::SolanaTransactionRequest; pub use stellar::StellarTransactionRequest; use utoipa::ToSchema; @@ -16,6 +18,7 @@ pub enum NetworkTransactionRequest { Evm(EvmTransactionRequest), Solana(SolanaTransactionRequest), Stellar(StellarTransactionRequest), + Midnight(MidnightTransactionRequest), } impl NetworkTransactionRequest { @@ -33,6 +36,9 @@ impl NetworkTransactionRequest { NetworkType::Stellar => Ok(Self::Stellar( serde_json::from_value(json).map_err(|e| ApiError::BadRequest(e.to_string()))?, )), + NetworkType::Midnight => Ok(Self::Midnight( + serde_json::from_value(json).map_err(|e| ApiError::BadRequest(e.to_string()))?, + )), } } diff --git a/src/models/transaction/response.rs b/src/models/transaction/response.rs index 2589b1423..de6d26367 100644 --- a/src/models/transaction/response.rs +++ b/src/models/transaction/response.rs @@ -11,6 +11,7 @@ pub enum TransactionResponse { Evm(EvmTransactionResponse), Solana(SolanaTransactionResponse), Stellar(StellarTransactionResponse), + Midnight(MidnightTransactionResponse), } #[derive(Debug, Serialize, Clone, PartialEq, Deserialize, ToSchema)] @@ -72,6 +73,23 @@ pub struct StellarTransactionResponse { pub sequence_number: i64, } +#[derive(Debug, Serialize, Clone, PartialEq, Deserialize, ToSchema)] +pub struct MidnightTransactionResponse { + pub id: String, + #[schema(nullable = false)] + pub hash: Option, + #[schema(nullable = false)] + pub pallet_hash: Option, + #[schema(nullable = false)] + pub block_hash: Option, + pub status: TransactionStatus, + pub created_at: String, + #[schema(nullable = false)] + pub sent_at: Option, + #[schema(nullable = false)] + pub confirmed_at: Option, +} + impl From for TransactionResponse { fn from(model: TransactionRepoModel) -> Self { match model.network_data { @@ -118,6 +136,18 @@ impl From for TransactionResponse { sequence_number: stellar_data.sequence_number.unwrap_or(0), }) } + NetworkTransactionData::Midnight(midnight_data) => { + TransactionResponse::Midnight(MidnightTransactionResponse { + id: model.id, + hash: midnight_data.hash, + pallet_hash: midnight_data.pallet_hash, + block_hash: midnight_data.block_hash, + status: model.status, + created_at: model.created_at, + sent_at: model.sent_at, + confirmed_at: model.confirmed_at, + }) + } } } } diff --git a/src/repositories/mod.rs b/src/repositories/mod.rs index e35cf9dfa..3e64d0fa0 100644 --- a/src/repositories/mod.rs +++ b/src/repositories/mod.rs @@ -24,6 +24,9 @@ pub use transaction_counter::*; mod network; pub use network::*; +mod sync_state; +pub use sync_state::*; + #[derive(Debug)] pub struct PaginatedResult { pub items: Vec, diff --git a/src/repositories/network.rs b/src/repositories/network.rs index f7c59c39b..8bd517174 100644 --- a/src/repositories/network.rs +++ b/src/repositories/network.rs @@ -165,7 +165,8 @@ impl NetworkRepository for InMemoryNetworkRepository { mod tests { use super::*; use crate::config::{ - EvmNetworkConfig, NetworkConfigCommon, SolanaNetworkConfig, StellarNetworkConfig, + network::IndexerUrls, EvmNetworkConfig, MidnightNetworkConfig, NetworkConfigCommon, + SolanaNetworkConfig, StellarNetworkConfig, }; fn create_test_network(name: String, network_type: NetworkType) -> NetworkRepoModel { @@ -201,6 +202,18 @@ mod tests { }; NetworkRepoModel::new_stellar(stellar_config) } + NetworkType::Midnight => { + let midnight_config = MidnightNetworkConfig { + common, + indexer_urls: IndexerUrls { + http: "https://indexer.midnight.network".to_string(), + ws: "wss://indexer.midnight.network".to_string(), + }, + prover_url: "http://localhost:6300".to_string(), + commitment_tree_ttl: None, + }; + NetworkRepoModel::new_midnight(midnight_config) + } } } diff --git a/src/repositories/relayer.rs b/src/repositories/relayer.rs index 05385efaf..f4cb3b2ca 100644 --- a/src/repositories/relayer.rs +++ b/src/repositories/relayer.rs @@ -11,9 +11,10 @@ use crate::config::{ ConfigFileRelayerSolanaFeePaymentStrategy, ConfigFileRelayerSolanaSwapPolicy, ConfigFileRelayerSolanaSwapStrategy, }; +use crate::constants::DEFAULT_MIDNIGHT_MIN_BALANCE; use crate::models::{ - JupiterSwapOptions, PaginationQuery, RelayerSolanaSwapConfig, SolanaAllowedTokensSwapConfig, - SolanaFeePaymentStrategy, SolanaSwapStrategy, + JupiterSwapOptions, PaginationQuery, RelayerMidnightPolicy, RelayerSolanaSwapConfig, + SolanaAllowedTokensSwapConfig, SolanaFeePaymentStrategy, SolanaSwapStrategy, }; use crate::{ config::{ConfigFileNetworkType, ConfigFileRelayerNetworkPolicy, RelayerFileConfig}, @@ -262,6 +263,7 @@ impl TryFrom for RelayerRepoModel { ConfigFileNetworkType::Evm => NetworkType::Evm, ConfigFileNetworkType::Stellar => NetworkType::Stellar, ConfigFileNetworkType::Solana => NetworkType::Solana, + ConfigFileNetworkType::Midnight => NetworkType::Midnight, }; let policies = if let Some(config_policies) = &config.policies { @@ -276,6 +278,9 @@ impl TryFrom for RelayerRepoModel { RelayerNetworkPolicy::Stellar(RelayerStellarPolicy::default()) } NetworkType::Solana => RelayerNetworkPolicy::Solana(RelayerSolanaPolicy::default()), + NetworkType::Midnight => { + RelayerNetworkPolicy::Midnight(RelayerMidnightPolicy::default()) + } } }; @@ -413,6 +418,11 @@ impl TryFrom for RelayerNetworkPolicy { min_balance: stellar.min_balance.unwrap_or(DEFAULT_STELLAR_MIN_BALANCE), })) } + ConfigFileRelayerNetworkPolicy::Midnight(midnight) => { + Ok(RelayerNetworkPolicy::Midnight(RelayerMidnightPolicy { + min_balance: midnight.min_balance.unwrap_or(DEFAULT_MIDNIGHT_MIN_BALANCE), + })) + } } } } diff --git a/src/repositories/sync_state.rs b/src/repositories/sync_state.rs new file mode 100644 index 000000000..523892717 --- /dev/null +++ b/src/repositories/sync_state.rs @@ -0,0 +1,329 @@ +//! This module provides in-memory implementation for sync state management. +//! +//! The `InMemorySyncState` struct is used to track the last synced blockchain index +//! and the serialized ledger context for different relayers, enabling the sync service +//! to resume from the correct position with the proper wallet state. +//! This implementation uses a `DashMap` for concurrent access and modification of the sync state. + +use dashmap::DashMap; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[cfg(test)] +use mockall::automock; + +/// Represents the sync state for a relayer, including blockchain index and ledger context +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RelayerSyncState { + /// The last synced blockchain index + pub last_synced_index: u64, + /// The serialized ledger context (optional, as it may not be available initially) + pub ledger_context: Option>, +} + +#[derive(Debug, Default, Clone)] +pub struct InMemorySyncState { + store: DashMap, // relayer_id -> sync state +} + +impl InMemorySyncState { + pub fn new() -> Self { + Self { + store: DashMap::new(), + } + } +} + +#[derive(Error, Debug, Serialize)] +pub enum SyncStateError { + #[error("Sync state not found for relayer {relayer_id}")] + NotFound { relayer_id: String }, + #[error("Invalid blockchain index {index} for relayer {relayer_id}")] + InvalidIndex { relayer_id: String, index: u64 }, + #[error("Failed to serialize/deserialize ledger context: {0}")] + SerializationError(String), +} + +#[allow(dead_code)] +#[cfg_attr(test, automock)] +pub trait SyncStateTrait { + /// Get the last synced blockchain index for a relayer + fn get_last_synced_index(&self, relayer_id: &str) -> Result, SyncStateError>; + + /// Get the serialized ledger context for a relayer + fn get_ledger_context(&self, relayer_id: &str) -> Result>, SyncStateError>; + + /// Set the last synced blockchain index for a relayer + fn set_last_synced_index(&self, relayer_id: &str, index: u64) -> Result<(), SyncStateError>; + + /// Set the ledger context for a relayer + fn set_ledger_context(&self, relayer_id: &str, context: Vec) -> Result<(), SyncStateError>; + + /// Set both the last synced index and ledger context for a relayer + fn set_sync_state( + &self, + relayer_id: &str, + index: u64, + context: Option>, + ) -> Result<(), SyncStateError>; + + /// Update the last synced blockchain index only if the new index is greater + fn update_if_greater(&self, relayer_id: &str, index: u64) -> Result; + + /// Reset the sync state for a relayer + fn reset(&self, relayer_id: &str) -> Result<(), SyncStateError>; + + /// Get all sync states + fn get_all(&self) -> Vec<(String, RelayerSyncState)>; +} + +impl SyncStateTrait for InMemorySyncState { + fn get_last_synced_index(&self, relayer_id: &str) -> Result, SyncStateError> { + Ok(self + .store + .get(relayer_id) + .map(|state| state.last_synced_index)) + } + + fn get_ledger_context(&self, relayer_id: &str) -> Result>, SyncStateError> { + Ok(self + .store + .get(relayer_id) + .and_then(|state| state.ledger_context.clone())) + } + + fn set_last_synced_index(&self, relayer_id: &str, index: u64) -> Result<(), SyncStateError> { + self.store + .entry(relayer_id.to_string()) + .and_modify(|state| state.last_synced_index = index) + .or_insert(RelayerSyncState { + last_synced_index: index, + ledger_context: None, + }); + Ok(()) + } + + fn set_ledger_context(&self, relayer_id: &str, context: Vec) -> Result<(), SyncStateError> { + self.store + .entry(relayer_id.to_string()) + .and_modify(|state| state.ledger_context = Some(context.clone())) + .or_insert(RelayerSyncState { + last_synced_index: 0, + ledger_context: Some(context), + }); + Ok(()) + } + + fn set_sync_state( + &self, + relayer_id: &str, + index: u64, + context: Option>, + ) -> Result<(), SyncStateError> { + self.store.insert( + relayer_id.to_string(), + RelayerSyncState { + last_synced_index: index, + ledger_context: context, + }, + ); + Ok(()) + } + + fn update_if_greater(&self, relayer_id: &str, index: u64) -> Result { + let mut updated = false; + self.store + .entry(relayer_id.to_string()) + .and_modify(|state| { + if index > state.last_synced_index { + state.last_synced_index = index; + updated = true; + } + }) + .or_insert_with(|| { + updated = true; + RelayerSyncState { + last_synced_index: index, + ledger_context: None, + } + }); + Ok(updated) + } + + fn reset(&self, relayer_id: &str) -> Result<(), SyncStateError> { + self.store.remove(relayer_id); + Ok(()) + } + + fn get_all(&self) -> Vec<(String, RelayerSyncState)> { + self.store + .iter() + .map(|entry| (entry.key().clone(), entry.value().clone())) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_sync_state_basic_operations() { + let store = InMemorySyncState::new(); + let relayer_id = "relayer_1"; + + // Initially should be None + assert_eq!(store.get_last_synced_index(relayer_id).unwrap(), None); + + // Set a value + store.set_last_synced_index(relayer_id, 100).unwrap(); + assert_eq!(store.get_last_synced_index(relayer_id).unwrap(), Some(100)); + + // Update to a higher value + store.set_last_synced_index(relayer_id, 200).unwrap(); + assert_eq!(store.get_last_synced_index(relayer_id).unwrap(), Some(200)); + + // Reset + store.reset(relayer_id).unwrap(); + assert_eq!(store.get_last_synced_index(relayer_id).unwrap(), None); + } + + #[test] + fn test_update_if_greater() { + let store = InMemorySyncState::new(); + let relayer_id = "relayer_1"; + + // First update should succeed + assert!(store.update_if_greater(relayer_id, 100).unwrap()); + assert_eq!(store.get_last_synced_index(relayer_id).unwrap(), Some(100)); + + // Update with lower value should not change + assert!(!store.update_if_greater(relayer_id, 50).unwrap()); + assert_eq!(store.get_last_synced_index(relayer_id).unwrap(), Some(100)); + + // Update with equal value should not change + assert!(!store.update_if_greater(relayer_id, 100).unwrap()); + assert_eq!(store.get_last_synced_index(relayer_id).unwrap(), Some(100)); + + // Update with higher value should succeed + assert!(store.update_if_greater(relayer_id, 150).unwrap()); + assert_eq!(store.get_last_synced_index(relayer_id).unwrap(), Some(150)); + } + + #[test] + fn test_multiple_relayers() { + let store = InMemorySyncState::new(); + + // Set different indices for different relayers + store.set_last_synced_index("relayer_1", 100).unwrap(); + store.set_last_synced_index("relayer_2", 200).unwrap(); + store.set_last_synced_index("relayer_3", 300).unwrap(); + + // Verify independent states + assert_eq!(store.get_last_synced_index("relayer_1").unwrap(), Some(100)); + assert_eq!(store.get_last_synced_index("relayer_2").unwrap(), Some(200)); + assert_eq!(store.get_last_synced_index("relayer_3").unwrap(), Some(300)); + + // Update one relayer shouldn't affect others + store.update_if_greater("relayer_1", 150).unwrap(); + assert_eq!(store.get_last_synced_index("relayer_1").unwrap(), Some(150)); + assert_eq!(store.get_last_synced_index("relayer_2").unwrap(), Some(200)); + assert_eq!(store.get_last_synced_index("relayer_3").unwrap(), Some(300)); + } + + #[test] + fn test_ledger_context_operations() { + let store = InMemorySyncState::new(); + let relayer_id = "relayer_1"; + + // Initially should be None + assert_eq!(store.get_ledger_context(relayer_id).unwrap(), None); + + // Set ledger context + let context = vec![1, 2, 3, 4, 5]; + store + .set_ledger_context(relayer_id, context.clone()) + .unwrap(); + assert_eq!( + store.get_ledger_context(relayer_id).unwrap(), + Some(context.clone()) + ); + + // Set sync state with both index and context + let new_context = vec![6, 7, 8, 9, 10]; + store + .set_sync_state(relayer_id, 100, Some(new_context.clone())) + .unwrap(); + assert_eq!(store.get_last_synced_index(relayer_id).unwrap(), Some(100)); + assert_eq!( + store.get_ledger_context(relayer_id).unwrap(), + Some(new_context) + ); + } + + #[test] + fn test_get_all() { + let store = InMemorySyncState::new(); + + // Initially empty + assert_eq!(store.get_all().len(), 0); + + // Add some relayers + store.set_last_synced_index("relayer_1", 100).unwrap(); + store + .set_sync_state("relayer_2", 200, Some(vec![1, 2, 3])) + .unwrap(); + + let all = store.get_all(); + assert_eq!(all.len(), 2); + + // Convert to HashMap for easier testing + let all_map: std::collections::HashMap<_, _> = all.into_iter().collect(); + assert_eq!( + all_map.get("relayer_1").map(|s| s.last_synced_index), + Some(100) + ); + assert_eq!( + all_map.get("relayer_2").map(|s| s.last_synced_index), + Some(200) + ); + assert!(all_map + .get("relayer_2") + .and_then(|s| s.ledger_context.as_ref()) + .is_some()); + } + + #[test] + fn test_concurrent_access() { + use std::sync::Arc; + use std::thread; + + let store = Arc::new(InMemorySyncState::new()); + let mut handles = vec![]; + + // Spawn multiple threads updating different relayers + for i in 0..10 { + let store_clone = Arc::clone(&store); + let handle = thread::spawn(move || { + let relayer_id = format!("relayer_{}", i); + for j in 0..100 { + store_clone + .update_if_greater(&relayer_id, j) + .expect("Failed to update"); + } + }); + handles.push(handle); + } + + // Wait for all threads to complete + for handle in handles { + handle.join().unwrap(); + } + + // Verify all relayers have the expected final value + for i in 0..10 { + let relayer_id = format!("relayer_{}", i); + assert_eq!(store.get_last_synced_index(&relayer_id).unwrap(), Some(99)); + } + } +} diff --git a/src/services/mod.rs b/src/services/mod.rs index 592bc3e3b..8463d35dd 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -28,3 +28,6 @@ pub use turnkey::*; mod google_cloud_kms; pub use google_cloud_kms::*; + +pub mod sync; +pub use sync::*; diff --git a/src/services/provider/evm/mod.rs b/src/services/provider/evm/mod.rs index f2b5e78da..7ddc6fae7 100644 --- a/src/services/provider/evm/mod.rs +++ b/src/services/provider/evm/mod.rs @@ -241,9 +241,13 @@ impl EvmProvider { operation_name, Self::is_retriable_error, Self::should_mark_provider_failed, - |url| match self.initialize_provider(url) { - Ok(provider) => Ok(provider), - Err(e) => Err(e), + { + let self_clone = self.clone(); + move |url: &str| { + let self_clone = self_clone.clone(); + let url = url.to_string(); + async move { self_clone.initialize_provider(&url) } + } }, operation, Some(self.retry_config.clone()), diff --git a/src/services/provider/midnight/mod.rs b/src/services/provider/midnight/mod.rs new file mode 100644 index 000000000..7dbec65bd --- /dev/null +++ b/src/services/provider/midnight/mod.rs @@ -0,0 +1,790 @@ +//! Midnight Provider implementation for interacting with Midnight blockchain networks. +//! +//! This module provides functionality to interact with Midnight blockchain through RPC calls. +//! It implements common operations like getting balances, sending transactions, and querying +//! blockchain state. + +pub mod remote_prover; + +use async_trait::async_trait; +use eyre::Result; +use hex; +use midnight_node_ledger_helpers::{ + serialize, DefaultDB, LedgerContext, NetworkId, Proof, Transaction, WalletSeed, NATIVE_TOKEN, +}; +use midnight_node_res::subxt_metadata::api as mn_meta; +use serde::{Deserialize, Serialize}; +use subxt::{OnlineClient, PolkadotConfig}; + +use super::rpc_selector::RpcSelector; +use super::{retry_rpc_call, RetryConfig}; +use crate::config::network::IndexerUrls; +use crate::models::{RpcConfig, U256}; +use crate::services::midnight::indexer::MidnightIndexerClient; + +#[cfg(test)] +use mockall::automock; + +use super::ProviderError; + +/// Response structure for transaction submission +#[derive(Debug, Serialize, Deserialize)] +pub struct TransactionSubmissionResult { + pub extrinsic_tx_hash: String, + pub pallet_tx_hash: String, +} + +/// Provider implementation for Midnight blockchain networks. +/// +/// Wraps a Substrate/Subxt client to interact with Midnight blockchain. +#[derive(Clone)] +pub struct MidnightProvider { + /// Indexer client for querying the chain + indexer_client: MidnightIndexerClient, + /// RPC selector for managing and selecting providers + selector: RpcSelector, + /// Timeout in seconds for new HTTP clients + timeout_seconds: u64, + /// Configuration for retry behavior + retry_config: RetryConfig, + /// Network ID for transaction serialization + network_id: NetworkId, +} + +/// Trait defining the interface for EVM blockchain interactions. +/// +/// This trait provides methods for common blockchain operations like querying balances, +/// sending transactions, and getting network state. +#[async_trait] +#[cfg_attr(test, automock)] +#[allow(dead_code)] +pub trait MidnightProviderTrait: Send + Sync { + /// Gets the balance of an address in the native currency. + /// + /// # Arguments + /// * `seed` - The seed to query the balance for + /// * `context` - The ledger context to use for the balance query + async fn get_balance( + &self, + seed: &WalletSeed, + context: &LedgerContext, + ) -> Result; + + /// Gets the current block number of the chain. + async fn get_block_number(&self) -> Result; + + /// Sends a transaction to the network. + /// + /// # Arguments + /// * `tx` - The transaction request to send + async fn send_transaction( + &self, + tx: Transaction, + ) -> Result; + + /// Performs a health check by attempting to get the latest block number. + async fn health_check(&self) -> Result; + + /// Gets the nonce for an address. + /// + /// # Arguments + /// * `address` - The address to query the nonce for + async fn get_nonce(&self, address: &str) -> Result; + + /// Get indexer client + fn get_indexer_client(&self) -> &MidnightIndexerClient; + + /// Get block by hash + async fn get_block_by_hash( + &self, + hash: &str, + ) -> Result, ProviderError>; + + /// Get transaction by hash + async fn get_transaction_by_hash( + &self, + hash: &str, + ) -> Result, ProviderError>; +} + +impl MidnightProvider { + /// Creates a new Midnight provider instance. + /// + /// # Arguments + /// * `configs` - A vector of RPC configurations (URL and weight) + /// * `timeout_seconds` - The timeout duration in seconds (defaults to 30 if None) + /// + /// # Returns + /// * `Result` - A new provider instance or an error + pub fn new( + configs: Vec, + indexer_urls: IndexerUrls, + network_id: NetworkId, + timeout_seconds: u64, + ) -> Result { + if configs.is_empty() { + return Err(ProviderError::NetworkConfiguration( + "At least one RPC configuration must be provided".to_string(), + )); + } + + RpcConfig::validate_list(&configs) + .map_err(|e| ProviderError::NetworkConfiguration(format!("Invalid URL: {}", e)))?; + + // Create the RPC selector + let selector = RpcSelector::new(configs).map_err(|e| { + ProviderError::NetworkConfiguration(format!("Failed to create RPC selector: {}", e)) + })?; + + let retry_config = RetryConfig::from_env(); + + let indexer_client = MidnightIndexerClient::new(indexer_urls); + + Ok(Self { + selector, + indexer_client, + timeout_seconds, + retry_config, + network_id, + }) + } + + // Error codes that indicate we can't use a provider + fn should_mark_provider_failed(error: &ProviderError) -> bool { + match error { + ProviderError::RequestError { status_code, .. } => { + match *status_code { + // 5xx Server Errors - RPC node is having issues + 500..=599 => true, + + // 4xx Client Errors that indicate we can't use this provider + 401 => true, // Unauthorized - auth required but not provided + 403 => true, // Forbidden - node is blocking requests or auth issues + 404 => true, // Not Found - endpoint doesn't exist or misconfigured + 410 => true, // Gone - endpoint permanently removed + + _ => false, + } + } + _ => false, + } + } + + // Errors that are retriable + fn is_retriable_error(error: &ProviderError) -> bool { + match error { + // Only retry these specific error types + ProviderError::Timeout | ProviderError::RateLimited | ProviderError::BadGateway => true, + + // Any other errors are not automatically retriable + _ => { + // Optionally inspect error message for network-related issues + let err_msg = format!("{}", error); + err_msg.to_lowercase().contains("timeout") + || err_msg.to_lowercase().contains("connection") + || err_msg.to_lowercase().contains("reset") + } + } + } + + /// Initialize a provider for a given URL + async fn initialize_provider( + &self, + url: &str, + ) -> Result, ProviderError> { + // Apply timeout to the connection attempt + let timeout_duration = std::time::Duration::from_secs(self.timeout_seconds); + + match tokio::time::timeout( + timeout_duration, + OnlineClient::::from_url(url), + ) + .await + { + Ok(Ok(client)) => Ok(client), + Ok(Err(e)) => Err(ProviderError::NetworkConfiguration(format!( + "Failed to connect to {}: {}", + url, e + ))), + Err(_) => Err(ProviderError::Timeout), + } + } + + /// Helper method to retry RPC calls with exponential backoff + /// + /// Uses the generic retry_rpc_call utility to handle retries and provider failover + async fn retry_rpc_call( + &self, + operation_name: &str, + operation: F, + ) -> Result + where + F: Fn(OnlineClient) -> Fut, + Fut: std::future::Future>, + { + // Classify which errors should be retried + retry_rpc_call( + &self.selector, + operation_name, + Self::is_retriable_error, + Self::should_mark_provider_failed, + { + let self_clone = self.clone(); + move |url: &str| { + let self_clone = self_clone.clone(); + let url = url.to_string(); + async move { self_clone.initialize_provider(&url).await } + } + }, + operation, + Some(self.retry_config.clone()), + ) + .await + } +} + +impl AsRef for MidnightProvider { + fn as_ref(&self) -> &MidnightProvider { + self + } +} + +#[async_trait] +impl MidnightProviderTrait for MidnightProvider { + async fn get_balance( + &self, + seed: &WalletSeed, + context: &LedgerContext, + ) -> Result { + let wallet = context.wallet_from_seed(*seed); + let mut balance = 0u128; + + for (_, qualified_coin_info) in wallet.state.coins.iter() { + let coin_info: midnight_node_ledger_helpers::CoinInfo = (&*qualified_coin_info).into(); + + if coin_info.type_ == NATIVE_TOKEN { + balance = balance.saturating_add(coin_info.value); + } + } + + Ok(U256::from(balance)) + } + + async fn get_nonce(&self, _address: &str) -> Result { + self.retry_rpc_call("get_nonce", move |_api| async move { + log::warn!("get_nonce not yet implemented for Midnight provider"); + Ok(0) + }) + .await + } + + async fn get_block_number(&self) -> Result { + self.retry_rpc_call("get_block_number", |api| async move { + let block = + api.blocks().at_latest().await.map_err(|e| { + ProviderError::Other(format!("Failed to get latest block: {}", e)) + })?; + + Ok(block.number().into()) + }) + .await + } + + async fn send_transaction( + &self, + tx: Transaction, + ) -> Result { + let network_id = self.network_id; + self.retry_rpc_call("send_transaction", move |api| { + let tx_clone = tx.clone(); + async move { + // Serialize the transaction + let serialized = serialize(&tx_clone, network_id).map_err(|e| { + ProviderError::Other(format!("Failed to serialize transaction: {:?}", e)) + })?; + + let mn_tx = mn_meta::tx() + .midnight() + .send_mn_transaction(hex::encode(&serialized).into_bytes()); + + let unsigned_extrinsic = api.tx().create_unsigned(&mn_tx).map_err(|e| { + ProviderError::Other(format!("Failed to create extrinsic: {}", e)) + })?; + + let tx_hash_string = + format!("0x{}", hex::encode(unsigned_extrinsic.hash().as_bytes())); + + let validation_result = unsigned_extrinsic.validate().await.map_err(|e| { + ProviderError::Other(format!("Failed to validate transaction: {}", e)) + })?; + + // Check if validation result indicates success + match validation_result { + subxt::tx::ValidationResult::Valid(_) => {} + subxt::tx::ValidationResult::Invalid(e) => { + return Err(ProviderError::Other(format!( + "Transaction validation failed: {:?}", + e + ))); + } + subxt::tx::ValidationResult::Unknown(e) => { + return Err(ProviderError::Other(format!( + "Transaction validation unknown: {:?}", + e + ))); + } + } + + // Get the transaction hash from the midnight transaction + let tx_hash = tx_clone.transaction_hash(); + // TransactionHash doesn't implement Display, but we can serialize it + // and convert to hex to get the proper format + let tx_hash_bytes = serialize(&tx_hash, network_id).map_err(|e| { + ProviderError::Other(format!("Failed to serialize transaction hash: {:?}", e)) + })?; + let pallet_tx_hash = format!("0x{}", hex::encode(tx_hash_bytes)); + + // Submit the transaction (returns immediately after acceptance) + let submit_result = unsigned_extrinsic.submit().await; + + match submit_result { + Ok(_extrinsic_hash) => { + // Transaction was accepted, return immediately + // The actual status monitoring will be handled by handle_transaction_status + let result = TransactionSubmissionResult { + extrinsic_tx_hash: tx_hash_string, + pallet_tx_hash, + }; + + // Serialize to JSON string + let json_result = serde_json::to_string(&result).map_err(|e| { + ProviderError::Other(format!("Failed to serialize result: {}", e)) + })?; + + Ok(json_result) + } + Err(e) => Err(ProviderError::Other(format!( + "Failed to submit transaction: {}", + e + ))), + } + } + }) + .await + } + + async fn health_check(&self) -> Result { + match self.get_block_number().await { + Ok(_) => Ok(true), + Err(e) => Err(e), + } + } + + fn get_indexer_client(&self) -> &MidnightIndexerClient { + &self.indexer_client + } + + async fn get_block_by_hash( + &self, + hash: &str, + ) -> Result, ProviderError> { + match self.indexer_client.get_block_by_hash(hash).await { + Ok(Some(block)) => Ok(Some(block)), + Ok(None) => Ok(None), + Err(e) => Err(ProviderError::Other(format!( + "Failed to get block by hash: {}", + e + ))), + } + } + + /// Get transaction by hash + async fn get_transaction_by_hash( + &self, + hash: &str, + ) -> Result, ProviderError> { + match self.indexer_client.get_transaction_by_hash(hash).await { + Ok(Some(tx)) => Ok(Some(tx)), + Ok(None) => Ok(None), + Err(e) => Err(ProviderError::Other(format!( + "Failed to get transaction by hash: {}", + e + ))), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use lazy_static::lazy_static; + use std::{sync::Mutex, time::Duration}; + + lazy_static! { + static ref MIDNIGHT_TEST_ENV_MUTEX: Mutex<()> = Mutex::new(()); + } + + struct MidnightTestEnvGuard { + _mutex_guard: std::sync::MutexGuard<'static, ()>, + } + + impl MidnightTestEnvGuard { + fn new(mutex_guard: std::sync::MutexGuard<'static, ()>) -> Self { + std::env::set_var( + "API_KEY", + "test_api_key_for_evm_provider_new_this_is_long_enough_32_chars", + ); + std::env::set_var("REDIS_URL", "redis://test-dummy-url-for-evm-provider"); + + Self { + _mutex_guard: mutex_guard, + } + } + } + + impl Drop for MidnightTestEnvGuard { + fn drop(&mut self) { + std::env::remove_var("API_KEY"); + std::env::remove_var("REDIS_URL"); + } + } + + // Helper function to set up the test environment + fn setup_test_env() -> MidnightTestEnvGuard { + let guard = MIDNIGHT_TEST_ENV_MUTEX + .lock() + .unwrap_or_else(|e| e.into_inner()); + MidnightTestEnvGuard::new(guard) + } + + #[tokio::test] + async fn test_reqwest_error_conversion() { + // Create a reqwest timeout error + let client = reqwest::Client::new(); + let result = client + .get("https://www.openzeppelin.com/") + .timeout(Duration::from_millis(1)) + .send() + .await; + + assert!( + result.is_err(), + "Expected the send operation to result in an error." + ); + let err = result.unwrap_err(); + + assert!( + err.is_timeout(), + "The reqwest error should be a timeout. Actual error: {:?}", + err + ); + + let provider_error = ProviderError::from(err); + assert!( + matches!(provider_error, ProviderError::Timeout), + "ProviderError should be Timeout. Actual: {:?}", + provider_error + ); + } + + #[test] + fn test_new_provider() { + let _env_guard = setup_test_env(); + + let provider = MidnightProvider::new( + vec![RpcConfig::new("http://localhost:8545".to_string())], + IndexerUrls { + http: "http://localhost:8545".to_string(), + ws: "ws://localhost:8545".to_string(), + }, + NetworkId::TestNet, + 30, + ); + assert!(provider.is_ok()); + + // Test with invalid URL + let provider = MidnightProvider::new( + vec![RpcConfig::new("invalid-url".to_string())], + IndexerUrls { + http: "http://localhost:8545".to_string(), + ws: "ws://localhost:8545".to_string(), + }, + NetworkId::TestNet, + 30, + ); + assert!(provider.is_err()); + } + + #[test] + fn test_new_provider_with_timeout() { + let _env_guard = setup_test_env(); + + // Test with valid URL and timeout + let provider = MidnightProvider::new( + vec![RpcConfig::new("http://localhost:8545".to_string())], + IndexerUrls { + http: "http://localhost:8545".to_string(), + ws: "ws://localhost:8545".to_string(), + }, + NetworkId::TestNet, + 30, + ); + assert!(provider.is_ok()); + + // Test with invalid URL + let provider = MidnightProvider::new( + vec![RpcConfig::new("invalid-url".to_string())], + IndexerUrls { + http: "http://localhost:8545".to_string(), + ws: "ws://localhost:8545".to_string(), + }, + NetworkId::TestNet, + 30, + ); + assert!(provider.is_err()); + + // Test with zero timeout + let provider = MidnightProvider::new( + vec![RpcConfig::new("http://localhost:8545".to_string())], + IndexerUrls { + http: "http://localhost:8545".to_string(), + ws: "ws://localhost:8545".to_string(), + }, + NetworkId::TestNet, + 0, + ); + assert!(provider.is_ok()); + + // Test with large timeout + let provider = MidnightProvider::new( + vec![RpcConfig::new("http://localhost:8545".to_string())], + IndexerUrls { + http: "http://localhost:8545".to_string(), + ws: "ws://localhost:8545".to_string(), + }, + NetworkId::TestNet, + 3600, + ); + assert!(provider.is_ok()); + } + + #[test] + fn test_should_mark_provider_failed_server_errors() { + // 5xx errors should mark provider as failed + for status_code in 500..=599 { + let error = ProviderError::RequestError { + error: format!("Server error {}", status_code), + status_code, + }; + assert!( + MidnightProvider::should_mark_provider_failed(&error), + "Status code {} should mark provider as failed", + status_code + ); + } + } + + #[test] + fn test_should_mark_provider_failed_auth_errors() { + // Authentication/authorization errors should mark provider as failed + let auth_errors = [401, 403]; + for &status_code in &auth_errors { + let error = ProviderError::RequestError { + error: format!("Auth error {}", status_code), + status_code, + }; + assert!( + MidnightProvider::should_mark_provider_failed(&error), + "Status code {} should mark provider as failed", + status_code + ); + } + } + + #[test] + fn test_should_mark_provider_failed_not_found_errors() { + // 404 and 410 should mark provider as failed (endpoint issues) + let not_found_errors = [404, 410]; + for &status_code in ¬_found_errors { + let error = ProviderError::RequestError { + error: format!("Not found error {}", status_code), + status_code, + }; + assert!( + MidnightProvider::should_mark_provider_failed(&error), + "Status code {} should mark provider as failed", + status_code + ); + } + } + + #[test] + fn test_should_mark_provider_failed_client_errors_not_failed() { + // These 4xx errors should NOT mark provider as failed (client-side issues) + let client_errors = [400, 405, 413, 414, 415, 422, 429]; + for &status_code in &client_errors { + let error = ProviderError::RequestError { + error: format!("Client error {}", status_code), + status_code, + }; + assert!( + !MidnightProvider::should_mark_provider_failed(&error), + "Status code {} should NOT mark provider as failed", + status_code + ); + } + } + + #[test] + fn test_should_mark_provider_failed_other_error_types() { + // Test non-RequestError types - these should NOT mark provider as failed + let errors = [ + ProviderError::Timeout, + ProviderError::RateLimited, + ProviderError::BadGateway, + ProviderError::InvalidAddress("test".to_string()), + ProviderError::NetworkConfiguration("test".to_string()), + ProviderError::Other("test".to_string()), + ]; + + for error in errors { + assert!( + !MidnightProvider::should_mark_provider_failed(&error), + "Error type {:?} should NOT mark provider as failed", + error + ); + } + } + + #[test] + fn test_should_mark_provider_failed_edge_cases() { + // Test some edge case status codes + let edge_cases = [ + (200, false), // Success - shouldn't happen in error context but test anyway + (300, false), // Redirection + (418, false), // I'm a teapot - should not mark as failed + (451, false), // Unavailable for legal reasons - client issue + (499, false), // Client closed request - client issue + ]; + + for (status_code, should_fail) in edge_cases { + let error = ProviderError::RequestError { + error: format!("Edge case error {}", status_code), + status_code, + }; + assert_eq!( + MidnightProvider::should_mark_provider_failed(&error), + should_fail, + "Status code {} should {} mark provider as failed", + status_code, + if should_fail { "" } else { "NOT" } + ); + } + } + + #[test] + fn test_is_retriable_error_retriable_types() { + // These error types should be retriable + let retriable_errors = [ + ProviderError::Timeout, + ProviderError::RateLimited, + ProviderError::BadGateway, + ]; + + for error in retriable_errors { + assert!( + MidnightProvider::is_retriable_error(&error), + "Error type {:?} should be retriable", + error + ); + } + } + + #[test] + fn test_is_retriable_error_non_retriable_types() { + // These error types should NOT be retriable + let non_retriable_errors = [ + ProviderError::InvalidAddress("test".to_string()), + ProviderError::NetworkConfiguration("test".to_string()), + ProviderError::RequestError { + error: "Some error".to_string(), + status_code: 400, + }, + ]; + + for error in non_retriable_errors { + assert!( + !MidnightProvider::is_retriable_error(&error), + "Error type {:?} should NOT be retriable", + error + ); + } + } + + #[test] + fn test_is_retriable_error_message_based_detection() { + // Test errors that should be retriable based on message content + let retriable_messages = [ + "Connection timeout occurred", + "Network connection reset", + "Connection refused", + "TIMEOUT error happened", + "Connection was reset by peer", + ]; + + for message in retriable_messages { + let error = ProviderError::Other(message.to_string()); + assert!( + MidnightProvider::is_retriable_error(&error), + "Error with message '{}' should be retriable", + message + ); + } + } + + #[test] + fn test_is_retriable_error_message_based_non_retriable() { + // Test errors that should NOT be retriable based on message content + let non_retriable_messages = [ + "Invalid address format", + "Bad request parameters", + "Authentication failed", + "Method not found", + "Some other error", + ]; + + for message in non_retriable_messages { + let error = ProviderError::Other(message.to_string()); + assert!( + !MidnightProvider::is_retriable_error(&error), + "Error with message '{}' should NOT be retriable", + message + ); + } + } + + #[test] + fn test_is_retriable_error_case_insensitive() { + // Test that message-based detection is case insensitive + let case_variations = [ + "TIMEOUT", + "Timeout", + "timeout", + "CONNECTION", + "Connection", + "connection", + "RESET", + "Reset", + "reset", + ]; + + for message in case_variations { + let error = ProviderError::Other(message.to_string()); + assert!( + MidnightProvider::is_retriable_error(&error), + "Error with message '{}' should be retriable (case insensitive)", + message + ); + } + } +} diff --git a/src/services/provider/midnight/remote_prover.rs b/src/services/provider/midnight/remote_prover.rs new file mode 100644 index 000000000..477644648 --- /dev/null +++ b/src/services/provider/midnight/remote_prover.rs @@ -0,0 +1,107 @@ +//! +//! Remote proof server integration for Midnight zero-knowledge proofs. +//! +//! Provides a client for submitting transactions to a remote proof server and retrieving +//! zero-knowledge proofs required for transaction construction on the Midnight network. +//! Copied from + +use async_trait::async_trait; +use backoff::{future::retry, ExponentialBackoff}; +use midnight_node_ledger_helpers::*; + +/// Remote proof server client for generating zero-knowledge proofs +pub struct RemoteProofServer { + url: String, + network_id: NetworkId, +} + +impl RemoteProofServer { + /// Creates a new remote proof server client + pub fn new(url: String, network_id: NetworkId) -> Self { + Self { url, network_id } + } + + /// Serializes a transaction and its circuit keys for the proof server + pub async fn serialize_request_body( + &self, + tx: &Transaction, + resolver: &Resolver, + ) -> Vec { + let circuits_used = tx + .calls() + .map(|c| String::from_utf8_lossy(&c.entry_point).into_owned()) + .collect::>(); + let mut keys = std::collections::HashMap::new(); + for k in circuits_used.into_iter() { + let k = KeyLocation(std::borrow::Cow::Owned(k)); + let data = resolver.resolve(&k).await.expect("failed to resolve key"); + if let Some(data) = data { + keys.insert(k, data); + } + } + let mut bytes = Vec::new(); + mn_ledger_serialize::serialize(tx, &mut bytes, self.network_id) + .expect("failed to serialize transaction"); + mn_ledger_serialize::serialize(&keys, &mut bytes, self.network_id) + .expect("failed to serialize keys"); + bytes + } +} + +#[async_trait] +impl ProofProvider for RemoteProofServer { + async fn prove( + &self, + tx: Transaction, + _rng: StdRng, + resolver: &Resolver, + ) -> Transaction { + let url = reqwest::Url::parse(&self.url) + .expect("failed to parse proof server URL") + .join("prove-tx") + .unwrap(); + + let client = reqwest::ClientBuilder::new() + .pool_idle_timeout(None) + .build() + .unwrap(); + let response_bytes = retry(ExponentialBackoff::default(), || async { + let body = self.serialize_request_body(&tx, resolver).await; + + let resp = client + .post(url.clone()) + .body(body) + .send() + .await + .map_err(|e| { + println!("Proof Server Send Error: {:?}", e); + backoff::Error::transient(e) + })?; + + let resp_err = resp.error_for_status_ref().err(); + let resp_bytes = resp.bytes().await.map_err(|e| { + println!("Proof Server to Bytes Error: {:?}", e); + backoff::Error::transient(e) + })?; + + if let Some(e) = resp_err { + println!( + "Proof Server Response Error: {:?}. Bytes: {:?}", + e, resp_bytes + ); + return Err(backoff::Error::transient(e)); + } + + Ok::, backoff::Error>(resp_bytes.to_vec()) + }) + .await + .expect("failed to send request"); + + if response_bytes.is_empty() { + panic!("Proof server returned empty response"); + } + + deserialize(&response_bytes[..], self.network_id) + .expect("failed to deserialize transaction") + } +} diff --git a/src/services/provider/mod.rs b/src/services/provider/mod.rs index 474de6eeb..962ed293c 100644 --- a/src/services/provider/mod.rs +++ b/src/services/provider/mod.rs @@ -1,7 +1,10 @@ +use std::collections::HashMap; use std::num::ParseIntError; +use crate::config::network::IndexerUrls; use crate::config::ServerConfig; -use crate::models::{EvmNetwork, RpcConfig, SolanaNetwork, StellarNetwork}; +use crate::domain::to_midnight_network_id; +use crate::models::{EvmNetwork, MidnightNetwork, RpcConfig, SolanaNetwork, StellarNetwork}; use serde::Serialize; use thiserror::Error; @@ -16,6 +19,9 @@ pub use solana::*; mod stellar; pub use stellar::*; +mod midnight; +pub use midnight::*; + mod retry; pub use retry::*; @@ -169,6 +175,7 @@ pub trait NetworkConfiguration: Sized { fn new_provider( rpc_urls: Vec, timeout_seconds: u64, + metadata: Option<&HashMap>, ) -> Result; } @@ -185,6 +192,7 @@ impl NetworkConfiguration for EvmNetwork { fn new_provider( rpc_urls: Vec, timeout_seconds: u64, + _metadata: Option<&HashMap>, ) -> Result { EvmProvider::new(rpc_urls, timeout_seconds) } @@ -203,6 +211,7 @@ impl NetworkConfiguration for SolanaNetwork { fn new_provider( rpc_urls: Vec, timeout_seconds: u64, + _metadata: Option<&HashMap>, ) -> Result { SolanaProvider::new(rpc_urls, timeout_seconds) } @@ -221,11 +230,55 @@ impl NetworkConfiguration for StellarNetwork { fn new_provider( rpc_urls: Vec, timeout_seconds: u64, + _metadata: Option<&HashMap>, ) -> Result { StellarProvider::new(rpc_urls, timeout_seconds) } } +impl NetworkConfiguration for MidnightNetwork { + type Provider = MidnightProvider; + + fn public_rpc_urls(&self) -> Vec { + (*self) + .public_rpc_urls() + .map(|urls| urls.to_vec()) + .unwrap_or_default() + } + + fn new_provider( + rpc_urls: Vec, + timeout_seconds: u64, + metadata: Option<&HashMap>, + ) -> Result { + let indexer_urls = metadata.map(|metadata| IndexerUrls { + http: metadata.get("http").cloned().unwrap_or_default(), + ws: metadata.get("ws").cloned().unwrap_or_default(), + }); + + if indexer_urls.is_none() { + return Err(ProviderError::NetworkConfiguration( + "Indexer URLs are required for Midnight network".to_string(), + )); + } + + let network_id = + metadata.map(|metadata| metadata.get("network").cloned().unwrap_or_default()); + + if network_id.is_none() { + return Err(ProviderError::NetworkConfiguration( + "Network ID is required for Midnight network".to_string(), + )); + } + + // We can safely unwrap because we checked for None above + let network_id = to_midnight_network_id(&network_id.unwrap()); + + // We can safely unwrap because we checked for None above + MidnightProvider::new(rpc_urls, indexer_urls.unwrap(), network_id, timeout_seconds) + } +} + /// Creates a network-specific provider instance based on the provided configuration. /// /// # Type Parameters @@ -241,6 +294,7 @@ impl NetworkConfiguration for StellarNetwork { /// are used to configure the provider. If `None` or `Some` but empty, the function /// falls back to using the public RPC URLs defined by the `network`'s /// `NetworkConfiguration` implementation. +/// * `metadata`: An `Option>`. If `Some`, it is used to configure the provider. /// /// # Returns /// @@ -251,6 +305,7 @@ impl NetworkConfiguration for StellarNetwork { pub fn get_network_provider( network: &N, custom_rpc_urls: Option>, + metadata: Option<&HashMap>, ) -> Result { let rpc_timeout_ms = ServerConfig::from_env().rpc_timeout_ms; let timeout_seconds = rpc_timeout_ms / 1000; // Convert ms to s @@ -268,7 +323,7 @@ pub fn get_network_provider( } }; - N::new_provider(rpc_urls, timeout_seconds) + N::new_provider(rpc_urls, timeout_seconds, metadata) } #[cfg(test)] @@ -461,7 +516,7 @@ mod tests { setup_test_env(); let network = create_test_evm_network(); - let result = get_network_provider(&network, None); + let result = get_network_provider(&network, None, None); cleanup_test_env(); assert!(result.is_ok()); @@ -483,7 +538,7 @@ mod tests { weight: 1, }, ]; - let result = get_network_provider(&network, Some(custom_urls)); + let result = get_network_provider(&network, Some(custom_urls), None); cleanup_test_env(); assert!(result.is_ok()); @@ -496,7 +551,7 @@ mod tests { let network = create_test_evm_network(); let custom_urls: Vec = vec![]; - let result = get_network_provider(&network, Some(custom_urls)); + let result = get_network_provider(&network, Some(custom_urls), None); cleanup_test_env(); assert!(result.is_ok()); // Should fall back to public URLs @@ -508,7 +563,7 @@ mod tests { setup_test_env(); let network = create_test_solana_network("mainnet-beta"); - let result = get_network_provider(&network, None); + let result = get_network_provider(&network, None, None); cleanup_test_env(); assert!(result.is_ok()); @@ -520,7 +575,7 @@ mod tests { setup_test_env(); let network = create_test_solana_network("testnet"); - let result = get_network_provider(&network, None); + let result = get_network_provider(&network, None, None); cleanup_test_env(); assert!(result.is_ok()); @@ -542,7 +597,7 @@ mod tests { weight: 1, }, ]; - let result = get_network_provider(&network, Some(custom_urls)); + let result = get_network_provider(&network, Some(custom_urls), None); cleanup_test_env(); assert!(result.is_ok()); @@ -555,7 +610,7 @@ mod tests { let network = create_test_solana_network("testnet"); let custom_urls: Vec = vec![]; - let result = get_network_provider(&network, Some(custom_urls)); + let result = get_network_provider(&network, Some(custom_urls), None); cleanup_test_env(); assert!(result.is_ok()); // Should fall back to public URLs @@ -568,7 +623,7 @@ mod tests { setup_test_env(); let network = create_test_stellar_network(); - let result = get_network_provider(&network, None); // No custom URLs + let result = get_network_provider(&network, None, None); // No custom URLs cleanup_test_env(); assert!(result.is_ok()); // Should fall back to public URLs for testnet @@ -586,7 +641,7 @@ mod tests { RpcConfig::with_weight("http://custom-stellar-rpc2.example.com".to_string(), 50) .unwrap(), ]; - let result = get_network_provider(&network, Some(custom_urls)); + let result = get_network_provider(&network, Some(custom_urls), None); cleanup_test_env(); assert!(result.is_ok()); @@ -600,7 +655,7 @@ mod tests { let network = create_test_stellar_network(); let custom_urls: Vec = vec![]; // Empty custom URLs - let result = get_network_provider(&network, Some(custom_urls)); + let result = get_network_provider(&network, Some(custom_urls), None); cleanup_test_env(); assert!(result.is_ok()); // Should fall back to public URLs for mainnet @@ -617,7 +672,7 @@ mod tests { RpcConfig::with_weight("http://zero-weight-rpc.example.com".to_string(), 0).unwrap(), RpcConfig::new("http://active-rpc.example.com".to_string()), // Default weight 100 ]; - let result = get_network_provider(&network, Some(custom_urls)); + let result = get_network_provider(&network, Some(custom_urls), None); cleanup_test_env(); assert!(result.is_ok()); // active-rpc should be chosen } @@ -637,7 +692,7 @@ mod tests { // The current get_network_provider logic passes the custom_urls to N::new_provider if Some and not empty. // If custom_urls becomes effectively empty *inside* N::new_provider (like StellarProvider::new after filtering weights), // then N::new_provider is responsible for erroring or handling. - let result = get_network_provider(&network, Some(custom_urls)); + let result = get_network_provider(&network, Some(custom_urls), None); cleanup_test_env(); assert!(result.is_err()); match result.unwrap_err() { @@ -654,7 +709,7 @@ mod tests { setup_test_env(); let network = create_test_stellar_network(); let custom_urls = vec![RpcConfig::new("ftp://custom-ftp.example.com".to_string())]; - let result = get_network_provider(&network, Some(custom_urls)); + let result = get_network_provider(&network, Some(custom_urls), None); cleanup_test_env(); assert!(result.is_err()); match result.unwrap_err() { diff --git a/src/services/provider/retry.rs b/src/services/provider/retry.rs index fe61b4bdb..e8a541437 100644 --- a/src/services/provider/retry.rs +++ b/src/services/provider/retry.rs @@ -189,7 +189,7 @@ impl RetryConfig { /// /// # Returns /// * The result of the operation if successful, or an error -pub async fn retry_rpc_call( +pub async fn retry_rpc_call( selector: &RpcSelector, operation_name: &str, is_retriable_error: impl Fn(&E) -> bool, @@ -203,7 +203,8 @@ where E: std::fmt::Display + From, F: Fn(P) -> Fut, Fut: Future>, - I: Fn(&str) -> Result, + I: Fn(&str) -> IFut, + IFut: Future>, { let config = config.unwrap_or_else(RetryConfig::from_env); let total_providers = selector.provider_count(); @@ -223,7 +224,7 @@ where while failover_count <= max_failovers && selector.available_provider_count() > 0 { // Try to get and initialize a provider let (provider, provider_url) = - match get_provider(selector, operation_name, &provider_initializer) { + match get_provider(selector, operation_name, &provider_initializer).await { Ok((provider, url)) => (provider, url), Err(e) => { last_error = Some(e); @@ -340,14 +341,15 @@ where } /// Helper function to get and initialize a provider -fn get_provider( +async fn get_provider( selector: &RpcSelector, operation_name: &str, provider_initializer: &I, ) -> Result<(P, String), E> where E: std::fmt::Display + From, - I: Fn(&str) -> Result, + I: Fn(&str) -> IFut, + IFut: Future>, { // Get the next provider URL from the selector let provider_url = selector @@ -359,7 +361,7 @@ where })?; // Initialize the provider - let provider = provider_initializer(&provider_url).map_err(|e| { + let provider = provider_initializer(&provider_url).await.map_err(|e| { log::warn!( "Failed to initialize provider '{}' for operation '{}': {}", provider_url, @@ -771,8 +773,8 @@ mod tests { let _config = RetryConfig::new(3, 1, 100, 0); } - #[test] - fn test_get_provider() { + #[tokio::test] + async fn test_get_provider() { let _guard = setup_test_env(); let configs = vec![ @@ -781,20 +783,22 @@ mod tests { ]; let selector = RpcSelector::new(configs).expect("Failed to create selector"); - let initializer = - |url: &str| -> Result { Ok(format!("provider-{}", url)) }; + let initializer = |url: &str| { + let url = url.to_string(); + async move { Ok::(format!("provider-{}", url)) } + }; - let result = get_provider(&selector, "test_operation", &initializer); + let result = get_provider(&selector, "test_operation", &initializer).await; assert!(result.is_ok()); let (provider, url) = result.unwrap(); assert_eq!(url, "http://localhost:8545"); assert_eq!(provider, "provider-http://localhost:8545"); - let initializer = |_: &str| -> Result { - Err(TestError("Failed to initialize".to_string())) + let initializer = |_: &str| async move { + Err::(TestError("Failed to initialize".to_string())) }; - let result = get_provider(&selector, "test_operation", &initializer); + let result = get_provider(&selector, "test_operation", &initializer).await; assert!(result.is_err()); let err = result.unwrap_err(); assert!(format!("{}", err).contains("Failed to initialize")); @@ -999,7 +1003,10 @@ mod tests { ]; let selector = RpcSelector::new(configs).expect("Failed to create selector"); - let provider_initializer = |url: &str| -> Result { Ok(url.to_string()) }; + let provider_initializer = |url: &str| { + let url = url.to_string(); + async move { Ok::(url) } + }; // Operation that always fails with a non-retriable error let operation = @@ -1041,7 +1048,10 @@ mod tests { ]; let selector = RpcSelector::new(configs).expect("Failed to create selector"); - let provider_initializer = |url: &str| -> Result { Ok(url.to_string()) }; + let provider_initializer = |url: &str| { + let url = url.to_string(); + async move { Ok::(url) } + }; // Operation that always fails with a retriable error let operation = |_provider: String| async { Err(TestError("Retriable error".to_string())) }; @@ -1084,7 +1094,7 @@ mod tests { let attempts_clone = attempts.clone(); let provider_initializer = - |_url: &str| -> Result { Ok("mock_provider".to_string()) }; + |_url: &str| async move { Ok::("mock_provider".to_string()) }; let operation = move |_provider: String| { let attempts = attempts_clone.clone(); @@ -1125,10 +1135,14 @@ mod tests { let current_provider = Arc::new(Mutex::new(String::new())); let current_provider_clone = current_provider.clone(); - let provider_initializer = move |url: &str| -> Result { - let mut provider = current_provider_clone.lock().unwrap(); - *provider = url.to_string(); - Ok(url.to_string()) + let provider_initializer = move |url: &str| { + let current_provider_clone = current_provider_clone.clone(); + let url = url.to_string(); + async move { + let mut provider = current_provider_clone.lock().unwrap(); + *provider = url.clone(); + Ok::(url) + } }; let operation = move |provider: String| async move { @@ -1175,7 +1189,7 @@ mod tests { let selector = RpcSelector::new(configs).expect("Failed to create selector"); let provider_initializer = - |_: &str| -> Result { Ok("mock_provider".to_string()) }; + |_: &str| async move { Ok::("mock_provider".to_string()) }; let operation = |_: String| async { Err(TestError("Always fails".to_string())) }; @@ -1209,7 +1223,7 @@ mod tests { }; let provider_initializer = - |_url: &str| -> Result { Ok("mock_provider".to_string()) }; + |_url: &str| async move { Ok::("mock_provider".to_string()) }; let operation = |_provider: String| async move { Ok::<_, TestError>(42) }; @@ -1242,12 +1256,16 @@ mod tests { let attempt_count = Arc::new(AtomicU8::new(0)); let attempt_count_clone = attempt_count.clone(); - let provider_initializer = move |url: &str| -> Result { - let count = attempt_count_clone.fetch_add(1, AtomicOrdering::SeqCst); - if count == 0 && url.contains("8545") { - Err(TestError("First provider init failed".to_string())) - } else { - Ok(url.to_string()) + let provider_initializer = move |url: &str| { + let attempt_count_clone = attempt_count_clone.clone(); + let url = url.to_string(); + async move { + let count = attempt_count_clone.fetch_add(1, AtomicOrdering::SeqCst); + if count == 0 && url.contains("8545") { + Err(TestError("First provider init failed".to_string())) + } else { + Ok::(url) + } } }; @@ -1271,8 +1289,8 @@ mod tests { assert!(attempt_count.load(AtomicOrdering::SeqCst) >= 2); // Should have tried multiple providers } - #[test] - fn test_get_provider_selector_errors() { + #[tokio::test] + async fn test_get_provider_selector_errors() { let _guard = setup_test_env(); // Create selector with a single provider, select it, then mark it as failed @@ -1283,11 +1301,13 @@ mod tests { let _ = selector.get_current_url().unwrap(); // This selects the provider selector.mark_current_as_failed(); // Now mark it as failed - let provider_initializer = - |url: &str| -> Result { Ok(format!("provider-{}", url)) }; + let provider_initializer = |url: &str| { + let url = url.to_string(); + async move { Ok::(format!("provider-{}", url)) } + }; // Now get_provider should fail because the only provider is marked as failed - let result = get_provider(&selector, "test_operation", &provider_initializer); + let result = get_provider(&selector, "test_operation", &provider_initializer).await; assert!(result.is_err()); } @@ -1299,7 +1319,10 @@ mod tests { let configs = vec![RpcConfig::new("http://localhost:8545".to_string())]; let selector = RpcSelector::new(configs).expect("Failed to create selector"); - let provider_initializer = |url: &str| -> Result { Ok(url.to_string()) }; + let provider_initializer = |url: &str| { + let url = url.to_string(); + async move { Ok::(url) } + }; // Operation that always fails with a retriable error let operation = |_provider: String| async { Err(TestError("Always fails".to_string())) }; @@ -1347,7 +1370,10 @@ mod tests { ]; let selector = RpcSelector::new(configs).expect("Failed to create selector"); - let provider_initializer = |url: &str| -> Result { Ok(url.to_string()) }; + let provider_initializer = |url: &str| { + let url = url.to_string(); + async move { Ok::(url) } + }; // Operation that always fails with a retriable error let operation = |_provider: String| async { Err(TestError("Always fails".to_string())) }; @@ -1389,7 +1415,10 @@ mod tests { ]; let selector = RpcSelector::new(configs).expect("Failed to create selector"); - let provider_initializer = |url: &str| -> Result { Ok(url.to_string()) }; + let provider_initializer = |url: &str| { + let url = url.to_string(); + async move { Ok::(url) } + }; // Operation that fails with a non-retriable error that SHOULD mark provider as failed let operation = |_provider: String| async move { @@ -1431,7 +1460,10 @@ mod tests { ]; let selector = RpcSelector::new(configs).expect("Failed to create selector"); - let provider_initializer = |url: &str| -> Result { Ok(url.to_string()) }; + let provider_initializer = |url: &str| { + let url = url.to_string(); + async move { Ok::(url) } + }; // Operation that fails with a non-retriable error that should NOT mark provider as failed let operation = |_provider: String| async move { @@ -1473,7 +1505,10 @@ mod tests { ]; let selector = RpcSelector::new(configs).expect("Failed to create selector"); - let provider_initializer = |url: &str| -> Result { Ok(url.to_string()) }; + let provider_initializer = |url: &str| { + let url = url.to_string(); + async move { Ok::(url) } + }; // Operation that always fails with a retriable error let operation = @@ -1516,7 +1551,10 @@ mod tests { ]; let selector = RpcSelector::new(configs).expect("Failed to create selector"); - let provider_initializer = |url: &str| -> Result { Ok(url.to_string()) }; + let provider_initializer = |url: &str| { + let url = url.to_string(); + async move { Ok::(url) } + }; let operation = |_provider: String| async move { Err(TestError("Critical network error".to_string())) }; @@ -1574,7 +1612,10 @@ mod tests { let configs = vec![RpcConfig::new("http://localhost:8545".to_string())]; let selector = RpcSelector::new(configs).expect("Failed to create selector"); - let provider_initializer = |url: &str| -> Result { Ok(url.to_string()) }; + let provider_initializer = |url: &str| { + let url = url.to_string(); + async move { Ok::(url) } + }; // Operation that fails with a non-retriable error that SHOULD mark provider as failed let operation = @@ -1623,7 +1664,10 @@ mod tests { let attempt_count = Arc::new(AtomicU8::new(0)); let attempt_count_clone = attempt_count.clone(); - let provider_initializer = |url: &str| -> Result { Ok(url.to_string()) }; + let provider_initializer = |url: &str| { + let url = url.to_string(); + async move { Ok::(url) } + }; // Operation that always fails with errors that should mark provider as failed let operation = move |_provider: String| { diff --git a/src/services/provider/solana/mod.rs b/src/services/provider/solana/mod.rs index 620f7e0cd..24c1e38ca 100644 --- a/src/services/provider/solana/mod.rs +++ b/src/services/provider/solana/mod.rs @@ -134,7 +134,7 @@ pub trait SolanaProviderTrait: Send + Sync { async fn calculate_total_fee(&self, message: &Message) -> Result; } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct SolanaProvider { // RPC selector for handling multiple client connections selector: RpcSelector, @@ -297,9 +297,13 @@ impl SolanaProvider { operation_name, is_retriable, |_| false, // TODO: implement fn to mark provider failed based on error - |url| match self.initialize_provider(url) { - Ok(provider) => Ok(provider), - Err(e) => Err(e), + { + let self_clone = self.clone(); + move |url: &str| { + let self_clone = self_clone.clone(); + let url = url.to_string(); + async move { self_clone.initialize_provider(&url) } + } }, operation, Some(self.retry_config.clone()), @@ -563,20 +567,20 @@ mod tests { use std::sync::Mutex; lazy_static! { - static ref EVM_TEST_ENV_MUTEX: Mutex<()> = Mutex::new(()); + static ref SOLANA_TEST_ENV_MUTEX: Mutex<()> = Mutex::new(()); } - struct EvmTestEnvGuard { + struct SolanaTestEnvGuard { _mutex_guard: std::sync::MutexGuard<'static, ()>, } - impl EvmTestEnvGuard { + impl SolanaTestEnvGuard { fn new(mutex_guard: std::sync::MutexGuard<'static, ()>) -> Self { std::env::set_var( "API_KEY", - "test_api_key_for_evm_provider_new_this_is_long_enough_32_chars", + "test_api_key_for_solana_provider_new_this_is_long_enough_32_chars", ); - std::env::set_var("REDIS_URL", "redis://test-dummy-url-for-evm-provider"); + std::env::set_var("REDIS_URL", "redis://test-dummy-url-for-solana-provider"); Self { _mutex_guard: mutex_guard, @@ -584,7 +588,7 @@ mod tests { } } - impl Drop for EvmTestEnvGuard { + impl Drop for SolanaTestEnvGuard { fn drop(&mut self) { std::env::remove_var("API_KEY"); std::env::remove_var("REDIS_URL"); @@ -592,9 +596,11 @@ mod tests { } // Helper function to set up the test environment - fn setup_test_env() -> EvmTestEnvGuard { - let guard = EVM_TEST_ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); - EvmTestEnvGuard::new(guard) + fn setup_test_env() -> SolanaTestEnvGuard { + let guard = SOLANA_TEST_ENV_MUTEX + .lock() + .unwrap_or_else(|e| e.into_inner()); + SolanaTestEnvGuard::new(guard) } fn get_funded_keypair() -> Keypair { diff --git a/src/services/signer/midnight/local_signer.rs b/src/services/signer/midnight/local_signer.rs new file mode 100644 index 000000000..b29cc9266 --- /dev/null +++ b/src/services/signer/midnight/local_signer.rs @@ -0,0 +1,228 @@ +//! # Midnight Local Signer Implementation +//! +//! This module provides a local signer implementation for Midnight transactions + +use crate::{ + domain::{ + SignDataRequest, SignDataResponse, SignTransactionResponse, + SignTransactionResponseMidnight, SignTypedDataRequest, + }, + models::{Address, MidnightAddress, NetworkTransactionData, SignerError, SignerRepoModel}, + services::Signer, +}; +use async_trait::async_trait; +use ed25519_dalek::Signer as Ed25519Signer; +use ed25519_dalek::{ed25519::signature::SignerMut, SigningKey}; +use eyre::Result; +use midnight_node_ledger_helpers::{DefaultDB, NetworkId, Wallet, WalletKind, WalletSeed}; +use sha2::{Digest, Sha256}; +use std::convert::TryInto; + +/// Local signer that stores the wallet seed and wallet in memory. +/// +/// # Security Considerations +/// The wallet seed and its derived wallet are stored in memory. In production environments, +/// consider using hardware security modules or secure enclaves for key storage. +pub struct LocalSigner { + network_id: NetworkId, + wallet_seed: WalletSeed, + wallet: Wallet, +} + +impl LocalSigner { + pub fn new(signer_model: &SignerRepoModel, network_id: NetworkId) -> Result { + let config = signer_model + .config + .get_local() + .ok_or_else(|| SignerError::Configuration("Local config not found".into()))?; + + let key_slice = config.raw_key.borrow(); + + if key_slice.len() != 32 { + return Err(SignerError::Configuration( + "Private key must be 32 bytes".into(), + )); + } + + let mut key_bytes = [0u8; 32]; + key_bytes.copy_from_slice(&key_slice); + + let wallet_seed = WalletSeed::from(key_bytes); + + // Clear the temporary key_bytes + key_bytes.iter_mut().for_each(|b| *b = 0); + + // Create the wallet from the seed + // Using index 0 and NoLegacy wallet kind as default + let wallet = Wallet::new(wallet_seed, 0, WalletKind::NoLegacy); + + Ok(Self { + network_id, + wallet_seed, + wallet, + }) + } + + /// Returns a reference to the wallet seed. + /// + /// # Security Note + /// This returns a reference to sensitive cryptographic material. + /// Avoid dereferencing (*) unless absolutely necessary, as it creates + /// copies in memory that could be exposed. Consider using secure + /// key storage solutions in production environments. + pub fn wallet_seed(&self) -> &WalletSeed { + &self.wallet_seed + } +} + +#[async_trait] +impl Signer for LocalSigner { + async fn address(&self) -> Result { + // Create the Midnight address using the stored wallet + let midnight_address = MidnightAddress::from_wallet(&self.wallet, self.network_id); + + // Encode the address to bech32m format + let encoded_address = midnight_address.encode(); + + Ok(Address::Midnight(encoded_address)) + } + + async fn sign_transaction( + &self, + tx: NetworkTransactionData, + ) -> Result { + let midnight_data = tx + .get_midnight_transaction_data() + .map_err(|e| SignerError::SigningError(format!("failed to get tx data: {e}")))?; + + // Extract the raw transaction bytes + let raw_tx = midnight_data + .raw + .as_ref() + .ok_or_else(|| SignerError::SigningError("Transaction data not serialized".into()))?; + + let signing_key = SigningKey::from_bytes(&self.wallet_seed.0); + + // Hash the transaction data with SHA-256 + let mut hasher = Sha256::new(); + hasher.update(raw_tx); + let tx_hash = hasher.finalize(); + + // Sign the transaction hash + let signature = signing_key.sign(&tx_hash); + + // Convert signature to hex string + let signature_hex = hex::encode(signature.to_bytes()); + + Ok(SignTransactionResponse::Midnight( + SignTransactionResponseMidnight { + signature: signature_hex, + }, + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + config::network::IndexerUrls, + models::{ + EvmTransactionData, LocalSignerConfig, MidnightNetwork, MidnightTransactionData, + SignerConfig, + }, + }; + use secrets::SecretVec; + + fn _create_test_midnight_network() -> MidnightNetwork { + MidnightNetwork { + network: "testnet".to_string(), + rpc_urls: vec!["https://rpc.testnet.midnight.org".to_string()], + explorer_urls: None, + average_blocktime_ms: 5000, + is_testnet: true, + tags: vec![], + indexer_urls: IndexerUrls { + http: "https://indexer.testnet.midnight.org".to_string(), + ws: "wss://indexer.testnet.midnight.org".to_string(), + }, + prover_url: "http://localhost:6300".to_string(), + } + } + + fn create_test_signer_model() -> SignerRepoModel { + let seed = vec![1u8; 32]; + let raw_key = SecretVec::new(32, |v| v.copy_from_slice(&seed)); + SignerRepoModel { + id: "test".to_string(), + config: SignerConfig::Local(LocalSignerConfig { raw_key }), + } + } + + #[tokio::test] + async fn test_new_local_signer_and_address() { + let signer = LocalSigner::new(&create_test_signer_model(), NetworkId::TestNet).unwrap(); + let address = signer.address().await.unwrap(); + match address { + Address::Midnight(addr) => { + assert!(addr.starts_with("mn_")); + assert!(!addr.is_empty()); + // Verify the address format includes network type + assert!(addr.contains("shield-addr_test")); + } + _ => panic!("Expected Midnight address"), + } + } + + #[tokio::test] + async fn test_sign_transaction_invalid_type() { + let signer = LocalSigner::new(&create_test_signer_model(), NetworkId::TestNet).unwrap(); + let evm_tx = NetworkTransactionData::Evm(EvmTransactionData::default()); + let result = signer.sign_transaction(evm_tx).await; + assert!(result.is_err()); + let err = result.err().unwrap(); + assert!(format!("{}", err).contains("failed to get tx data")); + } + + #[tokio::test] + async fn test_sign_transaction_midnight() { + let signer = LocalSigner::new(&create_test_signer_model(), NetworkId::TestNet).unwrap(); + let _source_account = match signer.address().await.unwrap() { + Address::Midnight(addr) => addr, + _ => panic!("Expected Midnight address"), + }; + + // Create test transaction data with raw bytes + let test_tx_bytes = vec![1, 2, 3, 4, 5, 6, 7, 8]; + let tx_data = MidnightTransactionData { + hash: None, + pallet_hash: None, + block_hash: None, + segment_results: None, + raw: Some(test_tx_bytes), + signature: None, + guaranteed_offer: None, + intents: vec![], + fallible_offers: vec![], + }; + + let response = signer + .sign_transaction(NetworkTransactionData::Midnight(tx_data)) + .await + .unwrap(); + + match response { + SignTransactionResponse::Midnight(res) => { + let sig = res.signature; + // Ed25519 signatures are 64 bytes, hex encoded = 128 chars + assert_eq!(sig.len(), 128); + // Verify it's valid hex + assert!(hex::decode(&sig).is_ok()); + // signature should not be all zeros + let sig_bytes = hex::decode(&sig).unwrap(); + assert!(sig_bytes.iter().any(|&b| b != 0)); + } + _ => panic!("Expected Midnight signature response"), + } + } +} diff --git a/src/services/signer/midnight/mod.rs b/src/services/signer/midnight/mod.rs new file mode 100644 index 000000000..6c3581cb0 --- /dev/null +++ b/src/services/signer/midnight/mod.rs @@ -0,0 +1,87 @@ +// openzeppelin-relayer/src/services/signer/midnight/mod.rs +//! Midnight signer implementation (local keystore) + +mod local_signer; +use async_trait::async_trait; +use local_signer::*; + +use crate::{ + domain::{SignDataRequest, SignDataResponse, SignTransactionResponse, SignTypedDataRequest}, + models::{Address, NetworkTransactionData, SignerConfig, SignerRepoModel}, + services::signer::{SignerError, SignerFactoryError}, + services::Signer, +}; +use midnight_node_ledger_helpers::NetworkId; + +use super::DataSignerTrait; + +/// Trait for Midnight-specific signer functionality +pub trait MidnightSignerTrait: Signer { + /// Get a reference to the wallet seed + fn wallet_seed(&self) -> &midnight_node_ledger_helpers::WalletSeed; +} + +pub enum MidnightSigner { + Local(LocalSigner), + Vault(LocalSigner), + VaultCloud(LocalSigner), +} + +#[async_trait] +impl Signer for MidnightSigner { + async fn address(&self) -> Result { + match self { + Self::Local(s) | Self::Vault(s) | Self::VaultCloud(s) => s.address().await, + } + } + + async fn sign_transaction( + &self, + tx: NetworkTransactionData, + ) -> Result { + match self { + Self::Local(s) | Self::Vault(s) | Self::VaultCloud(s) => s.sign_transaction(tx).await, + } + } +} + +impl MidnightSignerTrait for MidnightSigner { + fn wallet_seed(&self) -> &midnight_node_ledger_helpers::WalletSeed { + match self { + Self::Local(s) | Self::Vault(s) | Self::VaultCloud(s) => s.wallet_seed(), + } + } +} + +pub struct MidnightSignerFactory; + +impl MidnightSignerFactory { + pub fn create_midnight_signer( + m: &SignerRepoModel, + network_id: NetworkId, + ) -> Result { + let signer = match m.config { + SignerConfig::Local(_) + | SignerConfig::Test(_) + | SignerConfig::Vault(_) + | SignerConfig::VaultCloud(_) => { + MidnightSigner::Local(LocalSigner::new(m, network_id)?) + } + SignerConfig::AwsKms(_) => { + return Err(SignerFactoryError::UnsupportedType("AWS KMS".into())) + } + SignerConfig::VaultTransit(_) => { + return Err(SignerFactoryError::UnsupportedType("Vault Transit".into())) + } + SignerConfig::Turnkey(_) => { + return Err(SignerFactoryError::UnsupportedType("Turnkey".into())) + } + SignerConfig::GoogleCloudKms(_) => { + return Err(SignerFactoryError::UnsupportedType( + "Google Cloud KMS".into(), + )) + } + }; + Ok(signer) + } +} diff --git a/src/services/signer/mod.rs b/src/services/signer/mod.rs index 1832fc24d..07e6e869a 100644 --- a/src/services/signer/mod.rs +++ b/src/services/signer/mod.rs @@ -3,7 +3,7 @@ //! //! This module provides: //! - Common signer traits for different blockchain networks -//! - Network-specific signer implementations (EVM, Solana, Stellar) +//! - Network-specific signer implementations (EVM, Solana, Stellar, Midnight) //! - Factory methods for creating signers //! - Error handling for signing operations //! @@ -17,7 +17,10 @@ //! │ |── LocalSigner //! | |── GoogleCloudKmsSigner //! │ └── VaultTransitSigner -//! └── StellarSigner +//! ├── StellarSigner +//! │ └── LocalSigner +//! └── MidnightSigner +//! └── LocalSigner #![allow(unused_imports)] use async_trait::async_trait; @@ -36,8 +39,14 @@ pub use solana::*; mod stellar; pub use stellar::*; +mod midnight; +pub use midnight::*; + use crate::{ - domain::{SignDataRequest, SignDataResponse, SignTransactionResponse, SignTypedDataRequest}, + domain::{ + to_midnight_network_id, SignDataRequest, SignDataResponse, SignTransactionResponse, + SignTypedDataRequest, + }, models::{ Address, NetworkTransactionData, NetworkType, SignerError, SignerFactoryError, SignerRepoModel, SignerType, TransactionError, TransactionRepoModel, @@ -63,6 +72,7 @@ pub enum NetworkSigner { Evm(EvmSigner), Solana(SolanaSigner), Stellar(StellarSigner), + Midnight(MidnightSigner), } #[async_trait] @@ -72,6 +82,7 @@ impl Signer for NetworkSigner { Self::Evm(signer) => signer.address().await, Self::Solana(signer) => signer.address().await, Self::Stellar(signer) => signer.address().await, + Self::Midnight(signer) => signer.address().await, } } @@ -83,6 +94,7 @@ impl Signer for NetworkSigner { Self::Evm(signer) => signer.sign_transaction(transaction).await, Self::Solana(signer) => signer.sign_transaction(transaction).await, Self::Stellar(signer) => signer.sign_transaction(transaction).await, + Self::Midnight(signer) => signer.sign_transaction(transaction).await, } } } @@ -105,6 +117,9 @@ impl DataSignerTrait for NetworkSigner { Self::Stellar(_) => Err(SignerError::UnsupportedTypeError( "Stellar: sign data not supported".into(), )), + Self::Midnight(_) => Err(SignerError::UnsupportedTypeError( + "Midnight: sign data not supported".into(), + )), } } @@ -123,6 +138,9 @@ impl DataSignerTrait for NetworkSigner { Self::Stellar(_) => Err(SignerError::UnsupportedTypeError( "Stellar: Signing typed data not supported".into(), )), + Self::Midnight(_) => Err(SignerError::UnsupportedTypeError( + "Midnight: Signing typed data not supported".into(), + )), } } } @@ -133,6 +151,7 @@ impl SignerFactory { pub fn create_signer( network_type: &NetworkType, signer_model: &SignerRepoModel, + network: &str, ) -> Result { let signer = match network_type { NetworkType::Evm => { @@ -147,6 +166,13 @@ impl SignerFactory { let stellar_signer = StellarSignerFactory::create_stellar_signer(signer_model)?; NetworkSigner::Stellar(stellar_signer) } + NetworkType::Midnight => { + let midnight_signer = MidnightSignerFactory::create_midnight_signer( + signer_model, + to_midnight_network_id(network), + )?; + NetworkSigner::Midnight(midnight_signer) + } }; Ok(signer) diff --git a/src/services/sync/midnight/handler/events.rs b/src/services/sync/midnight/handler/events.rs new file mode 100644 index 000000000..058cb33b6 --- /dev/null +++ b/src/services/sync/midnight/handler/events.rs @@ -0,0 +1,263 @@ +//! Event system for wallet synchronization. +//! +//! This module defines the core event types, event handler traits, and the event dispatcher used +//! throughout the wallet sync process. Events are used to decouple the sync logic from the +//! processing of transactions, Merkle updates, progress, and errors. Sync strategies and services +//! emit events, which are then handled by registered event handlers. This enables flexible +//! composition and extension of sync behavior. +//! +//! The event system is central to the orchestration of wallet sync, allowing for modular and +//! testable components. + +use crate::services::midnight::{ + indexer::{ + CollapsedUpdateInfo, IndexerError, TransactionData, WalletSyncEvent as IndexerEvent, + ZswapChainStateUpdate, + }, + utils::{parse_collapsed_update, process_transaction}, + SyncError, +}; + +use log::error; +use midnight_ledger_prototype::transient_crypto::merkle_tree::MerkleTreeCollapsedUpdate; +use midnight_node_ledger_helpers::{DefaultDB, NetworkId, Proof, Transaction}; +use std::sync::{Arc, Mutex}; + +use crate::services::midnight::indexer::ApplyStage; + +/// Enum to track updates in chronological order during wallet synchronization. +/// +/// This enum is used to buffer and order both transaction and Merkle tree updates +/// as they are received from the indexer, ensuring correct application order. +/// +/// - `Transaction`: Represents a transaction update with its index, transaction data, and apply stage. +/// - `MerkleUpdate`: Represents a Merkle tree update with its index and update data. +#[derive(Clone)] +pub enum ChronologicalUpdate { + Transaction { + index: u64, + tx: Box>, + apply_stage: Option, + }, + MerkleUpdate { + index: u64, + update: Box, + }, +} + +/// Events that occur during wallet synchronization +pub enum SyncEvent { + /// A relevant transaction was received + TransactionReceived { + blockchain_index: u64, + transaction_data: TransactionData, + }, + /// A Merkle tree update was received + MerkleUpdateReceived { + blockchain_index: u64, + update_info: CollapsedUpdateInfo, + }, + /// Progress update from the indexer + ProgressUpdate { + highest_index: u64, + highest_relevant_wallet_index: u64, + }, + /// Sync has completed + SyncCompleted, + /// An error occurred during sync + SyncError { error: IndexerError }, +} + +/// Trait for handling sync events. +/// +/// Implementors receive all sync events and can perform side effects or state updates. +#[async_trait::async_trait] +pub trait SyncEventHandler: Send + Sync { + /// Handle a sync event. + /// + /// This method is called for every event dispatched by the orchestrator or sync strategy. + async fn handle(&mut self, event: &SyncEvent) -> Result<(), SyncError>; + + /// Get the name of this handler for logging and diagnostics. + fn name(&self) -> &'static str; +} + +/// Enum representing all possible event handlers +pub enum EventHandlerType { + /// The main event handler that buffers updates + EventHandler { + network: NetworkId, + updates_buffer: Arc>>, + }, + // Add more handler variants as needed in the future +} + +impl EventHandlerType { + /// Handle a sync event based on the handler type + async fn handle(&mut self, event: &SyncEvent) -> Result<(), SyncError> { + match self { + EventHandlerType::EventHandler { + network, + updates_buffer, + } => { + match event { + SyncEvent::TransactionReceived { + blockchain_index, + transaction_data, + } => { + // Process transaction and buffer it (don't apply yet) + if let Some(tx) = process_transaction(transaction_data, *network)? { + // Buffer the transaction for later application with its apply stage + updates_buffer + .lock() + .unwrap() + .push(ChronologicalUpdate::Transaction { + index: *blockchain_index, + tx: Box::new(tx), + apply_stage: transaction_data.apply_stage.clone(), + }); + } + } + SyncEvent::MerkleUpdateReceived { + update_info, + blockchain_index, + } => { + // Process and buffer merkle update (don't apply yet) + let update = parse_collapsed_update(update_info, *network)?; + + // Buffer the update for later application + updates_buffer + .lock() + .unwrap() + .push(ChronologicalUpdate::MerkleUpdate { + index: *blockchain_index, + update: Box::new(update), + }); + } + SyncEvent::SyncCompleted => { + // Sync completed, no additional processing needed + } + _ => {} + } + Ok(()) + } + } + } + + /// Get the name of this handler for logging and diagnostics + fn name(&self) -> &'static str { + match self { + EventHandlerType::EventHandler { .. } => "EventHandler", + } + } +} + +/// Event dispatcher that manages multiple event handlers. +/// +/// The dispatcher allows multiple handlers to be registered and ensures all are called for each event. +/// This enables logging, state updates, and persistence to be handled independently. +#[derive(Default)] +pub struct EventDispatcher { + handlers: Vec, +} + +impl EventDispatcher { + /// Create a new, empty event dispatcher. + pub fn new() -> Self { + Default::default() + } + + /// Register a new event handler. + /// + /// Handlers are called in the order they are registered. + pub fn register_handler(&mut self, handler: EventHandlerType) { + self.handlers.push(handler); + } + + /// Dispatch an event to all registered handlers. + /// + /// Errors from handlers are logged, but do not stop other handlers from running. + pub async fn dispatch(&mut self, event: &SyncEvent) -> Result<(), SyncError> { + for handler in &mut self.handlers { + if let Err(e) = handler.handle(event).await { + error!("Handler {} failed to process event: {}", handler.name(), e); + // Continue processing with other handlers + } + } + Ok(()) + } +} + +/// Convert indexer events to sync events. +/// +/// This function translates low-level indexer events into one or more high-level sync events, +/// ensuring Merkle updates are processed before transactions for consistency. +pub fn convert_indexer_event(event: IndexerEvent) -> Vec { + let mut sync_events = Vec::new(); + + match event { + IndexerEvent::ViewingUpdate { + type_name: _, + index, + update, + } => { + // IMPORTANT: Process merkle updates FIRST, then transactions + // This ensures the merkle tree state is correct before transactions are applied + + // First, collect all merkle updates + for update_item in &update { + if let ZswapChainStateUpdate::MerkleTreeCollapsedUpdate { + protocol_version, + start, + end, + update, + } = update_item + { + if !update.is_empty() { + let update_info = CollapsedUpdateInfo { + blockchain_index: index, + protocol_version: *protocol_version, + start: *start, + end: *end, + update_data: update.clone(), + }; + sync_events.push(SyncEvent::MerkleUpdateReceived { + blockchain_index: index, + update_info, + }); + } + } + } + + // Then, collect all transactions + for update_item in update { + if let ZswapChainStateUpdate::RelevantTransaction { + transaction, + start: _, + end: _, + } = update_item + { + // Use the ViewingUpdate's index as the blockchain position + // All transactions in this update occurred at the same blockchain index + sync_events.push(SyncEvent::TransactionReceived { + blockchain_index: index, + transaction_data: transaction, + }); + } + } + } + IndexerEvent::ProgressUpdate { + type_name: _, + highest_index, + highest_relevant_index: _, + highest_relevant_wallet_index, + } => { + sync_events.push(SyncEvent::ProgressUpdate { + highest_index, + highest_relevant_wallet_index, + }); + } + } + + sync_events +} diff --git a/src/services/sync/midnight/handler/manager.rs b/src/services/sync/midnight/handler/manager.rs new file mode 100644 index 000000000..e841da6e4 --- /dev/null +++ b/src/services/sync/midnight/handler/manager.rs @@ -0,0 +1,344 @@ +use std::sync::{Arc, Mutex}; + +use crate::{ + repositories::{InMemorySyncState, SyncStateTrait}, + services::midnight::{ + handler::{ + ChronologicalUpdate, EventDispatcher, EventHandlerType, ProgressTracker, SyncConfig, + SyncStrategy, + }, + indexer::MidnightIndexerClient, + utils::derive_viewing_key, + SyncError, + }, +}; + +use log::{debug, info, warn}; +use midnight_node_ledger_helpers::{ + mn_ledger_serialize::{deserialize, serialize}, + DefaultDB, LedgerContext, LedgerState, NetworkId, WalletSeed, WalletState, +}; + +/// Main sync manager that coordinates all sync components. +/// +/// This struct is the entry point for wallet synchronization. It manages the lifecycle of the sync +/// process, wires together all services, and ensures that events are handled and state is updated +/// correctly. It is responsible for selecting the sync strategy, managing persistence, and tracking progress. +pub struct SyncManager { + // The ledger context + // We use an Arc to allow for multiple references to the same context + context: Arc>, + // The wallet seed + seed: WalletSeed, + // The sync strategy + strategy: S, + // The network + network: NetworkId, + // The sync state store + sync_state_store: Arc, + // The relayer ID for tracking sync state + relayer_id: String, +} + +impl SyncManager { + /// Serialize the current ledger context to bytes + fn serialize_context(&self) -> Result, SyncError> { + // Serialize the wallet state for the current seed + let wallet_state = { + let wallets_guard = self + .context + .wallets + .lock() + .map_err(|e| SyncError::SyncError(format!("Failed to lock wallets: {}", e)))?; + + wallets_guard + .get(&self.seed) + .map(|wallet| { + // We only serialize the wallet state, not the secret keys + let mut state_bytes = Vec::new(); + serialize(&wallet.state, &mut state_bytes, self.network).map_err(|e| { + SyncError::SyncError(format!("Failed to serialize wallet state: {:?}", e)) + })?; + Ok::, SyncError>(state_bytes) + }) + .transpose()? + }; + + // Serialize the ledger state + let ledger_state_bytes = { + let ledger_state_guard = + self.context.ledger_state.lock().map_err(|e| { + SyncError::SyncError(format!("Failed to lock ledger state: {}", e)) + })?; + + let mut bytes = Vec::new(); + serialize(&*ledger_state_guard, &mut bytes, self.network).map_err(|e| { + SyncError::SyncError(format!("Failed to serialize ledger state: {:?}", e)) + })?; + bytes + }; + + // Combine wallet state and ledger state into a single serialized context + bincode::serialize(&(wallet_state, ledger_state_bytes)) + .map_err(|e| SyncError::SyncError(format!("Failed to serialize context: {}", e))) + } + + /// Restore the ledger context from serialized bytes + fn restore_context(&self, serialized_context: &[u8]) -> Result<(), SyncError> { + // Deserialize the combined context + match bincode::deserialize::<(Option>, Vec)>(serialized_context) { + Ok((wallet_state_bytes, ledger_state_bytes)) => { + // Restore ledger state + let mut reader = &ledger_state_bytes[..]; + match deserialize::, _>(&mut reader, self.network) { + Ok(ledger_state) => { + if let Ok(mut ledger_state_guard) = self.context.ledger_state.lock() { + *ledger_state_guard = ledger_state; + debug!("Successfully restored ledger state"); + } + } + Err(e) => { + warn!( + "Failed to deserialize ledger state: {:?}, starting fresh", + e + ); + } + } + + // Restore wallet state if available + if let Some(state_bytes) = wallet_state_bytes { + let mut reader = &state_bytes[..]; + match deserialize::, _>(&mut reader, self.network) { + Ok(wallet_state) => { + if let Ok(mut wallets_guard) = self.context.wallets.lock() { + if let Some(wallet) = wallets_guard.get_mut(&self.seed) { + wallet.update_state(wallet_state); + debug!("Successfully restored wallet state"); + } + } + } + Err(e) => { + warn!( + "Failed to deserialize wallet state: {:?}, starting fresh", + e + ); + } + } + } + Ok(()) + } + Err(e) => { + warn!("Failed to deserialize context: {}, starting fresh", e); + Ok(()) + } + } + } + + /// Create a new manager with the specified sync strategy and configuration. + /// + /// This method initializes all services and selects the sync strategy based on the provided options. + /// It will also attempt to restore the ledger context from a previous sync if available. + pub fn new( + indexer_client: &MidnightIndexerClient, + seed: &WalletSeed, + network: NetworkId, + sync_state_store: Arc, + relayer_id: String, + ) -> Result { + // Temporarily add random destination seed to the context until Midnight fixes this + // mn_shield-addr_test1cx5yug2suxqec6pzzfgwrg90crcvlk6ktlvg52eczkrw426suglqxqzl5ad0jpxv4mtdc0kpyswfjdjjqs8zh0fu0kupaha382r3py8wwqm03l5k + // TODO: Remove this once Midnight fixes this + let destination_seed = + WalletSeed::from("8e0622a9987a7bef7b6a1417c693172b79e75f2308fe3ae9cc897f6108e3a067"); + + let context = Arc::new(LedgerContext::new_from_wallet_seeds(&[ + *seed, + destination_seed, + ])); + + let wallet = context.wallet_from_seed(*seed); + let viewing_key = derive_viewing_key(&wallet, network)?; + + let config = SyncConfig { + viewing_key: Some(viewing_key), + ..SyncConfig::default() + }; + + // Create sync strategy + let strategy = S::new(indexer_client, Some(config)); + + let manager = Self { + context, + seed: *seed, + strategy, + network, + sync_state_store, + relayer_id, + }; + + // Try to restore the context from saved state + if let Ok(Some(serialized_context)) = manager + .sync_state_store + .get_ledger_context(&manager.relayer_id) + { + info!( + "Found saved ledger context for relayer {}, attempting to restore", + manager.relayer_id + ); + + if let Err(e) = manager.restore_context(&serialized_context) { + warn!("Failed to restore context: {}", e); + } + } + + Ok(manager) + } + + /// Start synchronization. + /// + /// This method runs the sync strategy, dispatches events, buffers updates, and applies them in order. + /// It also manages state persistence and progress tracking. + /// If start_index is None, it will read from the sync state store. + pub async fn sync(&mut self, start_index: Option) -> Result<(), SyncError> { + // Determine the starting index + let start_index = match start_index { + Some(index) => index, + None => { + // Read from sync state store + self.sync_state_store + .get_last_synced_index(&self.relayer_id) + .map_err(|e| SyncError::SyncError(format!("Failed to get sync state: {}", e)))? + .unwrap_or(0) + } + }; + + info!( + "Starting wallet synchronization from blockchain index: {} for relayer: {}", + start_index, self.relayer_id + ); + + // Create progress tracker + let mut progress_tracker = ProgressTracker::new(start_index); + + // Create shared buffer for chronological updates + let updates_buffer = Arc::new(Mutex::new(Vec::::new())); + + // Execute sync strategy with a custom event handler + let event_handler = EventHandlerType::EventHandler { + network: self.network, + updates_buffer: updates_buffer.clone(), + }; + + let mut event_dispatcher = EventDispatcher::new(); + event_dispatcher.register_handler(event_handler); + + self.strategy + .sync(start_index, &mut event_dispatcher, &mut progress_tracker) + .await?; + + // Apply all buffered updates in chronological order + debug!("Applying buffered updates in chronological order"); + + let mut updates = updates_buffer.lock().unwrap().clone(); + // Sort by blockchain index to ensure chronological order + updates.sort_by_key(|update| match update { + ChronologicalUpdate::Transaction { index, .. } => *index, + ChronologicalUpdate::MerkleUpdate { index, .. } => *index, + }); + + info!("Applying {} updates in chronological order", updates.len()); + + let mut last_blockchain_index = start_index; + + for update in updates { + match update { + ChronologicalUpdate::MerkleUpdate { index, update } => { + info!("Applying merkle update at index {}", index); + last_blockchain_index = index; + + let mut wallets_guard = self.context.wallets.lock().unwrap(); + + let wallet = wallets_guard.get_mut(&self.seed).ok_or_else(|| { + SyncError::MerkleTreeUpdateError("Wallet not found in context".to_string()) + })?; + + match wallet.state.apply_collapsed_update(&update) { + Ok(new_state) => { + debug!( + "Applied collapsed update: start={}, end={}, new first_free={} (was {})", + update.start, + update.end, + new_state.first_free, + wallet.state.first_free + ); + wallet.update_state(new_state); + } + Err(e) => { + return Err(SyncError::MerkleTreeUpdateError(format!( + "Failed to apply collapsed update to wallet state: {}", + e + ))); + } + } + } + ChronologicalUpdate::Transaction { + index, + tx, + apply_stage, + } => { + last_blockchain_index = index; + + // Only apply transactions that succeeded + let should_apply = match &apply_stage { + Some(stage) => stage.should_apply(), + None => true, // We have to be careful here, it may potentially apply transactions that failed + }; + + if should_apply { + debug!("Applying transaction at index {}", index); + self.context.update_from_txs(&[*tx]); + } + } + } + } + + // Save the last synced blockchain index and serialize the context + if last_blockchain_index > start_index { + // Serialize the current context + let context_bytes = self.serialize_context()?; + + // Save both the index and the serialized context + self.sync_state_store + .set_sync_state(&self.relayer_id, last_blockchain_index, Some(context_bytes)) + .map_err(|e| SyncError::SyncError(format!("Failed to save sync state: {}", e)))?; + + info!( + "Updated sync state to blockchain index {} for relayer {}", + last_blockchain_index, self.relayer_id + ); + } + + info!("Wallet synchronization completed successfully"); + Ok(()) + } + + pub fn get_context(&self) -> Arc> { + Arc::clone(&self.context) + } + + /// Convenience method to sync from the last stored height + pub async fn sync_incremental(&mut self) -> Result<(), SyncError> { + self.sync(None).await + } +} + +#[async_trait::async_trait] +impl super::SyncManagerTrait for SyncManager { + async fn sync(&mut self, start_index: u64) -> Result<(), SyncError> { + self.sync(Some(start_index)).await + } + + fn get_context(&self) -> Arc> { + self.get_context() + } +} diff --git a/src/services/sync/midnight/handler/mod.rs b/src/services/sync/midnight/handler/mod.rs new file mode 100644 index 000000000..0f140b08b --- /dev/null +++ b/src/services/sync/midnight/handler/mod.rs @@ -0,0 +1,25 @@ +pub mod events; +pub mod manager; +pub mod strategy; +pub mod tracker; + +use async_trait::async_trait; +use midnight_node_ledger_helpers::{DefaultDB, LedgerContext}; +use std::sync::Arc; + +use crate::services::midnight::SyncError; + +pub use events::*; +pub use manager::*; +pub use strategy::*; +pub use tracker::*; + +/// Trait for sync manager functionality required by the relayer +#[async_trait] +pub trait SyncManagerTrait: Send + Sync { + /// Synchronize the wallet state from the given blockchain index + async fn sync(&mut self, start_index: u64) -> Result<(), SyncError>; + + /// Get the current ledger context + fn get_context(&self) -> Arc>; +} diff --git a/src/services/sync/midnight/handler/strategy.rs b/src/services/sync/midnight/handler/strategy.rs new file mode 100644 index 000000000..8602037e9 --- /dev/null +++ b/src/services/sync/midnight/handler/strategy.rs @@ -0,0 +1,216 @@ +use crate::services::midnight::{ + handler::{convert_indexer_event, EventDispatcher, ProgressTracker, SyncEvent}, + indexer::{MidnightIndexerClient, ViewingKeyFormat}, + SyncError, +}; + +use futures_util::StreamExt; +use log::{debug, info}; +use tokio::time::{sleep_until, Duration, Instant}; + +/// Configuration for sync strategies. +/// +/// Controls timeouts and whether to send progress events. +#[derive(Debug, Clone)] +pub struct SyncConfig { + /// The viewing key to use for the sync. + pub viewing_key: Option, + /// Timeout for idle periods (no new events). + pub idle_timeout: Option, + /// Whether to send progress events. + pub send_progress_events: Option, +} + +impl Default for SyncConfig { + fn default() -> Self { + Self { + viewing_key: None, + // We set this to 5 seconds since 1 block is ~6 seconds + // so we can safely say if we haven't received any events in 5 seconds, + // we're likely just waiting for the next block to process + idle_timeout: Some(Duration::from_secs(5)), + send_progress_events: Some(true), + } + } +} + +/// Trait for different synchronization strategies. +/// +/// A sync strategy defines how the wallet fetches and processes blockchain data. It is responsible for +/// emitting events for transactions, Merkle updates, and progress, and for driving the sync lifecycle. +#[async_trait::async_trait] +pub trait SyncStrategy: Send + Sync { + /// Create a new sync strategy. + fn new(indexer_client: &MidnightIndexerClient, config: Option) -> Self; + + /// Execute the sync strategy from the given start index. + /// + /// This method should emit events via the dispatcher and update the progress tracker. + async fn sync( + &mut self, + start_index: u64, + event_dispatcher: &mut EventDispatcher, + progress_tracker: &mut ProgressTracker, + ) -> Result<(), SyncError>; +} + +/// Strategy for quick synchronization using the indexer +/// This is faster but requires sharing the viewing key with the indexer +/// The indexer will only send us the transactions that are relevant to our wallet +pub struct QuickSyncStrategy { + indexer_client: MidnightIndexerClient, + config: Option, +} + +impl QuickSyncStrategy { + /// Ensure that the config is Some and that the viewing key is also Some + fn ensure_config(&self) -> Result<&SyncConfig, SyncError> { + self.config.as_ref().ok_or(SyncError::SessionError( + "No config provided for quick sync".to_string(), + )) + } + + /// Establish a session with the indexer for the wallet's viewing key. + async fn establish_session(&self) -> Result { + let config = self.ensure_config()?; + + let viewing_key = config.viewing_key.as_ref().ok_or(SyncError::SessionError( + "No viewing key provided for quick sync".to_string(), + ))?; + + let session_id = self + .indexer_client + .connect_wallet(viewing_key) + .await + .map_err(|e| SyncError::SessionError(format!("Failed to connect wallet: {}", e)))?; + + debug!("Established wallet session: {}", session_id); + Ok(session_id) + } +} + +#[async_trait::async_trait] +impl SyncStrategy for QuickSyncStrategy { + /// Create a new relevant transaction sync strategy. + fn new(indexer_client: &MidnightIndexerClient, config: Option) -> Self { + Self { + indexer_client: indexer_client.clone(), + config, + } + } + + /// Execute the relevant transaction sync strategy. + async fn sync( + &mut self, + start_index: u64, + event_dispatcher: &mut EventDispatcher, + progress_tracker: &mut ProgressTracker, + ) -> Result<(), SyncError> { + let config = self.ensure_config()?; + let session_id = self.establish_session().await?; + + debug!("Starting quick sync from index {}", start_index); + + let send_progress_events = config.send_progress_events.unwrap_or(true); + let idle_timeout = config.idle_timeout.unwrap_or(Duration::from_secs(5)); + + // Subscribe to wallet events + let mut wallet_stream = self + .indexer_client + .subscribe_wallet(&session_id, Some(start_index), Some(send_progress_events)) + .await?; + + let mut last_event_time = Instant::now(); + + loop { + let timeout = sleep_until(last_event_time + idle_timeout); + tokio::pin!(timeout); + + tokio::select! { + Some(event_result) = wallet_stream.next() => { + last_event_time = Instant::now(); + + match event_result { + Ok(indexer_event) => { + debug!("Processing indexer event: {:#?}", indexer_event); + + // Convert and dispatch events + let sync_events = convert_indexer_event(indexer_event); + for event in sync_events { + // Update progress tracker based on event type + match &event { + SyncEvent::TransactionReceived { blockchain_index, .. } => { + progress_tracker.record_transaction(*blockchain_index); + } + SyncEvent::MerkleUpdateReceived { blockchain_index, .. } => { + progress_tracker.record_merkle_update(*blockchain_index); + } + SyncEvent::ProgressUpdate { + highest_index, + highest_relevant_wallet_index, + .. + } => { + debug!( + "Progress update: start_index={}, highest_index={}, highest_relevant_wallet_index={}", + start_index, highest_index, highest_relevant_wallet_index + ); + if progress_tracker.is_sync_complete(*highest_index, *highest_relevant_wallet_index) { + debug!("Sync completed based on progress update"); + // Dispatch completion event + event_dispatcher.dispatch(&SyncEvent::SyncCompleted).await?; + return Ok(()); + } + } + _ => {} + } + + // Process the event + event_dispatcher.dispatch(&event).await?; + } + } + Err(e) => { + event_dispatcher.dispatch(&SyncEvent::SyncError { error: e }).await?; + } + } + } + _ = &mut timeout => { + debug!("No new events for {} seconds, sync is complete", idle_timeout.as_secs()); + break; + } + } + } + + // Dispatch final completion event + info!("Sync completed"); + + event_dispatcher.dispatch(&SyncEvent::SyncCompleted).await?; + + Ok(()) + } +} + +/// Strategy for full synchronization +/// This is slower but does not require sharing the viewing key with the indexer +/// The indexer will send us all the transactions and Merkle updates +/// We apply them on a trial and error basis until we have the correct state +pub struct FullSyncStrategy { + _indexer_client: MidnightIndexerClient, +} + +#[async_trait::async_trait] +impl SyncStrategy for FullSyncStrategy { + fn new(indexer_client: &MidnightIndexerClient, _config: Option) -> Self { + Self { + _indexer_client: indexer_client.clone(), + } + } + + async fn sync( + &mut self, + _start_index: u64, + _event_dispatcher: &mut EventDispatcher, + _progress_tracker: &mut ProgressTracker, + ) -> Result<(), SyncError> { + unimplemented!() + } +} diff --git a/src/services/sync/midnight/handler/tracker.rs b/src/services/sync/midnight/handler/tracker.rs new file mode 100644 index 000000000..beafa4c16 --- /dev/null +++ b/src/services/sync/midnight/handler/tracker.rs @@ -0,0 +1,72 @@ +use std::collections::HashSet; + +/// Service for tracking synchronization progress +/// +/// The progress tracker records which blockchain indices have been processed, counts transactions +/// and Merkle updates, and provides statistics and validation methods for sync completeness. +#[derive(Debug, Clone)] +pub struct ProgressTracker { + /// Starting index for this sync session + start_index: u64, + /// The highest blockchain index we've processed + last_processed_index: u64, + /// Track all blockchain indices we've processed + processed_indices: HashSet, + /// Total transactions processed + total_transactions_processed: usize, + /// Total Merkle updates processed + total_merkle_updates_processed: usize, +} + +impl ProgressTracker { + /// Create a new progress tracker starting from the given index. + pub fn new(start_index: u64) -> Self { + Self { + start_index, + last_processed_index: start_index, + processed_indices: HashSet::new(), + total_transactions_processed: 0, + total_merkle_updates_processed: 0, + } + } + + /// Record that we processed data at a specific index + pub fn record_processed(&mut self, index: u64) { + self.last_processed_index = self.last_processed_index.max(index); + self.processed_indices.insert(index); + } + + /// Record a processed transaction at the given index + pub fn record_transaction(&mut self, index: u64) { + self.record_processed(index); + self.total_transactions_processed += 1; + } + + /// Record a processed Merkle update at the given index + pub fn record_merkle_update(&mut self, index: u64) { + self.record_processed(index); + self.total_merkle_updates_processed += 1; + } + + pub fn has_processed_data(&self) -> bool { + self.total_transactions_processed > 0 || self.total_merkle_updates_processed > 0 + } + + /// Check if sync is complete based on progress updates + /// + /// Returns true if we're already at or past the highest relevant index. + pub fn is_sync_complete(&self, highest_index: u64, highest_relevant_wallet_index: u64) -> bool { + // Consider sync complete if: + // 1. We started at or past the highest relevant wallet index (nothing new to sync) + // 2. OR we've reached the highest relevant index and processed data + if self.start_index >= highest_relevant_wallet_index { + // Already caught up - nothing new to sync + return true; + } + + // Otherwise, we need to have processed data up to the highest relevant index + (highest_index >= highest_relevant_wallet_index) + && self.last_processed_index >= highest_relevant_wallet_index + && self.has_processed_data() + } +} diff --git a/src/services/sync/midnight/indexer/client.rs b/src/services/sync/midnight/indexer/client.rs new file mode 100644 index 000000000..fbf2bdffc --- /dev/null +++ b/src/services/sync/midnight/indexer/client.rs @@ -0,0 +1,583 @@ +//! +//! GraphQL client for Midnight blockchain indexer with session management. +//! +//! This module provides an async client for interacting with the Midnight GraphQL indexer. +//! It supports wallet session management, real-time subscriptions to wallet and block events, +//! and GraphQL query execution. All methods are async and designed for use with Tokio. + +use crate::{config::network::IndexerUrls, services::midnight::indexer::types::*}; + +use futures_util::{SinkExt, StreamExt}; +use log::{debug, error, info}; +use reqwest::Client; +use serde_json::json; +use std::time::Duration; +use tokio_tungstenite::{ + connect_async, + tungstenite::{client::IntoClientRequest, Message}, +}; + +/// Midnight GraphQL indexer client +#[derive(Clone)] +pub struct MidnightIndexerClient { + /// The underlying HTTP client for GraphQL queries. + http_client: Client, + /// The HTTP URL for GraphQL queries. + http_url: String, + /// The WebSocket URL for real-time subscriptions. + ws_url: String, +} + +impl MidnightIndexerClient { + /// Create a new indexer client. + /// + /// # Arguments + /// * `http_url` - The HTTP endpoint for GraphQL queries. + /// * `ws_url` - The WebSocket endpoint for subscriptions. + /// + /// # Returns + /// A new `MidnightIndexerClient` instance. + pub fn new(indexer_urls: IndexerUrls) -> Self { + let http_client = Client::builder() + .timeout(Duration::from_secs(30)) + .build() + .expect("Failed to create HTTP client"); + + Self { + http_client, + http_url: indexer_urls.http, + ws_url: indexer_urls.ws, + } + } + + /// Establish wallet session with viewing key. + /// + /// # Arguments + /// * `viewing_key` - The viewing key to use for the wallet session. + /// + /// # Returns + /// The session ID as a string, or an `IndexerError` if the connection fails. + pub async fn connect_wallet( + &self, + viewing_key: &ViewingKeyFormat, + ) -> Result { + info!( + "Connecting wallet with viewing key: {}", + viewing_key.as_str() + ); + + let query = r#" + mutation ConnectWallet($viewingKey: ViewingKey!) { + connect(viewingKey: $viewingKey) + } + "#; + + let variables = json!({ + "viewingKey": viewing_key.as_str() + }); + + let response = self.execute_query(query, Some(variables)).await?; + + let session_id = response + .get("data") + .and_then(|data| data.get("connect")) + .and_then(|connect| connect.as_str()) + .ok_or_else(|| IndexerError::NoData)? + .to_string(); + + info!("Connected wallet with session ID: {}", session_id); + Ok(session_id) + } + + /// Subscribe to wallet updates using session ID. + /// + /// # Arguments + /// * `session_id` - The wallet session ID. + /// * `start_index` - Optional starting blockchain index for the subscription. + /// * `send_progress_updates` - Whether to include progress updates in the stream. + /// + /// # Returns + /// A pinned async stream of `WalletSyncEvent` results. Each item is either a wallet event or an error. + /// + /// # Errors + /// Returns `IndexerError` if the WebSocket connection or subscription fails. + pub async fn subscribe_wallet( + &self, + session_id: &str, + start_index: Option, + send_progress_updates: Option, + ) -> Result< + std::pin::Pin< + Box> + Send>, + >, + IndexerError, + > { + debug!("Attempting WebSocket connection to: {}", self.ws_url); + + // Create WebSocket request with required subprotocol + let mut request = self.ws_url.clone().into_client_request()?; + request.headers_mut().insert( + "Sec-WebSocket-Protocol", + "graphql-transport-ws".parse().map_err(|_| { + IndexerError::GraphQLError("Invalid WebSocket subprotocol header value".to_string()) + })?, + ); + + let (ws_stream, response) = connect_async(request).await?; + debug!( + "WebSocket connection established, response status: {}", + response.status() + ); + let (mut ws_sender, mut ws_receiver) = ws_stream.split(); + + // Send connection init + let init_message = json!({ + "type": "connection_init" + }); + ws_sender + .send(Message::Text(init_message.to_string())) + .await?; + + // Wait for connection ack + if let Some(msg) = ws_receiver.next().await { + match msg? { + Message::Text(text) => { + let parsed: serde_json::Value = serde_json::from_str(&text)?; + if parsed.get("type") + != Some(&serde_json::Value::String("connection_ack".to_string())) + { + return Err(IndexerError::SessionError( + "Connection not acknowledged".to_string(), + )); + } + } + _ => { + return Err(IndexerError::SessionError( + "Unexpected message type during handshake".to_string(), + )); + } + } + } + + // Start wallet subscription + let subscription_query = format!( + r#" + subscription WalletSync {{ + wallet(sessionId: "{}", index: {}, sendProgressUpdates: {}) {{ + __typename + ... on ViewingUpdate {{ + index + update {{ + __typename + ... on RelevantTransaction {{ + transaction {{ + hash + applyStage + raw + identifiers + merkleTreeRoot + protocolVersion + }} + start + end + }} + ... on MerkleTreeCollapsedUpdate {{ + protocolVersion + start + end + update + }} + }} + }} + ... on ProgressUpdate {{ + highestIndex + highestRelevantIndex + highestRelevantWalletIndex + }} + }} + }} + "#, + session_id, + start_index.unwrap_or(0), + send_progress_updates.unwrap_or(true) + ); + + let start_message = json!({ + "id": "wallet-sync", + "type": "subscribe", + "payload": { + "query": subscription_query + } + }); + + ws_sender + .send(Message::Text(start_message.to_string())) + .await?; + + // Return stream of wallet events + let stream = ws_receiver.filter_map(|msg| async move { + match msg { + Ok(Message::Text(text)) => { + match serde_json::from_str::(&text) { + Ok(parsed) => { + // Handle different message types + if let Some(msg_type) = parsed.get("type").and_then(|t| t.as_str()) { + match msg_type { + "next" => { + if let Some(wallet_data) = parsed + .get("payload") + .and_then(|p| p.get("data")) + .and_then(|d| d.get("wallet")) + { + match serde_json::from_value::( + wallet_data.clone(), + ) { + Ok(event) => Some(Ok(event)), + Err(e) => { + error!( + "Failed to deserialize wallet event: {}", + e + ); + error!( + "Raw data was: {}", + serde_json::to_string_pretty(&wallet_data) + .unwrap_or_else( + |_| "Invalid JSON".to_string() + ) + ); + Some(Err(IndexerError::JsonError(e))) + } + } + } else { + Some(Err(IndexerError::NoData)) + } + } + "error" => { + let error_msg = parsed + .get("payload") + .and_then(|p| p.get("message")) + .and_then(|m| m.as_str()) + .unwrap_or("Unknown subscription error"); + Some(Err(IndexerError::GraphQLError(error_msg.to_string()))) + } + "complete" => { + debug!("Wallet subscription completed"); + None // End the stream + } + _ => { + debug!("Ignoring message type: {}", msg_type); + None // Skip other message types + } + } + } else { + Some(Err(IndexerError::GraphQLError( + "Message missing type field".to_string(), + ))) + } + } + Err(e) => Some(Err(IndexerError::JsonError(e))), + } + } + Ok(_) => Some(Err(IndexerError::GraphQLError( + "Unexpected message type".to_string(), + ))), + Err(e) => Some(Err(IndexerError::WebSocketError(e))), + } + }); + + Ok(Box::pin(stream)) + } + + /// Subscribe to all blocks from a given height. + /// + /// # Arguments + /// * `start_height` - The starting block height for the subscription. + /// + /// # Returns + /// A pinned async stream of block data as JSON values, or errors. + /// + /// # Errors + /// Returns `IndexerError` if the WebSocket connection or subscription fails. + #[allow(dead_code)] + pub async fn subscribe_blocks( + &self, + start_height: u64, + ) -> Result< + std::pin::Pin< + Box> + Send>, + >, + IndexerError, + > { + debug!( + "Attempting WebSocket connection for blocks subscription to: {}", + self.ws_url + ); + + // Create WebSocket request with required subprotocol + let mut request = self.ws_url.clone().into_client_request()?; + request.headers_mut().insert( + "Sec-WebSocket-Protocol", + "graphql-transport-ws".parse().map_err(|_| { + IndexerError::GraphQLError("Invalid WebSocket subprotocol header value".to_string()) + })?, + ); + + let (ws_stream, response) = connect_async(request).await?; + debug!( + "WebSocket connection established for blocks, response status: {}", + response.status() + ); + let (mut ws_sender, mut ws_receiver) = ws_stream.split(); + + // Send connection init + let init_message = json!({ + "type": "connection_init" + }); + ws_sender + .send(Message::Text(init_message.to_string())) + .await?; + + // Wait for connection ack + if let Some(msg) = ws_receiver.next().await { + match msg? { + Message::Text(text) => { + let parsed: serde_json::Value = serde_json::from_str(&text)?; + if parsed.get("type") + != Some(&serde_json::Value::String("connection_ack".to_string())) + { + return Err(IndexerError::SessionError( + "Connection not acknowledged".to_string(), + )); + } + } + _ => { + return Err(IndexerError::SessionError( + "Unexpected message type during handshake".to_string(), + )); + } + } + } + + // Start blocks subscription + let subscription_query = format!( + r#" + subscription Blocks {{ + blocks(offset: {{ height: {} }}) {{ + hash + height + protocolVersion + timestamp + author + transactions {{ + hash + protocolVersion + applyStage + identifiers + raw + merkleTreeRoot + }} + }} + }} + "#, + start_height + ); + + let start_message = json!({ + "id": "blocks-sync", + "type": "subscribe", + "payload": { + "query": subscription_query + } + }); + + ws_sender + .send(Message::Text(start_message.to_string())) + .await?; + + // Return stream of block events + let stream = ws_receiver.filter_map(|msg| async move { + match msg { + Ok(Message::Text(text)) => { + match serde_json::from_str::(&text) { + Ok(parsed) => { + // Handle different message types + if let Some(msg_type) = parsed.get("type").and_then(|t| t.as_str()) { + match msg_type { + "next" => { + if let Some(blocks_data) = parsed + .get("payload") + .and_then(|p| p.get("data")) + .and_then(|d| d.get("blocks")) + { + debug!( + "Raw blocks data: {}", + serde_json::to_string_pretty(&blocks_data) + .unwrap_or_else(|_| "Invalid JSON".to_string()) + ); + Some(Ok(blocks_data.clone())) + } else { + Some(Err(IndexerError::NoData)) + } + } + "error" => { + let error_msg = parsed + .get("payload") + .and_then(|p| p.get("message")) + .and_then(|m| m.as_str()) + .unwrap_or("Unknown subscription error"); + Some(Err(IndexerError::GraphQLError(error_msg.to_string()))) + } + "complete" => { + info!("Blocks subscription completed"); + None // End the stream + } + _ => { + debug!("Ignoring message type: {}", msg_type); + None // Skip other message types + } + } + } else { + Some(Err(IndexerError::GraphQLError( + "Message missing type field".to_string(), + ))) + } + } + Err(e) => Some(Err(IndexerError::JsonError(e))), + } + } + Ok(_) => Some(Err(IndexerError::GraphQLError( + "Unexpected message type".to_string(), + ))), + Err(e) => Some(Err(IndexerError::WebSocketError(e))), + } + }); + + Ok(Box::pin(stream)) + } + + /// Execute a GraphQL query. + /// + /// # Arguments + /// * `query` - The GraphQL query string. + /// * `variables` - Optional variables for the query. + /// + /// # Returns + /// The JSON response from the indexer, or an `IndexerError` if the request fails. + pub async fn execute_query( + &self, + query: &str, + variables: Option, + ) -> Result { + let request_body = json!({ + "query": query, + "variables": variables + }); + + let response = self + .http_client + .post(&self.http_url) + .header("Content-Type", "application/json") + .json(&request_body) + .send() + .await?; + + if !response.status().is_success() { + return Err(IndexerError::GraphQLError(format!( + "HTTP error: {}", + response.status() + ))); + } + + let response_json: serde_json::Value = response.json().await?; + + if let Some(errors) = response_json.get("errors") { + return Err(IndexerError::GraphQLError(format!( + "GraphQL errors: {}", + errors + ))); + } + + Ok(response_json) + } + + /// Query block information by hash + pub async fn get_block_by_hash( + &self, + block_hash: &str, + ) -> Result, IndexerError> { + let query = r#" + query Block($hash: String!) { + block(offset: { hash: $hash }) { + height + timestamp + transactions { + hash + applyStage + raw + } + } + } + "#; + + let variables = serde_json::json!({ + "hash": block_hash + }); + + let response = self.execute_query(query, Some(variables)).await?; + + // Extract the block data from the response + if let Some(data) = response.get("data") { + if let Some(block) = data.get("block") { + if block.is_null() { + Ok(None) + } else { + Ok(Some(block.clone())) + } + } else { + Ok(None) + } + } else { + Ok(None) + } + } + + /// Query transaction information by hash + pub async fn get_transaction_by_hash( + &self, + tx_hash: &str, + ) -> Result, IndexerError> { + let query = r#" + query Transactions($hash: String!) { + transactions(offset: { hash: $hash }) { + applyStage + raw + } + } + "#; + + let variables = serde_json::json!({ + "hash": tx_hash + }); + + let response = self.execute_query(query, Some(variables)).await?; + + // Extract the transactions data from the response + if let Some(data) = response.get("data") { + if let Some(transactions) = data.get("transactions") { + if let Some(tx_array) = transactions.as_array() { + if !tx_array.is_empty() { + Ok(Some(tx_array[0].clone())) + } else { + Ok(None) + } + } else { + Ok(None) + } + } else { + Ok(None) + } + } else { + Ok(None) + } + } +} diff --git a/src/services/sync/midnight/indexer/mod.rs b/src/services/sync/midnight/indexer/mod.rs new file mode 100644 index 000000000..6d13a8429 --- /dev/null +++ b/src/services/sync/midnight/indexer/mod.rs @@ -0,0 +1,13 @@ +//! Indexer integration module for Midnight blockchain +//! +//! This module provides the client and types for interacting with the Midnight GraphQL indexer. +//! The indexer tracks blockchain state and provides APIs for querying transactions, blocks, +//! and wallet-specific data using viewing keys. + +/// GraphQL client for interacting with the Midnight indexer +mod client; +/// Type definitions for indexer data structures +mod types; + +pub use client::MidnightIndexerClient; +pub use types::*; diff --git a/src/services/sync/midnight/indexer/types.rs b/src/services/sync/midnight/indexer/types.rs new file mode 100644 index 000000000..12f96d3d8 --- /dev/null +++ b/src/services/sync/midnight/indexer/types.rs @@ -0,0 +1,173 @@ +//! Types for GraphQL indexer integration with session management + +use serde::{Deserialize, Serialize}; + +/// Transaction application stage from the indexer +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "PascalCase")] +pub enum ApplyStage { + /// Transaction is still pending + Pending, + /// Transaction succeeded entirely + SucceedEntirely, + /// Transaction succeeded partially + SucceedPartially, + /// Transaction failed entirely + FailEntirely, +} + +impl ApplyStage { + /// Check if the transaction should be applied to the wallet state + pub fn should_apply(&self) -> bool { + matches!( + self, + ApplyStage::SucceedEntirely | ApplyStage::SucceedPartially + ) + } +} + +/// Transaction data from the indexer containing transaction details and application status. +/// +/// This struct represents a transaction as returned by the indexer, including its hash, optional identifiers, +/// raw data, application stage, Merkle tree root, and protocol version. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TransactionData { + /// The transaction hash. + pub hash: String, + /// Optional list of identifiers associated with the transaction. + pub identifiers: Option>, + /// Optional raw transaction data as a hex string. + pub raw: Option, + /// The application stage of the transaction (pending, succeeded, failed, etc.). + #[serde(rename = "applyStage")] + pub apply_stage: Option, + /// Optional Merkle tree root associated with the transaction. + #[serde(rename = "merkleTreeRoot")] + pub merkle_tree_root: Option, + /// Optional protocol version for the transaction. + #[serde(rename = "protocolVersion")] + pub protocol_version: Option, +} + +/// Information about a collapsed Merkle tree update from the indexer. +/// +/// This struct contains details about a Merkle tree update, including the blockchain index, protocol version, +/// start and end indices, and the update data as a string. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CollapsedUpdateInfo { + /// The blockchain index at which the update occurred. + pub blockchain_index: u64, + /// The protocol version for the update. + pub protocol_version: u32, + /// The start index of the update range. + pub start: u64, + /// The end index of the update range. + pub end: u64, + /// The update data as a string (typically hex or base64 encoded). + pub update_data: String, +} + +/// Events emitted during wallet synchronization via GraphQL subscription. +/// +/// This enum represents the different event types that can be received from the indexer during wallet sync. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum WalletSyncEvent { + /// A viewing update event, containing Merkle updates and/or relevant transactions. + ViewingUpdate { + #[serde(rename = "__typename")] + type_name: String, + /// The blockchain index for this update. + index: u64, + /// The list of Zswap chain state updates (transactions or Merkle updates). + update: Vec, + }, + /// A progress update event, reporting sync progress indices. + ProgressUpdate { + #[serde(rename = "__typename")] + type_name: String, + /// The highest blockchain index seen. + #[serde(rename = "highestIndex")] + highest_index: u64, + /// The highest relevant index for the wallet. + #[serde(rename = "highestRelevantIndex")] + highest_relevant_index: u64, + /// The highest relevant wallet index. + #[serde(rename = "highestRelevantWalletIndex")] + highest_relevant_wallet_index: u64, + }, +} + +/// Updates to the Zswap chain state, including transactions and Merkle tree updates. +/// +/// This enum represents either a relevant transaction or a Merkle tree collapsed update. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "__typename")] +pub enum ZswapChainStateUpdate { + /// A relevant transaction update. + RelevantTransaction { + /// The transaction data. + transaction: TransactionData, + /// The start index for the transaction (optional, defaults to 0). + #[serde(default)] + start: u64, + /// The end index for the transaction (optional, defaults to 0). + #[serde(default)] + end: u64, + }, + /// A collapsed Merkle tree update. + MerkleTreeCollapsedUpdate { + /// The protocol version for the update (optional, defaults to 0). + #[serde(rename = "protocolVersion", default)] + protocol_version: u32, + /// The start index of the update range (optional, defaults to 0). + #[serde(default)] + start: u64, + /// The end index of the update range (optional, defaults to 0). + #[serde(default)] + end: u64, + /// The update data as a string (optional, defaults to empty string). + #[serde(default)] + update: String, + }, +} + +/// Formats for wallet viewing keys used to query the indexer. +/// +/// This enum represents the supported viewing key formats for wallet queries. +#[derive(Debug, Clone)] +pub enum ViewingKeyFormat { + /// Bech32m format (preferred): mn_shield-esk_dev1... + Bech32m(String), +} + +impl ViewingKeyFormat { + /// Get the viewing key as a string for API calls + pub fn as_str(&self) -> &str { + match self { + ViewingKeyFormat::Bech32m(key) => key, + } + } +} + +/// Error types for indexer operations and session management +#[derive(Debug, thiserror::Error)] +pub enum IndexerError { + #[error("GraphQL error: {0}")] + GraphQLError(String), + + #[error("No data returned")] + NoData, + + #[error("WebSocket error: {0}")] + WebSocketError(#[from] tokio_tungstenite::tungstenite::Error), + + #[error("HTTP error: {0}")] + HttpError(#[from] reqwest::Error), + + #[error("JSON parse error: {0}")] + JsonError(#[from] serde_json::Error), + + #[error("Session error: {0}")] + SessionError(String), +} diff --git a/src/services/sync/midnight/mod.rs b/src/services/sync/midnight/mod.rs new file mode 100644 index 000000000..6aa6112ab --- /dev/null +++ b/src/services/sync/midnight/mod.rs @@ -0,0 +1,35 @@ +pub mod handler; +pub mod indexer; +pub mod utils; + +use crate::services::midnight::indexer::IndexerError; + +/// +/// Error types for wallet synchronization and session management. +/// +/// Defines errors for indexer integration, session handling, viewing key issues, transaction parsing, +/// I/O, Merkle tree updates, and general sync errors. +#[allow(clippy::enum_variant_names)] +#[derive(Debug, thiserror::Error)] +pub enum SyncError { + #[error("Indexer error: {0}")] + IndexerError(#[from] IndexerError), + + #[error("Session error: {0}")] + SessionError(String), + + #[error("Viewing key error: {0}")] + ViewingKeyError(String), + + #[error("Transaction parse error: {0}")] + ParseError(String), + + #[error("IO error: {0}")] + IoError(#[from] std::io::Error), + + #[error("Merkle tree update error: {0}")] + MerkleTreeUpdateError(String), + + #[error("Sync error: {0}")] + SyncError(String), +} diff --git a/src/services/sync/midnight/utils/mod.rs b/src/services/sync/midnight/utils/mod.rs new file mode 100644 index 000000000..1b8d8c1ed --- /dev/null +++ b/src/services/sync/midnight/utils/mod.rs @@ -0,0 +1,95 @@ +use crate::services::midnight::{ + indexer::{CollapsedUpdateInfo, TransactionData, ViewingKeyFormat}, + SyncError, +}; + +use bech32::{Bech32m, Hrp}; +use midnight_ledger_prototype::transient_crypto::merkle_tree::MerkleTreeCollapsedUpdate; +use midnight_node_ledger_helpers::{ + deserialize, DefaultDB, NetworkId, Proof, Serializable, Transaction, Wallet, +}; + +/// Parse raw transaction hex into a Transaction type. +/// +/// This method decodes and deserializes the transaction data for further processing. +pub fn parse_transaction( + raw_hex: &str, + network: NetworkId, +) -> Result, SyncError> { + let tx_bytes = hex::decode(raw_hex) + .map_err(|e| SyncError::ParseError(format!("Failed to decode hex: {}", e)))?; + + let transaction: Transaction = deserialize(&tx_bytes[..], network) + .map_err(|e| SyncError::ParseError(format!("Failed to deserialize transaction: {}", e)))?; + + Ok(transaction) +} + +/// Process a transaction data object into a parsed transaction. +/// +/// Returns None if the transaction data does not contain raw data. +pub fn process_transaction( + transaction_data: &TransactionData, + network: NetworkId, +) -> Result>, SyncError> { + if let Some(raw_hex) = &transaction_data.raw { + let parsed_tx = parse_transaction(raw_hex, network)?; + Ok(Some(parsed_tx)) + } else { + Ok(None) + } +} + +/// Parse raw collapsed update hex into a MerkleTreeCollapsedUpdate type. +/// +/// This method decodes and deserializes the update data for further processing. +pub fn parse_collapsed_update( + update_info: &CollapsedUpdateInfo, + network: NetworkId, +) -> Result { + let update_bytes = hex::decode(&update_info.update_data) + .map_err(|e| SyncError::MerkleTreeUpdateError(format!("Failed to decode hex: {}", e)))?; + + let collapsed_update: MerkleTreeCollapsedUpdate = deserialize(&update_bytes[..], network) + .map_err(|e| { + SyncError::MerkleTreeUpdateError(format!( + "Failed to deserialize collapsed update: {}", + e + )) + })?; + + Ok(collapsed_update) +} + +/// Derive viewing key from wallet for the specified network. +/// +/// Used internally to generate the viewing key for relevant transaction sync. +pub fn derive_viewing_key( + wallet: &Wallet, + network: NetworkId, +) -> Result { + let secret_keys = &wallet.secret_keys; + let enc_secret_key = &secret_keys.encryption_secret_key; + let mut enc_secret_bytes = Vec::new(); + Serializable::serialize(enc_secret_key, &mut enc_secret_bytes).map_err(|e| { + SyncError::ViewingKeyError(format!("Failed to serialize encryption secret key: {}", e)) + })?; + + let network_suffix = match network { + NetworkId::MainNet => "", + NetworkId::TestNet => "_test", + NetworkId::DevNet => "_dev", + NetworkId::Undeployed => "_undeployed", + _ => "", + }; + + let hrp_str = format!("mn_shield-esk{}", network_suffix); + let hrp = Hrp::parse(&hrp_str) + .map_err(|e| SyncError::ViewingKeyError(format!("Invalid HRP for viewing key: {}", e)))?; + + let viewing_key_bech32 = bech32::encode::(hrp, &enc_secret_bytes).map_err(|e| { + SyncError::ViewingKeyError(format!("Failed to encode viewing key in Bech32m: {}", e)) + })?; + + Ok(ViewingKeyFormat::Bech32m(viewing_key_bech32)) +} diff --git a/src/services/sync/mod.rs b/src/services/sync/mod.rs new file mode 100644 index 000000000..bbea984b3 --- /dev/null +++ b/src/services/sync/mod.rs @@ -0,0 +1 @@ +pub mod midnight; diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 916aacac4..d3ad82d94 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -16,3 +16,6 @@ pub use transaction::*; mod base64; pub use base64::*; + +mod token; +pub use token::*; diff --git a/src/utils/token.rs b/src/utils/token.rs new file mode 100644 index 000000000..0650fb74f --- /dev/null +++ b/src/utils/token.rs @@ -0,0 +1,112 @@ +//! +//! Utility functions for formatting token amounts for display. +//! +//! Provides helpers for converting raw token values to human-readable strings with decimal places. + +/// Formats a token amount with the specified number of decimal places +pub fn format_token_amount(amount: u128, decimals: u32) -> String { + format!( + "{:.*}", + decimals as usize, + amount as f64 / 10f64.powi(decimals as i32) + ) +} + +/// Converts a token amount from one decimal precision to another +/// +/// # Arguments +/// * `amount` - The token amount in the source precision +/// * `from_decimals` - The number of decimals in the source precision +/// * `to_decimals` - The number of decimals in the target precision +/// +/// # Returns +/// The converted amount in the target precision +/// +/// # Example +/// ``` +/// // Convert 1 DUST (6 decimals) to 18 decimals precision +/// let wei_amount = convert_token_decimals(1_000_000, 6, 18); +/// assert_eq!(wei_amount, 1_000_000_000_000_000_000); +/// +/// // Convert 1 ETH (18 decimals) to 6 decimals precision +/// let usdc_amount = convert_token_decimals(1_000_000_000_000_000_000, 18, 6); +/// assert_eq!(usdc_amount, 1_000_000); +/// ``` +pub fn convert_token_decimals(amount: u128, from_decimals: u32, to_decimals: u32) -> u128 { + if from_decimals == to_decimals { + return amount; + } + + if from_decimals > to_decimals { + // Reduce precision + let divisor = 10u128.pow(from_decimals - to_decimals); + amount / divisor + } else { + // Increase precision + let multiplier = 10u128.pow(to_decimals - from_decimals); + amount * multiplier + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_format_token_amount() { + // Test ETH (18 decimals) + assert_eq!( + format_token_amount(1_000_000_000_000_000_000, 18), + "1.000000000000000000" + ); + assert_eq!( + format_token_amount(1_500_000_000_000_000_000, 18), + "1.500000000000000000" + ); + + // Test USDC (6 decimals) + assert_eq!(format_token_amount(1_000_000, 6), "1.000000"); + assert_eq!(format_token_amount(1_500_000, 6), "1.500000"); + + // Test DUST (6 decimals) + assert_eq!(format_token_amount(1_000_000, 6), "1.000000"); + assert_eq!(format_token_amount(999_999, 6), "0.999999"); + + // Test edge cases + assert_eq!(format_token_amount(0, 6), "0.000000"); + assert_eq!(format_token_amount(1, 6), "0.000001"); + } + + #[test] + fn test_convert_token_decimals() { + // Same decimals - no conversion + assert_eq!(convert_token_decimals(1_000_000, 6, 6), 1_000_000); + + // Convert from lower to higher decimals + assert_eq!( + convert_token_decimals(1_000_000, 6, 18), + 1_000_000_000_000_000_000 + ); + assert_eq!(convert_token_decimals(1, 6, 18), 1_000_000_000_000); + assert_eq!(convert_token_decimals(1_500_000, 6, 9), 1_500_000_000); + + // Convert from higher to lower decimals + assert_eq!( + convert_token_decimals(1_000_000_000_000_000_000, 18, 6), + 1_000_000 + ); + assert_eq!(convert_token_decimals(1_000_000_000_000, 18, 6), 1); // 0.000001 + assert_eq!(convert_token_decimals(999_999_999_999, 18, 6), 0); // Loss of precision + assert_eq!(convert_token_decimals(1_500_000_000, 9, 6), 1_500_000); + + // Edge cases + assert_eq!(convert_token_decimals(0, 6, 18), 0); + assert_eq!(convert_token_decimals(0, 18, 6), 0); + + // DUST to mDUST conversion (6 decimals to 3 decimals) + assert_eq!(convert_token_decimals(1_000_000, 6, 3), 1_000); + assert_eq!(convert_token_decimals(1_500_000, 6, 3), 1_500); + assert_eq!(convert_token_decimals(999_999, 6, 3), 999); + assert_eq!(convert_token_decimals(999, 6, 3), 0); // Loss of precision + } +} diff --git a/typos.toml b/typos.toml index c999b8e9b..1eb1321db 100644 --- a/typos.toml +++ b/typos.toml @@ -1,3 +1,4 @@ [default.extend-identifiers] HashiCorp = "HashiCorp" NOOPs = "NOOPs" +StandardTrasactionInfo = "StandardTrasactionInfo" From ef68b4490a37e3c572050aed1b2d13fcafe1cecc Mon Sep 17 00:00:00 2001 From: shahnami Date: Tue, 8 Jul 2025 15:39:32 +0400 Subject: [PATCH 03/11] fix: Yamlfix --- .github/workflows/semgrep.yml | 12 ++++-------- CHANGELOG.md | 4 ++-- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index ab56f73ee..2531bbaf7 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -1,5 +1,5 @@ +--- name: Semgrep - on: # Scan changed files in PRs (diff-aware scanning): pull_request: {} @@ -7,11 +7,11 @@ on: workflow_dispatch: {} # Scan mainline branches and report all findings: push: - branches: ["main"] + branches: + - main # Schedule the CI job (this method uses cron syntax): schedule: - - cron: '15 11 * * *' # Sets Semgrep to scan every day at 11:15 UTC. - + - cron: 15 11 * * * # Sets Semgrep to scan every day at 11:15 UTC. jobs: semgrep: name: semgrep/ci @@ -20,13 +20,11 @@ jobs: security-events: write contents: read actions: read - container: image: semgrep/semgrep@sha256:85f9de554201cc891c470774bb93a7f4faf41ea198ddccc34a855b53f7a51443 # v1.127.1 # Skip any PR created by dependabot to avoid permission issues: if: (github.actor != 'dependabot[bot]') - steps: - name: Harden Runner uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 @@ -39,9 +37,7 @@ jobs: env: # Connect to Semgrep AppSec Platform through your SEMGREP_APP_TOKEN. SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} - - name: Upload SARIF file for GitHub Advanced Security Dashboard uses: github/codeql-action/upload-sarif@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19 with: sarif_file: semgrep.sarif - diff --git a/CHANGELOG.md b/CHANGELOG.md index c42e5d716..fc4c3a1c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,7 +43,7 @@ * initial repo setup ([d8815b6](https://github.com/OpenZeppelin/openzeppelin-relayer/commit/d8815b6752931003536aa427370ca8fb1c57231c)) * Integrate Netlify with antora ([#74](https://github.com/OpenZeppelin/openzeppelin-relayer/issues/74)) ([09e3d48](https://github.com/OpenZeppelin/openzeppelin-relayer/commit/09e3d4894b54c58754b373da239e9d564df69aa9)) * Local signing for stellar ([#178](https://github.com/OpenZeppelin/openzeppelin-relayer/issues/178)) ([f69270a](https://github.com/OpenZeppelin/openzeppelin-relayer/commit/f69270ade4c9a9239bba874ac74858c8e7375298)) -* Pass arbitrary payloads to script exectution ([#312](https://github.com/OpenZeppelin/openzeppelin-relayer/issues/312)) ([adecaf5](https://github.com/OpenZeppelin/openzeppelin-relayer/commit/adecaf5d73c3df9083c6a3fcf62ed669bc90b25c)) +* Pass arbitrary payloads to script execution ([#312](https://github.com/OpenZeppelin/openzeppelin-relayer/issues/312)) ([adecaf5](https://github.com/OpenZeppelin/openzeppelin-relayer/commit/adecaf5d73c3df9083c6a3fcf62ed669bc90b25c)) * Plat 5744 implement an api key authentication mechanism ([#11](https://github.com/OpenZeppelin/openzeppelin-relayer/issues/11)) ([8891887](https://github.com/OpenZeppelin/openzeppelin-relayer/commit/88918872d51ab10632ec6d590689d52e59dfd640)) * Plat 5768 setup metrics endpoint ([#50](https://github.com/OpenZeppelin/openzeppelin-relayer/issues/50)) ([7c292a5](https://github.com/OpenZeppelin/openzeppelin-relayer/commit/7c292a572a7aef8213969fc72cadca74f9016fe8)) * Plat 6434 improve authorization header validation ([#122](https://github.com/OpenZeppelin/openzeppelin-relayer/issues/122)) ([eed7c31](https://github.com/OpenZeppelin/openzeppelin-relayer/commit/eed7c31e938c7b6ecaa82774ca5d3a508bb89281)) @@ -137,7 +137,7 @@ * Plat 6286 write tests for metrics and middleware functions ([#70](https://github.com/OpenZeppelin/openzeppelin-relayer/issues/70)) ([18124fb](https://github.com/OpenZeppelin/openzeppelin-relayer/commit/18124fbbfbc26f300648a7a4050ebf9be72465ac)) * PLAT-6426 Increase test coverage ([#118](https://github.com/OpenZeppelin/openzeppelin-relayer/issues/118)) ([1fa41f0](https://github.com/OpenZeppelin/openzeppelin-relayer/commit/1fa41f0f225c9d515690738e960073396dce66ce)) * PLAT-6478 create unit test for use of on relayers dotenv ([#139](https://github.com/OpenZeppelin/openzeppelin-relayer/issues/139)) ([509e166](https://github.com/OpenZeppelin/openzeppelin-relayer/commit/509e1664518823ef3844e52e818707f3371ddbff)) -* plat-6480 allow transfering wrapped sol tokens ([#132](https://github.com/OpenZeppelin/openzeppelin-relayer/issues/132)) ([f04e66a](https://github.com/OpenZeppelin/openzeppelin-relayer/commit/f04e66a568c877c2a4c5c5378fb6017c2e41d2c6)) +* plat-6480 allow transferring wrapped sol tokens ([#132](https://github.com/OpenZeppelin/openzeppelin-relayer/issues/132)) ([f04e66a](https://github.com/OpenZeppelin/openzeppelin-relayer/commit/f04e66a568c877c2a4c5c5378fb6017c2e41d2c6)) * Relayer plugins format output ([#307](https://github.com/OpenZeppelin/openzeppelin-relayer/issues/307)) ([8f25e5f](https://github.com/OpenZeppelin/openzeppelin-relayer/commit/8f25e5f55812e3d346c8bc0ff063cf07e2f0b753)) * Release merge conflicts ([#163](https://github.com/OpenZeppelin/openzeppelin-relayer/issues/163)) ([4cac422](https://github.com/OpenZeppelin/openzeppelin-relayer/commit/4cac4221817373a1ae7eff92db187dbae2f1665b)) * remove the ci job dependant from the test job ([#222](https://github.com/OpenZeppelin/openzeppelin-relayer/issues/222)) ([4056610](https://github.com/OpenZeppelin/openzeppelin-relayer/commit/40566108b66c701323145c2889ce0141b84714b8)) From 366284f186852de69deeaf218b00097a04d8d19b Mon Sep 17 00:00:00 2001 From: shahnami Date: Tue, 8 Jul 2025 16:03:36 +0400 Subject: [PATCH 04/11] test: Fix midnight tests --- src/config/config_file/network/collection.rs | 2 +- src/config/config_file/network/mod.rs | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/config/config_file/network/collection.rs b/src/config/config_file/network/collection.rs index ca9f34910..4b09d5505 100644 --- a/src/config/config_file/network/collection.rs +++ b/src/config/config_file/network/collection.rs @@ -654,7 +654,7 @@ mod tests { create_solana_network_wrapped("solana-parent"), create_solana_network_wrapped_with_parent("solana-child", "solana-parent"), create_stellar_network_wrapped("stellar-standalone"), - create_midnight_network_wrapped("midnight-standalone"), + create_midnight_network_wrapped("testnet"), ]; let config = NetworksFileConfig::new(networks).unwrap(); diff --git a/src/config/config_file/network/mod.rs b/src/config/config_file/network/mod.rs index e99c22f3a..70d2cc786 100644 --- a/src/config/config_file/network/mod.rs +++ b/src/config/config_file/network/mod.rs @@ -157,7 +157,7 @@ mod tests { #[test] fn test_validate_midnight_network_success() { - let config = create_midnight_network_wrapped("test-midnight"); + let config = create_midnight_network_wrapped("testnet"); let result = config.validate(); assert!(result.is_ok()); } @@ -519,7 +519,9 @@ mod tests { fn test_deserialize_midnight_from_json() { let json = r#"{ "type": "midnight", - "network": "test-midnight-json" + "network": "test-midnight-json", + "indexer_urls": {"http": "https://rpc.example.com", "ws": "wss://rpc.example.com"}, + "prover_url": "http://localhost:6300" }"#; let config: NetworkFileConfig = serde_json::from_str(json).unwrap(); @@ -680,7 +682,7 @@ mod tests { create_evm_network_wrapped("test-evm"), create_solana_network_wrapped("test-solana"), create_stellar_network_wrapped("test-stellar"), - create_midnight_network_wrapped("test-midnight"), + create_midnight_network_wrapped("testnet"), ]; // Ensure all methods work consistently across all network types From c8f45368b5e15425a9a704e9acc5bf72f1de895f Mon Sep 17 00:00:00 2001 From: Nami Date: Thu, 10 Jul 2025 15:02:26 +0400 Subject: [PATCH 05/11] test: Add tests for Midnight (#354) * test: Add unit tests * docs: Update script fixture readme * test: Add tests for config file --- Cargo.toml | 4 + scripts/fixtures/README.md | 77 + .../fixtures/generate_midnight_fixtures.rs | 263 ++++ .../fixtures/generate_midnight_fixtures.sh | 47 + src/config/config_file/network/common.rs | 2 +- src/config/config_file/network/midnight.rs | 251 ++++ src/domain/transaction/midnight/builder.rs | 97 ++ .../midnight/midnight_transaction.rs | 1288 +++++++++++++++++ src/domain/transaction/midnight/types.rs | 48 +- src/models/transaction/request/midnight.rs | 166 +++ src/repositories/sync_state.rs | 95 ++ src/services/sync/midnight/handler/events.rs | 155 ++ src/services/sync/midnight/handler/manager.rs | 309 +++- src/services/sync/midnight/handler/tracker.rs | 135 ++ src/services/sync/midnight/mod.rs | 3 + src/services/sync/midnight/test_utils.rs | 306 ++++ ...b1d61298cce264d4beca1529650e9041_14834.bin | Bin 0 -> 5381 bytes 17 files changed, 3243 insertions(+), 3 deletions(-) create mode 100644 scripts/fixtures/README.md create mode 100644 scripts/fixtures/generate_midnight_fixtures.rs create mode 100755 scripts/fixtures/generate_midnight_fixtures.sh create mode 100644 src/services/sync/midnight/test_utils.rs create mode 100644 tests/fixtures/midnight/context_0e0cc7db98c60a39a6b0888795ba3f1bb1d61298cce264d4beca1529650e9041_14834.bin diff --git a/Cargo.toml b/Cargo.toml index f5d6520c2..49392bde5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -125,6 +125,10 @@ path = "src/main.rs" doc = true doctest = true +[[bin]] +name = "generate_midnight_fixtures" +path = "scripts/fixtures/generate_midnight_fixtures.rs" + [[example]] name = "test_tx" path = "helpers/test_tx.rs" diff --git a/scripts/fixtures/README.md b/scripts/fixtures/README.md new file mode 100644 index 000000000..78333def8 --- /dev/null +++ b/scripts/fixtures/README.md @@ -0,0 +1,77 @@ +# Midnight Test Fixture Scripts + +This directory contains scripts for generating test fixtures for the Midnight blockchain integration. + +## Prerequisites + +1. A Midnight testnet wallet seed (32-byte hex string) +2. The wallet should have some tDUST tokens for meaningful test fixtures +3. Access to Midnight testnet (the examples use the public testnet endpoints) + +## Scripts + +### generate_midnight_fixtures.rs + +A unified fixture generator that creates complete context fixtures (including both wallet state and ledger state) for testing. + +```bash +# Generate complete context fixture +WALLET_SEED=your_32_byte_hex_seed cargo run --bin generate_midnight_fixtures + +# Starting sync from a specific height +WALLET_SEED=your_seed START_HEIGHT=1000 cargo run --bin generate_midnight_fixtures + +# With progress tracking +WALLET_SEED=your_seed SAVE_INTERVAL=1000 cargo run --bin generate_midnight_fixtures +``` + +### generate_midnight_fixtures.sh + +A convenient shell script wrapper that provides a user-friendly interface to the fixture generator: + +```bash +# Run the script (it will check for WALLET_SEED) +./scripts/fixtures/generate_midnight_fixtures.sh + +# Or with environment variables +WALLET_SEED=your_seed START_HEIGHT=1000 ./scripts/fixtures/generate_midnight_fixtures.sh +``` + +Environment variables: + +- `WALLET_SEED`: 32-byte hex string (required) +- `START_HEIGHT`: Blockchain height to start sync from (default: 0) +- `SAVE_INTERVAL`: Save progress every N blocks (optional) +- `RUST_LOG`: Log level (default: info) + +The fixtures will be saved to: `tests/fixtures/midnight/` + +- `context__.bin` - Complete context fixture (includes wallet + ledger state) + +## Using the Fixtures + +Once generated, these fixtures can be used in tests: + +```rust +use openzeppelin_relayer::services::sync::midnight::test_utils::create_context_from_serialized; + +// Load a complete context fixture +let context_bytes = fs::read("tests/fixtures/midnight/context__.bin")?; +let context = create_context_from_serialized(&context_bytes, &[seed], NetworkId::TestNet)?; + +// Or in transaction tests, the helper functions will automatically load context fixtures: +let sync_manager = create_sync_manager_with_fixture(&wallet_seed, &network, relayer_id); +``` + +## Important Notes + +1. **Testnet Only**: These examples connect to Midnight testnet. Never use mainnet wallets. +2. **Fund the Wallet**: Make sure your wallet has received tDUST tokens before generating fixtures. +3. **Sync Time**: Initial sync can take a while depending on the blockchain height. +4. **Storage**: Fixtures can be large if the wallet has extensive history. + +## Troubleshooting + +- If sync fails, check your internet connection and that the testnet endpoints are accessible. +- If the wallet state shows `first_free = 0`, the wallet is empty and needs funding. +- For detailed logs, set `RUST_LOG=debug` before running. diff --git a/scripts/fixtures/generate_midnight_fixtures.rs b/scripts/fixtures/generate_midnight_fixtures.rs new file mode 100644 index 000000000..2635e0966 --- /dev/null +++ b/scripts/fixtures/generate_midnight_fixtures.rs @@ -0,0 +1,263 @@ +//! Midnight Context Fixture Generator +//! +//! This tool generates complete context fixtures for Midnight blockchain testing. +//! A context fixture includes both wallet state (UTXOs, coins) and ledger state. +//! +//! Usage: +//! WALLET_SEED= cargo run --bin generate_midnight_fixtures +//! +//! Environment Variables: +//! - WALLET_SEED: 32-byte hex string (required for meaningful fixtures) +//! - START_HEIGHT: Blockchain height to start sync from (default: 0) +//! - SAVE_INTERVAL: Save progress every N blocks +//! - RUST_LOG: Log level (default: info) +//! +//! The fixtures will be saved to tests/fixtures/midnight/ + +use midnight_node_ledger_helpers::{ + mn_ledger_serialize::serialize, DefaultDB, LedgerContext, NetworkId, WalletSeed, +}; +use openzeppelin_relayer::{ + config::network::IndexerUrls, + repositories::{InMemorySyncState, SyncStateTrait}, + services::midnight::{ + handler::{QuickSyncStrategy, SyncManager}, + indexer::MidnightIndexerClient, + }, +}; +use std::env; +use std::fs; +use std::path::PathBuf; +use std::sync::Arc; + +/// Configuration for fixture generation +#[derive(Debug)] +struct FixtureConfig { + wallet_seed: WalletSeed, + start_height: u64, + save_interval: Option, +} + +impl FixtureConfig { + fn from_env() -> Result> { + let seed_hex = env::var("WALLET_SEED").map_err(|_| { + "WALLET_SEED environment variable is required. Example: WALLET_SEED=0e0cc7db98c60a39a6b0888795ba3f1bb1d61298cce264d4beca1529650e9041" + })?; + + let wallet_seed = WalletSeed::from(seed_hex.as_str()); + + let start_height = env::var("START_HEIGHT") + .ok() + .and_then(|h| h.parse().ok()) + .unwrap_or(0); + + let save_interval = env::var("SAVE_INTERVAL").ok().and_then(|h| h.parse().ok()); + + Ok(Self { + wallet_seed, + start_height, + save_interval, + }) + } +} + +/// Gets the fixture file path for a context +fn get_context_fixture_path(seed: &WalletSeed, height: u64) -> PathBuf { + let seed_hex = hex::encode(seed.0); + PathBuf::from("tests/fixtures/midnight").join(format!("context_{}_{}.bin", seed_hex, height)) +} + +/// Serialize the entire ledger context to bytes +fn serialize_context( + context: &Arc>, + seed: &WalletSeed, + network: NetworkId, +) -> Result, Box> { + // Serialize the wallet state for the current seed + let wallet_state = { + let wallets_guard = context + .wallets + .lock() + .map_err(|e| format!("Failed to lock wallets: {}", e))?; + + wallets_guard + .get(seed) + .map(|wallet| { + let mut state_bytes = Vec::new(); + serialize(&wallet.state, &mut state_bytes, network) + .map_err(|e| format!("Failed to serialize wallet state: {:?}", e))?; + Ok::, Box>(state_bytes) + }) + .transpose()? + }; + + // Serialize the ledger state + let ledger_state_bytes = { + let ledger_state_guard = context + .ledger_state + .lock() + .map_err(|e| format!("Failed to lock ledger state: {}", e))?; + + let mut bytes = Vec::new(); + serialize(&*ledger_state_guard, &mut bytes, network) + .map_err(|e| format!("Failed to serialize ledger state: {:?}", e))?; + bytes + }; + + // Combine wallet state and ledger state into a single serialized context + bincode::serialize(&(wallet_state, ledger_state_bytes)) + .map_err(|e| format!("Failed to serialize context: {}", e).into()) +} + +/// Generate context fixture +async fn generate_context_fixture( + sync_manager: &mut SyncManager, + config: &FixtureConfig, + final_height: u64, +) -> Result<(), Box> { + println!("🏗️ Generating complete context fixture..."); + + let context = sync_manager.get_context(); + + // Serialize the complete context + let context_bytes = serialize_context(&context, &config.wallet_seed, NetworkId::TestNet)?; + + // Save the context + let context_fixture_path = get_context_fixture_path(&config.wallet_seed, final_height); + fs::write(&context_fixture_path, &context_bytes)?; + + println!("✅ Context saved to: {}", context_fixture_path.display()); + println!(" Height: {}", final_height); + println!(" Size: {} bytes", context_bytes.len()); + + // Show wallet info from the context + let wallets_guard = context.wallets.lock().unwrap(); + if let Some(wallet) = wallets_guard.get(&config.wallet_seed) { + println!("\n📊 Wallet state in context:"); + println!(" First free: {}", wallet.state.first_free); + + let mut coin_count = 0; + for _ in wallet.state.coins.iter() { + coin_count += 1; + } + println!(" Coins: {}", coin_count); + + if wallet.state.first_free == 0 && coin_count == 0 { + println!("\n⚠️ WARNING: Wallet appears to be empty!"); + println!(" Make sure the wallet has received tDUST tokens on testnet."); + } + } + + Ok(()) +} + +/// Set up progress monitoring task +fn setup_progress_monitoring( + sync_state_store: Arc, + relayer_id: String, + save_interval: Option, +) { + if let Some(interval) = save_interval { + tokio::spawn(async move { + loop { + tokio::time::sleep(tokio::time::Duration::from_secs(10)).await; + + if let Ok(Some(height)) = sync_state_store.get_last_synced_index(&relayer_id) { + if height > 0 && height % interval == 0 { + println!("📊 Checkpoint: Synced to height {}", height); + } + } + } + }); + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize logging + if env::var("RUST_LOG").is_err() { + env::set_var("RUST_LOG", "info"); + } + + println!("🌙 Midnight Unified Test Fixture Generator"); + println!("==========================================\n"); + + // Parse configuration + let config = FixtureConfig::from_env()?; + + println!("📋 Configuration:"); + println!(" Wallet seed: {}", hex::encode(config.wallet_seed.0)); + println!(" Start height: {}", config.start_height); + println!(" Save interval: {:?}", config.save_interval); + println!(" Network: Midnight Testnet"); + println!(); + + // Create fixture directory + fs::create_dir_all("tests/fixtures/midnight")?; + + // Set up indexer client + let indexer_urls = IndexerUrls { + http: "https://indexer.testnet-02.midnight.network/api/v1/graphql".to_string(), + ws: "wss://indexer.testnet-02.midnight.network/api/v1/graphql/ws".to_string(), + }; + + println!("🌐 Connecting to Midnight testnet indexer..."); + let indexer_client = MidnightIndexerClient::new(indexer_urls); + + // Create sync manager + let sync_state_store = Arc::new(InMemorySyncState::new()); + let relayer_id = "unified-fixture-generator".to_string(); + + // Set up progress monitoring if needed + setup_progress_monitoring( + sync_state_store.clone(), + relayer_id.clone(), + config.save_interval, + ); + + println!("⚙️ Creating sync manager..."); + let mut sync_manager = SyncManager::::new( + &indexer_client, + &config.wallet_seed, + NetworkId::TestNet, + sync_state_store.clone(), + relayer_id.clone(), + )?; + + // Perform sync + println!("\n🔄 Starting sync from height {}...", config.start_height); + println!(" This may take a while depending on the blockchain height and wallet activity."); + + sync_manager.sync(Some(config.start_height)).await?; + println!("\n✅ Sync completed!"); + + // Get final sync height + let final_height = sync_state_store + .get_last_synced_index(&relayer_id)? + .unwrap_or(config.start_height); + + println!("📊 Final synced height: {}", final_height); + println!(); + + // Generate complete context fixture + generate_context_fixture(&mut sync_manager, &config, final_height).await?; + + println!("\n🎉 Fixture generation complete!"); + println!("\n📁 Generated fixtures:"); + + // List all files in the fixture directory + if let Ok(entries) = fs::read_dir("tests/fixtures/midnight") { + for entry in entries.flatten() { + let path = entry.path(); + if let Ok(metadata) = fs::metadata(&path) { + println!(" {} ({} bytes)", path.display(), metadata.len()); + } + } + } + + println!("\n📖 Usage in tests:"); + println!(" let context_bytes = fs::read(\"tests/fixtures/midnight/context__.bin\")?;"); + println!(" let context = create_context_from_serialized(&context_bytes, &[seed], NetworkId::TestNet)?;"); + + Ok(()) +} diff --git a/scripts/fixtures/generate_midnight_fixtures.sh b/scripts/fixtures/generate_midnight_fixtures.sh new file mode 100755 index 000000000..11e189518 --- /dev/null +++ b/scripts/fixtures/generate_midnight_fixtures.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +# Script to generate Midnight test fixtures using the unified generator + +set -e + +echo "🌙 Midnight Test Fixture Generator Script" +echo "========================================" +echo + +# Check if WALLET_SEED is provided +if [ -z "$WALLET_SEED" ]; then + echo "⚠️ No WALLET_SEED environment variable found." + echo "Please provide a funded wallet seed to generate meaningful fixtures." + echo + echo "Example:" + echo " WALLET_SEED=0e0cc7db98c60a39a6b0888795ba3f1bb1d61298cce264d4beca1529650e9041 $0" + echo + exit 1 +fi + +# Create fixture directory if it doesn't exist +mkdir -p tests/fixtures/midnight + +echo "📋 Current configuration:" +echo " WALLET_SEED: $WALLET_SEED" +echo " START_HEIGHT: ${START_HEIGHT:-0}" +echo " SAVE_INTERVAL: ${SAVE_INTERVAL:-not set}" +echo " RUST_LOG: ${RUST_LOG:-info}" +echo + +# Generate complete context fixture +echo "🏗️ Generating complete context fixture (wallet + ledger state)..." +echo +cargo run --bin generate_midnight_fixtures + +echo +echo "✅ Fixture generation complete!" +echo +echo "📁 Generated fixtures:" +ls -la tests/fixtures/midnight/ 2>/dev/null || echo "No fixtures found" + +echo +echo "📖 To use these fixtures in tests:" +echo "1. Ensure the fixtures are in the tests/fixtures/midnight/ directory" +echo "2. Use create_funded_test_context() with the same wallet seed" +echo "3. The test utilities will automatically load the fixtures" diff --git a/src/config/config_file/network/common.rs b/src/config/config_file/network/common.rs index ff6492b52..2340edf10 100644 --- a/src/config/config_file/network/common.rs +++ b/src/config/config_file/network/common.rs @@ -82,7 +82,7 @@ impl NetworkConfigCommon { pub fn merge_with_parent(&self, parent: &Self) -> Self { Self { network: self.network.clone(), - from: self.from.clone(), + from: self.from.clone().or(parent.from.clone()), rpc_urls: self.rpc_urls.clone().or_else(|| parent.rpc_urls.clone()), explorer_urls: self .explorer_urls diff --git a/src/config/config_file/network/midnight.rs b/src/config/config_file/network/midnight.rs index 108dcb054..000978321 100644 --- a/src/config/config_file/network/midnight.rs +++ b/src/config/config_file/network/midnight.rs @@ -82,9 +82,260 @@ impl MidnightNetworkConfig { pub fn merge_with_parent(&self, parent: &Self) -> Self { Self { common: self.common.merge_with_parent(&parent.common), + // For required fields, we always use the child's values since they must be present indexer_urls: self.indexer_urls.clone(), prover_url: self.prover_url.clone(), + // For optional fields, use child's value if present, otherwise parent's commitment_tree_ttl: self.commitment_tree_ttl.or(parent.commitment_tree_ttl), } } } + +#[cfg(test)] +mod tests { + use super::*; + + fn create_valid_config() -> MidnightNetworkConfig { + MidnightNetworkConfig { + common: NetworkConfigCommon { + network: "testnet".to_string(), + from: Some("0x1234567890abcdef".to_string()), + rpc_urls: Some(vec!["http://localhost:9944".to_string()]), + explorer_urls: Some(vec!["https://explorer.midnight.network".to_string()]), + average_blocktime_ms: Some(5000), + is_testnet: Some(true), + tags: Some(vec!["test".to_string()]), + }, + indexer_urls: IndexerUrls { + http: "https://indexer.midnight.network".to_string(), + ws: "wss://indexer.midnight.network".to_string(), + }, + prover_url: "https://prover.midnight.network".to_string(), + commitment_tree_ttl: Some(3600), + } + } + + #[test] + fn test_valid_midnight_config() { + let config = create_valid_config(); + assert!(config.validate().is_ok()); + } + + #[test] + fn test_indexer_urls_equality() { + let urls1 = IndexerUrls { + http: "http://localhost:8080".to_string(), + ws: "ws://localhost:8080".to_string(), + }; + let urls2 = IndexerUrls { + http: "http://localhost:8080".to_string(), + ws: "ws://localhost:8080".to_string(), + }; + assert_eq!(urls1, urls2); + } + + #[test] + fn test_indexer_urls_serialization() { + let urls = IndexerUrls { + http: "http://localhost:8080".to_string(), + ws: "ws://localhost:8080".to_string(), + }; + + let json = serde_json::to_string(&urls).unwrap(); + let deserialized: IndexerUrls = serde_json::from_str(&json).unwrap(); + + assert_eq!(urls, deserialized); + } + + #[test] + fn test_invalid_http_indexer_url() { + let mut config = create_valid_config(); + config.indexer_urls.http = "not-a-valid-url".to_string(); + + let result = config.validate(); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("Invalid indexer HTTP URL")); + } + + #[test] + fn test_invalid_ws_indexer_url() { + let mut config = create_valid_config(); + config.indexer_urls.ws = "not-a-valid-ws-url".to_string(); + + let result = config.validate(); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("Invalid indexer WebSocket URL")); + } + + #[test] + fn test_invalid_prover_url() { + let mut config = create_valid_config(); + config.prover_url = "invalid-url".to_string(); + + let result = config.validate(); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("Invalid prover URL")); + } + + #[test] + fn test_valid_network_ids() { + for network_id in &["mainnet", "testnet", "devnet"] { + let mut config = create_valid_config(); + config.common.network = network_id.to_string(); + assert!(config.validate().is_ok()); + } + } + + #[test] + fn test_invalid_network_id() { + let mut config = create_valid_config(); + config.common.network = "invalidnet".to_string(); + + let result = config.validate(); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("Invalid network_id")); + } + + #[test] + fn test_commitment_tree_ttl_zero() { + let mut config = create_valid_config(); + config.commitment_tree_ttl = Some(0); + + let result = config.validate(); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("commitment_tree_ttl must be greater than 0")); + } + + #[test] + fn test_commitment_tree_ttl_valid() { + let mut config = create_valid_config(); + config.commitment_tree_ttl = Some(3600); + assert!(config.validate().is_ok()); + } + + #[test] + fn test_commitment_tree_ttl_none() { + let mut config = create_valid_config(); + config.commitment_tree_ttl = None; + assert!(config.validate().is_ok()); + } + + #[test] + fn test_merge_with_parent() { + let parent = MidnightNetworkConfig { + common: NetworkConfigCommon { + network: "mainnet".to_string(), + from: Some("0xparent".to_string()), + rpc_urls: Some(vec!["http://parent:9944".to_string()]), + explorer_urls: None, + average_blocktime_ms: Some(6000), + is_testnet: Some(false), + tags: Some(vec!["parent".to_string()]), + }, + indexer_urls: IndexerUrls { + http: "http://parent-indexer".to_string(), + ws: "ws://parent-indexer".to_string(), + }, + prover_url: "http://parent-prover".to_string(), + commitment_tree_ttl: Some(7200), + }; + + let child = MidnightNetworkConfig { + common: NetworkConfigCommon { + network: "testnet".to_string(), + from: None, + rpc_urls: Some(vec!["http://child:9944".to_string()]), + explorer_urls: Some(vec!["https://child-explorer".to_string()]), + average_blocktime_ms: None, + is_testnet: None, + tags: None, + }, + indexer_urls: IndexerUrls { + http: "http://child-indexer".to_string(), + ws: "ws://child-indexer".to_string(), + }, + prover_url: "http://child-prover".to_string(), + commitment_tree_ttl: None, + }; + + let merged = child.merge_with_parent(&parent); + + // Child values take precedence + assert_eq!(merged.common.network, "testnet"); + assert_eq!(merged.indexer_urls.http, "http://child-indexer"); + assert_eq!(merged.prover_url, "http://child-prover"); + + // Parent values used as defaults + assert_eq!(merged.common.from, Some("0xparent".to_string())); + assert_eq!(merged.common.average_blocktime_ms, Some(6000)); + assert_eq!(merged.common.is_testnet, Some(false)); + assert_eq!(merged.common.tags, Some(vec!["parent".to_string()])); + assert_eq!(merged.commitment_tree_ttl, Some(7200)); + } + + #[test] + fn test_deserialization_with_missing_optional_fields() { + let json = r#"{ + "network": "testnet", + "rpc_urls": ["http://localhost:9944"], + "indexer_urls": { + "http": "http://indexer", + "ws": "ws://indexer" + }, + "prover_url": "http://prover" + }"#; + + let config: Result = serde_json::from_str(json); + assert!(config.is_ok()); + + let config = config.unwrap(); + assert_eq!(config.common.network, "testnet"); + assert!(config.commitment_tree_ttl.is_none()); + assert!(config.common.from.is_none()); + assert!(config.common.explorer_urls.is_none()); + } + + #[test] + fn test_serialization_roundtrip() { + let config = create_valid_config(); + let json = serde_json::to_string(&config).unwrap(); + let deserialized: MidnightNetworkConfig = serde_json::from_str(&json).unwrap(); + + assert_eq!(config.common.network, deserialized.common.network); + assert_eq!(config.indexer_urls, deserialized.indexer_urls); + assert_eq!(config.prover_url, deserialized.prover_url); + assert_eq!(config.commitment_tree_ttl, deserialized.commitment_tree_ttl); + } + + #[test] + fn test_deny_unknown_fields() { + let json = r#"{ + "network": "testnet", + "rpc_urls": ["http://localhost:9944"], + "indexer_urls": { + "http": "http://indexer", + "ws": "ws://indexer" + }, + "prover_url": "http://prover", + "unknown_field": "should fail" + }"#; + + let config: Result = serde_json::from_str(json); + assert!(config.is_err()); + } +} diff --git a/src/domain/transaction/midnight/builder.rs b/src/domain/transaction/midnight/builder.rs index 0ed7017e7..8995de14c 100644 --- a/src/domain/transaction/midnight/builder.rs +++ b/src/domain/transaction/midnight/builder.rs @@ -139,3 +139,100 @@ impl Default for MidnightTransactionBuilder { Self::new() } } + +#[cfg(test)] +mod tests { + use super::*; + use midnight_node_ledger_helpers::{DefaultDB, LedgerContext, WalletSeed}; + use std::sync::Arc; + + fn create_test_context() -> Arc> { + // Set required environment variable for Midnight tests + std::env::set_var( + "MIDNIGHT_LEDGER_TEST_STATIC_DIR", + "/tmp/midnight-test-static", + ); + + let wallet_seed = WalletSeed::from([1u8; 32]); + Arc::new(LedgerContext::new_from_wallet_seeds(&[wallet_seed])) + } + + #[test] + fn test_builder_new() { + let builder = MidnightTransactionBuilder::::new(); + assert!(builder.context.is_none()); + assert!(builder.proof_provider.is_none()); + assert!(builder.rng_seed.is_none()); + assert!(builder.guaranteed_offer.is_none()); + assert!(builder.intent_info.is_none()); + } + + #[test] + fn test_builder_default() { + let builder = MidnightTransactionBuilder::::default(); + assert!(builder.context.is_none()); + assert!(builder.proof_provider.is_none()); + assert!(builder.rng_seed.is_none()); + assert!(builder.guaranteed_offer.is_none()); + assert!(builder.intent_info.is_none()); + } + + #[test] + fn test_builder_with_context() { + let builder = MidnightTransactionBuilder::::new(); + let context = create_test_context(); + let builder_with_context = builder.with_context(context.clone()); + assert!(builder_with_context.context.is_some()); + } + + #[test] + fn test_builder_with_rng_seed() { + let builder = MidnightTransactionBuilder::::new(); + let seed = [42u8; 32]; + let builder_with_seed = builder.with_rng_seed(seed); + assert!(builder_with_seed.rng_seed.is_some()); + assert_eq!(builder_with_seed.rng_seed.unwrap(), seed); + } + + #[tokio::test] + async fn test_builder_build_without_context_fails() { + let builder = MidnightTransactionBuilder::::new(); + let result = builder.build().await; + assert!(result.is_err()); + match result.err().unwrap() { + TransactionError::ValidationError(msg) => { + assert!(msg.contains("Context not provided")); + } + _ => panic!("Expected ValidationError for missing context"), + } + } + + #[tokio::test] + async fn test_builder_build_without_proof_provider_fails() { + let builder = MidnightTransactionBuilder::::new(); + let context = create_test_context(); + let builder_with_context = builder.with_context(context); + let result = builder_with_context.build().await; + assert!(result.is_err()); + match result.err().unwrap() { + TransactionError::ValidationError(msg) => { + assert!(msg.contains("Proof provider not provided")); + } + _ => panic!("Expected ValidationError for missing proof provider"), + } + } + + #[test] + fn test_builder_chain_methods() { + let context = create_test_context(); + let seed = [42u8; 32]; + + let builder = MidnightTransactionBuilder::::new() + .with_context(context) + .with_rng_seed(seed); + + assert!(builder.context.is_some()); + assert!(builder.rng_seed.is_some()); + assert_eq!(builder.rng_seed.unwrap(), seed); + } +} diff --git a/src/domain/transaction/midnight/midnight_transaction.rs b/src/domain/transaction/midnight/midnight_transaction.rs index e6cfa700c..f664da84c 100644 --- a/src/domain/transaction/midnight/midnight_transaction.rs +++ b/src/domain/transaction/midnight/midnight_transaction.rs @@ -801,3 +801,1291 @@ pub type DefaultMidnightTransaction = MidnightTransaction< MidnightSigner, InMemoryTransactionCounter, >; + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::Address; + use crate::{ + config::network::IndexerUrls, + domain::{SignTransactionResponse, SignTransactionResponseMidnight}, + jobs::MockJobProducerTrait, + models::{ + midnight::{MidnightInputRequest, MidnightOutputRequest}, + MidnightNetwork, MidnightOfferRequest, MidnightTransactionData, + MidnightTransactionRequest, NetworkTransactionData, NetworkTransactionRequest, + NetworkType, RelayerMidnightPolicy, RelayerNetworkPolicy, RelayerRepoModel, + SignerError, TransactionRepoModel, TransactionStatus, U256, + }, + repositories::{ + InMemorySyncState, MockRepository, MockTransactionCounterTrait, + MockTransactionRepository, + }, + services::{ + midnight::{handler::SyncManager, indexer::MidnightIndexerClient}, + provider::MockMidnightProviderTrait, + MidnightSignerTrait, Signer, + }, + }; + use chrono::Utc; + use midnight_node_ledger_helpers::{NetworkId, WalletSeed}; + use mockall::predicate::*; + use std::fs; + use std::path::PathBuf; + use std::sync::Arc; + + // Helper functions for loading test fixtures + fn get_context_fixture_path(seed: &WalletSeed) -> Option { + let seed_hex = hex::encode(seed.0); + let fixture_dir = PathBuf::from("tests/fixtures/midnight"); + + // Look for any context fixture for this seed + if let Ok(entries) = fs::read_dir(&fixture_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if let Some(name) = path.file_name().and_then(|n| n.to_str()) { + if name.starts_with(&format!("context_{}_", seed_hex)) && name.ends_with(".bin") + { + return Some(path); + } + } + } + } + None + } + + // Helper to create a sync manager with context from fixture + fn create_sync_manager_with_fixture( + seed: &WalletSeed, + network: &MidnightNetwork, + relayer_id: String, + ) -> SyncManager { + let sync_state_store = Arc::new(InMemorySyncState::new()); + + // Create sync manager first + let sync_manager = SyncManager::new( + &MidnightIndexerClient::new(network.indexer_urls.clone()), + seed, + NetworkId::TestNet, + sync_state_store, + relayer_id, + ) + .unwrap(); + + // Try to load and restore context from fixture + if let Some(fixture_path) = get_context_fixture_path(seed) { + if let Ok(context_bytes) = fs::read(&fixture_path) { + eprintln!("Loading context fixture from: {:?}", fixture_path); + eprintln!("Context fixture size: {} bytes", context_bytes.len()); + + // Restore the context directly + if let Err(e) = sync_manager.restore_context(&context_bytes) { + eprintln!("Warning: Failed to restore context from fixture: {:?}", e); + } else { + eprintln!("Successfully restored context from fixture"); + } + } + } + + sync_manager + } + + // Test implementation for MidnightSignerTrait + struct TestMidnightSigner { + wallet_seed: WalletSeed, + } + + #[async_trait] + impl Signer for TestMidnightSigner { + async fn address(&self) -> Result { + Ok(Address::Midnight("test_midnight_address".to_string())) + } + + async fn sign_transaction( + &self, + _transaction: NetworkTransactionData, + ) -> Result { + Ok(SignTransactionResponse::Midnight( + SignTransactionResponseMidnight { + signature: "test_signature".to_string(), + }, + )) + } + } + + impl MidnightSignerTrait for TestMidnightSigner { + fn wallet_seed(&self) -> &midnight_node_ledger_helpers::WalletSeed { + &self.wallet_seed + } + } + + fn create_test_relayer() -> RelayerRepoModel { + RelayerRepoModel { + id: "test-relayer-id".to_string(), + name: "Test Relayer".to_string(), + network: "testnet".to_string(), + address: "test_midnight_address".to_string(), + paused: false, + system_disabled: false, + signer_id: "test-signer-id".to_string(), + notification_id: Some("test-notification-id".to_string()), + policies: RelayerNetworkPolicy::Midnight(RelayerMidnightPolicy { + min_balance: 100_000_000, // 0.1 tDUST + }), + network_type: NetworkType::Midnight, + custom_rpc_urls: None, + } + } + + fn create_test_transaction_request() -> NetworkTransactionRequest { + NetworkTransactionRequest::Midnight(MidnightTransactionRequest { + ttl: Some((Utc::now() + chrono::Duration::minutes(5)).to_rfc3339()), + guaranteed_offer: Some(MidnightOfferRequest { + inputs: vec![MidnightInputRequest { + origin: hex::encode([1u8; 32]), + token_type: hex::encode([2u8; 34]), + value: "100000000".to_string(), // 0.1 tDUST + }], + outputs: vec![MidnightOutputRequest { + destination: hex::encode([2u8; 32]), + token_type: hex::encode([2u8; 34]), + value: "50000000".to_string(), // 0.05 tDUST + }], + }), + intents: vec![], + fallible_offers: vec![], + }) + } + + fn create_test_transaction(relayer_id: &str) -> TransactionRepoModel { + TransactionRepoModel { + id: "test-tx-id".to_string(), + relayer_id: relayer_id.to_string(), + status: TransactionStatus::Pending, + status_reason: None, + created_at: Utc::now().to_rfc3339(), + sent_at: None, + confirmed_at: None, + valid_until: None, + network_type: NetworkType::Midnight, + network_data: NetworkTransactionData::Midnight(MidnightTransactionData { + guaranteed_offer: Some(MidnightOfferRequest { + inputs: vec![MidnightInputRequest { + origin: hex::encode([1u8; 32]), + token_type: hex::encode([2u8; 34]), + value: "100000000".to_string(), // 0.1 tDUST + }], + outputs: vec![MidnightOutputRequest { + destination: hex::encode([2u8; 32]), + token_type: hex::encode([2u8; 34]), + value: "50000000".to_string(), // 0.05 tDUST + }], + }), + intents: vec![], + fallible_offers: vec![], + raw: None, + signature: None, + hash: None, + pallet_hash: None, + block_hash: None, + segment_results: None, + }), + priced_at: None, + hashes: vec![], + noop_count: None, + is_canceled: Some(false), + } + } + + fn create_test_network() -> MidnightNetwork { + MidnightNetwork { + network: "testnet".to_string(), + rpc_urls: vec!["https://rpc.testnet.midnight.org".to_string()], + explorer_urls: None, + average_blocktime_ms: 5000, + is_testnet: true, + tags: vec![], + prover_url: "https://prover.testnet.midnight.org".to_string(), + indexer_urls: IndexerUrls { + http: "https://indexer.testnet.midnight.org".to_string(), + ws: "wss://indexer.testnet.midnight.org".to_string(), + }, + } + } + + fn setup_midnight_test() { + // Set required environment variable for Midnight tests + std::env::set_var( + "MIDNIGHT_LEDGER_TEST_STATIC_DIR", + "/tmp/midnight-test-static", + ); + } + + #[tokio::test] + async fn test_enqueue_submit() { + setup_midnight_test(); + + let relayer = create_test_relayer(); + let test_tx = create_test_transaction(&relayer.id); + let network = create_test_network(); + + // Mock repositories + let mock_relayer_repo = MockRepository::new(); + let mock_transaction_repo = MockTransactionRepository::new(); + let mut mock_job_producer = MockJobProducerTrait::new(); + let mock_provider = MockMidnightProviderTrait::new(); + let wallet_seed = WalletSeed::from([1u8; 32]); + let test_signer = TestMidnightSigner { wallet_seed }; + let mock_counter = MockTransactionCounterTrait::new(); + + // Set up expectations + mock_job_producer + .expect_produce_submit_transaction_job() + .withf(|job, delay| { + job.transaction_id == "test-tx-id" + && job.relayer_id == "test-relayer-id" + && delay.is_none() + }) + .returning(|_, _| Box::pin(async { Ok(()) })); + + // Create a minimal context for the sync manager - we won't use it in this test + let wallet_seed = WalletSeed::from([1u8; 32]); + + // Create transaction handler without using sync manager in this test + let midnight_transaction = MidnightTransaction::new( + relayer.clone(), + Arc::new(mock_provider), + Arc::new(mock_relayer_repo), + Arc::new(mock_transaction_repo), + Arc::new(mock_job_producer), + Arc::new(test_signer), + Arc::new(mock_counter), + Arc::new(tokio::sync::Mutex::new( + SyncManager::new( + &MidnightIndexerClient::new(network.indexer_urls.clone()), + &wallet_seed, + NetworkId::TestNet, + Arc::new(InMemorySyncState::new()), + relayer.id.clone(), + ) + .unwrap(), + )), + network, + ) + .unwrap(); + + // Execute test + let result = midnight_transaction.enqueue_submit(&test_tx).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_send_transaction_update_notification() { + setup_midnight_test(); + + let relayer = create_test_relayer(); + let test_tx = create_test_transaction(&relayer.id); + let network = create_test_network(); + + // Mock repositories + let mock_relayer_repo = MockRepository::new(); + let mock_transaction_repo = MockTransactionRepository::new(); + let mut mock_job_producer = MockJobProducerTrait::new(); + let mock_provider = MockMidnightProviderTrait::new(); + let wallet_seed = WalletSeed::from([1u8; 32]); + let test_signer = TestMidnightSigner { wallet_seed }; + let mock_counter = MockTransactionCounterTrait::new(); + + // Set up expectations + mock_job_producer + .expect_produce_send_notification_job() + .withf(|payload, _| payload.notification_id == "test-notification-id") + .returning(|_, _| Box::pin(async { Ok(()) })); + + // Create transaction handler + let wallet_seed = WalletSeed::from([1u8; 32]); + + let midnight_transaction = MidnightTransaction::new( + relayer.clone(), + Arc::new(mock_provider), + Arc::new(mock_relayer_repo), + Arc::new(mock_transaction_repo), + Arc::new(mock_job_producer), + Arc::new(test_signer), + Arc::new(mock_counter), + Arc::new(tokio::sync::Mutex::new( + SyncManager::new( + &MidnightIndexerClient::new(network.indexer_urls.clone()), + &wallet_seed, + NetworkId::TestNet, + Arc::new(InMemorySyncState::new()), + relayer.id.clone(), + ) + .unwrap(), + )), + network, + ) + .unwrap(); + + // Execute test + let result = midnight_transaction + .send_transaction_update_notification(&test_tx) + .await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_schedule_status_check() { + setup_midnight_test(); + + let relayer = create_test_relayer(); + let test_tx = create_test_transaction(&relayer.id); + let network = create_test_network(); + + // Mock repositories + let mock_relayer_repo = MockRepository::new(); + let mock_transaction_repo = MockTransactionRepository::new(); + let mut mock_job_producer = MockJobProducerTrait::new(); + let mock_provider = MockMidnightProviderTrait::new(); + let wallet_seed = WalletSeed::from([1u8; 32]); + let test_signer = TestMidnightSigner { wallet_seed }; + let mock_counter = MockTransactionCounterTrait::new(); + + // Set up expectations + mock_job_producer + .expect_produce_check_transaction_status_job() + .withf(|job, delay| { + job.transaction_id == "test-tx-id" + && job.relayer_id == "test-relayer-id" + && delay.is_some() + }) + .returning(|_, _| Box::pin(async { Ok(()) })); + + // Create transaction handler + let wallet_seed = WalletSeed::from([1u8; 32]); + + let midnight_transaction = MidnightTransaction::new( + relayer.clone(), + Arc::new(mock_provider), + Arc::new(mock_relayer_repo), + Arc::new(mock_transaction_repo), + Arc::new(mock_job_producer), + Arc::new(test_signer), + Arc::new(mock_counter), + Arc::new(tokio::sync::Mutex::new( + SyncManager::new( + &MidnightIndexerClient::new(network.indexer_urls.clone()), + &wallet_seed, + NetworkId::TestNet, + Arc::new(InMemorySyncState::new()), + relayer.id.clone(), + ) + .unwrap(), + )), + network, + ) + .unwrap(); + + // Execute test + let result = midnight_transaction + .schedule_status_check(&test_tx, Some(10)) + .await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_cancel_transaction_not_supported() { + setup_midnight_test(); + + let relayer = create_test_relayer(); + let test_tx = create_test_transaction(&relayer.id); + let network = create_test_network(); + + // Mock repositories + let mock_relayer_repo = MockRepository::new(); + let mock_transaction_repo = MockTransactionRepository::new(); + let mock_job_producer = MockJobProducerTrait::new(); + let mock_provider = MockMidnightProviderTrait::new(); + let wallet_seed = WalletSeed::from([1u8; 32]); + let test_signer = TestMidnightSigner { wallet_seed }; + let mock_counter = MockTransactionCounterTrait::new(); + + // Create transaction handler + let wallet_seed = WalletSeed::from([1u8; 32]); + + let midnight_transaction = MidnightTransaction::new( + relayer.clone(), + Arc::new(mock_provider), + Arc::new(mock_relayer_repo), + Arc::new(mock_transaction_repo), + Arc::new(mock_job_producer), + Arc::new(test_signer), + Arc::new(mock_counter), + Arc::new(tokio::sync::Mutex::new( + SyncManager::new( + &MidnightIndexerClient::new(network.indexer_urls.clone()), + &wallet_seed, + NetworkId::TestNet, + Arc::new(InMemorySyncState::new()), + relayer.id.clone(), + ) + .unwrap(), + )), + network, + ) + .unwrap(); + + // Execute test + let result = midnight_transaction.cancel_transaction(test_tx).await; + assert!(matches!(result, Err(TransactionError::NotSupported(_)))); + } + + #[tokio::test] + async fn test_replace_transaction_not_supported() { + setup_midnight_test(); + + let relayer = create_test_relayer(); + let test_tx = create_test_transaction(&relayer.id); + let new_request = create_test_transaction_request(); + let network = create_test_network(); + + // Mock repositories + let mock_relayer_repo = MockRepository::new(); + let mock_transaction_repo = MockTransactionRepository::new(); + let mock_job_producer = MockJobProducerTrait::new(); + let mock_provider = MockMidnightProviderTrait::new(); + let wallet_seed = WalletSeed::from([1u8; 32]); + let test_signer = TestMidnightSigner { wallet_seed }; + let mock_counter = MockTransactionCounterTrait::new(); + + // Create transaction handler + let wallet_seed = WalletSeed::from([1u8; 32]); + + let midnight_transaction = MidnightTransaction::new( + relayer.clone(), + Arc::new(mock_provider), + Arc::new(mock_relayer_repo), + Arc::new(mock_transaction_repo), + Arc::new(mock_job_producer), + Arc::new(test_signer), + Arc::new(mock_counter), + Arc::new(tokio::sync::Mutex::new( + SyncManager::new( + &MidnightIndexerClient::new(network.indexer_urls.clone()), + &wallet_seed, + NetworkId::TestNet, + Arc::new(InMemorySyncState::new()), + relayer.id.clone(), + ) + .unwrap(), + )), + network, + ) + .unwrap(); + + // Execute test + let result = midnight_transaction + .replace_transaction(test_tx, new_request) + .await; + assert!(matches!(result, Err(TransactionError::NotSupported(_)))); + } + + #[tokio::test] + async fn test_prepare_transaction_insufficient_balance() { + setup_midnight_test(); + + let relayer = create_test_relayer(); + let test_tx = create_test_transaction(&relayer.id); + let network = create_test_network(); + + // Mock repositories + let mock_relayer_repo = MockRepository::new(); + let mock_transaction_repo = MockTransactionRepository::new(); + let mock_job_producer = MockJobProducerTrait::new(); + let mut mock_provider = MockMidnightProviderTrait::new(); + let wallet_seed = WalletSeed::from([1u8; 32]); + let test_signer = TestMidnightSigner { wallet_seed }; + let mock_counter = MockTransactionCounterTrait::new(); + + // Wallet seed is already set in test_signer + + // Mock provider with insufficient balance + mock_provider + .expect_get_balance() + .returning(|_, _| Box::pin(async { Ok(U256::from(1000u128)) })); // 0.001 tDUST (insufficient) + + // Create transaction handler + let midnight_transaction = MidnightTransaction::new( + relayer.clone(), + Arc::new(mock_provider), + Arc::new(mock_relayer_repo), + Arc::new(mock_transaction_repo), + Arc::new(mock_job_producer), + Arc::new(test_signer), + Arc::new(mock_counter), + Arc::new(tokio::sync::Mutex::new( + SyncManager::new( + &MidnightIndexerClient::new(network.indexer_urls.clone()), + &wallet_seed, + NetworkId::TestNet, + Arc::new(InMemorySyncState::new()), + relayer.id.clone(), + ) + .unwrap(), + )), + network, + ) + .unwrap(); + + // Execute test - should fail during offer conversion due to insufficient balance + let result = midnight_transaction.prepare_transaction(test_tx).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_submit_transaction_missing_raw_data() { + setup_midnight_test(); + + let relayer = create_test_relayer(); + let test_tx = create_test_transaction(&relayer.id); + let network = create_test_network(); + + // Mock repositories + let mock_relayer_repo = MockRepository::new(); + let mock_transaction_repo = MockTransactionRepository::new(); + let mock_job_producer = MockJobProducerTrait::new(); + let mock_provider = MockMidnightProviderTrait::new(); + let wallet_seed = WalletSeed::from([1u8; 32]); + let test_signer = TestMidnightSigner { wallet_seed }; + let mock_counter = MockTransactionCounterTrait::new(); + + // Create transaction handler + let wallet_seed = WalletSeed::from([1u8; 32]); + let midnight_transaction = MidnightTransaction::new( + relayer.clone(), + Arc::new(mock_provider), + Arc::new(mock_relayer_repo), + Arc::new(mock_transaction_repo), + Arc::new(mock_job_producer), + Arc::new(test_signer), + Arc::new(mock_counter), + Arc::new(tokio::sync::Mutex::new( + SyncManager::new( + &MidnightIndexerClient::new(network.indexer_urls.clone()), + &wallet_seed, + midnight_node_ledger_helpers::NetworkId::TestNet, + Arc::new(InMemorySyncState::new()), + relayer.id.clone(), + ) + .unwrap(), + )), + network, + ) + .unwrap(); + + // Execute test - should fail because raw data is missing + let result = midnight_transaction.submit_transaction(test_tx).await; + assert!(result.is_err()); + assert!(matches!(result, Err(TransactionError::UnexpectedError(_)))); + } + + #[tokio::test] + async fn test_handle_transaction_status_success_entirely() { + setup_midnight_test(); + + let relayer = create_test_relayer(); + let mut test_tx = create_test_transaction(&relayer.id); + test_tx.status = TransactionStatus::Submitted; + let network = create_test_network(); + + // Add pallet hash + if let NetworkTransactionData::Midnight(ref mut data) = test_tx.network_data { + data.pallet_hash = Some("0xdef456".to_string()); + } + + // Mock repositories + let mock_relayer_repo = MockRepository::new(); + let mut mock_transaction_repo = MockTransactionRepository::new(); + let mut mock_job_producer = MockJobProducerTrait::new(); + let mut mock_provider = MockMidnightProviderTrait::new(); + let wallet_seed = WalletSeed::from([1u8; 32]); + let test_signer = TestMidnightSigner { wallet_seed }; + let mock_counter = MockTransactionCounterTrait::new(); + + // Mock provider get_transaction_by_hash + mock_provider + .expect_get_transaction_by_hash() + .withf(|hash| hash == "0xdef456") + .returning(|_| { + let mut tx_data = serde_json::Map::new(); + tx_data.insert( + "applyStage".to_string(), + serde_json::to_value(ApplyStage::SucceedEntirely).unwrap(), + ); + Box::pin(async move { Ok(Some(serde_json::Value::Object(tx_data))) }) + }); + + // Mock transaction repository update + mock_transaction_repo + .expect_partial_update() + .withf(|id, update| { + id == "test-tx-id" && update.status == Some(TransactionStatus::Confirmed) + }) + .returning(|id, _| { + let mut tx = create_test_transaction("test-relayer-id"); + tx.id = id; + tx.status = TransactionStatus::Confirmed; + Ok(tx) + }); + + // Mock job producer + mock_job_producer + .expect_produce_send_notification_job() + .returning(|_, _| Box::pin(async { Ok(()) })); + + // Create transaction handler + let wallet_seed = WalletSeed::from([1u8; 32]); + let midnight_transaction = MidnightTransaction::new( + relayer.clone(), + Arc::new(mock_provider), + Arc::new(mock_relayer_repo), + Arc::new(mock_transaction_repo), + Arc::new(mock_job_producer), + Arc::new(test_signer), + Arc::new(mock_counter), + Arc::new(tokio::sync::Mutex::new( + SyncManager::new( + &MidnightIndexerClient::new(network.indexer_urls.clone()), + &wallet_seed, + midnight_node_ledger_helpers::NetworkId::TestNet, + Arc::new(InMemorySyncState::new()), + relayer.id.clone(), + ) + .unwrap(), + )), + network, + ) + .unwrap(); + + // Execute test + let result = midnight_transaction + .handle_transaction_status(test_tx) + .await; + assert!(result.is_ok()); + let updated_tx = result.unwrap(); + assert_eq!(updated_tx.status, TransactionStatus::Confirmed); + } + + #[tokio::test] + async fn test_handle_transaction_status_fail_entirely() { + setup_midnight_test(); + + let relayer = create_test_relayer(); + let mut test_tx = create_test_transaction(&relayer.id); + test_tx.status = TransactionStatus::Submitted; + let network = create_test_network(); + + // Add pallet hash + if let NetworkTransactionData::Midnight(ref mut data) = test_tx.network_data { + data.pallet_hash = Some("0xdef456".to_string()); + } + + // Mock repositories + let mock_relayer_repo = MockRepository::new(); + let mut mock_transaction_repo = MockTransactionRepository::new(); + let mut mock_job_producer = MockJobProducerTrait::new(); + let mut mock_provider = MockMidnightProviderTrait::new(); + let wallet_seed = WalletSeed::from([1u8; 32]); + let test_signer = TestMidnightSigner { wallet_seed }; + let mock_counter = MockTransactionCounterTrait::new(); + + // Mock provider get_transaction_by_hash + mock_provider + .expect_get_transaction_by_hash() + .withf(|hash| hash == "0xdef456") + .returning(|_| { + let mut tx_data = serde_json::Map::new(); + tx_data.insert( + "applyStage".to_string(), + serde_json::to_value(ApplyStage::FailEntirely).unwrap(), + ); + Box::pin(async move { Ok(Some(serde_json::Value::Object(tx_data))) }) + }); + + // Mock transaction repository update + mock_transaction_repo + .expect_partial_update() + .withf(|id, update| { + id == "test-tx-id" && update.status == Some(TransactionStatus::Failed) + }) + .returning(|id, _| { + let mut tx = create_test_transaction("test-relayer-id"); + tx.id = id; + tx.status = TransactionStatus::Failed; + Ok(tx) + }); + + // Mock job producer + mock_job_producer + .expect_produce_send_notification_job() + .returning(|_, _| Box::pin(async { Ok(()) })); + + // Create transaction handler + let wallet_seed = WalletSeed::from([1u8; 32]); + let midnight_transaction = MidnightTransaction::new( + relayer.clone(), + Arc::new(mock_provider), + Arc::new(mock_relayer_repo), + Arc::new(mock_transaction_repo), + Arc::new(mock_job_producer), + Arc::new(test_signer), + Arc::new(mock_counter), + Arc::new(tokio::sync::Mutex::new( + SyncManager::new( + &MidnightIndexerClient::new(network.indexer_urls.clone()), + &wallet_seed, + midnight_node_ledger_helpers::NetworkId::TestNet, + Arc::new(InMemorySyncState::new()), + relayer.id.clone(), + ) + .unwrap(), + )), + network, + ) + .unwrap(); + + // Execute test + let result = midnight_transaction + .handle_transaction_status(test_tx) + .await; + assert!(result.is_ok()); + let updated_tx = result.unwrap(); + assert_eq!(updated_tx.status, TransactionStatus::Failed); + } + + #[tokio::test] + async fn test_handle_transaction_status_pending() { + setup_midnight_test(); + + let relayer = create_test_relayer(); + let mut test_tx = create_test_transaction(&relayer.id); + test_tx.status = TransactionStatus::Submitted; + let network = create_test_network(); + + // Add pallet hash + if let NetworkTransactionData::Midnight(ref mut data) = test_tx.network_data { + data.pallet_hash = Some("0xdef456".to_string()); + } + + // Mock repositories + let mock_relayer_repo = MockRepository::new(); + let mock_transaction_repo = MockTransactionRepository::new(); + let mut mock_job_producer = MockJobProducerTrait::new(); + let mut mock_provider = MockMidnightProviderTrait::new(); + let wallet_seed = WalletSeed::from([1u8; 32]); + let test_signer = TestMidnightSigner { wallet_seed }; + let mock_counter = MockTransactionCounterTrait::new(); + + // Mock provider get_transaction_by_hash + mock_provider + .expect_get_transaction_by_hash() + .withf(|hash| hash == "0xdef456") + .returning(|_| { + let mut tx_data = serde_json::Map::new(); + tx_data.insert( + "applyStage".to_string(), + serde_json::to_value(ApplyStage::Pending).unwrap(), + ); + Box::pin(async move { Ok(Some(serde_json::Value::Object(tx_data))) }) + }); + + // Mock job producer - expect schedule_status_check + mock_job_producer + .expect_produce_check_transaction_status_job() + .withf(|job, delay| job.transaction_id == "test-tx-id" && delay.is_some()) + .returning(|_, _| Box::pin(async { Ok(()) })); + + // Create transaction handler + let wallet_seed = WalletSeed::from([1u8; 32]); + let midnight_transaction = MidnightTransaction::new( + relayer.clone(), + Arc::new(mock_provider), + Arc::new(mock_relayer_repo), + Arc::new(mock_transaction_repo), + Arc::new(mock_job_producer), + Arc::new(test_signer), + Arc::new(mock_counter), + Arc::new(tokio::sync::Mutex::new( + SyncManager::new( + &MidnightIndexerClient::new(network.indexer_urls.clone()), + &wallet_seed, + midnight_node_ledger_helpers::NetworkId::TestNet, + Arc::new(InMemorySyncState::new()), + relayer.id.clone(), + ) + .unwrap(), + )), + network, + ) + .unwrap(); + + // Execute test + let result = midnight_transaction + .handle_transaction_status(test_tx.clone()) + .await; + assert!(result.is_ok()); + let updated_tx = result.unwrap(); + assert_eq!(updated_tx.status, TransactionStatus::Submitted); // Status unchanged + } + + #[tokio::test] + async fn test_handle_transaction_status_not_found() { + setup_midnight_test(); + + let relayer = create_test_relayer(); + let mut test_tx = create_test_transaction(&relayer.id); + test_tx.status = TransactionStatus::Submitted; + let network = create_test_network(); + + // Add pallet hash + if let NetworkTransactionData::Midnight(ref mut data) = test_tx.network_data { + data.pallet_hash = Some("0xdef456".to_string()); + } + + // Mock repositories + let mock_relayer_repo = MockRepository::new(); + let mock_transaction_repo = MockTransactionRepository::new(); + let mut mock_job_producer = MockJobProducerTrait::new(); + let mut mock_provider = MockMidnightProviderTrait::new(); + let wallet_seed = WalletSeed::from([1u8; 32]); + let test_signer = TestMidnightSigner { wallet_seed }; + let mock_counter = MockTransactionCounterTrait::new(); + + // Mock provider get_transaction_by_hash - transaction not found + mock_provider + .expect_get_transaction_by_hash() + .withf(|hash| hash == "0xdef456") + .returning(|_| Box::pin(async move { Ok(None) })); + + // Mock job producer - expect schedule_status_check + mock_job_producer + .expect_produce_check_transaction_status_job() + .withf(|job, delay| job.transaction_id == "test-tx-id" && delay.is_some()) + .returning(|_, _| Box::pin(async { Ok(()) })); + + // Create transaction handler + let wallet_seed = WalletSeed::from([1u8; 32]); + let midnight_transaction = MidnightTransaction::new( + relayer.clone(), + Arc::new(mock_provider), + Arc::new(mock_relayer_repo), + Arc::new(mock_transaction_repo), + Arc::new(mock_job_producer), + Arc::new(test_signer), + Arc::new(mock_counter), + Arc::new(tokio::sync::Mutex::new( + SyncManager::new( + &MidnightIndexerClient::new(network.indexer_urls.clone()), + &wallet_seed, + midnight_node_ledger_helpers::NetworkId::TestNet, + Arc::new(InMemorySyncState::new()), + relayer.id.clone(), + ) + .unwrap(), + )), + network, + ) + .unwrap(); + + // Execute test + let result = midnight_transaction + .handle_transaction_status(test_tx.clone()) + .await; + assert!(result.is_ok()); + let updated_tx = result.unwrap(); + assert_eq!(updated_tx.status, TransactionStatus::Submitted); // Status unchanged + } + + #[tokio::test] + async fn test_handle_transaction_status_final_state() { + setup_midnight_test(); + + let relayer = create_test_relayer(); + let mut test_tx = create_test_transaction(&relayer.id); + test_tx.status = TransactionStatus::Confirmed; // Already in final state + let network = create_test_network(); + + // Mock repositories + let mock_relayer_repo = MockRepository::new(); + let mock_transaction_repo = MockTransactionRepository::new(); + let mock_job_producer = MockJobProducerTrait::new(); + let mock_provider = MockMidnightProviderTrait::new(); + let wallet_seed = WalletSeed::from([1u8; 32]); + let test_signer = TestMidnightSigner { wallet_seed }; + let mock_counter = MockTransactionCounterTrait::new(); + + // Create transaction handler + let wallet_seed = WalletSeed::from([1u8; 32]); + let midnight_transaction = MidnightTransaction::new( + relayer.clone(), + Arc::new(mock_provider), + Arc::new(mock_relayer_repo), + Arc::new(mock_transaction_repo), + Arc::new(mock_job_producer), + Arc::new(test_signer), + Arc::new(mock_counter), + Arc::new(tokio::sync::Mutex::new( + SyncManager::new( + &MidnightIndexerClient::new(network.indexer_urls.clone()), + &wallet_seed, + midnight_node_ledger_helpers::NetworkId::TestNet, + Arc::new(InMemorySyncState::new()), + relayer.id.clone(), + ) + .unwrap(), + )), + network, + ) + .unwrap(); + + // Execute test - should return immediately without checking status + let result = midnight_transaction + .handle_transaction_status(test_tx.clone()) + .await; + assert!(result.is_ok()); + let updated_tx = result.unwrap(); + assert_eq!(updated_tx.status, TransactionStatus::Confirmed); + } + + // This test requires a fixture with actual funded wallet (coins in the wallet state) + // Uses funded wallet fixture from testnet + #[tokio::test] + async fn test_convert_offer_request_to_offer_info_success() { + setup_midnight_test(); + + let relayer = create_test_relayer(); + let network = create_test_network(); + + // Use funded wallet seed for test + let wallet_seed = + WalletSeed::from("0e0cc7db98c60a39a6b0888795ba3f1bb1d61298cce264d4beca1529650e9041"); + + // Check if context fixture exists + if get_context_fixture_path(&wallet_seed).is_none() { + eprintln!( + "Skipping test: No context fixture found for seed {}", + hex::encode(wallet_seed.0) + ); + eprintln!("To run this test, generate a fixture using:"); + eprintln!( + " WALLET_SEED={} cargo run --bin generate_midnight_fixtures", + hex::encode(wallet_seed.0) + ); + return; + } + + // Create offer request (origin must match wallet seed) + let offer_request = MidnightOfferRequest { + inputs: vec![MidnightInputRequest { + origin: hex::encode(wallet_seed.0), + token_type: hex::encode([2u8; 34]), + value: "100000000".to_string(), // 0.1 tDUST + }], + outputs: vec![MidnightOutputRequest { + destination: hex::encode([2u8; 32]), + token_type: hex::encode([2u8; 34]), + value: "30000000".to_string(), // 0.03 tDUST + }], + }; + + // Mock repositories + let mock_relayer_repo = MockRepository::new(); + let mock_transaction_repo = MockTransactionRepository::new(); + let mock_job_producer = MockJobProducerTrait::new(); + let mock_provider = MockMidnightProviderTrait::new(); + let test_signer = TestMidnightSigner { wallet_seed }; + let mock_counter = MockTransactionCounterTrait::new(); + + // Create transaction handler with fixture-loaded sync manager + let sync_manager = + create_sync_manager_with_fixture(&wallet_seed, &network, relayer.id.clone()); + let midnight_transaction = MidnightTransaction::new( + relayer.clone(), + Arc::new(mock_provider), + Arc::new(mock_relayer_repo), + Arc::new(mock_transaction_repo), + Arc::new(mock_job_producer), + Arc::new(test_signer), + Arc::new(mock_counter), + Arc::new(tokio::sync::Mutex::new(sync_manager)), + network.clone(), + ) + .unwrap(); + + // Get the context + let sync_manager = midnight_transaction.sync_manager(); + let sync_guard = sync_manager.lock().await; + let context = sync_guard.get_context(); + drop(sync_guard); + + // Execute test + let result = midnight_transaction + .convert_offer_request_to_offer_info(&offer_request, wallet_seed, &context) + .await; + + if result.is_err() { + eprintln!("Test failed with error: {:?}", result.as_ref().err()); + } + assert!(result.is_ok()); + let offer_info = result.unwrap(); + assert_eq!(offer_info.inputs.len(), 1); + assert_eq!(offer_info.outputs.len(), 2); // Original output + change output + } + + #[tokio::test] + async fn test_convert_offer_request_to_offer_info_invalid_origin() { + setup_midnight_test(); + + let relayer = create_test_relayer(); + let network = create_test_network(); + + // Create offer request with different origin + let offer_request = MidnightOfferRequest { + inputs: vec![MidnightInputRequest { + origin: hex::encode([3u8; 32]), // Different from wallet seed + token_type: hex::encode([2u8; 34]), + value: "100000000".to_string(), + }], + outputs: vec![MidnightOutputRequest { + destination: hex::encode([2u8; 32]), + token_type: hex::encode([2u8; 34]), + value: "30000000".to_string(), + }], + }; + + // Mock repositories + let mock_relayer_repo = MockRepository::new(); + let mock_transaction_repo = MockTransactionRepository::new(); + let mock_job_producer = MockJobProducerTrait::new(); + let mock_provider = MockMidnightProviderTrait::new(); + let wallet_seed = WalletSeed::from([1u8; 32]); + let test_signer = TestMidnightSigner { wallet_seed }; + let mock_counter = MockTransactionCounterTrait::new(); + + // Create transaction handler + let wallet_seed = WalletSeed::from([1u8; 32]); + let midnight_transaction = MidnightTransaction::new( + relayer.clone(), + Arc::new(mock_provider), + Arc::new(mock_relayer_repo), + Arc::new(mock_transaction_repo), + Arc::new(mock_job_producer), + Arc::new(test_signer), + Arc::new(mock_counter), + Arc::new(tokio::sync::Mutex::new( + SyncManager::new( + &MidnightIndexerClient::new(network.indexer_urls.clone()), + &wallet_seed, + midnight_node_ledger_helpers::NetworkId::TestNet, + Arc::new(InMemorySyncState::new()), + relayer.id.clone(), + ) + .unwrap(), + )), + network.clone(), + ) + .unwrap(); + + // Get the context + let sync_manager = midnight_transaction.sync_manager(); + let sync_guard = sync_manager.lock().await; + let context = sync_guard.get_context(); + drop(sync_guard); + + // Execute test + let result = midnight_transaction + .convert_offer_request_to_offer_info(&offer_request, wallet_seed, &context) + .await; + + assert!(result.is_err()); + assert!(matches!(result, Err(TransactionError::ValidationError(_)))); + } + + #[tokio::test] + async fn test_convert_offer_request_empty_inputs() { + setup_midnight_test(); + + let relayer = create_test_relayer(); + let network = create_test_network(); + + // Create offer request with no inputs + let offer_request = MidnightOfferRequest { + inputs: vec![], + outputs: vec![MidnightOutputRequest { + destination: hex::encode([2u8; 32]), + token_type: hex::encode([2u8; 34]), + value: "30000000".to_string(), + }], + }; + + // Mock repositories + let mock_relayer_repo = MockRepository::new(); + let mock_transaction_repo = MockTransactionRepository::new(); + let mock_job_producer = MockJobProducerTrait::new(); + let mock_provider = MockMidnightProviderTrait::new(); + let wallet_seed = WalletSeed::from([1u8; 32]); + let test_signer = TestMidnightSigner { wallet_seed }; + let mock_counter = MockTransactionCounterTrait::new(); + + // Create transaction handler + let wallet_seed = WalletSeed::from([1u8; 32]); + let midnight_transaction = MidnightTransaction::new( + relayer.clone(), + Arc::new(mock_provider), + Arc::new(mock_relayer_repo), + Arc::new(mock_transaction_repo), + Arc::new(mock_job_producer), + Arc::new(test_signer), + Arc::new(mock_counter), + Arc::new(tokio::sync::Mutex::new( + SyncManager::new( + &MidnightIndexerClient::new(network.indexer_urls.clone()), + &wallet_seed, + NetworkId::TestNet, + Arc::new(InMemorySyncState::new()), + relayer.id.clone(), + ) + .unwrap(), + )), + network.clone(), + ) + .unwrap(); + + // Get the context + let sync_manager = midnight_transaction.sync_manager(); + let sync_guard = sync_manager.lock().await; + let context = sync_guard.get_context(); + drop(sync_guard); + + // Execute test + let result = midnight_transaction + .convert_offer_request_to_offer_info(&offer_request, wallet_seed, &context) + .await; + + assert!(result.is_err()); + assert!(matches!(result, Err(TransactionError::ValidationError(_)))); + } + + #[tokio::test] + async fn test_sign_transaction_success() { + setup_midnight_test(); + + let relayer = create_test_relayer(); + let test_tx = create_test_transaction(&relayer.id); + let network = create_test_network(); + + // Mock repositories + let mock_relayer_repo = MockRepository::new(); + let mut mock_transaction_repo = MockTransactionRepository::new(); + let mock_job_producer = MockJobProducerTrait::new(); + let mock_provider = MockMidnightProviderTrait::new(); + let wallet_seed = WalletSeed::from([1u8; 32]); + let test_signer = TestMidnightSigner { wallet_seed }; + let mock_counter = MockTransactionCounterTrait::new(); + + // No need to mock sign_transaction - TestMidnightSigner already returns test_signature + + // Mock transaction repository update + mock_transaction_repo + .expect_partial_update() + .withf(|id, update| id == "test-tx-id" && update.network_data.is_some()) + .returning(|id, update| { + let mut tx = create_test_transaction("test-relayer-id"); + tx.id = id; + if let Some(NetworkTransactionData::Midnight(mut data)) = update.network_data { + data.signature = Some("test_signature".to_string()); + tx.network_data = NetworkTransactionData::Midnight(data); + } + Ok(tx) + }); + + // Create transaction handler + let wallet_seed = WalletSeed::from([1u8; 32]); + let midnight_transaction = MidnightTransaction::new( + relayer.clone(), + Arc::new(mock_provider), + Arc::new(mock_relayer_repo), + Arc::new(mock_transaction_repo), + Arc::new(mock_job_producer), + Arc::new(test_signer), + Arc::new(mock_counter), + Arc::new(tokio::sync::Mutex::new( + SyncManager::new( + &MidnightIndexerClient::new(network.indexer_urls.clone()), + &wallet_seed, + NetworkId::TestNet, + Arc::new(InMemorySyncState::new()), + relayer.id.clone(), + ) + .unwrap(), + )), + network, + ) + .unwrap(); + + // Execute test + let result = midnight_transaction.sign_transaction(test_tx).await; + assert!(result.is_ok()); + let signed_tx = result.unwrap(); + if let NetworkTransactionData::Midnight(data) = signed_tx.network_data { + assert_eq!(data.signature, Some("test_signature".to_string())); + } + } + + #[tokio::test] + async fn test_validate_transaction() { + setup_midnight_test(); + + let relayer = create_test_relayer(); + let test_tx = create_test_transaction(&relayer.id); + let network = create_test_network(); + + // Mock repositories + let mock_relayer_repo = MockRepository::new(); + let mock_transaction_repo = MockTransactionRepository::new(); + let mock_job_producer = MockJobProducerTrait::new(); + let mock_provider = MockMidnightProviderTrait::new(); + let wallet_seed = WalletSeed::from([1u8; 32]); + let test_signer = TestMidnightSigner { wallet_seed }; + let mock_counter = MockTransactionCounterTrait::new(); + + // Create transaction handler + let wallet_seed = WalletSeed::from([1u8; 32]); + let midnight_transaction = MidnightTransaction::new( + relayer.clone(), + Arc::new(mock_provider), + Arc::new(mock_relayer_repo), + Arc::new(mock_transaction_repo), + Arc::new(mock_job_producer), + Arc::new(test_signer), + Arc::new(mock_counter), + Arc::new(tokio::sync::Mutex::new( + SyncManager::new( + &MidnightIndexerClient::new(network.indexer_urls.clone()), + &wallet_seed, + NetworkId::TestNet, + Arc::new(InMemorySyncState::new()), + relayer.id.clone(), + ) + .unwrap(), + )), + network, + ) + .unwrap(); + + // Execute test - validate_transaction always returns true for Midnight + let result = midnight_transaction.validate_transaction(test_tx).await; + assert!(result.is_ok()); + assert!(result.unwrap()); + } +} diff --git a/src/domain/transaction/midnight/types.rs b/src/domain/transaction/midnight/types.rs index 3c0e4ce6b..c07c94733 100644 --- a/src/domain/transaction/midnight/types.rs +++ b/src/domain/transaction/midnight/types.rs @@ -237,9 +237,55 @@ pub const DUST_TOKEN_TYPE: TokenType = NATIVE_TOKEN; pub fn to_midnight_network_id(network: &str) -> NetworkId { match network.to_lowercase().as_str() { + "mainnet" => NetworkId::MainNet, "devnet" => NetworkId::DevNet, "testnet" => NetworkId::TestNet, - "mainnet" => NetworkId::MainNet, _ => NetworkId::Undeployed, } } + +#[cfg(test)] +mod tests { + use super::*; + use midnight_node_ledger_helpers::NetworkId; + + #[test] + fn test_to_midnight_network_id() { + assert_eq!(to_midnight_network_id("mainnet"), NetworkId::MainNet); + assert_eq!(to_midnight_network_id("MAINNET"), NetworkId::MainNet); + assert_eq!(to_midnight_network_id("testnet"), NetworkId::TestNet); + assert_eq!(to_midnight_network_id("TESTNET"), NetworkId::TestNet); + assert_eq!(to_midnight_network_id("devnet"), NetworkId::DevNet); + assert_eq!(to_midnight_network_id("DEVNET"), NetworkId::DevNet); + assert_eq!(to_midnight_network_id("localnet"), NetworkId::Undeployed); + assert_eq!(to_midnight_network_id("random"), NetworkId::Undeployed); + } + + #[test] + fn test_either_enum() { + let left: Either = Either::Left(42); + let right: Either = Either::Right("hello".to_string()); + + match left { + Either::Left(val) => assert_eq!(val, 42), + Either::Right(_) => panic!("Expected Left variant"), + } + + match right { + Either::Left(_) => panic!("Expected Right variant"), + Either::Right(val) => assert_eq!(val, "hello"), + } + } + + #[test] + fn test_either_serialization() { + let left: Either = Either::Left(42); + let json = serde_json::to_string(&left).unwrap(); + let deserialized: Either = serde_json::from_str(&json).unwrap(); + + match deserialized { + Either::Left(val) => assert_eq!(val, 42), + Either::Right(_) => panic!("Expected Left variant after deserialization"), + } + } +} diff --git a/src/models/transaction/request/midnight.rs b/src/models/transaction/request/midnight.rs index 73f7e6110..73957300f 100644 --- a/src/models/transaction/request/midnight.rs +++ b/src/models/transaction/request/midnight.rs @@ -61,3 +61,169 @@ pub struct MidnightOutputRequest { /// Amount to send (in smallest unit) pub value: String, } + +#[cfg(test)] +mod tests { + use super::*; + use chrono::{Duration, Utc}; + + #[test] + fn test_midnight_input_request_creation() { + let input = MidnightInputRequest { + origin: "b49408db310c043ab736fb57a98e15c8cedbed4c38450df3755ac9726ee14d0c".to_string(), + token_type: "02000000000000000000000000000000000000000000000000000000000000000000" + .to_string(), + value: "1000000".to_string(), + }; + + assert_eq!(input.origin.len(), 64); // Hex encoded 32 bytes + assert_eq!(input.token_type.len(), 68); // Hex encoded 34 bytes + assert_eq!(input.value, "1000000"); + } + + #[test] + fn test_midnight_output_request_creation() { + let output = MidnightOutputRequest { + destination: "ttmnta1a0q8lwqqsm5qc8tgj2kp5wnmvhthpqvutpvfpajpqkcqr4s8xgmkepxskr2jkc" + .to_string(), + token_type: "02000000000000000000000000000000000000000000000000000000000000000000" + .to_string(), + value: "500000".to_string(), + }; + + assert!(!output.destination.is_empty()); + assert_eq!(output.token_type.len(), 68); + assert_eq!(output.value, "500000"); + } + + #[test] + fn test_midnight_offer_request_creation() { + let offer = MidnightOfferRequest { + inputs: vec![MidnightInputRequest { + origin: "b49408db310c043ab736fb57a98e15c8cedbed4c38450df3755ac9726ee14d0c" + .to_string(), + token_type: "02000000000000000000000000000000000000000000000000000000000000000000" + .to_string(), + value: "1000000".to_string(), + }], + outputs: vec![MidnightOutputRequest { + destination: + "ttmnta1a0q8lwqqsm5qc8tgj2kp5wnmvhthpqvutpvfpajpqkcqr4s8xgmkepxskr2jkc" + .to_string(), + token_type: "02000000000000000000000000000000000000000000000000000000000000000000" + .to_string(), + value: "900000".to_string(), + }], + }; + + assert_eq!(offer.inputs.len(), 1); + assert_eq!(offer.outputs.len(), 1); + } + + #[test] + fn test_midnight_transaction_request_with_guaranteed_offer() { + let request = MidnightTransactionRequest { + ttl: Some((Utc::now() + Duration::minutes(5)).to_rfc3339()), + guaranteed_offer: Some(MidnightOfferRequest { + inputs: vec![MidnightInputRequest { + origin: "b49408db310c043ab736fb57a98e15c8cedbed4c38450df3755ac9726ee14d0c" + .to_string(), + token_type: + "02000000000000000000000000000000000000000000000000000000000000000000" + .to_string(), + value: "1000000".to_string(), + }], + outputs: vec![MidnightOutputRequest { + destination: + "ttmnta1a0q8lwqqsm5qc8tgj2kp5wnmvhthpqvutpvfpajpqkcqr4s8xgmkepxskr2jkc" + .to_string(), + token_type: + "02000000000000000000000000000000000000000000000000000000000000000000" + .to_string(), + value: "900000".to_string(), + }], + }), + intents: vec![], + fallible_offers: vec![], + }; + + assert!(request.guaranteed_offer.is_some()); + assert_eq!(request.intents.len(), 0); + assert_eq!(request.fallible_offers.len(), 0); + assert!(request.ttl.is_some()); + } + + #[test] + fn test_midnight_intent_request_creation() { + let intent = MidnightIntentRequest { + segment_id: 1, + actions: vec![MidnightContractAction {}], + }; + + assert_eq!(intent.segment_id, 1); + assert_eq!(intent.actions.len(), 1); + } + + #[test] + fn test_midnight_transaction_request_with_fallible_offers() { + let request = MidnightTransactionRequest { + ttl: None, + guaranteed_offer: None, + intents: vec![], + fallible_offers: vec![ + (1, MidnightOfferRequest { + inputs: vec![], + outputs: vec![ + MidnightOutputRequest { + destination: "ttmnta1a0q8lwqqsm5qc8tgj2kp5wnmvhthpqvutpvfpajpqkcqr4s8xgmkepxskr2jkc".to_string(), + token_type: "02000000000000000000000000000000000000000000000000000000000000000000".to_string(), + value: "50000".to_string(), + }, + ], + }), + (2, MidnightOfferRequest { + inputs: vec![], + outputs: vec![ + MidnightOutputRequest { + destination: "ttmnta1a0q8lwqqsm5qc8tgj2kp5wnmvhthpqvutpvfpajpqkcqr4s8xgmkepxskr2jkc".to_string(), + token_type: "02000000000000000000000000000000000000000000000000000000000000000000".to_string(), + value: "75000".to_string(), + }, + ], + }), + ], + }; + + assert_eq!(request.fallible_offers.len(), 2); + assert_eq!(request.fallible_offers[0].0, 1); // Segment ID + assert_eq!(request.fallible_offers[1].0, 2); // Segment ID + } + + #[test] + fn test_midnight_transaction_request_serialization() { + let request = MidnightTransactionRequest { + ttl: Some((Utc::now() + Duration::hours(1)).to_rfc3339()), + guaranteed_offer: Some(MidnightOfferRequest { + inputs: vec![MidnightInputRequest { + origin: "aabbccdd".repeat(8), // 64 chars + token_type: "02".to_string() + &"00".repeat(33), // 68 chars + value: "500000".to_string(), + }], + outputs: vec![MidnightOutputRequest { + destination: "ttmnta1recipient".to_string(), + token_type: "02".to_string() + &"00".repeat(33), + value: "450000".to_string(), + }], + }), + intents: vec![], + fallible_offers: vec![], + }; + + // Test serialization/deserialization + let serialized = serde_json::to_string(&request).unwrap(); + let deserialized: MidnightTransactionRequest = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(deserialized.guaranteed_offer.unwrap().inputs.len(), 1); + assert_eq!(deserialized.ttl, request.ttl); + } +} diff --git a/src/repositories/sync_state.rs b/src/repositories/sync_state.rs index 523892717..87d243ca7 100644 --- a/src/repositories/sync_state.rs +++ b/src/repositories/sync_state.rs @@ -326,4 +326,99 @@ mod tests { assert_eq!(store.get_last_synced_index(&relayer_id).unwrap(), Some(99)); } } + + #[test] + fn test_set_sync_state_overwrite() { + let store = InMemorySyncState::new(); + let relayer_id = "relayer_1"; + + // Set initial state + store + .set_sync_state(relayer_id, 100, Some(vec![1, 2, 3])) + .unwrap(); + assert_eq!(store.get_last_synced_index(relayer_id).unwrap(), Some(100)); + assert_eq!( + store.get_ledger_context(relayer_id).unwrap(), + Some(vec![1, 2, 3]) + ); + + // Overwrite with new state + store + .set_sync_state(relayer_id, 200, Some(vec![4, 5, 6])) + .unwrap(); + assert_eq!(store.get_last_synced_index(relayer_id).unwrap(), Some(200)); + assert_eq!( + store.get_ledger_context(relayer_id).unwrap(), + Some(vec![4, 5, 6]) + ); + + // Overwrite with no context + store.set_sync_state(relayer_id, 300, None).unwrap(); + assert_eq!(store.get_last_synced_index(relayer_id).unwrap(), Some(300)); + assert_eq!(store.get_ledger_context(relayer_id).unwrap(), None); + } + + #[test] + fn test_ledger_context_independence() { + let store = InMemorySyncState::new(); + + // Set ledger context without index + store + .set_ledger_context("relayer_1", vec![1, 2, 3]) + .unwrap(); + assert_eq!(store.get_last_synced_index("relayer_1").unwrap(), Some(0)); + assert_eq!( + store.get_ledger_context("relayer_1").unwrap(), + Some(vec![1, 2, 3]) + ); + + // Update index without affecting context + store.set_last_synced_index("relayer_1", 100).unwrap(); + assert_eq!(store.get_last_synced_index("relayer_1").unwrap(), Some(100)); + assert_eq!( + store.get_ledger_context("relayer_1").unwrap(), + Some(vec![1, 2, 3]) + ); + + // Update context without affecting index + store + .set_ledger_context("relayer_1", vec![4, 5, 6]) + .unwrap(); + assert_eq!(store.get_last_synced_index("relayer_1").unwrap(), Some(100)); + assert_eq!( + store.get_ledger_context("relayer_1").unwrap(), + Some(vec![4, 5, 6]) + ); + } + + #[test] + fn test_empty_ledger_context() { + let store = InMemorySyncState::new(); + let relayer_id = "relayer_1"; + + // Set empty context + store.set_ledger_context(relayer_id, vec![]).unwrap(); + assert_eq!(store.get_ledger_context(relayer_id).unwrap(), Some(vec![])); + + // Empty context is different from None + store.reset(relayer_id).unwrap(); + assert_eq!(store.get_ledger_context(relayer_id).unwrap(), None); + } + + #[test] + fn test_large_ledger_context() { + let store = InMemorySyncState::new(); + let relayer_id = "relayer_1"; + + // Create a large context (simulating serialized ledger state) + let large_context: Vec = (0..10000).map(|i| (i % 256) as u8).collect(); + + store + .set_ledger_context(relayer_id, large_context.clone()) + .unwrap(); + let retrieved = store.get_ledger_context(relayer_id).unwrap().unwrap(); + + assert_eq!(retrieved.len(), large_context.len()); + assert_eq!(retrieved, large_context); + } } diff --git a/src/services/sync/midnight/handler/events.rs b/src/services/sync/midnight/handler/events.rs index 058cb33b6..776af8094 100644 --- a/src/services/sync/midnight/handler/events.rs +++ b/src/services/sync/midnight/handler/events.rs @@ -261,3 +261,158 @@ pub fn convert_indexer_event(event: IndexerEvent) -> Vec { sync_events } + +#[cfg(test)] +mod tests { + use super::*; + use crate::services::midnight::indexer::{ApplyStage, TransactionData}; + + #[tokio::test] + async fn test_event_dispatcher_new() { + let dispatcher = EventDispatcher::new(); + assert_eq!(dispatcher.handlers.len(), 0); + } + + #[tokio::test] + async fn test_event_handler_type_name() { + let handler = EventHandlerType::EventHandler { + network: NetworkId::TestNet, + updates_buffer: Arc::new(Mutex::new(Vec::new())), + }; + assert_eq!(handler.name(), "EventHandler"); + } + + #[test] + fn test_chronological_update_clone() { + // Test that ChronologicalUpdate enum variants store the expected data + // Testing with index values to verify the enum works correctly + + // Test Transaction variant + let tx_index = 100; + let tx_stage = Some(ApplyStage::SucceedEntirely); + + // Test MerkleUpdate variant + let merkle_index = 200; + + // Just verify we can construct the enum variants + // Actual cloning would require valid Transaction/MerkleTreeCollapsedUpdate instances + assert_eq!(tx_index, 100); + assert_eq!(merkle_index, 200); + assert_eq!(tx_stage, Some(ApplyStage::SucceedEntirely)); + } + + #[test] + fn test_convert_indexer_event_viewing_update() { + // Test with merkle updates and transactions + let indexer_event = IndexerEvent::ViewingUpdate { + type_name: "test".to_string(), + index: 100, + update: vec![ + ZswapChainStateUpdate::MerkleTreeCollapsedUpdate { + protocol_version: 1, + start: 0, + end: 10, + update: "update_data".to_string(), + }, + ZswapChainStateUpdate::RelevantTransaction { + transaction: TransactionData { + hash: "hash1".to_string(), + identifiers: Some(vec!["id1".to_string()]), + raw: Some("raw1".to_string()), + merkle_tree_root: Some("root1".to_string()), + protocol_version: Some(1), + apply_stage: Some(ApplyStage::SucceedEntirely), + }, + start: 0, + end: 10, + }, + ], + }; + + let sync_events = convert_indexer_event(indexer_event); + assert_eq!(sync_events.len(), 2); + + // Merkle update should be first + match &sync_events[0] { + SyncEvent::MerkleUpdateReceived { + blockchain_index, .. + } => { + assert_eq!(*blockchain_index, 100); + } + _ => panic!("Expected MerkleUpdateReceived first"), + } + + // Transaction should be second + match &sync_events[1] { + SyncEvent::TransactionReceived { + blockchain_index, .. + } => { + assert_eq!(*blockchain_index, 100); + } + _ => panic!("Expected TransactionReceived second"), + } + } + + #[test] + fn test_convert_indexer_event_progress_update() { + let indexer_event = IndexerEvent::ProgressUpdate { + type_name: "test".to_string(), + highest_index: 1000, + highest_relevant_index: 950, + highest_relevant_wallet_index: 900, + }; + + let sync_events = convert_indexer_event(indexer_event); + assert_eq!(sync_events.len(), 1); + + match &sync_events[0] { + SyncEvent::ProgressUpdate { + highest_index, + highest_relevant_wallet_index, + } => { + assert_eq!(*highest_index, 1000); + assert_eq!(*highest_relevant_wallet_index, 900); + } + _ => panic!("Expected ProgressUpdate"), + } + } + + #[test] + fn test_convert_indexer_event_empty_merkle_update() { + let indexer_event = IndexerEvent::ViewingUpdate { + type_name: "test".to_string(), + index: 100, + update: vec![ZswapChainStateUpdate::MerkleTreeCollapsedUpdate { + protocol_version: 1, + start: 0, + end: 10, + update: "".to_string(), // Empty update + }], + }; + + let sync_events = convert_indexer_event(indexer_event); + assert_eq!(sync_events.len(), 0); // Empty updates should be filtered out + } + + #[test] + fn test_event_handler_sync_completed() { + // Test that SyncCompleted event is handled properly + let mut handler = EventHandlerType::EventHandler { + network: NetworkId::TestNet, + updates_buffer: Arc::new(Mutex::new(Vec::new())), + }; + + let event = SyncEvent::SyncCompleted; + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(handler.handle(&event)); + assert!(result.is_ok()); + } + + #[test] + fn test_apply_stage_should_apply() { + assert!(ApplyStage::SucceedEntirely.should_apply()); + assert!(ApplyStage::SucceedPartially.should_apply()); + assert!(!ApplyStage::FailEntirely.should_apply()); + assert!(!ApplyStage::Pending.should_apply()); + } +} diff --git a/src/services/sync/midnight/handler/manager.rs b/src/services/sync/midnight/handler/manager.rs index e841da6e4..0fe477ec2 100644 --- a/src/services/sync/midnight/handler/manager.rs +++ b/src/services/sync/midnight/handler/manager.rs @@ -84,7 +84,7 @@ impl SyncManager { } /// Restore the ledger context from serialized bytes - fn restore_context(&self, serialized_context: &[u8]) -> Result<(), SyncError> { + pub fn restore_context(&self, serialized_context: &[u8]) -> Result<(), SyncError> { // Deserialize the combined context match bincode::deserialize::<(Option>, Vec)>(serialized_context) { Ok((wallet_state_bytes, ledger_state_bytes)) => { @@ -342,3 +342,310 @@ impl super::SyncManagerTrait for SyncManager { self.get_context() } } + +#[cfg(test)] +mod tests { + use crate::{ + repositories::{InMemorySyncState, SyncStateTrait}, + services::{ + midnight::{ + handler::{QuickSyncStrategy, SyncManager, SyncManagerTrait}, + indexer::MidnightIndexerClient, + SyncError, + }, + sync::midnight::handler::{ + EventDispatcher, ProgressTracker, SyncConfig, SyncEvent, SyncStrategy, + }, + }, + }; + use midnight_node_ledger_helpers::{NetworkId, WalletSeed}; + use std::sync::Arc; + + fn setup_test_env() { + std::env::set_var( + "MIDNIGHT_LEDGER_TEST_STATIC_DIR", + "/tmp/midnight-test-static", + ); + } + // Mock sync strategy for testing + struct MockSyncStrategy { + _config: Option, + sync_called: std::sync::Mutex, + should_fail: bool, + } + + #[async_trait::async_trait] + impl SyncStrategy for MockSyncStrategy { + fn new(_indexer_client: &MidnightIndexerClient, config: Option) -> Self { + Self { + _config: config, + sync_called: std::sync::Mutex::new(false), + should_fail: false, + } + } + + async fn sync( + &mut self, + start_index: u64, + dispatcher: &mut EventDispatcher, + progress_tracker: &mut ProgressTracker, + ) -> Result<(), SyncError> { + *self.sync_called.lock().unwrap() = true; + + if self.should_fail { + return Err(SyncError::SyncError("Mock sync failed".to_string())); + } + + // Simulate some sync activity + progress_tracker.record_transaction(start_index + 1); + dispatcher + .dispatch(&SyncEvent::SyncCompleted) + .await + .unwrap(); + + Ok(()) + } + } + + #[tokio::test] + async fn test_sync_manager_new() { + setup_test_env(); + + let seed = WalletSeed::from([1u8; 32]); + let sync_state_store = Arc::new(InMemorySyncState::new()); + let relayer_id = "test-relayer".to_string(); + let indexer_urls = crate::config::network::IndexerUrls { + http: "http://localhost:8080".to_string(), + ws: "ws://localhost:8080".to_string(), + }; + let indexer_client = MidnightIndexerClient::new(indexer_urls); + + let manager = SyncManager::::new( + &indexer_client, + &seed, + NetworkId::TestNet, + sync_state_store, + relayer_id, + ); + + assert!(manager.is_ok()); + } + + #[tokio::test] + async fn test_sync_manager_sync_from_specific_index() { + setup_test_env(); + + let seed = WalletSeed::from([1u8; 32]); + let sync_state_store = Arc::new(InMemorySyncState::new()); + let relayer_id = "test-relayer".to_string(); + let indexer_urls = crate::config::network::IndexerUrls { + http: "http://localhost:8080".to_string(), + ws: "ws://localhost:8080".to_string(), + }; + let indexer_client = MidnightIndexerClient::new(indexer_urls); + + let mut manager = SyncManager::::new( + &indexer_client, + &seed, + NetworkId::TestNet, + sync_state_store.clone(), + relayer_id.clone(), + ) + .unwrap(); + + // Sync from index 100 + let result = manager.sync(Some(100)).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_sync_manager_sync_incremental() { + setup_test_env(); + + let seed = WalletSeed::from([1u8; 32]); + let sync_state_store = Arc::new(InMemorySyncState::new()); + let relayer_id = "test-relayer".to_string(); + + // Pre-set a sync state + sync_state_store + .set_last_synced_index(&relayer_id, 500) + .unwrap(); + + let indexer_urls = crate::config::network::IndexerUrls { + http: "http://localhost:8080".to_string(), + ws: "ws://localhost:8080".to_string(), + }; + let indexer_client = MidnightIndexerClient::new(indexer_urls); + + let mut manager = SyncManager::::new( + &indexer_client, + &seed, + NetworkId::TestNet, + sync_state_store.clone(), + relayer_id.clone(), + ) + .unwrap(); + + // Sync incrementally (should start from 500) + let result = manager.sync_incremental().await; + assert!(result.is_ok()); + + // Verify sync was called - the mock doesn't actually update the state + // because SyncManager only saves state when there are real chronological updates + let stored_index = sync_state_store.get_last_synced_index(&relayer_id).unwrap(); + assert_eq!(stored_index, Some(500)); // Should remain at the pre-set value + } + + #[tokio::test] + async fn test_sync_manager_get_context() { + setup_test_env(); + + let seed = WalletSeed::from([1u8; 32]); + let sync_state_store = Arc::new(InMemorySyncState::new()); + let relayer_id = "test-relayer".to_string(); + let indexer_urls = crate::config::network::IndexerUrls { + http: "http://localhost:8080".to_string(), + ws: "ws://localhost:8080".to_string(), + }; + let indexer_client = MidnightIndexerClient::new(indexer_urls); + + let manager = SyncManager::::new( + &indexer_client, + &seed, + NetworkId::TestNet, + sync_state_store, + relayer_id, + ) + .unwrap(); + + let context = manager.get_context(); + assert!(Arc::strong_count(&context) > 1); // Manager holds one, we hold one + } + + #[tokio::test] + async fn test_sync_manager_trait_implementation() { + setup_test_env(); + + let seed = WalletSeed::from([1u8; 32]); + let sync_state_store = Arc::new(InMemorySyncState::new()); + let relayer_id = "test-relayer".to_string(); + let indexer_urls = crate::config::network::IndexerUrls { + http: "http://localhost:8080".to_string(), + ws: "ws://localhost:8080".to_string(), + }; + let indexer_client = MidnightIndexerClient::new(indexer_urls); + + let mut manager: Box = Box::new( + SyncManager::::new( + &indexer_client, + &seed, + NetworkId::TestNet, + sync_state_store, + relayer_id, + ) + .unwrap(), + ); + + // Test trait methods + let result = manager.sync(200).await; + assert!(result.is_ok()); + + let context = manager.get_context(); + assert!(Arc::strong_count(&context) > 1); + } + + #[test] + fn test_sync_config_default() { + let config = SyncConfig::default(); + assert!(config.viewing_key.is_none()); + assert_eq!(config.idle_timeout, Some(std::time::Duration::from_secs(5))); + assert_eq!(config.send_progress_events, Some(true)); + } + + #[test] + fn test_sync_config_custom() { + let config = SyncConfig { + viewing_key: None, + idle_timeout: Some(std::time::Duration::from_secs(10)), + send_progress_events: Some(false), + }; + assert!(config.viewing_key.is_none()); + assert_eq!( + config.idle_timeout, + Some(std::time::Duration::from_secs(10)) + ); + assert_eq!(config.send_progress_events, Some(false)); + } + + #[tokio::test] + async fn test_serialize_and_restore_context() { + setup_test_env(); + + let seed = WalletSeed::from([1u8; 32]); + let sync_state_store = Arc::new(InMemorySyncState::new()); + let relayer_id = "test-relayer".to_string(); + let indexer_urls = crate::config::network::IndexerUrls { + http: "http://localhost:8080".to_string(), + ws: "ws://localhost:8080".to_string(), + }; + let indexer_client = MidnightIndexerClient::new(indexer_urls); + + // Create first manager + let _manager1 = SyncManager::::new( + &indexer_client, + &seed, + NetworkId::TestNet, + sync_state_store.clone(), + relayer_id.clone(), + ) + .unwrap(); + + // Can't test private serialize_context method directly + // Save some dummy context to sync state + let dummy_context = vec![1, 2, 3, 4, 5]; + sync_state_store + .set_sync_state(&relayer_id, 100, Some(dummy_context.clone())) + .unwrap(); + + // Create second manager - should attempt to restore context + let manager2 = SyncManager::::new( + &indexer_client, + &seed, + NetworkId::TestNet, + sync_state_store.clone(), + relayer_id.clone(), + ); + + assert!(manager2.is_ok()); // Should handle invalid data gracefully + } + + #[test] + fn test_restore_context_invalid_data() { + setup_test_env(); + + let seed = WalletSeed::from([1u8; 32]); + let sync_state_store = Arc::new(InMemorySyncState::new()); + let relayer_id = "test-relayer".to_string(); + let indexer_urls = crate::config::network::IndexerUrls { + http: "http://localhost:8080".to_string(), + ws: "ws://localhost:8080".to_string(), + }; + let indexer_client = MidnightIndexerClient::new(indexer_urls.clone()); + + // Save invalid data + sync_state_store + .set_ledger_context(&relayer_id, vec![1, 2, 3, 4, 5]) // Invalid serialized data + .unwrap(); + + // Create manager - should handle invalid data gracefully + let manager = SyncManager::::new( + &indexer_client, + &seed, + NetworkId::TestNet, + sync_state_store, + relayer_id, + ); + + assert!(manager.is_ok()); // Should not fail, just start fresh + } +} diff --git a/src/services/sync/midnight/handler/tracker.rs b/src/services/sync/midnight/handler/tracker.rs index beafa4c16..26437997a 100644 --- a/src/services/sync/midnight/handler/tracker.rs +++ b/src/services/sync/midnight/handler/tracker.rs @@ -70,3 +70,138 @@ impl ProgressTracker { && self.has_processed_data() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_progress_tracker_new() { + let tracker = ProgressTracker::new(100); + assert_eq!(tracker.start_index, 100); + assert_eq!(tracker.last_processed_index, 100); + assert!(tracker.processed_indices.is_empty()); + assert_eq!(tracker.total_transactions_processed, 0); + assert_eq!(tracker.total_merkle_updates_processed, 0); + assert!(!tracker.has_processed_data()); + } + + #[test] + fn test_record_processed() { + let mut tracker = ProgressTracker::new(100); + + tracker.record_processed(105); + assert_eq!(tracker.last_processed_index, 105); + assert!(tracker.processed_indices.contains(&105)); + + tracker.record_processed(103); + assert_eq!(tracker.last_processed_index, 105); // Should remain at max + assert!(tracker.processed_indices.contains(&103)); + assert!(tracker.processed_indices.contains(&105)); + + tracker.record_processed(110); + assert_eq!(tracker.last_processed_index, 110); + assert_eq!(tracker.processed_indices.len(), 3); + } + + #[test] + fn test_record_transaction() { + let mut tracker = ProgressTracker::new(100); + + tracker.record_transaction(105); + assert_eq!(tracker.last_processed_index, 105); + assert!(tracker.processed_indices.contains(&105)); + assert_eq!(tracker.total_transactions_processed, 1); + assert!(tracker.has_processed_data()); + + tracker.record_transaction(106); + assert_eq!(tracker.total_transactions_processed, 2); + } + + #[test] + fn test_record_merkle_update() { + let mut tracker = ProgressTracker::new(100); + + tracker.record_merkle_update(105); + assert_eq!(tracker.last_processed_index, 105); + assert!(tracker.processed_indices.contains(&105)); + assert_eq!(tracker.total_merkle_updates_processed, 1); + assert!(tracker.has_processed_data()); + + tracker.record_merkle_update(106); + assert_eq!(tracker.total_merkle_updates_processed, 2); + } + + #[test] + fn test_has_processed_data() { + let mut tracker = ProgressTracker::new(100); + assert!(!tracker.has_processed_data()); + + tracker.record_transaction(105); + assert!(tracker.has_processed_data()); + + let mut tracker2 = ProgressTracker::new(100); + tracker2.record_merkle_update(105); + assert!(tracker2.has_processed_data()); + + let mut tracker3 = ProgressTracker::new(100); + tracker3.record_processed(105); // Just recording index, no data + assert!(!tracker3.has_processed_data()); + } + + #[test] + fn test_is_sync_complete_already_caught_up() { + let tracker = ProgressTracker::new(200); + + // Already at or past highest relevant wallet index + assert!(tracker.is_sync_complete(250, 150)); + assert!(tracker.is_sync_complete(250, 200)); + } + + #[test] + fn test_is_sync_complete_needs_processing() { + let mut tracker = ProgressTracker::new(100); + + // Not complete - haven't processed anything + assert!(!tracker.is_sync_complete(250, 200)); + + // Process some data but not enough + tracker.record_transaction(150); + assert!(!tracker.is_sync_complete(250, 200)); + + // Process up to highest relevant index + tracker.record_transaction(200); + assert!(tracker.is_sync_complete(250, 200)); + } + + #[test] + fn test_is_sync_complete_edge_cases() { + let mut tracker = ProgressTracker::new(100); + + // Highest index is less than highest relevant wallet index + tracker.record_transaction(150); + assert!(!tracker.is_sync_complete(150, 200)); + + // Processed index equals highest relevant wallet index + tracker.record_merkle_update(200); + assert!(tracker.is_sync_complete(200, 200)); + } + + #[test] + fn test_mixed_processing() { + let mut tracker = ProgressTracker::new(100); + + // Mix of transactions and merkle updates + tracker.record_transaction(105); + tracker.record_merkle_update(106); + tracker.record_transaction(107); + tracker.record_processed(108); // Just an index + tracker.record_merkle_update(109); + + assert_eq!(tracker.last_processed_index, 109); + assert_eq!(tracker.processed_indices.len(), 5); + assert_eq!(tracker.total_transactions_processed, 2); + assert_eq!(tracker.total_merkle_updates_processed, 2); + assert!(tracker.has_processed_data()); + } +} diff --git a/src/services/sync/midnight/mod.rs b/src/services/sync/midnight/mod.rs index 6aa6112ab..ad2f43709 100644 --- a/src/services/sync/midnight/mod.rs +++ b/src/services/sync/midnight/mod.rs @@ -2,6 +2,9 @@ pub mod handler; pub mod indexer; pub mod utils; +#[cfg(test)] +pub mod test_utils; + use crate::services::midnight::indexer::IndexerError; /// diff --git a/src/services/sync/midnight/test_utils.rs b/src/services/sync/midnight/test_utils.rs new file mode 100644 index 000000000..5616ea895 --- /dev/null +++ b/src/services/sync/midnight/test_utils.rs @@ -0,0 +1,306 @@ +//! Test utilities for Midnight sync and transaction testing +//! +//! This module provides utilities for creating test contexts with funded wallets. +//! Since we cannot directly create coins in wallets without proper Midnight transaction +//! utilities, we support loading serialized wallet states from test fixtures. + +use midnight_node_ledger_helpers::{ + mn_ledger_serialize::{deserialize, serialize}, + DefaultDB, LedgerContext, LedgerState, NetworkId, WalletSeed, WalletState, +}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +/// Test fixture directory for storing serialized wallet states +const TEST_FIXTURE_DIR: &str = "tests/fixtures/midnight"; + +/// Creates a test LedgerContext with pre-funded wallets +/// +/// This function attempts to load wallet states from test fixtures if available. +/// If no fixtures exist, it creates an empty context with initialized wallets. +pub fn create_funded_test_context( + wallet_seeds: &[WalletSeed], + _initial_balance: u128, +) -> Arc> { + // Set required environment variable + std::env::set_var( + "MIDNIGHT_LEDGER_TEST_STATIC_DIR", + "/tmp/midnight-test-static", + ); + + let context = Arc::new(LedgerContext::new_from_wallet_seeds(wallet_seeds)); + + // Try to load wallet states from fixtures + for seed in wallet_seeds { + if let Ok(wallet_state) = load_wallet_state_fixture(seed, NetworkId::TestNet) { + if let Ok(mut wallets_guard) = context.wallets.lock() { + if let Some(wallet) = wallets_guard.get_mut(seed) { + wallet.update_state(wallet_state); + log::debug!("Loaded wallet state from fixture for seed: {:?}", seed); + } + } + } + } + + context +} + +/// Creates a mock transaction for testing +pub fn create_test_transaction_data() -> Vec { + // This would be a properly serialized Midnight transaction + // For now, return dummy data + vec![1, 2, 3, 4, 5] +} + +/// Saves a wallet state to a test fixture file +pub fn save_wallet_state_fixture( + seed: &WalletSeed, + wallet_state: &WalletState, + network: NetworkId, +) -> Result<(), std::io::Error> { + let fixture_path = get_wallet_fixture_path(seed); + + // Create directory if it doesn't exist + if let Some(parent) = fixture_path.parent() { + fs::create_dir_all(parent)?; + } + + // Serialize wallet state + let mut state_bytes = Vec::new(); + serialize(wallet_state, &mut state_bytes, network).map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::Other, + format!("Failed to serialize wallet state: {:?}", e), + ) + })?; + + // Write to file + fs::write(fixture_path, state_bytes)?; + Ok(()) +} + +/// Loads a wallet state from a test fixture file +pub fn load_wallet_state_fixture( + seed: &WalletSeed, + network: NetworkId, +) -> Result, std::io::Error> { + let fixture_path = get_wallet_fixture_path(seed); + + // Read fixture file + let state_bytes = fs::read(&fixture_path)?; + + // Deserialize wallet state + let mut reader = &state_bytes[..]; + deserialize::, _>(&mut reader, network).map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::Other, + format!("Failed to deserialize wallet state: {:?}", e), + ) + }) +} + +/// Gets the fixture file path for a wallet seed +fn get_wallet_fixture_path(seed: &WalletSeed) -> PathBuf { + let seed_hex = hex::encode(seed.0); + Path::new(TEST_FIXTURE_DIR).join(format!("wallet_{}.bin", seed_hex)) +} + +/// Creates a test context from a serialized ledger context +/// +/// This can be used to restore a complete ledger context including wallet states +/// and ledger state from a previous sync operation. +pub fn create_context_from_serialized( + serialized_context: &[u8], + wallet_seeds: &[WalletSeed], + network: NetworkId, +) -> Result>, String> { + // Set required environment variable + std::env::set_var( + "MIDNIGHT_LEDGER_TEST_STATIC_DIR", + "/tmp/midnight-test-static", + ); + + let context = Arc::new(LedgerContext::new_from_wallet_seeds(wallet_seeds)); + + // Deserialize the combined context (wallet states + ledger state) + match bincode::deserialize::<(Option>, Vec)>(serialized_context) { + Ok((wallet_state_bytes, ledger_state_bytes)) => { + // Restore ledger state + let mut reader = &ledger_state_bytes[..]; + if let Ok(ledger_state) = deserialize::, _>(&mut reader, network) + { + if let Ok(mut ledger_state_guard) = context.ledger_state.lock() { + *ledger_state_guard = ledger_state; + } + } + + // Restore wallet state if available + if let Some(state_bytes) = wallet_state_bytes { + let mut reader = &state_bytes[..]; + if let Ok(wallet_state) = + deserialize::, _>(&mut reader, network) + { + if let Ok(mut wallets_guard) = context.wallets.lock() { + // Apply to the first wallet seed + if let Some(seed) = wallet_seeds.first() { + if let Some(wallet) = wallets_guard.get_mut(seed) { + wallet.update_state(wallet_state); + } + } + } + } + } + Ok(context) + } + Err(e) => Err(format!("Failed to deserialize context: {}", e)), + } +} + +/// Example serialized context with a funded wallet for testing +/// +/// This is a placeholder - in a real implementation, this would be generated +/// by running a sync operation on testnet and serializing the resulting context. +pub const EXAMPLE_FUNDED_CONTEXT: &[u8] = &[ + // This would contain actual serialized wallet state with coins + // Generated by syncing a funded testnet wallet + 0x00, 0x01, 0x02, 0x03, +]; + +/// Builder for creating mock wallet states for testing +/// +/// This provides a way to create wallet states with specific properties +/// without needing real blockchain data. +pub struct MockWalletStateBuilder { + first_free: u64, + // Additional fields would be added here as needed +} + +impl MockWalletStateBuilder { + pub fn new() -> Self { + Self { first_free: 0 } + } + + pub fn with_first_free(mut self, first_free: u64) -> Self { + self.first_free = first_free; + self + } + + /// Builds a context with the mock wallet state + /// + /// Note: This creates a wallet with the specified first_free value + /// but without actual coins. For tests requiring real UTXOs, + /// use fixture-based approaches instead. + pub fn build(self, wallet_seeds: &[WalletSeed]) -> Arc> { + std::env::set_var( + "MIDNIGHT_LEDGER_TEST_STATIC_DIR", + "/tmp/midnight-test-static", + ); + + let context = Arc::new(LedgerContext::new_from_wallet_seeds(wallet_seeds)); + + // Apply mock state to all wallets + if let Ok(mut wallets_guard) = context.wallets.lock() { + for seed in wallet_seeds { + if let Some(wallet) = wallets_guard.get_mut(seed) { + wallet.state.first_free = self.first_free; + } + } + } + + context + } +} + +impl Default for MockWalletStateBuilder { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_create_funded_test_context() { + let seed = WalletSeed::from([1u8; 32]); + let context = create_funded_test_context(&[seed], 1_000_000_000); + + // Verify wallet is initialized + let wallets_guard = context.wallets.lock().unwrap(); + let wallet = wallets_guard.get(&seed).unwrap(); + // With empty fixtures, first_free will be 0, but the wallet should exist + assert_eq!(wallet.state.first_free, 0); + } + + #[test] + fn test_create_funded_test_context_multiple_wallets() { + let seed1 = WalletSeed::from([1u8; 32]); + let seed2 = WalletSeed::from([2u8; 32]); + let context = create_funded_test_context(&[seed1, seed2], 500_000_000); + + let wallets_guard = context.wallets.lock().unwrap(); + + let wallet1 = wallets_guard.get(&seed1).unwrap(); + // With fixtures, first_free depends on the actual wallet state + assert_eq!(wallet1.state.first_free, 0); + + let wallet2 = wallets_guard.get(&seed2).unwrap(); + // Without fixture for seed2, it will have default value of 0 + assert_eq!(wallet2.state.first_free, 0); + } + + #[test] + fn test_wallet_fixture_path() { + let seed = WalletSeed::from([1u8; 32]); + let path = get_wallet_fixture_path(&seed); + + let expected = format!( + "{}/wallet_{}.bin", + TEST_FIXTURE_DIR, "0101010101010101010101010101010101010101010101010101010101010101" + ); + assert_eq!(path.to_str().unwrap(), expected); + } + + #[test] + fn test_create_context_from_serialized_empty() { + let seed = WalletSeed::from([1u8; 32]); + let empty_context = bincode::serialize(&(None::>, vec![0u8; 0])) + .expect("Failed to serialize empty context"); + + let result = create_context_from_serialized(&empty_context, &[seed], NetworkId::TestNet); + assert!(result.is_ok()); + } + + #[test] + fn test_mock_wallet_state_builder() { + let seed = WalletSeed::from([1u8; 32]); + + let context = MockWalletStateBuilder::new() + .with_first_free(42) + .build(&[seed]); + + let wallets_guard = context.wallets.lock().unwrap(); + let wallet = wallets_guard.get(&seed).unwrap(); + assert_eq!(wallet.state.first_free, 42); + } + + #[test] + fn test_mock_wallet_state_builder_multiple_wallets() { + let seed1 = WalletSeed::from([1u8; 32]); + let seed2 = WalletSeed::from([2u8; 32]); + + let context = MockWalletStateBuilder::new() + .with_first_free(100) + .build(&[seed1, seed2]); + + let wallets_guard = context.wallets.lock().unwrap(); + + let wallet1 = wallets_guard.get(&seed1).unwrap(); + assert_eq!(wallet1.state.first_free, 100); + + let wallet2 = wallets_guard.get(&seed2).unwrap(); + assert_eq!(wallet2.state.first_free, 100); + } +} diff --git a/tests/fixtures/midnight/context_0e0cc7db98c60a39a6b0888795ba3f1bb1d61298cce264d4beca1529650e9041_14834.bin b/tests/fixtures/midnight/context_0e0cc7db98c60a39a6b0888795ba3f1bb1d61298cce264d4beca1529650e9041_14834.bin new file mode 100644 index 0000000000000000000000000000000000000000..f493bffe916e75704c2118bf5a4cd5cae3bc6fa2 GIT binary patch literal 5381 zcmd7WcTf}C;s@}M&|8262)#GyMXE}#f)tTnq!}p+A`qG&AP6Ez@1TGp0s>D!L276s z4{QH(oIqVwV zNx^v{1K*WBcZ7?>Qe)C?(=e} z?CO#Tty!U<#5?WtBzQUj{huhO1-1orT8Ds?|6BzMu9F$ePQ&~#kouCcq1g5XsN-|) zxQoNtcJ0NZra`W;>(dIF2#Vj)df%eP1b%vy&yUxM@Py}a>jl;}>%?1O4Bt8_s~_v~(NNNOSh9I(IliUGbXInP zkj0S-ABb|E%wV?Gj`PPy_4V5f97@9G@2C=k+qM&L6NoU>WOg;iu08~UAP!(LKs4@| zPn*JW4vY!4dZ{>Xz`#O`IlOM$s`+T;E{z!hFecegX6IVudoTN?A8(qx7V_FYI^(0h zyNeJi9CRIFrJXzFDnFUwSPOlsy891s?CcXAcZgM4X#TMl9W zYN5zhhuDT2&iu+hVD;lh*xoO3B*d>+Z>#W?aSOX}I#vJ{4-bcH+CJ^gzs}L*{Ij8y zDCD(4(@cy3MhS7S>Y>I6)t8zB@a0FlZX`^^JyGE~&DHpfHZ-+WCgdY=M)zUM_=7r2dcbek*#^xs=F-4oFBL5BRAS543b#FJm@2@b~Lug}%z3i_Zd z{NqPbO3`*lTytfOsfTn7QNdwT1N__a>?i3EV+stt$Sh5r^t%Py0PU`!*#o2yXO+#?KF=It0m#_B9lkxj;^O!r~AdZY${{ z={<-aHF?D(3T;6KM?ZtYxx5%N#t=8`U8+OM0Gz&Pi#e{HRR@H56CV?d&tQxht27La z>ILcWrwcp7jj;Hqf{&okS0Upe2UVF<$OC@x3=%3$tnz(9lPh&(BZ(CsM{espl64td zkK;Scau)g}9N00x0)Hxr{2V;9*QVfPpa75`N1vKBitO5h#dnSz7-i++W7YUqJW>V& zXIuN5Z#TWbk_6j%3#x|Pvll@+W|ujGofh13UHz2X%`V51G*+V(mCpv(W%E4graO}7 zVT$iI`=g=J>_=;hqTQ)-wqnL5I%LCYb`&7JKEvlYi3arTzh&mP2l=;Cz$LEip? z)%WxYShCIkyZFyI(GB-y<@M-|t2{==YX{(!7jrSb*0*ffdYFiDBiU0K7xrkkAb?ccUqZVOj6ua9?Qw{v@pXE{{Pf$tMh#fLPSR^m zv1lxrp72t$s1~WxL7t<_`tG@RNvor7OzWttFnFMAFD`?g$lRC-9}noE*TwnzPd|l060eF&QIC`Qc)&O6FkWCG41?|%CXjNG@;dY z40Q#ipgk-e(*ERV?_%TvFT4}&-dAU@L^b9A(;e;MqX&M++ z)5Z@G4`CJLq%H%VKAKk_I~BJ`inUn=UjT4HD?YhReQy$@-eO$G^-4di{Z8#RINCFdL>pkz4Gnd)s*ik)|s`dv!rnqHR<OTJEwCVgGUvsu_fQJ;!y zzT2^i2TNvO&SY+IOOu^8aVDlUSSE)w<$suND?X3ms}7>JLTFOp$WD5#%);yAFdyA@ zJ)X?u1N_gAB_xeJo**~kVG_SA+yU})&=|TDzy8)e{J9(k4pI&KoUVfKiX1cR{`rRX z&ZPTTa(iac09Bh}kr-J?^H^#im@{9{t-_yAz$Az^`q~~GPI=N3c1Tg;@Rx<3Hs)N! zYx;X67YU?2pT#hON7pjaiC>Ni>g8Jwj{*1>u0qw9c?N%sb(ufUiVH89U>Vr66>=L++4T+Q-fgjDvnsy<;LV$=F!)@NNtDy%m`6Btt_h>X%2P1O zMc%}sCB|Lnhs8^UK1RXj2%{`{)N6xz>btoRX!Nv0n~WmsxAeZL0Z-~PoEnEG;yq@! zdYi-Z?6cWeTfnz_ti#JylGMxEE0`T;uM#&Y0G}&N=eyv-RAAJ>-idcz8Z(|~D~S3? zBw|e{Ja7})N{Gc#8dP!vGWbLd8No;MeL5EP9FJt>%Zy}>GwZV}yxHwfAD{sij|PXg z4#KF&4x94kZr=S^ygwkz4-sw-wB~3tu?R~Nmd>09aMKKb$iZv@%ZH;X%Oj7)-q|Ea z`Q@y`H`=BO-xelo#;`aD^{7rbK|+gi#I{^EaaiTKT9vzls_&2N0oA~1B-JI@Nl(}T z2gBie)9FrZvuSrtH$J$b^i_vzB@+UNL;?kryx$YW%~q!Xcn(>opHtpFc4hI*UiEo0 zvfJ*t&Oz;b7{-N}m~b({3s}5Ji^!eXf<=pY;#l5zu_Q-V@=-N$v^rB?T2}SfOM*!F z84kzcZ&?H7+8R50gaRoCmrIwrG!vrsFm^|`cQ*PX7LKk*1NisaYDzn;9?wC`0mjy1I>pj!GsSC?NB{;hRL$t~Z8 z_6(=R;jq=~_4^t#(R4|Xl2YYdB45HL^WUPjT*(`|#czZ&4*__JFCtjc@~M=%UtWty z>=U}(A7Ra3LB1``cIP-tsk3ik@lS%HGJ`%An_4ugq^u#23%3f^_WFb%q<;8))X>ZY z9lA4|4u_Y#ZyLFvG1gfmZD%AJmqWK)6w+Vs??cTr5-3HaV{8N9+PN9S{i_Q~EN0y1 z3G&%ud?-j{&Zcf6Zw-muH@4YCEUtc^3;b#)*w0H;Z5@^IgnNshMnxt(x7f2&zd3$r zl9&Drr^n%|ts$>S0&2I+8!~!lmX9FQd)kY?wt^+5yAhxPyar_e|9m*p;#&|EN>Rh- z^&^7jS0o8lQKhHE>^M2S%JQ>KUMy}HY~8y@#6}zj?zv5z*Nn{G%^tgn3`98yl_xKu z-!dc4a0Cw54!#xtA|hx7Jcj~Gp$cfU8ATzWa4Q|qq9v~(~efS>HfurXy~%QAo$5>^`~W*`!V zzP9HS>nXnQZsZyfty|*J@wk@p9Rnl5;_n5CX0IOCaAJn42Q$%6Di%7)Q*J8`-M*0v zWycH`(J-CiOgMb+PF-8|D(8MK_jiPlkQHAK-*w7u)|ZZ+s#ZZGcQbYXT;!RJIWQ?FApyMo0FlCH3urH!tY6MIP;XH6Midj7=4)56GX z=4w!(!sdYo^BK;J!w-mO)h?D#??so~9M~g43LSI*461=hFBEE8qSWjrp9A>gYb8l8 zdV}SG11~6gJjypHbZHyrqMWu`dO(Ta%Jzk@I73QN(I__;-=lMH((@?%CZ8;jWs7ef z)SOF33KU;5(_lHnS#Y@Og|v3S0_eMI6f`&oQDSo1k2DU^i#kHMX{ z#cvGEURVo!h03y?;jB2kdu6MVWa{QzLTvS;TRgW0?3Ipo4BN`6hSC&~?VD6)04~)P zFGl#uJvX!J$z}73YHkMeq<=Lnb`_ASx}htaDypz}s>hwQUrh2{<1^3+57U-0QP;*) zfziIn*~n~?+xLwTY-czd4v)V-#qrbguyF4a(pbqdOPCaG9(?c3_~^I_+MeRmRt|t; z=FsKFqh2h9&*C?CT~hjYpPCwZ?h`-_rf*d67_C zIG;txU!VWqwg0#M{@wonsrR4eBTt_(Kp=@fuOseyQt|yh|I?>G(COdvNh40cl^{@- t=alNTKlcZ|lHkS)&~#e=iza*!R{U6+Y`0)S$ literal 0 HcmV?d00001 From 885087e418c6a9747aedb7d811a9671c7cea01aa Mon Sep 17 00:00:00 2001 From: Nami Date: Thu, 10 Jul 2025 19:18:00 +0400 Subject: [PATCH 06/11] docs: Add midnight docs (#357) --- config/config.example.json | 17 ++++- docs/modules/ROOT/pages/index.adoc | 4 ++ .../ROOT/pages/network_configuration.adoc | 69 ++++++++++++++++++- docs/modules/ROOT/pages/roadmap.adoc | 12 ++++ docs/modules/ROOT/pages/signers.adoc | 11 ++- 5 files changed, 109 insertions(+), 4 deletions(-) diff --git a/config/config.example.json b/config/config.example.json index 3600bb3bb..d62f079b9 100644 --- a/config/config.example.json +++ b/config/config.example.json @@ -37,7 +37,10 @@ "cron_schedule": "0 0 * * *", "min_balance_threshold": 0 }, - "allowed_programs": ["11111111111111111111111111111111", "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"], + "allowed_programs": [ + "11111111111111111111111111111111", + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + ], "allowed_tokens": [ { "mint": "Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr", @@ -74,6 +77,18 @@ } ] } + }, + { + "id": "midnight-testnet-example", + "name": "Midnight Testnet Example", + "network": "testnet", + "paused": false, + "notification_id": "notification-example", + "signer_id": "local-signer", + "network_type": "midnight", + "policies": { + "min_balance": "0" + } } ], "notifications": [ diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc index b911ca6a6..aef7f726c 100644 --- a/docs/modules/ROOT/pages/index.adoc +++ b/docs/modules/ROOT/pages/index.adoc @@ -34,6 +34,7 @@ OpenZeppelin Relayer supports multiple blockchain networks through a flexible JS - **Any EVM-compatible network** (Ethereum, Polygon, BSC, Arbitrum, Optimism, etc.) - **Solana networks** (mainnet-beta, devnet, testnet, custom RPC endpoints) - **Stellar networks** (Pubnet, Testnet, custom networks) +- **Midnight network** (Testnet) - **Create custom network configurations** with specific RPC endpoints, chain IDs, and network parameters - **Use inheritance** to create network variants that inherit from base configurations @@ -51,6 +52,9 @@ OpenZeppelin Relayer supports multiple blockchain networks through a flexible JS |`stellar` |Stellar blockchain networks (Partial support). Supports Stellar Public Network and Testnet. + +|`midnight` +|Midnight blockchain networks (Partial support). Supports Midnight testnet. |=== Networks can be loaded from: diff --git a/docs/modules/ROOT/pages/network_configuration.adoc b/docs/modules/ROOT/pages/network_configuration.adoc index 18ecc1036..49a6a278d 100644 --- a/docs/modules/ROOT/pages/network_configuration.adoc +++ b/docs/modules/ROOT/pages/network_configuration.adoc @@ -11,6 +11,7 @@ Networks are defined in JSON configuration files, allowing you to: * Configure **any EVM-compatible network** (Ethereum, Polygon, BSC, Arbitrum, Optimism, etc.) * Set up **Solana networks** (mainnet-beta, devnet, testnet, custom RPC endpoints) * Configure **Stellar networks** (Pubnet, Testnet, custom networks) +* Configure **Midnight networks** (Testnet) * Create **custom network configurations** with specific RPC endpoints, chain IDs, and network parameters * Use **inheritance** to create network variants without duplicating configuration @@ -28,6 +29,9 @@ Networks are defined in JSON configuration files, allowing you to: |`stellar` |Stellar blockchain networks. Supports Stellar Public Network and Testnet. + +|`midnight` +|Midnight blockchain networks. Supports Midnight testnet with indexer and prover integration. |=== == Configuration Methods @@ -83,6 +87,7 @@ networks/ ├── evm.json # {"networks": [...]} ├── solana.json # {"networks": [...]} └── stellar.json # {"networks": [...]} +└── midnight.json # {"networks": [...]} ``` === Method 2: Direct Configuration @@ -121,7 +126,7 @@ All network types support these configuration fields: |`type` |string |Yes -|Network type: `"evm"`, `"solana"`, or `"stellar"` +|Network type: `"evm"`, `"solana"`, `"stellar"` or `"midnight"` |`network` |string @@ -285,6 +290,44 @@ Here's an example showing a Stellar network configuration with passphrase: } ---- +=== Midnight-Specific Fields + +[cols="1,1,1,3"] +|=== +|Field |Type |Required |Description + +|`indexer_urls` +|object +|Yes* +|Indexer service URLs with `ws` and `http` endpoints (*Required for base networks, optional for inherited) + +|`prover_url` +|string +|Yes* +|URL for the Midnight prover service (*Required for base networks, optional for inherited) +|=== + +==== Example: Midnight Network Configuration + +Here's an example showing a Midnight network configuration: + +[source,json] +---- +{ + "type": "midnight", + "network": "testnet", + "rpc_urls": ["wss://rpc.testnet-02.midnight.network/"], + "explorer_urls": ["https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Frpc.testnet-02.midnight.network#/explorer"], + "indexer_urls": { + "ws": "wss://indexer.testnet-02.midnight.network/api/v1/graphql/ws", + "http": "https://indexer.testnet-02.midnight.network/api/v1/graphql" + }, + "prover_url": "http://localhost:6300", + "average_blocktime_ms": 6000, + "is_testnet": true +} +---- + == Configuration Examples === Basic EVM Network @@ -357,6 +400,30 @@ Here's an example showing a Stellar network configuration with passphrase: } ---- +=== Midnight Network + +[source,json] +---- +{ + "type": "midnight", + "network": "testnet", + "rpc_urls": [ + "wss://rpc.testnet-02.midnight.network/" + ], + "explorer_urls": [ + "https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Frpc.testnet-02.midnight.network#/explorer" + ], + "indexer_urls": { + "ws": "wss://indexer.testnet-02.midnight.network/api/v1/graphql/ws", + "http": "https://indexer.testnet-02.midnight.network/api/v1/graphql" + }, + "prover_url": "http://localhost:6300", + "average_blocktime_ms": 6000, + "is_testnet": true, + "tags": ["testnet", "midnight"] +} +---- + == Network Inheritance Networks can inherit from other networks of the same type, allowing you to create variants without duplicating configuration: diff --git a/docs/modules/ROOT/pages/roadmap.adoc b/docs/modules/ROOT/pages/roadmap.adoc index f5bd18dda..475c5d61e 100644 --- a/docs/modules/ROOT/pages/roadmap.adoc +++ b/docs/modules/ROOT/pages/roadmap.adoc @@ -88,6 +88,18 @@ This roadmap represents our current plans and is subject to change. We will upda * Hosted signers * Full CRUD API support +=== Midnight (🚧 Alpha) + +==== Current Status +* Basic transaction submission +* Network configuration with indexer and prover integration +* Transaction status tracking +* WebSocket RPC support +* Local prover service integration + +==== Upcoming Features +* TBA + == Community and Documentation === Continuous diff --git a/docs/modules/ROOT/pages/signers.adoc b/docs/modules/ROOT/pages/signers.adoc index 923b5fded..7d56d98e2 100644 --- a/docs/modules/ROOT/pages/signers.adoc +++ b/docs/modules/ROOT/pages/signers.adoc @@ -43,44 +43,51 @@ OpenZeppelin Relayer supports the following signer types: The following table shows which signer types are compatible with each network type: -[cols="1,1,1,1"] +[cols="1,1,1,1,1"] |=== -|Signer Type |EVM Networks |Solana Networks |Stellar Networks +|Signer Type |EVM Networks |Solana Networks |Stellar Networks |Midnight Networks |`local` |✅ Supported |✅ Supported |✅ Supported +|✅ Supported |`vault` |✅ Supported |✅ Supported |❌ Not supported +|❌ Not supported |`vault_cloud` |✅ Supported |✅ Supported |❌ Not supported +|❌ Not supported |`vault_transit` |❌ Not supported |✅ Supported |❌ Not supported +|❌ Not supported |`turnkey` |✅ Supported |✅ Supported |❌ Not supported +|❌ Not supported |`google_cloud_kms` |✅ Supported |✅ Supported |❌ Not supported +|❌ Not supported |`aws_kms` |✅ Supported |❌ Not supported |❌ Not supported +|❌ Not supported |=== [NOTE] From 0a799fb34e6928777bab97f0771277a6133a7bd7 Mon Sep 17 00:00:00 2001 From: shahnami Date: Thu, 10 Jul 2025 20:55:19 +0400 Subject: [PATCH 07/11] chore: Use --example flag for fixtures --- Cargo.toml | 5 +++-- scripts/fixtures/generate_midnight_fixtures.sh | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 49392bde5..57a03e3bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,8 @@ name = "openzeppelin-relayer" version = "1.0.0" edition = "2021" -rust-version = "1.85" #MSRV +rust-version = "1.85" #MSRV +default-run = "openzeppelin-relayer" [profile.release] opt-level = 0 @@ -125,7 +126,7 @@ path = "src/main.rs" doc = true doctest = true -[[bin]] +[[example]] name = "generate_midnight_fixtures" path = "scripts/fixtures/generate_midnight_fixtures.rs" diff --git a/scripts/fixtures/generate_midnight_fixtures.sh b/scripts/fixtures/generate_midnight_fixtures.sh index 11e189518..ab6803cf1 100755 --- a/scripts/fixtures/generate_midnight_fixtures.sh +++ b/scripts/fixtures/generate_midnight_fixtures.sh @@ -32,7 +32,7 @@ echo # Generate complete context fixture echo "🏗️ Generating complete context fixture (wallet + ledger state)..." echo -cargo run --bin generate_midnight_fixtures +cargo run --example generate_midnight_fixtures echo echo "✅ Fixture generation complete!" From 97eb3a4462889967797cfd18d7bbdc2285ce8ee9 Mon Sep 17 00:00:00 2001 From: Nami Date: Wed, 20 Aug 2025 21:55:51 +0200 Subject: [PATCH 08/11] Update config/config.example.json Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- config/config.example.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.example.json b/config/config.example.json index d62f079b9..4baba5684 100644 --- a/config/config.example.json +++ b/config/config.example.json @@ -87,7 +87,7 @@ "signer_id": "local-signer", "network_type": "midnight", "policies": { - "min_balance": "0" + "min_balance": 0 } } ], From 6d20927fce364fdf54d6f0756ccb575dbc59fe50 Mon Sep 17 00:00:00 2001 From: shahnami Date: Thu, 21 Aug 2025 16:26:01 +0200 Subject: [PATCH 09/11] docs: Add docker example --- Dockerfile.midnight.development | 87 ++++ docs/modules/ROOT/nav.adoc | 1 + docs/modules/ROOT/pages/midnight.adoc | 416 ++++++++++++++++ examples/midnight-basic-example/.env.example | 12 + examples/midnight-basic-example/.gitignore | 5 + examples/midnight-basic-example/README.md | 449 ++++++++++++++++++ .../midnight-basic-example/config/config.json | 58 +++ .../docker-compose.yaml | 104 ++++ .../midnight-api-examples.md | 161 ------- src/config/config_file/network/midnight.rs | 5 +- .../relayer/midnight/midnight_relayer.rs | 29 +- src/services/signer/midnight/mod.rs | 7 +- 12 files changed, 1148 insertions(+), 186 deletions(-) create mode 100644 Dockerfile.midnight.development create mode 100644 docs/modules/ROOT/pages/midnight.adoc create mode 100644 examples/midnight-basic-example/.env.example create mode 100644 examples/midnight-basic-example/.gitignore create mode 100644 examples/midnight-basic-example/README.md create mode 100644 examples/midnight-basic-example/config/config.json create mode 100644 examples/midnight-basic-example/docker-compose.yaml delete mode 100644 examples/midnight-basic-example/midnight-api-examples.md diff --git a/Dockerfile.midnight.development b/Dockerfile.midnight.development new file mode 100644 index 000000000..c42adf8fd --- /dev/null +++ b/Dockerfile.midnight.development @@ -0,0 +1,87 @@ +# Base image +FROM --platform=${BUILDPLATFORM} cgr.dev/chainguard/rust:latest-dev@sha256:faf49718aaa95c798ed1dfdf3e4edee2cdbc3790c8994705ca6ef35972128459 AS base + +USER root +RUN apk update && apk --no-cache add \ + openssl-dev \ + perl \ + libsodium-dev \ + git \ + curl + +ENV PKG_CONFIG_PATH=/usr/lib/pkgconfig + +WORKDIR /usr/app + +# Configure git to use credentials for private repos +# TEMPORARY: GitHub credentials are only required until Midnight repositories are open-sourced +# Once the repos are public, this authentication step can be removed entirely +# Credentials are provided via Docker secrets for security (not exposed in logs) +RUN --mount=type=secret,id=github_user \ + --mount=type=secret,id=github_pat \ + if [ ! -f /run/secrets/github_user ] || [ ! -f /run/secrets/github_pat ]; then \ + echo "ERROR: GitHub credentials must be provided as Docker secrets" && \ + echo "Please ensure GITHUB_USER and GITHUB_PAT are set in your .env file" && \ + echo "Note: This is temporary until Midnight repositories are open-sourced" && \ + exit 1; \ + fi && \ + GITHUB_USER=$(cat /run/secrets/github_user) && \ + GITHUB_PAT=$(cat /run/secrets/github_pat) && \ + echo "Configuring git for user: ${GITHUB_USER}" && \ + echo "Note: GitHub authentication is temporary until Midnight repos are public" && \ + mkdir -p /root/.cargo && \ + echo '[net]' >> /root/.cargo/config.toml && \ + echo 'git-fetch-with-cli = true' >> /root/.cargo/config.toml && \ + git config --global url."https://${GITHUB_USER}:${GITHUB_PAT}@github.com/".insteadOf "https://github.com/" && \ + echo "Git config set. Testing access..." && \ + git ls-remote https://github.com/midnightntwrk/midnight-ledger-prototype > /dev/null 2>&1 && echo "✓ Git access successful" || echo "✗ Git access failed" + +# Copy source +COPY . . + +# Build +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/app/target \ + cargo build --release && \ + cargo install --root /usr/app --path . --debug + +# Setting up build directories +FROM --platform=${BUILDPLATFORM} cgr.dev/chainguard/wolfi-base + +WORKDIR /app + +COPY --from=base --chown=nonroot:nonroot /usr/app/bin/openzeppelin-relayer /app/openzeppelin-relayer + +# Install plugin dependencies +ARG TARGETARCH +ARG NODE_VERSION=20.19 + +# Install Node.js +USER root +RUN apk add --no-cache nodejs=~${NODE_VERSION} npm + +ENV PATH="/usr/local/bin:$PATH" + +# Install pnpm and ts-node +RUN npm install -g pnpm ts-node typescript + +# removes apk and unneeded wolfi-base tools. +RUN apk del wolfi-base apk-tools + +# Copy plugins folder and install dependencies +COPY --chown=nonroot:nonroot ./plugins /app/plugins + +USER nonroot +WORKDIR /app/plugins +RUN pnpm install --frozen-lockfile + +# Return to app root +WORKDIR /app + +ENV APP_PORT=8080 +ENV METRICS_PORT=8081 + +EXPOSE ${APP_PORT}/tcp ${METRICS_PORT}/tcp + +# starting up +ENTRYPOINT ["/app/openzeppelin-relayer"] diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index c5a865ebe..1d176a19f 100755 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -7,6 +7,7 @@ * xref:evm.adoc[EVM Integration] * xref:solana.adoc[Solana Integration] * xref:stellar.adoc[Stellar Integration] +* xref:midnight.adoc[Midnight Integration] * link:https://release-v1-1-0%2D%2Dopenzeppelin-relayer.netlify.app/api_docs.html[API Reference^] * xref:structure.adoc[Project Structure] * xref:roadmap.adoc[Project Roadmap] diff --git a/docs/modules/ROOT/pages/midnight.adoc b/docs/modules/ROOT/pages/midnight.adoc new file mode 100644 index 000000000..c981e840e --- /dev/null +++ b/docs/modules/ROOT/pages/midnight.adoc @@ -0,0 +1,416 @@ += Midnight Integration + +:description: Comprehensive guide for using OpenZeppelin Relayer with Midnight networks, including configuration, privacy-preserving features, API usage, and zero-knowledge proof support. + +== Overview + +OpenZeppelin Relayer provides comprehensive support for Midnight blockchain networks, enabling secure transaction relaying with privacy-preserving features through zero-knowledge proofs. + +NOTE: Midnight support is currently under active development. The API interactions and specifics described below may evolve. + +== Features + +- Privacy-preserving transaction processing using zero-knowledge proofs +- Support for guaranteed offers, intents, and fallible offers +- Integration with Midnight's indexer and prover infrastructure +- Transaction status monitoring and state synchronization +- Custom RPC endpoints and network policies + +== Supported Networks + +Midnight networks are defined via JSON configuration files, providing flexibility to: + +- Configure Midnight testnet environments +- Set up custom Midnight-compatible networks with specific RPC endpoints +- Define indexer URLs for transaction monitoring +- Configure prover URLs for zero-knowledge proof generation + +Example Midnight network configuration: + +[source,json] +---- +{ + "networks": [ + { + "type": "midnight", + "network": "testnet", + "rpc_urls": ["http://localhost:9944"], + "explorer_urls": ["https://explorer.midnight.network"], + "average_blocktime_ms": 5000, + "is_testnet": true, + "indexer_urls": { + "http": "https://indexer.midnight.network", + "ws": "wss://indexer.midnight.network" + }, + "prover_url": "https://prover.midnight.network", + "commitment_tree_ttl": 3600, + "tags": ["midnight", "testnet", "privacy"] + } + ] +} +---- + +=== Network-Specific Configuration + +Midnight networks require additional configuration beyond standard network settings: + +[cols="1,1,1,2"] +|=== +|Field |Type |Required |Description + +|`indexer_urls` +|object +|Yes +|URLs for the indexer service with `http` and `ws` endpoints for monitoring blockchain state + +|`prover_url` +|string +|Yes +|URL for the prover service that generates zero-knowledge proofs + +|`commitment_tree_ttl` +|integer +|No +|Time-to-live in seconds for caching Merkle roots (default: 3600) +|=== + +For detailed network configuration options, see the xref:network_configuration.adoc[Network Configuration] guide. + +== Supported Signers + +- `local` (local keystore files) +- `vault` (HashiCorp Vault secret storage) +- `turnkey` (hosted Turnkey signer) +- `google_cloud_kms` (Google Cloud KMS) +- `aws_kms` (Amazon AWS KMS) + +[NOTE] +==== +In production systems, hosted signers (AWS KMS, Google Cloud KMS, Turnkey) are recommended for the best security model. +==== + +== Quickstart + +For a step-by-step setup, see xref:quickstart.adoc[Quick Start Guide]. +Key prerequisites: + +- Rust 2021, version `1.86` or later +- Redis +- Docker (required for prover server) +- Access to Midnight testnet + +=== Starting the Prover Server + +Midnight requires a prover server for generating zero-knowledge proofs. Start it using Docker: + +[source,bash] +---- +docker run -p 6300:6300 midnightnetwork/proof-server -- 'midnight-proof-server --network testnet' +---- + +This will start the prover server on port 6300, which should match the `prover_url` in your network configuration. + +Example configuration for a Midnight relayer: +[source,json] +---- +{ + "id": "midnight-testnet-example", + "name": "Midnight Testnet Example", + "network": "testnet", + "paused": false, + "notification_id": "notification-example", + "signer_id": "local-signer", + "network_type": "midnight" +} +---- + +For more configuration examples, visit the link:https://github.com/OpenZeppelin/openzeppelin-relayer/tree/main/examples[OpenZeppelin Relayer examples repository, window=_blank]. + +== Configuration + +=== Relayer Policies + +Midnight relayers support standard relayer configuration options along with Midnight-specific policies: + +[cols="1,1,1,2"] +|=== +|Policy |Type |Default |Description + +|`min_balance` +|integer +|0 +|Minimum balance in speck (smallest unit) required for the relayer account to operate +|=== + +Example configuration with policies: +[source,json] +---- +{ + "id": "midnight-example", + "name": "Midnight Example", + "network": "testnet", + "paused": false, + "network_type": "midnight", + "signer_id": "local-signer", + "policies": { + "min_balance": 1000000000 + } +} +---- + +For general relayer configuration options, check xref:index.adoc#3_relayers[User Documentation - Relayers]. + +== API Reference + +The Midnight API provides transaction management capabilities with privacy features. + +Common endpoints: + +- `POST /api/v1/relayers//transactions` - Submit a transaction +- `GET /api/v1/relayers//transactions` - List transactions +- `GET /api/v1/relayers//transactions/` - Get transaction by ID +- `GET /api/v1/relayers//balance` - Get relayer balance + +=== Transaction Structure + +Midnight transactions consist of three main components and an optional TTL: + +[cols="1,2"] +|=== +|Component |Description + +|`guaranteed_offer` +|Transfers that must succeed atomically. Used for simple value transfers between addresses. + +|`intents` +|Complex contract interactions that may involve multiple steps or conditions with segment IDs. + +|`fallible_offers` +|Operations that may fail independently without affecting other parts of the transaction. Each has a segment ID. + +|`ttl` +|Optional time-to-live as ISO 8601 timestamp. If not provided, uses default expiration. +|=== + +=== Submit Transaction + +Example of a simple value transfer: + +[source,bash] +---- +curl --location --request POST 'http://localhost:8080/api/v1/relayers//transactions' \ +--header 'Authorization: Bearer ' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "guaranteed_offer": { + "inputs": [ + { + "origin": "", + "token_type": "02000000000000000000000000000000000000000000000000000000000000000000", + "value": "1000000" + } + ], + "outputs": [ + { + "destination": "", + "token_type": "02000000000000000000000000000000000000000000000000000000000000000000", + "value": "1000000" + } + ] + }, + "intents": [], + "fallible_offers": [], + "ttl": "2025-12-31T23:59:59Z" +}' +---- + +NOTE: The `ttl` field is optional. If provided, it should be an ISO 8601 formatted timestamp. + +=== Transaction Fields + +[cols="1,1,2"] +|=== +|Field |Type |Description + +|`origin` +|string +|Hex-encoded wallet seed of the sender (32 bytes) + +|`destination` +|string +|Hex-encoded wallet seed of the recipient (32 bytes) + +|`token_type` +|string +|Token identifier (tDUST = "02" followed by zeros for testnet) + +|`value` +|string +|Amount in smallest units (speck, where 1 tDUST = 10^9 speck) +|=== + +=== Get Transaction Status + +[source,bash] +---- +curl --location --request GET 'http://localhost:8080/api/v1/relayers//transactions/' \ +--header 'Authorization: Bearer ' +---- + +Response example: +[source,json] +---- +{ + "success": true, + "data": { + "id": "a6a468d1-3e87-48fe-9183-e44a9cb92be7", + "hash": null, + "pallet_hash": null, + "block_hash": null, + "status": "pending", + "created_at": "2025-08-20T19:11:23.868603+00:00", + "sent_at": null, + "confirmed_at": null + }, + "error": null +} +---- + +=== Transaction Status Values + +- `pending`: Transaction is queued for processing +- `confirmed`: Transaction has been confirmed on the network +- `failed`: Transaction failed to process + +=== Get Balance + +[source,bash] +---- +curl --location --request GET 'http://localhost:8080/api/v1/relayers//balance' \ +--header 'Authorization: Bearer ' +---- + +Response example: +[source,json] +---- +{ + "success": true, + "data": { + "balance": 1000000000, + "unit": "speck" + }, + "error": null +} +---- + +See link:https://release-v1-0-0%2D%2Dopenzeppelin-relayer.netlify.app/api_docs.html[API Reference^] for full details and examples. + +== Privacy Features + +=== Zero-Knowledge Proofs + +Midnight uses zero-knowledge proofs to enable privacy-preserving transactions: + +- **Private State**: User balances and transaction details remain confidential +- **Public Verifiability**: Network validators can verify transaction validity without seeing details +- **Selective Disclosure**: Users can choose what information to reveal + +=== Commitment Trees + +The relayer manages Merkle commitment trees for efficient proof generation: + +- Caches commitment roots based on `commitment_tree_ttl` configuration +- Automatically synchronizes with the blockchain state +- Optimizes proof generation performance + +== Transaction Lifecycle + +=== 1. Transaction Creation +- Construct transaction with guaranteed offers, intents, or fallible offers +- Specify input and output tokens with amounts +- Define privacy requirements + +=== 2. Proof Generation +- Transaction data is sent to the prover service +- Zero-knowledge proofs are generated for privacy preservation +- Proofs are attached to the transaction + +=== 3. Transaction Submission +- Signed transaction with proofs is submitted to the network +- Transaction enters the mempool for processing + +=== 4. Transaction Processing +- Network validators verify the zero-knowledge proofs +- Transaction is included in a block if valid +- State updates are applied while maintaining privacy + +=== 5. Confirmation +- Transaction is confirmed after block finalization +- Indexer service updates transaction status +- Notifications are sent if configured + +== Security Best Practices + +=== Network Security +- Use secure connections (HTTPS/WSS) for all network communication +- Configure appropriate `max_fee` to prevent excessive transaction costs +- Monitor relayer balance and set appropriate `min_balance` +- Regularly rotate wallet seeds in test environments + +=== Privacy Considerations +- Never expose wallet seeds in logs or error messages +- Use secure key management for production deployments +- Implement proper access controls for relayer endpoints +- Audit transaction patterns to prevent privacy leaks + +=== Operational Security +- Deploy behind a secure reverse proxy +- Use HTTPS for all API communications +- Implement proper rate limiting +- Monitor for unusual transaction patterns +- Keep prover and indexer URLs secure + +== Monitoring and Observability + +Enable metrics and monitor: + +- Transaction success rates +- Proof generation times +- Network synchronization status +- Relayer balance levels +- Failed transaction patterns +- Indexer connectivity +- Prover service availability + +== Troubleshooting + +Common issues and solutions: + +=== Connection Issues +- Verify indexer URLs (both HTTP and WebSocket) are accessible +- Check prover service availability +- Ensure network RPC endpoints are responsive + +=== Transaction Failures +- Verify wallet seeds are correctly formatted (32-byte hex strings) +- Check token types match network configuration +- Ensure sufficient balance for transaction and fees +- Review commitment tree synchronization status + +=== Performance Issues +- Adjust `commitment_tree_ttl` for optimal caching +- Monitor prover service response times +- Check network latency to RPC endpoints + +== Support + +For help with Midnight integration: + +- Join our link:https://t.me/openzeppelin_tg/2[Telegram] community +- Open an issue on our link:https://github.com/OpenZeppelin/openzeppelin-relayer[GitHub repository] +- Check our link:https://docs.openzeppelin.com/relayer[comprehensive documentation] +- Visit the link:https://midnight.network[Midnight Network documentation] for protocol-specific details + +== License + +This project is licensed under the GNU Affero General Public License v3.0. diff --git a/examples/midnight-basic-example/.env.example b/examples/midnight-basic-example/.env.example new file mode 100644 index 000000000..0918f8f29 --- /dev/null +++ b/examples/midnight-basic-example/.env.example @@ -0,0 +1,12 @@ +# Required configuration +API_KEY= +WEBHOOK_SIGNING_KEY= +KEYSTORE_PASSPHRASE= + +# ============================================================================ +# TEMPORARY: GitHub credentials for accessing private Midnight repositories +# These are only required until Midnight repositories are open-sourced +# Once the repos become public, these credentials can be removed entirely +# ============================================================================ +GITHUB_USER= +GITHUB_PAT= diff --git a/examples/midnight-basic-example/.gitignore b/examples/midnight-basic-example/.gitignore new file mode 100644 index 000000000..152fc25a6 --- /dev/null +++ b/examples/midnight-basic-example/.gitignore @@ -0,0 +1,5 @@ +# Environment file with secrets +.env + +# Keys directory +config/keys/ diff --git a/examples/midnight-basic-example/README.md b/examples/midnight-basic-example/README.md new file mode 100644 index 000000000..239b2e967 --- /dev/null +++ b/examples/midnight-basic-example/README.md @@ -0,0 +1,449 @@ +# OpenZeppelin Relayer Midnight Example + +This comprehensive guide demonstrates how to configure and use the OpenZeppelin Relayer service with Midnight blockchain, a privacy-preserving network that uses zero-knowledge proofs to protect transaction data. + +## Table of Contents + +- [Overview](#overview) +- [Prerequisites](#prerequisites) +- [Getting Started](#getting-started) + - [Step 1: Clone the Repository](#step-1-clone-the-repository) + - [Step 2: Fetch Midnight Dependencies](#step-2-fetch-midnight-dependencies-temporary) + - [Step 3: Create a Signer](#step-3-create-a-signer) + - [Step 4: Configure Environment](#step-4-configure-environment) + - [Step 5: Configure Notifications](#step-5-configure-notifications) + - [Step 6: Configure API Key](#step-6-configure-api-key) + - [Step 7: Run the Service](#step-7-run-the-service) + - [Step 8: Verify the Setup](#step-8-verify-the-setup) +- [Testing the Relayer](#testing-the-relayer) +- [API Examples](#api-examples) +- [Architecture](#architecture) +- [Troubleshooting](#troubleshooting) +- [Advanced Configuration](#advanced-configuration) + +## Overview + +Midnight is a data protection blockchain that enables developers to build applications that safeguard personal and commercial data while ensuring regulatory compliance. This example demonstrates: + +- Setting up a Midnight testnet relayer +- Configuring the prover service for zero-knowledge proof generation +- Submitting privacy-preserving transactions +- Monitoring transaction status + +### Key Features + +- **Privacy-Preserving Transactions**: Use zero-knowledge proofs to keep transaction details private +- **Prover Service Integration**: Automatic proof generation for transactions +- **Docker-based Setup**: Easy deployment with Docker Compose +- **Complete API Examples**: Ready-to-use curl commands for testing + +## Prerequisites + +Before you begin, ensure you have the following installed: + +- [Docker](https://docs.docker.com/get-docker/) (version 20.10 or later) +- [Docker Compose](https://docs.docker.com/compose/install/) (version 2.0 or later) +- [Rust](https://www.rust-lang.org/tools/install) (version 1.86 or later, for key generation tools) +- [jq](https://stedolan.github.io/jq/) (optional, for JSON formatting) +- Access to Midnight testnet (wallet seeds for testing) +- **[TEMPORARY]** GitHub credentials with access to private Midnight repositories + +> **⚠️ TEMPORARY REQUIREMENT - WILL BE REMOVED** +> +> Currently, the Midnight blockchain repositories are private. To build the relayer, you need: +> - A GitHub username with access to the Midnight repositories +> - A GitHub Personal Access Token (PAT) or password +> +> **This requirement is temporary.** Once the Midnight repositories are open-sourced, all GitHub authentication code will be removed from this example. + +## Getting Started + +### Step 1: Clone the Repository + +Clone the OpenZeppelin Relayer repository and navigate to the project directory: + +```bash +git clone https://github.com/OpenZeppelin/openzeppelin-relayer +cd openzeppelin-relayer +``` + +### Step 2: Create a Signer + +The relayer needs a signer to authorize transactions. Create a new signer keystore using the provided key generation tool: + +```bash +cargo run --example create_key -- \ + --password \ + --output-dir examples/midnight-basic-example/config/keys \ + --filename local-signer.json +``` + +**Important Notes:** + +- Replace `` with a strong password (minimum 12 characters recommended) +- Keep this password secure - you'll need it in the next step +- The generated keystore file contains your private key encrypted with this password + +### Step 4: Configure Environment + +Create the environment configuration file from the template: + +```bash +cp examples/midnight-basic-example/.env.example examples/midnight-basic-example/.env +``` + +Edit the `.env` file and populate the following values: + +```bash +# The password you used in Step 3 +KEYSTORE_PASSPHRASE=your_keystore_password_here + +# Will be generated in Step 5 +WEBHOOK_SIGNING_KEY= + +# Will be generated in Step 6 +API_KEY= +``` + +### Step 5: Configure Notifications + +The relayer can send webhook notifications for transaction status updates. + +#### Option A: Using Webhook.site (Development) + +For quick testing: + +1. Visit [Webhook.site](https://webhook.site) +2. Copy your unique URL (e.g., `https://webhook.site/your-unique-id`) +3. Edit `examples/midnight-basic-example/config/config.json` +4. Update the `notifications[0].url` field with your webhook URL + +#### Option B: Using Your Own Webhook Endpoint + +If you have your own webhook endpoint, update the URL accordingly. + +#### Generate Webhook Signing Key + +Generate a signing key to secure webhook payloads: + +```bash +cargo run --example generate_uuid +``` + +Copy the generated UUID and update the `WEBHOOK_SIGNING_KEY` in your `.env` file. + +### Step 6: Configure API Key + +Generate an API key for authenticating requests to the relayer: + +```bash +cargo run --example generate_uuid +``` + +Copy the generated UUID and update the `API_KEY` in your `.env` file. + +Your `.env` file should now look similar to: + +```bash +API_KEY=a1b2c3d4-e5f6-7890-abcd-ef1234567890 +WEBHOOK_SIGNING_KEY=f9e8d7c6-b5a4-3210-fedc-ba0987654321 +KEYSTORE_PASSPHRASE=YourSecurePassword123! +``` + +### Step 7: Run the Service + +Start all services with Docker Compose: + +```bash +docker compose -f examples/midnight-basic-example/docker-compose.yaml up +``` + +This command starts: + +- **Midnight Prover Server**: Generates zero-knowledge proofs (port 6300) +- **OpenZeppelin Relayer**: Manages transactions (port 8080) +- **Redis**: Stores relayer state (port 6379) + +**Note**: The `MIDNIGHT_LEDGER_TEST_STATIC_DIR` environment variable is automatically set to `/tmp/midnight-test-static` as required by the Midnight test environment. + +Wait for the services to fully initialize. You should see logs indicating: + +- Prover server is running on port 6300 +- Relayer is listening on port 8080 +- Redis is ready to accept connections + +### Step 8: Verify the Setup + +Check that all services are running correctly: + +```bash +# Check relayer health +curl -X GET http://localhost:8080/health + +# List configured relayers +curl -X GET http://localhost:8080/api/v1/relayers \ + -H "Content-Type: application/json" \ + -H "AUTHORIZATION: Bearer YOUR_API_KEY" | jq +``` + +Expected response: + +```json +{ + "success": true, + "data": [ + { + "id": "midnight-testnet-example", + "name": "Midnight Testnet Example", + "network": "testnet", + "status": "active", + ... + } + ] +} +``` + +## Testing the Relayer + +### 1. Get Relayer Balance + +Check the balance of your relayer account: + +```bash +curl -X GET http://localhost:8080/api/v1/relayers/midnight-testnet-example/balance \ + -H "AUTHORIZATION: Bearer YOUR_API_KEY" | jq +``` + +### 2. Submit a Test Transaction + +Submit a simple value transfer transaction: + +```bash +curl -X POST http://localhost:8080/api/v1/relayers/midnight-testnet-example/transactions \ + -H "Content-Type: application/json" \ + -H "AUTHORIZATION: Bearer YOUR_API_KEY" \ + -d '{ + "guaranteed_offer": { + "inputs": [ + { + "origin": "{RELAYER_WALLET_SEED}", + "token_type": "02000000000000000000000000000000000000000000000000000000000000000000", + "value": "1000000" + } + ], + "outputs": [ + { + "destination": "{DESTINATION_WALLET_SEED}", + "token_type": "02000000000000000000000000000000000000000000000000000000000000000000", + "value": "1000000" + } + ] + }, + "intents": [], + "fallible_offers": [], + "ttl": "2025-12-31T23:59:59Z" + }' | jq +``` + +### 3. Check Transaction Status + +Replace `{transaction_id}` with the ID from the submission response: + +```bash +curl -X GET http://localhost:8080/api/v1/relayers/midnight-testnet-example/transactions/{transaction_id} \ + -H "AUTHORIZATION: Bearer YOUR_API_KEY" | jq +``` + +### 4. List Recent Transactions + +```bash +curl -X GET "http://localhost:8080/api/v1/relayers/midnight-testnet-example/transactions?per_page=5" \ + -H "AUTHORIZATION: Bearer YOUR_API_KEY" | jq +``` + +## API Examples + +For more detailed API examples including: + +- Complex transactions with intents +- Fallible offers +- Transaction filtering +- Pagination + +See [midnight-api-examples.md](midnight-api-examples.md) + +## Architecture + +The example setup consists of three main components: + +``` +┌─────────────────────────────────────────────────────────┐ +│ Docker Network │ +├─────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Prover │ │ Relayer │ │ Redis │ │ +│ │ Server │◄─┤ Service ├─►│ Storage │ │ +│ │ Port: 6300 │ │ Port: 8080 │ │ Port: 6379 │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ ▲ │ │ +│ │ ▼ │ +│ ┌──────────────────────────────────────┐ │ +│ │ Midnight Testnet Network │ │ +│ └──────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +### Component Responsibilities + +1. **Prover Server**: Generates zero-knowledge proofs for transactions +2. **Relayer Service**: Manages transaction lifecycle and API +3. **Redis**: Stores transaction state and relayer configuration + +## Troubleshooting + +### Common Issues and Solutions + +#### 1. Prover Server Connection Failed + +**Error**: "Failed to connect to prover service" + +**Solution**: + +```bash +# Check if prover is running +docker compose -f examples/midnight-basic-example/docker-compose.yaml ps + +# Check prover logs +docker compose -f examples/midnight-basic-example/docker-compose.yaml logs prover + +# Restart prover service +docker compose -f examples/midnight-basic-example/docker-compose.yaml restart prover +``` + +#### 2. Invalid Wallet Seed + +**Error**: "Invalid origin/destination format" + +**Solution**: + +- Ensure wallet seeds are 32-byte hex-encoded strings (64 characters) +- Check that there are no spaces or special characters + +#### 3. Insufficient Balance + +**Error**: "Insufficient balance for transaction" + +**Solution**: + +- Fund your relayer account with testnet tokens +- Check balance using the balance endpoint +- Ensure `min_balance` policy is configured appropriately + +#### 4. Transaction Timeout + +**Error**: "Transaction expired" + +**Solution**: + +- Increase the TTL value in your transaction request +- Check network connectivity +- Verify the indexer service is accessible + +### Checking Logs + +View logs for all services: + +```bash +docker compose -f examples/midnight-basic-example/docker-compose.yaml logs -f +``` + +View logs for specific service: + +```bash +docker compose -f examples/midnight-basic-example/docker-compose.yaml logs -f relayer +``` + +## Advanced Configuration + +### Custom Network Configuration + +To connect to a different Midnight network, edit the `networks` section in `config/config.json`: + +```json +{ + "networks": [ + { + "type": "midnight", + "network": "your-network-name", + "rpc_urls": ["http://your-rpc-url:9944"], + "indexer_urls": { + "http": "https://your-indexer-url", + "ws": "wss://your-indexer-ws-url" + }, + "prover_url": "http://prover:6300", + "commitment_tree_ttl": 3600 + } + ] +} +``` + +### Relayer Policies + +Configure relayer behavior by editing the `policies` section: + +```json +{ + "policies": { + "min_balance": 1000000000 // Minimum balance in speck + } +} +``` + +### Performance Tuning + +Optimize performance by adjusting: + +1. **Commitment Tree TTL**: Cache duration for Merkle roots +2. **Rate Limiting**: Adjust `RATE_LIMIT_REQUESTS_PER_SECOND` in docker-compose.yaml +3. **Redis Persistence**: Configure save intervals in docker-compose.yaml + +## Security Considerations + +1. **Production Deployment**: + + - Use hosted signers (AWS KMS, Google Cloud KMS) instead of local keystores + - Deploy behind a secure reverse proxy + - Enable TLS/HTTPS for all communications + - Implement proper access controls + +2. **Key Management**: + + - Never commit `.env` files or keystores to version control + - Rotate API keys and signing keys regularly + - Use strong passwords for keystores + +3. **Network Security**: + - Restrict network access to necessary ports only + - Use private networks for inter-service communication + - Monitor for unusual transaction patterns + +## Next Steps + +- Explore the [Midnight documentation](https://midnight.network/docs) +- Learn about [zero-knowledge proofs](https://docs.midnight.network/develop/tutorial/introduction/) +- Join the [OpenZeppelin Telegram community](https://t.me/openzeppelin_tg/2) +- Review the [API documentation](https://release-v1-0-0--openzeppelin-relayer.netlify.app/api_docs.html) + +## Support + +If you encounter any issues: + +1. Check the [troubleshooting section](#troubleshooting) +2. Review logs for error messages +3. Open an issue on [GitHub](https://github.com/OpenZeppelin/openzeppelin-relayer/issues) +4. Join our [Telegram community](https://t.me/openzeppelin_tg/2) for help + +## License + +This project is licensed under the GNU Affero General Public License v3.0. diff --git a/examples/midnight-basic-example/config/config.json b/examples/midnight-basic-example/config/config.json new file mode 100644 index 000000000..7d4c23a86 --- /dev/null +++ b/examples/midnight-basic-example/config/config.json @@ -0,0 +1,58 @@ +{ + "relayers": [ + { + "id": "midnight-testnet-example", + "name": "Midnight Testnet Example", + "network": "testnet", + "paused": false, + "notification_id": "notification-example", + "signer_id": "local-signer", + "network_type": "midnight", + "policies": { + "min_balance": 1000000000 + } + } + ], + "notifications": [ + { + "id": "notification-example", + "type": "webhook", + "url": "https://webhook.site/86c98e1a-38c1-4f31-a33c-858090c7d0ad", + "signing_key": { + "type": "env", + "value": "WEBHOOK_SIGNING_KEY" + } + } + ], + "signers": [ + { + "id": "local-signer", + "type": "local", + "config": { + "path": "config/keys/local-signer.json", + "passphrase": { + "type": "env", + "value": "KEYSTORE_PASSPHRASE" + } + } + } + ], + "networks": [ + { + "type": "midnight", + "network": "testnet", + "rpc_urls": ["wss://rpc.testnet-02.midnight.network/"], + "explorer_urls": [ + "https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Frpc.testnet-02.midnight.network#/explorer" + ], + "indexer_urls": { + "ws": "wss://indexer.testnet-02.midnight.network/api/v1/graphql/ws", + "http": "https://indexer.testnet-02.midnight.network/api/v1/graphql" + }, + "prover_url": "http://localhost:6300", + "average_blocktime_ms": 6000, + "is_testnet": true + } + ], + "plugins": [] +} diff --git a/examples/midnight-basic-example/docker-compose.yaml b/examples/midnight-basic-example/docker-compose.yaml new file mode 100644 index 000000000..555bc0506 --- /dev/null +++ b/examples/midnight-basic-example/docker-compose.yaml @@ -0,0 +1,104 @@ +--- +services: + # Midnight Prover Server + prover: + image: midnightnetwork/proof-server:latest + ports: + - 6300:6300/tcp + command: midnight-proof-server --network testnet + environment: + MIDNIGHT_LEDGER_TEST_STATIC_DIR: /tmp/midnight-test-static + security_opt: + - no-new-privileges + networks: + - relayer-network + restart: on-failure:5 + healthcheck: + test: + - CMD + - curl + - -f + - http://localhost:6300/health + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # OpenZeppelin Relayer + relayer: + build: + context: ../../ + dockerfile: Dockerfile.midnight.development + # TEMPORARY: GitHub secrets are only required until Midnight repositories are open-sourced + # Once the repos are public, these secrets can be removed + secrets: + - github_user + - github_pat + ports: + - 8080:8080/tcp + secrets: + - api_key + - webhook_signing_key + - keystore_passphrase + environment: + REDIS_URL: redis://redis:6379 + RATE_LIMIT_REQUESTS_PER_SECOND: 10 + RATE_LIMIT_BURST: 50 + WEBHOOK_SIGNING_KEY: ${WEBHOOK_SIGNING_KEY} + API_KEY: ${API_KEY} + KEYSTORE_PASSPHRASE: ${KEYSTORE_PASSPHRASE} + MIDNIGHT_LEDGER_TEST_STATIC_DIR: /tmp/midnight-test-static + security_opt: + - no-new-privileges + networks: + - relayer-network + - metrics-network + volumes: + - ./config:/app/config/ + - ../../config/networks:/app/config/networks + depends_on: + - redis + - prover + restart: on-failure:5 + + # Redis for state management + redis: + image: redis:bookworm + ports: + - 6379:6379/tcp + security_opt: + - no-new-privileges + volumes: + - redis_data:/data + command: + - redis-server + - --appendonly + - 'yes' + - --save + - '60' + - '1' + networks: + - relayer-network + - metrics-network + restart: on-failure:5 +networks: + metrics-network: + internal: true + relayer-network: + driver: bridge +volumes: + redis_data: + driver: local +secrets: + api_key: + environment: API_KEY + webhook_signing_key: + environment: WEBHOOK_SIGNING_KEY + keystore_passphrase: + environment: KEYSTORE_PASSPHRASE + # TEMPORARY: GitHub credentials are only required until Midnight repositories are open-sourced + # Once the repos are public, github_user and github_pat can be removed from secrets + github_user: + environment: GITHUB_USER + github_pat: + environment: GITHUB_PAT diff --git a/examples/midnight-basic-example/midnight-api-examples.md b/examples/midnight-basic-example/midnight-api-examples.md deleted file mode 100644 index 94380a5f3..000000000 --- a/examples/midnight-basic-example/midnight-api-examples.md +++ /dev/null @@ -1,161 +0,0 @@ -# Midnight Relayer CURL Examples - -## Authentication - -All requests require the Authorization header with a Bearer token: - -```bash --H "AUTHORIZATION: Bearer b5981311-f311-495c-a124-44086c219bce" -``` - -## 1. List All Relayers - -```bash -curl -X GET http://localhost:8080/api/v1/relayers \ - -H "Content-Type: application/json" \ - -H "AUTHORIZATION: Bearer b5981311-f311-495c-a124-44086c219bce" | jq -``` - -## 2. Get Specific Midnight Relayer - -```bash -curl -X GET http://localhost:8080/api/v1/relayers/midnight-testnet-example \ - -H "AUTHORIZATION: Bearer b5981311-f311-495c-a124-44086c219bce" | jq -``` - -## 3. Get Relayer Balance - -```bash -curl -X GET http://localhost:8080/api/v1/relayers/midnight-testnet-example/balance \ - -H "AUTHORIZATION: Bearer b5981311-f311-495c-a124-44086c219bce" | jq -``` - -## 4. Submit a Midnight Transaction - -```bash -curl -X POST http://localhost:8080/api/v1/relayers/midnight-testnet-example/transactions \ - -H "Content-Type: application/json" \ - -H "AUTHORIZATION: Bearer b5981311-f311-495c-a124-44086c219bce" \ - -d '{ - "guaranteed_offer": { - "inputs": [ - { - "origin": "78ad774b434f1c560eca9d2b6fbf02f16f5e21c1d3d7f79a93a30b686f30c188", - "token_type": "02000000000000000000000000000000000000000000000000000000000000000000", - "value": "1000000" - } - ], - "outputs": [ - { - "destination": "8e0622a9987a7bef7b6a1417c693172b79e75f2308fe3ae9cc897f6108e3a067", - "token_type": "02000000000000000000000000000000000000000000000000000000000000000000", - "value": "1000000" - } - ] - }, - "intents": [], - "fallible_offers": [] - }' | jq -``` - -### Transaction Fields Explanation: - -- **origin**: Hex-encoded wallet seed of the sender (32 bytes) -- **destination**: Hex-encoded wallet seed of the recipient (32 bytes) -- **token_type**: Token identifier (tDUST = "02" followed by zeros) -- **value**: Amount in smallest units (speck) -- **guaranteed_offer**: Used for simple transfers that must succeed -- **intents**: For complex contract interactions (empty array for simple transfers) -- **fallible_offers**: For operations that may fail independently (empty array for simple transfers) - -## 5. Check Transaction Status - -```bash -# Replace {transaction_id} with the actual transaction ID from the submission response -curl -X GET http://localhost:8080/api/v1/relayers/midnight-testnet-example/transactions/{transaction_id} \ - -H "AUTHORIZATION: Bearer b5981311-f311-495c-a124-44086c219bce" | jq -``` - -Example: - -```bash -curl -X GET http://localhost:8080/api/v1/relayers/midnight-testnet-example/transactions/a6a468d1-3e87-48fe-9183-e44a9cb92be7 \ - -H "AUTHORIZATION: Bearer b5981311-f311-495c-a124-44086c219bce" | jq -``` - -## 6. List Recent Transactions - -```bash -curl -X GET "http://localhost:8080/api/v1/relayers/midnight-testnet-example/transactions?per_page=5" \ - -H "AUTHORIZATION: Bearer b5981311-f311-495c-a124-44086c219bce" | jq -``` - -## 7. List Transactions with Status Filter - -```bash -# Show only transaction IDs and statuses -curl -X GET "http://localhost:8080/api/v1/relayers/midnight-testnet-example/transactions?per_page=5" \ - -H "AUTHORIZATION: Bearer b5981311-f311-495c-a124-44086c219bce" | jq '.data[] | {id: .id, status: .status, created_at: .created_at}' -``` - -## 8. Get Signer Information - -```bash -curl -X GET http://localhost:8080/api/v1/signers/local-signer \ - -H "AUTHORIZATION: Bearer b5981311-f311-495c-a124-44086c219bce" | jq -``` - -## Response Examples - -### Successful Transaction Submission Response: - -```json -{ - "success": true, - "data": { - "id": "a6a468d1-3e87-48fe-9183-e44a9cb92be7", - "hash": null, - "pallet_hash": null, - "block_hash": null, - "status": "pending", - "created_at": "2025-08-20T19:11:23.868603+00:00", - "sent_at": null, - "confirmed_at": null - }, - "error": null -} -``` - -### Transaction Status Values: - -- `pending`: Transaction is queued for processing -- `confirmed`: Transaction has been confirmed on the network -- `failed`: Transaction failed to process - -### Balance Response: - -```json -{ - "success": true, - "data": { - "balance": 1000000000, - "unit": "speck" - }, - "error": null -} -``` - -## Test Wallet Seeds - -For testing purposes, these wallet seeds were used: - -- **Relayer wallet seed**: `78ad774b434f1c560eca9d2b6fbf02f16f5e21c1d3d7f79a93a30b686f30c188` -- **Destination wallet seed**: `8e0622a9987a7bef7b6a1417c693172b79e75f2308fe3ae9cc897f6108e3a067` - -## Notes - -1. The endpoint path is `/api/v1/relayers/{relayer_id}/transactions`, not `/api/v1/transactions` -2. The transaction data should be placed directly in the request body (not wrapped in a "data" field) -3. All wallet seeds must be 32-byte hex-encoded strings (64 characters) -4. The token type for tDUST is always `"02000000000000000000000000000000000000000000000000000000000000000000"` -5. Transaction values are in the smallest unit (speck), where 1 tDUST = 10^9 speck diff --git a/src/config/config_file/network/midnight.rs b/src/config/config_file/network/midnight.rs index 000978321..5d816b4d7 100644 --- a/src/config/config_file/network/midnight.rs +++ b/src/config/config_file/network/midnight.rs @@ -55,11 +55,12 @@ impl MidnightNetworkConfig { })?; // Validate network_id if provided + // TODO: Add mainnet and other supported networks once announced match self.common.network.as_str() { - "mainnet" | "testnet" | "devnet" => {} + "testnet" => {} _ => { return Err(ConfigFileError::InvalidFormat(format!( - "Invalid network_id: {}. Must be one of: mainnet, testnet, devnet", + "Invalid network_id: {}. Must be one of: testnet", self.common.network ))) } diff --git a/src/domain/relayer/midnight/midnight_relayer.rs b/src/domain/relayer/midnight/midnight_relayer.rs index 2aaf3b905..32137ae38 100644 --- a/src/domain/relayer/midnight/midnight_relayer.rs +++ b/src/domain/relayer/midnight/midnight_relayer.rs @@ -28,10 +28,10 @@ use crate::{ }, jobs::{JobProducerTrait, TransactionRequest}, models::{ - produce_relayer_disabled_payload, DeletePendingTransactionsResponse, JsonRpcId, - JsonRpcRequest, JsonRpcResponse, MidnightNetwork, MidnightRpcResult, NetworkRpcRequest, - NetworkRpcResult, NetworkTransactionRequest, NetworkType, RelayerRepoModel, RelayerStatus, - RepositoryError, TransactionRepoModel, TransactionStatus, + produce_relayer_disabled_payload, DeletePendingTransactionsResponse, JsonRpcRequest, + JsonRpcResponse, MidnightNetwork, NetworkRpcRequest, NetworkRpcResult, + NetworkTransactionRequest, NetworkType, RelayerRepoModel, RelayerStatus, RepositoryError, + TransactionRepoModel, TransactionStatus, }, repositories::{ NetworkRepository, RelayerRepository, RelayerStateRepositoryStorage, Repository, @@ -376,12 +376,9 @@ where async fn delete_pending_transactions( &self, ) -> Result { - println!("Midnight delete_pending_transactions..."); - Ok(DeletePendingTransactionsResponse { - queued_for_cancellation_transaction_ids: vec![], - failed_to_queue_transaction_ids: vec![], - total_processed: 0, - }) + Err(RelayerError::NotSupported( + "Deleting transactions is not supported for Midnight".to_string(), + )) } async fn sign_data(&self, _request: SignDataRequest) -> Result { @@ -403,15 +400,9 @@ where &self, _request: JsonRpcRequest, ) -> Result, RelayerError> { - println!("Midnight rpc..."); - Ok(JsonRpcResponse { - id: Some(JsonRpcId::Number(1)), - jsonrpc: "2.0".to_string(), - result: Some(NetworkRpcResult::Midnight( - MidnightRpcResult::GenericRpcResult("".to_string()), - )), - error: None, - }) + Err(RelayerError::NotSupported( + "RPC is not supported for Midnight".to_string(), + )) } async fn validate_min_balance(&self) -> Result<(), RelayerError> { diff --git a/src/services/signer/midnight/mod.rs b/src/services/signer/midnight/mod.rs index af44b8c07..21bc206b8 100644 --- a/src/services/signer/midnight/mod.rs +++ b/src/services/signer/midnight/mod.rs @@ -26,14 +26,13 @@ pub trait MidnightSignerTrait: Signer { pub enum MidnightSigner { Local(LocalSigner), Vault(LocalSigner), - VaultCloud(LocalSigner), } #[async_trait] impl Signer for MidnightSigner { async fn address(&self) -> Result { match self { - Self::Local(s) | Self::Vault(s) | Self::VaultCloud(s) => s.address().await, + Self::Local(s) | Self::Vault(s) => s.address().await, } } @@ -42,7 +41,7 @@ impl Signer for MidnightSigner { tx: NetworkTransactionData, ) -> Result { match self { - Self::Local(s) | Self::Vault(s) | Self::VaultCloud(s) => s.sign_transaction(tx).await, + Self::Local(s) | Self::Vault(s) => s.sign_transaction(tx).await, } } } @@ -50,7 +49,7 @@ impl Signer for MidnightSigner { impl MidnightSignerTrait for MidnightSigner { fn wallet_seed(&self) -> &midnight_node_ledger_helpers::WalletSeed { match self { - Self::Local(s) | Self::Vault(s) | Self::VaultCloud(s) => s.wallet_seed(), + Self::Local(s) | Self::Vault(s) => s.wallet_seed(), } } } From adc0fbf690085fda0f0769fba00be977973c9beb Mon Sep 17 00:00:00 2001 From: shahnami Date: Thu, 21 Aug 2025 16:32:23 +0200 Subject: [PATCH 10/11] fix: Add status reason --- src/models/transaction/response.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/models/transaction/response.rs b/src/models/transaction/response.rs index fa795328f..26be59ddb 100644 --- a/src/models/transaction/response.rs +++ b/src/models/transaction/response.rs @@ -110,6 +110,7 @@ pub struct MidnightTransactionResponse { #[schema(nullable = false)] pub block_hash: Option, pub status: TransactionStatus, + pub status_reason: Option, pub created_at: String, #[schema(nullable = false)] pub sent_at: Option, @@ -176,6 +177,7 @@ impl From for TransactionResponse { pallet_hash: midnight_data.pallet_hash, block_hash: midnight_data.block_hash, status: model.status, + status_reason: model.status_reason, created_at: model.created_at, sent_at: model.sent_at, confirmed_at: model.confirmed_at, From e91a8f33171454b5c0f88f85053ec7bba2be465a Mon Sep 17 00:00:00 2001 From: shahnami Date: Thu, 21 Aug 2025 16:51:47 +0200 Subject: [PATCH 11/11] chore: Apply review suggestions --- Dockerfile.midnight.development | 14 +++++-- docs/modules/ROOT/pages/midnight.adoc | 1 - examples/midnight-basic-example/README.md | 42 +++++++------------ .../midnight-basic-example/config/config.json | 2 +- 4 files changed, 26 insertions(+), 33 deletions(-) diff --git a/Dockerfile.midnight.development b/Dockerfile.midnight.development index c42adf8fd..c112f1c9f 100644 --- a/Dockerfile.midnight.development +++ b/Dockerfile.midnight.development @@ -17,8 +17,8 @@ WORKDIR /usr/app # TEMPORARY: GitHub credentials are only required until Midnight repositories are open-sourced # Once the repos are public, this authentication step can be removed entirely # Credentials are provided via Docker secrets for security (not exposed in logs) -RUN --mount=type=secret,id=github_user \ - --mount=type=secret,id=github_pat \ +RUN --mount=type=secret,id=github_user,mode=0400 \ + --mount=type=secret,id=github_pat,mode=0400 \ if [ ! -f /run/secrets/github_user ] || [ ! -f /run/secrets/github_pat ]; then \ echo "ERROR: GitHub credentials must be provided as Docker secrets" && \ echo "Please ensure GITHUB_USER and GITHUB_PAT are set in your .env file" && \ @@ -41,9 +41,15 @@ COPY . . # Build RUN --mount=type=cache,target=/usr/local/cargo/registry \ - --mount=type=cache,target=/app/target \ + --mount=type=cache,target=/usr/app/target \ + --mount=type=secret,id=github_user,mode=0400 \ + --mount=type=secret,id=github_pat,mode=0400 \ + GITHUB_USER=$(cat /run/secrets/github_user) && \ + GITHUB_PAT=$(cat /run/secrets/github_pat) && \ cargo build --release && \ - cargo install --root /usr/app --path . --debug + cargo install --root /usr/app --path . --debug && \ + git config --global --unset-all url."https://${GITHUB_USER}:${GITHUB_PAT}@github.com/".insteadOf && \ + echo "Git credentials cleared" # Setting up build directories FROM --platform=${BUILDPLATFORM} cgr.dev/chainguard/wolfi-base diff --git a/docs/modules/ROOT/pages/midnight.adoc b/docs/modules/ROOT/pages/midnight.adoc index c981e840e..48b8cce43 100644 --- a/docs/modules/ROOT/pages/midnight.adoc +++ b/docs/modules/ROOT/pages/midnight.adoc @@ -353,7 +353,6 @@ The relayer manages Merkle commitment trees for efficient proof generation: === Network Security - Use secure connections (HTTPS/WSS) for all network communication -- Configure appropriate `max_fee` to prevent excessive transaction costs - Monitor relayer balance and set appropriate `min_balance` - Regularly rotate wallet seeds in test environments diff --git a/examples/midnight-basic-example/README.md b/examples/midnight-basic-example/README.md index 239b2e967..e2101f181 100644 --- a/examples/midnight-basic-example/README.md +++ b/examples/midnight-basic-example/README.md @@ -8,15 +8,13 @@ This comprehensive guide demonstrates how to configure and use the OpenZeppelin - [Prerequisites](#prerequisites) - [Getting Started](#getting-started) - [Step 1: Clone the Repository](#step-1-clone-the-repository) - - [Step 2: Fetch Midnight Dependencies](#step-2-fetch-midnight-dependencies-temporary) - - [Step 3: Create a Signer](#step-3-create-a-signer) - - [Step 4: Configure Environment](#step-4-configure-environment) - - [Step 5: Configure Notifications](#step-5-configure-notifications) - - [Step 6: Configure API Key](#step-6-configure-api-key) - - [Step 7: Run the Service](#step-7-run-the-service) - - [Step 8: Verify the Setup](#step-8-verify-the-setup) + - [Step 2: Create a Signer](#step-2-create-a-signer) + - [Step 3: Configure Environment](#step-3-configure-environment) + - [Step 4: Configure Notifications](#step-4-configure-notifications) + - [Step 5: Configure API Key](#step-5-configure-api-key) + - [Step 6: Run the Service](#step-6-run-the-service) + - [Step 7: Verify the Setup](#step-7-verify-the-setup) - [Testing the Relayer](#testing-the-relayer) -- [API Examples](#api-examples) - [Architecture](#architecture) - [Troubleshooting](#troubleshooting) - [Advanced Configuration](#advanced-configuration) @@ -51,6 +49,7 @@ Before you begin, ensure you have the following installed: > **⚠️ TEMPORARY REQUIREMENT - WILL BE REMOVED** > > Currently, the Midnight blockchain repositories are private. To build the relayer, you need: +> > - A GitHub username with access to the Midnight repositories > - A GitHub Personal Access Token (PAT) or password > @@ -84,7 +83,7 @@ cargo run --example create_key -- \ - Keep this password secure - you'll need it in the next step - The generated keystore file contains your private key encrypted with this password -### Step 4: Configure Environment +### Step 3: Configure Environment Create the environment configuration file from the template: @@ -95,17 +94,17 @@ cp examples/midnight-basic-example/.env.example examples/midnight-basic-example/ Edit the `.env` file and populate the following values: ```bash -# The password you used in Step 3 +# The password you used in Step 2 KEYSTORE_PASSPHRASE=your_keystore_password_here -# Will be generated in Step 5 +# Will be generated in Step 4 WEBHOOK_SIGNING_KEY= -# Will be generated in Step 6 +# Will be generated in Step 5 API_KEY= ``` -### Step 5: Configure Notifications +### Step 4: Configure Notifications The relayer can send webhook notifications for transaction status updates. @@ -132,7 +131,7 @@ cargo run --example generate_uuid Copy the generated UUID and update the `WEBHOOK_SIGNING_KEY` in your `.env` file. -### Step 6: Configure API Key +### Step 5: Configure API Key Generate an API key for authenticating requests to the relayer: @@ -150,7 +149,7 @@ WEBHOOK_SIGNING_KEY=f9e8d7c6-b5a4-3210-fedc-ba0987654321 KEYSTORE_PASSPHRASE=YourSecurePassword123! ``` -### Step 7: Run the Service +### Step 6: Run the Service Start all services with Docker Compose: @@ -172,7 +171,7 @@ Wait for the services to fully initialize. You should see logs indicating: - Relayer is listening on port 8080 - Redis is ready to accept connections -### Step 8: Verify the Setup +### Step 7: Verify the Setup Check that all services are running correctly: @@ -261,17 +260,6 @@ curl -X GET "http://localhost:8080/api/v1/relayers/midnight-testnet-example/tran -H "AUTHORIZATION: Bearer YOUR_API_KEY" | jq ``` -## API Examples - -For more detailed API examples including: - -- Complex transactions with intents -- Fallible offers -- Transaction filtering -- Pagination - -See [midnight-api-examples.md](midnight-api-examples.md) - ## Architecture The example setup consists of three main components: diff --git a/examples/midnight-basic-example/config/config.json b/examples/midnight-basic-example/config/config.json index 7d4c23a86..043bca3ac 100644 --- a/examples/midnight-basic-example/config/config.json +++ b/examples/midnight-basic-example/config/config.json @@ -17,7 +17,7 @@ { "id": "notification-example", "type": "webhook", - "url": "https://webhook.site/86c98e1a-38c1-4f31-a33c-858090c7d0ad", + "url": "https://your-webhook-endpoint.example.com/webhook", "signing_key": { "type": "env", "value": "WEBHOOK_SIGNING_KEY"