diff --git a/.gitignore b/.gitignore index 82e68e79dd..b393da92ff 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ yarn-error.log **/*.ignore tsconfig.editor.json +.vscode/launch.json diff --git a/rust/main/Cargo.lock b/rust/main/Cargo.lock index 5e0045999b..decfb1c677 100644 --- a/rust/main/Cargo.lock +++ b/rust/main/Cargo.lock @@ -317,6 +317,10 @@ name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +dependencies = [ + "borsh 1.5.5", + "serde", +] [[package]] name = "ascii" @@ -421,7 +425,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -432,7 +436,7 @@ checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -501,7 +505,7 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -616,7 +620,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -767,23 +771,22 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.65.1" +version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "cexpr", "clang-sys", + "itertools 0.12.1", "lazy_static", "lazycell", - "peeking_take_while", - "prettyplease", "proc-macro2 1.0.86", "quote 1.0.37", "regex", "rustc-hash", "shlex", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -925,11 +928,11 @@ dependencies = [ [[package]] name = "borsh" -version = "1.5.1" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6362ed55def622cddc70a4746a68554d7b687713770de539e59a739b249f8ed" +checksum = "5430e3be710b68d984d1391c854eb431a9d548640711faa54eecb1df93db91cc" dependencies = [ - "borsh-derive 1.5.1", + "borsh-derive 1.5.5", "cfg_aliases", ] @@ -948,16 +951,15 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.5.1" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b" +checksum = "f8b668d39970baad5356d7c83a86fee3a539e6f93bf6764c97368243e17a0487" dependencies = [ "once_cell", "proc-macro-crate 3.2.0", "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", - "syn_derive", + "syn 2.0.87", ] [[package]] @@ -1071,9 +1073,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.17.1" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773d90827bc3feecfb67fab12e24de0749aad83c74b9504ecde46237b5cd24e2" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" dependencies = [ "bytemuck_derive", ] @@ -1086,7 +1088,7 @@ checksum = "0cc8b54b395f2fcfbb3d90c47b01c7f444d94d05bdeb775811dec868ac3bbc26" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -1097,9 +1099,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" dependencies = [ "serde", ] @@ -1709,7 +1711,7 @@ checksum = "029910b409398fdf81955d7301b906caf81f2c42b013ea074fbd89720229c424" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -1757,7 +1759,7 @@ checksum = "edd3d80310cd7b86b09dbe886f4f2ca235a5ddb8d478493c6e50e720a3b38a42" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -2018,7 +2020,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -2214,7 +2216,7 @@ dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", "strsim 0.11.1", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -2247,7 +2249,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core 0.20.10", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -2378,7 +2380,7 @@ dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", "rustc_version", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -2398,7 +2400,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", "unicode-xid 0.2.5", ] @@ -2515,7 +2517,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -2673,8 +2675,10 @@ checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ "curve25519-dalek 4.1.3", "ed25519 2.2.3", + "serde", "sha2 0.10.8", "subtle", + "zeroize", ] [[package]] @@ -2828,7 +2832,7 @@ checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -2840,7 +2844,7 @@ dependencies = [ "once_cell", "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -3429,7 +3433,7 @@ dependencies = [ "regex", "serde", "serde_json", - "syn 2.0.77", + "syn 2.0.87", "thiserror", ] @@ -3604,7 +3608,7 @@ checksum = "89ad30ad1a11e5a811ae67b6b0cb6785ce21bcd5ef0afd442fd963d5be95d09d" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", "synstructure 0.13.1", ] @@ -3752,7 +3756,7 @@ dependencies = [ "quote 1.0.37", "regex", "serde_json", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -3793,7 +3797,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -3921,7 +3925,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -4284,6 +4288,9 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] [[package]] name = "histogram" @@ -4579,6 +4586,7 @@ dependencies = [ "hyperlane-ethereum", "hyperlane-fuel", "hyperlane-sealevel", + "hyperlane-sovereign", "hyperlane-test", "itertools 0.12.1", "maplit", @@ -4889,6 +4897,33 @@ dependencies = [ "thiserror", ] +[[package]] +name = "hyperlane-sovereign" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "base64 0.21.7", + "bech32 0.11.0", + "bytes", + "ed25519-dalek 2.1.1", + "ethers", + "futures", + "hex 0.4.3", + "hyperlane-core", + "k256 0.13.4", + "reqwest", + "serde", + "serde_json", + "sha2 0.10.8", + "sha3 0.10.8", + "sov-universal-wallet", + "tokio", + "tokio-tungstenite 0.23.1", + "tracing", + "url", +] + [[package]] name = "hyperlane-test" version = "0.1.0" @@ -5073,7 +5108,7 @@ dependencies = [ "autocfg", "impl-tools-lib", "proc-macro-error", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -5085,7 +5120,7 @@ dependencies = [ "proc-macro-error", "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -5387,9 +5422,9 @@ dependencies = [ [[package]] name = "librocksdb-sys" -version = "0.11.0+8.1.1" +version = "0.16.0+8.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3386f101bcb4bd252d8e9d2fb41ec3b0862a15a62b478c355b2982efa469e3e" +checksum = "ce3d60bc059831dc1c83903fb45c103f75db65c5a7bf22272764d9cc683e348c" dependencies = [ "bindgen", "bzip2-sys", @@ -5772,6 +5807,18 @@ dependencies = [ "libc", ] +[[package]] +name = "nmt-rs" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e408e823bdc9b4bb525a61b44e846239833a8f9bd86c03a43e4ca314a5497582" +dependencies = [ + "borsh 1.5.5", + "bytes", + "serde", + "sha2 0.10.8", +] + [[package]] name = "nom" version = "7.1.3" @@ -5893,7 +5940,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -6007,7 +6054,7 @@ dependencies = [ "proc-macro-crate 1.2.1", "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -6019,7 +6066,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -6057,9 +6104,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "opaque-debug" @@ -6121,7 +6168,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -6333,12 +6380,6 @@ dependencies = [ "hmac 0.12.1", ] -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - [[package]] name = "peg" version = "0.8.4" @@ -6421,7 +6462,7 @@ dependencies = [ "pest_meta", "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -6462,7 +6503,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -6593,16 +6634,6 @@ dependencies = [ "termtree", ] -[[package]] -name = "prettyplease" -version = "0.2.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" -dependencies = [ - "proc-macro2 1.0.86", - "syn 2.0.77", -] - [[package]] name = "primeorder" version = "0.13.6" @@ -6731,7 +6762,7 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -6764,7 +6795,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -6777,7 +6808,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -7132,7 +7163,7 @@ dependencies = [ "aho-corasick", "memchr", "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", ] [[package]] @@ -7152,7 +7183,7 @@ checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", ] [[package]] @@ -7163,9 +7194,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "relayer" @@ -7391,9 +7422,9 @@ dependencies = [ [[package]] name = "rocksdb" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb6f170a4041d50a0ce04b0d2e14916d6ca863ea2e422689a5b694395d299ffe" +checksum = "6bd13e55d6d7b8cd0ea569161127567cd587676c99f4472f779a0279aa60a7a7" dependencies = [ "libc", "librocksdb-sys", @@ -7587,7 +7618,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555" dependencies = [ "arrayvec", - "borsh 1.5.1", + "borsh 1.5.5", "bytes", "num-traits", "rand 0.8.5", @@ -7872,7 +7903,7 @@ dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", "serde_derive_internals", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -8221,9 +8252,9 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" [[package]] name = "serde" -version = "1.0.209" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] @@ -8276,13 +8307,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.209" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -8293,14 +8324,14 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] name = "serde_json" -version = "1.0.127" +version = "1.0.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" dependencies = [ "itoa", "memchr", @@ -8326,7 +8357,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -8377,7 +8408,7 @@ dependencies = [ "darling 0.20.10", "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -9166,6 +9197,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "sov-universal-wallet" +version = "0.1.0" +dependencies = [ + "arrayvec", + "bech32 0.11.0", + "borsh 1.5.5", + "bs58 0.5.1", + "hex 0.4.3", + "nmt-rs", + "schemars", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "spin" version = "0.5.2" @@ -9496,7 +9543,7 @@ dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", "rustversion", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -9509,7 +9556,7 @@ dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", "rustversion", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -9557,27 +9604,15 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.77" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", "unicode-ident", ] -[[package]] -name = "syn_derive" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" -dependencies = [ - "proc-macro-error", - "proc-macro2 1.0.86", - "quote 1.0.37", - "syn 2.0.77", -] - [[package]] name = "sync_wrapper" version = "0.1.2" @@ -9610,7 +9645,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -9804,22 +9839,22 @@ checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -9960,7 +9995,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -10067,6 +10102,18 @@ dependencies = [ "tungstenite 0.21.0", ] +[[package]] +name = "tokio-tungstenite" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.23.0", +] + [[package]] name = "tokio-util" version = "0.7.11" @@ -10264,7 +10311,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -10357,7 +10404,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" dependencies = [ "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -10407,6 +10454,24 @@ dependencies = [ "utf-8", ] +[[package]] +name = "tungstenite" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.2.0", + "httparse", + "log", + "rand 0.8.5", + "sha1", + "thiserror", + "utf-8", +] + [[package]] name = "typeid" version = "1.0.2" @@ -10440,7 +10505,7 @@ checksum = "70b20a22c42c8f1cd23ce5e34f165d4d37038f5b663ad20fb6adbdf029172483" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -10490,9 +10555,9 @@ checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" [[package]] name = "unicode-normalization" @@ -10792,7 +10857,7 @@ dependencies = [ "once_cell", "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", "wasm-bindgen-shared", ] @@ -10826,7 +10891,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -11285,7 +11350,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -11305,7 +11370,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.37", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] diff --git a/rust/main/Cargo.toml b/rust/main/Cargo.toml index 447220aaf2..7eae220e22 100644 --- a/rust/main/Cargo.toml +++ b/rust/main/Cargo.toml @@ -7,6 +7,7 @@ members = [ "chains/hyperlane-ethereum", "chains/hyperlane-fuel", "chains/hyperlane-sealevel", + "chains/hyperlane-sovereign", "ethers-prometheus", "hyperlane-base", "hyperlane-core", @@ -103,10 +104,10 @@ prometheus = "0.13" protobuf = "*" rand = "0.8.5" regex = "1.5" -reqwest = "0.11" +reqwest = { version = "0.11", features = ["json"] } ripemd = "0.1.3" rlp = "=0.5.2" -rocksdb = "0.21.0" +rocksdb = "0.22.0" sea-orm = { version = "0.11.1", features = [ "sqlx-postgres", "runtime-tokio-native-tls", diff --git a/rust/main/chains/hyperlane-sovereign/Cargo.toml b/rust/main/chains/hyperlane-sovereign/Cargo.toml new file mode 100644 index 0000000000..b3bb98394a --- /dev/null +++ b/rust/main/chains/hyperlane-sovereign/Cargo.toml @@ -0,0 +1,29 @@ +[package] + name = "hyperlane-sovereign" + version = "0.1.0" + edition.workspace = true + +[dependencies] + hyperlane-core = { path = "../../hyperlane-core", features = ["async"] } + sov-universal-wallet = { path = "../../../../../sovereign-sdk-wip/crates/universal-wallet/schema", features = ["serde"] } + + anyhow.workspace = true + async-trait.workspace = true + base64.workspace = true + bech32.workspace = true + bytes.workspace = true + ethers.workspace = true + futures.workspace = true + k256.workspace = true + reqwest.workspace = true + serde.workspace = true + serde_json.workspace = true + sha2.workspace = true + sha3.workspace = true + tokio = { workspace = true, features = ["fs", "macros"] } + tracing.workspace = true + url.workspace = true + hex.workspace = true + + ed25519-dalek = "2.1.1" + tokio-tungstenite = "0.23" diff --git a/rust/main/chains/hyperlane-sovereign/src/indexer.rs b/rust/main/chains/hyperlane-sovereign/src/indexer.rs new file mode 100644 index 0000000000..e019fda05e --- /dev/null +++ b/rust/main/chains/hyperlane-sovereign/src/indexer.rs @@ -0,0 +1,121 @@ +use crate::rest_client::{self, Tx, TxEvent}; +use async_trait::async_trait; +use core::ops::RangeInclusive; +use hyperlane_core::{ + ChainCommunicationError, ChainResult, Indexed, Indexer, LogMeta, SequenceAwareIndexer, H256, + H512, +}; +use std::fmt::Debug; + +// SovIndexer is a trait that contains default implementations for indexing +// various different event types on the Sovereign chain to reduce code duplication in +// e.g. SovereignMailboxIndexer, SovereignInterchainGasPaymasterIndexer, etc. +#[async_trait] +pub trait SovIndexer: Indexer + SequenceAwareIndexer +where + T: Into> + Debug + Clone + Send, +{ + fn client(&self) -> &rest_client::SovereignRestClient; + fn decode_event(&self, event: &TxEvent) -> ChainResult; + async fn latest_sequence(&self) -> ChainResult>; + const EVENT_KEY: &'static str; + + // Default implementation of Indexer + async fn fetch_logs_in_range( + &self, + range: RangeInclusive, + ) -> ChainResult, LogMeta)>> { + let mut results = + Vec::with_capacity(range.end().saturating_sub(*range.start()) as usize + 1); + + for batch_num in range { + let batch = self.client().get_batch(u64::from(batch_num)).await?; + let batch_hash = parse_hex_to_h256(&batch.hash, "invalid block hash")?; + results.extend( + batch + .txs + .iter() + .flat_map(|tx| self.process_tx(tx, batch_hash)) + .flatten(), + ); + } + + Ok(results) + } + + async fn get_finalized_block_number(&self) -> ChainResult { + let (_latest_slot, latest_batch) = self.client().get_latest_slot().await?; + Ok(latest_batch.unwrap_or_default()) + } + + async fn fetch_logs_by_tx_hash( + &self, + tx_hash: H512, + ) -> ChainResult, LogMeta)>> { + let tx_hash: H256 = tx_hash.into(); + let tx_hash = format!("0x{tx_hash:x}"); + let tx = self.client().get_tx_by_hash(tx_hash).await?; + let batch = self.client().get_batch(tx.batch_number).await?; + let batch_hash = parse_hex_to_h256(&batch.hash, "invalid block hash")?; + self.process_tx(&tx, batch_hash) + } + + // Default implementation of SequenceAwareIndexer + async fn latest_sequence_count_and_tip(&self) -> ChainResult<(Option, u32)> { + let (_, latest_batch) = self.client().get_latest_slot().await?; + let sequence = self.latest_sequence().await?; + + Ok((sequence, latest_batch.unwrap_or_default())) + } + + // Helper function to process a single transaction + fn process_tx(&self, tx: &Tx, batch_hash: H256) -> ChainResult, LogMeta)>> { + let mut results = Vec::new(); + + tx.events + .iter() + .filter(|e| e.key == Self::EVENT_KEY) + .try_for_each(|e| -> ChainResult<()> { + let (indexed_msg, meta) = self.process_event(tx, e, tx.batch_number, batch_hash)?; + results.push((indexed_msg, meta)); + Ok(()) + })?; + Ok(results) + } + + // Helper function to process a single event + fn process_event( + &self, + tx: &Tx, + event: &TxEvent, + batch_num: u64, + batch_hash: H256, + ) -> ChainResult<(Indexed, LogMeta)> { + let tx_hash = parse_hex_to_h256(&tx.hash, "invalid tx hash")?; + let decoded_event = self.decode_event(event)?; + + let meta = LogMeta { + address: batch_hash, + block_number: batch_num, + block_hash: batch_hash, + transaction_id: tx_hash.into(), + transaction_index: tx.number, + log_index: event.number.into(), + }; + + Ok((decoded_event.into(), meta)) + } +} + +fn parse_hex_to_h256(hex: &str, error_msg: &str) -> Result { + hex_to_h256(hex).ok_or(ChainCommunicationError::ParseError { + msg: error_msg.to_string(), + }) +} + +fn hex_to_h256(hex: &str) -> Option { + hex.strip_prefix("0x") + .and_then(|h| hex::decode(h).ok()) + .and_then(|bytes| bytes.try_into().ok()) + .map(|array: [u8; 32]| H256::from_slice(&array)) +} diff --git a/rust/main/chains/hyperlane-sovereign/src/interchain_gas.rs b/rust/main/chains/hyperlane-sovereign/src/interchain_gas.rs new file mode 100644 index 0000000000..fbdac09197 --- /dev/null +++ b/rust/main/chains/hyperlane-sovereign/src/interchain_gas.rs @@ -0,0 +1,115 @@ +use crate::{ + indexer::SovIndexer, + rest_client::{SovereignRestClient, TxEvent}, + ConnectionConf, Signer, SovereignProvider, +}; +use async_trait::async_trait; +use core::ops::RangeInclusive; +use hyperlane_core::{ + ChainResult, ContractLocator, HyperlaneChain, HyperlaneContract, HyperlaneDomain, + HyperlaneProvider, Indexed, Indexer, InterchainGasPaymaster, InterchainGasPayment, LogMeta, + SequenceAwareIndexer, H256, H512, +}; + +/// A reference to a `InterchainGasPaymasterIndexer` contract on some Sovereign chain +#[derive(Debug, Clone)] +pub struct SovereignInterchainGasPaymasterIndexer { + provider: Box, +} + +impl SovereignInterchainGasPaymasterIndexer { + /// Create a new `SovereignInterchainGasPaymasterIndexer`. + pub async fn new(conf: ConnectionConf, locator: ContractLocator<'_>) -> ChainResult { + let provider = SovereignProvider::new(locator.domain.clone(), &conf, None).await?; + + Ok(SovereignInterchainGasPaymasterIndexer { + provider: Box::new(provider), + }) + } +} + +#[async_trait] +impl crate::indexer::SovIndexer for SovereignInterchainGasPaymasterIndexer { + const EVENT_KEY: &'static str = "IGP/GasPayment"; + fn client(&self) -> &SovereignRestClient { + self.provider.client() + } + async fn latest_sequence(&self) -> ChainResult> { + Ok(None) + } + fn decode_event(&self, _event: &TxEvent) -> ChainResult { + todo!() + } +} + +#[async_trait] +impl SequenceAwareIndexer for SovereignInterchainGasPaymasterIndexer { + async fn latest_sequence_count_and_tip(&self) -> ChainResult<(Option, u32)> { + >::latest_sequence_count_and_tip(self).await + } +} + +#[async_trait] +impl Indexer for SovereignInterchainGasPaymasterIndexer { + async fn fetch_logs_in_range( + &self, + range: RangeInclusive, + ) -> ChainResult, LogMeta)>> { + >::fetch_logs_in_range(self, range).await + } + + async fn get_finalized_block_number(&self) -> ChainResult { + >::get_finalized_block_number(self).await + } + + async fn fetch_logs_by_tx_hash( + &self, + tx_hash: H512, + ) -> ChainResult, LogMeta)>> { + >::fetch_logs_by_tx_hash(self, tx_hash).await + } +} + +/// A struct for the Interchain Gas Paymaster on the Sovereign chain. +#[derive(Debug)] +pub struct SovereignInterchainGasPaymaster { + domain: HyperlaneDomain, + address: H256, + provider: SovereignProvider, +} + +impl SovereignInterchainGasPaymaster { + /// Create a new `SovereignInterchainGasPaymaster`. + pub async fn new( + conf: &ConnectionConf, + locator: ContractLocator<'_>, + signer: Option, + ) -> ChainResult { + let provider = + SovereignProvider::new(locator.domain.clone(), &conf.clone(), signer).await?; + Ok(SovereignInterchainGasPaymaster { + domain: locator.domain.clone(), + provider, + address: locator.address, + }) + } +} + +impl HyperlaneContract for SovereignInterchainGasPaymaster { + fn address(&self) -> H256 { + self.address + } +} + +impl HyperlaneChain for SovereignInterchainGasPaymaster { + fn domain(&self) -> &HyperlaneDomain { + &self.domain + } + + fn provider(&self) -> Box { + Box::new(self.provider.clone()) + } +} + +#[async_trait] +impl InterchainGasPaymaster for SovereignInterchainGasPaymaster {} diff --git a/rust/main/chains/hyperlane-sovereign/src/interchain_security_module.rs b/rust/main/chains/hyperlane-sovereign/src/interchain_security_module.rs new file mode 100644 index 0000000000..031ee44e42 --- /dev/null +++ b/rust/main/chains/hyperlane-sovereign/src/interchain_security_module.rs @@ -0,0 +1,66 @@ +use crate::{ConnectionConf, Signer, SovereignProvider}; +use async_trait::async_trait; +use hyperlane_core::{ + ChainResult, ContractLocator, HyperlaneChain, HyperlaneContract, HyperlaneDomain, + HyperlaneMessage, HyperlaneProvider, InterchainSecurityModule, ModuleType, H256, U256, +}; + +/// A struct for the ISM on the Sovereign chain. +#[derive(Debug)] +pub struct SovereignInterchainSecurityModule { + domain: HyperlaneDomain, + address: H256, + provider: SovereignProvider, +} + +impl SovereignInterchainSecurityModule { + /// Create a new `SovereignInterchainSecurityModule`. + pub async fn new( + conf: &ConnectionConf, + locator: ContractLocator<'_>, + signer: Option, + ) -> ChainResult { + let provider = + SovereignProvider::new(locator.domain.clone(), &conf.clone(), signer).await?; + Ok(SovereignInterchainSecurityModule { + domain: locator.domain.clone(), + provider, + address: locator.address, + }) + } +} + +impl HyperlaneContract for SovereignInterchainSecurityModule { + fn address(&self) -> H256 { + self.address + } +} + +impl HyperlaneChain for SovereignInterchainSecurityModule { + fn domain(&self) -> &HyperlaneDomain { + &self.domain + } + + fn provider(&self) -> Box { + Box::new(self.provider.clone()) + } +} + +#[async_trait] +impl InterchainSecurityModule for SovereignInterchainSecurityModule { + async fn dry_run_verify( + &self, + _message: &HyperlaneMessage, + _metadata: &[u8], + ) -> ChainResult> { + let result = self.provider.client().dry_run().await?; + + Ok(result) + } + + async fn module_type(&self) -> ChainResult { + let module_type = self.provider.client().module_type(self.address).await?; + + Ok(module_type) + } +} diff --git a/rust/main/chains/hyperlane-sovereign/src/lib.rs b/rust/main/chains/hyperlane-sovereign/src/lib.rs new file mode 100644 index 0000000000..d0a03f0c3f --- /dev/null +++ b/rust/main/chains/hyperlane-sovereign/src/lib.rs @@ -0,0 +1,17 @@ +pub use self::{ + interchain_gas::*, interchain_security_module::*, mailbox::*, merkle_tree_hook::*, + multisig_ism::*, provider::*, routing_ism::*, signers::*, trait_builder::*, + validator_announce::*, +}; +mod indexer; +mod interchain_gas; +mod interchain_security_module; +mod mailbox; +mod merkle_tree_hook; +mod multisig_ism; +mod provider; +mod routing_ism; +mod signers; +mod trait_builder; +mod universal_wallet_client; +mod validator_announce; diff --git a/rust/main/chains/hyperlane-sovereign/src/mailbox.rs b/rust/main/chains/hyperlane-sovereign/src/mailbox.rs new file mode 100644 index 0000000000..e157a0ec79 --- /dev/null +++ b/rust/main/chains/hyperlane-sovereign/src/mailbox.rs @@ -0,0 +1,214 @@ +use crate::{ + indexer::SovIndexer, + rest_client::{self, TxEvent}, + ConnectionConf, Signer, SovereignProvider, +}; +use async_trait::async_trait; +use core::ops::RangeInclusive; +use hyperlane_core::{ + ChainCommunicationError, ChainResult, ContractLocator, HyperlaneChain, HyperlaneContract, + HyperlaneDomain, HyperlaneMessage, HyperlaneProvider, Indexed, Indexer, LogMeta, Mailbox, + RawHyperlaneMessage, ReorgPeriod, SequenceAwareIndexer, TxCostEstimate, TxOutcome, H256, H512, + U256, +}; +use serde::Deserialize; +use std::fmt::Debug; + +/// Struct that retrieves event data for a Sovereign Mailbox contract +#[derive(Debug, Clone)] +pub struct SovereignMailboxIndexer { + _mailbox: SovereignMailbox, + provider: Box, +} + +impl SovereignMailboxIndexer { + /// Create a new `SovereignMailboxIndexer`. + pub async fn new( + conf: ConnectionConf, + locator: ContractLocator<'_>, + signer: Option, + ) -> ChainResult { + let mailbox = SovereignMailbox::new(&conf, locator.clone(), signer).await?; + let provider = SovereignProvider::new(locator.domain.clone(), &conf, None).await?; + + Ok(SovereignMailboxIndexer { + _mailbox: mailbox, + provider: Box::new(provider), + }) + } +} + +/// A Sovereign Rest message payload. +#[derive(Debug, Clone, Deserialize)] +pub struct DispatchEvent { + dispatch: DispatchEventInner, +} + +/// A Sovereign Rest message payload. +#[derive(Debug, Clone, Deserialize)] +pub struct DispatchEventInner { + message: String, +} + +#[async_trait] +impl crate::indexer::SovIndexer for SovereignMailboxIndexer { + const EVENT_KEY: &'static str = "Mailbox/Dispatch"; + + fn client(&self) -> &rest_client::SovereignRestClient { + self.provider.client() + } + + async fn latest_sequence(&self) -> ChainResult> { + let sequence = self.client().get_count(None).await?; + Ok(Some(sequence)) + } + + fn decode_event(&self, event: &TxEvent) -> ChainResult { + let inner_event: DispatchEvent = serde_json::from_value(event.value.clone())?; + let hex_msg = inner_event + .dispatch + .message + .strip_prefix("0x") + .ok_or_else(|| ChainCommunicationError::ParseError { + msg: "expected '0x' prefix in message".to_string(), + })?; + let raw_msg: RawHyperlaneMessage = hex::decode(hex_msg)?; + Ok(raw_msg.into()) + } +} + +#[async_trait] +impl SequenceAwareIndexer for SovereignMailboxIndexer { + async fn latest_sequence_count_and_tip(&self) -> ChainResult<(Option, u32)> { + >::latest_sequence_count_and_tip(self).await + } +} + +#[async_trait] +impl Indexer for SovereignMailboxIndexer { + async fn fetch_logs_in_range( + &self, + range: RangeInclusive, + ) -> ChainResult, LogMeta)>> { + >::fetch_logs_in_range(self, range).await + } + + async fn get_finalized_block_number(&self) -> ChainResult { + >::get_finalized_block_number(self).await + } + + async fn fetch_logs_by_tx_hash( + &self, + tx_hash: H512, + ) -> ChainResult, LogMeta)>> { + >::fetch_logs_by_tx_hash(self, tx_hash).await + } +} + +/// A reference to a Mailbox contract on some Sovereign chain. +#[derive(Clone, Debug)] +pub struct SovereignMailbox { + provider: SovereignProvider, + domain: HyperlaneDomain, + #[allow(dead_code)] + config: ConnectionConf, + address: H256, +} + +impl SovereignMailbox { + /// Create a new Sovereign mailbox. + pub async fn new( + conf: &ConnectionConf, + locator: ContractLocator<'_>, + signer: Option, + ) -> ChainResult { + let sovereign_provider = + SovereignProvider::new(locator.domain.clone(), &conf.clone(), signer).await?; + + Ok(SovereignMailbox { + provider: sovereign_provider, + domain: locator.domain.clone(), + config: conf.clone(), + address: locator.address, + }) + } +} + +impl HyperlaneContract for SovereignMailbox { + fn address(&self) -> H256 { + self.address + } +} + +impl HyperlaneChain for SovereignMailbox { + fn domain(&self) -> &HyperlaneDomain { + &self.domain + } + + fn provider(&self) -> Box { + Box::new(self.provider.clone()) + } +} + +#[async_trait] +impl Mailbox for SovereignMailbox { + async fn count(&self, reorg_period: &ReorgPeriod) -> ChainResult { + let lag = Some(reorg_period.as_blocks()?); + let count = self.provider.client().get_count(lag).await?; + + Ok(count) + } + + async fn delivered(&self, id: H256) -> ChainResult { + let delivered = self.provider.client().get_delivered_status(id).await?; + + Ok(delivered) + } + + async fn default_ism(&self) -> ChainResult { + let ism = self.provider.client().default_ism().await?; + + Ok(ism) + } + + async fn recipient_ism(&self, recipient: H256) -> ChainResult { + let ism = self.provider.client().recipient_ism(recipient).await?; + + Ok(ism) + } + + async fn process( + &self, + message: &HyperlaneMessage, + metadata: &[u8], + tx_gas_limit: Option, + ) -> ChainResult { + let result = self + .provider + .client() + .process(message, metadata, tx_gas_limit) + .await?; + + Ok(result) + } + + async fn process_estimate_costs( + &self, + message: &HyperlaneMessage, + metadata: &[u8], + ) -> ChainResult { + let costs = self + .provider + .client() + .process_estimate_costs(message, metadata) + .await?; + + Ok(costs) + } + + fn process_calldata(&self, _message: &HyperlaneMessage, _metadata: &[u8]) -> Vec { + // let calldata = self.provider.client().process_calldata(); + // calldata + todo!("Not yet implemented") + } +} diff --git a/rust/main/chains/hyperlane-sovereign/src/merkle_tree_hook.rs b/rust/main/chains/hyperlane-sovereign/src/merkle_tree_hook.rs new file mode 100644 index 0000000000..2ea447db11 --- /dev/null +++ b/rust/main/chains/hyperlane-sovereign/src/merkle_tree_hook.rs @@ -0,0 +1,199 @@ +use crate::{ + indexer::SovIndexer, + rest_client::{to_bech32, SovereignRestClient, TxEvent}, + ConnectionConf, Signer, SovereignProvider, +}; +use async_trait::async_trait; +use core::ops::RangeInclusive; +use hyperlane_core::{ + accumulator::incremental::IncrementalMerkle, ChainCommunicationError, ChainResult, Checkpoint, + ContractLocator, HyperlaneChain, HyperlaneContract, HyperlaneDomain, HyperlaneProvider, + Indexed, Indexer, LogMeta, MerkleTreeHook, MerkleTreeInsertion, ReorgPeriod, + SequenceAwareIndexer, H256, H512, +}; +use serde::Deserialize; +use std::str::FromStr; + +/// Struct that retrieves event data for a Sovereign Mailbox contract. +#[derive(Debug, Clone)] +pub struct SovereignMerkleTreeHookIndexer { + provider: Box, + bech32_address: String, +} + +impl SovereignMerkleTreeHookIndexer { + pub async fn new( + conf: ConnectionConf, + locator: ContractLocator<'_>, + _signer: Option, + ) -> ChainResult { + let provider = SovereignProvider::new(locator.domain.clone(), &conf, None).await?; + Ok(SovereignMerkleTreeHookIndexer { + provider: Box::new(provider), + bech32_address: to_bech32(locator.address)?, + }) + } +} + +#[async_trait] +impl crate::indexer::SovIndexer for SovereignMerkleTreeHookIndexer { + const EVENT_KEY: &'static str = "Merkle/InsertedIntoTree"; + + fn client(&self) -> &SovereignRestClient { + self.provider.client() + } + + async fn latest_sequence(&self) -> ChainResult> { + let sequence = self.client().tree(&self.bech32_address, None).await?; + + match u32::try_from(sequence.count) { + Ok(x) => Ok(Some(x)), + Err(e) => Err(ChainCommunicationError::CustomError(format!( + "Tree count error: {e:?}" + ))), + } + } + fn decode_event(&self, event: &TxEvent) -> ChainResult { + let parsed_event: InsertedIntoTreeEvent = serde_json::from_value(event.value.clone())?; + + let merkle_insertion = MerkleTreeInsertion::new( + parsed_event + .inserted_into_tree + .as_ref() + .and_then(|d| d.index) + .ok_or_else(|| { + ChainCommunicationError::CustomError(String::from( + "parsed_event contained None", + )) + })?, + H256::from_str( + &parsed_event + .inserted_into_tree + .and_then(|d| d.id) + .ok_or_else(|| { + ChainCommunicationError::CustomError(String::from( + "parsed_event contained None", + )) + })?, + )?, + ); + + Ok(merkle_insertion) + } +} + +#[derive(Clone, Debug, Deserialize)] +struct InsertedIntoTreeEvent { + inserted_into_tree: Option, +} + +#[derive(Clone, Debug, Deserialize)] +struct TreeEventBody { + id: Option, + index: Option, +} + +#[async_trait] +impl SequenceAwareIndexer for SovereignMerkleTreeHookIndexer { + async fn latest_sequence_count_and_tip(&self) -> ChainResult<(Option, u32)> { + >::latest_sequence_count_and_tip(self).await + } +} + +#[async_trait] +impl Indexer for SovereignMerkleTreeHookIndexer { + async fn fetch_logs_in_range( + &self, + range: RangeInclusive, + ) -> ChainResult, LogMeta)>> { + >::fetch_logs_in_range(self, range).await + } + + async fn get_finalized_block_number(&self) -> ChainResult { + >::get_finalized_block_number(self).await + } + + async fn fetch_logs_by_tx_hash( + &self, + tx_hash: H512, + ) -> ChainResult, LogMeta)>> { + >::fetch_logs_by_tx_hash(self, tx_hash).await + } +} + +/// A struct for the Merkle Tree Hook on the Sovereign chain +#[derive(Debug)] +pub struct SovereignMerkleTreeHook { + domain: HyperlaneDomain, + address: H256, + provider: SovereignProvider, +} + +impl SovereignMerkleTreeHook { + /// Create a new `SovereignMerkleTreeHook`. + pub async fn new( + conf: &ConnectionConf, + locator: ContractLocator<'_>, + signer: Option, + ) -> ChainResult { + let provider = + SovereignProvider::new(locator.domain.clone(), &conf.clone(), signer).await?; + Ok(SovereignMerkleTreeHook { + domain: locator.domain.clone(), + provider, + address: locator.address, + }) + } +} + +impl HyperlaneChain for SovereignMerkleTreeHook { + fn domain(&self) -> &HyperlaneDomain { + &self.domain + } + + fn provider(&self) -> Box { + Box::new(self.provider.clone()) + } +} + +impl HyperlaneContract for SovereignMerkleTreeHook { + fn address(&self) -> H256 { + self.address + } +} + +#[async_trait] +impl MerkleTreeHook for SovereignMerkleTreeHook { + async fn tree(&self, reorg_period: &ReorgPeriod) -> ChainResult { + let lag = Some(reorg_period.as_blocks()?); + let hook_id = to_bech32(self.address)?; + let tree = self.provider.client().tree(&hook_id, lag).await?; + + Ok(tree) + } + + async fn count(&self, reorg_period: &ReorgPeriod) -> ChainResult { + let lag = Some(reorg_period.as_blocks()?); + let hook_id = to_bech32(self.address)?; + let tree = self.provider.client().tree(&hook_id, lag).await?; + + match u32::try_from(tree.count) { + Ok(x) => Ok(x), + Err(e) => Err(ChainCommunicationError::CustomError(format!( + "Tree count error: {e:?}" + ))), + } + } + + async fn latest_checkpoint(&self, reorg_period: &ReorgPeriod) -> ChainResult { + let lag = Some(reorg_period.as_blocks()?); + let hook_id = to_bech32(self.address)?; + let checkpoint = self + .provider + .client() + .latest_checkpoint(&hook_id, lag, self.domain.id()) + .await?; + + Ok(checkpoint) + } +} diff --git a/rust/main/chains/hyperlane-sovereign/src/multisig_ism.rs b/rust/main/chains/hyperlane-sovereign/src/multisig_ism.rs new file mode 100644 index 0000000000..401e499885 --- /dev/null +++ b/rust/main/chains/hyperlane-sovereign/src/multisig_ism.rs @@ -0,0 +1,63 @@ +use crate::{ConnectionConf, Signer, SovereignProvider}; +use async_trait::async_trait; +use hyperlane_core::{ + ChainResult, ContractLocator, HyperlaneChain, HyperlaneContract, HyperlaneDomain, + HyperlaneMessage, HyperlaneProvider, MultisigIsm, H256, +}; + +/// A struct for the Multisig ISM on the Sovereign chain. +#[derive(Debug)] +pub struct SovereignMultisigIsm { + domain: HyperlaneDomain, + address: H256, + provider: SovereignProvider, +} + +impl SovereignMultisigIsm { + /// Create a new `SovereignMultisigIsm`. + pub async fn new( + conf: &ConnectionConf, + locator: ContractLocator<'_>, + signer: Option, + ) -> ChainResult { + let provider = + SovereignProvider::new(locator.domain.clone(), &conf.clone(), signer).await?; + Ok(SovereignMultisigIsm { + domain: locator.domain.clone(), + provider, + address: locator.address, + }) + } +} + +impl HyperlaneContract for SovereignMultisigIsm { + fn address(&self) -> H256 { + self.address + } +} + +impl HyperlaneChain for SovereignMultisigIsm { + fn domain(&self) -> &HyperlaneDomain { + &self.domain + } + + fn provider(&self) -> Box { + Box::new(self.provider.clone()) + } +} + +#[async_trait] +impl MultisigIsm for SovereignMultisigIsm { + async fn validators_and_threshold( + &self, + message: &HyperlaneMessage, + ) -> ChainResult<(Vec, u8)> { + let validators = self + .provider + .client() + .validators_and_threshold(message) + .await?; + + Ok(validators) + } +} diff --git a/rust/main/chains/hyperlane-sovereign/src/provider.rs b/rust/main/chains/hyperlane-sovereign/src/provider.rs new file mode 100644 index 0000000000..8c88757eb3 --- /dev/null +++ b/rust/main/chains/hyperlane-sovereign/src/provider.rs @@ -0,0 +1,87 @@ +use crate::{ConnectionConf, Signer}; +use async_trait::async_trait; +use hyperlane_core::{ + BlockInfo, ChainInfo, ChainResult, HyperlaneChain, HyperlaneDomain, HyperlaneProvider, TxnInfo, + H256, H512, U256, +}; + +pub mod rest_client; + +/// A wrapper around a Sovereign provider to get generic blockchain information. +#[derive(Debug, Clone)] +pub struct SovereignProvider { + domain: HyperlaneDomain, + client: rest_client::SovereignRestClient, + #[allow(dead_code)] + signer: Option, +} + +impl SovereignProvider { + /// Create a new `SovereignProvider`. + pub async fn new( + domain: HyperlaneDomain, + conf: &ConnectionConf, + signer: Option, + ) -> ChainResult { + let client = rest_client::SovereignRestClient::new(conf, domain.id()).await?; + + Ok(Self { + domain, + client, + signer, + }) + } + + /// Get a nonce. + pub async fn nonce_at_block(&self, _tip: u32) -> ChainResult { + let temp_key = "sov1l6n2cku82yfqld30lanm2nfw43n2auc8clw7r5u5m6s7p8jrm4zqrr8r94"; + let key = self.client().get_values_from_key(temp_key).await?; + let nonce = self.client().get_nonce(&key).await?; + Ok(nonce) + } + + /// Get a rest client. + pub(crate) fn client(&self) -> &rest_client::SovereignRestClient { + &self.client + } +} + +impl HyperlaneChain for SovereignProvider { + fn domain(&self) -> &HyperlaneDomain { + &self.domain + } + + fn provider(&self) -> Box { + Box::new(self.clone()) + } +} + +#[async_trait] +impl HyperlaneProvider for SovereignProvider { + async fn get_block_by_height(&self, height: u64) -> ChainResult { + let block = self.client.get_block_by_height(height).await?; + Ok(block) + } + + async fn get_txn_by_hash(&self, hash: &H512) -> ChainResult { + let txn = self.client.get_txn_by_hash(hash).await?; + Ok(txn) + } + + async fn is_contract(&self, address: &H256) -> ChainResult { + let block = self.client.is_contract(*address).await?; + Ok(block) + } + + async fn get_balance(&self, address: String) -> ChainResult { + let token_id = "token_1nyl0e0yweragfsatygt24zmd8jrr2vqtvdfptzjhxkguz2xxx3vs0y07u7"; + let balance = self.client.get_balance(token_id, address.as_str())?; + Ok(balance) + } + + async fn get_chain_metrics(&self) -> ChainResult> { + // let metrics = self.client.get_chain_metrics().await?; + // Ok(metrics) + todo!("Not yet implemented") + } +} diff --git a/rust/main/chains/hyperlane-sovereign/src/provider/rest_client.rs b/rust/main/chains/hyperlane-sovereign/src/provider/rest_client.rs new file mode 100644 index 0000000000..12e20d8929 --- /dev/null +++ b/rust/main/chains/hyperlane-sovereign/src/provider/rest_client.rs @@ -0,0 +1,1089 @@ +use crate::universal_wallet_client::{utils, UniversalClient}; +use crate::ConnectionConf; +use bech32::{Bech32m, Hrp}; +use bytes::Bytes; +use hyperlane_core::{ + accumulator::incremental::IncrementalMerkle, Announcement, BlockInfo, ChainCommunicationError, + ChainInfo, ChainResult, Checkpoint, FixedPointNumber, HyperlaneMessage, ModuleType, + RawHyperlaneMessage, SignedType, TxCostEstimate, TxOutcome, TxnInfo, TxnReceiptInfo, H160, + H256, H512, U256, +}; +use reqwest::StatusCode; +use reqwest::{header::HeaderMap, Client, Response}; +use serde::Deserialize; +use serde_json::{json, Value}; +use std::{fmt::Debug, str::FromStr}; +use url::Url; + +#[derive(Clone, Debug, Deserialize)] +struct Schema { + data: Option, + _errors: Option, + _meta: Option, +} + +#[derive(Clone, Debug, Deserialize)] +struct Meta { + _meta: Option, +} + +#[derive(Clone, Debug, Deserialize)] +struct Errors { + _details: Option, + _status: Option, + _title: Option, +} + +/// Convert H256 type to String. +pub fn to_bech32(input: H256) -> ChainResult { + let hrp = Hrp::parse("sov") + .map_err(|e| ChainCommunicationError::CustomError(format!("Failed to parse Hrp: {e:?}")))?; + let mut bech32_address = String::new(); + let addr = input.as_ref(); + + match addr.len() { + 28 => { + bech32::encode_to_fmt::(&mut bech32_address, hrp, addr).map_err( + |e| ChainCommunicationError::CustomError(format!("bech32 encoding error: {e:?}")), + )?; + + Ok(bech32_address) + } + 32 if addr[..4] == [0, 0, 0, 0] => { + bech32::encode_to_fmt::(&mut bech32_address, hrp, &addr[4..]) + .map_err(|e| { + ChainCommunicationError::CustomError(format!("bech32 encoding error: {e:?}")) + })?; + + Ok(bech32_address) + } + _ => Err(ChainCommunicationError::CustomError(format!( + "bech_32 encoding error: Address must be 28 bytes, received {addr:?}" + ))), + } +} + +fn from_bech32(input: &str) -> ChainResult { + let (_, slice) = bech32::decode(input).map_err(|e| { + ChainCommunicationError::CustomError(format!("bech32 decoding error: {e:?}")) + })?; + + match slice.len() { + 28 => { + let mut array = [0u8; 32]; + array[4..].copy_from_slice(&slice); + Ok(H256::from_slice(&array)) + } + _ => Err(ChainCommunicationError::CustomError(format!( + "bech_32 encoding error: Address must be 28 bytes, received {slice:?}" + ))), + } +} + +fn try_h256_to_string(input: H256) -> ChainResult { + if input[..12].iter().any(|&byte| byte != 0) { + return Err(ChainCommunicationError::CustomError( + "Input value exceeds size of H160".to_string(), + )); + } + + Ok(format!("{:?}", H160::from(input))) +} + +fn try_h512_to_h256(input: H512) -> ChainResult { + if input[..32] != [0; 32] { + return Err(ChainCommunicationError::CustomError(String::from( + "Invalid input length", + ))); + } + + let bytes = &input[32..]; + Ok(H256::from_slice(bytes)) +} + +#[derive(Clone, Debug)] +pub(crate) struct SovereignRestClient { + url: Url, + client: Client, + universal_wallet_client: UniversalClient, +} + +/// A Sovereign Rest response payload. +#[derive(Clone, Debug, Deserialize)] +pub struct TxEvent { + pub key: String, + pub value: serde_json::Value, + pub number: u64, +} + +/// A Sovereign Rest response payload. +#[derive(Clone, Debug, Deserialize)] +pub struct Tx { + pub number: u64, + pub hash: String, + pub events: Vec, + pub batch_number: u64, +} + +/// A Sovereign Rest response payload. +#[derive(Clone, Debug, Deserialize)] +pub struct Batch { + pub number: u64, + pub hash: String, + pub txs: Vec, + pub slot_number: u64, +} +trait HttpClient { + async fn http_get(&self, query: &str) -> Result; + async fn http_post(&self, query: &str, json: &Value) -> Result; + async fn parse_response(&self, response: Response) -> Result; +} + +impl HttpClient for SovereignRestClient { + async fn http_get(&self, query: &str) -> Result { + let mut header_map = HeaderMap::default(); + header_map.insert( + "content-type", + "application/json".parse().expect("Well-formed &str"), + ); + + let response = self + .client + .get(format!("{}{}", &self.url, query)) + .headers(header_map) + .send() + .await?; + + let result = self.parse_response(response).await?; + Ok(result) + } + + async fn http_post(&self, query: &str, json: &Value) -> Result { + let mut header_map = HeaderMap::default(); + header_map.insert( + "content-type", + "application/json".parse().expect("Well-formed &str"), + ); + + let response = self + .client + .post(format!("{}{}", &self.url, query)) + .headers(header_map) + .json(json) + .send() + .await?; + + let result = self.parse_response(response).await?; + Ok(result) + } + + async fn parse_response(&self, response: Response) -> Result { + match response.status() { + StatusCode::OK => { + // 200 + let response = response.bytes().await?; + Ok(response) + } + StatusCode::BAD_REQUEST => { + // 400 + let response = response.bytes().await?; + Ok(response) + } + StatusCode::NOT_FOUND => { + // 404 + let response = response.bytes().await?; + Ok(response) + } + _ => { + response.error_for_status_ref()?; + let bytes = response.bytes().await?; // Extract the body as Bytes + Ok(bytes) + } + } + } +} + +impl SovereignRestClient { + /// Create a new Rest client for the Sovereign Hyperlane chain. + pub async fn new(conf: &ConnectionConf, domain: u32) -> ChainResult { + let universal_wallet_client = + utils::get_universal_client(conf.url.as_str(), domain).await?; + Ok(SovereignRestClient { + url: conf.url.clone(), + client: Client::new(), + universal_wallet_client, + }) + } + + pub async fn get_values_from_key(&self, key: &str) -> ChainResult { + #[derive(Clone, Debug, Deserialize)] + struct Data { + _key: Option, + value: Option>, + } + + // /modules/accounts/state/credential-ids/items/{key} + let query = format!("/modules/accounts/state/credential-ids/items/{key}"); + + let response = self + .http_get(&query) + .await + .map_err(|e| ChainCommunicationError::CustomError(format!("HTTP Get Error: {e}")))?; + let response: Schema = serde_json::from_slice(&response)?; + + let response = response.data.clone().and_then(|d| d.value).ok_or_else(|| { + ChainCommunicationError::CustomError(String::from("Data contained None")) + })?; + + if response.is_empty() { + Err(ChainCommunicationError::CustomError(String::from( + "Received empty list", + ))) + } else { + Ok(response + .first() + .ok_or(ChainCommunicationError::CustomError(String::from( + "Failed to get first item", + )))? + .clone()) + } + } + + pub async fn get_nonce(&self, key: &str) -> ChainResult { + #[derive(Clone, Debug, Deserialize)] + struct Data { + _key: Option, + value: Option, + } + + // /modules/nonces/state/nonces/items/{key} + let query = format!("/modules/nonces/state/nonces/items/{key}"); + + let response = self + .http_get(&query) + .await + .map_err(|e| ChainCommunicationError::CustomError(format!("HTTP Get Error: {e}")))?; + let response: Schema = serde_json::from_slice(&response)?; + + let response = response.data.and_then(|d| d.value).ok_or_else(|| { + ChainCommunicationError::CustomError(String::from("Data contained None")) + })?; + Ok(response) + } + + // @Provider + pub async fn get_block_by_height(&self, height: u64) -> ChainResult { + #[derive(Clone, Debug, Deserialize)] + struct Data { + #[serde(rename = "type")] + _sovereign_type: Option, + number: Option, + hash: Option, + _event_range: Option, + _receipt: Option, + _body: Option, + _events: Option, + _batch_number: Option, + } + + #[derive(Clone, Debug, Deserialize)] + struct EventRange { + _start: Option, + _end: Option, + } + + // /ledger/slots/{slotId} + let children = 0; + let query = format!("/ledger/slots/{height:?}?children={children}"); + + let response = self + .http_get(&query) + .await + .map_err(|e| ChainCommunicationError::CustomError(format!("HTTP Get Error: {e}")))?; + let response: Schema = serde_json::from_slice(&response)?; + + if let Some(response_data) = response.data { + if let (Some(hash), Some(number)) = (response_data.hash, response_data.number) { + Ok(BlockInfo { + hash: H256::from_str(hash.as_str())?, + timestamp: u64::default(), + number, + }) + } else { + Err(ChainCommunicationError::CustomError(String::from( + "Bad response", + ))) + } + } else { + Err(ChainCommunicationError::CustomError(String::from( + "Bad response", + ))) + } + } + + // @Provider + pub async fn get_txn_by_hash(&self, height: &H512) -> ChainResult { + #[derive(Clone, Debug, Deserialize)] + struct Data { + id: Option, + _status: Option, + } + + let height = try_h512_to_h256(*height)?; + + // /sequencer/txs/{txHash} + let query = format!("/sequencer/txs/{height:?}"); + + let response = self + .http_get(&query) + .await + .map_err(|e| ChainCommunicationError::CustomError(format!("HTTP Get Error: {e}")))?; + let response: Schema = serde_json::from_slice(&response)?; + + let res = TxnInfo { + hash: H512::from_str( + response + .data + .and_then(|d| d.id) + .ok_or(ChainCommunicationError::CustomError( + "Invalid response".to_string(), + ))? + .as_str(), + )?, + gas_limit: U256::default(), + max_priority_fee_per_gas: Some(U256::default()), + max_fee_per_gas: Some(U256::default()), + gas_price: Some(U256::default()), + nonce: u64::default(), + sender: H256::default(), + recipient: Some(H256::default()), + receipt: Some(TxnReceiptInfo { + gas_used: U256::default(), + cumulative_gas_used: U256::default(), + effective_gas_price: Some(U256::default()), + }), + raw_input_data: None, + }; + Ok(res) + } + + pub async fn get_batch(&self, batch: u64) -> ChainResult { + let query = format!("/ledger/batches/{batch}?children=1"); + + let response = self + .http_get(&query) + .await + .map_err(|e| ChainCommunicationError::CustomError(format!("HTTP Get Error: {e}")))?; + let response: Schema = serde_json::from_slice(&response)?; + + response.data.ok_or_else(|| { + ChainCommunicationError::CustomError( + "Invalid response: missing batch field".to_string(), + ) + }) + } + + pub async fn get_tx_by_hash(&self, tx_id: String) -> ChainResult { + let query = format!("/ledger/txs/{tx_id}?children=1"); + + let response = self + .http_get(&query) + .await + .map_err(|e| ChainCommunicationError::CustomError(format!("HTTP Get Error: {e}")))?; + let response: Schema = serde_json::from_slice(&response)?; + + response.data.ok_or_else(|| { + ChainCommunicationError::CustomError("Invalid response: missing tx field".to_string()) + }) + } + + async fn get_compensated_rollup_height(&self, lag: u64) -> ChainResult { + let (_, latest_batch) = self.get_latest_slot().await?; + let batch = self + .get_batch( + latest_batch + .ok_or(ChainCommunicationError::CustomError(String::from( + "latest batch was None", + )))? + .into(), + ) + .await?; + batch.slot_number.checked_sub(lag).ok_or_else(|| { + ChainCommunicationError::CustomError("lag was greater than rollup height".to_string()) + }) + } + + // Return the latest slot, and the highest committed batch number in that slot. + pub async fn get_latest_slot(&self) -> ChainResult<(u32, Option)> { + #[derive(Clone, Debug, Deserialize)] + struct BatchRange { + // start: u32, + end: u32, + } + #[derive(Clone, Debug, Deserialize)] + struct Data { + batch_range: BatchRange, + number: u32, + } + + let query = "/ledger/slots/latest?children=0"; + + let response = self + .http_get(query) + .await + .map_err(|e| ChainCommunicationError::CustomError(format!("HTTP Get Error: {e}")))?; + let response: Schema = serde_json::from_slice(&response)?; + + let data = response.data.ok_or(ChainCommunicationError::CustomError( + "Invalid response".to_string(), + ))?; + + // bach_range.end is exclusive - it's one above the last committed batch number + let last_batch = data.batch_range.end.checked_sub(1); + + Ok((data.number, last_batch)) + } + + // @Provider - test working, need to test all variants + pub async fn is_contract(&self, key: H256) -> ChainResult { + #[derive(Clone, Debug, Deserialize)] + struct Data { + key: Option, + _value: Option, + } + + for module in [ + "mailbox-hook-registry", + "mailbox-ism-registry", + "mailbox-recipient-registry", + ] { + let query = format!( + "/modules/{}/state/registry/items/{}", + module, + to_bech32(key)? + ); + + let response = self.http_get(&query).await.map_err(|e| { + ChainCommunicationError::CustomError(format!("HTTP Get Error: {e}")) + })?; + let response: Schema = serde_json::from_slice(&response)?; + + if response.data.and_then(|data| data.key).is_some() { + return Ok(true); + } + } + Ok(false) + } + + // @Provider + pub fn get_balance(&self, _token_id: &str, _address: &str) -> ChainResult { + // // /modules/bank/tokens/{token_id}/balances/{address} + // let query = format!("/modules/bank/tokens/{}/balances/{}", token_id, address); + + // #[derive(Clone, Debug, Deserialize)] + // struct Data { + // _amount: Option, + // _token_id: Option, + // } + + // let response = self + // .http_get(&query) + // .await + // .map_err(|e| ChainCommunicationError::CustomError(format!("HTTP Get Error: {}", e)))?; + // let response: Schema = serde_json::from_slice(&response)?; + + // let response = U256::from(response); + Ok(U256::default()) + } + + // @Provider + pub fn _get_chain_metrics(&self) -> ChainResult> { + todo!("Not yet implemented") + } + + // @Mailbox + pub async fn get_count(&self, at_height: Option) -> ChainResult { + #[derive(Clone, Debug, Deserialize)] + struct Data { + value: Option, + } + + // /modules/mailbox/state/nonce + let query: String = match at_height { + Some(0) | None => "/modules/mailbox/state/nonce".to_owned(), + Some(lag) => { + let rollup_height = self.get_compensated_rollup_height(u64::from(lag)).await?; + format!("/modules/mailbox/state/nonce?rollup_height={rollup_height}") + } + }; + + let response = self + .http_get(&query) + .await + .map_err(|e| ChainCommunicationError::CustomError(format!("HTTP Get Error: {e}")))?; + let response: Schema = serde_json::from_slice(&response)?; + + let response = response + .data + .and_then(|data| data.value) + .unwrap_or_default(); + + Ok(response) + } + + // @Mailbox + pub async fn get_delivered_status(&self, message_id: H256) -> ChainResult { + #[derive(Clone, Debug, Deserialize)] + struct Data { + _value: Option, + } + + #[derive(Clone, Debug, Deserialize)] + struct StateMap { + _sender: Option, + _block_number: Option, + } + + // /modules/mailbox/state/deliveries/items/{key} + let query = format!("/modules/mailbox/state/deliveries/items/{message_id:?}"); + + let response = self + .http_get(&query) + .await + .map_err(|e| ChainCommunicationError::CustomError(format!("HTTP Get Error: {e}")))?; + let response: Schema = serde_json::from_slice(&response)?; + + Ok(response.data.is_some()) + } + + // @Mailbox - test working + pub async fn default_ism(&self) -> ChainResult { + #[derive(Clone, Debug, Deserialize)] + struct Data { + value: Option, + } + + let query = "/modules/mailbox/state/default-ism"; + + let response = self + .http_get(query) + .await + .map_err(|e| ChainCommunicationError::CustomError(format!("HTTP Get Error: {e}")))?; + let response: Schema = serde_json::from_slice(&response)?; + + let addr_bech32 = response.data.and_then(|d| d.value).ok_or_else(|| { + ChainCommunicationError::CustomError(String::from("Data contained None")) + })?; + from_bech32(&addr_bech32) + } + + // @Mailbox + pub async fn recipient_ism(&self, recipient_id: H256) -> ChainResult { + #[derive(Clone, Debug, Deserialize)] + struct Data { + address: Option, + } + + let recipient_bech32 = to_bech32(recipient_id)?; + + let query = format!("/modules/mailbox-recipient-registry/{recipient_bech32}/ism"); + + let response = self + .http_get(&query) + .await + .map_err(|e| ChainCommunicationError::CustomError(format!("HTTP Get Error: {e}")))?; + let response: Schema = serde_json::from_slice(&response)?; + + let addr_bech32 = response.data.and_then(|d| d.address).ok_or_else(|| { + ChainCommunicationError::CustomError(String::from("Data contained None")) + })?; + from_bech32(&addr_bech32) + } + + // @Mailbox - test working + pub async fn process( + &self, + message: &HyperlaneMessage, + metadata: &[u8], + _tx_gas_limit: Option, + ) -> ChainResult { + #[derive(Clone, Debug, Deserialize)] + struct TxData { + _id: Option, + status: Option, + } + + #[derive(Clone, Debug, Deserialize)] + struct BatchData { + _blob_hash: Option, + _da_transaction_id: Option>, + _tx_hashes: Option>, + } + + // /sequencer/txs + let query = "/sequencer/txs"; + + let body = + utils::get_submit_body_string(message, metadata, &self.universal_wallet_client).await?; + let json = json!({"body":body}); + let response = self + .http_post(query, &json) + .await + .map_err(|e| ChainCommunicationError::CustomError(format!("HTTP Error: {e}")))?; + let response: Schema = serde_json::from_slice(&response)?; + + let result = match response.data.and_then(|d| d.status) { + Some(s) => s == *"submitted", + None => false, + }; + + // /sequencer/batches + let query = "/sequencer/batches"; + + let json = json!( + { + "transactions":[body] + } + ); + let response = self + .http_post(query, &json) + .await + .map_err(|e| ChainCommunicationError::CustomError(format!("HTTP Error: {e}")))?; + + let _response: Schema = serde_json::from_slice(&response)?; + + let res = TxOutcome { + transaction_id: H512::default(), + executed: result, + gas_used: U256::default(), + gas_price: FixedPointNumber::default(), + }; + + Ok(res) + } + + // @Mailbox + pub async fn process_estimate_costs( + &self, + message: &HyperlaneMessage, + metadata: &[u8], + ) -> ChainResult { + #[derive(Clone, Debug, Deserialize)] + struct Data { + apply_tx_result: Option, + } + + #[derive(Clone, Debug, Deserialize)] + struct ApplyTxResult { + _receipt: Option, + transaction_consumption: Option, + } + + #[derive(Clone, Debug, Deserialize)] + struct Receipt { + _events: Option>, + _receipt: Option, + } + + #[derive(Clone, Debug, Deserialize)] + struct Events { + _key: Option, + _value: Option, + } + + #[derive(Clone, Debug, Deserialize)] + struct SubReceipt { + _content: Option, + _outcome: Option, + } + + #[derive(Clone, Debug, Deserialize)] + struct TransactionConsumption { + base_fee: Option>, + gas_price: Option>, + _priority_fee: Option, + _remaining_funds: Option, + } + + // /rollup/simulate + let query = "/rollup/simulate"; + + let json = utils::get_simulate_json_query(message, metadata, &self.universal_wallet_client) + .await?; + + let response = self + .http_post(query, &json) + .await + .map_err(|e| ChainCommunicationError::CustomError(format!("HTTP Error: {e}")))?; + let response: Schema = serde_json::from_slice(&response)?; + + let gas_price = FixedPointNumber::from( + response + .clone() + .data + .ok_or_else(|| { + ChainCommunicationError::CustomError(String::from("data contained None")) + })? + .apply_tx_result + .ok_or_else(|| { + ChainCommunicationError::CustomError(String::from( + "apply_tx_result contained None", + )) + })? + .transaction_consumption + .ok_or_else(|| { + ChainCommunicationError::CustomError(String::from( + "transaction_consumption contained None", + )) + })? + .gas_price + .ok_or_else(|| { + ChainCommunicationError::CustomError(String::from("gas_price contained None")) + })? + .first() + .ok_or_else(|| { + ChainCommunicationError::CustomError(String::from("Failed to get item(0)")) + })?, + ); + + let gas_limit = U256::from( + *response + .data + .ok_or_else(|| { + ChainCommunicationError::CustomError(String::from("data contained None")) + })? + .apply_tx_result + .ok_or_else(|| { + ChainCommunicationError::CustomError(String::from( + "apply_tx_result contained None", + )) + })? + .transaction_consumption + .ok_or_else(|| { + ChainCommunicationError::CustomError(String::from( + "transaction_consumption contained None", + )) + })? + .base_fee + .ok_or_else(|| { + ChainCommunicationError::CustomError(String::from("base_fee contained None")) + })? + .first() + .ok_or_else(|| { + ChainCommunicationError::CustomError(String::from("Failed to get item(0)")) + })?, + ); + + let res = TxCostEstimate { + gas_limit, + gas_price, + l2_gas_limit: None, + }; + + Ok(res) + } + + // @Mailbox + pub fn _process_calldata(&self) -> Vec { + todo!("Not yet implemented") + } + + // @ISM + pub async fn dry_run(&self) -> ChainResult> { + #[derive(Clone, Debug, Deserialize)] + struct Data { + _data: Option, + } + + // /rollup/simulate + let query = "/rollup/simulate"; + + let json = json!( + { + "body":{ + "details":{ + "chain_id":0, + "max_fee":0, + "max_priority_fee_bips":0 + }, + "encoded_call_message":"", + "nonce":0, + "sender_pub_key":"" + } + } + ); + + let response = self + .http_post(query, &json) + .await + .map_err(|e| ChainCommunicationError::CustomError(format!("HTTP Error: {e}")))?; + let _response: Schema = serde_json::from_slice(&response)?; + + Ok(None) + } + + // @ISM - test working + pub async fn module_type(&self, ism_id: H256) -> ChainResult { + let query = format!( + "/modules/mailbox-ism-registry/{}/module_type/", + to_bech32(ism_id)? + ); + + let response = self + .http_get(&query) + .await + .map_err(|e| ChainCommunicationError::CustomError(format!("HTTP Get Error: {e}")))?; + let response: Schema = serde_json::from_slice(&response)?; + + match response.data.ok_or_else(|| { + ChainCommunicationError::CustomError(String::from("Data contained None")) + })? { + 0 => Ok(ModuleType::Unused), + 1 => Ok(ModuleType::Routing), + 2 => Ok(ModuleType::Aggregation), + 3 => Ok(ModuleType::LegacyMultisig), + 4 => Ok(ModuleType::MerkleRootMultisig), + 5 => Ok(ModuleType::MessageIdMultisig), + 6 => Ok(ModuleType::Null), + 7 => Ok(ModuleType::CcipRead), + _ => Err(ChainCommunicationError::CustomError(String::from( + "Unknown ModuleType returned", + ))), + } + } + + // @Merkle Tree Hook + pub async fn tree(&self, hook_id: &str, slot: Option) -> ChainResult { + #[derive(Clone, Debug, Deserialize)] + struct Data { + count: Option, + branch: Option>, + } + + // /mailbox-hook-merkle-tree/{hook_id}/tree + let query = match slot { + Some(0) | None => { + format!("modules/mailbox-hook-merkle-tree/{hook_id}/tree") + } + Some(lag) => { + let rollup_height = self.get_compensated_rollup_height(u64::from(lag)).await?; + format!( + "modules/mailbox-hook-merkle-tree/{hook_id}/tree?rollup_height={rollup_height}" + ) + } + }; + + let response = self + .http_get(&query) + .await + .map_err(|e| ChainCommunicationError::CustomError(format!("HTTP Get Error: {e}")))?; + let response: Schema = serde_json::from_slice(&response)?; + + let mut incremental_merkle = IncrementalMerkle { + count: response.clone().data.and_then(|d| d.count).ok_or_else(|| { + ChainCommunicationError::ParseError { + msg: String::from("Empty field"), + } + })?, + ..Default::default() + }; + response + .data + .and_then(|d| d.branch) + .ok_or_else(|| { + ChainCommunicationError::CustomError(String::from("Data contained None")) + })? + .into_iter() + .enumerate() + .for_each(|(i, f)| incremental_merkle.branch[i] = H256::from_str(&f).unwrap()); + + Ok(incremental_merkle) + } + + // @Merkle Tree Hook + pub async fn latest_checkpoint( + &self, + hook_id: &str, + lag: Option, + mailbox_domain: u32, + ) -> ChainResult { + #[derive(Clone, Debug, Deserialize)] + struct Data { + index: Option, + root: Option, + } + + // /mailbox-hook-merkle-tree/{hook_id}/checkpoint + let query = match lag { + Some(0) | None => { + format!("modules/mailbox-hook-merkle-tree/{hook_id}/checkpoint") + } + Some(lag) => { + let rollup_height = self.get_compensated_rollup_height(u64::from(lag)).await?; + format!("modules/mailbox-hook-merkle-tree/{hook_id}/checkpoint?rollup_height={rollup_height}") + } + }; + + let response = self + .http_get(&query) + .await + .map_err(|e| ChainCommunicationError::CustomError(format!("HTTP Get Error: {e}")))?; + let response: Schema = serde_json::from_slice(&response)?; + + let response = Checkpoint { + merkle_tree_hook_address: from_bech32(hook_id)?, + mailbox_domain, + root: H256::from_str(&response.data.clone().and_then(|d| d.root).ok_or_else( + || ChainCommunicationError::ParseError { + msg: String::from("Empty field"), + }, + )?)?, + index: response.data.clone().and_then(|d| d.index).ok_or_else(|| { + ChainCommunicationError::ParseError { + msg: String::from("Empty field"), + } + })?, + }; + + Ok(response) + } + + // @MultiSig ISM + pub async fn validators_and_threshold( + &self, + message: &HyperlaneMessage, + ) -> ChainResult<(Vec, u8)> { + #[derive(Clone, Debug, Deserialize)] + struct Data { + validators: Option>, + threshold: Option, + } + + let ism_id = self.recipient_ism(message.recipient).await?; + let ism_id = to_bech32(ism_id)?; + + let message = hex::encode(RawHyperlaneMessage::from(message)); + let message = format!("0x{message}"); + + // /modules/mailbox-ism-registry/{ism_id}/validators_and_threshold + let query = format!( + "/modules/mailbox-ism-registry/{ism_id}/validators_and_threshold?data={message}" + ); + + let response = self + .http_get(&query) + .await + .map_err(|e| ChainCommunicationError::CustomError(format!("HTTP Get Error: {e}")))?; + let response: Schema = serde_json::from_slice(&response)?; + + let threshold = response.data.clone().and_then(|d| d.threshold).ok_or( + ChainCommunicationError::CustomError(String::from("Threshold contained None")), + )?; + let mut validators: Vec = Vec::new(); + response + .data + .and_then(|d| d.validators) + .ok_or(ChainCommunicationError::CustomError(String::from( + "Threshold contained None", + )))? + .iter() + .for_each(|v| { + let address = + H256::from_str(&format!("0x{:0>64}", v.trim_start_matches("0x"))).unwrap(); + validators.push(address); + }); + + let res = (validators, threshold); + + Ok(res) + } + + // @Routing ISM + pub fn _route(&self) -> ChainResult { + todo!("Not yet implemented") + } + + // @Validator Announce + pub async fn get_announced_storage_locations( + &self, + validators: &[H256], + ) -> ChainResult>> { + #[derive(Clone, Debug, Deserialize)] + struct Data { + _key: Option, + value: Option>, + } + + // /modules/mailbox-va/state/storage-locations/items/{key} + let mut res = Vec::new(); + + for (i, v) in validators.iter().enumerate() { + res.push(vec![]); + let validator = try_h256_to_string(*v)?; + + let query = format!("/modules/mailbox-va/state/storage-locations/items/{validator}"); + + let response = self.http_get(&query).await.map_err(|e| { + ChainCommunicationError::CustomError(format!("HTTP Get Error: {e}")) + })?; + let response: Schema = serde_json::from_slice(&response)?; + + if let Some(data) = response.data { + res[i].push(String::new()); + if let Some(storage_locations) = data.value { + storage_locations + .into_iter() + .enumerate() + .for_each(|(j, storage_location)| { + res[i][j] = storage_location; + }); + } + } + } + + Ok(res) + } + + // @Validator Announce + pub async fn announce(&self, announcement: SignedType) -> ChainResult { + #[derive(Clone, Debug, Deserialize)] + struct Data { + _key: Option, + _value: Option>, + } + + // /modules/mailbox-va/state/storage-locations/items/{key} + // check if already registered + let query = format!( + "/modules/mailbox-va/state/storage-locations/items/{:?}", + announcement.value.validator + ); + + let response = self + .http_get(&query) + .await + .map_err(|e| ChainCommunicationError::CustomError(format!("HTTP Get Error: {e}")))?; + let response: Schema = serde_json::from_slice(&response)?; + + let mut tx_outcome = TxOutcome { + transaction_id: H512::default(), + executed: bool::default(), + gas_used: U256::default(), + gas_price: FixedPointNumber::default(), + }; + if response.data.is_none() { + let res = + utils::announce_validator(announcement, &self.universal_wallet_client).await?; + tx_outcome.executed = true; + let tx_id = &format!("0x{:0>128}", res.trim_start_matches("0x")); + tx_outcome.transaction_id = H512::from_str(tx_id)?; + }; + + Ok(tx_outcome) + } + + // @Validator Announce + pub fn _announce_tokens_needed(&self) -> Option { + todo!("Not yet implemented") + } +} diff --git a/rust/main/chains/hyperlane-sovereign/src/routing_ism.rs b/rust/main/chains/hyperlane-sovereign/src/routing_ism.rs new file mode 100644 index 0000000000..aba324a189 --- /dev/null +++ b/rust/main/chains/hyperlane-sovereign/src/routing_ism.rs @@ -0,0 +1,56 @@ +use crate::{ConnectionConf, Signer, SovereignProvider}; +use async_trait::async_trait; +use hyperlane_core::{ + ChainResult, ContractLocator, HyperlaneChain, HyperlaneContract, HyperlaneDomain, + HyperlaneMessage, HyperlaneProvider, RoutingIsm, H256, +}; + +/// A struct for the Routing ISM on the Sovereign chain. +#[derive(Debug)] +pub struct SovereignRoutingIsm { + domain: HyperlaneDomain, + address: H256, + provider: SovereignProvider, +} + +impl SovereignRoutingIsm { + /// Create a new `SovereignRoutingIsm`. + pub async fn new( + conf: &ConnectionConf, + locator: ContractLocator<'_>, + signer: Option, + ) -> ChainResult { + let provider = + SovereignProvider::new(locator.domain.clone(), &conf.clone(), signer).await?; + Ok(SovereignRoutingIsm { + domain: locator.domain.clone(), + provider, + address: locator.address, + }) + } +} + +impl HyperlaneContract for SovereignRoutingIsm { + fn address(&self) -> H256 { + self.address + } +} + +impl HyperlaneChain for SovereignRoutingIsm { + fn domain(&self) -> &HyperlaneDomain { + &self.domain + } + + fn provider(&self) -> Box { + Box::new(self.provider.clone()) + } +} + +#[async_trait] +impl RoutingIsm for SovereignRoutingIsm { + async fn route(&self, _message: &HyperlaneMessage) -> ChainResult { + // let result = self.provider.client().route().await?; + // Ok(result) + todo!("Not yet implemented") + } +} diff --git a/rust/main/chains/hyperlane-sovereign/src/signers.rs b/rust/main/chains/hyperlane-sovereign/src/signers.rs new file mode 100644 index 0000000000..4ab3fc1fe9 --- /dev/null +++ b/rust/main/chains/hyperlane-sovereign/src/signers.rs @@ -0,0 +1,49 @@ +use ethers::types::Address; +use hyperlane_core::{ChainCommunicationError, ChainResult, H256}; +use k256::ecdsa::SigningKey; +use sha3::{Digest, Keccak256}; + +/// Signer for Sovereign chain. +#[derive(Clone, Debug)] +pub struct Signer { + /// The Signer's address. + pub address: String, +} + +impl Signer { + /// Create a new Sovereign signer. + pub fn new(private_key: &H256) -> ChainResult { + let address = address_from_h256(private_key)?; + Ok(Signer { address }) + } +} + +fn address_from_h256(private_key: &H256) -> ChainResult { + let signing_key = SigningKey::from_bytes(private_key.as_bytes().into()) + .map_err(|e| ChainCommunicationError::CustomError(format!("Key Failure: {e:?}")))?; + let public_key = signing_key.verifying_key(); + + let binding = public_key.to_encoded_point(false); + let public_key_bytes = binding.as_bytes(); + + let hash = Keccak256::digest(&public_key_bytes[1..]); + let address = Address::from_slice(&hash[12..]); + let address = format!("{address:?}"); + + Ok(address) +} + +#[cfg(test)] +mod test { + use super::*; + use std::str::FromStr; + + #[test] + fn test_address_from_h256() { + let private_key = + H256::from_str("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80") + .unwrap(); + let res = address_from_h256(&private_key).unwrap(); + assert_eq!("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", res) + } +} diff --git a/rust/main/chains/hyperlane-sovereign/src/trait_builder.rs b/rust/main/chains/hyperlane-sovereign/src/trait_builder.rs new file mode 100644 index 0000000000..f3b5e2692a --- /dev/null +++ b/rust/main/chains/hyperlane-sovereign/src/trait_builder.rs @@ -0,0 +1,11 @@ +use hyperlane_core::config::OperationBatchConfig; +use url::Url; + +/// Sovereign connection configuration. +#[derive(Debug, Clone)] +pub struct ConnectionConf { + /// Operation batching configuration. + pub operation_batch: OperationBatchConfig, + /// Endpoint address. + pub url: Url, +} diff --git a/rust/main/chains/hyperlane-sovereign/src/universal_wallet_client/crypto.rs b/rust/main/chains/hyperlane-sovereign/src/universal_wallet_client/crypto.rs new file mode 100644 index 0000000000..d8265a883d --- /dev/null +++ b/rust/main/chains/hyperlane-sovereign/src/universal_wallet_client/crypto.rs @@ -0,0 +1,73 @@ +use anyhow::{bail, Result}; +use ed25519_dalek::{Signer, SigningKey}; +use sha2::{Digest, Sha256}; + +/// Collection of Private Key types. +#[derive(Clone, Debug)] +pub enum PrivateKey { + Ed25519(SigningKey), +} + +/// Collection of Hashers. +#[derive(Clone, Debug)] +pub enum Hasher { + Sha256, +} + +/// Collection of Address types. +#[derive(Clone, Debug)] +pub enum Address { + Bech32m { hrp: bech32::Hrp, size_bytes: usize }, +} + +/// Struct for Crypto. +#[derive(Clone, Debug)] +pub struct Crypto { + pub private_key: PrivateKey, + pub hasher: Hasher, + pub address_type: Address, +} + +impl Crypto { + /// Do signature. + #[must_use] + pub fn sign(&self, input: &[u8]) -> Vec { + match self.private_key { + PrivateKey::Ed25519(ref key) => key.sign(input).to_bytes().to_vec(), + } + } + + /// Get the pub key. + #[must_use] + pub fn public_key(&self) -> Vec { + match self.private_key { + PrivateKey::Ed25519(ref key) => key.verifying_key().as_bytes().to_vec(), + } + } + + /// Get an address. + pub fn address(&self) -> Result { + let hash = match self.hasher { + Hasher::Sha256 => { + let mut h = Sha256::new(); + h.update(self.public_key()); + h.finalize() + } + }; + + match self.address_type { + Address::Bech32m { hrp, size_bytes } => { + let mut bech32_address = String::new(); + if size_bytes > hash.len() { + bail!("address size > hash size") + } + bech32::encode_to_fmt::( + &mut bech32_address, + hrp, + &hash[..size_bytes], + )?; + Ok(bech32_address) + } + } + } +} diff --git a/rust/main/chains/hyperlane-sovereign/src/universal_wallet_client/mod.rs b/rust/main/chains/hyperlane-sovereign/src/universal_wallet_client/mod.rs new file mode 100644 index 0000000000..26f24bd8d2 --- /dev/null +++ b/rust/main/chains/hyperlane-sovereign/src/universal_wallet_client/mod.rs @@ -0,0 +1,186 @@ +use anyhow::{bail, Context, Result}; +use base64::prelude::BASE64_STANDARD; +use base64::Engine; +use futures::StreamExt; +use reqwest::{Client, ClientBuilder}; +use serde_json::{json, Value}; +use sov_universal_wallet::schema::{RollupRoots, Schema}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use tokio::time::{timeout_at, Instant}; + +mod crypto; +mod tx_state; +mod types; +pub mod utils; + +use types::TxStatus; + +/// A `UnversalClient` for interacting with the Universal Wallet. +#[derive(Clone, Debug)] +pub struct UniversalClient { + api_url: String, + chain_hash: [u8; 32], + chain_id: u64, + http_client: Client, + crypto: crypto::Crypto, + #[allow(dead_code)] + address: String, +} + +impl UniversalClient { + /// Create a new `UniversalClient`. + async fn new(api_url: &str, crypto: crypto::Crypto, chain_id: u64) -> anyhow::Result { + let http_client = ClientBuilder::default().build()?; + let mut schema = Self::fetch_schema(api_url, &http_client).await?; + + Ok(Self { + api_url: api_url.to_string(), + chain_hash: schema.chain_hash()?, + chain_id, + http_client, + address: crypto.address()?, + crypto, + }) + } + + /// Build a transaction and submit it to the rollup. + async fn build_and_submit(&self, call_message: Value) -> Result<(String, String)> { + let utx = self.build_tx_json(&call_message); + let tx = self.sign_tx(utx).await?; + let body = self.serialise_tx(&tx).await?; + let hash = self.submit_tx(body.clone()).await?; + self.wait_for_tx(hash.clone()).await?; + + Ok((hash, body)) + } + + async fn wait_for_tx(&self, tx_hash: String) -> Result<()> { + let mut slot_subscription = self.subscribe_to_tx_status_updates(tx_hash).await?; + + let end_wait_time = Instant::now() + Duration::from_secs(300); + let start_wait = Instant::now(); + + while Instant::now() < end_wait_time { + match timeout_at(end_wait_time, slot_subscription.next()).await? { + Some(Ok(tx_info)) => match tx_info.status { + TxStatus::Processed | TxStatus::Finalized => return Ok(()), + TxStatus::Dropped => bail!("Transaction dropped"), + _ => continue, + }, + Some(Err(e)) => bail!(format!("Received stream error: {e:?}")), + None => bail!("Subscription closed unexpectedly"), + } + } + anyhow::bail!( + "Giving up waiting for target batch to be published after {:?}", + start_wait.elapsed() + ); + } + + fn build_tx_json(&self, call_message: &Value) -> Value { + json!({ + "runtime_call": call_message, + "generation": self.get_generation(), + "details": { + "max_priority_fee_bips": 100, + "max_fee": 100_000_000, + "gas_limit": serde_json::Value::Null, + "chain_id": self.chain_id + } + }) + } + + /// Query the Universale Wallet for the encoded transaction body. + async fn encoded_call_message(&self, call_message: &Value) -> Result { + let schema = Self::fetch_schema(&self.api_url, &self.http_client).await?; + let rtc_index = schema.rollup_expected_index(RollupRoots::RuntimeCall)?; + let bytes = schema.json_to_borsh(rtc_index, &call_message.to_string())?; + + Ok(format!("{bytes:?}")) + } + + async fn sign_tx(&self, mut utx_json: Value) -> Result { + let schema = Self::fetch_schema(&self.api_url, &self.http_client).await?; + let utx_index = schema.rollup_expected_index(RollupRoots::UnsignedTransaction)?; + let mut utx_bytes = schema.json_to_borsh(utx_index, &utx_json.to_string())?; + utx_bytes.extend_from_slice(&self.chain_hash); + + let signature = self.crypto.sign(&utx_bytes); + + if let Some(obj) = utx_json.as_object_mut() { + obj.insert("signature".to_string(), json!({"msg_sig": signature})); + obj.insert( + "pub_key".to_string(), + json!({ + "pub_key": self.crypto.public_key() + }), + ); + } + Ok(utx_json) + } + + async fn serialise_tx(&self, tx_json: &Value) -> Result { + let schema = Self::fetch_schema(&self.api_url, &self.http_client).await?; + let tx_index = schema.rollup_expected_index(RollupRoots::Transaction)?; + let tx_bytes = schema.json_to_borsh(tx_index, &tx_json.to_string())?; + + Ok(BASE64_STANDARD.encode(&tx_bytes)) + } + + async fn submit_tx(&self, tx: String) -> Result { + let url = format!("{}/sequencer/txs", self.api_url); + let resp = self + .http_client + .post(url) + .json(&json!({"body": tx})) + .send() + .await?; + + if !resp.status().is_success() { + let status = resp.status(); + let error_text = resp + .text() + .await + .unwrap_or_else(|_| "Unknown error".to_string()); + bail!("Request failed with status {}: {}", status, error_text); + } + + let parsed_response: serde_json::Value = resp.json().await?; + + let Some(id) = parsed_response + .get("data") + .and_then(|data| data.get("id")) + .and_then(|id| id.as_str()) + else { + bail!("ID not found in response"); + }; + Ok(id.to_string()) + } + + /// Query the rollup REST API for it's schema, in JSON format (used to serialise json transactions into borsh ones) + async fn fetch_schema(api_url: &str, client: &Client) -> Result { + let resp = client + .get(format!("{api_url}/rollup/schema")) + .send() + .await + .context("querying rollup schema")? + .error_for_status()?; + let schema_json: Value = resp.json().await?; + let schema_text = schema_json["data"].to_string(); + + let schema = Schema::from_json(&schema_text).context("parsing rollup schema")?; + Ok(schema) + } + + /// Get the current 'generation' - the timestamp in seconds suffices; + /// # Panics + /// + /// Will panic if system time is before epoch + #[must_use] + fn get_generation(&self) -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs() + } +} diff --git a/rust/main/chains/hyperlane-sovereign/src/universal_wallet_client/tx_state.rs b/rust/main/chains/hyperlane-sovereign/src/universal_wallet_client/tx_state.rs new file mode 100644 index 0000000000..9c0e5135e5 --- /dev/null +++ b/rust/main/chains/hyperlane-sovereign/src/universal_wallet_client/tx_state.rs @@ -0,0 +1,54 @@ +pub type WsSubscription = Result>, WsError>; +use crate::universal_wallet_client::UniversalClient; +use futures::stream::BoxStream; +use futures::StreamExt; +use tokio_tungstenite::connect_async; +use tokio_tungstenite::tungstenite::{Error as WsError, Message}; + +impl UniversalClient { + /// Subscribe to a websocket for status updates. + pub async fn subscribe_to_tx_status_updates( + &self, + tx_hash: String, + ) -> WsSubscription { + self.subscribe_to_ws(&format!("/sequencer/txs/{tx_hash}/ws")) + .await + } + + async fn subscribe_to_ws( + &self, + path: &str, + ) -> WsSubscription { + let url = format!("{}{}", self.api_url, path).replace("http://", "ws://"); + + let (ws, _) = connect_async(url).await?; + + Ok(ws + .filter_map(|msg| async { + match msg { + Ok(Message::Text(text)) => match serde_json::from_str(&text) { + Ok(tx_status) => Some(Ok(tx_status)), + Err(err) => Some(Err(anyhow::anyhow!( + "failed to deserialize JSON {} into type: {}", + text, + err + ))), + }, + Ok(Message::Binary(msg)) => { + tracing::warn!( + ?msg, + "Received unsupported binary message from WebSocket connection" + ); + None + } + // All other kinds of messages are ignored because + // `tokio-tungstenite` ought to handle all + // meta-communication messages (ping, pong, clonse) for us anyway. + Ok(_) => None, + // Errors are not handled here but passed to the caller. + Err(err) => Some(Err(anyhow::anyhow!("{}", err))), + } + }) + .boxed()) + } +} diff --git a/rust/main/chains/hyperlane-sovereign/src/universal_wallet_client/types.rs b/rust/main/chains/hyperlane-sovereign/src/universal_wallet_client/types.rs new file mode 100644 index 0000000000..1db977e1d5 --- /dev/null +++ b/rust/main/chains/hyperlane-sovereign/src/universal_wallet_client/types.rs @@ -0,0 +1,21 @@ +use serde::Deserialize; + +/// Collection of transaction statuses. +#[derive(Deserialize, Debug)] +#[serde(rename_all = "snake_case")] +pub enum TxStatus { + Unknown, + Dropped, + Submitted, + Published, + Processed, + Finalized, +} + +/// Transaction information. +#[derive(Deserialize, Debug)] +pub struct TxInfo { + #[allow(dead_code)] + pub id: String, + pub status: TxStatus, +} diff --git a/rust/main/chains/hyperlane-sovereign/src/universal_wallet_client/utils.rs b/rust/main/chains/hyperlane-sovereign/src/universal_wallet_client/utils.rs new file mode 100644 index 0000000000..9e2dc64279 --- /dev/null +++ b/rust/main/chains/hyperlane-sovereign/src/universal_wallet_client/utils.rs @@ -0,0 +1,128 @@ +use crate::universal_wallet_client::{crypto, UniversalClient}; +use hyperlane_core::{ + Announcement, ChainCommunicationError, ChainResult, Encode, HyperlaneMessage, SignedType, +}; +use serde_json::{json, Value}; +use std::env; +use tokio::fs; + +async fn key_from_key_file(key_file_path: &str) -> ChainResult<[u8; 32]> { + let data = fs::read_to_string(key_file_path).await.map_err(|e| { + ChainCommunicationError::CustomError(format!( + "Failed to read file at {key_file_path}: {e:?}" + )) + })?; + let outer_value: serde_json::Value = serde_json::from_str(&data)?; + let inner_value = outer_value["private_key"]["key_pair"].clone(); + let bytes: [u8; 32] = serde_json::from_value(inner_value)?; + Ok(bytes) +} + +pub async fn get_universal_client(api_url: &str, domain: u32) -> ChainResult { + let key = "TOKEN_KEY_FILE"; + let key_file = env::var(key).map_err(|e| { + ChainCommunicationError::CustomError(format!( + "Environment variable {key} does not exist: {e:?}" + )) + })?; + let key_bytes = key_from_key_file(&key_file).await?; + + let crypto = crypto::Crypto { + private_key: crypto::PrivateKey::Ed25519(key_bytes.into()), + hasher: crypto::Hasher::Sha256, + address_type: crypto::Address::Bech32m { + size_bytes: 28, + hrp: bech32::Hrp::parse("sov").unwrap(), + }, + }; + UniversalClient::new(api_url, crypto.clone(), u64::from(domain)) + .await + .map_err(|e| { + ChainCommunicationError::CustomError(format!( + "Failed to create Universal Client: {e:?}" + )) + }) +} + +pub async fn get_simulate_json_query( + message: &HyperlaneMessage, + metadata: &[u8], + client: &UniversalClient, +) -> ChainResult { + let call_message = json!({ + "mailbox": { + "process": { + "metadata": metadata.to_vec(), + "message": message.to_vec(), + } + }, + }); + + let encoded_call_message = client + .encoded_call_message(&call_message) + .await + .map_err(|e| ChainCommunicationError::CustomError(format!("{e:?}")))?; + + let res = json!( + { + "body":{ + "details":{ + "chain_id":message.destination, + "max_fee":100_000_000, + "max_priority_fee_bips":0 + }, + "encoded_call_message":encoded_call_message, + "nonce":message.nonce, + "generation":0, // get _generation + "sender_pub_key": "\"f8ad2437a279e1c8932c07358c91dc4fe34864a98c6c25f298e2a0199c1509ff\"" + } + } + ); + Ok(res) +} + +pub async fn get_submit_body_string( + message: &HyperlaneMessage, + metadata: &[u8], + client: &UniversalClient, +) -> ChainResult { + let call_message = json!({ + "mailbox": { + "process": { + "metadata": metadata.to_vec(), + "message": message.to_vec(), + } + }, + }); + + let res = client + .build_and_submit(call_message) + .await + .map_err(|e| ChainCommunicationError::CustomError(format!("{e:?}")))?; + + Ok(res.1) +} + +pub async fn announce_validator( + announcement: SignedType, + client: &UniversalClient, +) -> ChainResult { + let sig_hyperlane = announcement.signature; + let sig_bytes: [u8; 65] = sig_hyperlane.into(); + let call_message = json!({ + "mailbox_va": { + "announce": { + "validator_address": announcement.value.validator, + "storage_location": announcement.value.storage_location, + "signature": sig_bytes.to_vec() + } + }, + }); + + let res = client + .build_and_submit(call_message) + .await + .map_err(|e| ChainCommunicationError::CustomError(format!("{e:?}")))?; + + Ok(res.0) +} diff --git a/rust/main/chains/hyperlane-sovereign/src/validator_announce.rs b/rust/main/chains/hyperlane-sovereign/src/validator_announce.rs new file mode 100644 index 0000000000..ac19e22c01 --- /dev/null +++ b/rust/main/chains/hyperlane-sovereign/src/validator_announce.rs @@ -0,0 +1,74 @@ +use crate::{ConnectionConf, Signer, SovereignProvider}; +use async_trait::async_trait; +use hyperlane_core::{ + Announcement, ChainResult, ContractLocator, HyperlaneChain, HyperlaneContract, HyperlaneDomain, + HyperlaneProvider, SignedType, TxOutcome, ValidatorAnnounce, H256, U256, +}; + +/// A reference to a `ValidatorAnnounce` contract on some Sovereign chain. +#[derive(Debug)] +pub struct SovereignValidatorAnnounce { + domain: HyperlaneDomain, + provider: SovereignProvider, + address: H256, +} + +impl SovereignValidatorAnnounce { + /// Create a new Sovereign `ValidatorAnnounce`. + pub async fn new( + conf: &ConnectionConf, + locator: ContractLocator<'_>, + signer: Option, + ) -> ChainResult { + let provider = SovereignProvider::new(locator.domain.clone(), conf, signer).await?; + + Ok(Self { + domain: locator.domain.clone(), + provider, + address: locator.address, + }) + } +} + +impl HyperlaneContract for SovereignValidatorAnnounce { + fn address(&self) -> H256 { + self.address + } +} + +impl HyperlaneChain for SovereignValidatorAnnounce { + fn domain(&self) -> &HyperlaneDomain { + &self.domain + } + + fn provider(&self) -> Box { + Box::new(self.provider.clone()) + } +} + +#[async_trait] +impl ValidatorAnnounce for SovereignValidatorAnnounce { + async fn get_announced_storage_locations( + &self, + validators: &[H256], + ) -> ChainResult>> { + self.provider + .client() + .get_announced_storage_locations(validators) + .await + } + + async fn announce(&self, announcement: SignedType) -> ChainResult { + self.provider.client().announce(announcement).await + } + + async fn announce_tokens_needed( + &self, + _announcement: SignedType, + ) -> Option { + // Possibly required in the future, for now, just return 0 + // let tokens = self.provider.client().announce_tokens_needed().await?; + + Some(U256::zero()) + } +} diff --git a/rust/main/hyperlane-base/Cargo.toml b/rust/main/hyperlane-base/Cargo.toml index ef25d99405..37ef5db032 100644 --- a/rust/main/hyperlane-base/Cargo.toml +++ b/rust/main/hyperlane-base/Cargo.toml @@ -52,6 +52,7 @@ hyperlane-ethereum = { path = "../chains/hyperlane-ethereum" } hyperlane-fuel = { path = "../chains/hyperlane-fuel" } hyperlane-sealevel = { path = "../chains/hyperlane-sealevel" } hyperlane-cosmos = { path = "../chains/hyperlane-cosmos" } +hyperlane-sovereign = { path = "../chains/hyperlane-sovereign" } hyperlane-test = { path = "../hyperlane-test" } # dependency version is determined by etheres diff --git a/rust/main/hyperlane-base/src/contract_sync/cursors/mod.rs b/rust/main/hyperlane-base/src/contract_sync/cursors/mod.rs index 2fb36453e4..1da1afd5d3 100644 --- a/rust/main/hyperlane-base/src/contract_sync/cursors/mod.rs +++ b/rust/main/hyperlane-base/src/contract_sync/cursors/mod.rs @@ -1,6 +1,7 @@ use hyperlane_core::{ Delivery, HyperlaneDomainProtocol, HyperlaneMessage, InterchainGasPayment, MerkleTreeInsertion, }; +use tracing::info; pub(crate) mod sequence_aware; pub(crate) use sequence_aware::ForwardBackwardSequenceAwareSyncCursor; @@ -38,6 +39,10 @@ impl Indexable for HyperlaneMessage { HyperlaneDomainProtocol::Fuel => todo!(), HyperlaneDomainProtocol::Sealevel => CursorType::SequenceAware, HyperlaneDomainProtocol::Cosmos => CursorType::SequenceAware, + HyperlaneDomainProtocol::Sovereign => { + info!("indexing_cursor(HyperlaneMessage domain: HyperlaneDomainProtocol)"); + CursorType::SequenceAware + } } } @@ -58,6 +63,10 @@ impl Indexable for InterchainGasPayment { HyperlaneDomainProtocol::Fuel => todo!(), HyperlaneDomainProtocol::Sealevel => CursorType::SequenceAware, HyperlaneDomainProtocol::Cosmos => CursorType::RateLimited, + HyperlaneDomainProtocol::Sovereign => { + info!("indexing_cursor(InterchainGasPayment domain: HyperlaneDomainProtocol)"); + CursorType::RateLimited + } } } @@ -73,6 +82,10 @@ impl Indexable for MerkleTreeInsertion { HyperlaneDomainProtocol::Fuel => todo!(), HyperlaneDomainProtocol::Sealevel => CursorType::SequenceAware, HyperlaneDomainProtocol::Cosmos => CursorType::SequenceAware, + HyperlaneDomainProtocol::Sovereign => { + info!("indexing_cursor(MerkleTreeInsertion domain: HyperlaneDomainProtocol)"); + CursorType::SequenceAware + } } } @@ -88,6 +101,10 @@ impl Indexable for Delivery { HyperlaneDomainProtocol::Fuel => todo!(), HyperlaneDomainProtocol::Sealevel => CursorType::SequenceAware, HyperlaneDomainProtocol::Cosmos => CursorType::RateLimited, + HyperlaneDomainProtocol::Sovereign => { + info!("indexing_cursor(Delivery domain: HyperlaneDomainProtocol)"); + todo!() + } } } diff --git a/rust/main/hyperlane-base/src/settings/chains.rs b/rust/main/hyperlane-base/src/settings/chains.rs index f50f6ddea0..d267b7d1d8 100644 --- a/rust/main/hyperlane-base/src/settings/chains.rs +++ b/rust/main/hyperlane-base/src/settings/chains.rs @@ -3,6 +3,8 @@ use ethers::prelude::Selector; use h_cosmos::CosmosProvider; use std::{collections::HashMap, sync::Arc}; +use tracing::info; + use eyre::{eyre, Context, Result}; use ethers_prometheus::middleware::{ChainInfo, ContractInfo, PrometheusMiddlewareConf}; @@ -20,6 +22,7 @@ use hyperlane_ethereum::{ }; use hyperlane_fuel as h_fuel; use hyperlane_sealevel as h_sealevel; +use hyperlane_sovereign as h_sovereign; use crate::{ metrics::AgentMetricsConf, @@ -137,6 +140,8 @@ pub enum ChainConnectionConf { Sealevel(h_sealevel::ConnectionConf), /// Cosmos configuration. Cosmos(h_cosmos::ConnectionConf), + /// Sovereign configuration. + Sovereign(h_sovereign::ConnectionConf), } impl ChainConnectionConf { @@ -147,6 +152,7 @@ impl ChainConnectionConf { Self::Fuel(_) => HyperlaneDomainProtocol::Fuel, Self::Sealevel(_) => HyperlaneDomainProtocol::Sealevel, Self::Cosmos(_) => HyperlaneDomainProtocol::Cosmos, + Self::Sovereign(_) => HyperlaneDomainProtocol::Sovereign, } } @@ -156,6 +162,7 @@ impl ChainConnectionConf { Self::Ethereum(conf) => Some(&conf.operation_batch), Self::Cosmos(conf) => Some(&conf.operation_batch), Self::Sealevel(conf) => Some(&conf.operation_batch), + Self::Sovereign(conf) => Some(&conf.operation_batch), _ => None, } } @@ -217,6 +224,11 @@ impl ChainConf { )?; Ok(Box::new(provider) as Box) } + ChainConnectionConf::Sovereign(conf) => { + let provider = + h_sovereign::SovereignProvider::new(locator.domain.clone(), conf, None).await?; + Ok(Box::new(provider) as Box) + } } .context(ctx) } @@ -254,6 +266,14 @@ impl ChainConf { .map(|m| Box::new(m) as Box) .map_err(Into::into) } + ChainConnectionConf::Sovereign(conf) => { + info!("Build Sov mailbox {:?}", conf); + let signer = self.sovereign_signer().await.context(ctx)?; + h_sovereign::SovereignMailbox::new(conf, locator, signer) + .await + .map(|m| Box::new(m) as Box) + .map_err(Into::into) + } } .context(ctx) } @@ -286,6 +306,17 @@ impl ChainConf { Ok(Box::new(hook) as Box) } + ChainConnectionConf::Sovereign(conf) => { + info!("Build Sov merkle_tree_hook"); + let signer = self.sovereign_signer().await.context(ctx)?; + let hook = h_sovereign::SovereignMerkleTreeHook::new( + &conf.clone(), + locator.clone(), + signer, + ) + .await?; + Ok(Box::new(hook) as Box) + } } .context(ctx) } @@ -331,6 +362,18 @@ impl ChainConf { )?); Ok(indexer as Box>) } + ChainConnectionConf::Sovereign(conf) => { + info!( + "build_message_indexer(&self, metrics: &CoreMetrics) {:?}", + conf + ); + let signer = self.sovereign_signer().await.context(ctx)?; + let indexer = Box::new( + h_sovereign::SovereignMailboxIndexer::new(conf.clone(), locator, signer) + .await?, + ); + Ok(indexer as Box>) + } } .context(ctx) } @@ -376,6 +419,7 @@ impl ChainConf { )?); Ok(indexer as Box>) } + ChainConnectionConf::Sovereign(_conf) => todo!(), } .context(ctx) } @@ -415,6 +459,17 @@ impl ChainConf { )?); Ok(paymaster as Box) } + ChainConnectionConf::Sovereign(conf) => { + info!("Build Sov IGP"); + let signer = self.sovereign_signer().await.context(ctx)?; + let igp = h_sovereign::SovereignInterchainGasPaymaster::new( + &conf.clone(), + locator.clone(), + signer, + ) + .await?; + Ok(Box::new(igp) as Box) + } } .context(ctx) } @@ -464,6 +519,14 @@ impl ChainConf { )?); Ok(indexer as Box>) } + ChainConnectionConf::Sovereign(conf) => { + info!("build_interchain_gas_payment_indexer( &self, metrics: &CoreMetrics)"); + let indexer = Box::new( + h_sovereign::SovereignInterchainGasPaymasterIndexer::new(conf.clone(), locator) + .await?, + ); + Ok(indexer as Box>) + } } .context(ctx) } @@ -513,6 +576,16 @@ impl ChainConf { )?); Ok(indexer as Box>) } + ChainConnectionConf::Sovereign(conf) => { + info!("build_merkle_tree_hook_indexer(&self, metrics: &CoreMetrics)"); + let signer = self.sovereign_signer().await.context(ctx)?; + let indexer = Box::new( + h_sovereign::SovereignMerkleTreeHookIndexer::new(conf.clone(), locator, signer) + .await?, + ); + + Ok(indexer as Box>) + } } .context(ctx) } @@ -544,6 +617,14 @@ impl ChainConf { Ok(va as Box) } + ChainConnectionConf::Sovereign(conf) => { + info!("Build Sov validator"); + let signer = self.sovereign_signer().await.context(ctx)?; + let va = Box::new( + h_sovereign::SovereignValidatorAnnounce::new(conf, locator, signer).await?, + ); + Ok(va as Box) + } } .context("Building ValidatorAnnounce") } @@ -585,6 +666,17 @@ impl ChainConf { )?); Ok(ism as Box) } + ChainConnectionConf::Sovereign(conf) => { + info!("Build Sov ISM"); + let signer = self.sovereign_signer().await.context(ctx)?; + let ism = h_sovereign::SovereignInterchainSecurityModule::new( + &conf.clone(), + locator.clone(), + signer, + ) + .await?; + Ok(Box::new(ism) as Box) + } } .context(ctx) } @@ -623,6 +715,13 @@ impl ChainConf { )?); Ok(ism as Box) } + ChainConnectionConf::Sovereign(conf) => { + let signer = self.sovereign_signer().await.context(ctx)?; + let multisign_ism = + h_sovereign::SovereignMultisigIsm::new(&conf.clone(), locator.clone(), signer) + .await?; + Ok(Box::new(multisign_ism) as Box) + } } .context(ctx) } @@ -657,6 +756,14 @@ impl ChainConf { )?); Ok(ism as Box) } + ChainConnectionConf::Sovereign(conf) => { + info!("Build Sov R-ISM"); + let signer = self.sovereign_signer().await.context(ctx)?; + let routing_ism = + h_sovereign::SovereignRoutingIsm::new(&conf.clone(), locator.clone(), signer) + .await?; + Ok(Box::new(routing_ism) as Box) + } } .context(ctx) } @@ -692,6 +799,7 @@ impl ChainConf { Ok(ism as Box) } + ChainConnectionConf::Sovereign(_conf) => todo!(), } .context(ctx) } @@ -720,6 +828,7 @@ impl ChainConf { ChainConnectionConf::Cosmos(_) => { Err(eyre!("Cosmos does not support CCIP read ISM yet")).context(ctx) } + ChainConnectionConf::Sovereign(_conf) => todo!(), } .context(ctx) } @@ -744,6 +853,9 @@ impl ChainConf { Box::new(conf.build::().await?) } ChainConnectionConf::Cosmos(_) => Box::new(conf.build::().await?), + ChainConnectionConf::Sovereign(_) => { + Box::new(conf.build::().await?) + } }; Ok(Some(chain_signer)) } else { @@ -769,6 +881,10 @@ impl ChainConf { self.signer().await } + async fn sovereign_signer(&self) -> Result> { + self.signer().await + } + /// Try to build an agent metrics configuration from the chain config pub async fn agent_metrics_conf(&self, agent_name: String) -> Result { let chain_signer_address = self.chain_signer().await?.map(|s| s.address_string()); diff --git a/rust/main/hyperlane-base/src/settings/mod.rs b/rust/main/hyperlane-base/src/settings/mod.rs index 6c71e20080..7885a24408 100644 --- a/rust/main/hyperlane-base/src/settings/mod.rs +++ b/rust/main/hyperlane-base/src/settings/mod.rs @@ -73,6 +73,7 @@ mod envs { pub use hyperlane_ethereum as h_eth; pub use hyperlane_fuel as h_fuel; pub use hyperlane_sealevel as h_sealevel; + pub use hyperlane_sovereign as h_sovereign; } /// AWS Credentials provider. diff --git a/rust/main/hyperlane-base/src/settings/parser/connection_parser.rs b/rust/main/hyperlane-base/src/settings/parser/connection_parser.rs old mode 100644 new mode 100755 index b3f91ee88e..c57b44efef --- a/rust/main/hyperlane-base/src/settings/parser/connection_parser.rs +++ b/rust/main/hyperlane-base/src/settings/parser/connection_parser.rs @@ -379,5 +379,11 @@ pub fn build_connection_conf( HyperlaneDomainProtocol::Cosmos => { build_cosmos_connection_conf(rpcs, chain, err, operation_batch) } + HyperlaneDomainProtocol::Sovereign => rpcs.iter().next().map(|url| { + ChainConnectionConf::Sovereign(h_sovereign::ConnectionConf { + url: url.clone(), + operation_batch, + }) + }), } } diff --git a/rust/main/hyperlane-base/src/settings/parser/mod.rs b/rust/main/hyperlane-base/src/settings/parser/mod.rs old mode 100644 new mode 100755 index d176ad857b..ce8cdca525 --- a/rust/main/hyperlane-base/src/settings/parser/mod.rs +++ b/rust/main/hyperlane-base/src/settings/parser/mod.rs @@ -166,6 +166,7 @@ fn parse_chain( .and_then(|d| match d.domain_protocol() { HyperlaneDomainProtocol::Ethereum => Some(IndexMode::Block), HyperlaneDomainProtocol::Sealevel => Some(IndexMode::Sequence), + HyperlaneDomainProtocol::Sovereign => Some(IndexMode::Block), _ => None, }) .unwrap_or_default() diff --git a/rust/main/hyperlane-base/src/settings/signers.rs b/rust/main/hyperlane-base/src/settings/signers.rs index 4233a5c692..303e649b61 100644 --- a/rust/main/hyperlane-base/src/settings/signers.rs +++ b/rust/main/hyperlane-base/src/settings/signers.rs @@ -166,3 +166,20 @@ impl ChainSigner for hyperlane_cosmos::Signer { self.address.clone() } } + +#[async_trait] +impl BuildableWithSignerConf for hyperlane_sovereign::Signer { + async fn build(conf: &SignerConf) -> Result { + if let SignerConf::HexKey { key } = conf { + Ok(hyperlane_sovereign::Signer::new(key)?) + } else { + bail!(format!("{conf:?} key is not supported by Sovereign")); + } + } +} + +impl ChainSigner for hyperlane_sovereign::Signer { + fn address_string(&self) -> String { + self.address.clone() + } +} diff --git a/rust/main/hyperlane-core/src/chain.rs b/rust/main/hyperlane-core/src/chain.rs old mode 100644 new mode 100755 index 97637fafa5..8e7bf3d6e4 --- a/rust/main/hyperlane-core/src/chain.rs +++ b/rust/main/hyperlane-core/src/chain.rs @@ -276,6 +276,8 @@ pub enum HyperlaneDomainProtocol { Sealevel, /// A Cosmos-based chain type which uses hyperlane-cosmos. Cosmos, + /// A Sovereign-based chain type which uses hyperlane-sovereign. + Sovereign, } impl HyperlaneDomainProtocol { @@ -286,6 +288,7 @@ impl HyperlaneDomainProtocol { Fuel => format!("{:?}", addr), Sealevel => format!("{:?}", addr), Cosmos => format!("{:?}", addr), + Sovereign => format!("{addr:?}"), } } } @@ -592,7 +595,7 @@ impl HyperlaneDomain { use HyperlaneDomainProtocol::*; let protocol = self.domain_protocol(); many_to_one!(match protocol { - IndexMode::Block: [Ethereum, Cosmos], + IndexMode::Block: [Ethereum, Cosmos, Sovereign], IndexMode::Sequence : [Sealevel, Fuel], }) } diff --git a/rust/sealevel/Cargo.toml b/rust/sealevel/Cargo.toml index 145b9bfcc2..c0ea4a34c1 100644 --- a/rust/sealevel/Cargo.toml +++ b/rust/sealevel/Cargo.toml @@ -93,7 +93,7 @@ regex = "1.5" reqwest = "0.11" ripemd = "0.1.3" rlp = "=0.5.2" -rocksdb = "0.21.0" +rocksdb = "0.22.0" sea-orm = { version = "0.11.1", features = [ "sqlx-postgres", "runtime-tokio-native-tls",