diff --git a/.gitignore b/.gitignore index 4b76dfb8..fe072ce2 100644 --- a/.gitignore +++ b/.gitignore @@ -65,6 +65,11 @@ yarn-error.log* lerna-debug.log* .pnpm-debug.log* +### Solana ### +# Logs +.anchor +test-ledger + ### Soroban ### .soroban test_snapshots diff --git a/.gitmodules b/.gitmodules index 8eba4d08..ac832894 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,9 +16,3 @@ [submodule "contracts/evm/lib/solidity-examples"] path = contracts/evm/lib/solidity-examples url = https://github.com/LayerZero-Labs/solidity-examples -[submodule "contracts/evm/lib/openzeppelin-contracts"] - path = contracts/evm/lib/openzeppelin-contracts - url = https://github.com/OpenZeppelin/openzeppelin-contracts -[submodule "contracts/evm/lib/openzeppelin-contracts-upgradeable"] - path = contracts/evm/lib/openzeppelin-contracts-upgradeable - url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable diff --git a/Cargo.lock b/Cargo.lock index ae3ea0de..36dc3585 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "ahash" version = "0.7.6" @@ -19,12 +34,30 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arrayvec" version = "0.7.4" @@ -37,6 +70,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + [[package]] name = "base16ct" version = "0.1.1" @@ -49,6 +97,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base32" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" + [[package]] name = "base64" version = "0.11.0" @@ -67,6 +121,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.6.0" @@ -154,6 +214,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "byte-slice-cast" version = "1.2.2" @@ -175,12 +241,31 @@ dependencies = [ "serde", ] +[[package]] +name = "bytes-lit" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0adabf37211a5276e46335feabcbb1530c95eb3fdf85f324c7db942770aa025d" +dependencies = [ + "num-bigint", + "proc-macro2 1.0.70", + "quote 1.0.33", + "syn 2.0.42", +] + [[package]] name = "cc" version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +[[package]] +name = "centralized-connection" +version = "0.0.0" +dependencies = [ + "soroban-sdk", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -194,7 +279,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" dependencies = [ "android-tzdata", + "iana-time-zone", "num-traits", + "serde", + "winapi", ] [[package]] @@ -211,7 +299,7 @@ dependencies = [ "displaydoc", "dyn-clone", "hex", - "hex-literal", + "hex-literal 0.3.4", "ibc-proto", "ics23", "pbjson", @@ -224,7 +312,7 @@ dependencies = [ "serde", "serde-json-wasm 0.5.2", "serde_json", - "sha2 0.10.6", + "sha2 0.10.8", "sha3", "subtle-encoding", "tendermint", @@ -237,6 +325,12 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913" +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cosmwasm" version = "0.7.2" @@ -314,7 +408,7 @@ dependencies = [ "schemars 0.8.12", "serde", "serde-json-wasm 0.5.2", - "sha2 0.10.6", + "sha2 0.10.8", "static_assertions", "thiserror", ] @@ -338,6 +432,17 @@ dependencies = [ "libc", ] +[[package]] +name = "crate-git-revision" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c521bf1f43d31ed2f73441775ed31935d77901cb3451e44b38a1c1612fcbaf98" +dependencies = [ + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "crunchy" version = "0.2.2" @@ -378,6 +483,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "ctor" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" +dependencies = [ + "quote 1.0.33", + "syn 2.0.42", +] + [[package]] name = "curve25519-dalek" version = "3.2.0" @@ -391,6 +506,33 @@ dependencies = [ "zeroize", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2 1.0.70", + "quote 1.0.33", + "syn 2.0.42", +] + [[package]] name = "curve25519-dalek-ng" version = "4.1.1" @@ -542,7 +684,7 @@ dependencies = [ "cw-storage-plus 0.15.1", "cw-utils 0.15.1", "derivative", - "itertools", + "itertools 0.10.5", "prost 0.9.0", "schemars 0.8.12", "serde", @@ -560,7 +702,7 @@ dependencies = [ "cw-storage-plus 1.0.1", "cw-utils 1.0.1", "derivative", - "itertools", + "itertools 0.10.5", "k256 0.11.6", "prost 0.9.0", "schemars 0.8.12", @@ -788,6 +930,41 @@ dependencies = [ "serde", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2 1.0.70", + "quote 1.0.33", + "strsim", + "syn 2.0.42", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote 1.0.33", + "syn 2.0.42", +] + [[package]] name = "debug_print" version = "1.0.0" @@ -825,6 +1002,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2 1.0.70", + "quote 1.0.33", + "syn 2.0.42", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -874,6 +1062,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "dyn-clone" version = "1.0.11" @@ -915,6 +1109,16 @@ dependencies = [ "signature 1.6.4", ] +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8 0.10.2", + "signature 2.2.0", +] + [[package]] name = "ed25519-consensus" version = "2.1.0" @@ -928,13 +1132,28 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek 4.1.3", + "ed25519 2.2.3", + "rand_core 0.6.4", + "serde", + "sha2 0.10.8", + "subtle", + "zeroize", +] + [[package]] name = "ed25519-zebra" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c24f403d068ad0b359e577a77f92392118be3f3c927538f2bb544a5ecd828c6" dependencies = [ - "curve25519-dalek", + "curve25519-dalek 3.2.0", "hashbrown 0.12.3", "hex", "rand_core 0.6.4", @@ -1024,6 +1243,18 @@ dependencies = [ "libc", ] +[[package]] +name = "escape-bytes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfcf67fea2815c2fc3b90873fae90957be12ff417335dfadc7f52927feb03b2" + +[[package]] +name = "ethnum" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b90ca2580b73ab6a1f724b76ca11ab632df820fd6040c336200d2c1df7b3c82c" + [[package]] name = "fastrand" version = "1.9.0" @@ -1053,6 +1284,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "fixed-hash" version = "0.8.0" @@ -1077,6 +1314,12 @@ dependencies = [ "paste", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "forward_ref" version = "1.0.0" @@ -1157,15 +1400,23 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.9" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + [[package]] name = "group" version = "0.12.1" @@ -1220,6 +1471,9 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] [[package]] name = "hex-buffer-serde" @@ -1237,6 +1491,12 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + [[package]] name = "hmac" version = "0.12.1" @@ -1246,6 +1506,29 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "ibc" version = "0.32.0" @@ -1269,7 +1552,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "sha2 0.10.6", + "sha2 0.10.8", "subtle-encoding", "tendermint", "tendermint-light-client-verifier", @@ -1308,10 +1591,16 @@ dependencies = [ "hex", "prost 0.11.9", "ripemd", - "sha2 0.10.6", + "sha2 0.10.8", "sha3", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "impl-serde" version = "0.4.0" @@ -1340,6 +1629,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] @@ -1350,8 +1640,15 @@ checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", "hashbrown 0.14.3", + "serde", ] +[[package]] +name = "indexmap-nostd" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e04e2fd2b8188ea827b32ef11de88377086d690286ab35747ef7f9bf3ccb590" + [[package]] name = "instant" version = "0.1.12" @@ -1381,12 +1678,30 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "k256" version = "0.11.6" @@ -1396,7 +1711,7 @@ dependencies = [ "cfg-if", "ecdsa 0.14.8", "elliptic-curve 0.12.3", - "sha2 0.10.6", + "sha2 0.10.8", ] [[package]] @@ -1409,7 +1724,7 @@ dependencies = [ "ecdsa 0.16.9", "elliptic-curve 0.13.8", "once_cell", - "sha2 0.10.6", + "sha2 0.10.8", "signature 2.2.0", ] @@ -1430,9 +1745,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.145" +version = "0.2.159" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" + +[[package]] +name = "libm" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc86cde3ff845662b8f4ef6cb50ea0e20c524eb3d29ae048287e06a1b3fa6a81" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "linux-raw-sys" @@ -1452,12 +1773,41 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mock-dapp-multi" +version = "0.0.0" +dependencies = [ + "soroban-rlp", + "soroban-sdk", + "soroban-xcall-lib", + "xcall", +] + [[package]] name = "multimap" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-derive" version = "0.3.3" @@ -1469,15 +1819,44 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2 1.0.70", + "quote 1.0.33", + "syn 2.0.42", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.18.0" @@ -1490,6 +1869,18 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", + "primeorder", + "sha2 0.10.8", +] + [[package]] name = "parity-scale-codec" version = "3.6.9" @@ -1537,7 +1928,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bdbb7b706f2afc610f3853550cdbbf6372fd324824a087806bd4480ea4996e24" dependencies = [ "heck", - "itertools", + "itertools 0.10.5", "prost 0.11.9", "prost-types", ] @@ -1600,15 +1991,43 @@ dependencies = [ ] [[package]] -name = "prettyplease" -version = "0.1.25" +name = "ppv-lite86" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ - "proc-macro2 1.0.70", + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86" +dependencies = [ + "proc-macro2 1.0.70", "syn 1.0.109", ] +[[package]] +name = "prettyplease" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" +dependencies = [ + "proc-macro2 1.0.70", + "syn 2.0.42", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve 0.13.8", +] + [[package]] name = "primitive-types" version = "0.12.2" @@ -1695,12 +2114,12 @@ checksum = "119533552c9a7ffacc21e099c24a0ac8bb19c2a2a3f363de84cd9b844feab270" dependencies = [ "bytes", "heck", - "itertools", + "itertools 0.10.5", "lazy_static", "log", "multimap", "petgraph", - "prettyplease", + "prettyplease 0.1.25", "prost 0.11.9", "prost-types", "regex", @@ -1716,7 +2135,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" dependencies = [ "anyhow", - "itertools", + "itertools 0.10.5", "proc-macro2 1.0.70", "quote 1.0.33", "syn 1.0.109", @@ -1729,7 +2148,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" dependencies = [ "anyhow", - "itertools", + "itertools 0.10.5", "proc-macro2 1.0.70", "quote 1.0.33", "syn 1.0.109", @@ -1762,6 +2181,27 @@ dependencies = [ "proc-macro2 1.0.70", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -1842,12 +2282,27 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + [[package]] name = "rustc-hex" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.37.19" @@ -2097,9 +2552,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "cb0652c533506ad7a2e353cce269330d6afd8bdfb6d75e0ace5b35aacbd7b9e9" dependencies = [ "itoa", "ryu", @@ -2117,6 +2572,36 @@ dependencies = [ "syn 2.0.42", ] +[[package]] +name = "serde_with" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.1.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" +dependencies = [ + "darling", + "proc-macro2 1.0.70", + "quote 1.0.33", + "syn 2.0.42", +] + [[package]] name = "sha2" version = "0.9.9" @@ -2132,9 +2617,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", @@ -2171,6 +2656,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + [[package]] name = "snafu" version = "0.5.0" @@ -2192,6 +2683,210 @@ dependencies = [ "syn 0.15.44", ] +[[package]] +name = "soroban-builtin-sdk-macros" +version = "21.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f57a68ef8777e28e274de0f3a88ad9a5a41d9a2eb461b4dd800b086f0e83b80" +dependencies = [ + "itertools 0.11.0", + "proc-macro2 1.0.70", + "quote 1.0.33", + "syn 2.0.42", +] + +[[package]] +name = "soroban-env-common" +version = "21.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1c89463835fe6da996318156d39f424b4f167c725ec692e5a7a2d4e694b3d" +dependencies = [ + "arbitrary", + "crate-git-revision", + "ethnum", + "num-derive 0.4.2", + "num-traits", + "serde", + "soroban-env-macros", + "soroban-wasmi", + "static_assertions", + "stellar-xdr", + "wasmparser", +] + +[[package]] +name = "soroban-env-guest" +version = "21.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bfb2536811045d5cd0c656a324cbe9ce4467eb734c7946b74410d90dea5d0ce" +dependencies = [ + "soroban-env-common", + "static_assertions", +] + +[[package]] +name = "soroban-env-host" +version = "21.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b7a32c28f281c423189f1298960194f0e0fc4eeb72378028171e556d8cd6160" +dependencies = [ + "backtrace", + "curve25519-dalek 4.1.3", + "ecdsa 0.16.9", + "ed25519-dalek", + "elliptic-curve 0.13.8", + "generic-array", + "getrandom", + "hex-literal 0.4.1", + "hmac", + "k256 0.13.1", + "num-derive 0.4.2", + "num-integer", + "num-traits", + "p256", + "rand", + "rand_chacha", + "sec1 0.7.3", + "sha2 0.10.8", + "sha3", + "soroban-builtin-sdk-macros", + "soroban-env-common", + "soroban-wasmi", + "static_assertions", + "stellar-strkey", + "wasmparser", +] + +[[package]] +name = "soroban-env-macros" +version = "21.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "242926fe5e0d922f12d3796cd7cd02dd824e5ef1caa088f45fce20b618309f64" +dependencies = [ + "itertools 0.11.0", + "proc-macro2 1.0.70", + "quote 1.0.33", + "serde", + "serde_json", + "stellar-xdr", + "syn 2.0.42", +] + +[[package]] +name = "soroban-ledger-snapshot" +version = "21.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84589856911dfd6731695c9b51c858aed6d4540118c0a1e5c4c858ea13bc744c" +dependencies = [ + "serde", + "serde_json", + "serde_with", + "soroban-env-common", + "soroban-env-host", + "thiserror", +] + +[[package]] +name = "soroban-rlp" +version = "0.1.0" +dependencies = [ + "soroban-sdk", +] + +[[package]] +name = "soroban-sdk" +version = "21.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b888f866ce621257311dd779ea49280ce29faa5ee37f22270ba7c32a8fe25d" +dependencies = [ + "arbitrary", + "bytes-lit", + "ctor", + "ed25519-dalek", + "rand", + "serde", + "serde_json", + "soroban-env-guest", + "soroban-env-host", + "soroban-ledger-snapshot", + "soroban-sdk-macros", + "stellar-strkey", +] + +[[package]] +name = "soroban-sdk-macros" +version = "21.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63c2173f1aacd56b4405eed71cb2a9694dff99d51ba72d4f0cbc5e4961fdabf4" +dependencies = [ + "crate-git-revision", + "darling", + "itertools 0.11.0", + "proc-macro2 1.0.70", + "quote 1.0.33", + "rustc_version", + "sha2 0.10.8", + "soroban-env-common", + "soroban-spec", + "soroban-spec-rust", + "stellar-xdr", + "syn 2.0.42", +] + +[[package]] +name = "soroban-spec" +version = "21.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7705bffbcc747c08e81698b87b4a787f8b268c25d88f777160091dc1ee8121cb" +dependencies = [ + "base64 0.13.1", + "stellar-xdr", + "thiserror", + "wasmparser", +] + +[[package]] +name = "soroban-spec-rust" +version = "21.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48207ebc8616c2804a17203d1d86c53c3d3c804b682cbab011a135893db1cf78" +dependencies = [ + "prettyplease 0.2.15", + "proc-macro2 1.0.70", + "quote 1.0.33", + "sha2 0.10.8", + "soroban-spec", + "stellar-xdr", + "syn 2.0.42", + "thiserror", +] + +[[package]] +name = "soroban-wasmi" +version = "0.31.1-soroban.20.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710403de32d0e0c35375518cb995d4fc056d0d48966f2e56ea471b8cb8fc9719" +dependencies = [ + "smallvec", + "spin", + "wasmi_arena", + "wasmi_core", + "wasmparser-nostd", +] + +[[package]] +name = "soroban-xcall-lib" +version = "0.1.0" +dependencies = [ + "soroban-sdk", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "spki" version = "0.6.0" @@ -2218,6 +2913,39 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stellar-strkey" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12d2bf45e114117ea91d820a846fd1afbe3ba7d717988fee094ce8227a3bf8bd" +dependencies = [ + "base32", + "crate-git-revision", + "thiserror", +] + +[[package]] +name = "stellar-xdr" +version = "21.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2675a71212ed39a806e415b0dbf4702879ff288ec7f5ee996dda42a135512b50" +dependencies = [ + "arbitrary", + "base64 0.13.1", + "crate-git-revision", + "escape-bytes", + "hex", + "serde", + "serde_with", + "stellar-strkey", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "strum" version = "0.25.0" @@ -2312,7 +3040,7 @@ checksum = "cda53c85447577769cdfc94c10a56f34afef2c00e4108badb57fce6b1a0c75eb" dependencies = [ "bytes", "digest 0.10.7", - "ed25519", + "ed25519 1.5.3", "ed25519-consensus", "flex-error", "futures", @@ -2324,7 +3052,7 @@ dependencies = [ "serde_bytes", "serde_json", "serde_repr", - "sha2 0.10.6", + "sha2 0.10.8", "signature 1.6.4", "subtle", "subtle-encoding", @@ -2354,7 +3082,7 @@ checksum = "c943f78c929cdf14553842f705f2c30324bc35b9179caaa5c9b80620f60652e6" dependencies = [ "bytes", "flex-error", - "num-derive", + "num-derive 0.3.3", "num-traits", "prost 0.11.9", "prost-types", @@ -2373,7 +3101,7 @@ dependencies = [ "cosmwasm-std", "cw-multi-test 0.16.4", "hex", - "hex-literal", + "hex-literal 0.3.4", "ibc-proto", "prost 0.11.9", "serde", @@ -2406,6 +3134,8 @@ version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" dependencies = [ + "itoa", + "serde", "time-core", "time-macros", ] @@ -2526,6 +3256,98 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2 1.0.70", + "quote 1.0.33", + "syn 2.0.42", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +dependencies = [ + "quote 1.0.33", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +dependencies = [ + "proc-macro2 1.0.70", + "quote 1.0.33", + "syn 2.0.42", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + +[[package]] +name = "wasmi_arena" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "104a7f73be44570cac297b3035d76b169d6599637631cf37a1703326a0727073" + +[[package]] +name = "wasmi_core" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf1a7db34bff95b85c261002720c00c3a6168256dcb93041d3fa2054d19856a" +dependencies = [ + "downcast-rs", + "libm", + "num-traits", + "paste", +] + +[[package]] +name = "wasmparser" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a58e28b80dd8340cb07b8242ae654756161f6fc8d0038123d679b7b99964fa50" +dependencies = [ + "indexmap 2.1.0", + "semver", +] + +[[package]] +name = "wasmparser-nostd" +version = "0.100.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5a015fe95f3504a94bb1462c717aae75253e39b9dd6c3fb1062c934535c64aa" +dependencies = [ + "indexmap-nostd", +] + [[package]] name = "which" version = "4.4.0" @@ -2537,6 +3359,37 @@ dependencies = [ "once_cell", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -2585,6 +3438,22 @@ dependencies = [ "windows_x86_64_msvc 0.48.0", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -2597,6 +3466,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -2609,6 +3484,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -2621,6 +3502,18 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -2633,6 +3526,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -2645,6 +3544,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -2657,6 +3562,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -2669,6 +3580,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "winnow" version = "0.5.30" @@ -2678,6 +3595,36 @@ dependencies = [ "memchr", ] +[[package]] +name = "xcall" +version = "0.1.0" +dependencies = [ + "soroban-rlp", + "soroban-sdk", + "soroban-xcall-lib", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2 1.0.70", + "quote 1.0.33", + "syn 2.0.42", +] + [[package]] name = "zeroize" version = "1.8.1" diff --git a/contracts/solana/Anchor.toml b/contracts/solana/Anchor.toml new file mode 100644 index 00000000..be65dc86 --- /dev/null +++ b/contracts/solana/Anchor.toml @@ -0,0 +1,27 @@ +[toolchain] + +[features] +resolution = false +skip-lint = false + +[programs.localnet] +centralized-connection = "8oxnXrSmqWJqkb2spZk2uz1cegzPsLy6nJp9XwFhkMD5" +mock-dapp-multi = "hSruQVdc5a9dUAqHfRaLcn2S9cbgdpoomG5eWhhDS5W" +xcall = "6PDxNoATKJpTdkEEiiKQucnhUJYvhZg3BNVckyV8sS1q" + +[registry] +url = "https://api.apr.dev" + +[provider] +cluster = "Localnet" +wallet = "~/.config/solana/id.json" + +[scripts] +add-connection-mock-dapp = "yarn ts-node --project ./tsconfig.json ./scripts/mock-dapp-multi/add-connection.ts" +initialize-centralized = "yarn ts-node --project ./tsconfig.json ./scripts/centralized-connection/initialize.ts" +initialize-mock-dapp-multi = "yarn ts-node --project ./tsconfig.json ./scripts/mock-dapp-multi/initialize.ts" +initialize-xcall = "yarn ts-node --project ./tsconfig.json ./scripts/xcall/initialize.ts" +set-admin-xcall = "yarn ts-node --project ./tsconfig.json ./scripts/xcall/set-admin.ts" +set-protocol-fee-handler-xcall = "yarn ts-node --project ./tsconfig.json ./scripts/xcall/set-protocol-fee-handler.ts" +set-protocol-fee-xcall = "yarn ts-node --project ./tsconfig.json ./scripts/xcall/set-protocol-fee.ts" +test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" diff --git a/contracts/solana/Cargo.lock b/contracts/solana/Cargo.lock new file mode 100644 index 00000000..8ce7dd91 --- /dev/null +++ b/contracts/solana/Cargo.lock @@ -0,0 +1,2042 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.15", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anchor-attribute-access-control" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47fe28365b33e8334dd70ae2f34a43892363012fe239cf37d2ee91693575b1f8" +dependencies = [ + "anchor-syn", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-account" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c288d496168268d198d9b53ee9f4f9d260a55ba4df9877ea1d4486ad6109e0f" +dependencies = [ + "anchor-syn", + "bs58 0.5.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-constant" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49b77b6948d0eeaaa129ce79eea5bbbb9937375a9241d909ca8fb9e006bb6e90" +dependencies = [ + "anchor-syn", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-error" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d20bb569c5a557c86101b944721d865e1fd0a4c67c381d31a44a84f07f84828" +dependencies = [ + "anchor-syn", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-event" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cebd8d0671a3a9dc3160c48598d652c34c77de6be4d44345b8b514323284d57" +dependencies = [ + "anchor-syn", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-program" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efb2a5eb0860e661ab31aff7bb5e0288357b176380e985bade4ccb395981b42d" +dependencies = [ + "anchor-lang-idl", + "anchor-syn", + "anyhow", + "bs58 0.5.1", + "heck", + "proc-macro2", + "quote", + "serde_json", + "syn 1.0.109", +] + +[[package]] +name = "anchor-derive-accounts" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04368b5abef4266250ca8d1d12f4dff860242681e4ec22b885dcfe354fd35aa1" +dependencies = [ + "anchor-syn", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-derive-serde" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0bb0e0911ad4a70cab880cdd6287fe1e880a1a9d8e4e6defa8e9044b9796a6c" +dependencies = [ + "anchor-syn", + "borsh-derive-internal 0.10.3", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-derive-space" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef415ff156dc82e9ecb943189b0cb241b3a6bfc26a180234dc21bd3ef3ce0cb" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-lang" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6620c9486d9d36a4389cab5e37dc34a42ed0bfaa62e6a75a2999ce98f8f2e373" +dependencies = [ + "anchor-attribute-access-control", + "anchor-attribute-account", + "anchor-attribute-constant", + "anchor-attribute-error", + "anchor-attribute-event", + "anchor-attribute-program", + "anchor-derive-accounts", + "anchor-derive-serde", + "anchor-derive-space", + "anchor-lang-idl", + "arrayref", + "base64 0.21.7", + "bincode", + "borsh 0.10.3", + "bytemuck", + "getrandom 0.2.15", + "solana-program", + "thiserror", +] + +[[package]] +name = "anchor-lang-idl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31cf97b4e6f7d6144a05e435660fcf757dbc3446d38d0e2b851d11ed13625bba" +dependencies = [ + "anchor-lang-idl-spec", + "anyhow", + "heck", + "regex", + "serde", + "serde_json", + "sha2 0.10.8", +] + +[[package]] +name = "anchor-lang-idl-spec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bdf143115440fe621bdac3a29a1f7472e09f6cd82b2aa569429a0c13f103838" +dependencies = [ + "anyhow", + "serde", +] + +[[package]] +name = "anchor-syn" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f99daacb53b55cfd37ce14d6c9905929721137fd4c67bbab44a19802aecb622f" +dependencies = [ + "anyhow", + "bs58 0.5.1", + "cargo_toml", + "heck", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.8", + "syn 1.0.109", + "thiserror", +] + +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "ark-bn254" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a22f4561524cd949590d78d7d4c5df8f592430d221f7f3c9497bbafd8972120f" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-std", +] + +[[package]] +name = "ark-ec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defd9a439d56ac24968cca0571f598a61bc8c55f71d50a89cda591cb750670ba" +dependencies = [ + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "derivative", + "hashbrown 0.13.2", + "itertools", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", + "derivative", + "digest 0.10.7", + "itertools", + "num-bigint", + "num-traits", + "paste", + "rustc_version", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-poly" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d320bfc44ee185d899ccbadfa8bc31aab923ce1558716e1997a1e74057fe86bf" +dependencies = [ + "ark-ff", + "ark-serialize", + "ark-std", + "derivative", + "hashbrown 0.13.2", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-serialize-derive", + "ark-std", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae3281bc6d0fd7e549af32b52511e1302185bd688fd3359fa36423346ff682ea" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "arrayref" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] + +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + +[[package]] +name = "blake3" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cca6d3674597c30ddf2c587bf8d9d65c9a84d2326d941cc79c9842dfe0ef52" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "digest 0.10.7", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "borsh" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa" +dependencies = [ + "borsh-derive 0.9.3", + "hashbrown 0.11.2", +] + +[[package]] +name = "borsh" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" +dependencies = [ + "borsh-derive 0.10.3", + "hashbrown 0.13.2", +] + +[[package]] +name = "borsh" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6362ed55def622cddc70a4746a68554d7b687713770de539e59a739b249f8ed" +dependencies = [ + "borsh-derive 1.5.1", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6441c552f230375d18e3cc377677914d2ca2b0d36e52129fe15450a2dce46775" +dependencies = [ + "borsh-derive-internal 0.9.3", + "borsh-schema-derive-internal 0.9.3", + "proc-macro-crate 0.1.5", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "borsh-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0754613691538d51f329cce9af41d7b7ca150bc973056f1156611489475f54f7" +dependencies = [ + "borsh-derive-internal 0.10.3", + "borsh-schema-derive-internal 0.10.3", + "proc-macro-crate 0.1.5", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "borsh-derive" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b" +dependencies = [ + "once_cell", + "proc-macro-crate 3.1.0", + "proc-macro2", + "quote", + "syn 2.0.75", + "syn_derive", +] + +[[package]] +name = "borsh-derive-internal" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "borsh-derive-internal" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb438156919598d2c7bad7e1c0adf3d26ed3840dbc010db1a882a65583ca2fb" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634205cc43f74a1b9046ef87c4540ebda95696ec0f315024860cad7c5b0f5ccd" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bs58" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8834bb1d8ee5dc048ee3124f2c7c1afcc6bc9aed03f11e9dfd8c69470a5db340" +dependencies = [ + "feature-probe", + "serde", +] + +[[package]] +name = "bytemuck" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fd4c6dcc3b0aea2f5c0b4b82c2b15fe39ddbc76041a310848f4706edf76bb31" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc8b54b395f2fcfbb3d90c47b01c7f444d94d05bdeb775811dec868ac3bbc26" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.75", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" + +[[package]] +name = "cargo_toml" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a98356df42a2eb1bd8f1793ae4ee4de48e384dd974ce5eac8eee802edb7492be" +dependencies = [ + "serde", + "toml 0.8.19", +] + +[[package]] +name = "cc" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "centralized-connection" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "xcall-lib", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "console_log" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89f72f65e8501878b8a004d5a1afb780987e2ce2b4532c562e367a72c57499f" +dependencies = [ + "log", + "web-sys", +] + +[[package]] +name = "constant_time_eq" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" + +[[package]] +name = "cpufeatures" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f9d052967f590a76e62eb387bd0bbb1b000182c3cefe5364db6b7211651bc0" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common", + "subtle", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "feature-probe" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "serde", + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash 0.8.11", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + +[[package]] +name = "hmac-drbg" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" +dependencies = [ + "digest 0.9.0", + "generic-array", + "hmac", +] + +[[package]] +name = "im" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9" +dependencies = [ + "bitmaps", + "rand_core 0.6.4", + "rand_xoshiro", + "rayon", + "serde", + "sized-chunks", + "typenum", + "version_check", +] + +[[package]] +name = "indexmap" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +dependencies = [ + "equivalent", + "hashbrown 0.14.5", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "libsecp256k1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9d220bc1feda2ac231cb78c3d26f27676b8cf82c96971f7aeef3d0cf2797c73" +dependencies = [ + "arrayref", + "base64 0.12.3", + "digest 0.9.0", + "hmac-drbg", + "libsecp256k1-core", + "libsecp256k1-gen-ecmult", + "libsecp256k1-gen-genmult", + "rand 0.7.3", + "serde", + "sha2 0.9.9", + "typenum", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f6ab710cec28cef759c5f18671a27dae2a5f952cdaaee1d8e2908cb2478a80" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccab96b584d38fac86a83f07e659f0deafd0253dc096dab5a36d53efe653c5c3" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67abfe149395e3aa1c48a2beb32b068e2334402df8181f818d3aee2b304c4f5d" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "light-poseidon" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c9a85a9752c549ceb7578064b4ed891179d20acd85f27318573b64d2d7ee7ee" +dependencies = [ + "ark-bn254", + "ark-ff", + "num-bigint", + "thiserror", +] + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memmap2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mock-dapp-multi" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "rlp", + "xcall-lib", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.75", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pbkdf2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216eaa586a190f0a738f2f918511eecfa90f13295abec0e457cdebcceda80cbd" +dependencies = [ + "crypto-mac", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml 0.5.11", +] + +[[package]] +name = "proc-macro-crate" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +dependencies = [ + "toml_edit 0.21.1", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "rlp" +version = "0.1.0" +dependencies = [ + "bytes", + "hex", + "rustc-hex", + "serde", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.208" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.208" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.75", +] + +[[package]] +name = "serde_json" +version = "1.0.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +dependencies = [ + "serde", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "solana-frozen-abi" +version = "1.18.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20a6ef2db80dceb124b7bf81cca3300804bf427d2711973fc3df450ed7dfb26d" +dependencies = [ + "block-buffer 0.10.4", + "bs58 0.4.0", + "bv", + "either", + "generic-array", + "im", + "lazy_static", + "log", + "memmap2", + "rustc_version", + "serde", + "serde_bytes", + "serde_derive", + "sha2 0.10.8", + "solana-frozen-abi-macro", + "subtle", + "thiserror", +] + +[[package]] +name = "solana-frozen-abi-macro" +version = "1.18.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70088de7d4067d19a7455609e2b393e6086bd847bb39c4d2bf234fc14827ef9e" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.75", +] + +[[package]] +name = "solana-program" +version = "1.18.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb2b2c8babfae4cace1a25b6efa00418f3acd852cf55d7cecc0360d3c5050479" +dependencies = [ + "ark-bn254", + "ark-ec", + "ark-ff", + "ark-serialize", + "base64 0.21.7", + "bincode", + "bitflags", + "blake3", + "borsh 0.10.3", + "borsh 0.9.3", + "borsh 1.5.1", + "bs58 0.4.0", + "bv", + "bytemuck", + "cc", + "console_error_panic_hook", + "console_log", + "curve25519-dalek", + "getrandom 0.2.15", + "itertools", + "js-sys", + "lazy_static", + "libc", + "libsecp256k1", + "light-poseidon", + "log", + "memoffset", + "num-bigint", + "num-derive", + "num-traits", + "parking_lot", + "rand 0.8.5", + "rustc_version", + "rustversion", + "serde", + "serde_bytes", + "serde_derive", + "serde_json", + "sha2 0.10.8", + "sha3", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-sdk-macro", + "thiserror", + "tiny-bip39", + "wasm-bindgen", + "zeroize", +] + +[[package]] +name = "solana-sdk-macro" +version = "1.18.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c55c196c8050834c391a34b58e3c9fd86b15452ef1feeeafa1dbeb9d2291dfec" +dependencies = [ + "bs58 0.4.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.75", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9" +dependencies = [ + "proc-macro2", + "quote", + "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", + "quote", + "syn 2.0.75", +] + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.75", +] + +[[package]] +name = "tiny-bip39" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc59cb9dfc85bb312c3a78fd6aa8a8582e310b0fa885d5bb877f6dcc601839d" +dependencies = [ + "anyhow", + "hmac", + "once_cell", + "pbkdf2", + "rand 0.7.3", + "rustc-hash", + "sha2 0.9.9", + "thiserror", + "unicode-normalization", + "wasm-bindgen", + "zeroize", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.20", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.6.18", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.75", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.75", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + +[[package]] +name = "web-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +dependencies = [ + "memchr", +] + +[[package]] +name = "xcall" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "hex", + "rlp", + "xcall-lib", +] + +[[package]] +name = "xcall-lib" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "borsh 1.5.1", + "rlp", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.75", +] + +[[package]] +name = "zeroize" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.75", +] diff --git a/contracts/solana/Cargo.toml b/contracts/solana/Cargo.toml new file mode 100644 index 00000000..54bae78d --- /dev/null +++ b/contracts/solana/Cargo.toml @@ -0,0 +1,24 @@ +[workspace] +members = [ + "programs/*", + "libs/*" +] +resolver = "2" + +[workspace.dependencies] +anchor-lang = { version = "0.30.1", features = ["init-if-needed"] } +borsh = { version = "1.5.1" } +hex ={ version = "0.4.3", default-features = false } +anchor-lang-idl = { version = "0.1.1", features = ["convert"] } + +rlp = { path = "./libs/rlp" } +xcall-lib = { path = "./libs/xcall-lib" } + +[profile.release] +overflow-checks = true +lto = "fat" +codegen-units = 1 +[profile.release.build-override] +opt-level = 3 +incremental = false +codegen-units = 1 diff --git a/contracts/solana/libs/rlp/Cargo.toml b/contracts/solana/libs/rlp/Cargo.toml new file mode 100644 index 00000000..5934a5ef --- /dev/null +++ b/contracts/solana/libs/rlp/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "rlp" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +bytes = "1.6.0" +rustc-hex = { version = "2.1.0", default-features = false } +hex = "0.4.3" +serde = { version = "1.0.203", features = ["derive"] } diff --git a/contracts/solana/libs/rlp/src/error.rs b/contracts/solana/libs/rlp/src/error.rs new file mode 100644 index 00000000..558b274a --- /dev/null +++ b/contracts/solana/libs/rlp/src/error.rs @@ -0,0 +1,51 @@ +// Copyright 2020 Parity Technologies +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use core::fmt; +#[cfg(feature = "std")] +use std::error::Error as StdError; + +#[derive(Debug, PartialEq, Eq, Clone)] +/// Error concerning the RLP decoder. +pub enum DecoderError { + /// Data has additional bytes at the end of the valid RLP fragment. + RlpIsTooBig, + /// Data has too few bytes for valid RLP. + RlpIsTooShort, + /// Expect an encoded list, RLP was something else. + RlpExpectedToBeList, + /// Expect encoded data, RLP was something else. + RlpExpectedToBeData, + /// Expected a different size list. + RlpIncorrectListLen, + /// Data length number has a prefixed zero byte, invalid for numbers. + RlpDataLenWithZeroPrefix, + /// List length number has a prefixed zero byte, invalid for numbers. + RlpListLenWithZeroPrefix, + /// Non-canonical (longer than necessary) representation used for data or list. + RlpInvalidIndirection, + /// Declared length is inconsistent with data specified after. + RlpInconsistentLengthAndData, + /// Declared length is invalid and results in overflow + RlpInvalidLength, + /// Custom rlp decoding error. + Custom(&'static str), +} + +#[cfg(feature = "std")] +impl StdError for DecoderError { + fn description(&self) -> &str { + "builder error" + } +} + +impl fmt::Display for DecoderError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(&self, f) + } +} diff --git a/contracts/solana/libs/rlp/src/impls.rs b/contracts/solana/libs/rlp/src/impls.rs new file mode 100644 index 00000000..eaafea10 --- /dev/null +++ b/contracts/solana/libs/rlp/src/impls.rs @@ -0,0 +1,331 @@ +// Copyright 2020 Parity Technologies +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use bytes::{Bytes, BytesMut}; +use core::{ + iter::{empty, once}, + mem, str, +}; + +use crate::{ + error::DecoderError, + rlpin::Rlp, + stream::RlpStream, + traits::{Decodable, Encodable} +}; + +use super::{encode,decode}; + +pub fn decode_usize(bytes: &[u8]) -> Result { + match bytes.len() { + l if l <= mem::size_of::() => { + if bytes[0] == 0 { + return Err(DecoderError::RlpInvalidIndirection); + } + let mut res = 0usize; + for (i, byte) in bytes.iter().enumerate().take(l) { + let shift = (l - 1 - i) * 8; + res += (*byte as usize) << shift; + } + Ok(res) + } + _ => Err(DecoderError::RlpIsTooBig), + } +} + +impl Encodable for Box { + fn rlp_append(&self, s: &mut RlpStream) { + Encodable::rlp_append(&**self, s) + } +} + +impl Decodable for Box { + fn decode(rlp: &Rlp) -> Result { + T::decode(rlp).map(Box::new) + } +} + +impl Encodable for bool { + fn rlp_append(&self, s: &mut RlpStream) { + let as_uint = u8::from(*self); + Encodable::rlp_append(&as_uint, s); + } +} + +impl Decodable for bool { + fn decode(rlp: &Rlp) -> Result { + let as_uint = ::decode(rlp)?; + match as_uint { + 0 => Ok(false), + 1 => Ok(true), + _ => Err(DecoderError::Custom("invalid boolean value")), + } + } +} + +impl<'a> Encodable for &'a [u8] { + fn rlp_append(&self, s: &mut RlpStream) { + s.encoder().encode_value(self); + } +} + +impl Encodable for Vec { + fn rlp_append(&self, s: &mut RlpStream) { + s.encoder().encode_value(self); + } +} + +impl Decodable for Vec { + fn decode(rlp: &Rlp) -> Result { + rlp.decoder().decode_value(|bytes| Ok(bytes.to_vec())) + } +} + +impl Encodable for Bytes { + fn rlp_append(&self, s: &mut RlpStream) { + s.encoder().encode_value(self); + } +} + +impl Decodable for Bytes { + fn decode(rlp: &Rlp) -> Result { + rlp.decoder() + .decode_value(|bytes| Ok(Bytes::copy_from_slice(bytes))) + } +} + +impl Encodable for BytesMut { + fn rlp_append(&self, s: &mut RlpStream) { + s.encoder().encode_value(self); + } +} + +impl Decodable for BytesMut { + fn decode(rlp: &Rlp) -> Result { + rlp.decoder().decode_value(|bytes| Ok(bytes.into())) + } +} + +impl Encodable for Option +where + T: Encodable, +{ + fn rlp_append(&self, s: &mut RlpStream) { + match *self { + None => { + s.begin_list(0); + } + Some(ref value) => { + s.begin_list(1); + s.append(value); + } + } + } +} + +impl Decodable for Option +where + T: Decodable, +{ + fn decode(rlp: &Rlp) -> Result { + let items = rlp.item_count()?; + match items { + 1 => rlp.val_at(0).map(Some), + 0 => Ok(None), + _ => Err(DecoderError::RlpIncorrectListLen), + } + } +} + +impl Encodable for u8 { + fn rlp_append(&self, s: &mut RlpStream) { + s.encoder().encode_iter(once(*self)); + } +} + +impl Decodable for u8 { + fn decode(rlp: &Rlp) -> Result { + rlp.decoder().decode_value(|bytes| match bytes.len() { + 1 => Ok(bytes[0]), + 0 => Ok(0), + _ => Err(DecoderError::RlpIsTooBig), + }) + } +} + +macro_rules! impl_encodable_for_u { + ($name: ident) => { + impl Encodable for $name { + fn rlp_append(&self, s: &mut RlpStream) { + let bytes = |mut v: $name| -> Vec { + if v == 0 { + vec![0] + } else { + let mut buffer: Vec = vec![0_u8; 16]; + for i in (0..=15).rev() { + let b: u8 = (v & 0xff) as u8; + buffer[i] = b; + v >>= 8; + if v == 0 && (b & 0x80) == 0 { + return buffer[i..].to_vec(); + } + } + buffer + } + }(*self); + s.encoder().encode_value(&bytes); + } + } + }; +} + +macro_rules! impl_decodable_for_u { + ($name: ident) => { + impl Decodable for $name { + fn decode(rlp: &Rlp) -> Result { + rlp.decoder().decode_value(|bytes| match bytes.len() { + 0 | 1 => u8::decode(rlp).map(|v| v as $name), + l if l <= mem::size_of::<$name>() => { + let mut res = 0 as $name; + for (i, byte) in bytes.iter().enumerate().take(l) { + let shift = (l - 1 - i) * 8; + res += (*byte as $name) << shift; + } + Ok(res) + } + _ => Err(DecoderError::RlpIsTooBig), + }) + } + } + }; +} + +impl_encodable_for_u!(u16); +impl_encodable_for_u!(u32); +impl_encodable_for_u!(u64); +impl_encodable_for_u!(u128); + +impl_decodable_for_u!(u16); +impl_decodable_for_u!(u32); +impl_decodable_for_u!(u64); +impl_decodable_for_u!(u128); + +impl Encodable for usize { + fn rlp_append(&self, s: &mut RlpStream) { + (*self as u64).rlp_append(s); + } +} + +impl Decodable for usize { + fn decode(rlp: &Rlp) -> Result { + u64::decode(rlp).map(|value| value as usize) + } +} + +impl<'a> Encodable for &'a str { + fn rlp_append(&self, s: &mut RlpStream) { + s.encoder().encode_value(self.as_bytes()); + } +} + +impl Encodable for String { + fn rlp_append(&self, s: &mut RlpStream) { + s.encoder().encode_value(self.as_bytes()); + } +} + +impl Decodable for String { + fn decode(rlp: &Rlp) -> Result { + rlp.decoder().decode_value(|bytes| { + match str::from_utf8(bytes) { + Ok(s) => Ok(s.to_owned()), + // consider better error type here + Err(_err) => Err(DecoderError::RlpExpectedToBeData), + } + }) + } +} + +impl Encodable for i8 { + fn rlp_append(&self, s: &mut RlpStream) { + Encodable::rlp_append(&(*self as u8), s); + } +} + +impl Decodable for i8 { + fn decode(rlp: &Rlp) -> Result { + rlp.decoder() + .decode_value(|bytes| match bytes.len() as u32 { + len if len == u8::BITS / 8 => Ok(bytes[0] as i8), + _ => Err(DecoderError::RlpInvalidLength), + }) + } +} + +impl Encodable for i64 { + fn rlp_append(&self, s: &mut RlpStream) { + Encodable::rlp_append(&(*self as u64), s); + } +} + +impl Decodable for i64 { + fn decode(rlp: &Rlp) -> Result { + rlp.decoder().decode_value(|bytes| { + if bytes.len() as u32 > u64::BITS / 8 { + return Err(DecoderError::RlpInvalidLength); + } + let mut result: i64 = 0; + + for &byte in bytes { + result <<= 8; + result |= byte as i64; + } + Ok(result) + }) + } +} + +impl Encodable for i32 { + fn rlp_append(&self, s: &mut RlpStream) { + Encodable::rlp_append(&(*self as u64), s); + } +} + +impl Decodable for i32 { + fn decode(rlp: &Rlp) -> Result { + rlp.decoder().decode_value(|bytes| { + if bytes.len() as u32 > u32::BITS / 8 { + return Err(DecoderError::RlpInvalidLength); + } + let mut result: i32 = 0; + + for &byte in bytes { + result <<= 8; + result |= byte as i32; + } + Ok(result) + }) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_i64_encoding() { + + let value: i64 = 12098; + let encoded = encode(&value).to_vec(); + println!("{}", hex::encode(&encoded)); + + let decoded: i64 = decode(&encoded).unwrap(); + assert_eq!(decoded, value); + } +} diff --git a/contracts/solana/libs/rlp/src/lib.rs b/contracts/solana/libs/rlp/src/lib.rs new file mode 100644 index 00000000..d2d1b675 --- /dev/null +++ b/contracts/solana/libs/rlp/src/lib.rs @@ -0,0 +1,126 @@ +#![allow(unused)] +// Copyright 2020 Parity Technologies +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! Recursive Length Prefix serialization crate. +//! +//! Allows encoding, decoding, and view onto rlp-slice +//! +//! # What should you use when? +//! +//! ### Use `encode` function when: +//! * You want to encode something inline. +//! * You do not work on big set of data. +//! * You want to encode whole data structure at once. +//! +//! ### Use `decode` function when: +//! * You want to decode something inline. +//! * You do not work on big set of data. +//! * You want to decode whole rlp at once. +//! +//! ### Use `RlpStream` when: +//! * You want to encode something in portions. +//! * You encode a big set of data. +//! +//! ### Use `Rlp` when: +//! * You need to handle data corruption errors. +//! * You are working on input data. +//! * You want to get view onto rlp-slice. +//! * You don't want to decode whole rlp at once. + +mod error; +mod impls; +mod nullable; +mod rlpin; +mod stream; +mod traits; + +use bytes::BytesMut; +use core::borrow::Borrow; + +pub use self::{ + error::DecoderError, + rlpin::{PayloadInfo, Prototype, Rlp, RlpIterator}, + stream::RlpStream, + traits::{Decodable, Encodable}, +}; + +/// The RLP encoded empty data (used to mean "null value"). +pub const NULL_RLP: [u8; 2] = [0xf8, 0x00]; +/// The RLP encoded empty list. +pub const EMPTY_LIST_RLP: [u8; 1] = [0xC0; 1]; +pub use nullable::Nullable; +/// Shortcut function to decode trusted rlp +/// +/// ``` +/// use common::rlp::{self}; +/// let data = vec![0x83, b'c', b'a', b't']; +/// let animal: String = rlp::decode(&data).expect("could not decode"); +/// assert_eq!(animal, "cat".to_owned()); +/// ``` +pub fn decode(bytes: &[u8]) -> Result +where + T: Decodable, +{ + let rlp = Rlp::new(bytes); + rlp.as_val() +} + +pub fn decode_list(bytes: &[u8]) -> Vec +where + T: Decodable, +{ + let rlp = Rlp::new(bytes); + rlp.as_list().expect("trusted rlp should be valid") +} + +/// Shortcut function to encode structure into rlp. +/// +/// ``` +/// use common::rlp::{self}; +/// let animal = "cat"; +/// let out = rlp::encode(&animal); +/// assert_eq!(out, vec![0x83, b'c', b'a', b't']); +/// ``` +pub fn encode(object: &E) -> BytesMut +where + E: Encodable, +{ + let mut stream = RlpStream::new(); + stream.append(object); + stream.out() +} + +pub fn encode_list(object: &[K]) -> BytesMut +where + E: Encodable, + K: Borrow, +{ + let mut stream = RlpStream::new(); + stream.append_list(object); + stream.out() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_solidity_encoding() { + let expected = "00"; + let zero: u64 = 0; + let tag: u64 = 128; + let false_val = false; + let result = super::encode(&zero).to_vec(); + assert_eq!(expected, hex::encode(result)); + let result = super::encode(&false_val).to_vec(); + assert_eq!(expected, hex::encode(result)); + let result = super::encode(&tag).to_vec(); + assert_eq!("820080", hex::encode(result)); + } +} diff --git a/contracts/solana/libs/rlp/src/nullable.rs b/contracts/solana/libs/rlp/src/nullable.rs new file mode 100644 index 00000000..b1242326 --- /dev/null +++ b/contracts/solana/libs/rlp/src/nullable.rs @@ -0,0 +1,44 @@ +use serde::{Deserialize, Serialize}; + +use super::*; + +#[derive(Clone, Default, PartialEq, Eq, Debug, Serialize, Deserialize)] +pub struct Nullable(pub Option); + +impl Nullable { + pub fn new(item: Option) -> Self { + Self(item) + } + + pub fn is_some(&self) -> bool { + self.0.is_some() + } + + pub fn is_none(&self) -> bool { + self.0.is_none() + } + + pub fn get(&self) -> Result<&T, &'static str> { + self.0.as_ref().ok_or("object is null") + } +} + +impl Decodable for Nullable { + fn decode(rlp: &Rlp) -> Result { + if rlp.is_null() { + Ok(Self(None)) + } else { + Ok(Self(Some(rlp.as_val()?))) + } + } +} + +impl Encodable for Nullable { + fn rlp_append(&self, stream: &mut RlpStream) { + if self.is_none() { + stream.append_null_internal(); + } else { + stream.append_internal(self.get().unwrap()); + } + } +} diff --git a/contracts/solana/libs/rlp/src/rlpin.rs b/contracts/solana/libs/rlp/src/rlpin.rs new file mode 100644 index 00000000..8cb66769 --- /dev/null +++ b/contracts/solana/libs/rlp/src/rlpin.rs @@ -0,0 +1,461 @@ +// Copyright 2020 Parity Technologies +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use core::{cell::Cell, fmt}; + +use rustc_hex::ToHex; + +use crate::{error::DecoderError, impls::decode_usize, traits::Decodable}; + +/// rlp offset +#[derive(Copy, Clone, Debug)] +struct OffsetCache { + index: usize, + offset: usize, +} + +impl OffsetCache { + const fn new(index: usize, offset: usize) -> OffsetCache { + OffsetCache { index, offset } + } +} + +#[derive(Debug)] +/// RLP prototype +pub enum Prototype { + /// Empty + Null, + /// Value + Data(usize), + /// List + List(usize), +} + +/// Stores basic information about item +#[derive(Debug)] +pub struct PayloadInfo { + /// Header length in bytes + pub header_len: usize, + /// Value length in bytes + pub value_len: usize, +} + +fn calculate_payload_info( + header_bytes: &[u8], + len_of_len: usize, +) -> Result { + let header_len = 1 + len_of_len; + match header_bytes.get(1) { + Some(&0) => return Err(DecoderError::RlpDataLenWithZeroPrefix), + None => return Err(DecoderError::RlpIsTooShort), + _ => (), + } + if header_bytes.len() < header_len { + return Err(DecoderError::RlpIsTooShort); + } + let value_len = decode_usize(&header_bytes[1..header_len])?; + if value_len <= 55 { + return Err(DecoderError::RlpInvalidIndirection); + } + Ok(PayloadInfo::new(header_len, value_len)) +} + +impl PayloadInfo { + const fn new(header_len: usize, value_len: usize) -> PayloadInfo { + PayloadInfo { + header_len, + value_len, + } + } + + /// Total size of the RLP. + pub fn total(&self) -> usize { + self.header_len + self.value_len + } + + /// Create a new object from the given bytes RLP. The bytes + pub fn from(header_bytes: &[u8]) -> Result { + let l = *header_bytes.first().ok_or(DecoderError::RlpIsTooShort)?; + + if l <= 0x7f { + Ok(PayloadInfo::new(0, 1)) + } else if l <= 0xb7 { + Ok(PayloadInfo::new(1, l as usize - 0x80)) + } else if l <= 0xbf { + let len_of_len = l as usize - 0xb7; + calculate_payload_info(header_bytes, len_of_len) + } else if l <= 0xf7 { + Ok(PayloadInfo::new(1, l as usize - 0xc0)) + } else if l == 0xf8 && header_bytes[1] == 0 { + Ok(PayloadInfo::new(1, 1)) + } else { + let len_of_len = l as usize - 0xf7; + calculate_payload_info(header_bytes, len_of_len) + } + } +} + +/// Data-oriented view onto rlp-slice. +/// +/// This is an immutable structure. No operations change it. +/// +/// Should be used in places where, error handling is required, +/// eg. on input +#[derive(Debug, Clone)] +pub struct Rlp<'a> { + bytes: &'a [u8], + offset_cache: Cell>, + count_cache: Cell>, +} + +impl<'a> fmt::Display for Rlp<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + match self.prototype() { + Ok(Prototype::Null) => write!(f, "null"), + Ok(Prototype::Data(_)) => { + write!(f, "\"0x{}\"", self.data().unwrap().to_hex::()) + } + Ok(Prototype::List(len)) => { + write!(f, "[")?; + for i in 0..len - 1 { + write!(f, "{}, ", self.at(i).unwrap())?; + } + write!(f, "{}", self.at(len - 1).unwrap())?; + write!(f, "]") + } + Err(err) => write!(f, "{err:?}"), + } + } +} + +impl<'a> Rlp<'a> { + pub const fn new(bytes: &'a [u8]) -> Rlp<'a> { + Rlp { + bytes, + offset_cache: Cell::new(None), + count_cache: Cell::new(None), + } + } + + pub fn as_raw<'view>(&'view self) -> &'a [u8] + where + 'a: 'view, + { + self.bytes + } + + pub fn prototype(&self) -> Result { + // optimize? && return appropriate errors + if self.is_data() { + Ok(Prototype::Data(self.size())) + } else if self.is_list() { + self.item_count().map(Prototype::List) + } else { + Ok(Prototype::Null) + } + } + + pub fn payload_info(&self) -> Result { + BasicDecoder::payload_info(self.bytes) + } + + pub fn data<'view>(&'view self) -> Result<&'a [u8], DecoderError> + where + 'a: 'view, + { + let pi = BasicDecoder::payload_info(self.bytes)?; + Ok(&self.bytes[pi.header_len..(pi.header_len + pi.value_len)]) + } + + pub fn item_count(&self) -> Result { + if self.is_list() { + match self.count_cache.get() { + Some(c) => Ok(c), + None => { + let c = self.iter().count(); + self.count_cache.set(Some(c)); + Ok(c) + } + } + } else { + Err(DecoderError::RlpExpectedToBeList) + } + } + + pub fn size(&self) -> usize { + if self.is_data() { + // TODO: No panic on malformed data, but ideally would Err on no PayloadInfo. + BasicDecoder::payload_info(self.bytes) + .map(|b| b.value_len) + .unwrap_or(0) + } else { + 0 + } + } + + /// Returns an Rlp item in a list at the given index. + /// + /// Returns an error if this Rlp is not a list or if the index is out of range. + pub fn at<'view>(&'view self, index: usize) -> Result, DecoderError> + where + 'a: 'view, + { + let (rlp, _offset) = self.at_with_offset(index)?; + Ok(rlp) + } + + /// Returns an Rlp item in a list at the given index along with the byte offset into the + /// raw data slice. + /// + /// Returns an error if this Rlp is not a list or if the index is out of range. + pub fn at_with_offset<'view>( + &'view self, + index: usize, + ) -> Result<(Rlp<'a>, usize), DecoderError> + where + 'a: 'view, + { + if !self.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + + // move to cached position if its index is less or equal to + // current search index, otherwise move to beginning of list + let cache = self.offset_cache.get(); + let (bytes, indexes_to_skip, bytes_consumed) = match cache { + Some(ref cache) if cache.index <= index => ( + Rlp::consume(self.bytes, cache.offset)?, + index - cache.index, + cache.offset, + ), + _ => { + let (bytes, consumed) = self.consume_list_payload()?; + (bytes, index, consumed) + } + }; + + // skip up to x items + let (bytes, consumed) = Rlp::consume_items(bytes, indexes_to_skip)?; + + // update the cache + let offset = bytes_consumed + consumed; + self.offset_cache.set(Some(OffsetCache::new(index, offset))); + + // construct new rlp + let found = BasicDecoder::payload_info(bytes)?; + Ok(( + Rlp::new(&bytes[0..found.header_len + found.value_len]), + offset, + )) + } + + pub fn is_null(&self) -> bool { + self.bytes.is_empty() || (self.bytes[0] == 0xf8 && self.bytes[1] == 0) + } + + pub fn is_empty(&self) -> bool { + !self.is_null() && (self.bytes[0] == 0xc0 || self.bytes[0] == 0x80) + } + + pub fn is_list(&self) -> bool { + !self.is_null() && self.bytes[0] >= 0xc0 + } + + pub fn is_data(&self) -> bool { + !self.is_null() && self.bytes[0] < 0xc0 + } + + pub fn is_int(&self) -> bool { + if self.is_null() { + return false; + } + + match self.bytes[0] { + 0..=0x80 => true, + 0x81..=0xb7 => self.bytes[1] != 0, + b @ 0xb8..=0xbf => { + let payload_idx = 1 + b as usize - 0xb7; + payload_idx < self.bytes.len() && self.bytes[payload_idx] != 0 + } + _ => false, + } + } + + pub fn iter<'view>(&'view self) -> RlpIterator<'a, 'view> + where + 'a: 'view, + { + self.into_iter() + } + + pub fn as_val(&self) -> Result + where + T: Decodable, + { + T::decode(self) + } + + pub fn as_list(&self) -> Result, DecoderError> + where + T: Decodable, + { + self.iter().map(|rlp| rlp.as_val()).collect() + } + + pub fn val_at(&self, index: usize) -> Result + where + T: Decodable, + { + self.at(index)?.as_val() + } + + pub fn list_at(&self, index: usize) -> Result, DecoderError> + where + T: Decodable, + { + self.at(index)?.as_list() + } + + pub fn decoder(&self) -> BasicDecoder { + BasicDecoder::new(self.bytes) + } + + /// consumes first found prefix + fn consume_list_payload(&self) -> Result<(&'a [u8], usize), DecoderError> { + let item = BasicDecoder::payload_info(self.bytes)?; + if self.bytes.len() < (item.header_len + item.value_len) { + return Err(DecoderError::RlpIsTooShort); + } + Ok(( + &self.bytes[item.header_len..item.header_len + item.value_len], + item.header_len, + )) + } + + /// consumes fixed number of items + fn consume_items(bytes: &'a [u8], items: usize) -> Result<(&'a [u8], usize), DecoderError> { + let mut result = bytes; + let mut consumed = 0; + for _ in 0..items { + let i = BasicDecoder::payload_info(result)?; + let to_consume = i.header_len + i.value_len; + result = Rlp::consume(result, to_consume)?; + consumed += to_consume; + } + Ok((result, consumed)) + } + + /// consumes slice prefix of length `len` + fn consume(bytes: &'a [u8], len: usize) -> Result<&'a [u8], DecoderError> { + if bytes.len() >= len { + Ok(&bytes[len..]) + } else { + Err(DecoderError::RlpIsTooShort) + } + } +} + +/// Iterator over rlp-slice list elements. +pub struct RlpIterator<'a, 'view> +where + 'a: 'view, +{ + rlp: &'view Rlp<'a>, + index: usize, +} + +impl<'a, 'view> IntoIterator for &'view Rlp<'a> +where + 'a: 'view, +{ + type Item = Rlp<'a>; + type IntoIter = RlpIterator<'a, 'view>; + + fn into_iter(self) -> Self::IntoIter { + RlpIterator { + rlp: self, + index: 0, + } + } +} + +impl<'a, 'view> Iterator for RlpIterator<'a, 'view> { + type Item = Rlp<'a>; + + fn next(&mut self) -> Option> { + let index = self.index; + let result = self.rlp.at(index).ok(); + self.index += 1; + result + } +} + +impl<'a, 'view> ExactSizeIterator for RlpIterator<'a, 'view> { + fn len(&self) -> usize { + self.rlp.item_count().unwrap_or(0) + } +} + +pub struct BasicDecoder<'a> { + rlp: &'a [u8], +} + +impl<'a> BasicDecoder<'a> { + pub const fn new(rlp: &'a [u8]) -> BasicDecoder<'a> { + BasicDecoder { rlp } + } + + /// Return first item info. + fn payload_info(bytes: &[u8]) -> Result { + let item = PayloadInfo::from(bytes)?; + match item.header_len.checked_add(item.value_len) { + Some(x) if x <= bytes.len() => Ok(item), + _ => Err(DecoderError::RlpIsTooShort), + } + } + + pub fn decode_value(&self, f: F) -> Result + where + F: Fn(&[u8]) -> Result, + { + let bytes = self.rlp; + + let l = *bytes.first().ok_or(DecoderError::RlpIsTooShort)?; + + if l <= 0x7f { + Ok(f(&[l])?) + } else if l <= 0xb7 { + let last_index_of = 1 + l as usize - 0x80; + if bytes.len() < last_index_of { + return Err(DecoderError::RlpInconsistentLengthAndData); + } + let d = &bytes[1..last_index_of]; + if l == 0x81 && d[0] < 0x80 { + return Err(DecoderError::RlpInvalidIndirection); + } + Ok(f(d)?) + } else if l <= 0xbf { + let len_of_len = l as usize - 0xb7; + let begin_of_value = 1_usize + len_of_len; + if bytes.len() < begin_of_value { + return Err(DecoderError::RlpInconsistentLengthAndData); + } + let len = decode_usize(&bytes[1..begin_of_value])?; + + let last_index_of_value = begin_of_value + .checked_add(len) + .ok_or(DecoderError::RlpInvalidLength)?; + if bytes.len() < last_index_of_value { + return Err(DecoderError::RlpInconsistentLengthAndData); + } + Ok(f(&bytes[begin_of_value..last_index_of_value])?) + } else { + Err(DecoderError::RlpExpectedToBeData) + } + } +} diff --git a/contracts/solana/libs/rlp/src/stream.rs b/contracts/solana/libs/rlp/src/stream.rs new file mode 100644 index 00000000..698307c5 --- /dev/null +++ b/contracts/solana/libs/rlp/src/stream.rs @@ -0,0 +1,466 @@ +// Copyright 2020 Parity Technologies +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use bytes::{BufMut, BytesMut}; +use core::borrow::Borrow; + +use crate::traits::Encodable; + +#[derive(Debug, Copy, Clone)] +struct ListInfo { + position: usize, + current: usize, + max: Option, +} + +impl ListInfo { + fn new(position: usize, max: Option) -> ListInfo { + ListInfo { + position, + current: 0, + max, + } + } +} + +/// Appendable rlp encoder. +pub struct RlpStream { + unfinished_lists: Vec, + start_pos: usize, + buffer: BytesMut, + finished_list: bool, +} + +impl Default for RlpStream { + fn default() -> Self { + RlpStream::new() + } +} + +impl RlpStream { + /// Initializes instance of empty `Stream`. + pub fn new() -> Self { + Self::new_with_buffer(BytesMut::with_capacity(1024)) + } + + /// Initializes the `Stream` as a list. + pub fn new_list(len: usize) -> Self { + Self::new_list_with_buffer(BytesMut::with_capacity(1024), len) + } + + /// Initializes instance of empty `Stream`. + pub fn new_with_buffer(buffer: BytesMut) -> Self { + RlpStream { + unfinished_lists: Vec::with_capacity(16), + start_pos: buffer.len(), + buffer, + finished_list: false, + } + } + + /// Initializes the `Stream` as a list. + pub fn new_list_with_buffer(buffer: BytesMut, len: usize) -> Self { + let mut stream = RlpStream::new_with_buffer(buffer); + stream.begin_list(len); + stream + } + + fn total_written(&self) -> usize { + self.buffer.len() - self.start_pos + } + + /// Apends null to the end of stream, chainable. + /// + /// ``` + /// use common::rlp::{RlpStream}; + /// let mut stream = RlpStream::new_list(2); + /// stream.append_null().append_null(); + /// let out = stream.out(); + /// println!("{:?}", b"\xc4\xf8\0\xf8\0"); + /// assert_eq!(out, vec![0xc4, 0xf8, 0, 0xf8, 0]); + /// ``` + pub fn append_null(&mut self) -> &mut Self { + // self push raw item + self.buffer.put_u8(0xf8); + self.buffer.put_u8(0x00); + // try to finish and prepend the length + self.note_appended(1); + + // return chainable self + self + } + + pub fn append_null_internal(&mut self) -> &mut Self { + // self push raw item + self.buffer.put_u8(0xf8); + self.buffer.put_u8(0x00); + // return chainable self + self + } + + /// Apends null to the end of stream, chainable. + /// + /// ``` + /// use common::rlp::RlpStream; + /// let mut stream = RlpStream::new_list(2); + /// stream.append_empty_data().append_empty_data(); + /// let out = stream.out(); + /// assert_eq!(out, vec![0xc2, 0x80, 0x80]); + /// ``` + pub fn append_empty_data(&mut self) -> &mut Self { + // self push raw item + self.buffer.put_u8(0x80); + + // try to finish and prepend the length + self.note_appended(1); + + // return chainable self + self + } + + /// Appends raw (pre-serialised) RLP data. Use with caution. Chainable. + pub fn append_raw(&mut self, bytes: &[u8], item_count: usize) -> &mut Self { + // push raw items + self.buffer.extend_from_slice(bytes); + + // try to finish and prepend the length + self.note_appended(item_count); + + // return chainable self + self + } + + /// Appends value to the end of stream, chainable. + /// + /// ``` + /// use common::rlp::{RlpStream}; + /// let mut stream = RlpStream::new_list(2); + /// stream.append(&"cat").append(&"dog"); + /// let out = stream.out(); + /// assert_eq!(out, vec![0xc8, 0x83, b'c', b'a', b't', 0x83, b'd', b'o', b'g']); + /// ``` + pub fn append(&mut self, value: &E) -> &mut Self + where + E: Encodable, + { + self.finished_list = false; + value.rlp_append(self); + if !self.finished_list { + self.note_appended(1); + } + self + } + + /// Appends iterator to the end of stream, chainable. + /// + /// ``` + /// use common::rlp::{RlpStream}; + /// let mut stream = RlpStream::new_list(2); + /// stream.append(&"cat").append_iter("dog".as_bytes().iter().cloned()); + /// let out = stream.out(); + /// assert_eq!(out, vec![0xc8, 0x83, b'c', b'a', b't', 0x83, b'd', b'o', b'g']); + /// ``` + pub fn append_iter(&mut self, value: I) -> &mut Self + where + I: IntoIterator, + { + self.finished_list = false; + self.encoder().encode_iter(value); + if !self.finished_list { + self.note_appended(1); + } + self + } + + /// Appends list of values to the end of stream, chainable. + pub fn append_list(&mut self, values: &[K]) -> &mut Self + where + E: Encodable, + K: Borrow, + { + self.begin_list(values.len()); + for value in values { + self.append(value.borrow()); + } + self + } + + /// Appends value to the end of stream, but do not count it as an appended item. + /// It's useful for wrapper types + pub fn append_internal(&mut self, value: &E) -> &mut Self + where + E: Encodable, + { + value.rlp_append(self); + self + } + + /// Declare appending the list of given size, chainable. + /// + /// ``` + /// use common::rlp::{RlpStream}; + /// let mut stream = RlpStream::new_list(2); + /// stream.begin_list(2).append(&"cat").append(&"dog"); + /// stream.append(&""); + /// let out = stream.out(); + /// assert_eq!(out, vec![0xca, 0xc8, 0x83, b'c', b'a', b't', 0x83, b'd', b'o', b'g', 0x80]); + /// ``` + pub fn begin_list(&mut self, len: usize) -> &mut RlpStream { + self.finished_list = false; + match len { + 0 => { + // we may finish, if the appended list len is equal 0 + self.buffer.put_u8(0xc0u8); + self.note_appended(1); + self.finished_list = true; + } + _ => { + // payload is longer than 1 byte only for lists > 55 bytes + // by pushing always this 1 byte we may avoid unnecessary shift of data + self.buffer.put_u8(0); + + let position = self.total_written(); + self.unfinished_lists + .push(ListInfo::new(position, Some(len))); + } + } + + // return chainable self + self + } + + /// Declare appending the list of unknown size, chainable. + pub fn begin_unbounded_list(&mut self) -> &mut RlpStream { + self.finished_list = false; + // payload is longer than 1 byte only for lists > 55 bytes + // by pushing always this 1 byte we may avoid unnecessary shift of data + self.buffer.put_u8(0); + let position = self.total_written(); + self.unfinished_lists.push(ListInfo::new(position, None)); + // return chainable self + self + } + + /// Appends raw (pre-serialised) RLP data. Checks for size overflow. + pub fn append_raw_checked(&mut self, bytes: &[u8], item_count: usize, max_size: usize) -> bool { + if self.estimate_size(bytes.len()) > max_size { + return false; + } + self.append_raw(bytes, item_count); + true + } + + /// Calculate total RLP size for appended payload. + pub fn estimate_size(&self, add: usize) -> usize { + let total_size = self.total_written() + add; + let mut base_size = total_size; + for list in &self.unfinished_lists[..] { + let len = total_size - list.position; + if len > 55 { + let leading_empty_bytes = (len as u64).leading_zeros() as usize / 8; + let size_bytes = 8 - leading_empty_bytes; + base_size += size_bytes; + } + } + base_size + } + + /// Returns current RLP size in bytes for the data pushed into the list. + pub fn len(&self) -> usize { + self.estimate_size(0) + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Clear the output stream so far. + /// + /// ``` + /// use common::rlp::{RlpStream}; + /// let mut stream = RlpStream::new_list(3); + /// stream.append(&"cat"); + /// stream.clear(); + /// stream.append(&"dog"); + /// let out = stream.out(); + /// assert_eq!(out, vec![0x83, b'd', b'o', b'g']); + /// ``` + pub fn clear(&mut self) { + // clear bytes + self.buffer.truncate(self.start_pos); + + // clear lists + self.unfinished_lists.clear(); + } + + /// Returns true if stream doesnt expect any more items. + /// + /// ``` + /// use common::rlp::{RlpStream}; + /// let mut stream = RlpStream::new_list(2); + /// stream.append(&"cat"); + /// assert_eq!(stream.is_finished(), false); + /// stream.append(&"dog"); + /// assert_eq!(stream.is_finished(), true); + /// let out = stream.out(); + /// assert_eq!(out, vec![0xc8, 0x83, b'c', b'a', b't', 0x83, b'd', b'o', b'g']); + /// ``` + pub fn is_finished(&self) -> bool { + self.unfinished_lists.is_empty() + } + + /// Get raw encoded bytes + pub fn as_raw(&self) -> &[u8] { + //&self.encoder.bytes + &self.buffer + } + + /// Streams out encoded bytes. + /// + /// panic! if stream is not finished. + pub fn out(self) -> BytesMut { + if self.is_finished() { + self.buffer + } else { + panic!() + } + } + + /// Try to finish lists + fn note_appended(&mut self, inserted_items: usize) { + if self.unfinished_lists.is_empty() { + return; + } + + let back = self.unfinished_lists.len() - 1; + let should_finish = match self.unfinished_lists.get_mut(back) { + None => false, + Some(ref mut x) => { + x.current += inserted_items; + match x.max { + Some(ref max) if x.current > *max => { + panic!("You cannot append more items than you expect!") + } + Some(ref max) => x.current == *max, + _ => false, + } + } + }; + if should_finish { + let x = self.unfinished_lists.pop().unwrap(); + let len = self.total_written() - x.position; + self.encoder().insert_list_payload(len, x.position); + self.note_appended(1); + } + self.finished_list = should_finish; + } + + pub fn encoder(&mut self) -> BasicEncoder { + BasicEncoder::new(self, self.start_pos) + } + + /// Finalize current unbounded list. Panics if no unbounded list has been opened. + pub fn finalize_unbounded_list(&mut self) { + let list = self.unfinished_lists.pop().expect("No open list."); + if list.max.is_some() { + panic!("List type mismatch."); + } + let len = self.total_written() - list.position; + self.encoder().insert_list_payload(len, list.position); + self.note_appended(1); + self.finished_list = true; + } +} + +pub struct BasicEncoder<'a> { + buffer: &'a mut BytesMut, + start_pos: usize, +} + +impl<'a> BasicEncoder<'a> { + fn new(stream: &'a mut RlpStream, start_pos: usize) -> Self { + BasicEncoder { + buffer: &mut stream.buffer, + start_pos, + } + } + + fn total_written(&self) -> usize { + self.buffer.len() - self.start_pos + } + + fn insert_size(&mut self, size: usize, position: usize) -> u8 { + let size = size as u32; + let leading_empty_bytes = size.leading_zeros() as usize / 8; + let size_bytes = 4 - leading_empty_bytes as u8; + let buffer: [u8; 4] = size.to_be_bytes(); + assert!(position <= self.total_written()); + + self.buffer + .extend_from_slice(&buffer[leading_empty_bytes..]); + self.buffer[self.start_pos + position..].rotate_right(size_bytes as usize); + size_bytes + } + + /// Inserts list prefix at given position + fn insert_list_payload(&mut self, len: usize, pos: usize) { + // 1 byte was already reserved for payload earlier + match len { + 0..=55 => { + self.buffer[self.start_pos + pos - 1] = 0xc0u8 + len as u8; + } + _ => { + let inserted_bytes = self.insert_size(len, pos); + self.buffer[self.start_pos + pos - 1] = 0xf7u8 + inserted_bytes; + } + }; + } + + pub fn encode_value(&mut self, value: &[u8]) { + self.encode_iter(value.iter().cloned()); + } + + /// Pushes encoded value to the end of buffer + pub fn encode_iter(&mut self, value: I) + where + I: IntoIterator, + { + let mut value = value.into_iter(); + let len = match value.size_hint() { + (lower, Some(upper)) if lower == upper => lower, + _ => { + let value = value.collect::>(); + return self.encode_iter(value); + } + }; + match len { + // just 0 + 0 => self.buffer.put_u8(0x80u8), + len @ 1..=55 => { + let first = value.next().expect("iterator length is higher than 1"); + if len == 1 && first < 0x80 { + // byte is its own encoding if < 0x80 + self.buffer.put_u8(first); + } else { + // (prefix + length), followed by the string + self.buffer.put_u8(0x80u8 + len as u8); + self.buffer.put_u8(first); + self.buffer.extend(value); + } + } + // (prefix + length of length), followed by the length, followd by the string + len => { + self.buffer.put_u8(0); + let position = self.total_written(); + let inserted_bytes = self.insert_size(len, position); + self.buffer[self.start_pos + position - 1] = 0xb7 + inserted_bytes; + self.buffer.extend(value); + } + } + } +} diff --git a/contracts/solana/libs/rlp/src/temp.rs b/contracts/solana/libs/rlp/src/temp.rs new file mode 100644 index 00000000..dafe9eec --- /dev/null +++ b/contracts/solana/libs/rlp/src/temp.rs @@ -0,0 +1,120 @@ +#![allow(unused)] +// Copyright 2020 Parity Technologies +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! Recursive Length Prefix serialization crate. +//! +//! Allows encoding, decoding, and view onto rlp-slice +//! +//! # What should you use when? +//! +//! ### Use `encode` function when: +//! * You want to encode something inline. +//! * You do not work on big set of data. +//! * You want to encode whole data structure at once. +//! +//! ### Use `decode` function when: +//! * You want to decode something inline. +//! * You do not work on big set of data. +//! * You want to decode whole rlp at once. +//! +//! ### Use `RlpStream` when: +//! * You want to encode something in portions. +//! * You encode a big set of data. +//! +//! ### Use `Rlp` when: +//! * You need to handle data corruption errors. +//! * You are working on input data. +//! * You want to get view onto rlp-slice. +//! * You don't want to decode whole rlp at once. + + +use bytes::BytesMut; +use core::borrow::Borrow; + +pub use self::{ + error::DecoderError, + rlpin::{PayloadInfo, Prototype, Rlp, RlpIterator}, + stream::RlpStream, + traits::{Decodable, Encodable}, +}; + +/// The RLP encoded empty data (used to mean "null value"). +pub const NULL_RLP: [u8; 2] = [0xf8, 0x00]; +/// The RLP encoded empty list. +pub const EMPTY_LIST_RLP: [u8; 1] = [0xC0; 1]; +pub use nullable::Nullable; +/// Shortcut function to decode trusted rlp +/// +/// ``` +/// use common::rlp::{self}; +/// let data = vec![0x83, b'c', b'a', b't']; +/// let animal: String = rlp::decode(&data).expect("could not decode"); +/// assert_eq!(animal, "cat".to_owned()); +/// ``` +pub fn decode(bytes: &[u8]) -> Result +where + T: Decodable, +{ + let rlp = Rlp::new(bytes); + rlp.as_val() +} + +pub fn decode_list(bytes: &[u8]) -> Vec +where + T: Decodable, +{ + let rlp = Rlp::new(bytes); + rlp.as_list().expect("trusted rlp should be valid") +} + +/// Shortcut function to encode structure into rlp. +/// +/// ``` +/// use common::rlp::{self}; +/// let animal = "cat"; +/// let out = rlp::encode(&animal); +/// assert_eq!(out, vec![0x83, b'c', b'a', b't']); +/// ``` +pub fn encode(object: &E) -> BytesMut +where + E: Encodable, +{ + let mut stream = RlpStream::new(); + stream.append(object); + stream.out() +} + +pub fn encode_list(object: &[K]) -> BytesMut +where + E: Encodable, + K: Borrow, +{ + let mut stream = RlpStream::new(); + stream.append_list(object); + stream.out() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_solidity_encoding() { + let expected = "00"; + let zero: u64 = 0; + let tag: u64 = 128; + let false_val = false; + let result = super::encode(&zero).to_vec(); + assert_eq!(expected, hex::encode(result)); + let result = super::encode(&false_val).to_vec(); + assert_eq!(expected, hex::encode(result)); + let result = super::encode(&tag).to_vec(); + assert_eq!("820080", hex::encode(result)); + } +} diff --git a/contracts/solana/libs/rlp/src/traits.rs b/contracts/solana/libs/rlp/src/traits.rs new file mode 100644 index 00000000..b83f8ace --- /dev/null +++ b/contracts/solana/libs/rlp/src/traits.rs @@ -0,0 +1,31 @@ +// Copyright 2020 Parity Technologies +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! Common RLP traits +use bytes::BytesMut; + +use crate::{error::DecoderError, rlpin::Rlp, stream::RlpStream}; + +/// RLP decodable trait +pub trait Decodable: Sized { + /// Decode a value from RLP bytes + fn decode(rlp: &Rlp) -> Result; +} + +/// Structure encodable to RLP +pub trait Encodable { + /// Append a value to the stream + fn rlp_append(&self, s: &mut RlpStream); + + /// Get rlp-encoded bytes for this instance + fn rlp_bytes(&self) -> BytesMut { + let mut s = RlpStream::new(); + self.rlp_append(&mut s); + s.out() + } +} diff --git a/contracts/solana/libs/xcall-lib/Cargo.toml b/contracts/solana/libs/xcall-lib/Cargo.toml new file mode 100644 index 00000000..3cb1f859 --- /dev/null +++ b/contracts/solana/libs/xcall-lib/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "xcall-lib" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] + +[features] +library = [] + +[dependencies] +borsh = { workspace = true } +rlp = { workspace = true } +anchor-lang = { workspace = true } diff --git a/contracts/solana/libs/xcall-lib/src/error.rs b/contracts/solana/libs/xcall-lib/src/error.rs new file mode 100644 index 00000000..44d04132 --- /dev/null +++ b/contracts/solana/libs/xcall-lib/src/error.rs @@ -0,0 +1,7 @@ +use anchor_lang::prelude::error_code; + +#[error_code] +pub enum NetworkError { + #[msg("Invalid network address")] + InvalidNetworkAddress, +} diff --git a/contracts/solana/libs/xcall-lib/src/lib.rs b/contracts/solana/libs/xcall-lib/src/lib.rs new file mode 100644 index 00000000..f6054cb0 --- /dev/null +++ b/contracts/solana/libs/xcall-lib/src/lib.rs @@ -0,0 +1,7 @@ +pub mod error; +pub mod message; +pub mod network_address; +pub mod query_account_type; +pub mod xcall_connection_type; +pub mod xcall_dapp_type; +pub mod xcall_type; diff --git a/contracts/solana/libs/xcall-lib/src/message/call_message.rs b/contracts/solana/libs/xcall-lib/src/message/call_message.rs new file mode 100644 index 00000000..6ab21922 --- /dev/null +++ b/contracts/solana/libs/xcall-lib/src/message/call_message.rs @@ -0,0 +1,53 @@ +use super::*; + +#[derive(Clone, Debug, AnchorSerialize, AnchorDeserialize)] +pub struct CallMessage { + pub data: Vec, +} + +impl Encodable for CallMessage { + fn rlp_append(&self, stream: &mut RlpStream) { + stream.begin_list(1).append(&self.data); + } +} + +impl Decodable for CallMessage { + fn decode(rlp: &rlp::Rlp) -> Result { + Ok(Self { + data: rlp.val_at(0)?, + }) + } +} + +impl IMessage for CallMessage { + fn rollback(&self) -> Option> { + None + } + + fn data(&self) -> Vec { + self.data.clone() + } + + fn to_bytes(&self) -> Result, DecoderError> { + Ok(rlp::encode(self).to_vec()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rlp::Rlp; + + #[test] + fn test_encoding_decoding_message() { + let original_message = CallMessage { + data: vec![1, 2, 3, 4], + }; + let encoded = original_message.to_bytes().unwrap(); + + let decoded_rlp = Rlp::new(&encoded); + let decoded_message = CallMessage::decode(&decoded_rlp).unwrap(); + + assert_eq!(decoded_message.data, original_message.data); + } +} diff --git a/contracts/solana/libs/xcall-lib/src/message/call_message_persisted.rs b/contracts/solana/libs/xcall-lib/src/message/call_message_persisted.rs new file mode 100644 index 00000000..96ed0fe8 --- /dev/null +++ b/contracts/solana/libs/xcall-lib/src/message/call_message_persisted.rs @@ -0,0 +1,34 @@ +use super::*; + +#[derive(Clone, Debug, AnchorSerialize, AnchorDeserialize)] +pub struct CallMessagePersisted { + pub data: Vec, +} + +impl Encodable for CallMessagePersisted { + fn rlp_append(&self, stream: &mut RlpStream) { + stream.begin_list(1).append(&self.data); + } +} + +impl Decodable for CallMessagePersisted { + fn decode(rlp: &rlp::Rlp) -> Result { + Ok(Self { + data: rlp.val_at(0)?, + }) + } +} + +impl IMessage for CallMessagePersisted { + fn rollback(&self) -> Option> { + None + } + + fn data(&self) -> Vec { + self.data.clone() + } + + fn to_bytes(&self) -> Result, rlp::DecoderError> { + Ok(rlp::encode(self).to_vec()) + } +} diff --git a/contracts/solana/libs/xcall-lib/src/message/call_message_rollback.rs b/contracts/solana/libs/xcall-lib/src/message/call_message_rollback.rs new file mode 100644 index 00000000..28643c88 --- /dev/null +++ b/contracts/solana/libs/xcall-lib/src/message/call_message_rollback.rs @@ -0,0 +1,64 @@ +use super::*; + +#[derive(Clone, Debug, AnchorSerialize, AnchorDeserialize)] +pub struct CallMessageWithRollback { + pub data: Vec, + pub rollback: Vec, +} + +impl Encodable for CallMessageWithRollback { + fn rlp_append(&self, stream: &mut RlpStream) { + stream + .begin_list(2) + .append(&self.data) + .append(&self.rollback); + } +} + +impl Decodable for CallMessageWithRollback { + fn decode(rlp: &rlp::Rlp) -> Result { + Ok(Self { + data: rlp.val_at(0)?, + rollback: rlp.val_at(1)?, + }) + } +} + +impl IMessage for CallMessageWithRollback { + fn rollback(&self) -> Option> { + Some(self.rollback.clone()) + } + + fn data(&self) -> Vec { + self.data.clone() + } + + fn to_bytes(&self) -> Result, DecoderError> { + Ok(rlp::encode(self).to_vec()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rlp::Encodable; + use rlp::Rlp; + + #[test] + fn test_encoding_decoding_message() { + let original_message = CallMessageWithRollback { + data: vec![0, 11, 255], + rollback: vec![1, 2, 3], + }; + + let mut stream = RlpStream::new(); + original_message.rlp_append(&mut stream); + let encoded = stream.out(); + + let decoded_rlp = Rlp::new(&encoded); + let decoded_message = CallMessageWithRollback::decode(&decoded_rlp).unwrap(); + + assert_eq!(decoded_message.data, original_message.data); + assert_eq!(decoded_message.rollback, original_message.rollback); + } +} diff --git a/contracts/solana/libs/xcall-lib/src/message/envelope.rs b/contracts/solana/libs/xcall-lib/src/message/envelope.rs new file mode 100644 index 00000000..67359949 --- /dev/null +++ b/contracts/solana/libs/xcall-lib/src/message/envelope.rs @@ -0,0 +1,91 @@ +use super::*; + +#[derive(Clone, AnchorSerialize, AnchorDeserialize)] +pub struct Envelope { + pub message: AnyMessage, + pub sources: Vec, + pub destinations: Vec, +} + +impl Envelope { + pub fn new(msg: AnyMessage, sources: Vec, destinations: Vec) -> Self { + Self { + message: msg, + sources, + destinations, + } + } +} + +impl Encodable for Envelope { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + stream.begin_list(4); + stream.append(&Into::::into(self.message.msg_type().clone())); + stream.append(&self.message.to_bytes().unwrap()); + stream.begin_list(self.sources.len()); + for source in self.sources.iter() { + stream.append(source); + } + stream.begin_list(self.destinations.len()); + for dest in self.destinations.iter() { + stream.append(dest); + } + } +} + +impl Decodable for Envelope { + fn decode(rlp: &rlp::Rlp) -> Result { + let msg_int: u8 = rlp.val_at(0)?; + let msg_type = MessageType::from(msg_int); + let message_bytes: Vec = rlp.val_at(1)?; + let message = decode_message(msg_type, message_bytes)?; + + let sources = rlp.at(2)?; + let sources: Vec = sources.as_list()?; + let destinations = rlp.at(3)?; + let destinations: Vec = destinations.as_list()?; + + Ok(Envelope { + message, + sources, + destinations, + }) + } +} + +pub fn decode_message(msg_type: MessageType, bytes: Vec) -> Result { + match msg_type { + MessageType::CallMessage => { + let msg: CallMessage = rlp::decode(&bytes)?; + Ok(AnyMessage::CallMessage(msg)) + } + MessageType::CallMessageWithRollback => { + let msg: CallMessageWithRollback = rlp::decode(&bytes)?; + Ok(AnyMessage::CallMessageWithRollback(msg)) + } + MessageType::CallMessagePersisted => { + let msg: CallMessagePersisted = rlp::decode(&bytes)?; + Ok(AnyMessage::CallMessagePersisted(msg)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_decode_message() { + let msg_bytes = CallMessagePersisted { + data: vec![1, 2, 3], + }; + + let encoded = msg_bytes.to_bytes(); + + // Decode the message + let decoded_message = + decode_message(MessageType::CallMessagePersisted, encoded.clone().unwrap()).unwrap(); + + assert_eq!(decoded_message.data(), msg_bytes.data); + } +} diff --git a/contracts/solana/libs/xcall-lib/src/message/mod.rs b/contracts/solana/libs/xcall-lib/src/message/mod.rs new file mode 100644 index 00000000..ea148a86 --- /dev/null +++ b/contracts/solana/libs/xcall-lib/src/message/mod.rs @@ -0,0 +1,56 @@ +use self::{ + call_message::CallMessage, call_message_persisted::CallMessagePersisted, + call_message_rollback::CallMessageWithRollback, msg_trait::IMessage, msg_type::MessageType, +}; +use anchor_lang::{prelude::borsh, AnchorDeserialize, AnchorSerialize}; +use rlp::{Decodable, DecoderError, Encodable, RlpStream}; + +pub mod call_message; +pub mod call_message_persisted; +pub mod call_message_rollback; +pub mod envelope; +pub mod msg_trait; +pub mod msg_type; + +#[derive(Clone, Debug, AnchorSerialize, AnchorDeserialize)] +pub enum AnyMessage { + CallMessage(CallMessage), + CallMessageWithRollback(CallMessageWithRollback), + CallMessagePersisted(CallMessagePersisted), +} + +impl IMessage for AnyMessage { + fn rollback(&self) -> Option> { + match self { + AnyMessage::CallMessage(m) => m.rollback(), + AnyMessage::CallMessageWithRollback(m) => m.rollback(), + AnyMessage::CallMessagePersisted(m) => m.rollback(), + } + } + + fn data(&self) -> Vec { + match self { + AnyMessage::CallMessage(m) => m.data(), + AnyMessage::CallMessageWithRollback(m) => m.data(), + AnyMessage::CallMessagePersisted(m) => m.data(), + } + } + + fn to_bytes(&self) -> Result, DecoderError> { + match self { + AnyMessage::CallMessage(m) => m.to_bytes(), + AnyMessage::CallMessageWithRollback(m) => m.to_bytes(), + AnyMessage::CallMessagePersisted(m) => m.to_bytes(), + } + } +} + +impl AnyMessage { + pub fn msg_type(&self) -> MessageType { + match self { + AnyMessage::CallMessage(_m) => MessageType::CallMessage, + AnyMessage::CallMessageWithRollback(_m) => MessageType::CallMessageWithRollback, + AnyMessage::CallMessagePersisted(_m) => MessageType::CallMessagePersisted, + } + } +} diff --git a/contracts/solana/libs/xcall-lib/src/message/msg_trait.rs b/contracts/solana/libs/xcall-lib/src/message/msg_trait.rs new file mode 100644 index 00000000..ae70e518 --- /dev/null +++ b/contracts/solana/libs/xcall-lib/src/message/msg_trait.rs @@ -0,0 +1,8 @@ +use rlp::DecoderError; + +pub trait IMessage: Clone { + fn rollback(&self) -> Option>; + fn data(&self) -> Vec; + + fn to_bytes(&self) -> Result, DecoderError>; +} diff --git a/contracts/solana/libs/xcall-lib/src/message/msg_type.rs b/contracts/solana/libs/xcall-lib/src/message/msg_type.rs new file mode 100644 index 00000000..875c1fae --- /dev/null +++ b/contracts/solana/libs/xcall-lib/src/message/msg_type.rs @@ -0,0 +1,38 @@ +use super::{borsh, AnchorDeserialize, AnchorSerialize}; + +#[derive(Clone, Debug, PartialEq, AnchorSerialize, AnchorDeserialize)] +pub enum MessageType { + CallMessage = 0, + CallMessageWithRollback = 1, + CallMessagePersisted = 2, +} + +impl From for u8 { + fn from(val: MessageType) -> Self { + match val { + MessageType::CallMessage => 0, + MessageType::CallMessageWithRollback => 1, + MessageType::CallMessagePersisted => 2, + } + } +} + +impl From for MessageType { + fn from(value: u8) -> Self { + match value { + 0 => MessageType::CallMessage, + 1 => MessageType::CallMessageWithRollback, + 2 => MessageType::CallMessagePersisted, + _ => panic!("unsupported message type"), + } + } +} + +impl MessageType { + pub fn as_int(&self) -> u8 { + self.clone().into() + } + pub fn from_int(val: u8) -> Self { + MessageType::from(val) + } +} diff --git a/contracts/solana/libs/xcall-lib/src/network_address.rs b/contracts/solana/libs/xcall-lib/src/network_address.rs new file mode 100644 index 00000000..47f24224 --- /dev/null +++ b/contracts/solana/libs/xcall-lib/src/network_address.rs @@ -0,0 +1,96 @@ +use anchor_lang::prelude::borsh; +use anchor_lang::{AnchorDeserialize, AnchorSerialize}; + +use crate::error::NetworkError; +use std::str::FromStr; + +#[derive(Debug, PartialEq, Eq, AnchorSerialize, AnchorDeserialize)] +pub struct NetId(String); + +impl From for NetId { + fn from(value: String) -> Self { + Self(value) + } +} + +impl ToString for NetId { + fn to_string(&self) -> String { + self.0.to_string() + } +} + +impl NetId { + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl FromStr for NetId { + type Err = NetworkError; + + fn from_str(s: &str) -> Result { + Ok(Self(s.to_owned())) + } +} + +#[derive(Clone, Debug, AnchorSerialize, AnchorDeserialize)] +pub struct NetworkAddress(String); + +impl NetworkAddress { + pub fn new(nid: &str, address: &str) -> Self { + Self(format!("{}/{}", nid, address)) + } + + pub fn nid(&self) -> String { + self.get_parts()[0].to_string() + } + + pub fn account(&self) -> String { + self.get_parts()[1].to_owned() + } + + pub fn parse_network_address(&self) -> (String, String) { + let parts = self.get_parts(); + (parts[0].to_owned(), parts[1].to_owned()) + } + + pub fn get_parts(&self) -> Vec<&str> { + let parts = self.0.split('/').collect::>(); + if parts.len() != 2 { + panic!("Invalid Network Address"); + } + parts + } +} + +impl ToString for NetworkAddress { + fn to_string(&self) -> String { + self.0.to_string() + } +} + +impl FromStr for NetworkAddress { + type Err = NetworkError; + + fn from_str(s: &str) -> Result { + let parts = s.split('/').collect::>(); + if parts.len() != 2 { + return Err(NetworkError::InvalidNetworkAddress); + } + let na = format!("{}/{}", parts[0], parts[1]); + Ok(Self(na)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_split_network_address() { + let na = String::from("0x1.icon/hx124324687"); + let parsed = NetworkAddress::from_str(&na).unwrap(); + assert_eq!(String::from("0x1.icon"), parsed.nid().to_string()); + assert_eq!(String::from("hx124324687"), parsed.account()); + } +} diff --git a/contracts/solana/libs/xcall-lib/src/query_account_type.rs b/contracts/solana/libs/xcall-lib/src/query_account_type.rs new file mode 100644 index 00000000..c745cdb4 --- /dev/null +++ b/contracts/solana/libs/xcall-lib/src/query_account_type.rs @@ -0,0 +1,65 @@ +use anchor_lang::prelude::*; + +#[derive(Debug, Default, PartialEq, Eq, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct AccountMetadata { + pub pubkey: Pubkey, + pub is_writable: bool, + pub is_signer: bool, +} + +impl AccountMetadata { + pub fn new(pubkey: Pubkey, is_signer: bool) -> Self { + Self { + pubkey, + is_signer, + is_writable: true, + } + } + + pub fn new_readonly(pubkey: Pubkey, is_signer: bool) -> Self { + Self { + pubkey, + is_signer, + is_writable: false, + } + } +} + +#[derive(Debug, Default, PartialEq, Eq, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct QueryAccountsResponse { + pub accounts: Vec, +} + +#[derive(Debug, Default, PartialEq, Eq, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct QueryAccountsPaginateResponse { + pub accounts: Vec, + pub total_accounts: u8, + pub limit: u8, + pub page: u8, + pub has_next_page: bool, +} + +impl QueryAccountsPaginateResponse { + pub fn new(accounts: Vec, page: u8, limit: u8) -> Self { + let offset = ((page - 1) * limit) as usize; + let total = accounts.len(); + + let to_index = if offset + limit as usize > total { + total + } else { + offset + limit as usize + }; + + let accounts = accounts[offset..to_index].to_vec(); + let total_accounts = total as u8; + let has_next_page = total > to_index; + + QueryAccountsPaginateResponse { + accounts, + total_accounts, + limit, + page, + has_next_page, + } + } +} diff --git a/contracts/solana/libs/xcall-lib/src/state.rs b/contracts/solana/libs/xcall-lib/src/state.rs new file mode 100644 index 00000000..8a59e1c9 --- /dev/null +++ b/contracts/solana/libs/xcall-lib/src/state.rs @@ -0,0 +1,8 @@ +use anchor_lang::prelude::borsh; +use anchor_lang::{AnchorDeserialize, AnchorSerialize}; + +#[derive(AnchorSerialize, AnchorDeserialize, Debug)] +pub struct CpiDappResponse { + pub success: bool, + pub data: Option, +} diff --git a/contracts/solana/libs/xcall-lib/src/xcall_connection_type.rs b/contracts/solana/libs/xcall-lib/src/xcall_connection_type.rs new file mode 100644 index 00000000..751fb6e8 --- /dev/null +++ b/contracts/solana/libs/xcall-lib/src/xcall_connection_type.rs @@ -0,0 +1,21 @@ +use anchor_lang::prelude::*; + +pub const CONNECTION_AUTHORITY_SEED: &str = "connection_authority"; + +pub const GET_FEE_IX: &str = "get_fee"; +pub const SEND_MESSAGE_IX: &str = "send_message"; + +pub const QUERY_SEND_MESSAGE_ACCOUNTS_IX: &str = "query_send_message_accounts"; + +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct SendMessageArgs { + pub to: String, + pub sn: i64, + pub msg: Vec, +} + +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct GetFeeArgs { + pub network_id: String, + pub response: bool, +} diff --git a/contracts/solana/libs/xcall-lib/src/xcall_dapp_type.rs b/contracts/solana/libs/xcall-lib/src/xcall_dapp_type.rs new file mode 100644 index 00000000..9e8af407 --- /dev/null +++ b/contracts/solana/libs/xcall-lib/src/xcall_dapp_type.rs @@ -0,0 +1,22 @@ +use anchor_lang::prelude::*; + +use crate::network_address::NetworkAddress; + +pub const DAPP_AUTHORITY_SEED: &str = "dapp_authority"; + +pub const HANDLE_CALL_MESSAGE_IX: &str = "handle_call_message"; + +pub const QUERY_HANDLE_CALL_MESSAGE_IX: &str = "query_handle_call_message_accounts"; + +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct HandleCallMessageArgs { + pub from: NetworkAddress, + pub data: Vec, + pub protocols: Vec, +} + +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct HandleCallMessageResponse { + pub success: bool, + pub message: String, +} diff --git a/contracts/solana/libs/xcall-lib/src/xcall_type.rs b/contracts/solana/libs/xcall-lib/src/xcall_type.rs new file mode 100644 index 00000000..3ec5cf07 --- /dev/null +++ b/contracts/solana/libs/xcall-lib/src/xcall_type.rs @@ -0,0 +1,51 @@ +use anchor_lang::prelude::*; + +use crate::network_address::NetworkAddress; + +pub const SEND_CALL_IX: &str = "send_call"; +pub const HANDLE_MESSAGE_IX: &str = "handle_message"; +pub const HANDLE_REQUEST_IX: &str = "handle_request"; +pub const HANDLE_RESULT_IX: &str = "handle_result"; +pub const HANDLE_ERROR_IX: &str = "handle_error"; +pub const EXECUTE_CALL_IX: &str = "execute_call"; +pub const HANDLE_FORCED_ROLLBACK_IX: &str = "handle_forced_rollback"; + +pub const QUERY_HANDLE_MESSAGE_ACCOUNTS_IX: &str = "query_handle_message_accounts"; +pub const QUERY_HANDLE_ERROR_ACCOUNTS_IX: &str = "query_handle_error_accounts"; +pub const QUERY_EXECUTE_CALL_ACCOUNTS_IX: &str = "query_execute_call_accounts"; + +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct SendCallArgs { + pub msg: Vec, + pub to: NetworkAddress, +} + +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct HandleMessageArgs { + pub from_nid: String, + pub message: Vec, + pub sequence_no: u128, +} + +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct HandleRequestArgs { + pub from_nid: String, + pub msg_payload: Vec, +} + +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct HandleResultArgs { + pub from_nid: String, + pub msg_payload: Vec, + pub sequence_no: u128, +} + +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct HandleErrorArgs { + pub sequence_no: u128, +} + +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct HandleForcedRollback { + pub req_id: u128, +} diff --git a/contracts/solana/migrations/deploy.ts b/contracts/solana/migrations/deploy.ts new file mode 100644 index 00000000..82fb175f --- /dev/null +++ b/contracts/solana/migrations/deploy.ts @@ -0,0 +1,12 @@ +// Migrations are an early feature. Currently, they're nothing more than this +// single deploy script that's invoked from the CLI, injecting a provider +// configured from the workspace's Anchor.toml. + +const anchor = require("@coral-xyz/anchor"); + +module.exports = async function (provider) { + // Configure client to use the provider. + anchor.setProvider(provider); + + // Add your deploy script here. +}; diff --git a/contracts/solana/package.json b/contracts/solana/package.json new file mode 100644 index 00000000..381ce802 --- /dev/null +++ b/contracts/solana/package.json @@ -0,0 +1,20 @@ +{ + "scripts": { + "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", + "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check" + }, + "dependencies": { + "@coral-xyz/anchor": "^0.30.1", + "rlp": "^3.0.0" + }, + "devDependencies": { + "@types/bn.js": "^5.1.0", + "@types/chai": "^4.3.0", + "@types/mocha": "^9.0.0", + "chai": "^4.3.4", + "mocha": "^9.0.3", + "prettier": "^2.6.2", + "ts-mocha": "^10.0.0", + "typescript": "^4.3.5" + } +} diff --git a/contracts/solana/programs/centralized-connection/Cargo.toml b/contracts/solana/programs/centralized-connection/Cargo.toml new file mode 100644 index 00000000..36d562d4 --- /dev/null +++ b/contracts/solana/programs/centralized-connection/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "centralized-connection" +version = "0.1.0" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "centralized_connection" + +[features] +default = [] +cpi = ["no-entrypoint"] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +idl-build = ["anchor-lang/idl-build"] + +[dependencies] +anchor-lang = { workspace = true, features = ["init-if-needed"] } + +xcall-lib = { workspace = true } diff --git a/contracts/solana/programs/centralized-connection/Xargo.toml b/contracts/solana/programs/centralized-connection/Xargo.toml new file mode 100644 index 00000000..475fb71e --- /dev/null +++ b/contracts/solana/programs/centralized-connection/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/contracts/solana/programs/centralized-connection/src/constants.rs b/contracts/solana/programs/centralized-connection/src/constants.rs new file mode 100644 index 00000000..fe8aad0a --- /dev/null +++ b/contracts/solana/programs/centralized-connection/src/constants.rs @@ -0,0 +1 @@ +pub const ACCOUNT_DISCRIMINATOR_SIZE: usize = 8; diff --git a/contracts/solana/programs/centralized-connection/src/contexts.rs b/contracts/solana/programs/centralized-connection/src/contexts.rs new file mode 100644 index 00000000..1927aa0f --- /dev/null +++ b/contracts/solana/programs/centralized-connection/src/contexts.rs @@ -0,0 +1,188 @@ +use anchor_lang::prelude::*; + +use crate::{error::ConnectionError, state::*}; + +#[derive(Accounts)] +pub struct Initialize<'info> { + /// Rent payer + #[account(mut)] + pub signer: Signer<'info>, + + /// System Program: Required for creating the centralized-connection config + pub system_program: Program<'info, System>, + + /// Config + #[account( + init, + payer = signer, + seeds = [Config::SEED_PREFIX.as_bytes()], + bump, + space = Config::LEN + )] + pub config: Account<'info, Config>, + + #[account( + init, + payer = signer, + space = Authority::LEN, + seeds = [Authority::SEED_PREFIX.as_bytes()], + bump + )] + pub authority: Account<'info, Authority>, +} + +#[derive(Accounts)] +#[instruction(to: String)] +pub struct SendMessage<'info> { + #[account(mut)] + pub signer: Signer<'info>, + + pub system_program: Program<'info, System>, + + #[account( + owner = config.xcall @ ConnectionError::OnlyXcall + )] + pub xcall: Signer<'info>, + + #[account( + mut, + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump, + )] + pub config: Account<'info, Config>, + + #[account( + seeds = [NetworkFee::SEED_PREFIX.as_bytes(), to.as_bytes()], + bump = network_fee.bump + )] + pub network_fee: Account<'info, NetworkFee>, +} + +#[derive(Accounts)] +#[instruction(src_network: String, conn_sn: u128)] +pub struct RecvMessage<'info> { + #[account(mut)] + pub admin: Signer<'info>, + + pub system_program: Program<'info, System>, + + /// Config + #[account( + mut, + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump, + has_one = admin @ ConnectionError::OnlyAdmin, + )] + pub config: Account<'info, Config>, + + #[account( + init, + payer = admin, + seeds = [Receipt::SEED_PREFIX.as_bytes(), src_network.as_bytes(), &conn_sn.to_be_bytes()], + space = Receipt::LEN, + bump + )] + pub receipt: Account<'info, Receipt>, + + #[account( + seeds = [Authority::SEED_PREFIX.as_bytes()], + bump = authority.bump + )] + pub authority: Account<'info, Authority>, +} + +#[derive(Accounts)] +pub struct RevertMessage<'info> { + #[account(mut)] + pub admin: Signer<'info>, + + pub system_program: Program<'info, System>, + + /// Config + #[account( + mut, + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump, + has_one = admin @ ConnectionError::OnlyAdmin, + )] + pub config: Account<'info, Config>, + + #[account( + seeds = [Authority::SEED_PREFIX.as_bytes()], + bump = authority.bump + )] + pub authority: Account<'info, Authority>, +} + +#[derive(Accounts)] +pub struct SetAdmin<'info> { + /// Transaction signer + #[account(mut)] + pub admin: Signer<'info>, + + /// Config + #[account( + mut, + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump, + has_one = admin @ ConnectionError::OnlyAdmin, + )] + pub config: Account<'info, Config>, +} + +#[derive(Accounts)] +#[instruction(network_id: String)] +pub struct SetFee<'info> { + /// Rent payer + #[account(mut)] + pub admin: Signer<'info>, + + /// System Program: Required to create program-derived address + pub system_program: Program<'info, System>, + + /// Fee + #[account( + init_if_needed, + payer = admin, + seeds = [NetworkFee::SEED_PREFIX.as_bytes(), network_id.as_bytes()], + bump, + space = NetworkFee::LEN + )] + pub network_fee: Account<'info, NetworkFee>, + + /// Config + #[account( + mut, + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump, + has_one = admin @ ConnectionError::OnlyAdmin, + )] + pub config: Account<'info, Config>, +} + +#[derive(Accounts)] +#[instruction(network_id: String)] +pub struct GetFee<'info> { + /// Fee + #[account( + seeds = [NetworkFee::SEED_PREFIX.as_bytes(), network_id.as_bytes()], + bump = network_fee.bump + )] + pub network_fee: Account<'info, NetworkFee>, +} + +#[derive(Accounts)] +pub struct ClaimFees<'info> { + /// Rent payer + #[account(mut)] + pub admin: Signer<'info>, + + /// Config + #[account( + mut, + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump, + has_one = admin @ ConnectionError::OnlyAdmin, + )] + pub config: Account<'info, Config>, +} diff --git a/contracts/solana/programs/centralized-connection/src/error.rs b/contracts/solana/programs/centralized-connection/src/error.rs new file mode 100644 index 00000000..d21f7543 --- /dev/null +++ b/contracts/solana/programs/centralized-connection/src/error.rs @@ -0,0 +1,10 @@ +use anchor_lang::prelude::*; + +#[error_code] +pub enum ConnectionError { + #[msg("Only admin")] + OnlyAdmin, + + #[msg("Only xcall")] + OnlyXcall, +} diff --git a/contracts/solana/programs/centralized-connection/src/event.rs b/contracts/solana/programs/centralized-connection/src/event.rs new file mode 100644 index 00000000..e379a589 --- /dev/null +++ b/contracts/solana/programs/centralized-connection/src/event.rs @@ -0,0 +1,10 @@ +#![allow(non_snake_case)] + +use anchor_lang::prelude::*; + +#[event] +pub struct SendMessage { + pub targetNetwork: String, + pub connSn: u128, + pub msg: Vec, +} diff --git a/contracts/solana/programs/centralized-connection/src/helper.rs b/contracts/solana/programs/centralized-connection/src/helper.rs new file mode 100644 index 00000000..927c17a5 --- /dev/null +++ b/contracts/solana/programs/centralized-connection/src/helper.rs @@ -0,0 +1,129 @@ +use anchor_lang::{ + prelude::*, + solana_program::{ + hash, + instruction::Instruction, + program::{invoke, invoke_signed}, + system_instruction, + }, +}; + +use crate::contexts::*; +use crate::state::*; + +use xcall_lib::xcall_type; + +pub fn transfer_lamports<'info>( + from: &AccountInfo<'info>, + to: &AccountInfo<'info>, + system_program: &AccountInfo<'info>, + amount: u64, +) -> Result<()> { + let ix = system_instruction::transfer(&from.key(), &to.key(), amount); + invoke( + &ix, + &[from.to_owned(), to.to_owned(), system_program.to_owned()], + )?; + + Ok(()) +} + +pub fn get_instruction_data(ix_name: &str, data: Vec) -> Vec { + let preimage = format!("{}:{}", "global", ix_name); + + let mut ix_discriminator = [0u8; 8]; + ix_discriminator.copy_from_slice(&hash::hash(preimage.as_bytes()).to_bytes()[..8]); + + let mut ix_data = Vec::new(); + ix_data.extend_from_slice(&ix_discriminator); + ix_data.extend_from_slice(&data); + + ix_data +} + +pub fn call_xcall_handle_message<'info>( + ctx: Context<'_, '_, '_, 'info, RecvMessage<'info>>, + from_nid: String, + message: Vec, + sequence_no: u128, +) -> Result<()> { + let mut data = vec![]; + let args = xcall_type::HandleMessageArgs { + from_nid, + message, + sequence_no, + }; + args.serialize(&mut data)?; + + let ix_data = get_instruction_data("handle_message", data); + + invoke_instruction( + ix_data, + &ctx.accounts.config, + &ctx.accounts.authority, + &ctx.accounts.admin, + &ctx.accounts.system_program, + ctx.remaining_accounts, + ) +} + +pub fn call_xcall_handle_error<'info>( + ctx: Context<'_, '_, '_, 'info, RevertMessage<'info>>, + sequence_no: u128, +) -> Result<()> { + let mut data = vec![]; + let args = xcall_type::HandleErrorArgs { sequence_no }; + args.serialize(&mut data)?; + + let ix_data = get_instruction_data("handle_error", data); + + invoke_instruction( + ix_data, + &ctx.accounts.config, + &ctx.accounts.authority, + &ctx.accounts.admin, + &ctx.accounts.system_program, + &ctx.remaining_accounts, + ) +} + +pub fn invoke_instruction<'info>( + ix_data: Vec, + config: &Account<'info, Config>, + authority: &Account<'info, Authority>, + admin: &Signer<'info>, + system_program: &Program<'info, System>, + remaining_accounts: &[AccountInfo<'info>], +) -> Result<()> { + let mut account_metas = vec![ + AccountMeta::new(admin.key(), true), + AccountMeta::new_readonly(authority.key(), true), + AccountMeta::new_readonly(system_program.key(), false), + ]; + let mut account_infos = vec![ + admin.to_account_info(), + authority.to_account_info(), + system_program.to_account_info(), + ]; + for i in remaining_accounts { + if i.is_writable { + account_metas.push(AccountMeta::new(i.key(), i.is_signer)); + } else { + account_metas.push(AccountMeta::new_readonly(i.key(), i.is_signer)) + } + account_infos.push(i.to_account_info()); + } + let ix = Instruction { + program_id: config.xcall, + accounts: account_metas, + data: ix_data.clone(), + }; + + invoke_signed( + &ix, + &account_infos, + &[&[Authority::SEED_PREFIX.as_bytes(), &[authority.bump]]], + )?; + + Ok(()) +} diff --git a/contracts/solana/programs/centralized-connection/src/instructions/mod.rs b/contracts/solana/programs/centralized-connection/src/instructions/mod.rs new file mode 100644 index 00000000..912ce532 --- /dev/null +++ b/contracts/solana/programs/centralized-connection/src/instructions/mod.rs @@ -0,0 +1,3 @@ +pub mod query_accounts; + +pub use query_accounts::*; diff --git a/contracts/solana/programs/centralized-connection/src/instructions/query_accounts.rs b/contracts/solana/programs/centralized-connection/src/instructions/query_accounts.rs new file mode 100644 index 00000000..7f149623 --- /dev/null +++ b/contracts/solana/programs/centralized-connection/src/instructions/query_accounts.rs @@ -0,0 +1,194 @@ +use anchor_lang::{ + prelude::*, + solana_program::{ + instruction::Instruction, + program::{get_return_data, invoke}, + system_program, + }, +}; +use xcall_lib::{ + query_account_type::{AccountMetadata, QueryAccountsPaginateResponse, QueryAccountsResponse}, + xcall_connection_type, + xcall_type::{self, QUERY_HANDLE_ERROR_ACCOUNTS_IX, QUERY_HANDLE_MESSAGE_ACCOUNTS_IX}, +}; + +use crate::{helper, id, state::*}; + +pub fn query_send_message_accounts<'info>( + ctx: Context<'_, '_, '_, 'info, QueryAccountsCtx<'info>>, + dst_network: String, +) -> Result { + let config = &ctx.accounts.config; + + let (network_fee, _) = Pubkey::find_program_address( + &[NetworkFee::SEED_PREFIX.as_bytes(), dst_network.as_bytes()], + &id(), + ); + + let account_metas = vec![ + AccountMetadata::new(config.key(), false), + AccountMetadata::new(network_fee, false), + ]; + + Ok(QueryAccountsResponse { + accounts: account_metas, + }) +} + +pub fn query_recv_message_accounts( + ctx: Context, + src_network: String, + conn_sn: u128, + msg: Vec, + sequence_no: u128, + page: u8, + limit: u8, +) -> Result { + let config = &ctx.accounts.config; + let (receipt, _) = Pubkey::find_program_address( + &[ + Receipt::SEED_PREFIX.as_bytes(), + src_network.as_bytes(), + &conn_sn.to_be_bytes(), + ], + &id(), + ); + let (authority, _) = Pubkey::find_program_address( + &[xcall_connection_type::CONNECTION_AUTHORITY_SEED.as_bytes()], + &id(), + ); + + let mut account_metas = vec![ + AccountMetadata::new(system_program::id(), false), + AccountMetadata::new(config.key(), false), + AccountMetadata::new(receipt, false), + AccountMetadata::new(authority, false), + ]; + + let mut xcall_account_metas = vec![]; + let mut xcall_account_infos = vec![]; + + for (_, account) in ctx.remaining_accounts.iter().enumerate() { + if account.is_writable { + xcall_account_metas.push(AccountMeta::new(account.key(), account.is_signer)); + } else { + xcall_account_metas.push(AccountMeta::new_readonly(account.key(), account.is_signer)); + } + + xcall_account_infos.push(account.to_account_info()) + } + + let ix_data = get_handle_message_ix_data(src_network, msg, sequence_no)?; + + let ix = Instruction { + program_id: config.xcall, + accounts: xcall_account_metas, + data: ix_data, + }; + + invoke(&ix, &xcall_account_infos)?; + + let (_, data) = get_return_data().unwrap(); + let mut data_slice: &[u8] = &data; + let res = QueryAccountsResponse::deserialize(&mut data_slice)?; + let mut res_accounts = res.accounts; + + account_metas.append(&mut res_accounts); + + Ok(QueryAccountsPaginateResponse::new( + account_metas, + page, + limit, + )) +} + +pub fn query_revert_message_accounts( + ctx: Context, + sequence_no: u128, + page: u8, + limit: u8, +) -> Result { + let config = &ctx.accounts.config; + let (authority, _) = Pubkey::find_program_address( + &[xcall_connection_type::CONNECTION_AUTHORITY_SEED.as_bytes()], + &id(), + ); + + let mut account_metas = vec![ + AccountMetadata::new(system_program::id(), false), + AccountMetadata::new(config.key(), false), + AccountMetadata::new(authority, false), + ]; + + let mut xcall_account_metas = vec![]; + let mut xcall_account_infos = vec![]; + + for (_, account) in ctx.remaining_accounts.iter().enumerate() { + if account.is_writable { + xcall_account_metas.push(AccountMeta::new(account.key(), account.is_signer)); + } else { + xcall_account_metas.push(AccountMeta::new_readonly(account.key(), account.is_signer)); + } + + xcall_account_infos.push(account.to_account_info()) + } + + let ix_data = get_handle_error_ix_data(sequence_no)?; + + let ix = Instruction { + program_id: config.xcall, + accounts: xcall_account_metas, + data: ix_data, + }; + + invoke(&ix, &xcall_account_infos)?; + + let (_, data) = get_return_data().unwrap(); + let mut data_slice: &[u8] = &data; + let res = QueryAccountsResponse::deserialize(&mut data_slice)?; + let mut res_accounts = res.accounts; + + account_metas.append(&mut res_accounts); + account_metas.push(AccountMetadata::new(config.xcall, false)); + + Ok(QueryAccountsPaginateResponse::new( + account_metas, + page, + limit, + )) +} + +pub fn get_handle_error_ix_data(sequence_no: u128) -> Result> { + let mut ix_args_data = vec![]; + let ix_args = xcall_type::HandleErrorArgs { sequence_no }; + ix_args.serialize(&mut ix_args_data)?; + + let ix_data = helper::get_instruction_data(QUERY_HANDLE_ERROR_ACCOUNTS_IX, ix_args_data); + Ok(ix_data) +} + +pub fn get_handle_message_ix_data( + from_nid: String, + message: Vec, + sequence_no: u128, +) -> Result> { + let mut ix_args_data = vec![]; + let ix_args = xcall_type::HandleMessageArgs { + from_nid, + message, + sequence_no, + }; + ix_args.serialize(&mut ix_args_data)?; + + let ix_data = helper::get_instruction_data(QUERY_HANDLE_MESSAGE_ACCOUNTS_IX, ix_args_data); + Ok(ix_data) +} + +#[derive(Accounts)] +pub struct QueryAccountsCtx<'info> { + #[account( + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump, + )] + pub config: Account<'info, Config>, +} diff --git a/contracts/solana/programs/centralized-connection/src/lib.rs b/contracts/solana/programs/centralized-connection/src/lib.rs new file mode 100644 index 00000000..7bb0ac67 --- /dev/null +++ b/contracts/solana/programs/centralized-connection/src/lib.rs @@ -0,0 +1,161 @@ +use std::ops::DerefMut; + +use anchor_lang::prelude::*; + +pub mod constants; +pub mod contexts; +pub mod error; +pub mod event; +pub mod helper; +pub mod instructions; +pub mod state; + +use contexts::*; +use instructions::*; +use state::*; + +use xcall_lib::query_account_type::{QueryAccountsPaginateResponse, QueryAccountsResponse}; + +declare_id!("8oxnXrSmqWJqkb2spZk2uz1cegzPsLy6nJp9XwFhkMD5"); + +#[program] +pub mod centralized_connection { + use super::*; + + pub fn initialize(ctx: Context, xcall: Pubkey, admin: Pubkey) -> Result<()> { + ctx.accounts + .config + .set_inner(Config::new(xcall, admin, ctx.bumps.config)); + ctx.accounts + .authority + .set_inner(Authority::new(ctx.bumps.authority)); + + Ok(()) + } + + pub fn send_message( + ctx: Context, + to: String, + sn: i64, + msg: Vec, + ) -> Result<()> { + let next_conn_sn = ctx.accounts.config.get_next_conn_sn()?; + + let mut fee = 0; + if sn >= 0 { + fee = ctx.accounts.network_fee.get(sn > 0)?; + } + + if fee > 0 { + helper::transfer_lamports( + &ctx.accounts.signer, + &ctx.accounts.config.to_account_info(), + &ctx.accounts.system_program, + fee, + )? + } + + emit!(event::SendMessage { + targetNetwork: to, + connSn: next_conn_sn, + msg: msg + }); + + Ok(()) + } + + #[allow(unused_variables)] + pub fn recv_message<'info>( + ctx: Context<'_, '_, '_, 'info, RecvMessage<'info>>, + src_network: String, + conn_sn: u128, + msg: Vec, + sequence_no: u128, + ) -> Result<()> { + helper::call_xcall_handle_message(ctx, src_network, msg, sequence_no) + } + + pub fn revert_message<'info>( + ctx: Context<'_, '_, '_, 'info, RevertMessage<'info>>, + sequence_no: u128, + ) -> Result<()> { + helper::call_xcall_handle_error(ctx, sequence_no) + } + + pub fn set_admin(ctx: Context, account: Pubkey) -> Result<()> { + let config = ctx.accounts.config.deref_mut(); + config.admin = account; + + Ok(()) + } + + #[allow(unused_variables)] + pub fn set_fee( + ctx: Context, + network_id: String, + message_fee: u64, + response_fee: u64, + ) -> Result<()> { + ctx.accounts.network_fee.set_inner(NetworkFee::new( + message_fee, + response_fee, + ctx.bumps.network_fee, + )); + + Ok(()) + } + + #[allow(unused_variables)] + pub fn get_fee(ctx: Context, network_id: String, response: bool) -> Result { + ctx.accounts.network_fee.get(response) + } + + pub fn claim_fees(ctx: Context) -> Result<()> { + let config = ctx.accounts.config.to_account_info(); + let fee = ctx.accounts.config.get_claimable_fees(&config)?; + + **config.try_borrow_mut_lamports()? -= fee; + **ctx.accounts.admin.try_borrow_mut_lamports()? += fee; + + Ok(()) + } + + #[allow(unused_variables)] + pub fn query_send_message_accounts<'info>( + ctx: Context<'_, '_, '_, 'info, QueryAccountsCtx<'info>>, + to: String, + sn: i64, + msg: Vec, + ) -> Result { + instructions::query_send_message_accounts(ctx, to) + } + + pub fn query_recv_message_accounts<'info>( + ctx: Context<'_, '_, '_, 'info, QueryAccountsCtx<'info>>, + src_network: String, + conn_sn: u128, + msg: Vec, + sequence_no: u128, + page: u8, + limit: u8, + ) -> Result { + instructions::query_recv_message_accounts( + ctx, + src_network, + conn_sn, + msg, + sequence_no, + page, + limit, + ) + } + + pub fn query_revert_message_accounts<'info>( + ctx: Context<'_, '_, '_, 'info, QueryAccountsCtx<'info>>, + sequence_no: u128, + page: u8, + limit: u8, + ) -> Result { + instructions::query_revert_message_accounts(ctx, sequence_no, page, limit) + } +} diff --git a/contracts/solana/programs/centralized-connection/src/state.rs b/contracts/solana/programs/centralized-connection/src/state.rs new file mode 100644 index 00000000..5096466a --- /dev/null +++ b/contracts/solana/programs/centralized-connection/src/state.rs @@ -0,0 +1,117 @@ +use anchor_lang::prelude::*; +use xcall_lib::xcall_connection_type; + +use crate::{constants, error::*}; + +/// The `Config` state of the centralized connection - the inner data of the +/// program-derived address +#[account] +pub struct Config { + pub admin: Pubkey, + pub xcall: Pubkey, + pub sn: u128, + pub bump: u8, +} + +impl Config { + /// The Config seed phrase to derive it's program-derived address + pub const SEED_PREFIX: &'static str = "config"; + + /// Account discriminator + Xcall public key + Admin public key + connection + /// sequence + bump + pub const LEN: usize = constants::ACCOUNT_DISCRIMINATOR_SIZE + 32 + 32 + 16 + 1 + 1; + + /// Creates a new centralized connection `Config` state + pub fn new(xcall: Pubkey, admin: Pubkey, bump: u8) -> Self { + Self { + xcall, + admin, + sn: 0, + bump, + } + } + + /// It throws error if `signer` is not an admin account + pub fn ensure_admin(&self, signer: Pubkey) -> Result<()> { + if self.admin != signer { + return Err(ConnectionError::OnlyAdmin.into()); + } + Ok(()) + } + + /// It throws error if `address` is not an xcall account + pub fn ensure_xcall(&self, address: Pubkey) -> Result<()> { + if self.xcall != address { + return Err(ConnectionError::OnlyXcall.into()); + } + Ok(()) + } + + pub fn get_next_conn_sn(&mut self) -> Result { + self.sn += 1; + Ok(self.sn) + } + + pub fn get_claimable_fees(&self, account: &AccountInfo) -> Result { + let rent = Rent::default(); + let rent_exempt_balance = rent.minimum_balance(Config::LEN); + + Ok(account.lamports() - rent_exempt_balance) + } +} + +#[account] +pub struct NetworkFee { + pub message_fee: u64, + pub response_fee: u64, + pub bump: u8, +} + +impl NetworkFee { + /// The Fee seed phrase to derive it's program-derived address + pub const SEED_PREFIX: &'static str = "fee"; + + /// Account discriminator + Message fee + Response fee + bump + pub const LEN: usize = constants::ACCOUNT_DISCRIMINATOR_SIZE + 8 + 8 + 1; + + /// Creates a new `Fee` state for a network_id + pub fn new(message_fee: u64, response_fee: u64, bump: u8) -> Self { + Self { + message_fee, + response_fee, + bump, + } + } + + pub fn get(&self, response: bool) -> Result { + let mut fee = self.message_fee; + if response { + fee += self.response_fee + } + + Ok(fee) + } +} + +#[account] +pub struct Receipt {} + +impl Receipt { + pub const SEED_PREFIX: &'static str = "receipt"; + + pub const LEN: usize = constants::ACCOUNT_DISCRIMINATOR_SIZE; +} + +#[account] +pub struct Authority { + pub bump: u8, +} + +impl Authority { + pub const SEED_PREFIX: &'static str = xcall_connection_type::CONNECTION_AUTHORITY_SEED; + pub const LEN: usize = 8 + 1; + + pub fn new(bump: u8) -> Self { + Self { bump } + } +} diff --git a/contracts/solana/programs/mock-dapp-multi/Cargo.toml b/contracts/solana/programs/mock-dapp-multi/Cargo.toml new file mode 100644 index 00000000..71273010 --- /dev/null +++ b/contracts/solana/programs/mock-dapp-multi/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "mock-dapp-multi" +version = "0.1.0" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "mock_dapp_multi" + +[features] +default = [] +cpi = ["no-entrypoint"] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +idl-build = ["anchor-lang/idl-build"] + +[dependencies] +anchor-lang = { workspace = true, features = ["init-if-needed"] } + +xcall-lib = { workspace = true } +rlp = { workspace = true } diff --git a/contracts/solana/programs/mock-dapp-multi/Xargo.toml b/contracts/solana/programs/mock-dapp-multi/Xargo.toml new file mode 100644 index 00000000..475fb71e --- /dev/null +++ b/contracts/solana/programs/mock-dapp-multi/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/contracts/solana/programs/mock-dapp-multi/src/constants.rs b/contracts/solana/programs/mock-dapp-multi/src/constants.rs new file mode 100644 index 00000000..fe8aad0a --- /dev/null +++ b/contracts/solana/programs/mock-dapp-multi/src/constants.rs @@ -0,0 +1 @@ +pub const ACCOUNT_DISCRIMINATOR_SIZE: usize = 8; diff --git a/contracts/solana/programs/mock-dapp-multi/src/error.rs b/contracts/solana/programs/mock-dapp-multi/src/error.rs new file mode 100644 index 00000000..64168535 --- /dev/null +++ b/contracts/solana/programs/mock-dapp-multi/src/error.rs @@ -0,0 +1,22 @@ +use anchor_lang::prelude::error_code; + +#[error_code] +pub enum DappError { + #[msg("Address Mismatch")] + AddressMismatch, + + #[msg("Rollback Mismatch")] + RollbackMismatch, + + #[msg("Invalid Rollback Message")] + InvalidRollbackMessage, + + #[msg("Uninitialized")] + Uninitialized, + + #[msg("Invalid Source")] + InvalidSource, + + #[msg("Only xcall")] + OnlyXcall, +} diff --git a/contracts/solana/programs/mock-dapp-multi/src/event.rs b/contracts/solana/programs/mock-dapp-multi/src/event.rs new file mode 100644 index 00000000..f7058446 --- /dev/null +++ b/contracts/solana/programs/mock-dapp-multi/src/event.rs @@ -0,0 +1,14 @@ +use anchor_lang::prelude::*; + +#[event] +pub struct MessageReceived { + pub from: String, + pub data: Vec, +} + +#[event] +pub struct RollbackDataReceived { + pub from: String, + pub ssn: u128, + pub rollback: Vec, +} diff --git a/contracts/solana/programs/mock-dapp-multi/src/helpers.rs b/contracts/solana/programs/mock-dapp-multi/src/helpers.rs new file mode 100644 index 00000000..39984112 --- /dev/null +++ b/contracts/solana/programs/mock-dapp-multi/src/helpers.rs @@ -0,0 +1,52 @@ +use anchor_lang::{prelude::*, solana_program::hash}; + +use xcall_lib::message::call_message::CallMessage; +use xcall_lib::message::call_message_persisted::CallMessagePersisted; +use xcall_lib::message::call_message_rollback::CallMessageWithRollback; +use xcall_lib::message::{msg_type::*, AnyMessage}; + +use crate::{CallMessageCtx, DappError}; + +pub fn process_message(message_type: u8, data: Vec, rollback: Vec) -> Result { + let msg_type: MessageType = message_type.into(); + + let message = if msg_type == MessageType::CallMessagePersisted { + AnyMessage::CallMessagePersisted(CallMessagePersisted { data }) + } else if msg_type == MessageType::CallMessageWithRollback { + if rollback.len() > 0 { + AnyMessage::CallMessageWithRollback(CallMessageWithRollback { data, rollback }) + } else { + return Err(DappError::InvalidRollbackMessage.into()); + } + } else { + AnyMessage::CallMessage(CallMessage { data }) + }; + + Ok(message) +} + +pub fn get_network_connections(ctx: &Context) -> Result<(Vec, Vec)> { + let connections = ctx.accounts.connections_account.connections.clone(); + + let mut sources = Vec::new(); + let mut destinations = Vec::new(); + for conn in connections { + sources.push(conn.src_endpoint); + destinations.push(conn.dst_endpoint); + } + + Ok((sources, destinations)) +} + +pub fn get_instruction_data(ix_name: &str, data: Vec) -> Vec { + let preimage = format!("{}:{}", "global", ix_name); + + let mut ix_discriminator = [0u8; 8]; + ix_discriminator.copy_from_slice(&hash::hash(preimage.as_bytes()).to_bytes()[..8]); + + let mut ix_data = Vec::new(); + ix_data.extend_from_slice(&ix_discriminator); + ix_data.extend_from_slice(&data); + + ix_data +} diff --git a/contracts/solana/programs/mock-dapp-multi/src/instructions/execute_forced_rollback.rs b/contracts/solana/programs/mock-dapp-multi/src/instructions/execute_forced_rollback.rs new file mode 100644 index 00000000..3fe45ae7 --- /dev/null +++ b/contracts/solana/programs/mock-dapp-multi/src/instructions/execute_forced_rollback.rs @@ -0,0 +1,41 @@ +use anchor_lang::prelude::*; +use xcall_lib::xcall_dapp_type; + +use crate::{state::*, xcall}; + +pub fn execute_forced_rollback<'info>( + ctx: Context<'_, '_, '_, 'info, ExecuteForcedRollbackCtx<'info>>, + req_id: u128, +) -> Result<()> { + let ix_data = xcall::get_handle_forced_rollback_ix_data(req_id)?; + + xcall::call_xcall_handle_forced_rollback( + &ix_data, + &ctx.accounts.config, + &ctx.accounts.sender, + &ctx.accounts.authority, + &ctx.accounts.system_program, + &ctx.remaining_accounts, + ) +} + +#[derive(Accounts)] +pub struct ExecuteForcedRollbackCtx<'info> { + #[account( + mut, + seeds = [Config::SEED_PREFIX.as_bytes()], + bump + )] + pub config: Account<'info, Config>, + + #[account( + seeds = [xcall_dapp_type::DAPP_AUTHORITY_SEED.as_bytes()], + bump + )] + pub authority: Account<'info, Authority>, + + #[account(mut)] + pub sender: Signer<'info>, + + pub system_program: Program<'info, System>, +} diff --git a/contracts/solana/programs/mock-dapp-multi/src/instructions/handle_message.rs b/contracts/solana/programs/mock-dapp-multi/src/instructions/handle_message.rs new file mode 100644 index 00000000..d8df457d --- /dev/null +++ b/contracts/solana/programs/mock-dapp-multi/src/instructions/handle_message.rs @@ -0,0 +1,54 @@ +use anchor_lang::prelude::*; +use xcall_lib::{network_address::NetworkAddress, xcall_dapp_type::HandleCallMessageResponse}; + +use crate::{error::*, state::*}; + +use std::str; + +pub fn handle_call_message<'info>( + ctx: Context<'_, '_, '_, 'info, HandleCallMessageCtx<'info>>, + from: NetworkAddress, + data: Vec, + _protocols: Vec, +) -> Result { + let (_, account) = from.parse_network_address(); + if ctx.accounts.signer.key().to_string() == account { + return Ok(HandleCallMessageResponse { + success: true, + message: "success".to_owned(), + }); + }; + + let msg_data = str::from_utf8(&data).unwrap(); + if msg_data == "rollback" { + return Ok(HandleCallMessageResponse { + success: false, + message: "Revert from dapp".to_owned(), + }); + } + + return Ok(HandleCallMessageResponse { + success: true, + message: "success".to_owned(), + }); +} + +#[derive(Accounts)] +pub struct HandleCallMessageCtx<'info> { + #[account(mut)] + pub signer: Signer<'info>, + + #[account( + owner = config.xcall_address @ DappError::OnlyXcall + )] + pub xcall: Signer<'info>, + + pub system_program: Program<'info, System>, + + #[account( + mut, + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump + )] + pub config: Account<'info, Config>, +} diff --git a/contracts/solana/programs/mock-dapp-multi/src/instructions/mod.rs b/contracts/solana/programs/mock-dapp-multi/src/instructions/mod.rs new file mode 100644 index 00000000..e48ebadd --- /dev/null +++ b/contracts/solana/programs/mock-dapp-multi/src/instructions/mod.rs @@ -0,0 +1,9 @@ +pub mod execute_forced_rollback; +pub mod handle_message; +pub mod query_accounts; +pub mod send_message; + +pub use execute_forced_rollback::*; +pub use handle_message::*; +pub use query_accounts::*; +pub use send_message::*; diff --git a/contracts/solana/programs/mock-dapp-multi/src/instructions/query_accounts.rs b/contracts/solana/programs/mock-dapp-multi/src/instructions/query_accounts.rs new file mode 100644 index 00000000..f3d44c16 --- /dev/null +++ b/contracts/solana/programs/mock-dapp-multi/src/instructions/query_accounts.rs @@ -0,0 +1,28 @@ +use anchor_lang::{prelude::*, solana_program::system_program}; +use xcall_lib::query_account_type::{AccountMetadata, QueryAccountsResponse}; + +use crate::state::*; + +pub fn query_handle_call_message_accounts( + ctx: Context, +) -> Result { + let config = &ctx.accounts.config; + + let account_metas = vec![ + AccountMetadata::new_readonly(system_program::id(), false), + AccountMetadata::new(config.key(), false), + ]; + + Ok(QueryAccountsResponse { + accounts: account_metas, + }) +} + +#[derive(Accounts)] +pub struct QueryAccountsCtx<'info> { + #[account( + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump, + )] + pub config: Account<'info, Config>, +} diff --git a/contracts/solana/programs/mock-dapp-multi/src/instructions/send_message.rs b/contracts/solana/programs/mock-dapp-multi/src/instructions/send_message.rs new file mode 100644 index 00000000..f12e304e --- /dev/null +++ b/contracts/solana/programs/mock-dapp-multi/src/instructions/send_message.rs @@ -0,0 +1,119 @@ +use anchor_lang::prelude::*; +use xcall_lib::{message::envelope::Envelope, network_address::*, xcall_dapp_type}; + +use crate::{helpers, xcall, state::*}; + +pub fn send_message<'info>( + ctx: Context<'_, '_, '_, 'info, CallMessageCtx<'info>>, + to: NetworkAddress, + data: Vec, + msg_type: u32, + rollback: Vec, +) -> Result<()> { + let message = helpers::process_message(msg_type as u8, data, rollback).unwrap(); + let (sources, destinations) = helpers::get_network_connections(&ctx)?; + + let envelope = Envelope::new(message, sources.clone(), destinations); + let msg = rlp::encode(&envelope).to_vec(); + + let ix_data = xcall::get_send_call_ix_data(msg, to)?; + + xcall::call_xcall_send_call( + &ix_data, + &ctx.accounts.config, + &ctx.accounts.sender, + &ctx.accounts.authority, + &ctx.accounts.system_program, + &ctx.remaining_accounts, + ) +} + +pub fn add_connection( + ctx: Context, + _network_id: String, + src_endpoint: String, + dst_endpoint: String, +) -> Result<()> { + ctx.accounts + .connection_account + .connections + .push(Connection { + src_endpoint, + dst_endpoint, + }); + + Ok(()) +} + +#[derive(Accounts)] +pub struct InitializeCtx<'info> { + #[account( + init, + payer = sender, + space = Config::MAX_SPACE, + seeds = [Config::SEED_PREFIX.as_bytes()], + bump + )] + pub config: Account<'info, Config>, + + #[account( + init, + payer = sender, + space = Authority::MAX_SPACE, + seeds = [xcall_dapp_type::DAPP_AUTHORITY_SEED.as_bytes()], + bump + )] + pub authority: Account<'info, Authority>, + + #[account(mut)] + pub sender: Signer<'info>, + + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +#[instruction(to: NetworkAddress )] +pub struct CallMessageCtx<'info> { + #[account( + mut, + seeds = [Config::SEED_PREFIX.as_bytes()], + bump + )] + pub config: Account<'info, Config>, + + #[account( + seeds = [xcall_dapp_type::DAPP_AUTHORITY_SEED.as_bytes()], + bump + )] + pub authority: Account<'info, Authority>, + + #[account( + mut, + seeds = [Connections::SEED_PREFIX.as_bytes(), to.nid().as_bytes()], + bump + )] + pub connections_account: Account<'info, Connections>, + + #[account(mut)] + pub sender: Signer<'info>, + + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +#[instruction(network_id: String)] +pub struct AddConnectionCtx<'info> { + #[account( + init_if_needed, + payer = sender, + space= Connections::MAX_SPACE, + seeds=[Connections::SEED_PREFIX.as_bytes(), network_id.as_bytes()], + bump + )] + pub connection_account: Account<'info, Connections>, + + #[account(mut)] + pub sender: Signer<'info>, + + pub system_program: Program<'info, System>, +} diff --git a/contracts/solana/programs/mock-dapp-multi/src/lib.rs b/contracts/solana/programs/mock-dapp-multi/src/lib.rs new file mode 100644 index 00000000..c56b9bb8 --- /dev/null +++ b/contracts/solana/programs/mock-dapp-multi/src/lib.rs @@ -0,0 +1,80 @@ +use anchor_lang::prelude::*; +use xcall_lib::{network_address::*, query_account_type::QueryAccountsResponse, xcall_dapp_type}; + +pub mod constants; +pub mod error; +pub mod event; +pub mod helpers; +pub mod instructions; +pub mod state; +pub mod xcall; + +use error::*; +use instructions::*; +use state::*; + +declare_id!("hSruQVdc5a9dUAqHfRaLcn2S9cbgdpoomG5eWhhDS5W"); + +#[program] +pub mod mock_dapp_multi { + use super::*; + + pub fn initialize(ctx: Context, xcall_address: Pubkey) -> Result<()> { + ctx.accounts.config.set_inner(Config { + xcall_address, + sn: 0, + bump: ctx.bumps.config, + }); + ctx.accounts.authority.set_inner(Authority { + bump: ctx.bumps.authority, + }); + Ok(()) + } + + pub fn send_call_message<'info>( + ctx: Context<'_, '_, '_, 'info, CallMessageCtx<'info>>, + to: NetworkAddress, + data: Vec, + msg_type: u32, + rollback: Vec, + ) -> Result<()> { + let _ = instructions::send_message::send_message(ctx, to, data, msg_type, rollback); + Ok(()) + } + + pub fn handle_call_message<'info>( + ctx: Context<'_, '_, '_, 'info, HandleCallMessageCtx<'info>>, + from: NetworkAddress, + data: Vec, + protocols: Vec, + ) -> Result { + instructions::handle_message::handle_call_message(ctx, from, data, protocols) + } + + pub fn add_connection( + ctx: Context, + network_id: String, + src_endpoint: String, + dst_endpoint: String, + ) -> Result<()> { + instructions::send_message::add_connection(ctx, network_id, src_endpoint, dst_endpoint)?; + Ok(()) + } + + pub fn execute_forced_rollback<'info>( + ctx: Context<'_, '_, '_, 'info, ExecuteForcedRollbackCtx<'info>>, + req_id: u128, + ) -> Result<()> { + instructions::execute_forced_rollback(ctx, req_id) + } + + #[allow(unused_variables)] + pub fn query_handle_call_message_accounts( + ctx: Context, + from: NetworkAddress, + data: Vec, + protocols: Vec, + ) -> Result { + instructions::query_handle_call_message_accounts(ctx) + } +} diff --git a/contracts/solana/programs/mock-dapp-multi/src/state.rs b/contracts/solana/programs/mock-dapp-multi/src/state.rs new file mode 100644 index 00000000..82e90b21 --- /dev/null +++ b/contracts/solana/programs/mock-dapp-multi/src/state.rs @@ -0,0 +1,41 @@ +use anchor_lang::prelude::*; +use xcall_lib::xcall_dapp_type; + +#[account] +pub struct Config { + pub sn: u128, + pub xcall_address: Pubkey, + pub bump: u8, +} + +impl Config { + pub const SEED_PREFIX: &'static str = "config"; + pub const MAX_SPACE: usize = 8 + 16 + 32 + 1; +} + +#[account] +pub struct Connections { + pub connections: Vec, +} + +impl Connections { + pub const SEED_PREFIX: &'static str = "connections"; + pub const MAX_SPACE: usize = 4 + 256 + 4 + 256; +} + +#[account] +#[derive(Debug)] +pub struct Connection { + pub src_endpoint: String, + pub dst_endpoint: String, +} + +#[account] +pub struct Authority { + pub bump: u8, +} + +impl Authority { + pub const SEED_PREFIX: &'static str = xcall_dapp_type::DAPP_AUTHORITY_SEED; + pub const MAX_SPACE: usize = 8 + 1; +} diff --git a/contracts/solana/programs/mock-dapp-multi/src/xcall.rs b/contracts/solana/programs/mock-dapp-multi/src/xcall.rs new file mode 100644 index 00000000..d4cc32f7 --- /dev/null +++ b/contracts/solana/programs/mock-dapp-multi/src/xcall.rs @@ -0,0 +1,110 @@ +use anchor_lang::{ + prelude::*, + solana_program::{instruction::Instruction, program::invoke_signed}, +}; +use xcall_lib::{ + network_address::NetworkAddress, + xcall_type::{self, HANDLE_FORCED_ROLLBACK_IX, SEND_CALL_IX}, +}; + +use crate::{helpers, state::*, Config}; + +pub fn call_xcall_send_call<'info>( + ix_data: &Vec, + config: &Account<'info, Config>, + signer: &Signer<'info>, + authority: &Account<'info, Authority>, + system_program: &Program<'info, System>, + remaining_accounts: &[AccountInfo<'info>], +) -> Result<()> { + let mut account_metas: Vec = vec![ + AccountMeta::new(signer.key(), true), + AccountMeta::new_readonly(authority.key(), true), + AccountMeta::new_readonly(system_program.key(), false), + ]; + let mut account_infos: Vec> = vec![ + signer.to_account_info(), + authority.to_account_info(), + system_program.to_account_info(), + ]; + for (_, account) in remaining_accounts.iter().enumerate() { + if account.is_writable { + account_metas.push(AccountMeta::new(account.key(), account.is_signer)) + } else { + account_metas.push(AccountMeta::new_readonly(account.key(), account.is_signer)) + } + account_infos.push(account.to_account_info()); + } + let ix = Instruction { + program_id: config.xcall_address, + accounts: account_metas, + data: ix_data.clone(), + }; + + invoke_signed( + &ix, + &account_infos, + &[&[Authority::SEED_PREFIX.as_bytes(), &[authority.bump]]], + )?; + + Ok(()) +} + +pub fn call_xcall_handle_forced_rollback<'info>( + ix_data: &Vec, + config: &Account<'info, Config>, + signer: &Signer<'info>, + authority: &Account<'info, Authority>, + system_program: &Program<'info, System>, + remaining_accounts: &[AccountInfo<'info>], +) -> Result<()> { + let mut account_metas: Vec = vec![ + AccountMeta::new(signer.key(), true), + AccountMeta::new_readonly(authority.key(), true), + AccountMeta::new_readonly(system_program.key(), false), + ]; + let mut account_infos: Vec> = vec![ + signer.to_account_info(), + authority.to_account_info(), + system_program.to_account_info(), + ]; + for (_, account) in remaining_accounts.iter().enumerate() { + if account.is_writable { + account_metas.push(AccountMeta::new(account.key(), account.is_signer)) + } else { + account_metas.push(AccountMeta::new_readonly(account.key(), account.is_signer)) + } + account_infos.push(account.to_account_info()); + } + let ix = Instruction { + program_id: config.xcall_address, + accounts: account_metas, + data: ix_data.clone(), + }; + + invoke_signed( + &ix, + &account_infos, + &[&[Authority::SEED_PREFIX.as_bytes(), &[authority.bump]]], + )?; + + Ok(()) +} + +pub fn get_send_call_ix_data(msg: Vec, to: NetworkAddress) -> Result> { + let mut ix_args_data = vec![]; + let ix_args = xcall_type::SendCallArgs { msg, to }; + ix_args.serialize(&mut ix_args_data)?; + + let ix_data = helpers::get_instruction_data(SEND_CALL_IX, ix_args_data); + Ok(ix_data) +} + +pub fn get_handle_forced_rollback_ix_data(req_id: u128) -> Result> { + let mut ix_args_data = vec![]; + let ix_args = xcall_type::HandleForcedRollback { req_id }; + ix_args.serialize(&mut ix_args_data)?; + + let ix_data = helpers::get_instruction_data(HANDLE_FORCED_ROLLBACK_IX, ix_args_data); + Ok(ix_data) +} diff --git a/contracts/solana/programs/xcall/Cargo.toml b/contracts/solana/programs/xcall/Cargo.toml new file mode 100644 index 00000000..b79aab7a --- /dev/null +++ b/contracts/solana/programs/xcall/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "xcall" +version = "0.1.0" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "xcall" + +[features] +default = [] +cpi = ["no-entrypoint"] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +idl-build = ["anchor-lang/idl-build"] + +[dependencies] +anchor-lang = { workspace = true, features = ["init-if-needed"] } + +hex = { workspace = true } +rlp = { workspace = true } +xcall-lib = { workspace = true } diff --git a/contracts/solana/programs/xcall/Xargo.toml b/contracts/solana/programs/xcall/Xargo.toml new file mode 100644 index 00000000..475fb71e --- /dev/null +++ b/contracts/solana/programs/xcall/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/contracts/solana/programs/xcall/src/connection.rs b/contracts/solana/programs/xcall/src/connection.rs new file mode 100644 index 00000000..ee2ad0d3 --- /dev/null +++ b/contracts/solana/programs/xcall/src/connection.rs @@ -0,0 +1,174 @@ +use std::str::FromStr; + +use anchor_lang::{ + prelude::*, + solana_program::{ + instruction::Instruction, + program::{get_return_data, invoke, invoke_signed}, + }, +}; +use xcall_lib::xcall_connection_type::{self, QUERY_SEND_MESSAGE_ACCOUNTS_IX, SEND_MESSAGE_IX}; + +use crate::{error::*, helper::get_instruction_data, state::*}; + +/// Queries the fee for sending a message through a connection program. +/// +/// This function sends an instruction to the connection program, querying the network fee for sending +/// a message. It prepares the required accounts and instruction data, invokes the program, and +/// retrieves the fee from the program's response. +/// +/// # Parameters +/// - `source`: A reference to a string representing the connection's public key as a string. +/// - `ix_data`: A reference to a vector containing the serialized instruction data. +/// - `network_fee_account`: A reference to the account that holds the network fee. +/// +/// # Returns +/// - `Result`: The fee required by the connection program, or an error if the query fails. +pub fn query_connection_fee<'info>( + source: &String, + ix_data: &Vec, + network_fee_account: &AccountInfo, +) -> Result { + let connection = Pubkey::from_str(&source).map_err(|_| XcallError::InvalidPubkey)?; + + let account_metas = vec![AccountMeta::new(network_fee_account.key(), false)]; + let account_infos = vec![network_fee_account.to_account_info()]; + + let ix = Instruction { + program_id: connection, + accounts: account_metas, + data: ix_data.clone(), + }; + + invoke(&ix, &account_infos)?; + + let (_, fee) = get_return_data().unwrap(); + let fee = u64::deserialize(&mut fee.as_ref())?; + + Ok(fee) +} + +/// Sends a cross-chain message to a connection program. +/// +/// This function constructs and sends an `Instruction` to a connection program, using the provided +/// configuration and remaining accounts. It handles the indexing of accounts based on the +/// specified `index`, prepares the necessary `AccountMeta` and `AccountInfo` objects, and then +/// invokes the instruction +/// +/// # Arguments +/// - `index`: The index of the connection-related accounts in the `remaining_accounts` array. +/// Each connection requires three accounts: connection, connection configuration, and network fee +/// - `ix_data`: A vector of bytes representing the serialized instruction data to be sent. +/// - `sysvar_account`: The instruction sysvar account, used to verify if the current instruction is a +/// program invocation. +/// - `signer`: The account that signs the transaction. +/// - `system_program`: The system program account. +/// - `remaining_accounts`: A slice of `AccountInfo` containing additional accounts required +/// for the connection, including the connection program, connection configuration, and network fee +/// +/// # Returns +/// - `Result<()>`: Returns `Ok(())` if the message was successfully sent, otherwise returns an error. +pub fn call_connection_send_message<'info>( + index: usize, + ix_data: &Vec, + protocols: &Vec, + config: &Account<'info, Config>, + signer: &Signer<'info>, + system_program: &Program<'info, System>, + remaining_accounts: &[AccountInfo<'info>], +) -> Result<()> { + let connection = &remaining_accounts[3 * index]; + let conn_config = &remaining_accounts[3 * index + 1]; + let network_fee = &remaining_accounts[3 * index + 2]; + + if !protocols.contains(&connection.key.to_string()) { + return Err(XcallError::ProtocolMismatch.into()); + } + + let account_metas: Vec = vec![ + AccountMeta::new(signer.key(), true), + AccountMeta::new_readonly(system_program.key(), false), + AccountMeta::new_readonly(config.key(), true), + AccountMeta::new(conn_config.key(), false), + AccountMeta::new_readonly(network_fee.key(), false), + ]; + let account_infos: Vec> = vec![ + conn_config.to_account_info(), + signer.to_account_info(), + system_program.to_account_info(), + config.to_account_info(), + network_fee.to_account_info(), + ]; + let ix = Instruction { + program_id: connection.key(), + accounts: account_metas, + data: ix_data.clone(), + }; + + invoke_signed( + &ix, + &account_infos, + &[&[Config::SEED_PREFIX.as_bytes(), &[config.bump]]], + )?; + + Ok(()) +} + +/// Prepares the instruction data to call the `send_message` instruction on a connection program. +/// +/// This function constructs the instruction data required to invoke the `send_message` instruction +/// on a specified connection (source) program. It serializes the message arguments, including the +/// destination network address, sequence number, and the message payload, into a format that the +/// connection program expects. +/// +/// # Arguments +/// - `to`: A reference to the destination network address. +/// - `sn`: The sequence number associated with the message. +/// - `message`: A vector of bytes representing the message payload. +/// +/// # Returns +/// - `Result>`: Serialized instruction data for the `send_message` instruction. +/// Returns an error if serialization fails. +pub fn get_send_message_ix_data(to: &String, sn: i64, message: Vec) -> Result> { + let mut data = vec![]; + let args = xcall_connection_type::SendMessageArgs { + to: to.to_owned(), + sn, + msg: message, + }; + args.serialize(&mut data)?; + + let ix_data = get_instruction_data(SEND_MESSAGE_IX, data); + Ok(ix_data) +} + +/// Prepares the instruction data to query send message accounts of a connection program. +/// +/// This function constructs the data required to invoke the `query_send_message_accounts` +/// instruction on a connection program. It serializes the message arguments, including the +/// destination network address, sequence number, and the message payload, into the expected format. +/// +/// # Arguments +/// - `to`: A reference to the destination network address. +/// - `sn`: The sequence number associated with the message. +/// - `message`: A vector of bytes representing the message payload. +/// +/// # Returns +/// - `Result>`: Serialized instruction data for the `query_send_message_accounts` +/// instruction. Returns an error if serialization fails. +pub fn get_query_send_message_accounts_ix_data( + to: &String, + sn: i64, + message: Vec, +) -> Result> { + let mut data = vec![]; + let args = xcall_connection_type::SendMessageArgs { + to: to.to_owned(), + sn, + msg: message, + }; + args.serialize(&mut data)?; + + let ix_data = get_instruction_data(QUERY_SEND_MESSAGE_ACCOUNTS_IX, data); + Ok(ix_data) +} diff --git a/contracts/solana/programs/xcall/src/constants.rs b/contracts/solana/programs/xcall/src/constants.rs new file mode 100644 index 00000000..820b948b --- /dev/null +++ b/contracts/solana/programs/xcall/src/constants.rs @@ -0,0 +1,4 @@ +pub const ACCOUNT_DISCRIMINATOR_SIZE: usize = 8; + +pub const MAX_ROLLBACK_SIZE: usize = 1024; +pub const MAX_DATA_SIZE: usize = 2048; diff --git a/contracts/solana/programs/xcall/src/dapp.rs b/contracts/solana/programs/xcall/src/dapp.rs new file mode 100644 index 00000000..1623f43e --- /dev/null +++ b/contracts/solana/programs/xcall/src/dapp.rs @@ -0,0 +1,165 @@ +use anchor_lang::{ + prelude::*, + solana_program::{ + instruction::Instruction, + program::{get_return_data, invoke_signed}, + }, +}; +use xcall_lib::{ + network_address::NetworkAddress, + xcall_dapp_type::{ + self, HandleCallMessageResponse, HANDLE_CALL_MESSAGE_IX, QUERY_HANDLE_CALL_MESSAGE_IX, + }, +}; + +use crate::{error::XcallError, event, helper, state::*, types::result::CSResponseType}; + +/// Handles the response from the DApp after executing a call. +/// +/// This function processes the response from the DApp's `handle_call_message` instruction, +/// determining whether the call was successful or not. It then emits an event indicating the +/// outcome of the call and returns the appropriate response type. +/// +/// # Parameters +/// - `req_id`: The unique identifier of the request for which the call was executed. +/// - `dapp_res`: The response from the DApp, containing information about whether the call was +/// successful and any associated message. +/// +/// # Returns +/// - `Result`: The response type indicating success or failure. +pub fn handle_response( + req_id: u128, + dapp_res: HandleCallMessageResponse, +) -> Result { + let res_code = match dapp_res.success { + true => CSResponseType::CSResponseSuccess, + false => CSResponseType::CSResponseFailure, + }; + + emit!(event::CallExecuted { + reqId: req_id, + code: res_code.clone().into(), + msg: dapp_res.message, + }); + + Ok(res_code) +} + +/// Invokes the `handle_call_message` instruction on a DApp program. +/// +/// This function sends an instruction to the DApp program to handle a call message. It prepares +/// the required accounts and instruction data, invokes the instruction, and retrieves the response +/// from the DApp program. +/// +/// # Parameters +/// - `dapp_key`: The public key of the DApp program that will handle the call message. +/// - `ix_data`: The serialized instruction data to be sent to the DApp program. +/// - `config`: The configuration account, used for verifying the program's settings and bump seed. +/// - `signer`: The account that signs the transaction, typically the one paying for it. +/// - `remaining_accounts`: A slice of account infos required by the DApp program, which will be +/// provided as additional accounts to the instruction. +/// +/// # Returns +/// - `Result`: The response from the DApp program, +/// indicating the result of handling the call message, or an error if the invocation fails. +pub fn invoke_handle_call_message_ix<'info>( + dapp_key: Pubkey, + ix_data: Vec, + config: &Account<'info, Config>, + signer: &Signer<'info>, + remaining_accounts: &[AccountInfo<'info>], +) -> Result { + let mut account_metas: Vec = vec![ + AccountMeta::new(signer.key(), true), + AccountMeta::new_readonly(config.key(), true), + ]; + let mut account_infos: Vec> = + vec![signer.to_account_info(), config.to_account_info()]; + for account in remaining_accounts { + if account.is_writable { + account_metas.push(AccountMeta::new(account.key(), account.is_signer)) + } else { + account_metas.push(AccountMeta::new_readonly(account.key(), account.is_signer)) + } + account_infos.push(account.to_account_info()); + } + let ix = Instruction { + program_id: dapp_key, + accounts: account_metas, + data: ix_data, + }; + + invoke_signed( + &ix, + &account_infos, + &[&[Config::SEED_PREFIX.as_bytes(), &[config.bump]]], + )?; + + let (_, data) = get_return_data().ok_or(XcallError::InvalidResponse)?; + let mut data_slice: &[u8] = &data; + let res = xcall_dapp_type::HandleCallMessageResponse::deserialize(&mut data_slice)?; + + Ok(res) +} + +/// Prepares the instruction data to call the `handle_call_message` instruction on a DApp program. +/// +/// This function constructs the instruction data required to invoke the `handle_call_message` +/// instruction on a DApp program. It serializes the message arguments, including the +/// from network address, data, and the protocols, into a format that the DApp program expects. +/// +/// # Arguments +/// - `from`: A reference to the source network address. +/// - `data`: A vector of bytes representing the message payload. +/// - `protocols`: The list of protocols used to receive message in source and destination chain. +/// +/// # Returns +/// - `Result>`: Serialized instruction data for the `handle_call_message` instruction. +pub fn get_handle_call_message_ix_data( + from: NetworkAddress, + data: Vec, + protocols: Vec, +) -> Result> { + let mut ix_args_data = vec![]; + let ix_args = xcall_dapp_type::HandleCallMessageArgs { + from, + data, + protocols, + }; + ix_args.serialize(&mut ix_args_data)?; + + let ix_data = helper::get_instruction_data(HANDLE_CALL_MESSAGE_IX, ix_args_data); + Ok(ix_data) +} + +/// Prepares the instruction data to call the `query_handle_call_message_accounts` instruction +/// on DApp program. +/// +/// This function constructs the instruction data to invoke the`query_handle_call_message_accounts` +/// instruction on a DApp program. It serializes the message arguments, including the +/// from network address, data, and the protocols, into a format that the DApp program expects. +/// +/// # Arguments +/// - `from`: A reference to the source network address. +/// - `data`: A vector of bytes representing the message payload. +/// - `protocols`: The list of protocols used to receive message in source and destination chain. +/// +/// # Returns +/// - `Result>`: Serialized instruction data for the `query_handle_call_message_accounts` +/// instruction. +pub fn get_query_handle_call_message_accounts_ix_data( + from: NetworkAddress, + data: Vec, + protocols: Vec, +) -> Result> { + let mut ix_args_data = vec![]; + let ix_args = xcall_dapp_type::HandleCallMessageArgs { + from, + data, + protocols, + }; + ix_args.serialize(&mut ix_args_data)?; + + let ix_data = helper::get_instruction_data(QUERY_HANDLE_CALL_MESSAGE_IX, ix_args_data); + Ok(ix_data) +} diff --git a/contracts/solana/programs/xcall/src/error.rs b/contracts/solana/programs/xcall/src/error.rs new file mode 100644 index 00000000..bdce05a6 --- /dev/null +++ b/contracts/solana/programs/xcall/src/error.rs @@ -0,0 +1,106 @@ +use anchor_lang::prelude::error_code; + +#[error_code] +pub enum XcallError { + #[msg("Only Admin")] + OnlyAdmin, + + #[msg("Invalid admin key")] + InvalidAdminKey, + + #[msg("Invalid few handler")] + InvalidFeeHandler, + + #[msg("Invalid signer")] + InvalidSigner, + + #[msg("Maximum rollback data size exceeded")] + MaxRollbackSizeExceeded, + + #[msg("Invalid SN")] + InvalidSn, + + #[msg("Rollback not enabled")] + RollbackNotEnabled, + + #[msg("Maximum data size exceeded")] + MaxDataSizeExceeded, + + #[msg("Dapp authority not provided")] + DappAuthorityNotProvided, + + #[msg("Protocol mismatch")] + ProtocolMismatch, + + #[msg("Source protocols not specified")] + SourceProtocolsNotSpecified, + + #[msg("Destination protocols not specified")] + DestinationProtocolsNotSpecified, + + #[msg("Rollback not possible")] + RollbackNotPossible, + + #[msg("Call request not found")] + CallRequestNotFound, + + #[msg("No rollback data")] + NoRollbackData, + + #[msg("Revert from dapp")] + RevertFromDapp, + + #[msg("Invalid reply received")] + InvalidReplyReceived, + + #[msg("Decode failed")] + DecodeFailed, + + #[msg("Invalid source")] + InvalidSource, + + #[msg("Invalid request id")] + InvalidRequestId, + + #[msg("Data mismatch")] + DataMismatch, + + #[msg("Invalid pubkey")] + InvalidPubkey, + + #[msg("Invalid response from dapp")] + InvalidResponse, + + #[msg("Request is still pending")] + RequestPending, + + #[msg("Proxy request account is not specified")] + ProxyRequestAccountNotSpecified, + + #[msg("Proxy request account must not be specified")] + ProxyRequestAccountMustNotBeSpecified, + + #[msg("Rollback account is not specified")] + RollbackAccountNotSpecified, + + #[msg("Rollback account must not be specified")] + RollbackAccountMustNotBeSpecified, + + #[msg("Pending request account is not specified")] + PendingRequestAccountNotSpecified, + + #[msg("Pending request account must not be specified")] + PendingRequestAccountMustNotBeSpecified, + + #[msg("Pending response account is not specified")] + PendingResponseAccountNotSpecified, + + #[msg("Pending response account must not be specified")] + PendingResponseAccountMustNotBeSpecified, + + #[msg("Successful response account is not specified")] + SuccessfulResponseAccountNotSpecified, + + #[msg("Successful response account must not be specified")] + SuccessfulResponseAccountMustNotBeSpecified, +} diff --git a/contracts/solana/programs/xcall/src/event.rs b/contracts/solana/programs/xcall/src/event.rs new file mode 100644 index 00000000..c2190054 --- /dev/null +++ b/contracts/solana/programs/xcall/src/event.rs @@ -0,0 +1,42 @@ +#![allow(non_snake_case)] + +use anchor_lang::prelude::*; + +#[event] +pub struct CallMessageSent { + pub from: Pubkey, + pub to: String, + pub sn: u128, +} + +#[event] +pub struct CallMessage { + pub from: String, + pub to: String, + pub sn: u128, + pub reqId: u128, + pub data: Vec, +} + +#[event] +pub struct CallExecuted { + pub reqId: u128, + pub code: u8, + pub msg: String, +} + +#[event] +pub struct ResponseMessage { + pub code: u8, + pub sn: u128, +} + +#[event] +pub struct RollbackMessage { + pub sn: u128, +} + +#[event] +pub struct RollbackExecuted { + pub sn: u128, +} diff --git a/contracts/solana/programs/xcall/src/helper.rs b/contracts/solana/programs/xcall/src/helper.rs new file mode 100644 index 00000000..e47eb6a2 --- /dev/null +++ b/contracts/solana/programs/xcall/src/helper.rs @@ -0,0 +1,79 @@ +use anchor_lang::{ + prelude::*, + solana_program::{hash, sysvar::instructions::get_instruction_relative}, +}; +use xcall_lib::{ + xcall_connection_type::CONNECTION_AUTHORITY_SEED, xcall_dapp_type::DAPP_AUTHORITY_SEED, +}; + +use crate::{constants::*, error::*}; + +pub fn ensure_data_length(data: &[u8]) -> Result<()> { + require_gte!( + MAX_DATA_SIZE, + data.len() as usize, + XcallError::MaxDataSizeExceeded + ); + + Ok(()) +} + +pub fn ensure_rollback_size(rollback: &[u8]) -> Result<()> { + if rollback.is_empty() { + return Err(XcallError::NoRollbackData.into()); + } + if rollback.len() > MAX_ROLLBACK_SIZE { + return Err(XcallError::MaxRollbackSizeExceeded.into()); + } + + Ok(()) +} + +pub fn ensure_dapp_authority(dapp_program_id: &Pubkey, dapp_authority_key: Pubkey) -> Result<()> { + let (derived_key, _) = + Pubkey::find_program_address(&[DAPP_AUTHORITY_SEED.as_bytes()], &dapp_program_id); + if derived_key != dapp_authority_key { + return Err(XcallError::InvalidSigner.into()); + } + + Ok(()) +} + +pub fn ensure_connection_authority( + conn_program_id: &Pubkey, + conn_authority_key: Pubkey, +) -> Result<()> { + let (derived_key, _) = + Pubkey::find_program_address(&[CONNECTION_AUTHORITY_SEED.as_bytes()], &conn_program_id); + if derived_key != conn_authority_key { + return Err(XcallError::InvalidSigner.into()); + } + + Ok(()) +} + +pub fn is_program(sysvar_account_info: &AccountInfo) -> Result { + let current_ix = get_instruction_relative(0, sysvar_account_info)?; + if current_ix.program_id != crate::id() { + return Ok(true); + } + + Ok(false) +} + +pub fn hash_data(data: &Vec) -> Vec { + return hash::hash(data).to_bytes().to_vec(); +} + +pub fn get_instruction_data(ix_name: &str, data: Vec) -> Vec { + let preimage = format!("{}:{}", "global", ix_name); + + let mut ix_discriminator = [0u8; 8]; + ix_discriminator.copy_from_slice(&hash::hash(preimage.as_bytes()).to_bytes()[..8]); + + let mut ix_data = Vec::new(); + ix_data.extend_from_slice(&ix_discriminator); + ix_data.extend_from_slice(&data); + + ix_data +} diff --git a/contracts/solana/programs/xcall/src/instructions/codec.rs b/contracts/solana/programs/xcall/src/instructions/codec.rs new file mode 100644 index 00000000..8ded55b1 --- /dev/null +++ b/contracts/solana/programs/xcall/src/instructions/codec.rs @@ -0,0 +1,36 @@ +use anchor_lang::prelude::*; + +use crate::types::{ + message::{CSMessage, CSMessageDecoded, CSMessageType}, + request::CSMessageRequest, + result::CSMessageResult, +}; + +pub fn decode_cs_message(message: Vec) -> Result { + let cs_message: CSMessage = message.try_into()?; + + match cs_message.message_type() { + CSMessageType::CSMessageRequest => { + let request: CSMessageRequest = cs_message.payload().try_into()?; + Ok(CSMessageDecoded { + message_type: cs_message.message_type, + request: Some(request), + result: None, + }) + } + CSMessageType::CSMessageResult => { + let result: CSMessageResult = cs_message.payload().try_into()?; + Ok(CSMessageDecoded { + message_type: cs_message.message_type, + request: None, + result: Some(result), + }) + } + } +} + +#[derive(Accounts)] +pub struct DecodeCSMessageContext<'info> { + /// The solana system program account, used for creating and managing accounts. + pub system_program: Program<'info, System>, +} diff --git a/contracts/solana/programs/xcall/src/instructions/config.rs b/contracts/solana/programs/xcall/src/instructions/config.rs new file mode 100644 index 00000000..d3b6498c --- /dev/null +++ b/contracts/solana/programs/xcall/src/instructions/config.rs @@ -0,0 +1,70 @@ +use anchor_lang::prelude::*; + +use crate::{error::XcallError, state::*}; + +pub fn initialize(ctx: Context, network_id: String) -> Result<()> { + ctx.accounts + .config + .new(ctx.accounts.signer.key(), network_id, ctx.bumps.config); + + Ok(()) +} + +pub fn set_admin(ctx: Context, account: Pubkey) -> Result<()> { + ctx.accounts.config.set_admin(account); + + Ok(()) +} + +#[derive(Accounts)] +pub struct ConfigCtx<'info> { + /// The configuration account, which stores important settings for the program. + /// This account is initialized only once during the lifetime of program and it will + /// throw error if tries to initialize twice + #[account( + init, + payer = signer, + space = Config::SIZE, + seeds = [Config::SEED_PREFIX.as_bytes()], + bump + )] + pub config: Account<'info, Config>, + + /// The account that signs and pays for the transaction. This account is mutable + /// because it will be debited for any fees or rent required during the transaction. + #[account(mut)] + pub signer: Signer<'info>, + + /// The solana system program account, used for creating and managing accounts. + system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct GetConfigCtx<'info> { + /// The configuration account, which stores important settings for the program. + #[account( + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump + )] + pub config: Account<'info, Config>, +} + +#[derive(Accounts)] +pub struct SetAdminCtx<'info> { + /// The configuration account, which stores important settings for the program. + /// This account is mutable because the admin of the program will be updated. + #[account( + mut, + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump + )] + pub config: Account<'info, Config>, + + /// The account that signs and pays for the transaction. This account is checked + /// against the `config.admin` to ensure it is valid. + #[account( + mut, + address = config.admin @ XcallError::OnlyAdmin + )] + pub admin: Signer<'info>, +} diff --git a/contracts/solana/programs/xcall/src/instructions/execute_call.rs b/contracts/solana/programs/xcall/src/instructions/execute_call.rs new file mode 100644 index 00000000..1ea976b7 --- /dev/null +++ b/contracts/solana/programs/xcall/src/instructions/execute_call.rs @@ -0,0 +1,133 @@ +use std::{str::FromStr, vec}; + +use anchor_lang::prelude::*; +use xcall_lib::message::msg_type::MessageType; + +use crate::{ + connection, dapp, + error::XcallError, + helper, + state::*, + types::{message::CSMessage, result::CSMessageResult}, +}; + +/// Executes a call based on a proxy request. +/// +/// This function processes a call by verifying the provided data against the request's data hash +/// and then invoking the `handle_call_message` instruction on the DApp. Depending on the message +/// type, it handles the response accordingly, potentially sending a result back through the +/// connection program. +/// +/// # Parameters +/// - `ctx`: The context containing all the necessary accounts and program state. +/// - `req_id`: The unique identifier for the request being processed. +/// - `data`: The data associated with the call request, which will be verified and processed. +/// +/// # Returns +/// - `Result<()>`: Returns `Ok(())` if the call was executed successfully, or an error if it failed. +pub fn execute_call<'info>( + ctx: Context<'_, '_, '_, 'info, ExecuteCallCtx<'info>>, + req_id: u128, + data: Vec, +) -> Result<()> { + let req = &ctx.accounts.proxy_request.req; + + if helper::hash_data(&data) != req.data() { + return Err(XcallError::DataMismatch.into()); + } + + let dapp_key = Pubkey::from_str(&req.to()).map_err(|_| XcallError::InvalidPubkey)?; + + // Prepare the instruction data for the DApp's `handle_call_message` instruction. + let dapp_ix_data = dapp::get_handle_call_message_ix_data( + req.from().to_owned(), + data.clone(), + req.protocols(), + )?; + + // Invoke the DApp's `handle_call_message` instruction. + let dapp_res = dapp::invoke_handle_call_message_ix( + dapp_key, + dapp_ix_data, + &ctx.accounts.config, + &ctx.accounts.signer, + &ctx.remaining_accounts[(req.protocols().len() * 3)..], + )?; + + // Handle the response based on the message type. + match req.msg_type() { + MessageType::CallMessage => { + dapp::handle_response(req_id, dapp_res)?; + } + MessageType::CallMessagePersisted => { + if !dapp_res.success { + msg!("Error executing call from dapp: {:?}", dapp_res.message); + return Err(XcallError::RevertFromDapp.into()); + } + dapp::handle_response(req_id, dapp_res)?; + } + MessageType::CallMessageWithRollback => { + let res_code = dapp::handle_response(req_id, dapp_res)?; + + let result = CSMessageResult::new(req.sequence_no(), res_code, None); + let cs_message = rlp::encode(&CSMessage::from(result)).to_vec(); + + let ix_data = connection::get_send_message_ix_data( + &req.from().nid(), + -(req.sequence_no() as i64), + cs_message, + )?; + + for (i, _) in req.protocols().iter().enumerate() { + connection::call_connection_send_message( + i, + &ix_data, + &req.protocols(), + &ctx.accounts.config, + &ctx.accounts.signer, + &ctx.accounts.system_program, + &ctx.remaining_accounts, + )?; + } + } + } + + Ok(()) +} + +#[derive(Accounts)] +#[instruction(req_id : u128)] +pub struct ExecuteCallCtx<'info> { + /// The account that signs and pays for the transaction. This account is mutable + /// because it will be debited for any fees or rent required during the transaction. + #[account(mut)] + pub signer: Signer<'info>, + + /// The solana system program account, used for creating and managing accounts. + pub system_program: Program<'info, System>, + + /// The configuration account, which stores important settings and counters for the program. + #[account( + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump, + )] + pub config: Account<'info, Config>, + + /// CHECK: This is safe because this account is checked against the `config.admin` to ensure + /// it is valid. + #[account( + mut, + address = config.admin @ XcallError::OnlyAdmin + )] + pub admin: AccountInfo<'info>, + + /// The proxy request account, identified by a request ID, which is used for executing + /// calls. The account is closed after use, with any remaining funds sent to the `admin`. + #[account( + mut, + seeds = [ProxyRequest::SEED_PREFIX.as_bytes(), &req_id.to_be_bytes()], + bump = proxy_request.bump, + close = admin + )] + pub proxy_request: Account<'info, ProxyRequest>, +} diff --git a/contracts/solana/programs/xcall/src/instructions/execute_rollback.rs b/contracts/solana/programs/xcall/src/instructions/execute_rollback.rs new file mode 100644 index 00000000..e086387f --- /dev/null +++ b/contracts/solana/programs/xcall/src/instructions/execute_rollback.rs @@ -0,0 +1,91 @@ +use anchor_lang::prelude::*; +use xcall_lib::network_address::NetworkAddress; + +use crate::{dapp, error::XcallError, event, id, state::*}; + +/// Executes a rollback operation using the stored rollback data. +/// +/// This function retrieves the rollback data from the rollback account and prepares the necessary +/// instruction data to invoke the rollback operation in a DApp. It then executes the rollback +/// by calling the DApp's handle call message instruction, and emits an event if the rollback +/// is successful. +/// +/// # Arguments +/// - `ctx`: The context containing all the necessary accounts and program state. +/// - `sn`: The sequence number associated with the rollback operation. +/// +/// # Returns +/// - `Result<()>`: Returns `Ok(())` if the rollback was executed successfully, or an error if it +/// failed. +pub fn execute_rollback<'info>( + ctx: Context<'_, '_, '_, 'info, ExecuteRollbackCtx<'info>>, + sn: u128, +) -> Result<()> { + let rollback = &ctx.accounts.rollback_account.rollback; + if !rollback.enabled() { + return Err(XcallError::RollbackNotEnabled.into()); + } + + // Prepare the instruction data needed to invoke the rollback operation in the DApp. + let ix_data = dapp::get_handle_call_message_ix_data( + NetworkAddress::new(&ctx.accounts.config.network_id, &id().to_string()), + rollback.rollback().to_owned(), + rollback.protocols().clone(), + )?; + + // Invoke the DApp's handle call message instruction with the prepared data and accounts. + let res = dapp::invoke_handle_call_message_ix( + rollback.from().to_owned(), + ix_data, + &ctx.accounts.config, + &ctx.accounts.signer, + &ctx.remaining_accounts, + )?; + + // If the DApp reports a failure, log the error and return an error. + if !res.success { + msg!("Error executing rollback from dapp: {:?}", res.message); + return Err(XcallError::RevertFromDapp.into()); + } + + emit!(event::RollbackExecuted { sn }); + + Ok(()) +} + +#[derive(Accounts)] +#[instruction(sn : u128,)] +pub struct ExecuteRollbackCtx<'info> { + /// The account that signs and pays for the transaction. This account is mutable + /// because it will be debited for any fees or rent required during the transaction. + #[account(mut)] + pub signer: Signer<'info>, + + /// The solana system program account, used for creating and managing accounts. + pub system_program: Program<'info, System>, + + /// The configuration account, which stores important settings and counters for the program. + #[account( + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump + )] + pub config: Account<'info, Config>, + + /// CHECK: This is safe because this account is checked against the `config.admin` to ensure + /// it is valid. + #[account( + mut, + address = config.admin @ XcallError::OnlyAdmin + )] + pub admin: AccountInfo<'info>, + + /// The rollback account, identified by a sequence number (`sn`), used for executing rollback. + /// The account is closed after use, with any remaining funds sent to the `admin`. + #[account( + mut, + seeds = [RollbackAccount::SEED_PREFIX.as_bytes(), &sn.to_be_bytes()], + bump = rollback_account.bump, + close = admin, + )] + pub rollback_account: Account<'info, RollbackAccount>, +} diff --git a/contracts/solana/programs/xcall/src/instructions/fee.rs b/contracts/solana/programs/xcall/src/instructions/fee.rs new file mode 100644 index 00000000..9d360cb6 --- /dev/null +++ b/contracts/solana/programs/xcall/src/instructions/fee.rs @@ -0,0 +1,113 @@ +use anchor_lang::prelude::*; +use xcall_lib::xcall_connection_type::{self, GET_FEE_IX}; + +use crate::{connection, error::*, helper, state::*}; + +pub fn set_protocol_fee(ctx: Context, fee: u64) -> Result<()> { + ctx.accounts.config.set_protocol_fee(fee); + + Ok(()) +} + +pub fn set_protocol_fee_handler(ctx: Context, fee_handler: Pubkey) -> Result<()> { + ctx.accounts.config.set_fee_handler(fee_handler); + + Ok(()) +} + +/// Calculates and retrieves the total fee for a cross-chain message, including the protocol fee +/// and connection-specific fees. +/// +/// This function computes the total fee required to send a cross-chain message by adding the +/// protocol fee stored in the configuration account and any additional fees specific to the +/// connections used in the message. It first validates the input parameters, then queries the +/// fee for each connection specified in the `sources` list, and adds it to the protocol fee. +/// +/// # Arguments +/// - `ctx`: The context of the solana program instruction. +/// - `nid`: A string representing the network ID for which the fee is being calculated. +/// - `is_rollback`: A boolean indicating whether a rollback is required, affecting the fee. +/// - `sources`: A vector of strings representing the source protocols involved in the transaction. +/// +/// # Returns +/// - `Result`: Returns the total fee as a `u64` value if successful, otherwise returns +/// an error. +pub fn get_fee( + ctx: Context, + nid: String, + is_rollback: bool, + sources: Vec, +) -> Result { + if sources.is_empty() { + return Err(XcallError::SourceProtocolsNotSpecified.into()); + } + + let mut data = vec![]; + let args = xcall_connection_type::GetFeeArgs { + network_id: nid, + response: is_rollback, + }; + args.serialize(&mut data)?; + + let ix_data = helper::get_instruction_data(GET_FEE_IX, data); + + let mut connection_fee = ctx.accounts.config.protocol_fee; + for (i, source) in sources.iter().enumerate() { + let fee = connection::query_connection_fee(source, &ix_data, &ctx.remaining_accounts[i])?; + if fee > 0 { + connection_fee = connection_fee.checked_add(fee).expect("no overflow") + } + } + + Ok(connection_fee) +} + +#[derive(Accounts)] +pub struct SetFeeHandlerCtx<'info> { + /// The configuration account, which stores important settings for the program. + /// This account is mutable because the fee handler of the protocol will be updated. + #[account( + mut, + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump + )] + pub config: Account<'info, Config>, + + /// The account that signs and pays for the transaction. This account is checked + /// against the `config.admin` to ensure it is valid. + #[account( + mut, + address = config.admin @ XcallError::OnlyAdmin + )] + pub admin: Signer<'info>, +} + +#[derive(Accounts)] +pub struct SetFeeCtx<'info> { + /// The configuration account, which stores important settings for the program. + /// This account is mutable because the fee handler of the protocol will be updated. + #[account( + mut, + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump + )] + pub config: Account<'info, Config>, + + /// The account that signs and pays for the transaction. This account is checked + /// against the `config.admin` to ensure it is valid. + #[account( + mut, + address = config.admin @ XcallError::OnlyAdmin + )] + pub admin: Signer<'info>, +} + +#[derive(Accounts)] +pub struct GetFeeCtx<'info> { + /// The configuration account, which stores important settings for the program. + #[account( + seeds = [Config::SEED_PREFIX.as_bytes()], + bump, + )] + pub config: Account<'info, Config>, +} diff --git a/contracts/solana/programs/xcall/src/instructions/handle_forced_rollback.rs b/contracts/solana/programs/xcall/src/instructions/handle_forced_rollback.rs new file mode 100644 index 00000000..19b02b37 --- /dev/null +++ b/contracts/solana/programs/xcall/src/instructions/handle_forced_rollback.rs @@ -0,0 +1,119 @@ +use std::{str::FromStr, vec}; + +use anchor_lang::prelude::*; +use xcall_lib::message::msg_type::MessageType; + +use crate::{ + connection, + error::XcallError, + helper, + state::*, + types::{ + message::CSMessage, + result::{CSMessageResult, CSResponseType}, + }, +}; + +/// Handles a forced rollback of a cross-chain message when an unknown error occurs after +/// the message is received on the destination chain from the source chain. +/// This allows the dApp to trigger a rollback, sending a failure response back to the source chain. +/// +/// The function verifies the dApp's authority, ensures the message type supports rollback, +/// and constructs a failure response to be sent back across the specified protocols (connections). +/// The rollback is enforced by sending a failure message for each protocol involved in the +/// original message, enabling the state to revert as though the message was never successfully +/// processed. +/// +/// # Arguments +/// * `ctx` - Context containing the accounts required for processing the forced rollback. +/// +/// # Returns +/// * `Result<()>` - Returns `Ok(())` on successful execution, or an error if any validation +/// or execution step fails. +pub fn handle_forced_rollback<'info>( + ctx: Context<'_, '_, '_, 'info, HandleForcedRollbackCtx<'info>>, +) -> Result<()> { + let req = &ctx.accounts.proxy_request.req; + + // Ensures that the request is not received by specified protocols + if req.to().is_empty() { + return Err(XcallError::RequestPending.into()); + } + + if req.msg_type() != MessageType::CallMessageWithRollback { + return Err(XcallError::RollbackNotPossible.into()); + } + + let dapp_authority = &ctx.accounts.dapp_authority; + helper::ensure_dapp_authority(dapp_authority.owner, dapp_authority.key())?; + + let to = Pubkey::from_str(&req.to()).map_err(|_| XcallError::InvalidPubkey)?; + if dapp_authority.owner != &to { + return Err(XcallError::InvalidSigner.into()); + } + + let result = CSMessageResult::new(req.sequence_no(), CSResponseType::CSResponseFailure, None); + let cs_message = rlp::encode(&CSMessage::from(result)).to_vec(); + + let ix_data = connection::get_send_message_ix_data( + &req.from().nid(), + -(req.sequence_no() as i64), + cs_message, + )?; + + for (i, _) in req.protocols().iter().enumerate() { + connection::call_connection_send_message( + i, + &ix_data, + &req.protocols(), + &ctx.accounts.config, + &ctx.accounts.signer, + &ctx.accounts.system_program, + &ctx.remaining_accounts, + )?; + } + + Ok(()) +} + +#[derive(Accounts)] +#[instruction(req_id: u128)] +pub struct HandleForcedRollbackCtx<'info> { + /// The account that signs and pays for the transaction. This account is mutable because + /// it will be debited for any fees or rent required during the transaction. + #[account(mut)] + pub signer: Signer<'info>, + + /// The account representing the dApp authority, which must sign the transaction to enforce + /// the rollback. + pub dapp_authority: Signer<'info>, + + /// The Solana system program account, used for creating and managing accounts. + pub system_program: Program<'info, System>, + + /// The configuration account, which stores important settings and counters for the program. + /// The `seeds` and `bump` ensure that this account is securely derived. + #[account( + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump, + )] + pub config: Account<'info, Config>, + + /// CHECK: This is safe because this account is checked against `config.admin` to ensure + /// it matches the expected admin address. + #[account( + mut, + address = config.admin @ XcallError::OnlyAdmin + )] + pub admin: AccountInfo<'info>, + + /// The proxy request account, identified by a request ID. This account is used for executing + /// calls and is closed after use, with any remaining funds sent to the `admin`. + #[account( + mut, + seeds = [ProxyRequest::SEED_PREFIX.as_bytes(), &req_id.to_be_bytes()], + bump = proxy_request.bump, + close = admin + )] + pub proxy_request: Account<'info, ProxyRequest>, +} diff --git a/contracts/solana/programs/xcall/src/instructions/handle_message.rs b/contracts/solana/programs/xcall/src/instructions/handle_message.rs new file mode 100644 index 00000000..562ea80e --- /dev/null +++ b/contracts/solana/programs/xcall/src/instructions/handle_message.rs @@ -0,0 +1,738 @@ +use anchor_lang::{ + prelude::*, + solana_program::{hash, instruction::Instruction, program::invoke_signed}, +}; + +use crate::{ + error::*, + event, helper, id, + state::*, + types::{ + message::{CSMessage, CSMessageType}, + request::CSMessageRequest, + result::{CSMessageResult, CSResponseType}, + }, +}; + +/// Handles incoming cross-chain messages by processing requests or responses from the source or +/// destination chain. This instruction serves as the entry point for handling messages received +/// from other chains. It determines the type of message (either a request from a source chain or +/// a response from a destination chain), and then invokes the appropriate inner instruction of +/// the xcall program to process the message. +/// +/// # Parameters +/// - `ctx`: The context containing all necessary accounts and program-specific information. +/// - `from_nid`: The network ID of the chain that sent the message. +/// - `message`: The encoded message payload received from the chain. +/// - `sequence_no`: The sequence number associated with the message, used to track message +/// ordering and responses. +/// +/// # Returns +/// - `Result<()>`: Returns `Ok(())` if successful, or an appropriate error if any validation or +/// invocation fails. +pub fn handle_message<'info>( + ctx: Context<'_, '_, '_, 'info, HandleMessageCtx<'info>>, + from_nid: String, + message: Vec, + sequence_no: u128, +) -> Result<()> { + let config = &ctx.accounts.config; + if config.network_id == from_nid.to_string() { + return Err(XcallError::ProtocolMismatch.into()); + } + + let cs_message: CSMessage = message.try_into()?; + match cs_message.message_type() { + CSMessageType::CSMessageRequest => { + if ctx.accounts.pending_response.is_some() { + return Err(XcallError::PendingResponseAccountMustNotBeSpecified.into()); + } + + invoke_handle_request(ctx, from_nid, cs_message.payload)? + } + CSMessageType::CSMessageResult => { + let rollback_account = ctx + .accounts + .rollback_account + .as_ref() + .ok_or(XcallError::CallRequestNotFound)?; + + let all_sources_delivered = check_sources_and_pending_response( + &ctx.accounts.connection, + rollback_account.rollback.protocols(), + &mut ctx.accounts.pending_response, + &ctx.accounts.admin, + )?; + if !all_sources_delivered { + return Ok(()); + } + + invoke_handle_result(ctx, from_nid, cs_message.payload, sequence_no)?; + } + } + Ok(()) +} + +/// Processes an incoming cross-chain request, validating the source and managing the request state. +/// +/// This function handles a request from a source chain, verifying the sender's network ID and +/// protocols. It manages the state of any pending requests, emits a `CallMessage` event with +/// the request's details, and stores the request data in the `ProxyRequest` account for further +/// processing. +/// +/// # Parameters +/// - `ctx`: Context containing all relevant accounts and program-specific information. +/// - `from_nid`: Network ID of the source chain that sent the request. +/// - `payload`: Encoded payload of the request message. +/// +/// # Returns +/// - `Result<()>`: Returns `Ok(())` if the request is successfully processed, or an error if +/// validation or account updates fail. +pub fn handle_request( + ctx: Context, + from_nid: String, + payload: &[u8], +) -> Result<()> { + let mut req: CSMessageRequest = payload.try_into()?; + + let (src_nid, _) = req.from().parse_network_address(); + if src_nid != from_nid { + return Err(XcallError::ProtocolMismatch.into()); + } + let source = &ctx.accounts.connection; + let source_valid = is_valid_source(&source, &req.protocols())?; + if !source_valid { + return Err(XcallError::ProtocolMismatch.into()); + } + + if req.protocols().len() > 1 { + let pending_request = ctx + .accounts + .pending_request + .as_mut() + .ok_or(XcallError::PendingRequestAccountNotSpecified)?; + + if !pending_request.sources.contains(&source.owner) { + pending_request.sources.push(source.owner.to_owned()) + } + if pending_request.sources.len() != req.protocols().len() { + return Ok(()); + } + pending_request.close(ctx.accounts.admin.clone())?; + } + + let req_id = ctx.accounts.config.get_next_req_id(); + + emit!(event::CallMessage { + from: req.from().to_string(), + to: req.to().clone(), + sn: req.sequence_no(), + reqId: req_id, + data: req.data() + }); + + let proxy_request = &mut ctx.accounts.proxy_request; + + req.hash_data(); + proxy_request.set(req, ctx.bumps.proxy_request); + + Ok(()) +} + +/// Handles the result of a cross-chain message response, determining the next steps +/// based on the response code. +/// +/// This function processes the outcome of a cross-chain operation, performing different +/// actions depending on whether the result was successful or not. If the operation was successful, +/// it finalizes the process by closing the rollback account and marking the operation as successful. +/// If the operation failed, it enables rollback and emits an event indicating a rollback action. +/// Additionally, it ensures that the appropriate accounts are present or absent based on the outcome. +/// +/// # Arguments +/// - `ctx`: The context of accounts involved in the operation. +/// - `payload`: The raw result data from the cross-chain operation, which is decoded and processed. +/// +/// # Returns +/// - `Result<()>`: Returns `Ok(())` if the operation completes successfully, or an error if something +/// goes wrong. +pub fn handle_result(ctx: Context, payload: &[u8]) -> Result<()> { + let result: CSMessageResult = payload.try_into()?; + let proxy_request = &ctx.accounts.proxy_request; + let rollback_account = &mut ctx.accounts.rollback_account; + + let response_code = result.response_code(); + + emit!(event::ResponseMessage { + code: response_code.clone().into(), + sn: result.sequence_no() + }); + + match response_code { + CSResponseType::CSResponseSuccess => { + rollback_account.close(ctx.accounts.admin.clone())?; + + let success_res = ctx + .accounts + .successful_response + .as_mut() + .ok_or(XcallError::SuccessfulResponseAccountNotSpecified)?; + + success_res.success = true; + + if let Some(message) = &mut result.message() { + handle_reply(ctx, message)?; + } else { + if proxy_request.is_some() { + return Err(XcallError::ProxyRequestAccountMustNotBeSpecified.into()); + } + } + } + _ => { + if ctx.accounts.successful_response.is_some() { + return Err(XcallError::SuccessfulResponseAccountMustNotBeSpecified.into()); + } + if proxy_request.is_some() { + return Err(XcallError::ProxyRequestAccountMustNotBeSpecified.into()); + } + + rollback_account.rollback.enable_rollback(); + + emit!(event::RollbackMessage { + sn: result.sequence_no() + }); + } + } + + Ok(()) +} + +/// Handles the error for a specific message sequence originally sent from Solana. +/// If something goes wrong, this function enables rollback and emits relevant events +/// to revert the state for the dApp that initiated the message. It's invoked by each +/// connection involved in sending the message from Solana to another chain. +/// +/// # Arguments: +/// * `ctx` - Context containing the necessary accounts to process the rollback. +/// * `sequence_no` - The sequence number of the message to be rolled back. +/// +/// # Returns +/// - `Result<()>` - Returns `Ok(())` if the rollback is successfully enabled and processed, +/// or an appropriate error if the validation or rollback fails. +pub fn handle_error(ctx: Context, sequence_no: u128) -> Result<()> { + let rollback_account = &mut ctx.accounts.rollback_account; + + let all_sources_delivered = check_sources_and_pending_response( + &ctx.accounts.connection, + rollback_account.rollback.protocols(), + &mut ctx.accounts.pending_response, + &ctx.accounts.admin, + )?; + if !all_sources_delivered { + return Ok(()); + } + + emit!(event::ResponseMessage { + code: CSResponseType::CSResponseFailure.into(), + sn: sequence_no + }); + emit!(event::RollbackMessage { sn: sequence_no }); + + rollback_account.rollback.enable_rollback(); + + Ok(()) +} + +/// Handles reply messages from cross-chain communication after receiving a response. +/// Verifies the origin of the reply and prepares it for further processing. +/// Emits an event with details about the message and sets up the `proxy_request` +/// for the subsequent steps. +/// +/// # Arguments +/// * `ctx` - The context containing relevant accounts for handling the reply. +/// * `reply` - The mutable reference to the incoming reply message to be processed. +/// +/// # Returns +/// - `Result<()>`: Returns `Ok(())` if the operation completes successfully, or an error if something +/// goes wrong. +pub fn handle_reply(ctx: Context, reply: &mut CSMessageRequest) -> Result<()> { + let rollback = &ctx.accounts.rollback_account.rollback; + if rollback.to().nid() != reply.from().nid() { + return Err(XcallError::InvalidReplyReceived.into()); + } + + let req_id = ctx.accounts.config.get_next_req_id(); + + emit!(event::CallMessage { + from: reply.from().to_string(), + to: reply.to().clone(), + sn: reply.sequence_no(), + reqId: req_id, + data: reply.data() + }); + + let proxy_request = ctx + .accounts + .proxy_request + .as_deref_mut() + .ok_or(XcallError::ProxyRequestAccountNotSpecified)?; + + reply.hash_data(); + reply.set_protocols(rollback.protocols().clone()); + proxy_request.set(reply.to_owned(), ctx.bumps.proxy_request.unwrap()); + + Ok(()) +} + +/// Executes the inner instruction to handle a request received from the source chain. +/// +/// This function is invoked after the `handle_message` instruction determines that the received +/// message is a request from the source chain. It prepares the necessary accounts and instruction +/// data, and then calls the appropriate inner instruction of the xcall program to process the +/// request. This process ensures that the request is correctly handled and forwarded as needed +/// within the cross-chain communication context. +/// +/// # Parameters +/// - `ctx`: The context containing the accounts and program-specific info needed for the instruction. +/// - `from_nid`: The network ID of the chain that sent the request. +/// - `msg_payload`: The payload of the request message received from the source chain. +/// +/// # Returns +/// - `Result<()>`: Indicates whether the invocation was successful or encountered an error. +pub fn invoke_handle_request<'info>( + ctx: Context<'_, '_, '_, 'info, HandleMessageCtx<'info>>, + from_nid: String, + msg_payload: Vec, +) -> Result<()> { + let mut data = vec![]; + let args = xcall_lib::xcall_type::HandleRequestArgs { + from_nid, + msg_payload, + }; + args.serialize(&mut data)?; + let ix_data = helper::get_instruction_data(xcall_lib::xcall_type::HANDLE_REQUEST_IX, data); + + let mut account_metas: Vec = vec![ + AccountMeta::new(ctx.accounts.signer.key(), true), + AccountMeta::new_readonly(ctx.accounts.connection.key(), true), + AccountMeta::new_readonly(ctx.accounts.config.key(), true), + AccountMeta::new_readonly(ctx.accounts.system_program.key(), false), + AccountMeta::new(ctx.accounts.admin.key(), false), + ]; + let mut account_infos: Vec> = vec![ + ctx.accounts.signer.to_account_info(), + ctx.accounts.connection.to_account_info(), + ctx.accounts.config.to_account_info(), + ctx.accounts.system_program.to_account_info(), + ctx.accounts.admin.to_account_info(), + ]; + for account in ctx.remaining_accounts { + if account.is_writable { + account_metas.push(AccountMeta::new(account.key(), account.is_signer)) + } else { + account_metas.push(AccountMeta::new_readonly(account.key(), account.is_signer)) + } + account_infos.push(account.to_account_info()); + } + + let ix = Instruction { + program_id: id(), + accounts: account_metas, + data: ix_data.clone(), + }; + + invoke_signed( + &ix, + &account_infos, + &[&[Config::SEED_PREFIX.as_bytes(), &[ctx.accounts.config.bump]]], + )?; + + Ok(()) +} + +/// Executes the inner instruction to handle a response received from the destination chain. +/// +/// This function is invoked after the `handle_message` instruction determines that the received +/// message is a response from the destination chain. It prepares the necessary accounts and +/// instruction data, and then calls the appropriate inner instruction of the xcall program to +/// process the response. The response is associated with a specific sequence number and may involve +/// a rollback if specified. +/// +/// # Parameters +/// - `ctx`: The context containing the accounts and program-specific info needed for the instruction. +/// - `from_nid`: The network ID of the chain that sent the response. +/// - `msg_payload`: The payload of the message received from the destination chain. +/// - `sequence_no`: The sequence number associated with the original request message. +/// +/// # Returns +/// - `Result<()>`: Indicates whether the invocation was successful or encountered an error. +pub fn invoke_handle_result<'info>( + ctx: Context<'_, '_, '_, 'info, HandleMessageCtx<'info>>, + from_nid: String, + msg_payload: Vec, + sequence_no: u128, +) -> Result<()> { + let mut data = vec![]; + let args = xcall_lib::xcall_type::HandleResultArgs { + from_nid, + msg_payload, + sequence_no, + }; + args.serialize(&mut data)?; + let ix_data = helper::get_instruction_data(xcall_lib::xcall_type::HANDLE_RESULT_IX, data); + + let mut account_metas: Vec = vec![ + AccountMeta::new(ctx.accounts.signer.key(), true), + AccountMeta::new_readonly(ctx.accounts.connection.key(), true), + AccountMeta::new_readonly(ctx.accounts.config.key(), true), + AccountMeta::new_readonly(ctx.accounts.system_program.key(), false), + AccountMeta::new(ctx.accounts.admin.key(), false), + ]; + let mut account_infos: Vec> = vec![ + ctx.accounts.signer.to_account_info(), + ctx.accounts.connection.to_account_info(), + ctx.accounts.system_program.to_account_info(), + ctx.accounts.config.to_account_info(), + ctx.accounts.admin.to_account_info(), + ]; + + for account in ctx.remaining_accounts { + if account.is_writable { + account_metas.push(AccountMeta::new(account.key(), account.is_signer)) + } else { + account_metas.push(AccountMeta::new_readonly(account.key(), account.is_signer)) + } + account_infos.push(account.to_account_info()); + } + + // append all accounts with lamport changes to the end of your CPI instruction accounts list + if ctx.accounts.pending_response.is_some() { + let pending_response = ctx.accounts.pending_response.as_ref().unwrap(); + + account_metas.push(AccountMeta::new_readonly(pending_response.key(), false)); + account_infos.push(pending_response.to_account_info()); + } + + let ix = Instruction { + program_id: id(), + accounts: account_metas, + data: ix_data.clone(), + }; + + invoke_signed( + &ix, + &account_infos, + &[&[Config::SEED_PREFIX.as_bytes(), &[ctx.accounts.config.bump]]], + )?; + + Ok(()) +} + +/// Validates the source and manages the pending response for a multi-protocol message. +/// +/// This function checks if the sender is a valid source for the given protocols and updates +/// the pending response account if multiple protocols are used. It ensures that the sender +/// is listed in the pending response account and closes the account when all expected sources +/// are received. +/// +/// # Parameters +/// - `sender`: The `Signer` representing the sender account to validate. +/// - `protocols`: A vector of protocol names that the sender must be listed in. +/// - `pending_response`: An optional mutable reference to the `PendingResponse` account. +/// - `admin`: The admin account for closing the `PendingResponse` account. +/// +/// # Returns +/// - `Result`: `Ok(true)` if all sources are valid and the pending response is closed, +/// or `Ok(false)` if the message is still pending (not all sources have responded). Returns +/// an error if validation fails. +pub fn check_sources_and_pending_response<'info>( + sender: &Signer, + protocols: &Vec, + pending_response: &mut Option>, + admin: &AccountInfo<'info>, +) -> Result { + let source_valid = is_valid_source(&sender, protocols)?; + if !source_valid { + return Err(XcallError::ProtocolMismatch.into()); + }; + + if protocols.len() > 1 { + let pending_response = pending_response + .as_mut() + .ok_or(XcallError::PendingResponseAccountNotSpecified)?; + + if !pending_response.sources.contains(&sender.owner) { + pending_response.sources.push(sender.owner.to_owned()) + } + if pending_response.sources.len() != protocols.len() { + return Ok(false); + } + pending_response.close(admin.to_owned())?; + } + + Ok(true) +} + +/// Checks if the given sender is a valid source for the provided protocols. +/// +/// This function verifies if the sender's authority is correct and if the sender is listed +/// among the allowed protocols. It ensures the sender has the proper authorization and +/// is recognized by the system. +/// +/// # Parameters +/// - `sender`: A reference to the `Signer` representing the sender account. +/// - `protocols`: A vector of protocol names that the sender must be listed in. +/// +/// # Returns +/// - `Result`: `true` if the sender is authorized and listed in the protocols, +/// `false` otherwise. +pub fn is_valid_source(sender: &Signer, protocols: &Vec) -> Result { + helper::ensure_connection_authority(sender.owner, sender.key())?; + if protocols.contains(&sender.owner.to_string()) { + return Ok(true); + } + + Ok(false) +} + +#[derive(Accounts)] +#[instruction(from_nid: String, msg: Vec, sequence_no: u128)] +pub struct HandleMessageCtx<'info> { + /// The account that signs and pays for the transaction. This account is mutable + /// because it will be debited for any fees or rent required during the transaction. + #[account(mut)] + pub signer: Signer<'info>, + + /// The signer account representing the connection through which the message is being processed. + pub connection: Signer<'info>, + + /// The solana system program account, used for creating and managing accounts. + pub system_program: Program<'info, System>, + + /// CHECK: This is safe because this account is checked against the `config.admin` to ensure + /// it is valid. + #[account( + mut, + address = config.admin @ XcallError::InvalidAdminKey + )] + pub admin: AccountInfo<'info>, + + /// The configuration account, which stores important settings and counters for the program. + #[account( + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump + )] + pub config: Account<'info, Config>, + + /// An optional account that is created when sending a rollback message to a destination chain. + /// It stores essential details related to the message, which are required to handle the response + /// from the destination chain. The `rollback_account` is only needed if a response is expected + /// for a specific sequence of the message that was sent from this chain. + #[account( + seeds = [RollbackAccount::SEED_PREFIX.as_bytes(), &sequence_no.to_be_bytes()], + bump + )] + pub rollback_account: Option>, + + /// An optional account created to track whether a response has been received from each connection + /// specified in a message. This account is only initialized if multiple connections are used for + /// sending and receiving messages, enhancing security by avoiding reliance on a single connection. + #[account( + init_if_needed, + payer = signer, + space = PendingResponse::SIZE, + seeds = [PendingResponse::SEED_PREFIX.as_bytes(), &hash::hash(&msg).to_bytes()], + bump + )] + pub pending_response: Option>, +} + +#[derive(Accounts)] +#[instruction(from_nid: String, msg_payload: Vec)] +pub struct HandleRequestCtx<'info> { + /// The account that signs and pays for the transaction. This account is mutable + /// because it will be debited for any fees or rent required during the transaction. + #[account(mut)] + pub signer: Signer<'info>, + + /// The signer account representing the connection through which the message is being processed. + pub connection: Signer<'info>, + + /// The xcall signer account, used to verify that the provided signer is authorized + /// by the xcall program. + #[account( + owner = id() + )] + pub xcall: Signer<'info>, + + /// The solana system program account, used for creating and managing accounts. + pub system_program: Program<'info, System>, + + /// CHECK: This is safe because this account is checked against the `config.admin` to ensure + /// it is valid. + #[account( + mut, + address = config.admin @ XcallError::InvalidAdminKey + )] + pub admin: AccountInfo<'info>, + + /// The configuration account, which stores important settings and counters for the program. + /// This account is mutable because the request sequence may be updated during instruction + /// processing + #[account( + mut, + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump + )] + pub config: Account<'info, Config>, + + /// Stores details of each cross-chain message request sent from the source to the destination chain. + #[account( + init_if_needed, + payer = signer, + space = ProxyRequest::SIZE, + seeds = [ProxyRequest::SEED_PREFIX.as_bytes(), &(config.last_req_id + 1).to_be_bytes()], + bump + )] + pub proxy_request: Account<'info, ProxyRequest>, + + /// Tracks the receipt of requests from a multi-connection message. This account is optional and + /// only created if multiple connections are used to send a message, ensuring the request is fully + /// received. + #[account( + init_if_needed, + payer = signer, + space = PendingRequest::SIZE, + seeds = [PendingRequest::SEED_PREFIX.as_bytes(), &hash::hash(&msg_payload).to_bytes()], + bump, + )] + pub pending_request: Option>, +} + +#[derive(Accounts)] +#[instruction(from_nid: String, msg_payload: Vec, sequence_no: u128)] +pub struct HandleResultCtx<'info> { + /// The account that signs and pays for the transaction. This account is mutable + /// because it will be debited for any fees or rent required during the transaction. + #[account(mut)] + pub signer: Signer<'info>, + + /// The signer account representing the connection through which the message is being processed. + pub connection: Signer<'info>, + + /// The xcall signer account, used to verify that the provided signer is authorized + /// by the xcall program. + #[account( + owner = id() + )] + pub xcall: Signer<'info>, + + /// The solana system program account, used for creating and managing accounts. + pub system_program: Program<'info, System>, + + /// CHECK: This is safe because this account is checked against the `config.admin` to ensure + /// it is valid. + #[account( + mut, + address = config.admin @ XcallError::InvalidAdminKey + )] + pub admin: AccountInfo<'info>, + + /// The configuration account, which stores important settings and counters for the program. + /// This account is mutable because the request sequence may be updated during instruction + /// processing + #[account( + mut, + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump + )] + pub config: Account<'info, Config>, + + /// A rollback account created when sending a rollback message to a destination chain. + /// It stores essential details related to the message, necessary for handling the response + /// from the destination chain. The `rollback_account` is required only if a response is + /// expected for a specific sequence of the message sent from this chain. + #[account( + mut, + seeds = [RollbackAccount::SEED_PREFIX.as_bytes(), &sequence_no.to_be_bytes()], + bump + )] + pub rollback_account: Account<'info, RollbackAccount>, + + /// Stores details of each cross-chain message request sent from the source to the destination + /// chain. + #[account( + init_if_needed, + payer = signer, + space = ProxyRequest::SIZE, + seeds = [ProxyRequest::SEED_PREFIX.as_bytes(), &(config.last_req_id + 1).to_be_bytes()], + bump + )] + pub proxy_request: Option>, + + /// Stores details of a successful response received from the destination chain. This account + /// is optional and created only when a successful response is expected for a specific sequence + /// number. + #[account( + init_if_needed, + payer = signer, + space = SuccessfulResponse::SIZE, + seeds = [SuccessfulResponse::SEED_PREFIX.as_bytes(), &sequence_no.to_be_bytes()], + bump + )] + pub successful_response: Option>, +} + +#[derive(Accounts)] +#[instruction(sequence_no: u128)] +pub struct HandleErrorCtx<'info> { + /// The account that signs and pays for the transaction. This account is mutable + /// because it will be debited for any fees or rent required during the transaction. + #[account(mut)] + pub signer: Signer<'info>, + + /// The signer account representing the connection through which the message is being processed. + pub connection: Signer<'info>, + + /// The solana system program account, used for creating and managing accounts. + pub system_program: Program<'info, System>, + + /// The configuration account, which stores important settings and counters for the + /// program. This account is mutable because the last request ID of config will be updated. + #[account( + seeds = [Config::SEED_PREFIX.as_bytes()], + bump + )] + pub config: Account<'info, Config>, + + /// CHECK: This is safe because this account is checked against the `config.admin` to ensure + /// it is valid. + #[account( + mut, + address = config.admin @ XcallError::InvalidAdminKey + )] + pub admin: AccountInfo<'info>, + + /// A rollback account created when initiating a rollback message to a destination chain. + /// This account stores crucial details about the message, which are necessary for processing + /// the response from the destination chain. In this instruction, the `rollback_account` is + /// used to enable and execute the rollback operation within the DApp that originally sent + /// the message. + #[account( + mut, + seeds = [RollbackAccount::SEED_PREFIX.as_bytes(), &sequence_no.to_be_bytes()], + bump + )] + pub rollback_account: Account<'info, RollbackAccount>, + + /// An optional account created to track whether a response has been received from each connection + /// specified in a message. This account is only initialized if multiple connections are used for + /// sending and receiving messages, enhancing security by avoiding reliance on a single connection. + #[account( + init_if_needed, + payer = signer, + space = PendingResponse::SIZE, + seeds = [PendingResponse::SEED_PREFIX.as_bytes(), &hash::hash(&CSMessageResult::new(sequence_no, CSResponseType::CSResponseFailure, None).as_bytes()).to_bytes()], + bump + )] + pub pending_response: Option>, +} diff --git a/contracts/solana/programs/xcall/src/instructions/mod.rs b/contracts/solana/programs/xcall/src/instructions/mod.rs new file mode 100644 index 00000000..a7adb76c --- /dev/null +++ b/contracts/solana/programs/xcall/src/instructions/mod.rs @@ -0,0 +1,19 @@ +pub mod codec; +pub mod config; +pub mod execute_call; +pub mod execute_rollback; +pub mod fee; +pub mod handle_forced_rollback; +pub mod handle_message; +pub mod query_accounts; +pub mod send_message; + +pub use codec::*; +pub use config::*; +pub use execute_call::*; +pub use execute_rollback::*; +pub use fee::*; +pub use handle_forced_rollback::*; +pub use handle_message::*; +pub use query_accounts::*; +pub use send_message::*; diff --git a/contracts/solana/programs/xcall/src/instructions/query_accounts.rs b/contracts/solana/programs/xcall/src/instructions/query_accounts.rs new file mode 100644 index 00000000..23038e50 --- /dev/null +++ b/contracts/solana/programs/xcall/src/instructions/query_accounts.rs @@ -0,0 +1,406 @@ +use std::str::FromStr; + +use anchor_lang::{ + prelude::*, + solana_program::{ + hash, + instruction::Instruction, + program::{get_return_data, invoke}, + system_program, + }, +}; +use xcall_lib::{ + network_address::NetworkAddress, + query_account_type::{AccountMetadata, QueryAccountsPaginateResponse, QueryAccountsResponse}, +}; + +use crate::{ + connection, dapp, + error::*, + id, + state::*, + types::{ + message::{CSMessage, CSMessageType}, + request::CSMessageRequest, + result::{CSMessageResult, CSResponseType}, + }, +}; + +pub fn query_handle_message_accounts( + ctx: Context, + msg: Vec, +) -> Result { + let config = &ctx.accounts.config; + let admin = config.admin; + + let (proxy_request, _) = Pubkey::find_program_address( + &[ + ProxyRequest::SEED_PREFIX.as_bytes(), + &(config.last_req_id + 1).to_be_bytes(), + ], + &id(), + ); + + let mut account_metas = vec![ + AccountMetadata::new(admin, false), + AccountMetadata::new_readonly(config.key(), false), + ]; + + let cs_message: CSMessage = msg.clone().try_into()?; + match cs_message.message_type() { + CSMessageType::CSMessageRequest => { + let request: CSMessageRequest = cs_message.payload().try_into()?; + + // Optional pending response account + account_metas.push(AccountMetadata::new(id(), false)); + + // Optional rollback account + account_metas.push(AccountMetadata::new(id(), false)); + + // Mutable config account for handle_request instruction + account_metas.push(AccountMetadata::new(config.key(), false)); + + // Proxy request account + account_metas.push(AccountMetadata::new(proxy_request, false)); + + // Optional pending request account + let (pending_request, _) = Pubkey::find_program_address( + &[ + PendingRequest::SEED_PREFIX.as_bytes(), + &hash::hash(&cs_message.payload).to_bytes(), + ], + &id(), + ); + if request.protocols().len() > 1 { + account_metas.push(AccountMetadata::new(pending_request, false)) + } else { + account_metas.push(AccountMetadata::new(id(), false)) + } + } + CSMessageType::CSMessageResult => { + let result: CSMessageResult = cs_message.payload().try_into()?; + let sequence_no = result.sequence_no(); + + // Rollback account + let (rollback_account_pda, _) = Pubkey::find_program_address( + &[ + RollbackAccount::SEED_PREFIX.as_bytes(), + &sequence_no.to_be_bytes(), + ], + &id(), + ); + account_metas.push(AccountMetadata::new(rollback_account_pda, false)); + + let rollback_account = ctx + .accounts + .rollback_account + .as_ref() + .ok_or(XcallError::RollbackAccountNotSpecified)?; + + // Optional pending response account + let (pending_response, _) = Pubkey::find_program_address( + &[ + PendingResponse::SEED_PREFIX.as_bytes(), + &hash::hash(&msg).to_bytes(), + ], + &id(), + ); + if rollback_account.rollback.protocols().len() > 1 { + account_metas.push(AccountMetadata::new(pending_response, false)); + } else { + account_metas.push(AccountMetadata::new(id(), false)) + } + + // Mutable config account for handle_result instruction + account_metas.push(AccountMetadata::new(config.key(), false)); + + // Mutable rollback account for handle_result instruction + account_metas.push(AccountMetadata::new(rollback_account_pda, false)); + + // Optional proxy request account + if result.response_code() == &CSResponseType::CSResponseSuccess + && result.message().is_some() + { + account_metas.push(AccountMetadata::new(proxy_request, false)) + } else { + account_metas.push(AccountMetadata::new(id(), false)) + } + + // Optional successful response account + let (successful_response, _) = Pubkey::find_program_address( + &[ + SuccessfulResponse::SEED_PREFIX.as_bytes(), + &sequence_no.to_be_bytes(), + ], + &id(), + ); + if result.response_code() == &CSResponseType::CSResponseSuccess { + account_metas.push(AccountMetadata::new(successful_response, false)) + } else { + account_metas.push(AccountMetadata::new(id(), false)); + } + } + } + + Ok(QueryAccountsResponse { + accounts: account_metas, + }) +} + +pub fn query_execute_call_accounts( + ctx: Context, + req_id: u128, + data: Vec, + page: u8, + limit: u8, +) -> Result { + let config = &ctx.accounts.config; + let req = &ctx.accounts.proxy_request.req; + + let (proxy_request, _) = Pubkey::find_program_address( + &[ProxyRequest::SEED_PREFIX.as_bytes(), &req_id.to_be_bytes()], + &id(), + ); + + let mut account_metadata = vec![ + AccountMetadata::new_readonly(system_program::id(), false), + AccountMetadata::new(config.key(), false), + AccountMetadata::new(config.admin, false), + AccountMetadata::new(proxy_request, false), + ]; + + let conn_ix_data = connection::get_query_send_message_accounts_ix_data( + &req.from().nid(), + -(req.sequence_no() as i64), + Vec::new(), + )?; + + for (i, source) in req.protocols().iter().enumerate() { + let connection_key = Pubkey::from_str(&source).map_err(|_| XcallError::InvalidPubkey)?; + + let res = query_connection_send_message_accoounts( + i, + connection_key, + conn_ix_data.clone(), + ctx.remaining_accounts, + )?; + + let mut res_accounts = res.accounts; + account_metadata.push(AccountMetadata::new(connection_key, false)); + account_metadata.append(&mut res_accounts); + } + + let dapp_key = Pubkey::from_str(req.to()).map_err(|_| XcallError::InvalidPubkey)?; + let dapp_ix_data = dapp::get_query_handle_call_message_accounts_ix_data( + req.from().to_owned(), + data, + req.protocols(), + )?; + + let res = query_dapp_handle_call_message_accounts( + dapp_key, + dapp_ix_data, + &ctx.remaining_accounts[(req.protocols().len())..], + )?; + + let mut res_accounts = res.accounts; + account_metadata.append(&mut res_accounts); + account_metadata.push(AccountMetadata::new_readonly(dapp_key, false)); + + Ok(QueryAccountsPaginateResponse::new( + account_metadata, + page, + limit, + )) +} + +pub fn query_execute_rollback_accounts( + ctx: Context, + page: u8, + limit: u8, +) -> Result { + let config = &ctx.accounts.config; + let rollback_account = &ctx.accounts.rollback_account; + let rollback = &rollback_account.rollback; + + let mut account_metas = vec![ + AccountMetadata::new_readonly(system_program::id(), false), + AccountMetadata::new_readonly(config.key(), false), + AccountMetadata::new(config.admin, false), + AccountMetadata::new(rollback_account.key(), false), + ]; + + let ix_data = dapp::get_query_handle_call_message_accounts_ix_data( + NetworkAddress::new(&config.network_id, &id().to_string()), + rollback.rollback().to_owned(), + rollback.protocols().clone(), + )?; + + let dapp_key = rollback.from().to_owned(); + + let res = query_dapp_handle_call_message_accounts(dapp_key, ix_data, &ctx.remaining_accounts)?; + + let mut res_accounts = res.accounts; + account_metas.append(&mut res_accounts); + account_metas.push(AccountMetadata::new_readonly(dapp_key, false)); + + Ok(QueryAccountsPaginateResponse::new( + account_metas, + page, + limit, + )) +} + +pub fn query_handle_error_accounts( + ctx: Context, + sequence_no: u128, +) -> Result { + let config = &ctx.accounts.config; + let rollback_account = &ctx.accounts.rollback_account; + + let mut account_metas = vec![ + AccountMetadata::new(config.key(), false), + AccountMetadata::new(config.admin, false), + AccountMetadata::new(rollback_account.key(), false), + ]; + + if rollback_account.rollback.protocols().len() > 1 { + let msg = CSMessageResult::new(sequence_no, CSResponseType::CSResponseFailure, None); + let (pending_response, _) = Pubkey::find_program_address( + &[ + PendingResponse::SEED_PREFIX.as_bytes(), + &hash::hash(&msg.as_bytes()).to_bytes(), + ], + &id(), + ); + account_metas.push(AccountMetadata::new(pending_response, false)); + } else { + account_metas.push(AccountMetadata::new(id(), false)) + } + + Ok(QueryAccountsResponse { + accounts: account_metas, + }) +} + +pub fn query_dapp_handle_call_message_accounts<'info>( + dapp_key: Pubkey, + ix_data: Vec, + remaining_accounts: &[AccountInfo<'info>], +) -> Result { + let mut account_metas = vec![]; + let mut account_infos = vec![]; + + for (_, account) in remaining_accounts.iter().enumerate() { + if account.is_writable { + account_metas.push(AccountMeta::new(account.key(), account.is_signer)); + } else { + account_metas.push(AccountMeta::new_readonly(account.key(), account.is_signer)); + } + account_infos.push(account.to_account_info()) + } + + let ix = Instruction { + program_id: dapp_key, + accounts: account_metas, + data: ix_data, + }; + + invoke(&ix, &account_infos)?; + + let (_, data) = get_return_data().unwrap(); + let mut data_slice: &[u8] = &data; + let res = QueryAccountsResponse::deserialize(&mut data_slice)?; + + Ok(res) +} + +pub fn query_connection_send_message_accoounts<'info>( + i: usize, + connection_key: Pubkey, + ix_data: Vec, + remaining_accounts: &[AccountInfo<'info>], +) -> Result { + let conn_config = &remaining_accounts[i]; + + let account_metas = vec![AccountMeta::new(conn_config.key(), false)]; + let account_infos = vec![conn_config.to_account_info()]; + + let ix = Instruction { + program_id: connection_key, + accounts: account_metas, + data: ix_data, + }; + + invoke(&ix, &account_infos)?; + + let (_, data) = get_return_data().unwrap(); + let mut data_slice: &[u8] = &data; + let res = QueryAccountsResponse::deserialize(&mut data_slice)?; + + Ok(res) +} + +#[derive(Accounts)] +#[instruction(req_id: u128, data: Vec)] +pub struct QueryExecuteCallAccountsCtx<'info> { + #[account( + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump, + )] + pub config: Account<'info, Config>, + + #[account( + seeds = [ProxyRequest::SEED_PREFIX.as_bytes(), &req_id.to_be_bytes()], + bump = proxy_request.bump + )] + pub proxy_request: Account<'info, ProxyRequest>, +} + +#[derive(Accounts)] +#[instruction(from_nid: String, msg: Vec, sequence_no: u128)] +pub struct QueryHandleMessageAccountsCtx<'info> { + #[account( + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump, + )] + pub config: Account<'info, Config>, + + #[account( + seeds = [RollbackAccount::SEED_PREFIX.as_bytes(), &sequence_no.to_be_bytes()], + bump + )] + pub rollback_account: Option>, +} + +#[derive(Accounts)] +#[instruction(sn: u128)] +pub struct QueryExecuteRollbackAccountsCtx<'info> { + #[account( + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump, + )] + pub config: Account<'info, Config>, + + #[account( + seeds = [RollbackAccount::SEED_PREFIX.as_bytes(), &sn.to_be_bytes()], + bump = rollback_account.bump + )] + pub rollback_account: Account<'info, RollbackAccount>, +} + +#[derive(Accounts)] +#[instruction(sequence_no: u128)] +pub struct QueryHandleErrorAccountsCtx<'info> { + #[account( + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump, + )] + pub config: Account<'info, Config>, + + #[account( + seeds = [RollbackAccount::SEED_PREFIX.as_bytes(), &sequence_no.to_be_bytes()], + bump + )] + pub rollback_account: Account<'info, RollbackAccount>, +} diff --git a/contracts/solana/programs/xcall/src/instructions/send_message.rs b/contracts/solana/programs/xcall/src/instructions/send_message.rs new file mode 100644 index 00000000..3e36e2de --- /dev/null +++ b/contracts/solana/programs/xcall/src/instructions/send_message.rs @@ -0,0 +1,261 @@ +use anchor_lang::{ + prelude::*, + solana_program::{program::invoke, system_instruction, sysvar}, +}; +use xcall_lib::{ + message::{envelope::Envelope, msg_trait::IMessage, AnyMessage}, + network_address::NetworkAddress, +}; + +use crate::{ + connection, + error::XcallError, + event, helper, + state::*, + types::{message::CSMessage, request::CSMessageRequest, rollback::Rollback}, +}; + +/// Sends a cross-chain message to a specified network address. +/// +/// This function handles encoding, validation, and sending of a cross-chain message. +/// It also manages the creation of a rollback account if needed and emits an event upon successful +/// sending +/// +/// # Arguments +/// - `ctx`: The context of the solana program instruction +/// - `message`: The `Envelope` payload, encoded as rlp bytes +/// - `to`: The target network address where the message is to be sent +/// +/// # Returns +/// - `Result`: The sequence number of the message if successful, wrapped in a `Result`. +pub fn send_call<'info>( + ctx: Context<'_, '_, '_, 'info, SendCallCtx<'info>>, + message: Vec, + to: NetworkAddress, +) -> Result { + let envelope: Envelope = rlp::decode(&message).map_err(|_| XcallError::DecodeFailed)?; + + let sequence_no = ctx.accounts.config.get_next_sn(); + let config = &ctx.accounts.config; + + // Determine the sender's key + let from_key = if helper::is_program(&ctx.accounts.instruction_sysvar)? { + let dapp_authority = ctx + .accounts + .dapp_authority + .as_ref() + .ok_or(XcallError::DappAuthorityNotProvided)?; + + helper::ensure_dapp_authority(dapp_authority.owner, dapp_authority.key())?; + dapp_authority.owner.to_owned() + } else { + ctx.accounts.signer.key() + }; + + // Validate the message payload and rollback account + validate_payload( + &ctx.accounts.rollback_account, + &ctx.accounts.instruction_sysvar, + &envelope, + )?; + + // Handle rollback logic if rollback message is present + if envelope.message.rollback().is_some() { + let rollback = Rollback::new( + from_key, + to.clone(), + envelope.sources.clone(), + envelope.message.rollback().unwrap(), + false, + ); + + let rollback_account = ctx.accounts.rollback_account.as_deref_mut().unwrap(); + rollback_account.set(rollback, ctx.bumps.rollback_account.unwrap()); + } + + let from = NetworkAddress::new(&config.network_id, &from_key.to_string()); + + let request = CSMessageRequest::new( + from, + to.account(), + sequence_no, + envelope.message.msg_type(), + envelope.message.data(), + envelope.destinations, + ); + + // Determine if a response is needed for the request + let need_response = request.need_response(); + + let cs_message = CSMessage::from(request.clone()).as_bytes(); + helper::ensure_data_length(&cs_message)?; + + let sn: i64 = if need_response { sequence_no as i64 } else { 0 }; + let ix_data = connection::get_send_message_ix_data(&to.nid(), sn, cs_message)?; + + // Send the message to all specified source addresses + for (i, _) in envelope.sources.iter().enumerate() { + connection::call_connection_send_message( + i, + &ix_data, + &envelope.sources, + &ctx.accounts.config, + &ctx.accounts.signer, + &ctx.accounts.system_program, + &ctx.remaining_accounts, + )?; + } + + // If a protocol fee is configured, claim it from signer to fee handler account + if config.protocol_fee > 0 { + claim_protocol_fee( + &ctx.accounts.signer, + &ctx.accounts.fee_handler, + &ctx.accounts.system_program, + config.protocol_fee, + )?; + } + + emit!(event::CallMessageSent { + from: from_key, + to: to.to_string(), + sn: sequence_no, + }); + + Ok(sequence_no) +} + +/// Validates the payload of an envelope message +/// +/// This function checks that the sources and destinations of the envelope are specified, +/// and that the correct conditions are met for different types of messages, including +/// handling of rollback accounts when necessary. +/// +/// # Arguments +/// - `rollback_account`: An optional account that holds rollback information. This should be +/// `None` if rollback is not applicable. +/// - `sysvar_account_info`: Account information for the system variable used to determine +/// if the caller is a program. +/// - `envelope`: The envelope containing the message to be validated. +/// +/// # Returns +/// - `Result<()>`: Returns `Ok(())` if validation passes, or an error if validation fails. +pub fn validate_payload( + rollback_account: &Option>, + sysvar_account_info: &AccountInfo, + envelope: &Envelope, +) -> Result<()> { + if envelope.sources.is_empty() { + return Err(XcallError::SourceProtocolsNotSpecified.into()); + } + if envelope.destinations.is_empty() { + return Err(XcallError::DestinationProtocolsNotSpecified.into()); + } + + match &envelope.message { + AnyMessage::CallMessage(_) => { + if rollback_account.is_some() { + return Err(XcallError::RollbackAccountMustNotBeSpecified.into()); + } + } + AnyMessage::CallMessagePersisted(_) => { + if rollback_account.is_some() { + return Err(XcallError::RollbackAccountMustNotBeSpecified.into()); + } + } + AnyMessage::CallMessageWithRollback(msg) => { + if rollback_account.is_none() { + return Err(XcallError::RollbackAccountNotSpecified.into()); + } + if !helper::is_program(sysvar_account_info)? { + return Err(XcallError::RollbackNotPossible.into()); + } + helper::ensure_rollback_size(&msg.rollback)?; + } + } + + Ok(()) +} + +/// Claims the protocol fee by transferring the specified amount from the signer to the fee handler. +/// +/// This function creates a system instruction to transfer the `protocol_fee` from the `signer` +/// account to the `fee_handler` account using the system_program. +/// +/// # Arguments +/// - `signer`: The account that is paying the protocol fee. This account must have sufficient +/// funds to cover the `protocol_fee`. +/// - `fee_handler`: The account that receives the protocol fee. Typically, this is a +/// designated fee collector or treasury account. +/// - `system_program`: The system program that facilitates the transfer. This must be the +/// standard Solana system program account. +/// - `protocol_fee`: The amount of fee, in lamports, to be transferred from the `signer` to +/// the `fee_handler`. +/// +/// # Returns +/// - `Result<()>`: Returns `Ok(())` if the fee transfer is successful, or an error if the +/// transfer fails. +pub fn claim_protocol_fee<'info>( + signer: &AccountInfo<'info>, + fee_handler: &AccountInfo<'info>, + system_program: &AccountInfo<'info>, + protocol_fee: u64, +) -> Result<()> { + let ix = system_instruction::transfer(&signer.key(), &fee_handler.key(), protocol_fee); + invoke( + &ix, + &[ + signer.to_owned(), + fee_handler.to_owned(), + system_program.to_owned(), + ], + )?; + + Ok(()) +} + +#[derive(Accounts)] +pub struct SendCallCtx<'info> { + /// The account that signs and pays for the transaction. This account is mutable + /// because it will be debited for any fees or rent required during the transaction. + #[account(mut)] + pub signer: Signer<'info>, + + pub dapp_authority: Option>, + + /// The solana system program account, used for creating and managing accounts. + pub system_program: Program<'info, System>, + + /// CHECK: The instruction sysvar account, used to verify if the current instruction is a + /// program invocation. This account is an unchecked account because the constraints are + /// verified within the account trait. + #[account(address = sysvar::instructions::id())] + pub instruction_sysvar: UncheckedAccount<'info>, + + /// The configuration account, which stores important settings and counters for the + /// program. This account is mutable because the sequence number for messages will be updated. + #[account( + mut, + seeds = [Config::SEED_PREFIX.as_bytes()], + bump = config.bump + )] + pub config: Account<'info, Config>, + + /// CHECK: The account designated to receive protocol fees. This account is checked + /// against the `config.fee_handler` to ensure it is valid. This is a safe unchecked account + /// because the validity of the fee handler is verified during instruction execution + #[account(mut, address = config.fee_handler @ XcallError::InvalidFeeHandler)] + pub fee_handler: AccountInfo<'info>, + + /// An optional rollback account that stores information for undoing the effects of the call + /// if needed. The account is initialized when necessary, with the `signer` paying for its + /// creation. + #[account( + init, + payer = signer, + space = RollbackAccount::SIZE, + seeds = [RollbackAccount::SEED_PREFIX.as_bytes(), &(config.sequence_no + 1).to_be_bytes()], + bump, + )] + pub rollback_account: Option>, +} diff --git a/contracts/solana/programs/xcall/src/lib.rs b/contracts/solana/programs/xcall/src/lib.rs new file mode 100644 index 00000000..4506dc4e --- /dev/null +++ b/contracts/solana/programs/xcall/src/lib.rs @@ -0,0 +1,453 @@ +use anchor_lang::prelude::*; + +pub mod connection; +pub mod constants; +pub mod dapp; +pub mod error; +pub mod event; +pub mod helper; +pub mod instructions; +pub mod state; +pub mod types; + +use instructions::*; + +use types::message::CSMessageDecoded; +use xcall_lib::{ + network_address::NetworkAddress, + query_account_type::{QueryAccountsPaginateResponse, QueryAccountsResponse}, +}; + +declare_id!("GjmGXdyHoJohGvg5k2PUxBLtgfanRLfTXHWLXWDD6BZY"); + +#[program] +pub mod xcall { + use super::*; + + /// Instruction: Initialize + /// + /// Initializes the initial program configuration + /// + /// This function sets up the initial configuration for the program, including specifying + /// the network ID. + /// + /// # Arguments + /// - `ctx`: The context of the solana program instruction + /// - `network_id`: A string representing the network ID to be set in the configuration. + /// + /// # Returns + /// - `Result<()>`: Returns `Ok(())` if the initialization is successful, otherwise returns an + /// error. + pub fn initialize(ctx: Context, network_id: String) -> Result<()> { + instructions::initialize(ctx, network_id) + } + + /// Instruction: Set Admin + /// + /// Sets a new admin account in the configuration. + /// + /// This function updates the admin account in the program’s configuration. Only the current + /// admin (as verified by the context) can change the admin account. + /// + /// # Arguments + /// - `ctx`: The context of the solana program instruction + /// - `account`: The public key of the new admin account to be set. + /// + /// # Returns + /// - `Result<()>`: Returns `Ok(())` if the admin account is successfully updated, otherwise + /// returns an error. + pub fn set_admin(ctx: Context, account: Pubkey) -> Result<()> { + instructions::set_admin(ctx, account) + } + + /// Instruction: Set Protocol Fee + /// + /// Sets the protocol fee in the configuration account. + /// + /// This function verifies that the signer is an admin, and updates the protocol fee in the + /// program's configuration account. The protocol fee is the amount charged for each + /// cross-chain message sent. + /// + /// # Arguments + /// - `ctx`: The context of the solana program instruction + /// - `fee`: The new protocol fee to be set, specified as a `u64` value. + /// + /// # Returns + /// - `Result<()>`: Returns `Ok(())` if the protocol fee is successfully set, otherwise returns + /// an error. + pub fn set_protocol_fee(ctx: Context, fee: u64) -> Result<()> { + instructions::set_protocol_fee(ctx, fee) + } + + /// Instruction: Set Protocol Fee Handler + /// + /// Sets the specified pubkey as a protocol fee handler + /// + /// This function verifies that the signer is an admin of the program and sets `fee_handler` as + /// a protocol fee handler. Typically, this is a designated fee collector or treasury account + /// + /// # Arguments + /// - `ctx`: The context of the solana program instruction + /// - `fee_handler`: The pubkey of the new fee handler. + /// + /// # Returns + /// - `Result<()>`: Returns `Ok(())` if the transaction is successful, or an error if it fails. + pub fn set_protocol_fee_handler( + ctx: Context, + fee_handler: Pubkey, + ) -> Result<()> { + instructions::set_protocol_fee_handler(ctx, fee_handler) + } + + /// Instruction: Send Call + /// + /// Sends a cross-chain message to a specified network address. + /// + /// This function handles encoding, validation, and sending of a cross-chain message. + /// It also manages the creation of a rollback account if needed and emits an event upon successful + /// sending + /// + /// # Arguments + /// - `ctx`: The context of the solana program instruction + /// - `message`: The `Envelope` payload, encoded as rlp bytes + /// - `to`: The target network address where the message is to be sent + /// + /// # Returns + /// - `Result`: The sequence number of the message if successful, wrapped in a `Result`. + pub fn send_call<'info>( + ctx: Context<'_, '_, '_, 'info, SendCallCtx<'info>>, + envelope: Vec, + to: NetworkAddress, + ) -> Result { + instructions::send_call(ctx, envelope, to) + } + + /// Instruction: Handle Message + /// + /// Entry point for handling cross-chain messages within the xcall program. + /// + /// This function delegates the processing of an incoming message to the inner `handle_message` + /// function, passing along the necessary context and message details. It determines the type of + /// the message and invokes the appropriate logic to handle requests or responses from other + /// chains. + /// + /// # Parameters + /// - `ctx`: The context containing all necessary accounts and program-specific information. + /// - `from_nid`: The network ID of the chain that sent the message. + /// - `msg`: The encoded message payload received from the chain. + /// - `sequence_no`: The sequence number associated with the message, used to track message + /// ordering and responses. + /// + /// # Returns + /// - `Result<()>`: Returns `Ok(())` if the message is successfully handled, or an error if any + /// validation or processing fails. + pub fn handle_message<'info>( + ctx: Context<'_, '_, '_, 'info, HandleMessageCtx<'info>>, + from_nid: String, + msg: Vec, + sequence_no: u128, + ) -> Result<()> { + instructions::handle_message(ctx, from_nid, msg, sequence_no) + } + + /// Instruction: Handle Request + /// + /// Invokes the inner `handle_request` function to process an incoming cross-chain request. + /// + /// This instruction is specifically designed to be called by the xcall program. It delegates + /// the processing of the request message to the inner `handle_request` function, passing + /// along the necessary context and message payload. + /// + /// # Parameters + /// - `ctx`: Context containing all relevant accounts and program-specific information. + /// - `from_nid`: Network ID of the chain that sent the request. + /// - `msg_payload`: Encoded payload of the request message. + /// + /// # Returns + /// - `Result<()>`: Returns `Ok(())` if the request is processed successfully, or an error if + /// validation or processing fails. + pub fn handle_request<'info>( + ctx: Context<'_, '_, '_, 'info, HandleRequestCtx<'info>>, + from_nid: String, + msg_payload: Vec, + ) -> Result<()> { + instructions::handle_request(ctx, from_nid, &msg_payload) + } + + /// Instruction: Handle Result + /// + /// Invokes the inner `handle_result` function to process an incoming cross-chain result. + /// + /// This instruction is specifically designed to be called by the xcall program. It forwards + /// the result message along with its associated sequence number to the inner `handle_result` + /// function for further processing. + /// + /// # Parameters + /// - `ctx`: Context containing all relevant accounts and program-specific information. + /// - `from_nid`: Network ID of the chain that sent the result. + /// - `msg_payload`: Encoded payload of the result message. + /// - `sequence_no`: Unique sequence number of the result message. + /// + /// # Returns + /// - `Result<()>`: Returns `Ok(())` if the result is processed successfully, or an error if + /// validation or processing fails. + #[allow(unused_variables)] + pub fn handle_result<'info>( + ctx: Context<'_, '_, '_, 'info, HandleResultCtx<'info>>, + from_nid: String, + msg_payload: Vec, + sequence_no: u128, + ) -> Result<()> { + instructions::handle_result(ctx, &msg_payload) + } + + /// Instruction: Handle Error + /// + /// Handles an error for a specific sequence of messages, enabling a rollback to revert the state. + /// This function is called when a rollback message is received for a sequence originally sent from + /// the Solana chain. It triggers a rollback to revert the state to the point before the error occurred. + /// + /// # Arguments + /// + /// * `ctx` - The context providing access to accounts and program state. + /// * `sequence_no` - The unique identifier for the message sequence that encountered the error. + /// + /// # Returns + /// + /// Returns a `Result` indicating the success or failure of the rollback operation. + pub fn handle_error<'info>( + ctx: Context<'_, '_, '_, 'info, HandleErrorCtx<'info>>, + sequence_no: u128, + ) -> Result<()> { + instructions::handle_error(ctx, sequence_no) + } + + /// Instruction: Get Fee + /// + /// Calculates and retrieves the total fee for a cross-chain message, including the protocol fee + /// and connection-specific fees. + /// + /// This function computes the total fee required to send a cross-chain message by adding the + /// protocol fee stored in the configuration account and any additional fees specific to the + /// connections used in the message. + /// + /// # Arguments + /// - `ctx`: The context of the solana program instruction. + /// - `nid`: A string representing the network ID for which the fee is being calculated. + /// - `is_rollback`: A boolean indicating whether a rollback is required, affecting the fee. + /// - `sources`: A vector of strings representing the source protocols involved in the transaction. + /// + /// # Returns + /// - `Result`: Returns the total fee as a `u64` value if successful, otherwise returns + /// an error. + pub fn get_fee( + ctx: Context, + nid: String, + is_rollback: bool, + sources: Option>, + ) -> Result { + instructions::get_fee(ctx, nid, is_rollback, sources.unwrap_or(vec![])) + } + + /// Instruction: Get Admin + /// + /// Retrieves the admin public key from the configuration. + /// + /// This function returns the public key of the admin account, as stored in the configuration + /// account. + /// + /// # Arguments + /// - `ctx`: The context of the solana program instruction + /// + /// # Returns + /// - `Result`: Returns the public key of the admin account if successful, + /// otherwise returns an error. + pub fn get_admin(ctx: Context) -> Result { + Ok(ctx.accounts.config.admin) + } + + /// Instruction: Get Protocol Fee + /// + /// Retrieves the current protocol fee from the configuration. + /// + /// This function returns the protocol fee amount stored in the configuration account. + /// The protocol fee is a value used to determine the amount charged for each cross-chain + /// message. + /// + /// # Arguments + /// - `ctx`: The context of the solana program instruction + /// + /// # Returns + /// - `Result`: Returns the protocol fee as a `u64` value if successful, + /// otherwise returns an error. + pub fn get_protocol_fee(ctx: Context) -> Result { + Ok(ctx.accounts.config.protocol_fee) + } + + /// Instruction: Get Protocol Fee Handler + /// + /// Retrieves the protocol fee handler public key from the configuration. + /// + /// This function returns the public key of the protocol fee handler account, as stored + /// in the configuration account. + /// + /// # Arguments + /// - `ctx`: The context of the solana program instruction + /// + /// # Returns + /// - `Result`: Returns the public key of the fee handler account if successful, + /// otherwise returns an error. + pub fn get_protocol_fee_handler(ctx: Context) -> Result { + Ok(ctx.accounts.config.fee_handler) + } + + /// Instruction: Get Network Address + /// + /// Retrieves the network address from the configuration. + /// + /// This function constructs and returns a `NetworkAddress` based on the network ID stored + /// in the configuration account and the program's ID. + /// + /// # Arguments + /// - `ctx`: The context of the solana program instruction + /// + /// # Returns + /// - `Result`: Returns the constructed `NetworkAddress` if successful, + /// otherwise returns an error. + pub fn get_network_address(ctx: Context) -> Result { + Ok(NetworkAddress::new( + &ctx.accounts.config.network_id, + &id().to_string(), + )) + } + + /// Instruction: Decode CS Message + /// + /// Decodes a cross-chain message into its constituent parts. + /// + /// This function takes a serialized cross-chain message (`CSMessage`) and decodes it into + /// a structured format (`CSMessageDecoded`). Depending on the message type, it will decode + /// the message as either a `CSMessageRequest` or `CSMessageResult`. The decoded message + /// is returned as a `CSMessageDecoded` struct, which contains either the request or the result. + /// + /// # Parameters + /// - `ctx`: The context of the solana program instruction + /// - `message`: A vector of bytes representing the serialized cross-chain message to be decoded + /// + /// # Returns + /// - `Result`: Returns the decoded message as a `CSMessageDecoded` struct + /// if successful, otherwise returns an error. + #[allow(unused_variables)] + pub fn decode_cs_message<'info>( + ctx: Context<'_, '_, '_, 'info, DecodeCSMessageContext<'info>>, + message: Vec, + ) -> Result { + instructions::decode_cs_message(message) + } + + /// Instruction: Execute Call + /// + /// Executes a call of specified `req_id`. + /// + /// This instruction processes a call by verifying the provided data against the request's data + /// and then invoking the `handle_call_message` instruction on the DApp. Depending on the message + /// type, it handles the response accordingly, potentially sending a result back through the + /// connection program. + /// + /// # Parameters + /// - `ctx`: The context of the solana program instruction + /// - `req_id`: The unique identifier for the request being processed. + /// - `data`: The data associated with the call request, which will be verified and processed. + /// + /// # Returns + /// - `Result<()>`: Returns `Ok(())` if the call was executed successfully, or an error if it failed. + pub fn execute_call<'info>( + ctx: Context<'_, '_, '_, 'info, ExecuteCallCtx<'info>>, + req_id: u128, + data: Vec, + ) -> Result<()> { + instructions::execute_call(ctx, req_id, data) + } + + /// Instruction: Execute Rollback + /// + /// Executes a rollback operation using the stored rollback data. + /// + /// This function initiates a rollback process by delegating to the `instructions::execute_rollback`. + /// It handles the rollback of a operation with the specified context and sequence number. + /// + /// # Arguments + /// - `ctx`: The context containing all the necessary accounts and program state. + /// - `sn`: The sequence number associated with the rollback operation. + /// + /// # Returns + /// - `Result<()>`: Returns `Ok(())` if the rollback was executed successfully, or an error if it + /// failed. + pub fn execute_rollback<'info>( + ctx: Context<'_, '_, '_, 'info, ExecuteRollbackCtx<'info>>, + sn: u128, + ) -> Result<()> { + instructions::execute_rollback(ctx, sn) + } + + /// Initiates the handling of a forced rollback for a cross-chain message. This function acts + /// as a wrapper, calling the inner `handle_forced_rollback` instruction to handle the rollback + /// process. + /// + /// The rollback is triggered in response to a failure or error that occurred after a message + /// was received on the destination chain. It allows the dApp to revert the state by sending + /// a failure response back to the source chain, ensuring the original message is effectively + /// rolled back. + /// + /// # Arguments + /// * `ctx` - Context containing the accounts required for processing the forced rollback. + /// * `req_id` - The unique request ID associated with the message being rolled back. + /// + /// # Returns + /// * `Result<()>` - Returns `Ok(())` on successful execution, or an error if the rollback process + /// fails. + #[allow(unused_variables)] + pub fn handle_forced_rollback<'info>( + ctx: Context<'_, '_, '_, 'info, HandleForcedRollbackCtx<'info>>, + req_id: u128, + ) -> Result<()> { + instructions::handle_forced_rollback(ctx) + } + + pub fn query_execute_call_accounts<'info>( + ctx: Context<'_, '_, '_, 'info, QueryExecuteCallAccountsCtx<'info>>, + req_id: u128, + data: Vec, + page: u8, + limit: u8, + ) -> Result { + instructions::query_execute_call_accounts(ctx, req_id, data, page, limit) + } + + #[allow(unused_variables)] + pub fn query_execute_rollback_accounts<'info>( + ctx: Context<'_, '_, '_, 'info, QueryExecuteRollbackAccountsCtx<'info>>, + sn: u128, + page: u8, + limit: u8, + ) -> Result { + instructions::query_execute_rollback_accounts(ctx, page, limit) + } + + #[allow(unused_variables)] + pub fn query_handle_message_accounts( + ctx: Context, + from_nid: String, + msg: Vec, + sequence_no: u128, + ) -> Result { + instructions::query_handle_message_accounts(ctx, msg) + } + + pub fn query_handle_error_accounts( + ctx: Context, + sequence_no: u128, + ) -> Result { + instructions::query_handle_error_accounts(ctx, sequence_no) + } +} diff --git a/contracts/solana/programs/xcall/src/state.rs b/contracts/solana/programs/xcall/src/state.rs new file mode 100644 index 00000000..4bd29d5f --- /dev/null +++ b/contracts/solana/programs/xcall/src/state.rs @@ -0,0 +1,139 @@ +use anchor_lang::prelude::*; + +use crate::{ + constants::*, + error::XcallError, + types::{request::CSMessageRequest, rollback::Rollback}, +}; + +#[account] +#[derive(Debug)] +pub struct Config { + pub admin: Pubkey, + pub fee_handler: Pubkey, + pub network_id: String, + pub protocol_fee: u64, + pub sequence_no: u128, + pub last_req_id: u128, + pub bump: u8, +} + +impl Config { + pub const SEED_PREFIX: &'static str = "config"; + + pub const SIZE: usize = ACCOUNT_DISCRIMINATOR_SIZE + 32 + 32 + 32 + 8 + 16 + 16 + 1; + + pub fn new(&mut self, admin: Pubkey, network_id: String, bump: u8) { + self.admin = admin; + self.bump = bump; + self.fee_handler = admin; + self.network_id = network_id; + self.protocol_fee = 0; + self.sequence_no = 0; + self.last_req_id = 0; + } + + pub fn ensure_admin(&self, signer: Pubkey) -> Result<()> { + if self.admin != signer { + return Err(XcallError::OnlyAdmin.into()); + } + Ok(()) + } + + pub fn ensure_fee_handler(&self, signer: Pubkey) -> Result<()> { + if self.fee_handler != signer { + return Err(XcallError::OnlyAdmin.into()); + } + Ok(()) + } + + pub fn set_admin(&mut self, account: Pubkey) { + self.admin = account + } + + pub fn set_fee_handler(&mut self, fee_handler: Pubkey) { + self.fee_handler = fee_handler + } + + pub fn set_protocol_fee(&mut self, fee: u64) { + self.protocol_fee = fee + } + + pub fn get_next_sn(&mut self) -> u128 { + self.sequence_no += 1; + self.sequence_no + } + + pub fn get_next_req_id(&mut self) -> u128 { + self.last_req_id += 1; + self.last_req_id + } +} + +#[derive(Debug)] +#[account] +pub struct RollbackAccount { + pub rollback: Rollback, + pub bump: u8, +} + +impl RollbackAccount { + pub const SEED_PREFIX: &'static str = "rollback"; + + pub const SIZE: usize = 8 + 512 + 1; + + pub fn set(&mut self, rollback: Rollback, bump: u8) { + self.rollback = rollback; + self.bump = bump + } +} + +#[account] +pub struct PendingRequest { + pub sources: Vec, +} + +impl PendingRequest { + pub const SEED_PREFIX: &'static str = "req"; + + pub const SIZE: usize = ACCOUNT_DISCRIMINATOR_SIZE + 320; +} + +#[account] +pub struct PendingResponse { + pub sources: Vec, +} + +impl PendingResponse { + pub const SEED_PREFIX: &'static str = "res"; + + pub const SIZE: usize = ACCOUNT_DISCRIMINATOR_SIZE + 320; +} + +#[account] +pub struct SuccessfulResponse { + pub success: bool, +} + +impl SuccessfulResponse { + pub const SEED_PREFIX: &'static str = "success"; + + pub const SIZE: usize = ACCOUNT_DISCRIMINATOR_SIZE + 1; +} + +#[account] +pub struct ProxyRequest { + pub req: CSMessageRequest, + pub bump: u8, +} + +impl ProxyRequest { + pub const SEED_PREFIX: &'static str = "proxy"; + + pub const SIZE: usize = ACCOUNT_DISCRIMINATOR_SIZE + 1024 + 1; + + pub fn set(&mut self, req: CSMessageRequest, bump: u8) { + self.req = req; + self.bump = bump + } +} diff --git a/contracts/solana/programs/xcall/src/types/message.rs b/contracts/solana/programs/xcall/src/types/message.rs new file mode 100644 index 00000000..6a027c53 --- /dev/null +++ b/contracts/solana/programs/xcall/src/types/message.rs @@ -0,0 +1,102 @@ +use super::*; + +use crate::error::XcallError; +use request::CSMessageRequest; +use result::CSMessageResult; + +#[derive(Clone, Debug, AnchorSerialize, AnchorDeserialize)] +pub enum CSMessageType { + CSMessageRequest = 1, + CSMessageResult, +} + +#[derive(Clone, AnchorSerialize, AnchorDeserialize)] +pub struct CSMessageDecoded { + pub message_type: CSMessageType, + pub request: Option, + pub result: Option, +} + +#[derive(Clone)] +pub struct CSMessage { + pub message_type: CSMessageType, + pub payload: Vec, +} + +impl CSMessage { + pub fn new(message_type: CSMessageType, payload: Vec) -> Self { + Self { + message_type, + payload: payload.to_vec(), + } + } + + pub fn message_type(&self) -> &CSMessageType { + &self.message_type + } + + pub fn payload(&self) -> &[u8] { + &self.payload + } + + pub fn as_bytes(&self) -> Vec { + rlp::encode(&self.clone()).to_vec() + } +} + +impl Encodable for CSMessage { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + let msg_type: u8 = match self.message_type { + CSMessageType::CSMessageRequest => 1, + CSMessageType::CSMessageResult => 2, + }; + + stream.begin_list(2).append(&msg_type).append(&self.payload); + } +} + +impl Decodable for CSMessage { + fn decode(rlp: &rlp::Rlp) -> Result { + if !rlp.is_list() || rlp.item_count()? != 2 { + return Err(rlp::DecoderError::RlpIncorrectListLen); + } + + let msg_type: u8 = rlp.val_at(0)?; + + Ok(Self { + message_type: match msg_type { + 1 => Ok(CSMessageType::CSMessageRequest), + 2 => Ok(CSMessageType::CSMessageResult), + _ => Err(rlp::DecoderError::Custom("Invalid type")), + }?, + payload: rlp.val_at(1)?, + }) + } +} + +impl From for CSMessage { + fn from(value: CSMessageRequest) -> Self { + Self { + message_type: CSMessageType::CSMessageRequest, + payload: rlp::encode(&value).to_vec(), + } + } +} + +impl From for CSMessage { + fn from(value: CSMessageResult) -> Self { + Self { + message_type: CSMessageType::CSMessageResult, + payload: rlp::encode(&value).to_vec(), + } + } +} + +impl TryFrom> for CSMessage { + type Error = XcallError; + + fn try_from(value: Vec) -> Result { + let rlp = rlp::Rlp::new(&value); + Self::decode(&rlp).map_err(|_error| XcallError::DecodeFailed) + } +} diff --git a/contracts/solana/programs/xcall/src/types/mod.rs b/contracts/solana/programs/xcall/src/types/mod.rs new file mode 100644 index 00000000..e6d5a6db --- /dev/null +++ b/contracts/solana/programs/xcall/src/types/mod.rs @@ -0,0 +1,10 @@ +pub mod message; +pub mod request; +pub mod result; +pub mod rollback; + +use anchor_lang::{ + prelude::{borsh, Pubkey}, + solana_program, AnchorDeserialize, AnchorSerialize, +}; +use rlp::{Decodable, Encodable}; diff --git a/contracts/solana/programs/xcall/src/types/request.rs b/contracts/solana/programs/xcall/src/types/request.rs new file mode 100644 index 00000000..514e2861 --- /dev/null +++ b/contracts/solana/programs/xcall/src/types/request.rs @@ -0,0 +1,139 @@ +use super::*; +use std::str::FromStr; + +use crate::error::*; +use xcall_lib::{message::msg_type::MessageType, network_address::NetworkAddress}; + +#[derive(Clone, Debug, AnchorSerialize, AnchorDeserialize)] +pub struct CSMessageRequest { + from: NetworkAddress, + to: String, + sequence_no: u128, + msg_type: MessageType, + data: Vec, // TODO: cosmos this is nullable?? + protocols: Vec, +} + +impl CSMessageRequest { + pub fn new( + from: NetworkAddress, + to: String, + sequence_no: u128, + msg_type: MessageType, + data: Vec, + protocols: Vec, + ) -> Self { + Self { + from, + to, + sequence_no, + msg_type, + data, + protocols, + } + } + + pub fn from(&self) -> &NetworkAddress { + &self.from + } + + pub fn to(&self) -> &String { + &self.to + } + + pub fn sequence_no(&self) -> u128 { + self.sequence_no + } + + pub fn msg_type(&self) -> MessageType { + self.msg_type.clone() + } + + pub fn data(&self) -> Vec { + self.data.clone() + } + + pub fn hash_data(&mut self) { + let hash = solana_program::hash::hash(&self.data()); + self.data = hash.to_bytes().to_vec(); + } + + pub fn set_data(&mut self, data: Vec) { + self.data = data + } + + pub fn set_protocols(&mut self, protocols: Vec) { + self.protocols = protocols + } + + pub fn need_response(&self) -> bool { + self.msg_type == MessageType::CallMessageWithRollback + } + + pub fn allow_retry(&self) -> bool { + self.msg_type == MessageType::CallMessagePersisted + } + + pub fn protocols(&self) -> Vec { + self.protocols.clone() + } + + pub fn as_bytes(&self) -> Vec { + rlp::encode(self).to_vec() + } +} + +impl Encodable for CSMessageRequest { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + stream.begin_list(6); + + stream.append(&self.from.to_string()); + stream.append(&self.to); + stream.append(&self.sequence_no); + stream.append(&self.msg_type.as_int()); + stream.append(&self.data); + stream.begin_list(self.protocols.len()); + for protocol in self.protocols.iter() { + stream.append(protocol); + } + } +} + +impl Decodable for CSMessageRequest { + fn decode(rlp: &rlp::Rlp) -> Result { + if rlp.item_count()? != 6 { + return Err(rlp::DecoderError::RlpIncorrectListLen); + } + + let rlp_protocols = rlp.at(5)?; + let list: Vec = rlp_protocols.as_list()?; + let str_from: String = rlp.val_at(0)?; + let int_msg_type: u8 = rlp.val_at(3)?; + + Ok(Self { + from: NetworkAddress::from_str(&str_from) + .map_err(|_e| rlp::DecoderError::RlpInvalidLength)?, + to: rlp.val_at(1)?, + sequence_no: rlp.val_at(2)?, + msg_type: MessageType::from_int(int_msg_type), + data: rlp.val_at(4)?, + protocols: list, + }) + } +} + +impl TryFrom<&Vec> for CSMessageRequest { + type Error = XcallError; + fn try_from(value: &Vec) -> Result { + let rlp = rlp::Rlp::new(value as &[u8]); + Self::decode(&rlp).map_err(|_error| XcallError::DecodeFailed) + } +} + +impl TryFrom<&[u8]> for CSMessageRequest { + type Error = XcallError; + fn try_from(value: &[u8]) -> Result { + let rlp = rlp::Rlp::new(value); + Self::decode(&rlp).map_err(|_error| XcallError::DecodeFailed) + } +} diff --git a/contracts/solana/programs/xcall/src/types/result.rs b/contracts/solana/programs/xcall/src/types/result.rs new file mode 100644 index 00000000..a09507aa --- /dev/null +++ b/contracts/solana/programs/xcall/src/types/result.rs @@ -0,0 +1,102 @@ +use super::*; + +use crate::error::*; +use request::CSMessageRequest; + +#[derive(Clone, Debug, PartialEq, AnchorSerialize, AnchorDeserialize)] +pub enum CSResponseType { + CSResponseFailure, + CSResponseSuccess, +} + +impl From for u8 { + fn from(val: CSResponseType) -> Self { + val as u8 + } +} + +impl TryFrom for CSResponseType { + type Error = rlp::DecoderError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(CSResponseType::CSResponseFailure), + 1 => Ok(CSResponseType::CSResponseSuccess), + _ => Err(rlp::DecoderError::Custom("Invalid type")), + } + } +} + +#[derive(Clone, Debug, PartialEq, AnchorSerialize, AnchorDeserialize)] +pub struct CSMessageResult { + sequence_no: u128, + response_code: CSResponseType, + message: Vec, +} + +impl CSMessageResult { + pub fn new(sequence_no: u128, response_code: CSResponseType, reply: Option>) -> Self { + Self { + sequence_no, + response_code, + message: reply.unwrap_or(vec![]), + } + } + + pub fn sequence_no(&self) -> u128 { + self.sequence_no + } + + pub fn response_code(&self) -> &CSResponseType { + &self.response_code + } + + pub fn message(&self) -> Option { + if self.message.is_empty() { + return None; + } + rlp::decode(&self.message).ok() + } + + pub fn as_bytes(&self) -> Vec { + rlp::encode(&self.clone()).to_vec() + } +} + +impl Encodable for CSMessageResult { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + let code: u8 = self.response_code.clone().into(); + stream.begin_list(3); + stream.append(&self.sequence_no()); + stream.append(&code); + stream.append(&self.message); + } +} + +impl Decodable for CSMessageResult { + fn decode(rlp: &rlp::Rlp) -> Result { + let code: u8 = rlp.val_at(1)?; + + Ok(Self { + sequence_no: rlp.val_at(0)?, + response_code: CSResponseType::try_from(code)?, + message: rlp.val_at(2).unwrap_or(vec![]), + }) + } +} + +impl TryFrom<&Vec> for CSMessageResult { + type Error = XcallError; + fn try_from(value: &Vec) -> Result { + let rlp = rlp::Rlp::new(value as &[u8]); + Self::decode(&rlp).map_err(|_error| XcallError::DecodeFailed) + } +} + +impl TryFrom<&[u8]> for CSMessageResult { + type Error = XcallError; + fn try_from(value: &[u8]) -> Result { + let rlp = rlp::Rlp::new(value); + Self::decode(&rlp).map_err(|_error| XcallError::DecodeFailed) + } +} diff --git a/contracts/solana/programs/xcall/src/types/rollback.rs b/contracts/solana/programs/xcall/src/types/rollback.rs new file mode 100644 index 00000000..083d4aa9 --- /dev/null +++ b/contracts/solana/programs/xcall/src/types/rollback.rs @@ -0,0 +1,54 @@ +use super::*; + +use xcall_lib::network_address::NetworkAddress; + +#[derive(Clone, Debug, AnchorSerialize, AnchorDeserialize)] +pub struct Rollback { + from: Pubkey, + to: NetworkAddress, + enabled: bool, + rollback: Vec, + protocols: Vec, +} + +impl Rollback { + pub fn new( + from: Pubkey, + to: NetworkAddress, + protocols: Vec, + rollback: Vec, + enabled: bool, + ) -> Self { + Self { + from, + to, + rollback, + protocols, + enabled, + } + } + + pub fn from(&self) -> &Pubkey { + &self.from + } + + pub fn to(&self) -> &NetworkAddress { + &self.to + } + + pub fn rollback(&self) -> &[u8] { + &self.rollback + } + + pub fn enabled(&self) -> bool { + self.enabled + } + + pub fn protocols(&self) -> &Vec { + &self.protocols + } + + pub fn enable_rollback(&mut self) { + self.enabled = true; + } +} diff --git a/contracts/solana/scripts/centralized-connection/initialize.ts b/contracts/solana/scripts/centralized-connection/initialize.ts new file mode 100644 index 00000000..39531140 --- /dev/null +++ b/contracts/solana/scripts/centralized-connection/initialize.ts @@ -0,0 +1,41 @@ +import { PublicKey } from "@solana/web3.js"; +import { SYSTEM_PROGRAM_ID } from "@coral-xyz/anchor/dist/cjs/native/system"; + +import { ConnectionPDA } from "../../tests/centralized-connection/setup"; +import { connection, connectionProgram, wallet } from ".."; +import { TxnHelpers } from "../utils/transaction"; + +const args = process.argv.slice(2); +if (args.length != 2) throw new Error("Invalid arguments"); + +const xcallKey = new PublicKey(args[0]); +const adminKey = new PublicKey(args[1]); +let txnHelpers = new TxnHelpers(connection, wallet.payer); + +const initializeContract = async () => { + let connConfig = await connectionProgram.account.config.fetchNullable( + ConnectionPDA.config().pda + ); + if (!connConfig) { + return await connectionProgram.methods + .initialize(xcallKey, adminKey) + .signers([wallet.payer]) + .accountsStrict({ + signer: wallet.publicKey, + systemProgram: SYSTEM_PROGRAM_ID, + config: ConnectionPDA.config().pda, + authority: ConnectionPDA.authority().pda, + }) + .rpc(); + } +}; + +initializeContract() + .then(async (res) => { + console.log("Contract initializing"); + if (res) await txnHelpers.logParsedTx(res); + console.log("Contract initialized successfully"); + }) + .catch((err) => { + console.log(err); + }); diff --git a/contracts/solana/scripts/centralized-connection/setup.ts b/contracts/solana/scripts/centralized-connection/setup.ts new file mode 100644 index 00000000..c950dbeb --- /dev/null +++ b/contracts/solana/scripts/centralized-connection/setup.ts @@ -0,0 +1,44 @@ +import { PublicKey } from "@solana/web3.js"; + +import { uint128ToArray } from "../utils"; +import { connectionProgram } from ".."; + +export class ConnectionPDA { + constructor() {} + + static config() { + let [pda, bump] = PublicKey.findProgramAddressSync( + [Buffer.from("config")], + connectionProgram.programId + ); + + return { bump, pda }; + } + + static network_fee(networkId: string) { + const [pda, bump] = PublicKey.findProgramAddressSync( + [Buffer.from("fee"), Buffer.from(networkId)], + connectionProgram.programId + ); + + return { pda, bump }; + } + + static receipt(networkId: string, sn: number) { + const [pda, bump] = PublicKey.findProgramAddressSync( + [Buffer.from("receipt"), Buffer.from(networkId), uint128ToArray(sn)], + connectionProgram.programId + ); + + return { pda, bump }; + } + + static authority() { + let [pda, bump] = PublicKey.findProgramAddressSync( + [Buffer.from("connection_authority")], + connectionProgram.programId + ); + + return { bump, pda }; + } +} diff --git a/contracts/solana/scripts/index.ts b/contracts/solana/scripts/index.ts new file mode 100644 index 00000000..03c8a133 --- /dev/null +++ b/contracts/solana/scripts/index.ts @@ -0,0 +1,44 @@ +import os from "os"; + +import * as anchor from "@coral-xyz/anchor"; +import { Connection } from "@solana/web3.js"; + +import { loadKeypairFromFile } from "./utils"; + +/** PROGRAMS TYPE */ +import { Xcall } from "../target/types/xcall"; +import { CentralizedConnection } from "../target/types/centralized_connection"; +import { MockDappMulti } from "../target/types/mock_dapp_multi"; + +/** PROGRAMS IDL */ +import dappIdl from "../target/idl/mock_dapp_multi.json"; +import connectionIdl from "../target/idl/centralized_connection.json"; +import xcallIdl from "../target/idl/xcall.json"; + +/** RPC PROVIDER */ +export const RPC_URL = "http://127.0.0.1:8899"; +export const connection = new Connection(RPC_URL, "confirmed"); + +/** WALLET KEYPAIR */ +let keypairFilePath = os.homedir + "/.config/solana/id.json"; +export const keypair = loadKeypairFromFile(keypairFilePath); +export const wallet = new anchor.Wallet(keypair); + +/** PROVIDER FOR CLIENT */ +export const provider = new anchor.AnchorProvider(connection, wallet); +anchor.setProvider(provider); + +export const mockDappProgram: anchor.Program = + new anchor.Program( + dappIdl as anchor.Idl, + provider + ) as unknown as anchor.Program; +export const connectionProgram: anchor.Program = + new anchor.Program( + connectionIdl as anchor.Idl, + provider + ) as unknown as anchor.Program; +export const xcallProgram: anchor.Program = new anchor.Program( + xcallIdl as anchor.Idl, + provider +) as unknown as anchor.Program; diff --git a/contracts/solana/scripts/mock-dapp-multi/add-connection.ts b/contracts/solana/scripts/mock-dapp-multi/add-connection.ts new file mode 100644 index 00000000..7d87e929 --- /dev/null +++ b/contracts/solana/scripts/mock-dapp-multi/add-connection.ts @@ -0,0 +1,33 @@ +import { SYSTEM_PROGRAM_ID } from "@coral-xyz/anchor/dist/cjs/native/system"; + +import { DappPDA } from "../../tests/mock-dapp-multi/setup"; +import { connection, mockDappProgram, wallet } from ".."; +import { TxnHelpers } from "../utils/transaction"; + +const args = process.argv.slice(2); +if (args.length != 3) throw new Error("Invalid arguments"); + +const networkId = args[0]; +const srcEndpoint = args[1]; +const dstEndpoint = args[2]; +let txnHelpers = new TxnHelpers(connection, wallet.payer); + +const addConnection = async () => { + return await mockDappProgram.methods + .addConnection(networkId, srcEndpoint, dstEndpoint) + .accounts({ + connectionAccount: DappPDA.connections(networkId).pda, + sender: wallet.publicKey, + systemProgram: SYSTEM_PROGRAM_ID, + }) + .signers([wallet.payer]) + .rpc(); +}; + +addConnection() + .then(async (sig) => { + await txnHelpers.logParsedTx(sig); + }) + .catch((err) => { + console.log("Error while adding connection: ", err); + }); diff --git a/contracts/solana/scripts/mock-dapp-multi/initialize.ts b/contracts/solana/scripts/mock-dapp-multi/initialize.ts new file mode 100644 index 00000000..320ebfac --- /dev/null +++ b/contracts/solana/scripts/mock-dapp-multi/initialize.ts @@ -0,0 +1,40 @@ +import { PublicKey } from "@solana/web3.js"; +import { SYSTEM_PROGRAM_ID } from "@coral-xyz/anchor/dist/cjs/native/system"; + +import { DappPDA } from "../../tests/mock-dapp-multi/setup"; +import { connection, mockDappProgram, wallet } from ".."; +import { TxnHelpers } from "../utils/transaction"; + +const args = process.argv.slice(2); +if (args.length != 1) throw new Error("Invalid arguments"); + +const xcallKey = new PublicKey(args[0]); +let txnHelpers = new TxnHelpers(connection, wallet.payer); + +const initializeContract = async () => { + let dappConfig = await mockDappProgram.account.config.fetchNullable( + DappPDA.config().pda + ); + if (!dappConfig) { + return await mockDappProgram.methods + .initialize(xcallKey) + .signers([wallet.payer]) + .accountsStrict({ + sender: wallet.publicKey, + authority: DappPDA.authority().pda, + systemProgram: SYSTEM_PROGRAM_ID, + config: DappPDA.config().pda, + }) + .rpc(); + } +}; + +initializeContract() + .then(async (res) => { + console.log("Contract initializing"); + if (res) await txnHelpers.logParsedTx(res); + console.log("Contract initialized successfully"); + }) + .catch((err) => { + console.log(err); + }); diff --git a/contracts/solana/scripts/mock-dapp-multi/setup.ts b/contracts/solana/scripts/mock-dapp-multi/setup.ts new file mode 100644 index 00000000..574f7b63 --- /dev/null +++ b/contracts/solana/scripts/mock-dapp-multi/setup.ts @@ -0,0 +1,38 @@ +import { PublicKey } from "@solana/web3.js"; + +import { mockDappProgram } from ".."; + +export class DappPDA { + constructor() {} + + static config() { + let [pda, bump] = PublicKey.findProgramAddressSync( + [Buffer.from("config")], + mockDappProgram.programId + ); + + return { bump, pda }; + } + + static connections(networkId: string) { + const buffer1 = Buffer.from("connections"); + const buffer2 = Buffer.from(networkId); + const seed = [buffer1, buffer2]; + + const [pda, bump] = PublicKey.findProgramAddressSync( + seed, + mockDappProgram.programId + ); + + return { pda, bump }; + } + + static authority() { + let [pda, bump] = PublicKey.findProgramAddressSync( + [Buffer.from("dapp_authority")], + mockDappProgram.programId + ); + + return { bump, pda }; + } +} diff --git a/contracts/solana/scripts/utils/index.ts b/contracts/solana/scripts/utils/index.ts new file mode 100644 index 00000000..ce3eb6e6 --- /dev/null +++ b/contracts/solana/scripts/utils/index.ts @@ -0,0 +1,37 @@ +import fs from "fs"; +import { createHash } from "crypto"; +import { Keypair, PublicKey } from "@solana/web3.js"; + +export function loadKeypairFromFile(path: string): Keypair { + return Keypair.fromSecretKey( + Buffer.from(JSON.parse(fs.readFileSync(path, "utf-8"))) + ); +} + +export const sleep = (seconds: number) => { + return new Promise((resolve) => setTimeout(resolve, seconds * 1000)); +}; + +export const hash = (message: Uint8Array) => { + return createHash("sha256").update(message).digest("hex"); +}; + +export const uint128ToArray = (num: any) => { + if (typeof num === "string" || typeof num === "number") { + num = BigInt(num); + } else if (!(num instanceof BigInt)) { + throw new Error("Input must be a BigInt or convertible to a BigInt."); + } + + let buffer = new ArrayBuffer(16); + let view = new DataView(buffer); + + view.setBigUint64(0, num >> BigInt(64), false); + view.setBigUint64(8, num & BigInt("0xFFFFFFFFFFFFFFFF"), false); + + return new Uint8Array(buffer); +}; + +export const SYSVAR_INSTRUCTIONS_ID = new PublicKey( + "Sysvar1nstructions1111111111111111111111111" +); diff --git a/contracts/solana/scripts/utils/transaction.ts b/contracts/solana/scripts/utils/transaction.ts new file mode 100644 index 00000000..6a2d05ca --- /dev/null +++ b/contracts/solana/scripts/utils/transaction.ts @@ -0,0 +1,128 @@ +import { + Connection, + Keypair, + AddressLookupTableProgram, + TransactionMessage, + TransactionInstruction, + PublicKey, + VersionedTransaction, +} from "@solana/web3.js"; + +import { sleep } from "."; + +export class TxnHelpers { + connection: Connection; + payer: Keypair; + lookupTable: PublicKey; + + constructor(connection: Connection, payer: Keypair) { + this.connection = connection; + this.payer = payer; + } + + async createAddressLookupTable() { + let recentSlot = await this.connection.getSlot("max"); + + let [createLookupTableIx, lookupTable] = + AddressLookupTableProgram.createLookupTable({ + authority: this.payer.publicKey, + payer: this.payer.publicKey, + recentSlot, + }); + + const tx = await this.buildV0Txn([createLookupTableIx], [this.payer]); + + await this.connection.sendTransaction(tx); + return (this.lookupTable = lookupTable); + } + + async extendAddressLookupTable(addresses: PublicKey[]) { + await sleep(2); + + let extendLookupTableIx = AddressLookupTableProgram.extendLookupTable({ + addresses, + authority: this.payer.publicKey, + lookupTable: this.lookupTable, + payer: this.payer.publicKey, + }); + + const tx = await this.buildV0Txn([extendLookupTableIx], [this.payer]); + await this.connection.sendTransaction(tx); + } + + async getAddressLookupTable() { + return await this.connection + .getAddressLookupTable(this.lookupTable) + .then((res) => res.value); + } + + async printAddressLookupTable() { + await sleep(2); + + const lookupTableAccount = await this.getAddressLookupTable(); + console.log(`Lookup Table: ${this.lookupTable}`); + + for (let i = 0; i < lookupTableAccount.state.addresses.length; i++) { + const address = lookupTableAccount.state.addresses[i]; + console.log( + `Index: ${i.toString().padEnd(2)} Address: ${address.toBase58()}` + ); + } + } + + async buildV0Txn(instructions: TransactionInstruction[], signers: Keypair[]) { + let blockHash = await this.connection + .getLatestBlockhash() + .then((res) => res.blockhash); + + const messageV0 = new TransactionMessage({ + payerKey: signers[0].publicKey, + recentBlockhash: blockHash, + instructions, + }).compileToV0Message(); + + const tx = new VersionedTransaction(messageV0); + signers.forEach((s) => tx.sign([s])); + return tx; + } + + async buildTxnWithLookupTable( + instructions: TransactionInstruction[], + signers: Keypair[] + ) { + await sleep(2); + + const lookupTableAccount = await this.connection + .getAddressLookupTable(this.lookupTable) + .then((res) => res.value); + + let blockhash = await this.connection + .getLatestBlockhash() + .then((res) => res.blockhash); + + let messageV0 = new TransactionMessage({ + payerKey: signers[0].publicKey, + recentBlockhash: blockhash, + instructions, + }).compileToV0Message([lookupTableAccount]); + + const tx = new VersionedTransaction(messageV0); + signers.forEach((s) => tx.sign([s])); + return tx; + } + + async airdrop(to: PublicKey, lamports: number) { + let aridropTx = await this.connection.requestAirdrop(to, lamports); + await this.connection.confirmTransaction(aridropTx, "confirmed"); + } + + async logParsedTx(txSignature: string) { + await sleep(2); + console.log( + await this.connection.getParsedTransaction(txSignature, { + commitment: "confirmed", + maxSupportedTransactionVersion: 0, + }) + ); + } +} diff --git a/contracts/solana/scripts/utils/types/envelope.ts b/contracts/solana/scripts/utils/types/envelope.ts new file mode 100644 index 00000000..1397a7fd --- /dev/null +++ b/contracts/solana/scripts/utils/types/envelope.ts @@ -0,0 +1,77 @@ +import * as rlp from "rlp"; + +export class CallMessage { + data: Uint8Array; + + constructor(data: Uint8Array) { + this.data = data; + } + + encode() { + let rlpInput: rlp.Input = [Buffer.from(this.data)]; + + return rlp.encode(rlpInput); + } +} + +export class CallMessageWithRollback { + data: Uint8Array; + rollback: Uint8Array; + + constructor(data: Uint8Array, rollback: Uint8Array) { + this.data = data; + this.rollback = rollback; + } + + encode() { + let rlpInput: rlp.Input = [ + Buffer.from(this.data), + Buffer.from(this.rollback), + ]; + + return rlp.encode(rlpInput); + } +} + +export class CallMessagePersisted { + data: Uint8Array; + + constructor(data: Uint8Array) { + this.data = data; + } + + encode() { + let rlpInput: rlp.Input = [Buffer.from(this.data)]; + return rlp.encode(rlpInput); + } +} + +export class Envelope { + msg_type: number; + message: Uint8Array; + sources: string[]; + destinations: string[]; + + constructor( + msg_type: number, + message: Uint8Array, + sources: string[], + destinations: string[] + ) { + this.msg_type = msg_type; + this.message = message; + this.sources = sources; + this.destinations = destinations; + } + + encode() { + let rlpInput: rlp.Input = [ + this.msg_type, + Buffer.from(this.message), + this.sources, + this.destinations, + ]; + + return rlp.encode(rlpInput); + } +} diff --git a/contracts/solana/scripts/utils/types/index.ts b/contracts/solana/scripts/utils/types/index.ts new file mode 100644 index 00000000..90a07596 --- /dev/null +++ b/contracts/solana/scripts/utils/types/index.ts @@ -0,0 +1,4 @@ +export * from "./message"; +export * from "./request"; +export * from "./result"; +export * from "./envelope"; diff --git a/contracts/solana/scripts/utils/types/message.ts b/contracts/solana/scripts/utils/types/message.ts new file mode 100644 index 00000000..5b562da6 --- /dev/null +++ b/contracts/solana/scripts/utils/types/message.ts @@ -0,0 +1,32 @@ +import * as rlp from "rlp"; + +export enum MessageType { + CallMessage = 0, + CallMessageWithRollback, + CallMessagePersisted, +} + +export enum CSResponseType { + CSMessageFailure, + CSResponseSuccess, +} + +export enum CSMessageType { + CSMessageRequest = 1, + CSMessageResult, +} + +export class CSMessage { + message_type: CSMessageType; + payload: Uint8Array; + + constructor(message_type: CSMessageType, payload: Uint8Array) { + this.message_type = message_type; + this.payload = payload; + } + + encode() { + let rlpInput: rlp.Input = [this.message_type, Buffer.from(this.payload)]; + return rlp.encode(rlpInput); + } +} diff --git a/contracts/solana/scripts/utils/types/request.ts b/contracts/solana/scripts/utils/types/request.ts new file mode 100644 index 00000000..f2ab6278 --- /dev/null +++ b/contracts/solana/scripts/utils/types/request.ts @@ -0,0 +1,41 @@ +import * as rlp from "rlp"; + +import { MessageType } from "."; + +export class CSMessageRequest { + from: string; + to: string; + sequence_no: number; + msg_type: MessageType; + data: Uint8Array; + protocols: string[]; + + constructor( + from: string, + to: string, + sequence_no: number, + msg_type: MessageType, + data: Uint8Array, + protocols: string[] + ) { + this.from = from; + this.to = to; + this.sequence_no = sequence_no; + this.msg_type = msg_type; + this.data = data; + this.protocols = protocols; + } + + encode() { + let rlpInput: rlp.Input = [ + this.from, + this.to, + this.sequence_no, + this.msg_type, + Buffer.from(this.data), + this.protocols, + ]; + + return rlp.encode(rlpInput); + } +} diff --git a/contracts/solana/scripts/utils/types/result.ts b/contracts/solana/scripts/utils/types/result.ts new file mode 100644 index 00000000..2d74ed3d --- /dev/null +++ b/contracts/solana/scripts/utils/types/result.ts @@ -0,0 +1,29 @@ +import * as rlp from "rlp"; + +import { CSMessageType, CSResponseType } from "./message"; + +export class CSMessageResult { + sequence_no: number; + response_code: CSResponseType; + data: Uint8Array | null; + + constructor( + sequence_no: number, + response_code: CSResponseType, + data: Uint8Array | null + ) { + this.sequence_no = sequence_no; + this.response_code = response_code; + this.data = data; + } + + encode() { + let rlpInput: rlp.Input = [ + this.sequence_no, + this.response_code, + Buffer.from(this.data), + ]; + + return rlp.encode(rlpInput); + } +} diff --git a/contracts/solana/scripts/xcall/initialize.ts b/contracts/solana/scripts/xcall/initialize.ts new file mode 100644 index 00000000..1f46834d --- /dev/null +++ b/contracts/solana/scripts/xcall/initialize.ts @@ -0,0 +1,38 @@ +import { SYSTEM_PROGRAM_ID } from "@coral-xyz/anchor/dist/cjs/native/system"; + +import { XcallPDA } from "./setup"; +import { connection, xcallProgram, wallet } from ".."; +import { TxnHelpers } from "../utils/transaction"; + +const args = process.argv.slice(2); +if (args.length != 1) throw new Error("Invalid arguments"); + +const networkId = args[0]; +let txnHelpers = new TxnHelpers(connection, wallet.payer); + +const initializeContract = async () => { + let xcallConfig = await xcallProgram.account.config.fetchNullable( + XcallPDA.config().pda + ); + if (!xcallConfig) { + return await xcallProgram.methods + .initialize(networkId) + .signers([wallet.payer]) + .accountsStrict({ + signer: wallet.publicKey, + systemProgram: SYSTEM_PROGRAM_ID, + config: XcallPDA.config().pda, + }) + .rpc(); + } +}; + +initializeContract() + .then(async (res) => { + console.log("Contract initializing"); + if (res) await txnHelpers.logParsedTx(res); + console.log("Contract initialized successfully"); + }) + .catch((err) => { + console.log(err); + }); diff --git a/contracts/solana/scripts/xcall/set-admin.ts b/contracts/solana/scripts/xcall/set-admin.ts new file mode 100644 index 00000000..c3a1dc58 --- /dev/null +++ b/contracts/solana/scripts/xcall/set-admin.ts @@ -0,0 +1,31 @@ +import { PublicKey } from "@solana/web3.js"; + +import { TxnHelpers } from "../utils/transaction"; +import { connection, wallet, xcallProgram } from ".."; +import { XcallPDA } from "./setup"; + +let args = process.argv.slice(2); +if (args.length != 1) throw new Error("Invalid arguments"); + +const adminKey = new PublicKey(args[0]); + +let txnHelpers = new TxnHelpers(connection, wallet.payer); + +const setAdmin = async () => { + return await xcallProgram.methods + .setAdmin(adminKey) + .accountsStrict({ + admin: wallet.publicKey, + config: XcallPDA.config().pda, + }) + .signers([wallet.payer]) + .rpc(); +}; + +setAdmin() + .then(async (sig) => { + await txnHelpers.logParsedTx(sig); + }) + .catch((err) => { + console.log("Error while setting admin: ", err); + }); diff --git a/contracts/solana/scripts/xcall/set-protocol-fee-handler.ts b/contracts/solana/scripts/xcall/set-protocol-fee-handler.ts new file mode 100644 index 00000000..3b1bbc84 --- /dev/null +++ b/contracts/solana/scripts/xcall/set-protocol-fee-handler.ts @@ -0,0 +1,31 @@ +import { PublicKey } from "@solana/web3.js"; + +import { TxnHelpers } from "../utils/transaction"; +import { connection, wallet, xcallProgram } from ".."; +import { XcallPDA } from "./setup"; + +let args = process.argv.slice(2); +if (args.length != 1) throw new Error("Invalid arguments"); + +const feeHandler = new PublicKey(args[0]); + +let txnHelpers = new TxnHelpers(connection, wallet.payer); + +const setProtocolFeeHandler = async () => { + return await xcallProgram.methods + .setProtocolFeeHandler(feeHandler) + .accountsStrict({ + admin: wallet.publicKey, + config: XcallPDA.config().pda, + }) + .signers([wallet.payer]) + .rpc(); +}; + +setProtocolFeeHandler() + .then(async (sig) => { + await txnHelpers.logParsedTx(sig); + }) + .catch((err) => { + console.log("Error while setting fee handler: ", err); + }); diff --git a/contracts/solana/scripts/xcall/set-protocol-fee.ts b/contracts/solana/scripts/xcall/set-protocol-fee.ts new file mode 100644 index 00000000..cc50c78b --- /dev/null +++ b/contracts/solana/scripts/xcall/set-protocol-fee.ts @@ -0,0 +1,31 @@ +import * as anchor from "@coral-xyz/anchor"; + +import { TxnHelpers } from "../utils/transaction"; +import { connection, wallet, xcallProgram } from ".."; +import { XcallPDA } from "./setup"; + +let args = process.argv.slice(2); +if (args.length != 1) throw new Error("Invalid arguments"); + +const protocolFee = args[0]; + +let txnHelpers = new TxnHelpers(connection, wallet.payer); + +const setProtocolFee = async () => { + return await xcallProgram.methods + .setProtocolFee(new anchor.BN(protocolFee)) + .accountsStrict({ + admin: wallet.publicKey, + config: XcallPDA.config().pda, + }) + .signers([wallet.payer]) + .rpc(); +}; + +setProtocolFee() + .then(async (sig) => { + await txnHelpers.logParsedTx(sig); + }) + .catch((err) => { + console.log("Error while setting fee: ", err); + }); diff --git a/contracts/solana/scripts/xcall/setup.ts b/contracts/solana/scripts/xcall/setup.ts new file mode 100644 index 00000000..9aea8148 --- /dev/null +++ b/contracts/solana/scripts/xcall/setup.ts @@ -0,0 +1,71 @@ +import { PublicKey } from "@solana/web3.js"; + +import { uint128ToArray } from "../utils"; +import { xcallProgram } from ".."; + +export class XcallPDA { + constructor() {} + + static config() { + let [pda, bump] = PublicKey.findProgramAddressSync( + [Buffer.from("config")], + xcallProgram.programId + ); + + return { bump, pda }; + } + + static proxyRequest(requestId: number) { + const [pda, bump] = PublicKey.findProgramAddressSync( + [Buffer.from("proxy"), uint128ToArray(requestId)], + xcallProgram.programId + ); + + return { pda, bump }; + } + + static successRes(sequenceNo: number) { + const [pda, bump] = PublicKey.findProgramAddressSync( + [Buffer.from("success"), uint128ToArray(sequenceNo)], + xcallProgram.programId + ); + + return { pda, bump }; + } + + static defaultConnection(netId: String) { + const [pda, bump] = PublicKey.findProgramAddressSync( + [Buffer.from("conn"), Buffer.from(netId)], + xcallProgram.programId + ); + + return { pda, bump }; + } + + static pendingRequest(messageBytes: Buffer) { + const [pda, bump] = PublicKey.findProgramAddressSync( + [Buffer.from("req"), messageBytes], + xcallProgram.programId + ); + + return { pda, bump }; + } + + static pendingResponse(messageBytes: Buffer) { + const [pda, bump] = PublicKey.findProgramAddressSync( + [Buffer.from("res"), messageBytes], + xcallProgram.programId + ); + + return { pda, bump }; + } + + static rollback(sequenceNo: number) { + const [pda, bump] = PublicKey.findProgramAddressSync( + [Buffer.from("rollback"), uint128ToArray(sequenceNo)], + xcallProgram.programId + ); + + return { pda, bump }; + } +} diff --git a/contracts/solana/tests/begin-initialize/initialize.ts b/contracts/solana/tests/begin-initialize/initialize.ts new file mode 100644 index 00000000..7783cfd1 --- /dev/null +++ b/contracts/solana/tests/begin-initialize/initialize.ts @@ -0,0 +1,115 @@ +import * as anchor from "@coral-xyz/anchor"; +import { assert, expect } from "chai"; + +import { TestContext as ConnectionTestContext } from "../centralized-connection/setup"; +import { TxnHelpers, sleep } from "../utils"; + +import { TestContext as DappTestCtx, DappPDA } from "../mock-dapp-multi/setup"; + +import { Xcall } from "../../target/types/xcall"; +import { TestContext as XcallTestContext, XcallPDA } from "../xcall/setup"; +import { CentralizedConnection } from "../../target/types/centralized_connection"; + +import { MockDappMulti } from "../../target/types/mock_dapp_multi"; +const dappProgram: anchor.Program = + anchor.workspace.MockDappMulti; + +const xcallProgram: anchor.Program = anchor.workspace.Xcall; +const connectionProgram: anchor.Program = + anchor.workspace.CentralizedConnection; + +describe("Initialize", () => { + const provider = anchor.AnchorProvider.env(); + let connection = provider.connection; + let wallet = provider.wallet as anchor.Wallet; + + let txnHelpers = new TxnHelpers(connection, wallet.payer); + + let connectionCtx = new ConnectionTestContext( + connection, + txnHelpers, + wallet.payer + ); + let xcallCtx = new XcallTestContext(connection, txnHelpers, wallet.payer); + let dappCtx = new DappTestCtx(connection, txnHelpers, wallet.payer); + + before(async () => { + await dappCtx.add_connection( + connectionCtx.dstNetworkId, + connectionProgram.programId.toString(), + connectionProgram.programId.toString() + ); + await sleep(2); + }); + + it("should initialize xcall program", async () => { + let ctx = new XcallTestContext(connection, txnHelpers, wallet.payer); + + let networkId = "solana"; + + await xcallCtx.initialize(networkId); + await sleep(2); + + let data = await ctx.getConfig(); + + assert.equal(data.networkId.toString(), networkId); + assert.equal(data.admin.toString(), wallet.publicKey.toString()); + assert.equal(data.feeHandler.toString(), wallet.publicKey.toString()); + assert.equal(data.protocolFee.toString(), new anchor.BN(0).toString()); + assert.equal(data.sequenceNo.toString(), new anchor.BN(0).toString()); + assert.equal(data.lastReqId.toString(), new anchor.BN(0).toString()); + }); + + it("should fail when initializing xcall program two times", async () => { + try { + await xcallCtx.initialize("solana"); + } catch (err) { + expect(err.message).to.includes( + "Error processing Instruction 0: custom program error: 0x0" + ); + } + }); + + it("should initialize centralized connection program", async () => { + await connectionCtx.initialize(); + await sleep(2); + + let data = await connectionCtx.getConfig(); + + assert.equal( + data.admin.toString(), + connectionCtx.signer.publicKey.toString() + ); + assert.equal(data.xcall.toString(), xcallProgram.programId.toString()); + assert.equal(data.sn.toString(), new anchor.BN(0).toString()); + }); + + it("should fail when initializing connection progarm two times", async () => { + try { + await connectionCtx.initialize(); + } catch (err) { + expect(err.message).to.includes( + "Error processing Instruction 0: custom program error: 0x0" + ); + } + }); + + it("should initialize dapp program", async () => { + await dappCtx.initialize(); + await sleep(2); + + let { xcallAddress } = await dappCtx.getConfig(); + + assert.equal(xcallProgram.programId.toString(), xcallAddress.toString()); + }); + + it("should fail when initializing dapp program two times", async () => { + try { + await dappCtx.initialize(); + } catch (err) { + expect(err.message).to.includes( + "Error processing Instruction 0: custom program error: 0x0" + ); + } + }); +}); diff --git a/contracts/solana/tests/centralized-connection/centralized-connection.ts b/contracts/solana/tests/centralized-connection/centralized-connection.ts new file mode 100644 index 00000000..7d3d51f2 --- /dev/null +++ b/contracts/solana/tests/centralized-connection/centralized-connection.ts @@ -0,0 +1,752 @@ +import * as anchor from "@coral-xyz/anchor"; +import { assert, expect } from "chai"; +import { Keypair, PublicKey } from "@solana/web3.js"; + +import { TestContext, ConnectionPDA } from "./setup"; +import { SYSVAR_INSTRUCTIONS_ID, TxnHelpers, hash, sleep } from "../utils"; +import { SYSTEM_PROGRAM_ID } from "@coral-xyz/anchor/dist/cjs/native/system"; +import { CentralizedConnection } from "../../target/types/centralized_connection"; + +import { Xcall } from "../../target/types/xcall"; +import { XcallPDA } from "../xcall/setup"; +import { + CSMessage, + CSMessageRequest, + CSMessageResult, + CSMessageType, + CSResponseType, + MessageType, +} from "../xcall/types"; +import { TestContext as XcallTestContext } from "../xcall/setup"; + +import { MockDappMulti } from "../../target/types/mock_dapp_multi"; +import { DappPDA, TestContext as MockDappCtx } from "../mock-dapp-multi/setup"; + +const xcallProgram: anchor.Program = anchor.workspace.Xcall; +const connectionProgram: anchor.Program = + anchor.workspace.CentralizedConnection; +const mockDappProgram: anchor.Program = + anchor.workspace.MockDappMulti; + +describe("CentralizedConnection", () => { + const provider = anchor.AnchorProvider.env(); + const connection = provider.connection; + const wallet = provider.wallet as anchor.Wallet; + + let txnHelpers = new TxnHelpers(connection, wallet.payer); + let ctx = new TestContext(connection, txnHelpers, wallet.payer); + + let xcallCtx = new XcallTestContext(connection, txnHelpers, wallet.payer); + + let mockDappCtx = new MockDappCtx(connection, txnHelpers, ctx.admin); + + before(async () => { + await ctx.setNetworkFee("icon", 50, 50); + sleep(2); + }); + + it("[set_admin]: should set the new admin", async () => { + let newAdmin = Keypair.generate(); + await ctx.setAdmin(newAdmin); + + await sleep(2); + + let { admin } = await ctx.getConfig(); + assert.equal(ctx.admin.publicKey.toString(), admin.toString()); + }); + + it("[set_admin]: should fail if not called by admin", async () => { + let non_admin = Keypair.generate(); + + try { + await ctx.program.methods + .setAdmin(Keypair.generate().publicKey) + .accountsStrict({ + admin: non_admin.publicKey, + config: ConnectionPDA.config().pda, + }) + .signers([non_admin]) + .rpc(); + } catch (err) { + expect(err.message).to.includes("Only admin"); + } + }); + + it("[set_fee]: should set the fee for network ID", async () => { + let msg_fee = 50; + let res_fee = 100; + + await txnHelpers.airdrop(ctx.admin.publicKey, 1e9); + await sleep(2); + + await ctx.setNetworkFee(ctx.dstNetworkId, msg_fee, res_fee); + await sleep(2); + + let fee = await ctx.getFee(ctx.dstNetworkId); + assert.equal(fee.messageFee.toNumber(), msg_fee); + assert.equal(fee.responseFee.toNumber(), res_fee); + }); + + it("[claim_fees]: should claim fee stored in PDA account", async () => { + let config = ConnectionPDA.config().pda; + + let transfer_amount = 500_000; + await txnHelpers.airdrop(config, transfer_amount); + await sleep(2); + + const min_rent_exempt_balance = + await ctx.connection.getMinimumBalanceForRentExemption(90); + const before_pda_balance = (await ctx.connection.getAccountInfo(config)) + .lamports; + assert.equal(min_rent_exempt_balance + transfer_amount, before_pda_balance); + + await ctx.program.methods + .claimFees() + .accountsStrict({ + admin: ctx.admin.publicKey, + config: ConnectionPDA.config().pda, + }) + .signers([ctx.admin]) + .rpc(); + + const after_pda_balance = (await ctx.connection.getAccountInfo(config)) + .lamports; + assert.equal(min_rent_exempt_balance, after_pda_balance); + }); + + it("[claim_fees]: should fail if not called by admin", async () => { + let new_admin = Keypair.generate(); + + try { + await ctx.program.methods + .claimFees() + .accountsStrict({ + admin: new_admin.publicKey, + config: ConnectionPDA.config().pda, + }) + .signers([new_admin]) + .rpc(); + } catch (err) { + expect(err.message).includes("OnlyAdmin"); + } + }); + + it("[recv_message]: should fail if not called by an admin", async () => { + const connSn = 1; + const fromNetwork = ctx.dstNetworkId; + let csMessage = new Uint8Array([1, 2, 3]); + + try { + await ctx.program.methods + .recvMessage( + fromNetwork, + new anchor.BN(connSn), + Buffer.from(csMessage), + new anchor.BN(connSn) + ) + .accountsStrict({ + config: ConnectionPDA.config().pda, + admin: ctx.signer.publicKey, + receipt: ConnectionPDA.receipt(fromNetwork, connSn).pda, + authority: ConnectionPDA.authority().pda, + systemProgram: SYSTEM_PROGRAM_ID, + }) + .signers([ctx.signer]) + .rpc(); + } catch (err) { + expect(err.message).includes("Only admin"); + } + }); + + it("[recv_message]: should receive message and call handle message request of xcall", async () => { + let xcallConfig = await xcallCtx.getConfig(); + + const connSn = 1; + const fromNetwork = "icon"; + let nextReqId = xcallConfig.lastReqId.toNumber() + 1; + let nextSequenceNo = xcallConfig.sequenceNo.toNumber() + 1; + + let data = Buffer.from("rollback", "utf-8"); + let request = new CSMessageRequest( + "icon/abc", + mockDappProgram.programId.toString(), + nextSequenceNo, + MessageType.CallMessageWithRollback, + data, + [connectionProgram.programId.toString()] + ); + + let cs_message = new CSMessage( + CSMessageType.CSMessageRequest, + request.encode() + ).encode(); + + let recvMessageAccounts = await ctx.getRecvMessageAccounts( + connSn, + nextSequenceNo, + cs_message, + CSMessageType.CSMessageRequest + ); + + await ctx.program.methods + .recvMessage( + fromNetwork, + new anchor.BN(connSn), + Buffer.from(cs_message), + new anchor.BN(nextSequenceNo) + ) + .accountsStrict({ + config: ConnectionPDA.config().pda, + admin: ctx.admin.publicKey, + receipt: ConnectionPDA.receipt(fromNetwork, connSn).pda, + authority: ConnectionPDA.authority().pda, + systemProgram: SYSTEM_PROGRAM_ID, + }) + .remainingAccounts([...recvMessageAccounts.slice(4)]) + .signers([ctx.admin]) + .rpc(); + + await sleep(2); + + // expect receipt account to be initialized + expect(await ctx.getReceipt(fromNetwork, nextSequenceNo)).to.be.empty; + + // expect proxy request in xcall PDA's account + let proxyRequest = await xcallCtx.getProxyRequest(nextReqId); + expect(proxyRequest.req.protocols).to.includes( + connectionProgram.programId.toString() + ); + expect(proxyRequest.req.from[0]).to.equal(request.from); + expect(proxyRequest.req.data.toString()).to.equal( + Buffer.from(hash(data), "hex").toString() + ); + + // expect request to be increased in xcall config PDA's + expect((await xcallCtx.getConfig()).lastReqId.toString()).to.equal( + nextReqId.toString() + ); + + // call xcall execute_call + let executeCallAccounts = await xcallCtx.getExecuteCallAccounts( + nextReqId, + data + ); + + await xcallProgram.methods + .executeCall(new anchor.BN(nextReqId), Buffer.from(data)) + .accounts({ + signer: ctx.admin.publicKey, + systemProgram: SYSTEM_PROGRAM_ID, + config: XcallPDA.config().pda, + admin: xcallConfig.admin, + proxyRequest: XcallPDA.proxyRequest(nextReqId).pda, + }) + .remainingAccounts([...executeCallAccounts.slice(4)]) + .signers([ctx.admin]) + .rpc(); + }); + + it("[recv_message]: should receive message and call xcall handle message result", async () => { + // send rollback message using mock dapp + await mockDappCtx.add_connection( + "icon", + connectionProgram.programId.toString(), + connectionProgram.programId.toString() + ); + await sleep(2); + + const to = { "0": "icon/abc" }; + let data = Buffer.from("rollback", "utf-8"); + let msgType = MessageType.CallMessageWithRollback; + + let xcallConfig = await xcallCtx.getConfig(); + let nextSequenceNo = xcallConfig.sequenceNo.toNumber() + 1; + + let sendCallIx = await mockDappProgram.methods + .sendCallMessage(to, data, msgType, data) + .accountsStrict({ + config: DappPDA.config().pda, + systemProgram: SYSTEM_PROGRAM_ID, + connectionsAccount: DappPDA.connections("icon").pda, + sender: ctx.admin.publicKey, + authority: DappPDA.authority().pda, + }) + .remainingAccounts([ + { + pubkey: SYSVAR_INSTRUCTIONS_ID, + isSigner: false, + isWritable: false, + }, + { + pubkey: XcallPDA.config().pda, + isSigner: false, + isWritable: true, + }, + { + pubkey: xcallConfig.feeHandler, + isSigner: false, + isWritable: true, + }, + { + pubkey: XcallPDA.rollback(nextSequenceNo).pda, + isSigner: false, + isWritable: true, + }, + { + pubkey: connectionProgram.programId, + isSigner: false, + isWritable: true, + }, + { + pubkey: ConnectionPDA.config().pda, + isSigner: false, + isWritable: true, + }, + { + pubkey: ConnectionPDA.network_fee("icon").pda, + isSigner: false, + isWritable: true, + }, + { + pubkey: xcallProgram.programId, + isSigner: false, + isWritable: true, + }, + { + pubkey: connectionProgram.programId, + isSigner: false, + isWritable: true, + }, + ]) + .instruction(); + + let sendCallTx = await txnHelpers.buildV0Txn([sendCallIx], [ctx.admin]); + await connection.sendTransaction(sendCallTx); + await sleep(2); + + // receive message of rollback message + let connSn = 2; + let responseCode = CSResponseType.CSResponseSuccess; + + let request = new CSMessageRequest( + "icon/abc", + "icon", + nextSequenceNo, + MessageType.CallMessagePersisted, + new Uint8Array([0, 1, 2, 3]), + [connectionProgram.programId.toString()] + ); + + let result = new CSMessageResult( + nextSequenceNo, + responseCode, + request.encode() + ); + let csMessage = new CSMessage( + CSMessageType.CSMessageResult, + result.encode() + ).encode(); + + let recvMessageAccounts = await ctx.getRecvMessageAccounts( + connSn, + nextSequenceNo, + csMessage, + CSMessageType.CSMessageResult + ); + + await ctx.program.methods + .recvMessage( + ctx.dstNetworkId, + new anchor.BN(connSn), + Buffer.from(csMessage), + new anchor.BN(nextSequenceNo) + ) + .accountsStrict({ + config: ConnectionPDA.config().pda, + admin: ctx.admin.publicKey, + receipt: ConnectionPDA.receipt(ctx.dstNetworkId, connSn).pda, + authority: ConnectionPDA.authority().pda, + systemProgram: SYSTEM_PROGRAM_ID, + }) + .remainingAccounts([...recvMessageAccounts.slice(4)]) + .signers([ctx.admin]) + .rpc(); + await sleep(2); + + assert.equal((await xcallCtx.getSuccessRes(nextSequenceNo)).success, true); + + try { + await xcallCtx.getRollback(nextSequenceNo); + } catch (err) { + expect(err.message).to.includes("Account does not exist"); + } + }); + + it("[recv_message]: should receive message and execute rollback", async () => { + let data = Buffer.from("rollback_data", "utf-8"); + + let xcallConfig = await xcallCtx.getConfig(); + let nextSequenceNo = xcallConfig.sequenceNo.toNumber() + 1; + + const to = { "0": "0x3.icon/" + mockDappProgram.programId.toString() }; + const msg_type = MessageType.CallMessageWithRollback; + + let sendCallIx = await mockDappProgram.methods + .sendCallMessage(to, data, msg_type, data) + .accountsStrict({ + config: DappPDA.config().pda, + systemProgram: SYSTEM_PROGRAM_ID, + connectionsAccount: DappPDA.connections(ctx.dstNetworkId).pda, + sender: ctx.admin.publicKey, + authority: DappPDA.authority().pda, + }) + .remainingAccounts([ + { + pubkey: SYSVAR_INSTRUCTIONS_ID, + isSigner: false, + isWritable: false, + }, + { + pubkey: XcallPDA.config().pda, + isSigner: false, + isWritable: true, + }, + { + pubkey: xcallConfig.feeHandler, + isSigner: false, + isWritable: true, + }, + { + pubkey: XcallPDA.rollback(nextSequenceNo).pda, + isSigner: false, + isWritable: true, + }, + { + pubkey: connectionProgram.programId, + isSigner: false, + isWritable: true, + }, + { + pubkey: ConnectionPDA.config().pda, + isSigner: false, + isWritable: true, + }, + { + pubkey: ConnectionPDA.network_fee(ctx.dstNetworkId).pda, + isSigner: false, + isWritable: true, + }, + { + pubkey: xcallProgram.programId, + isSigner: false, + isWritable: true, + }, + { + pubkey: connectionProgram.programId, + isSigner: false, + isWritable: true, + }, + ]) + .instruction(); + + let sendCallTx = await txnHelpers.buildV0Txn([sendCallIx], [ctx.admin]); + await connection.sendTransaction(sendCallTx); + await sleep(2); + + // receive message of rollback message + let connSn = 3; + + let result = new CSMessageResult( + nextSequenceNo, + CSResponseType.CSMessageFailure, + new Uint8Array([]) + ); + let csMessage = new CSMessage( + CSMessageType.CSMessageResult, + result.encode() + ).encode(); + + let recvMessageAccounts = await ctx.getRecvMessageAccounts( + connSn, + nextSequenceNo, + csMessage, + CSMessageType.CSMessageResult + ); + + let recvMessageIx = await ctx.program.methods + .recvMessage( + ctx.dstNetworkId, + new anchor.BN(connSn), + Buffer.from(csMessage), + new anchor.BN(nextSequenceNo) + ) + .accountsStrict({ + config: ConnectionPDA.config().pda, + admin: ctx.admin.publicKey, + receipt: ConnectionPDA.receipt(ctx.dstNetworkId, connSn).pda, + authority: ConnectionPDA.authority().pda, + systemProgram: SYSTEM_PROGRAM_ID, + }) + .remainingAccounts([...recvMessageAccounts.slice(4)]) + .instruction(); + + let recvMessageTx = await txnHelpers.buildV0Txn( + [recvMessageIx], + [ctx.admin] + ); + await connection.sendTransaction(recvMessageTx); + await sleep(2); + + // execute rollback message + let rollbackAccount = await xcallCtx.getRollback(nextSequenceNo); + assert.equal(rollbackAccount.rollback.enabled, true); + + let executeRollbackAccounts = await xcallCtx.getExecuteRollbackAccounts( + nextSequenceNo + ); + + let executeRollbackIx = await xcallProgram.methods + .executeRollback(new anchor.BN(nextSequenceNo)) + .accountsStrict({ + signer: ctx.admin.publicKey, + systemProgram: SYSTEM_PROGRAM_ID, + config: XcallPDA.config().pda, + admin: xcallConfig.admin, + rollbackAccount: XcallPDA.rollback(nextSequenceNo).pda, + }) + .remainingAccounts([...executeRollbackAccounts.slice(4)]) + .instruction(); + + let executeRollbackTx = await txnHelpers.buildV0Txn( + [executeRollbackIx], + [ctx.admin] + ); + await connection.sendTransaction(executeRollbackTx); + }); + + it("[revert_message]: should fail if not called by an admin", async () => { + let fromNetwork = ctx.dstNetworkId; + let sequenceNo = 1; + + try { + await connectionProgram.methods + .revertMessage(new anchor.BN(sequenceNo)) + .accountsStrict({ + config: ConnectionPDA.config().pda, + admin: ctx.signer.publicKey, + authority: ConnectionPDA.authority().pda, + systemProgram: SYSTEM_PROGRAM_ID, + }) + .remainingAccounts([]) + .signers([ctx.signer]) + .rpc(); + } catch (err) { + expect(err.message).includes("Only admin"); + } + }); + + it("[revert_message]: should revert message and call xcall handle error", async () => { + let xcallConfig = await xcallCtx.getConfig(); + let nextSequenceNo = xcallConfig.sequenceNo.toNumber() + 1; + + let data = Buffer.from("rollback", "utf-8"); + const to = { "0": "0x3.icon/" + mockDappProgram.programId.toString() }; + + const msg_type = MessageType.CallMessageWithRollback; + + // send rollback message using mock dapp + let sendCallIx = await mockDappProgram.methods + .sendCallMessage(to, data, msg_type, data) + .accountsStrict({ + config: DappPDA.config().pda, + systemProgram: SYSTEM_PROGRAM_ID, + connectionsAccount: DappPDA.connections(ctx.dstNetworkId).pda, + sender: ctx.admin.publicKey, + authority: DappPDA.authority().pda, + }) + .remainingAccounts([ + { + pubkey: SYSVAR_INSTRUCTIONS_ID, + isSigner: false, + isWritable: false, + }, + { + pubkey: XcallPDA.config().pda, + isSigner: false, + isWritable: true, + }, + { + pubkey: xcallConfig.feeHandler, + isSigner: false, + isWritable: true, + }, + { + pubkey: XcallPDA.rollback(nextSequenceNo).pda, + isSigner: false, + isWritable: true, + }, + { + pubkey: connectionProgram.programId, + isSigner: false, + isWritable: true, + }, + { + pubkey: ConnectionPDA.config().pda, + isSigner: false, + isWritable: true, + }, + { + pubkey: ConnectionPDA.network_fee(ctx.dstNetworkId).pda, + isSigner: false, + isWritable: true, + }, + { + pubkey: xcallProgram.programId, + isSigner: false, + isWritable: true, + }, + ]) + .instruction(); + + let sendCallTx = await txnHelpers.buildV0Txn([sendCallIx], [ctx.admin]); + await connection.sendTransaction(sendCallTx); + await sleep(2); + + expect(await xcallCtx.getRollback(nextSequenceNo)).to.not.be.empty; + + let revertMessageAccounts = await ctx.getRevertMessageAccounts( + nextSequenceNo + ); + + let revertMessageIx = await connectionProgram.methods + .revertMessage(new anchor.BN(nextSequenceNo)) + .accountsStrict({ + config: ConnectionPDA.config().pda, + admin: ctx.admin.publicKey, + authority: ConnectionPDA.authority().pda, + systemProgram: SYSTEM_PROGRAM_ID, + }) + .remainingAccounts([...revertMessageAccounts.slice(3)]) + .instruction(); + + let revertMessageTx = await txnHelpers.buildV0Txn( + [revertMessageIx], + [ctx.admin] + ); + await connection.sendTransaction(revertMessageTx); + await sleep(2); + + let rollback = await xcallCtx.getRollback(nextSequenceNo); + assert.equal(rollback.rollback.enabled, true); + }); + + it("[revert_message]: should receive message message and handle forced rollback message of xcall", async () => { + let xcallConfig = await xcallCtx.getConfig(); + + const connSn = 5; + const fromNetwork = "icon"; + let nextReqId = xcallConfig.lastReqId.toNumber() + 1; + let nextSequenceNo = xcallConfig.sequenceNo.toNumber() + 1; + + let data = Buffer.from("rollback", "utf-8"); + let request = new CSMessageRequest( + "icon/abc", + mockDappProgram.programId.toString(), + nextSequenceNo, + MessageType.CallMessageWithRollback, + data, + [connectionProgram.programId.toString()] + ); + + let cs_message = new CSMessage( + CSMessageType.CSMessageRequest, + request.encode() + ).encode(); + + let recvMessageAccounts = await ctx.getRecvMessageAccounts( + connSn, + nextSequenceNo, + cs_message, + CSMessageType.CSMessageRequest + ); + + await ctx.program.methods + .recvMessage( + fromNetwork, + new anchor.BN(connSn), + Buffer.from(cs_message), + new anchor.BN(nextSequenceNo) + ) + .accountsStrict({ + config: ConnectionPDA.config().pda, + admin: ctx.admin.publicKey, + receipt: ConnectionPDA.receipt(fromNetwork, connSn).pda, + authority: ConnectionPDA.authority().pda, + systemProgram: SYSTEM_PROGRAM_ID, + }) + .remainingAccounts([...recvMessageAccounts.slice(4)]) + .signers([ctx.admin]) + .rpc(); + + await sleep(2); + + let executeForcedRollbackIx = await mockDappProgram.methods + .executeForcedRollback(new anchor.BN(nextReqId)) + .accountsStrict({ + config: DappPDA.config().pda, + systemProgram: SYSTEM_PROGRAM_ID, + sender: ctx.admin.publicKey, + authority: DappPDA.authority().pda, + }) + .remainingAccounts([ + { + pubkey: XcallPDA.config().pda, + isSigner: false, + isWritable: true, + }, + { + pubkey: xcallConfig.admin, + isSigner: false, + isWritable: true, + }, + { + pubkey: XcallPDA.proxyRequest(nextReqId).pda, + isSigner: false, + isWritable: true, + }, + { + pubkey: connectionProgram.programId, + isSigner: false, + isWritable: true, + }, + { + pubkey: ConnectionPDA.config().pda, + isSigner: false, + isWritable: true, + }, + { + pubkey: ConnectionPDA.network_fee(fromNetwork).pda, + isSigner: false, + isWritable: true, + }, + { + pubkey: xcallProgram.programId, + isSigner: false, + isWritable: true, + }, + { + pubkey: connectionProgram.programId, + isSigner: false, + isWritable: true, + }, + ]) + .instruction(); + + let executeForcedRollbackTx = await txnHelpers.buildV0Txn( + [executeForcedRollbackIx], + [ctx.admin] + ); + await connection.sendTransaction(executeForcedRollbackTx); + await sleep(2); + }); +}); diff --git a/contracts/solana/tests/centralized-connection/setup.ts b/contracts/solana/tests/centralized-connection/setup.ts new file mode 100644 index 00000000..de6eacb2 --- /dev/null +++ b/contracts/solana/tests/centralized-connection/setup.ts @@ -0,0 +1,213 @@ +import * as anchor from "@coral-xyz/anchor"; +import { PublicKey, Connection, Keypair } from "@solana/web3.js"; + +import { CentralizedConnection } from "../../target/types/centralized_connection"; +import { SYSTEM_PROGRAM_ID } from "@coral-xyz/anchor/dist/cjs/native/system"; +import { TxnHelpers, uint128ToArray } from "../utils"; +import { CSMessageType } from "../xcall/types"; + +import { Xcall } from "../../target/types/xcall"; +import { XcallPDA } from "../xcall/setup"; + +const xcallProgram: anchor.Program = anchor.workspace.Xcall; + +const connectionProgram: anchor.Program = + anchor.workspace.CentralizedConnection; + +export class TestContext { + program: anchor.Program; + signer: Keypair; + admin: Keypair; + connection: Connection; + networkId: string; + dstNetworkId: string; + txnHelpers: TxnHelpers; + isInitialized: boolean; + + constructor(connection: Connection, txnHelpers: TxnHelpers, admin: Keypair) { + let provider = anchor.AnchorProvider.env(); + anchor.setProvider(provider); + + this.program = anchor.workspace.CentralizedConnection; + this.signer = admin; + this.admin = admin; + this.connection = connection; + this.txnHelpers = txnHelpers; + this.networkId = "solana"; + this.dstNetworkId = "0x3.icon"; + } + + async initialize() { + await this.program.methods + .initialize(xcallProgram.programId, this.signer.publicKey) + .signers([this.signer]) + .accountsStrict({ + signer: this.signer.publicKey, + systemProgram: SYSTEM_PROGRAM_ID, + config: ConnectionPDA.config().pda, + authority: ConnectionPDA.authority().pda, + }) + .rpc(); + } + + async setAdmin(keypair: Keypair) { + await this.program.methods + .setAdmin(keypair.publicKey) + .accountsStrict({ + admin: this.admin.publicKey, + config: ConnectionPDA.config().pda, + }) + .signers([this.admin]) + .rpc(); + + this.admin = keypair; + } + + async setNetworkFee(networkId: string, msgFee: number, resFee) { + await connectionProgram.methods + .setFee(networkId, new anchor.BN(msgFee), new anchor.BN(resFee)) + .accountsStrict({ + config: ConnectionPDA.config().pda, + networkFee: ConnectionPDA.network_fee(networkId).pda, + admin: this.admin.publicKey, + systemProgram: SYSTEM_PROGRAM_ID, + }) + .signers([this.admin]) + .rpc(); + } + + async getRecvMessageAccounts( + connSn: number, + sequenceNo: number, + csMessage: Uint8Array, + csMessageType: CSMessageType + ) { + const remainingAccounts = [ + { + pubkey: XcallPDA.config().pda, + isWritable: true, + isSigner: false, + }, + ]; + + if (csMessageType == CSMessageType.CSMessageResult) { + remainingAccounts.push({ + pubkey: XcallPDA.rollback(sequenceNo).pda, + isWritable: false, + isSigner: false, + }); + } + + remainingAccounts.push({ + pubkey: xcallProgram.programId, + isWritable: false, + isSigner: false, + }); + + let res = await connectionProgram.methods + .queryRecvMessageAccounts( + this.dstNetworkId, + new anchor.BN(connSn), + Buffer.from(csMessage), + new anchor.BN(sequenceNo), + 1, + 30 + ) + .accountsStrict({ + config: ConnectionPDA.config().pda, + }) + .remainingAccounts(remainingAccounts) + .view({ commitment: "confirmed" }); + + return res.accounts; + } + + async getRevertMessageAccounts(sequenceNo: number) { + let res = await connectionProgram.methods + .queryRevertMessageAccounts(new anchor.BN(sequenceNo), 1, 30) + .accountsStrict({ + config: ConnectionPDA.config().pda, + }) + .remainingAccounts([ + { + pubkey: XcallPDA.config().pda, + isWritable: false, + isSigner: false, + }, + { + pubkey: XcallPDA.rollback(sequenceNo).pda, + isWritable: true, + isSigner: false, + }, + { + pubkey: xcallProgram.programId, + isWritable: false, + isSigner: false, + }, + ]) + .view({ commitment: "confirmed" }); + + return res.accounts; + } + + async getConfig() { + return await this.program.account.config.fetch( + ConnectionPDA.config().pda, + "confirmed" + ); + } + + async getFee(nid: string) { + return await this.program.account.networkFee.fetch( + ConnectionPDA.network_fee(nid).pda, + "confirmed" + ); + } + + async getReceipt(networkId: string, sequenceNo: number) { + return await this.program.account.receipt.fetch( + ConnectionPDA.receipt(networkId, sequenceNo).pda, + "confirmed" + ); + } +} + +export class ConnectionPDA { + constructor() {} + + static config() { + let [pda, bump] = PublicKey.findProgramAddressSync( + [Buffer.from("config")], + connectionProgram.programId + ); + + return { bump, pda }; + } + + static network_fee(networkId: string) { + const [pda, bump] = PublicKey.findProgramAddressSync( + [Buffer.from("fee"), Buffer.from(networkId)], + connectionProgram.programId + ); + + return { pda, bump }; + } + + static receipt(networkId: string, sn: number) { + const [pda, bump] = PublicKey.findProgramAddressSync( + [Buffer.from("receipt"), Buffer.from(networkId), uint128ToArray(sn)], + connectionProgram.programId + ); + + return { pda, bump }; + } + + static authority() { + let [pda, bump] = PublicKey.findProgramAddressSync( + [Buffer.from("connection_authority")], + connectionProgram.programId + ); + + return { bump, pda }; + } +} diff --git a/contracts/solana/tests/mock-dapp-multi/mock-dapp-multi.ts b/contracts/solana/tests/mock-dapp-multi/mock-dapp-multi.ts new file mode 100644 index 00000000..8a822329 --- /dev/null +++ b/contracts/solana/tests/mock-dapp-multi/mock-dapp-multi.ts @@ -0,0 +1,109 @@ +import * as anchor from "@coral-xyz/anchor"; + +import { TestContext as DappTestCtx, DappPDA } from "./setup"; +import { SYSVAR_INSTRUCTIONS_ID, TxnHelpers } from "../utils"; +import { SYSTEM_PROGRAM_ID } from "@coral-xyz/anchor/dist/cjs/native/system"; +import { TestContext as XcallTestCtx, XcallPDA } from "../xcall/setup"; + +import { Xcall } from "../../target/types/xcall"; +import { Envelope, CallMessage, MessageType } from "../xcall/types"; + +import { CentralizedConnection } from "../../target/types/centralized_connection"; + +import { ConnectionPDA } from "../centralized-connection/setup"; +import { MockDappMulti } from "../../target/types/mock_dapp_multi"; + +const connectionProgram: anchor.Program = + anchor.workspace.CentralizedConnection; + +const xcallProgram: anchor.Program = anchor.workspace.Xcall; +const dappProgram: anchor.Program = + anchor.workspace.MockDappMulti; + +describe("Mock Dapp", () => { + const provider = anchor.AnchorProvider.env(); + const connection = provider.connection; + const wallet = provider.wallet as anchor.Wallet; + + let txnHelpers = new TxnHelpers(connection, wallet.payer); + let ctx = new DappTestCtx(connection, txnHelpers, wallet.payer); + + it("should send message", async () => { + let xcall_context = new XcallTestCtx(connection, txnHelpers, wallet.payer); + + let envelope = new Envelope( + MessageType.CallMessage, + new CallMessage(new Uint8Array([])).encode(), + [connectionProgram.programId.toString()], + [wallet.publicKey.toString()] + ).encode(); + + const to = { "0": "0x3.icon/abc" }; + const msg_type = 0; + const rollback = Buffer.from("rollback"); + const message = Buffer.from(envelope); + + let remaining_accounts = [ + { + pubkey: SYSVAR_INSTRUCTIONS_ID, + isSigner: false, + isWritable: false, + }, + { + pubkey: XcallPDA.config().pda, + isSigner: false, + isWritable: true, + }, + { + pubkey: (await xcall_context.getConfig()).feeHandler, + isSigner: false, + isWritable: true, + }, + { + pubkey: xcallProgram.programId, + isSigner: false, + isWritable: false, + }, + { + pubkey: connectionProgram.programId, + isSigner: false, + isWritable: true, + }, + { + pubkey: ConnectionPDA.config().pda, + isSigner: false, + isWritable: true, + }, + { + pubkey: ConnectionPDA.network_fee(ctx.networkId).pda, + isSigner: false, + isWritable: true, + }, + { + pubkey: xcallProgram.programId, + isSigner: false, + isWritable: true, + }, + { + pubkey: connectionProgram.programId, + isSigner: false, + isWritable: true, + }, + ]; + + let sendCallIx = await dappProgram.methods + .sendCallMessage(to, message, msg_type, rollback) + .accountsStrict({ + config: DappPDA.config().pda, + systemProgram: SYSTEM_PROGRAM_ID, + connectionsAccount: DappPDA.connections(ctx.networkId).pda, + sender: wallet.payer.publicKey, + authority: DappPDA.authority().pda, + }) + .remainingAccounts(remaining_accounts) + .instruction(); + + let sendCallTx = await txnHelpers.buildV0Txn([sendCallIx], [wallet.payer]); + await connection.sendTransaction(sendCallTx); + }); +}); diff --git a/contracts/solana/tests/mock-dapp-multi/send-rollback-message.ts b/contracts/solana/tests/mock-dapp-multi/send-rollback-message.ts new file mode 100644 index 00000000..7768da70 --- /dev/null +++ b/contracts/solana/tests/mock-dapp-multi/send-rollback-message.ts @@ -0,0 +1,118 @@ +import * as anchor from "@coral-xyz/anchor"; + +import { TestContext, DappPDA } from "./setup"; +import { SYSVAR_INSTRUCTIONS_ID, TxnHelpers, sleep } from "../utils"; +import { SYSTEM_PROGRAM_ID } from "@coral-xyz/anchor/dist/cjs/native/system"; +import { CentralizedConnection } from "../../target/types/centralized_connection"; + +import { ConnectionPDA } from "../centralized-connection/setup"; + +import { Xcall } from "../../target/types/xcall"; +import { XcallPDA } from "../xcall/setup"; +import { CallMessageWithRollback, Envelope, MessageType } from "../xcall/types"; +import { TestContext as XcallTestContext } from "../xcall/setup"; + +import { MockDappMulti } from "../../target/types/mock_dapp_multi"; + +const xcallProgram: anchor.Program = anchor.workspace.Xcall; +const connectionProgram: anchor.Program = + anchor.workspace.CentralizedConnection; +const mockDappProgram: anchor.Program = + anchor.workspace.MockDappMulti; + +describe("CentralizedConnection", () => { + const provider = anchor.AnchorProvider.env(); + const connection = provider.connection; + const wallet = provider.wallet as anchor.Wallet; + + let txnHelpers = new TxnHelpers(connection, wallet.payer); + let ctx = new TestContext(connection, txnHelpers, wallet.payer); + + let xcallCtx = new XcallTestContext(connection, txnHelpers, wallet.payer); + + it("should send rollback message", async () => { + let data = Buffer.from("rollback", "utf-8"); + let rollback_data = Buffer.from("rollback", "utf-8"); + + let xcallConfig = await xcallCtx.getConfig(); + let nextSequenceNo = xcallConfig.sequenceNo.toNumber() + 1; + + let envelope = new Envelope( + MessageType.CallMessageWithRollback, + new CallMessageWithRollback(data, rollback_data).encode(), + [connectionProgram.programId.toString()], + [connectionProgram.programId.toString()] + ).encode(); + + const to = { + "0": `${ctx.dstNetworkId}/${mockDappProgram.programId.toString()}`, + }; + + let sendCallIx = await mockDappProgram.methods + .sendCallMessage( + to, + Buffer.from(envelope), + MessageType.CallMessageWithRollback, + Buffer.from("rollback") + ) + .accountsStrict({ + config: DappPDA.config().pda, + systemProgram: SYSTEM_PROGRAM_ID, + connectionsAccount: DappPDA.connections(ctx.dstNetworkId).pda, + sender: ctx.admin.publicKey, + authority: DappPDA.authority().pda, + }) + .remainingAccounts([ + { + pubkey: SYSVAR_INSTRUCTIONS_ID, + isSigner: false, + isWritable: false, + }, + { + pubkey: XcallPDA.config().pda, + isSigner: false, + isWritable: true, + }, + { + pubkey: xcallConfig.feeHandler, + isSigner: false, + isWritable: true, + }, + { + pubkey: XcallPDA.rollback(nextSequenceNo).pda, + isSigner: false, + isWritable: true, + }, + { + pubkey: connectionProgram.programId, + isSigner: false, + isWritable: true, + }, + { + pubkey: ConnectionPDA.config().pda, + isSigner: false, + isWritable: true, + }, + { + pubkey: ConnectionPDA.network_fee(ctx.dstNetworkId).pda, + isSigner: false, + isWritable: true, + }, + { + pubkey: xcallProgram.programId, + isSigner: false, + isWritable: true, + }, + { + pubkey: connectionProgram.programId, + isSigner: false, + isWritable: true, + }, + ]) + .instruction(); + + let sendCallTx = await txnHelpers.buildV0Txn([sendCallIx], [ctx.admin]); + await connection.sendTransaction(sendCallTx); + await sleep(2); + }); +}); diff --git a/contracts/solana/tests/mock-dapp-multi/setup.ts b/contracts/solana/tests/mock-dapp-multi/setup.ts new file mode 100644 index 00000000..8d7ded57 --- /dev/null +++ b/contracts/solana/tests/mock-dapp-multi/setup.ts @@ -0,0 +1,113 @@ +import * as anchor from "@coral-xyz/anchor"; +import { PublicKey, Connection, Keypair } from "@solana/web3.js"; + +import { MockDappMulti } from "../../target/types/mock_dapp_multi"; +import { SYSTEM_PROGRAM_ID } from "@coral-xyz/anchor/dist/cjs/native/system"; +import { TxnHelpers } from "../utils"; + +import { Xcall } from "../../target/types/xcall"; + +const xcallProgram: anchor.Program = anchor.workspace.Xcall; + +const dappProgram: anchor.Program = + anchor.workspace.MockDappMulti; + +export class TestContext { + program: anchor.Program; + signer: Keypair; + admin: Keypair; + connection: Connection; + networkId: string; + dstNetworkId: string; + txnHelpers: TxnHelpers; + isInitialized: boolean; + + constructor(connection: Connection, txnHelpers: TxnHelpers, admin: Keypair) { + let provider = anchor.AnchorProvider.env(); + anchor.setProvider(provider); + + this.program = anchor.workspace.MockDappMulti; + this.signer = admin; + this.admin = admin; + this.connection = connection; + this.txnHelpers = txnHelpers; + this.networkId = "0x3.icon"; + this.dstNetworkId = "0x3.icon"; + this.isInitialized = false; + } + + async initialize() { + await this.program.methods + .initialize(xcallProgram.programId) + .signers([this.signer]) + .accountsStrict({ + sender: this.signer.publicKey, + authority: DappPDA.authority().pda, + systemProgram: SYSTEM_PROGRAM_ID, + config: DappPDA.config().pda, + }) + .rpc(); + + this.isInitialized = true; + } + + async add_connection( + _networkId: string, + src_endpoint: string, + dst_endpoint: string + ) { + const result = await this.program.methods + .addConnection(_networkId, src_endpoint, dst_endpoint) + .accounts({ + connectionAccount: DappPDA.connections(_networkId).pda, + sender: this.signer.publicKey, + systemProgram: SYSTEM_PROGRAM_ID, + }) + .signers([this.admin]) + .rpc(); + + return result; + } + + async getConfig() { + return await this.program.account.config.fetch( + DappPDA.config().pda, + "confirmed" + ); + } +} + +export class DappPDA { + constructor() {} + + static config() { + let [pda, bump] = PublicKey.findProgramAddressSync( + [Buffer.from("config")], + dappProgram.programId + ); + + return { bump, pda }; + } + + static connections(networkId: string) { + const buffer1 = Buffer.from("connections"); + const buffer2 = Buffer.from(networkId); + const seed = [buffer1, buffer2]; + + const [pda, bump] = PublicKey.findProgramAddressSync( + seed, + dappProgram.programId + ); + + return { pda, bump }; + } + + static authority() { + let [pda, bump] = PublicKey.findProgramAddressSync( + [Buffer.from("dapp_authority")], + dappProgram.programId + ); + + return { bump, pda }; + } +} diff --git a/contracts/solana/tests/utils/index.ts b/contracts/solana/tests/utils/index.ts new file mode 100644 index 00000000..5741cfc4 --- /dev/null +++ b/contracts/solana/tests/utils/index.ts @@ -0,0 +1,39 @@ +import fs from "fs"; +import { createHash } from "crypto"; +import { Keypair, Connection, PublicKey } from "@solana/web3.js"; + +export const loadKeypariFromFile = (path: string) => { + return Keypair.fromSecretKey( + Buffer.from(JSON.parse(fs.readFileSync(path, "utf-8"))) + ); +}; + +export const sleep = (seconds: number) => { + return new Promise((resolve) => setTimeout(resolve, seconds * 1000)); +}; + +export const hash = (message: Uint8Array) => { + return createHash("sha256").update(message).digest("hex"); +}; + +export const uint128ToArray = (num: any) => { + if (typeof num === "string" || typeof num === "number") { + num = BigInt(num); + } else if (!(num instanceof BigInt)) { + throw new Error("Input must be a BigInt or convertible to a BigInt."); + } + + let buffer = new ArrayBuffer(16); + let view = new DataView(buffer); + + view.setBigUint64(0, num >> BigInt(64), false); + view.setBigUint64(8, num & BigInt("0xFFFFFFFFFFFFFFFF"), false); + + return new Uint8Array(buffer); +}; + +export const SYSVAR_INSTRUCTIONS_ID = new PublicKey( + "Sysvar1nstructions1111111111111111111111111" +); + +export * from "./transaction"; diff --git a/contracts/solana/tests/utils/transaction.ts b/contracts/solana/tests/utils/transaction.ts new file mode 100644 index 00000000..6a2d05ca --- /dev/null +++ b/contracts/solana/tests/utils/transaction.ts @@ -0,0 +1,128 @@ +import { + Connection, + Keypair, + AddressLookupTableProgram, + TransactionMessage, + TransactionInstruction, + PublicKey, + VersionedTransaction, +} from "@solana/web3.js"; + +import { sleep } from "."; + +export class TxnHelpers { + connection: Connection; + payer: Keypair; + lookupTable: PublicKey; + + constructor(connection: Connection, payer: Keypair) { + this.connection = connection; + this.payer = payer; + } + + async createAddressLookupTable() { + let recentSlot = await this.connection.getSlot("max"); + + let [createLookupTableIx, lookupTable] = + AddressLookupTableProgram.createLookupTable({ + authority: this.payer.publicKey, + payer: this.payer.publicKey, + recentSlot, + }); + + const tx = await this.buildV0Txn([createLookupTableIx], [this.payer]); + + await this.connection.sendTransaction(tx); + return (this.lookupTable = lookupTable); + } + + async extendAddressLookupTable(addresses: PublicKey[]) { + await sleep(2); + + let extendLookupTableIx = AddressLookupTableProgram.extendLookupTable({ + addresses, + authority: this.payer.publicKey, + lookupTable: this.lookupTable, + payer: this.payer.publicKey, + }); + + const tx = await this.buildV0Txn([extendLookupTableIx], [this.payer]); + await this.connection.sendTransaction(tx); + } + + async getAddressLookupTable() { + return await this.connection + .getAddressLookupTable(this.lookupTable) + .then((res) => res.value); + } + + async printAddressLookupTable() { + await sleep(2); + + const lookupTableAccount = await this.getAddressLookupTable(); + console.log(`Lookup Table: ${this.lookupTable}`); + + for (let i = 0; i < lookupTableAccount.state.addresses.length; i++) { + const address = lookupTableAccount.state.addresses[i]; + console.log( + `Index: ${i.toString().padEnd(2)} Address: ${address.toBase58()}` + ); + } + } + + async buildV0Txn(instructions: TransactionInstruction[], signers: Keypair[]) { + let blockHash = await this.connection + .getLatestBlockhash() + .then((res) => res.blockhash); + + const messageV0 = new TransactionMessage({ + payerKey: signers[0].publicKey, + recentBlockhash: blockHash, + instructions, + }).compileToV0Message(); + + const tx = new VersionedTransaction(messageV0); + signers.forEach((s) => tx.sign([s])); + return tx; + } + + async buildTxnWithLookupTable( + instructions: TransactionInstruction[], + signers: Keypair[] + ) { + await sleep(2); + + const lookupTableAccount = await this.connection + .getAddressLookupTable(this.lookupTable) + .then((res) => res.value); + + let blockhash = await this.connection + .getLatestBlockhash() + .then((res) => res.blockhash); + + let messageV0 = new TransactionMessage({ + payerKey: signers[0].publicKey, + recentBlockhash: blockhash, + instructions, + }).compileToV0Message([lookupTableAccount]); + + const tx = new VersionedTransaction(messageV0); + signers.forEach((s) => tx.sign([s])); + return tx; + } + + async airdrop(to: PublicKey, lamports: number) { + let aridropTx = await this.connection.requestAirdrop(to, lamports); + await this.connection.confirmTransaction(aridropTx, "confirmed"); + } + + async logParsedTx(txSignature: string) { + await sleep(2); + console.log( + await this.connection.getParsedTransaction(txSignature, { + commitment: "confirmed", + maxSupportedTransactionVersion: 0, + }) + ); + } +} diff --git a/contracts/solana/tests/xcall/send-message.ts b/contracts/solana/tests/xcall/send-message.ts new file mode 100644 index 00000000..768edf14 --- /dev/null +++ b/contracts/solana/tests/xcall/send-message.ts @@ -0,0 +1,96 @@ +import * as anchor from "@coral-xyz/anchor"; +import { Keypair } from "@solana/web3.js"; +import { assert } from "chai"; +import { SYSTEM_PROGRAM_ID } from "@coral-xyz/anchor/dist/cjs/native/system"; + +import { TestContext, XcallPDA } from "./setup"; +import { SYSVAR_INSTRUCTIONS_ID, TxnHelpers, sleep } from "../utils"; +import { Xcall } from "../../target/types/xcall"; +import { + Envelope, + CallMessage, + MessageType, + CallMessageWithRollback, +} from "./types"; + +import { CentralizedConnection } from "../../target/types/centralized_connection"; +import { ConnectionPDA } from "../centralized-connection/setup"; + +const connectionProgram: anchor.Program = + anchor.workspace.CentralizedConnection; + +const xcallProgram: anchor.Program = anchor.workspace.Xcall; + +describe("xcall - send message", () => { + const provider = anchor.AnchorProvider.env(); + const connection = provider.connection; + const wallet = provider.wallet as anchor.Wallet; + + let txnHelpers = new TxnHelpers(connection, wallet.payer); + let ctx = new TestContext(connection, txnHelpers, wallet.payer); + + before(async () => { + let fee_handler = Keypair.generate(); + await ctx.setFeeHandler(fee_handler); + await txnHelpers.airdrop(fee_handler.publicKey, 1e9); + + await ctx.setProtocolFee(5000); + }); + + it("should send message", async () => { + let envelope = new Envelope( + MessageType.CallMessage, + new CallMessage(new Uint8Array([1, 2])).encode(), + [connectionProgram.programId.toString()], + [wallet.publicKey.toString()] + ).encode(); + const to = { "0": "0x3.icon/abc" }; + + let config = await ctx.getConfig(); + let feeHandler = await connection.getAccountInfo(ctx.feeHandler.publicKey); + let nextSequence = config.sequenceNo.toNumber() + 1; + + let sendCallIx = await xcallProgram.methods + .sendCall(Buffer.from(envelope), to) + .accountsStrict({ + systemProgram: SYSTEM_PROGRAM_ID, + config: XcallPDA.config().pda, + signer: wallet.payer.publicKey, + dappAuthority: xcallProgram.programId, + rollbackAccount: null, + instructionSysvar: SYSVAR_INSTRUCTIONS_ID, + feeHandler: ctx.feeHandler.publicKey, + }) + .remainingAccounts([ + { + pubkey: connectionProgram.programId, + isSigner: false, + isWritable: true, + }, + { + pubkey: ConnectionPDA.config().pda, + isSigner: false, + isWritable: true, + }, + { + pubkey: ConnectionPDA.network_fee(ctx.dstNetworkId).pda, + isSigner: false, + isWritable: true, + }, + ]) + .instruction(); + + let sendCallTx = await txnHelpers.buildV0Txn([sendCallIx], [wallet.payer]); + await connection.sendTransaction(sendCallTx); + await sleep(2); + + assert.equal( + nextSequence.toString(), + (await ctx.getConfig()).sequenceNo.toString() + ); + assert.equal( + (await connection.getAccountInfo(ctx.feeHandler.publicKey)).lamports, + feeHandler.lamports + ctx.protocolFee + ); + }); +}); diff --git a/contracts/solana/tests/xcall/setup.ts b/contracts/solana/tests/xcall/setup.ts new file mode 100644 index 00000000..7cf19fee --- /dev/null +++ b/contracts/solana/tests/xcall/setup.ts @@ -0,0 +1,248 @@ +import * as anchor from "@coral-xyz/anchor"; + +import { PublicKey, Connection, Keypair } from "@solana/web3.js"; +import { Xcall } from "../../target/types/xcall"; +import { TxnHelpers, sleep, uint128ToArray } from "../utils"; +import { SYSTEM_PROGRAM_ID } from "@coral-xyz/anchor/dist/cjs/native/system"; + +const xcallProgram: anchor.Program = anchor.workspace.Xcall; + +import { DappPDA } from "../mock-dapp-multi/setup"; +import { MockDappMulti } from "../../target/types/mock_dapp_multi"; +const mockDappProgram: anchor.Program = + anchor.workspace.MockDappMulti; + +import { ConnectionPDA } from "../centralized-connection/setup"; +import { CentralizedConnection } from "../../target/types/centralized_connection"; +const connectionProgram: anchor.Program = + anchor.workspace.CentralizedConnection; + +export class TestContext { + networkId: string; + dstNetworkId: string; + admin: Keypair; + feeHandler: Keypair; + connection: Connection; + txnHelpers: TxnHelpers; + protocolFee: number; + + constructor(connection: Connection, txnHelpers: TxnHelpers, admin: Keypair) { + this.networkId = "solana"; + this.dstNetworkId = "0x3.icon"; + this.connection = connection; + this.txnHelpers = txnHelpers; + this.admin = admin; + this.feeHandler = admin; + } + + async initialize(netId: string) { + let initializeIx = await xcallProgram.methods + .initialize(netId) + .accountsStrict({ + signer: this.admin.publicKey, + systemProgram: SYSTEM_PROGRAM_ID, + config: XcallPDA.config().pda, + }) + .instruction(); + + let tx = await this.txnHelpers.buildV0Txn([initializeIx], [this.admin]); + await this.connection.sendTransaction(tx); + await sleep(2); + } + + async setFeeHandler(fee_handler: Keypair) { + this.feeHandler = fee_handler; + + let ix = await xcallProgram.methods + .setProtocolFeeHandler(fee_handler.publicKey) + .accountsStrict({ + admin: this.admin.publicKey, + config: XcallPDA.config().pda, + }) + .instruction(); + + let tx = await this.txnHelpers.buildV0Txn([ix], [this.admin]); + await this.connection.sendTransaction(tx); + await sleep(2); + } + + async setProtocolFee(fee: number) { + this.protocolFee = fee; + + let ix = await xcallProgram.methods + .setProtocolFee(new anchor.BN(fee)) + .accountsStrict({ + admin: this.admin.publicKey, + config: XcallPDA.config().pda, + }) + .instruction(); + + let tx = await this.txnHelpers.buildV0Txn([ix], [this.admin]); + await this.connection.sendTransaction(tx); + await sleep(2); + } + + async getExecuteCallAccounts(reqId: number, data: Uint8Array) { + const res = await xcallProgram.methods + .queryExecuteCallAccounts(new anchor.BN(reqId), Buffer.from(data), 1, 30) + .accountsStrict({ + config: XcallPDA.config().pda, + proxyRequest: XcallPDA.proxyRequest(reqId).pda, + }) + .remainingAccounts([ + { + pubkey: ConnectionPDA.config().pda, + isWritable: true, + isSigner: false, + }, + { + pubkey: DappPDA.config().pda, + isWritable: true, + isSigner: false, + }, + { + pubkey: connectionProgram.programId, + isWritable: true, + isSigner: false, + }, + { + pubkey: mockDappProgram.programId, + isWritable: true, + isSigner: false, + }, + ]) + .view({ commitment: "confirmed" }); + + return res.accounts; + } + + async getExecuteRollbackAccounts(sequenceNo: number) { + let res = await xcallProgram.methods + .queryExecuteRollbackAccounts(new anchor.BN(sequenceNo), 1, 30) + .accountsStrict({ + config: XcallPDA.config().pda, + rollbackAccount: XcallPDA.rollback(sequenceNo).pda, + }) + .remainingAccounts([ + { + pubkey: DappPDA.config().pda, + isWritable: false, + isSigner: false, + }, + { + pubkey: mockDappProgram.programId, + isWritable: false, + isSigner: false, + }, + ]) + .view({ commitment: "confirmed" }); + + return res.accounts; + } + + async getConfig() { + let { pda } = XcallPDA.config(); + return await xcallProgram.account.config.fetch(pda); + } + + async getProxyRequest(requestId: number) { + return await xcallProgram.account.proxyRequest.fetch( + XcallPDA.proxyRequest(requestId).pda, + "confirmed" + ); + } + + async getSuccessRes(sequenceNo: number) { + return await xcallProgram.account.successfulResponse.fetch( + XcallPDA.successRes(sequenceNo).pda, + "confirmed" + ); + } + + async getPendingRequest(messageBytes: Buffer) { + return await xcallProgram.account.pendingRequest.fetch( + XcallPDA.pendingRequest(messageBytes).pda, + "confirmed" + ); + } + + async getPendingResponse(messageBytes: Buffer) { + return await xcallProgram.account.pendingResponse.fetch( + XcallPDA.pendingResponse(messageBytes).pda, + "confirmed" + ); + } + + async getRollback(sequenceNo: number) { + return await xcallProgram.account.rollbackAccount.fetch( + XcallPDA.rollback(sequenceNo).pda, + "confirmed" + ); + } +} +export class XcallPDA { + constructor() {} + + static config() { + let [pda, bump] = PublicKey.findProgramAddressSync( + [Buffer.from("config")], + xcallProgram.programId + ); + + return { bump, pda }; + } + + static proxyRequest(requestId: number) { + const [pda, bump] = PublicKey.findProgramAddressSync( + [Buffer.from("proxy"), uint128ToArray(requestId)], + xcallProgram.programId + ); + + return { pda, bump }; + } + + static successRes(sequenceNo: number) { + const [pda, bump] = PublicKey.findProgramAddressSync( + [Buffer.from("success"), uint128ToArray(sequenceNo)], + xcallProgram.programId + ); + + return { pda, bump }; + } + + static defaultConnection(netId: String) { + const [pda, bump] = PublicKey.findProgramAddressSync( + [Buffer.from("conn"), Buffer.from(netId)], + xcallProgram.programId + ); + + return { pda, bump }; + } + + static pendingRequest(messageBytes: Buffer) { + const [pda, bump] = PublicKey.findProgramAddressSync( + [Buffer.from("req"), messageBytes], + xcallProgram.programId + ); + + return { pda, bump }; + } + + static pendingResponse(messageBytes: Buffer) { + const [pda, bump] = PublicKey.findProgramAddressSync( + [Buffer.from("res"), messageBytes], + xcallProgram.programId + ); + + return { pda, bump }; + } + + static rollback(sequenceNo: number) { + const [pda, bump] = PublicKey.findProgramAddressSync( + [Buffer.from("rollback"), uint128ToArray(sequenceNo)], + xcallProgram.programId + ); + + return { pda, bump }; + } +} diff --git a/contracts/solana/tests/xcall/types/envelope.ts b/contracts/solana/tests/xcall/types/envelope.ts new file mode 100644 index 00000000..1397a7fd --- /dev/null +++ b/contracts/solana/tests/xcall/types/envelope.ts @@ -0,0 +1,77 @@ +import * as rlp from "rlp"; + +export class CallMessage { + data: Uint8Array; + + constructor(data: Uint8Array) { + this.data = data; + } + + encode() { + let rlpInput: rlp.Input = [Buffer.from(this.data)]; + + return rlp.encode(rlpInput); + } +} + +export class CallMessageWithRollback { + data: Uint8Array; + rollback: Uint8Array; + + constructor(data: Uint8Array, rollback: Uint8Array) { + this.data = data; + this.rollback = rollback; + } + + encode() { + let rlpInput: rlp.Input = [ + Buffer.from(this.data), + Buffer.from(this.rollback), + ]; + + return rlp.encode(rlpInput); + } +} + +export class CallMessagePersisted { + data: Uint8Array; + + constructor(data: Uint8Array) { + this.data = data; + } + + encode() { + let rlpInput: rlp.Input = [Buffer.from(this.data)]; + return rlp.encode(rlpInput); + } +} + +export class Envelope { + msg_type: number; + message: Uint8Array; + sources: string[]; + destinations: string[]; + + constructor( + msg_type: number, + message: Uint8Array, + sources: string[], + destinations: string[] + ) { + this.msg_type = msg_type; + this.message = message; + this.sources = sources; + this.destinations = destinations; + } + + encode() { + let rlpInput: rlp.Input = [ + this.msg_type, + Buffer.from(this.message), + this.sources, + this.destinations, + ]; + + return rlp.encode(rlpInput); + } +} diff --git a/contracts/solana/tests/xcall/types/index.ts b/contracts/solana/tests/xcall/types/index.ts new file mode 100644 index 00000000..90a07596 --- /dev/null +++ b/contracts/solana/tests/xcall/types/index.ts @@ -0,0 +1,4 @@ +export * from "./message"; +export * from "./request"; +export * from "./result"; +export * from "./envelope"; diff --git a/contracts/solana/tests/xcall/types/message.ts b/contracts/solana/tests/xcall/types/message.ts new file mode 100644 index 00000000..5b562da6 --- /dev/null +++ b/contracts/solana/tests/xcall/types/message.ts @@ -0,0 +1,32 @@ +import * as rlp from "rlp"; + +export enum MessageType { + CallMessage = 0, + CallMessageWithRollback, + CallMessagePersisted, +} + +export enum CSResponseType { + CSMessageFailure, + CSResponseSuccess, +} + +export enum CSMessageType { + CSMessageRequest = 1, + CSMessageResult, +} + +export class CSMessage { + message_type: CSMessageType; + payload: Uint8Array; + + constructor(message_type: CSMessageType, payload: Uint8Array) { + this.message_type = message_type; + this.payload = payload; + } + + encode() { + let rlpInput: rlp.Input = [this.message_type, Buffer.from(this.payload)]; + return rlp.encode(rlpInput); + } +} diff --git a/contracts/solana/tests/xcall/types/request.ts b/contracts/solana/tests/xcall/types/request.ts new file mode 100644 index 00000000..f2ab6278 --- /dev/null +++ b/contracts/solana/tests/xcall/types/request.ts @@ -0,0 +1,41 @@ +import * as rlp from "rlp"; + +import { MessageType } from "."; + +export class CSMessageRequest { + from: string; + to: string; + sequence_no: number; + msg_type: MessageType; + data: Uint8Array; + protocols: string[]; + + constructor( + from: string, + to: string, + sequence_no: number, + msg_type: MessageType, + data: Uint8Array, + protocols: string[] + ) { + this.from = from; + this.to = to; + this.sequence_no = sequence_no; + this.msg_type = msg_type; + this.data = data; + this.protocols = protocols; + } + + encode() { + let rlpInput: rlp.Input = [ + this.from, + this.to, + this.sequence_no, + this.msg_type, + Buffer.from(this.data), + this.protocols, + ]; + + return rlp.encode(rlpInput); + } +} diff --git a/contracts/solana/tests/xcall/types/result.ts b/contracts/solana/tests/xcall/types/result.ts new file mode 100644 index 00000000..2d74ed3d --- /dev/null +++ b/contracts/solana/tests/xcall/types/result.ts @@ -0,0 +1,29 @@ +import * as rlp from "rlp"; + +import { CSMessageType, CSResponseType } from "./message"; + +export class CSMessageResult { + sequence_no: number; + response_code: CSResponseType; + data: Uint8Array | null; + + constructor( + sequence_no: number, + response_code: CSResponseType, + data: Uint8Array | null + ) { + this.sequence_no = sequence_no; + this.response_code = response_code; + this.data = data; + } + + encode() { + let rlpInput: rlp.Input = [ + this.sequence_no, + this.response_code, + Buffer.from(this.data), + ]; + + return rlp.encode(rlpInput); + } +} diff --git a/contracts/solana/tests/xcall/xcall.ts b/contracts/solana/tests/xcall/xcall.ts new file mode 100644 index 00000000..6c4ce9e2 --- /dev/null +++ b/contracts/solana/tests/xcall/xcall.ts @@ -0,0 +1,63 @@ +import * as anchor from "@coral-xyz/anchor"; +import { assert } from "chai"; + +import { TxnHelpers } from "../utils/transaction"; +import { Xcall } from "../../target/types/xcall"; + +import { CentralizedConnection } from "../../target/types/centralized_connection"; +import { ConnectionPDA } from "../centralized-connection/setup"; +import { TestContext, XcallPDA } from "./setup"; +import { sleep } from "../utils"; + +const connectionProgram: anchor.Program = + anchor.workspace.CentralizedConnection; + +const xcallProgram: anchor.Program = anchor.workspace.Xcall; + +describe("Xcall", async () => { + const provider = anchor.AnchorProvider.env(); + const connection = provider.connection; + const wallet = provider.wallet as anchor.Wallet; + + let txnHelpers = new TxnHelpers(connection, wallet.payer); + let ctx = new TestContext(connection, txnHelpers, wallet.payer); + + it("[get_fee]: should get fee", async () => { + let isResponse = true; + + let fee = await xcallProgram.methods + .getFee(ctx.dstNetworkId, isResponse, [ + connectionProgram.programId.toString(), + ]) + .accountsStrict({ + config: XcallPDA.config().pda, + }) + .remainingAccounts([ + { + pubkey: ConnectionPDA.network_fee(ctx.dstNetworkId).pda, + isSigner: false, + isWritable: true, + }, + { + pubkey: connectionProgram.programId, + isSigner: false, + isWritable: true, + }, + ]) + .view({ commitment: "confirmed" }); + await sleep(2); + + let connectionFee = await connectionProgram.methods + .getFee(ctx.dstNetworkId, isResponse) + .accountsStrict({ + networkFee: ConnectionPDA.network_fee(ctx.dstNetworkId).pda, + }) + .view(); + + let xcallConfig = await ctx.getConfig(); + assert.equal( + fee.toString(), + xcallConfig.protocolFee.toNumber() + connectionFee.toNumber() + ); + }); +}); diff --git a/contracts/solana/tsconfig.json b/contracts/solana/tsconfig.json new file mode 100644 index 00000000..247d160a --- /dev/null +++ b/contracts/solana/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "types": ["mocha", "chai"], + "typeRoots": ["./node_modules/@types"], + "lib": ["es2015"], + "module": "commonjs", + "target": "es6", + "esModuleInterop": true, + "resolveJsonModule": true + } +} diff --git a/contracts/solana/yarn.lock b/contracts/solana/yarn.lock new file mode 100644 index 00000000..0cf3ca99 --- /dev/null +++ b/contracts/solana/yarn.lock @@ -0,0 +1,1186 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/runtime@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.0.tgz#3af9a91c1b739c569d5d80cc917280919c544ecb" + integrity sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw== + dependencies: + regenerator-runtime "^0.14.0" + +"@coral-xyz/anchor-errors@^0.30.1": + version "0.30.1" + resolved "https://registry.yarnpkg.com/@coral-xyz/anchor-errors/-/anchor-errors-0.30.1.tgz#bdfd3a353131345244546876eb4afc0e125bec30" + integrity sha512-9Mkradf5yS5xiLWrl9WrpjqOrAV+/W2RQHDlbnAZBivoGpOs1ECjoDCkVk4aRG8ZdiFiB8zQEVlxf+8fKkmSfQ== + +"@coral-xyz/anchor@^0.30.1": + version "0.30.1" + resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.30.1.tgz#17f3e9134c28cd0ea83574c6bab4e410bcecec5d" + integrity sha512-gDXFoF5oHgpriXAaLpxyWBHdCs8Awgf/gLHIo6crv7Aqm937CNdY+x+6hoj7QR5vaJV7MxWSQ0NGFzL3kPbWEQ== + dependencies: + "@coral-xyz/anchor-errors" "^0.30.1" + "@coral-xyz/borsh" "^0.30.1" + "@noble/hashes" "^1.3.1" + "@solana/web3.js" "^1.68.0" + bn.js "^5.1.2" + bs58 "^4.0.1" + buffer-layout "^1.2.2" + camelcase "^6.3.0" + cross-fetch "^3.1.5" + crypto-hash "^1.3.0" + eventemitter3 "^4.0.7" + pako "^2.0.3" + snake-case "^3.0.4" + superstruct "^0.15.4" + toml "^3.0.0" + +"@coral-xyz/borsh@^0.30.1": + version "0.30.1" + resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.30.1.tgz#869d8833abe65685c72e9199b8688477a4f6b0e3" + integrity sha512-aaxswpPrCFKl8vZTbxLssA2RvwX2zmKLlRCIktJOwW+VpVwYtXRtlWiIP+c2pPRKneiTiWCN2GEMSH9j1zTlWQ== + dependencies: + bn.js "^5.1.2" + buffer-layout "^1.2.0" + +"@noble/curves@^1.4.2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.5.0.tgz#7a9b9b507065d516e6dce275a1e31db8d2a100dd" + integrity sha512-J5EKamIHnKPyClwVrzmaf5wSdQXgdHcPZIZLu3bwnbeCx8/7NPK5q2ZBWF+5FvYGByjiQQsJYX6jfgB2wDPn3A== + dependencies: + "@noble/hashes" "1.4.0" + +"@noble/hashes@1.4.0", "@noble/hashes@^1.3.1", "@noble/hashes@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" + integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== + +"@solana/buffer-layout@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz#b996235eaec15b1e0b5092a8ed6028df77fa6c15" + integrity sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA== + dependencies: + buffer "~6.0.3" + +"@solana/web3.js@^1.68.0": + version "1.95.3" + resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.95.3.tgz#70b5f4d76823f56b5af6403da51125fffeb65ff3" + integrity sha512-O6rPUN0w2fkNqx/Z3QJMB9L225Ex10PRDH8bTaIUPZXMPV0QP8ZpPvjQnXK+upUczlRgzHzd6SjKIha1p+I6og== + dependencies: + "@babel/runtime" "^7.25.0" + "@noble/curves" "^1.4.2" + "@noble/hashes" "^1.4.0" + "@solana/buffer-layout" "^4.0.1" + agentkeepalive "^4.5.0" + bigint-buffer "^1.1.5" + bn.js "^5.2.1" + borsh "^0.7.0" + bs58 "^4.0.1" + buffer "6.0.3" + fast-stable-stringify "^1.0.0" + jayson "^4.1.1" + node-fetch "^2.7.0" + rpc-websockets "^9.0.2" + superstruct "^2.0.2" + +"@swc/helpers@^0.5.11": + version "0.5.12" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.12.tgz#37aaca95284019eb5d2207101249435659709f4b" + integrity sha512-KMZNXiGibsW9kvZAO1Pam2JPTDBm+KSHMMHWdsyI/1DbIZjT2A6Gy3hblVXUMEDvUAKq+e0vL0X0o54owWji7g== + dependencies: + tslib "^2.4.0" + +"@types/bn.js@^5.1.0": + version "5.1.5" + resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.5.tgz#2e0dacdcce2c0f16b905d20ff87aedbc6f7b4bf0" + integrity sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A== + dependencies: + "@types/node" "*" + +"@types/chai@^4.3.0": + version "4.3.17" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.17.tgz#9195f9d242f2ac3b429908864b6b871a8f73f489" + integrity sha512-zmZ21EWzR71B4Sscphjief5djsLre50M6lI622OSySTmn9DB3j+C3kWroHfBQWXbOBwbgg/M8CG/hUxDLIloow== + +"@types/connect@^3.4.33": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== + +"@types/mocha@^9.0.0": + version "9.1.1" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4" + integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== + +"@types/node@*": + version "22.5.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.5.0.tgz#10f01fe9465166b4cab72e75f60d8b99d019f958" + integrity sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg== + dependencies: + undici-types "~6.19.2" + +"@types/node@^12.12.54": + version "12.20.55" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" + integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== + +"@types/uuid@^8.3.4": + version "8.3.4" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" + integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== + +"@types/ws@^7.4.4": + version "7.4.7" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702" + integrity sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww== + dependencies: + "@types/node" "*" + +"@types/ws@^8.2.2": + version "8.5.12" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.12.tgz#619475fe98f35ccca2a2f6c137702d85ec247b7e" + integrity sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ== + dependencies: + "@types/node" "*" + +"@ungap/promise-all-settled@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" + integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== + +JSONStream@^1.3.5: + version "1.3.5" + resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" + integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== + dependencies: + jsonparse "^1.2.0" + through ">=2.2.7 <3" + +agentkeepalive@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.5.0.tgz#2673ad1389b3c418c5a20c5d7364f93ca04be923" + integrity sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew== + dependencies: + humanize-ms "^1.2.1" + +ansi-colors@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +arrify@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA== + +assertion-error@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" + integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base-x@^3.0.2: + version "3.0.10" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.10.tgz#62de58653f8762b5d6f8d9fe30fa75f7b2585a75" + integrity sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ== + dependencies: + safe-buffer "^5.0.1" + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +bigint-buffer@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/bigint-buffer/-/bigint-buffer-1.1.5.tgz#d038f31c8e4534c1f8d0015209bf34b4fa6dd442" + integrity sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA== + dependencies: + bindings "^1.3.0" + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +bindings@^1.3.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" + integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== + dependencies: + file-uri-to-path "1.0.0" + +bn.js@^5.1.2, bn.js@^5.2.0, bn.js@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" + integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== + +borsh@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/borsh/-/borsh-0.7.0.tgz#6e9560d719d86d90dc589bca60ffc8a6c51fec2a" + integrity sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA== + dependencies: + bn.js "^5.2.0" + bs58 "^4.0.0" + text-encoding-utf-8 "^1.0.2" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +bs58@^4.0.0, bs58@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" + integrity sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw== + dependencies: + base-x "^3.0.2" + +buffer-from@^1.0.0, buffer-from@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer-layout@^1.2.0, buffer-layout@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/buffer-layout/-/buffer-layout-1.2.2.tgz#b9814e7c7235783085f9ca4966a0cfff112259d5" + integrity sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA== + +buffer@6.0.3, buffer@^6.0.3, buffer@~6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + +bufferutil@^4.0.1: + version "4.0.8" + resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.8.tgz#1de6a71092d65d7766c4d8a522b261a6e787e8ea" + integrity sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw== + dependencies: + node-gyp-build "^4.3.0" + +camelcase@^6.0.0, camelcase@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +chai@^4.3.4: + version "4.5.0" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.5.0.tgz#707e49923afdd9b13a8b0b47d33d732d13812fd8" + integrity sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw== + dependencies: + assertion-error "^1.1.0" + check-error "^1.0.3" + deep-eql "^4.1.3" + get-func-name "^2.0.2" + loupe "^2.3.6" + pathval "^1.1.1" + type-detect "^4.1.0" + +chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +check-error@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.3.tgz#a6502e4312a7ee969f646e83bb3ddd56281bd694" + integrity sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg== + dependencies: + get-func-name "^2.0.2" + +chokidar@3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +commander@^2.20.3: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +cross-fetch@^3.1.5: + version "3.1.8" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" + integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg== + dependencies: + node-fetch "^2.6.12" + +crypto-hash@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/crypto-hash/-/crypto-hash-1.3.0.tgz#b402cb08f4529e9f4f09346c3e275942f845e247" + integrity sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg== + +debug@4.3.3: + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + dependencies: + ms "2.1.2" + +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + +deep-eql@^4.1.3: + version "4.1.4" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.4.tgz#d0d3912865911bb8fac5afb4e3acfa6a28dc72b7" + integrity sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg== + dependencies: + type-detect "^4.0.0" + +delay@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" + integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw== + +diff@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + +diff@^3.1.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== + +dot-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" + integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +es6-promise@^4.0.3: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + integrity sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ== + dependencies: + es6-promise "^4.0.3" + +escalade@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== + +escape-string-regexp@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eventemitter3@^4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +eventemitter3@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" + integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== + +eyes@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" + integrity sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ== + +fast-stable-stringify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz#5c5543462b22aeeefd36d05b34e51c78cb86d313" + integrity sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag== + +file-uri-to-path@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" + integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +find-up@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-func-name@^2.0.1, get-func-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" + integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +growl@1.10.5: + version "1.10.5" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" + integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== + dependencies: + ms "^2.0.0" + +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isomorphic-ws@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" + integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== + +jayson@^4.1.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/jayson/-/jayson-4.1.2.tgz#443c26a8658703e0b2e881117b09395d88b6982e" + integrity sha512-5nzMWDHy6f+koZOuYsArh2AXs73NfWYVlFyJJuCedr93GpY+Ku8qq10ropSXVfHK+H0T6paA88ww+/dV+1fBNA== + dependencies: + "@types/connect" "^3.4.33" + "@types/node" "^12.12.54" + "@types/ws" "^7.4.4" + JSONStream "^1.3.5" + commander "^2.20.3" + delay "^5.0.0" + es6-promisify "^5.0.0" + eyes "^0.1.8" + isomorphic-ws "^4.0.1" + json-stringify-safe "^5.0.1" + uuid "^8.3.2" + ws "^7.5.10" + +js-yaml@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +json-stringify-safe@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== + +json5@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== + dependencies: + minimist "^1.2.0" + +jsonparse@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" + integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +log-symbols@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +loupe@^2.3.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697" + integrity sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA== + dependencies: + get-func-name "^2.0.1" + +lower-case@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" + integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== + dependencies: + tslib "^2.0.3" + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +minimatch@4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-4.2.1.tgz#40d9d511a46bdc4e563c22c3080cde9c0d8299b4" + integrity sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^3.0.4: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.0, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +mkdirp@^0.5.1: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mocha@^9.0.3: + version "9.2.2" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.2.2.tgz#d70db46bdb93ca57402c809333e5a84977a88fb9" + integrity sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g== + dependencies: + "@ungap/promise-all-settled" "1.1.2" + ansi-colors "4.1.1" + browser-stdout "1.3.1" + chokidar "3.5.3" + debug "4.3.3" + diff "5.0.0" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "7.2.0" + growl "1.10.5" + he "1.2.0" + js-yaml "4.1.0" + log-symbols "4.1.0" + minimatch "4.2.1" + ms "2.1.3" + nanoid "3.3.1" + serialize-javascript "6.0.0" + strip-json-comments "3.1.1" + supports-color "8.1.1" + which "2.0.2" + workerpool "6.2.0" + yargs "16.2.0" + yargs-parser "20.2.4" + yargs-unparser "2.0.0" + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3, ms@^2.0.0: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nanoid@3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" + integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== + +no-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" + integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== + dependencies: + lower-case "^2.0.2" + tslib "^2.0.3" + +node-fetch@^2.6.12, node-fetch@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + +node-gyp-build@^4.3.0: + version "4.8.1" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.1.tgz#976d3ad905e71b76086f4f0b0d3637fe79b6cda5" + integrity sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +pako@^2.0.3: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" + integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +pathval@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" + integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== + +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +prettier@^2.6.2: + version "2.8.8" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" + integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +rlp@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/rlp/-/rlp-3.0.0.tgz#5a60725ca4314a3a165feecca1836e4f2c1e2343" + integrity sha512-PD6U2PGk6Vq2spfgiWZdomLvRGDreBLxi5jv5M8EpRo3pU6VEm31KO+HFxE18Q3vgqfDrQ9pZA3FP95rkijNKw== + +rpc-websockets@^9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-9.0.2.tgz#4c1568d00b8100f997379a363478f41f8f4b242c" + integrity sha512-YzggvfItxMY3Lwuax5rC18inhbjJv9Py7JXRHxTIi94JOLrqBsSsUUc5bbl5W6c11tXhdfpDPK0KzBhoGe8jjw== + dependencies: + "@swc/helpers" "^0.5.11" + "@types/uuid" "^8.3.4" + "@types/ws" "^8.2.2" + buffer "^6.0.3" + eventemitter3 "^5.0.1" + uuid "^8.3.2" + ws "^8.5.0" + optionalDependencies: + bufferutil "^4.0.1" + utf-8-validate "^5.0.2" + +safe-buffer@^5.0.1, safe-buffer@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +serialize-javascript@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" + integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== + dependencies: + randombytes "^2.1.0" + +snake-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" + integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + +source-map-support@^0.5.6: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + +strip-json-comments@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +superstruct@^0.15.4: + version "0.15.5" + resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.15.5.tgz#0f0a8d3ce31313f0d84c6096cd4fa1bfdedc9dab" + integrity sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ== + +superstruct@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-2.0.2.tgz#3f6d32fbdc11c357deff127d591a39b996300c54" + integrity sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A== + +supports-color@8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +text-encoding-utf-8@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13" + integrity sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg== + +"through@>=2.2.7 <3": + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toml@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/toml/-/toml-3.0.0.tgz#342160f1af1904ec9d204d03a5d61222d762c5ee" + integrity sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w== + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +ts-mocha@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/ts-mocha/-/ts-mocha-10.0.0.tgz#41a8d099ac90dbbc64b06976c5025ffaebc53cb9" + integrity sha512-VRfgDO+iiuJFlNB18tzOfypJ21xn2xbuZyDvJvqpTbWgkAgD17ONGr8t+Tl8rcBtOBdjXp5e/Rk+d39f7XBHRw== + dependencies: + ts-node "7.0.1" + optionalDependencies: + tsconfig-paths "^3.5.0" + +ts-node@7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-7.0.1.tgz#9562dc2d1e6d248d24bc55f773e3f614337d9baf" + integrity sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw== + dependencies: + arrify "^1.0.0" + buffer-from "^1.1.0" + diff "^3.1.0" + make-error "^1.1.1" + minimist "^1.2.0" + mkdirp "^0.5.1" + source-map-support "^0.5.6" + yn "^2.0.0" + +tsconfig-paths@^3.5.0: + version "3.15.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" + integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tslib@^2.0.3, tslib@^2.4.0: + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== + +type-detect@^4.0.0, type-detect@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.1.0.tgz#deb2453e8f08dcae7ae98c626b13dddb0155906c" + integrity sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw== + +typescript@^4.3.5: + version "4.9.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" + integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== + +undici-types@~6.19.2: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + +utf-8-validate@^5.0.2: + version "5.0.10" + resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz#d7d10ea39318171ca982718b6b96a8d2442571a2" + integrity sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ== + dependencies: + node-gyp-build "^4.3.0" + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +which@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +workerpool@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b" + integrity sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A== + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +ws@^7.5.10: + version "7.5.10" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" + integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== + +ws@^8.5.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yargs-parser@20.2.4: + version "20.2.4" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-unparser@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== + dependencies: + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" + +yargs@16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yn@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a" + integrity sha512-uTv8J/wiWTgUTg+9vLTi//leUl5vDQS6uii/emeTb2ssY7vl6QWf2fFbIIGjnhjvbdKlU0ed7QPgY1htTC86jQ== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/docs/solana-xcall.md b/docs/solana-xcall.md new file mode 100644 index 00000000..eb6f52ef --- /dev/null +++ b/docs/solana-xcall.md @@ -0,0 +1,67 @@ +# Changes in the Implementation of xCall in Solana + +## Overview + +This document outlines the major changes in the implementation of xCall on the Solana blockchain and the rationale behind these updates. Key modifications include the removal of reply logic and default connections, improvements in handling cross-contract errors, the introduction of a forced rollback mechanism, and the splitting of message handling into multiple instructions to address stack memory limitations. + +## Major Changes + +**1. Remove reply state** + +**Change** + +- The `send_call` and `execute_call` instructions have been simplified by removing the reply logic +- When xCall invokes a dApp's instruction, the dApp is no longer able invoke any further instructions back to xCall within the same transaction + +**Rationale** + +- Solana's security model restricts cross-program reentrancy, preventing dApps from re-invoking xCall during the same execution flow. This ensures a more secure and predictable execution environment. + +**2. Remove default connection** + +**Change** + +- The default connection state has been removed, requiring dApps to explicitly specify the connections they wish to use for cross-chain messaging. + +**Rationale** + +- Solana programs are inherently stateless, meaning they do not maintain persistent account states. +- The initial purpose of a default connection was to simplify the process for users, allowing them to avoid managing connections for each chain individually. However, since users must always specify the connection account they intend to use, the default connection became redundant and unnecessary. + +**3. Handling cross-contract error** + +**Change** + +- Introduced the `HandleCallMessageResponse` type for dApps (e.g., Balanced), which will be returned by dApps in both success and failure cases. + +**Rationale** + +- Solana lacks native exception handling mechanisms like try-catch, making it difficult to roll back every failed message during `execute_call`. +- Instead of throwing an error and causing the entire transaction to fail, the dApp will now return a standardized response type (`HandleCallMessageResponse`). This type will allow xCall to appropriately handle the outcome of the message, whether it succeeds or fails. + +**4. Splitting the HandleMessage Instruction** + +**Change** + +- The `handle_message` instruction has been split into two separate instructions: `handle_request` and `handle_result`. These instructions are invoked internally by xCall after `handle_message` is executed. + +**Rationale** + +- Solana imposes a strict limit on stack frame size, capped at 4KB. When using the Anchor framework, combining the accounts for both `handle_request` and `handle_result` exceeded this limit, leading to potential execution failures. +- To address this, we split the logic of receiving messages into multiple instructions. The `handle_message` instruction, which is executed for every incoming message, now includes only the common state accounts. This reduces stack memory consumption as only necessary accounts are deserialized in this step. +- After executing `handle_message`, xCall invokes either `handle_request` or `handle_result` based on the message type, passing the remaining accounts specific to each context. This ensures that the stack memory usage stays within limits, resolving the original issue. +- We considered alternative solutions like using `Box` and `Zero_Copy` provided by the Anchor framework. While these approaches worked in the `solana-test-validator` environment, they were not viable on devnet, likely due to the lack of feature activation on that network. Therefore, we opted for the current solution, splitting the instructions in a way that guarantees they are invoked only by xCall. + +**5. Handle Force Rollback Implementation** + +**Change** + +- Introduced the `handle_forced_rollback` instruction to manage forced rollbacks of cross-chain messages when an unknown error occurs after the message is received on the destination chain. + +**Rationale** + +- The `handle_forced_rollback` function allows dApps to initiate a rollback when a cross-chain message encounters an unknown error post-reception on the destination chain. Instead of letting the message fail silently or causing undefined behavior, this function provides a controlled way to send a failure response back to the source chain, effectively reversing the state to reflect that the message was not successfully processed. + +## Conclusion + +The updated implementation of xCall on Solana deviates from the original architecture but maintains functional equivalence, ensuring that the protocol remains robust and efficient within Solana's unique constraints.