From ec9a72423168a912fd6d0a7b8724de0cf680a473 Mon Sep 17 00:00:00 2001 From: Fran Domovic <93442516+frdomovic@users.noreply.github.com> Date: Sat, 7 Dec 2024 02:50:28 +0100 Subject: [PATCH] feat(node): ICP protocol integration - from init to query / mutate (#986) --- .github/workflows/rust.yml | 1 - Cargo.lock | 327 ++++++++++++++++-- Cargo.toml | 3 + contracts/icp/context-config/Cargo.toml | 2 +- contracts/icp/context-config/src/mutate.rs | 13 +- contracts/icp/context-config/src/query.rs | 15 +- .../icp/context-config/tests/integration.rs | 1 + crates/context/config/Cargo.toml | 27 +- crates/context/config/src/client.rs | 177 ++++++---- crates/context/config/src/client/config.rs | 3 + crates/context/config/src/client/env.rs | 10 +- .../config/src/client/env/config/mutate.rs | 36 +- .../client/env/config/query/application.rs | 22 +- .../env/config/query/application_revision.rs | 20 +- .../src/client/env/config/query/has_member.rs | 24 +- .../src/client/env/config/query/members.rs | 32 +- .../env/config/query/members_revision.rs | 21 +- .../src/client/env/config/query/privileges.rs | 39 ++- .../client/env/config/query/proxy_contract.rs | 22 +- .../src/client/env/config/types/starknet.rs | 1 - .../config/src/client/env/proxy/mutate.rs | 28 ++ .../env/proxy/query/active_proposals.rs | 19 +- .../src/client/env/proxy/query/proposal.rs | 24 +- .../env/proxy/query/proposal_approvals.rs | 31 +- .../env/proxy/query/proposal_approvers.rs | 34 +- .../src/client/env/proxy/query/proposals.rs | 23 +- crates/context/config/src/client/protocol.rs | 1 + .../context/config/src/client/protocol/icp.rs | 247 +++++++++++++ crates/context/config/src/icp.rs | 134 +++++++ crates/context/config/src/icp/repr.rs | 1 + crates/context/config/src/icp/types.rs | 72 +++- crates/context/config/src/types.rs | 25 +- crates/merod/Cargo.toml | 3 + crates/merod/src/cli/init.rs | 49 ++- crates/merod/src/cli/relay.rs | 74 +--- 35 files changed, 1348 insertions(+), 213 deletions(-) create mode 100644 crates/context/config/src/client/protocol/icp.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 93161276a..184087cef 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -4,7 +4,6 @@ on: push: branches: ['master'] pull_request: - branches: ['master'] env: CARGO_TERM_COLOR: always diff --git a/Cargo.lock b/Cargo.lock index 4b052293f..49668cd17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -525,6 +525,17 @@ dependencies = [ "tower-service", ] +[[package]] +name = "backoff" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" +dependencies = [ + "getrandom", + "instant", + "rand 0.8.5", +] + [[package]] name = "backtrace" version = "0.3.71" @@ -546,6 +557,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.13.1" @@ -814,6 +831,19 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "cached" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8466736fe5dbcaf8b8ee24f9bbefe43c884dc3e9ff7178da70f55bffca1133c" +dependencies = [ + "ahash 0.8.11", + "hashbrown 0.14.5", + "instant", + "once_cell", + "thiserror 1.0.69", +] + [[package]] name = "calimero-blobstore" version = "0.1.0" @@ -825,7 +855,7 @@ dependencies = [ "eyre", "futures-util", "serde", - "sha2", + "sha2 0.10.8", "thiserror 1.0.69", "tokio", "tokio-util", @@ -875,10 +905,12 @@ dependencies = [ "borsh", "bs58 0.5.1", "candid", + "ed25519-consensus", "ed25519-dalek", "either", "eyre", "hex", + "ic-agent", "near-crypto", "near-jsonrpc-client", "near-jsonrpc-primitives", @@ -1062,7 +1094,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", - "sha2", + "sha2 0.10.8", "thiserror 1.0.69", "url", ] @@ -1170,7 +1202,7 @@ dependencies = [ "rust-embed", "serde", "serde_json", - "sha2", + "sha2 0.10.8", "starknet", "starknet-crypto 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "thiserror 1.0.69", @@ -1212,7 +1244,7 @@ dependencies = [ "indexmap 2.6.0", "rand 0.8.5", "serde", - "sha2", + "sha2 0.10.8", "thiserror 1.0.69", "velcro", ] @@ -1751,6 +1783,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array 0.14.7", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -1802,6 +1835,19 @@ dependencies = [ "syn 2.0.89", ] +[[package]] +name = "curve25519-dalek-ng" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.6.4", + "subtle-ng", + "zeroize", +] + [[package]] name = "darling" version = "0.20.10" @@ -1884,6 +1930,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", + "pem-rfc7468", "zeroize", ] @@ -1976,6 +2023,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", + "const-oid", "crypto-common", "subtle", ] @@ -2067,6 +2115,20 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53aff6fdc1b181225acdcb5b14c47106726fd8e486707315b1b138baed68ee31" +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + [[package]] name = "ed25519" version = "2.2.3" @@ -2077,6 +2139,21 @@ dependencies = [ "signature", ] +[[package]] +name = "ed25519-consensus" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8465edc8ee7436ffea81d21a019b16676ee3db267aa8d5a8d729581ecf998b" +dependencies = [ + "curve25519-dalek-ng", + "hex", + "rand_core 0.6.4", + "serde", + "sha2 0.9.9", + "thiserror 1.0.69", + "zeroize", +] + [[package]] name = "ed25519-dalek" version = "2.1.1" @@ -2087,7 +2164,7 @@ dependencies = [ "ed25519", "rand_core 0.6.4", "serde", - "sha2", + "sha2 0.10.8", "subtle", "zeroize", ] @@ -2098,6 +2175,26 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array 0.14.7", + "group", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -2221,7 +2318,7 @@ dependencies = [ "scrypt", "serde", "serde_json", - "sha2", + "sha2 0.10.8", "sha3", "thiserror 1.0.69", "uuid 0.8.2", @@ -2638,6 +2735,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -3150,6 +3248,50 @@ dependencies = [ "cc", ] +[[package]] +name = "ic-agent" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "158138fcb769fe6288e63d5db221c904e472cfb7d376aba13a38c060f2984e63" +dependencies = [ + "async-lock", + "async-trait", + "backoff", + "cached", + "candid", + "der", + "ecdsa", + "ed25519-consensus", + "elliptic-curve", + "futures-util", + "hex", + "http 1.1.0", + "http-body 1.0.1", + "ic-certification", + "ic-transport-types 0.39.1", + "ic-verify-bls-signature 0.5.0", + "k256", + "leb128", + "p256", + "pem", + "pkcs8", + "rand 0.8.5", + "rangemap", + "reqwest 0.12.9", + "sec1", + "serde", + "serde_bytes", + "serde_cbor", + "serde_repr", + "sha2 0.10.8", + "simple_asn1", + "thiserror 1.0.69", + "time", + "tokio", + "tower-service", + "url", +] + [[package]] name = "ic-canister-sig-creation" version = "1.1.0" @@ -3165,7 +3307,7 @@ dependencies = [ "serde", "serde_bytes", "serde_cbor", - "sha2", + "sha2 0.10.8", "thiserror 1.0.69", ] @@ -3259,7 +3401,7 @@ dependencies = [ "hex", "serde", "serde_bytes", - "sha2", + "sha2 0.10.8", ] [[package]] @@ -3274,7 +3416,7 @@ dependencies = [ "ic-cdk 0.17.0", "serde", "serde_bytes", - "sha2", + "sha2 0.10.8", ] [[package]] @@ -3284,7 +3426,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08ae59483e377cd9aad94ec339ed1d2583b0d5929cab989328dac2d853b2f570" dependencies = [ "leb128", - "sha2", + "sha2 0.10.8", ] [[package]] @@ -3295,12 +3437,12 @@ checksum = "f9de5065430a9b1a61934f7e4a65474a7a11658db84a8b5b0c42baced6c33752" dependencies = [ "ic-canister-sig-creation", "ic-certification", - "ic-verify-bls-signature", + "ic-verify-bls-signature 0.6.0", "ic_principal", "serde", "serde_bytes", "serde_cbor", - "sha2", + "sha2 0.10.8", ] [[package]] @@ -3316,10 +3458,42 @@ dependencies = [ "serde", "serde_bytes", "serde_repr", - "sha2", + "sha2 0.10.8", + "thiserror 1.0.69", +] + +[[package]] +name = "ic-transport-types" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d8789a5c176bb1b925fa58ca97c651a3995d504e76101e93d2a17f558bdcf66" +dependencies = [ + "candid", + "hex", + "ic-certification", + "leb128", + "serde", + "serde_bytes", + "serde_cbor", + "serde_repr", + "sha2 0.10.8", "thiserror 1.0.69", ] +[[package]] +name = "ic-verify-bls-signature" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d420b25c0091059f6c3c23a21427a81915e6e0aca3b79e0d403ed767f286a3b9" +dependencies = [ + "hex", + "ic_bls12_381", + "lazy_static", + "pairing", + "rand 0.8.5", + "sha2 0.10.8", +] + [[package]] name = "ic-verify-bls-signature" version = "0.6.0" @@ -3330,7 +3504,7 @@ dependencies = [ "ic_bls12_381", "lazy_static", "pairing", - "sha2", + "sha2 0.10.8", ] [[package]] @@ -3362,7 +3536,7 @@ dependencies = [ "crc32fast", "data-encoding", "serde", - "sha2", + "sha2 0.10.8", "thiserror 1.0.69", ] @@ -3811,6 +3985,20 @@ dependencies = [ "simple_asn1", ] +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if 1.0.0", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2 0.10.8", + "signature", +] + [[package]] name = "keccak" version = "0.1.5" @@ -3856,7 +4044,7 @@ checksum = "bbc2a4da0d9e52ccfe6306801a112e81a8fc0c76aa3e4449fefeda7fef72bb34" dependencies = [ "lambdaworks-math", "serde", - "sha2", + "sha2 0.10.8", "sha3", ] @@ -4062,7 +4250,7 @@ dependencies = [ "quick-protobuf-codec 0.3.1", "rand 0.8.5", "regex", - "sha2", + "sha2 0.10.8", "smallvec", "tracing", "void", @@ -4104,7 +4292,7 @@ dependencies = [ "quick-protobuf", "rand 0.8.5", "serde", - "sha2", + "sha2 0.10.8", "thiserror 1.0.69", "tracing", "zeroize", @@ -4131,7 +4319,7 @@ dependencies = [ "quick-protobuf", "quick-protobuf-codec 0.3.1", "rand 0.8.5", - "sha2", + "sha2 0.10.8", "smallvec", "thiserror 1.0.69", "tracing", @@ -4198,7 +4386,7 @@ dependencies = [ "once_cell", "quick-protobuf", "rand 0.8.5", - "sha2", + "sha2 0.10.8", "snow", "static_assertions", "thiserror 1.0.69", @@ -4683,12 +4871,15 @@ dependencies = [ "color-eyre", "const_format", "dirs", + "ed25519-consensus", "eyre", "futures-util", "hex", + "ic-agent", "libp2p", "multiaddr", "near-crypto", + "rand 0.8.5", "starknet", "tokio", "toml_edit", @@ -4918,7 +5109,7 @@ dependencies = [ "num-rational", "serde", "serde_json", - "sha2", + "sha2 0.10.8", "smart-default", "time", "tracing", @@ -5096,7 +5287,7 @@ dependencies = [ "num-rational", "serde", "serde_repr", - "sha2", + "sha2 0.10.8", "thiserror 1.0.69", ] @@ -5232,7 +5423,7 @@ dependencies = [ "rustix", "serde", "serde_repr", - "sha2", + "sha2 0.10.8", "sha3", "strum 0.24.1", "tempfile", @@ -5267,7 +5458,7 @@ dependencies = [ "reqwest 0.12.9", "serde", "serde_json", - "sha2", + "sha2 0.10.8", "tempfile", "thiserror 1.0.69", "tokio", @@ -5676,6 +5867,18 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.8", +] + [[package]] name = "pairing" version = "0.23.0" @@ -5767,7 +5970,7 @@ dependencies = [ "digest 0.10.7", "hmac", "password-hash", - "sha2", + "sha2 0.10.8", ] [[package]] @@ -5780,6 +5983,15 @@ dependencies = [ "serde", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -5844,14 +6056,14 @@ dependencies = [ "candid", "hex", "ic-certification", - "ic-transport-types", + "ic-transport-types 0.37.1", "reqwest 0.12.9", "schemars", "serde", "serde_bytes", "serde_cbor", "serde_json", - "sha2", + "sha2 0.10.8", "slog", "strum 0.26.3", "strum_macros 0.26.4", @@ -5947,6 +6159,15 @@ dependencies = [ "syn 2.0.89", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "primitive-types" version = "0.10.1" @@ -6246,6 +6467,12 @@ dependencies = [ "serde", ] +[[package]] +name = "rangemap" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60fcc7d6849342eff22c4350c8b9a989ee8ceabc4b481253e8946b9fe83d684" + [[package]] name = "rayon" version = "1.10.0" @@ -6662,7 +6889,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d" dependencies = [ "mime_guess", - "sha2", + "sha2 0.10.8", "walkdir", ] @@ -6897,7 +7124,7 @@ dependencies = [ "hmac", "pbkdf2", "salsa20", - "sha2", + "sha2 0.10.8", ] [[package]] @@ -6916,6 +7143,20 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array 0.14.7", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "secp256k1" version = "0.27.0" @@ -7188,6 +7429,19 @@ dependencies = [ "digest 0.10.7", ] +[[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 1.0.0", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + [[package]] name = "sha2" version = "0.10.8" @@ -7249,6 +7503,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ + "digest 0.10.7", "rand_core 0.6.4", ] @@ -7330,7 +7585,7 @@ dependencies = [ "rand_core 0.6.4", "ring 0.17.8", "rustc_version", - "sha2", + "sha2 0.10.8", "subtle", ] @@ -7486,7 +7741,7 @@ dependencies = [ "num-integer", "num-traits", "rfc6979", - "sha2", + "sha2 0.10.8", "starknet-curve 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "starknet-types-core", "zeroize", @@ -7504,7 +7759,7 @@ dependencies = [ "num-integer", "num-traits", "rfc6979", - "sha2", + "sha2 0.10.8", "starknet-curve 0.5.1 (git+https://github.com/xJonathanLEI/starknet-rs?rev=5c676a6)", "starknet-types-core", "zeroize", @@ -7648,6 +7903,12 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "subtle-ng" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" + [[package]] name = "syn" version = "1.0.109" @@ -8844,7 +9105,7 @@ dependencies = [ "indexmap 1.9.3", "more-asserts", "rkyv", - "sha2", + "sha2 0.10.8", "target-lexicon", "thiserror 1.0.69", "xxhash-rust", diff --git a/Cargo.toml b/Cargo.toml index d5dd833b5..4013dd509 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,7 @@ const_format = "0.2.32" curve25519-dalek = "4.1.3" dirs = "5.0.1" ed25519-dalek = "2.1.1" +ed25519-consensus = "2.1.0" either = "1.13.0" eyre = "0.6.12" fixedstr = "0.5.7" @@ -75,7 +76,9 @@ generic-array = "1.0.0" hex = "0.4.3" http = "1.1.0" http-serde = "2.1.1" +ic-agent = "0.39.1" ic-canister-sig-creation = "1.1" +ic-cdk = "0.16" ic-signature-verification = "0.2" indexmap = "2.6.0" jsonwebtoken = "9.3.0" diff --git a/contracts/icp/context-config/Cargo.toml b/contracts/icp/context-config/Cargo.toml index ccce30db5..78655436b 100644 --- a/contracts/icp/context-config/Cargo.toml +++ b/contracts/icp/context-config/Cargo.toml @@ -11,7 +11,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] bs58.workspace = true calimero-context-config = { workspace = true, features = ["icp"] } -candid = "0.10" +candid.workspace = true ed25519-dalek.workspace = true ic-cdk = "0.16" ic-cdk-macros = "0.16" diff --git a/contracts/icp/context-config/src/mutate.rs b/contracts/icp/context-config/src/mutate.rs index 45d08eaf8..910017621 100644 --- a/contracts/icp/context-config/src/mutate.rs +++ b/contracts/icp/context-config/src/mutate.rs @@ -21,11 +21,14 @@ pub async fn mutate(signed_request: ICSigned) -> Result<(), String> { .parse(|r| *r.signer_id) .map_err(|e| format!("Failed to verify signature: {}", e))?; - // Check request timestamp - let current_time = ic_cdk::api::time(); - if current_time.saturating_sub(request.timestamp_ms) > 1000 * 5 { - // 5 seconds threshold - return Err("request expired".to_string()); + // Add debug logging + let current_time = ic_cdk::api::time() / 1_000_000; + let time_diff = current_time.saturating_sub(request.timestamp_ms); + if time_diff > 1000 * 5 { + return Err(format!( + "request expired: diff={}ms, current={}, request={}", + time_diff, current_time, request.timestamp_ms + )); } match request.kind { diff --git a/contracts/icp/context-config/src/query.rs b/contracts/icp/context-config/src/query.rs index 249ef4650..0c5f534a9 100644 --- a/contracts/icp/context-config/src/query.rs +++ b/contracts/icp/context-config/src/query.rs @@ -5,11 +5,10 @@ use calimero_context_config::icp::types::{ICApplication, ICCapability}; use calimero_context_config::repr::ReprTransmute; use calimero_context_config::types::{ContextId, ContextIdentity, SignerId}; use candid::Principal; -use ic_cdk_macros::query; use crate::with_state; -#[query] +#[ic_cdk::query] fn application(context_id: ICRepr) -> ICApplication { with_state(|configs| { let context = configs @@ -21,7 +20,7 @@ fn application(context_id: ICRepr) -> ICApplication { }) } -#[query] +#[ic_cdk::query] fn application_revision(context_id: ICRepr) -> u64 { with_state(|configs| { let context = configs @@ -33,7 +32,7 @@ fn application_revision(context_id: ICRepr) -> u64 { }) } -#[query] +#[ic_cdk::query] fn proxy_contract(context_id: ICRepr) -> Principal { with_state(|configs| { let context = configs @@ -45,7 +44,7 @@ fn proxy_contract(context_id: ICRepr) -> Principal { }) } -#[query] +#[ic_cdk::query] fn members( context_id: ICRepr, offset: usize, @@ -62,7 +61,7 @@ fn members( }) } -#[query] +#[ic_cdk::query] fn has_member(context_id: ICRepr, identity: ICRepr) -> bool { with_state(|configs| { let context = configs @@ -74,7 +73,7 @@ fn has_member(context_id: ICRepr, identity: ICRepr) }) } -#[query] +#[ic_cdk::query] fn members_revision(context_id: ICRepr) -> u64 { with_state(|configs| { let context = configs @@ -86,7 +85,7 @@ fn members_revision(context_id: ICRepr) -> u64 { }) } -#[query] +#[ic_cdk::query] fn privileges( context_id: ICRepr, identities: Vec>, diff --git a/contracts/icp/context-config/tests/integration.rs b/contracts/icp/context-config/tests/integration.rs index 47e9e9bec..6c5def441 100644 --- a/contracts/icp/context-config/tests/integration.rs +++ b/contracts/icp/context-config/tests/integration.rs @@ -808,6 +808,7 @@ fn test_edge_cases() { } } +#[ignore = "we're deprecating timestamp checks, in favor of nonce checks"] #[test] fn test_timestamp_scenarios() { let (pic, canister) = setup(); diff --git a/crates/context/config/Cargo.toml b/crates/context/config/Cargo.toml index 112ae7472..70088b520 100644 --- a/crates/context/config/Cargo.toml +++ b/crates/context/config/Cargo.toml @@ -10,10 +10,12 @@ license.workspace = true bs58.workspace = true borsh = { workspace = true, features = ["derive"] } candid = { workspace = true, optional = true } +ed25519-consensus = { workspace = true, optional = true } ed25519-dalek.workspace = true either = { workspace = true, optional = true } eyre = { workspace = true, optional = true } -hex.workspace = true +hex = { workspace = true, optional = true } +ic-agent = { workspace = true, optional = true } near-crypto = { workspace = true, optional = true } near-jsonrpc-client = { workspace = true, optional = true } near-jsonrpc-primitives = { workspace = true, optional = true } @@ -32,17 +34,34 @@ workspace = true [features] client = [ + "near_client", + "icp_client", + "starknet_client", "dep:either", "dep:eyre", + "reqwest/json", + "url/serde", +] + +near_client = [ "dep:near-crypto", "dep:near-jsonrpc-client", "dep:near-jsonrpc-primitives", "dep:near-primitives", - "reqwest/json", +] + +icp = ["candid"] + +icp_client = [ + "icp", + "dep:ic-agent", + "dep:ed25519-consensus", +] + +starknet_client = [ + "dep:hex", "dep:starknet", "dep:starknet-crypto", "dep:starknet-types-core", - "url/serde", ] -icp = ["candid"] diff --git a/crates/context/config/src/client.rs b/crates/context/config/src/client.rs index 859276e6f..2cba2ae7b 100644 --- a/crates/context/config/src/client.rs +++ b/crates/context/config/src/client.rs @@ -13,15 +13,17 @@ pub mod relayer; pub mod transport; pub mod utils; -use config::{ClientConfig, ClientSelectedSigner, Credentials}; -use protocol::{near, starknet, Protocol}; +use config::{ClientConfig, ClientSelectedSigner, Credentials, LocalConfig}; +use protocol::{icp, near, starknet, Protocol}; use transport::{Both, Transport, TransportArguments, TransportRequest, UnsupportedProtocol}; -pub type AnyTransport = Either< - relayer::RelayerTransport, - Both, starknet::StarknetTransport<'static>>, +pub type LocalTransports = Both< + near::NearTransport<'static>, + Both, icp::IcpTransport<'static>>, >; +pub type AnyTransport = Either; + #[derive(Clone, Debug)] pub struct Client { transport: T, @@ -38,70 +40,114 @@ impl Client { pub fn from_config(config: &ClientConfig) -> Self { let transport = match config.signer.selected { ClientSelectedSigner::Relayer => { - // If the selected signer is Relayer, use the Left variant. Either::Left(relayer::RelayerTransport::new(&relayer::RelayerConfig { url: config.signer.relayer.url.clone(), })) } + ClientSelectedSigner::Local => { + let local_client = + Self::from_local_config(&config.signer.local).expect("validation error"); + + Either::Right(local_client.transport) + } + }; - ClientSelectedSigner::Local => Either::Right(Both { - left: near::NearTransport::new(&near::NearConfig { - networks: config - .signer - .local - .near - .iter() - .map(|(network, config)| { - let (account_id, secret_key) = match &config.credentials { - Credentials::Near(credentials) => ( - credentials.account_id.clone(), - credentials.secret_key.clone(), - ), - Credentials::Starknet(_) => { - panic!("Expected Near credentials but got something else.") - } - }; - ( - network.clone().into(), - near::NetworkConfig { - rpc_url: config.rpc_url.clone(), - account_id, - access_key: secret_key, - }, + Self::new(transport) + } + + pub fn from_local_config(config: &LocalConfig) -> eyre::Result> { + let near_transport = near::NearTransport::new(&near::NearConfig { + networks: config + .near + .iter() + .map(|(network, config)| { + let (account_id, secret_key) = match &config.credentials { + Credentials::Near(credentials) => ( + credentials.account_id.clone(), + credentials.secret_key.clone(), + ), + Credentials::Starknet(_) | Credentials::Icp(_) => { + eyre::bail!( + "Expected Near credentials but got {:?}", + config.credentials ) - }) - .collect(), - }), - right: starknet::StarknetTransport::new(&starknet::StarknetConfig { - networks: config - .signer - .local - .starknet - .iter() - .map(|(network, config)| { - let (account_id, secret_key) = match &config.credentials { - Credentials::Starknet(credentials) => { - (credentials.account_id, credentials.secret_key) - } - Credentials::Near(_) => { - panic!("Expected Starknet credentials but got something else.") - } - }; - ( - network.clone().into(), - starknet::NetworkConfig { - rpc_url: config.rpc_url.clone(), - account_id, - access_key: secret_key, - }, + } + }; + Ok(( + network.clone().into(), + near::NetworkConfig { + rpc_url: config.rpc_url.clone(), + account_id, + access_key: secret_key, + }, + )) + }) + .collect::>()?, + }); + + let starknet_transport = starknet::StarknetTransport::new(&starknet::StarknetConfig { + networks: config + .starknet + .iter() + .map(|(network, config)| { + let (account_id, secret_key) = match &config.credentials { + Credentials::Starknet(credentials) => { + (credentials.account_id, credentials.secret_key) + } + Credentials::Near(_) | Credentials::Icp(_) => { + eyre::bail!( + "Expected Starknet credentials but got {:?}", + config.credentials ) - }) - .collect(), - }), - }), + } + }; + Ok(( + network.clone().into(), + starknet::NetworkConfig { + rpc_url: config.rpc_url.clone(), + account_id, + access_key: secret_key, + }, + )) + }) + .collect::>()?, + }); + + let icp_transport = icp::IcpTransport::new(&icp::IcpConfig { + networks: config + .icp + .iter() + .map(|(network, config)| { + let (account_id, secret_key) = match &config.credentials { + Credentials::Icp(credentials) => ( + credentials.account_id.clone(), + credentials.secret_key.clone(), + ), + Credentials::Near(_) | Credentials::Starknet(_) => { + eyre::bail!("Expected ICP credentials but got {:?}", config.credentials) + } + }; + Ok(( + network.clone().into(), + icp::NetworkConfig { + rpc_url: config.rpc_url.clone(), + account_id, + secret_key, + }, + )) + }) + .collect::>()?, + }); + + let all_transports = Both { + left: near_transport, + right: Both { + left: starknet_transport, + right: icp_transport, + }, }; - Self::new(transport) + Ok(Client::new(all_transports)) } } @@ -180,6 +226,17 @@ impl Client { } } +impl Transport for Client { + type Error = T::Error; + + async fn try_send<'a>( + &self, + args: TransportArguments<'a>, + ) -> Result, Self::Error>, UnsupportedProtocol<'a>> { + self.transport.try_send(args).await + } +} + #[derive(Debug)] pub struct CallClient<'a, T> { protocol: Cow<'a, str>, diff --git a/crates/context/config/src/client/config.rs b/crates/context/config/src/client/config.rs index 1cbe3f580..b09a150e9 100644 --- a/crates/context/config/src/client/config.rs +++ b/crates/context/config/src/client/config.rs @@ -4,6 +4,7 @@ use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; use url::Url; +use crate::client::protocol::icp::Credentials as IcpCredentials; use crate::client::protocol::near::Credentials as NearCredentials; use crate::client::protocol::starknet::Credentials as StarknetCredentials; @@ -24,6 +25,7 @@ pub struct ClientNew { pub struct LocalConfig { pub near: BTreeMap, pub starknet: BTreeMap, + pub icp: BTreeMap, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -62,4 +64,5 @@ pub struct ClientLocalSigner { pub enum Credentials { Near(NearCredentials), Starknet(StarknetCredentials), + Icp(IcpCredentials), } diff --git a/crates/context/config/src/client/env.rs b/crates/context/config/src/client/env.rs index f3657ea15..1a0ed0cfe 100644 --- a/crates/context/config/src/client/env.rs +++ b/crates/context/config/src/client/env.rs @@ -16,6 +16,7 @@ mod utils { #![expect(clippy::type_repetition_in_bounds, reason = "Useful for clarity")] use super::Method; + use crate::client::protocol::icp::Icp; use crate::client::protocol::near::Near; use crate::client::protocol::starknet::Starknet; use crate::client::protocol::Protocol; @@ -30,13 +31,20 @@ mod utils { where M: Method, M: Method, + M: Method, { match &*client.protocol { Near::PROTOCOL => client.send::(params).await, Starknet::PROTOCOL => client.send::(params).await, + Icp::PROTOCOL => client.send::(params).await, unsupported_protocol => Err(ClientError::UnsupportedProtocol { found: unsupported_protocol.to_owned(), - expected: vec![Near::PROTOCOL.into(), Starknet::PROTOCOL.into()].into(), + expected: vec![ + Near::PROTOCOL.into(), + Starknet::PROTOCOL.into(), + Icp::PROTOCOL.into(), + ] + .into(), }), } } diff --git a/crates/context/config/src/client/env/config/mutate.rs b/crates/context/config/src/client/env/config/mutate.rs index 1c9ad5880..51e68c6a9 100644 --- a/crates/context/config/src/client/env/config/mutate.rs +++ b/crates/context/config/src/client/env/config/mutate.rs @@ -1,16 +1,18 @@ use std::fmt::Debug; use ed25519_dalek::{Signer, SigningKey}; -use starknet::core::codec::Encode; +use starknet::core::codec::Encode as StarknetEncode; use starknet::signers::SigningKey as StarknetSigningKey; use starknet_crypto::{poseidon_hash_many, Felt}; use super::types::starknet::{Request as StarknetRequest, Signed as StarknetSigned}; use crate::client::env::{utils, Method}; +use crate::client::protocol::icp::Icp; use crate::client::protocol::near::Near; use crate::client::protocol::starknet::Starknet; use crate::client::transport::Transport; use crate::client::{CallClient, ClientError, Operation}; +use crate::icp::types::{ICRequest, ICSigned}; use crate::repr::{Repr, ReprTransmute}; use crate::types::Signed; use crate::{ContextIdentity, Request, RequestKind}; @@ -126,7 +128,37 @@ impl<'a> Method for Mutate<'a> { } } -impl<'a, T: Transport + Debug> ContextConfigMutateRequest<'a, T> { +impl<'a> Method for Mutate<'a> { + type Returns = (); + + const METHOD: &'static str = "mutate"; + + fn encode(self) -> eyre::Result> { + let signer_sk = SigningKey::from_bytes(&self.signing_key); + + let request = ICRequest::new(signer_sk.verifying_key().rt()?, self.kind.into()); + + let signed = ICSigned::new(request, |b| signer_sk.sign(b))?; + + let encoded = candid::encode_one(&signed)?; + + Ok(encoded) + } + + fn decode(response: Vec) -> eyre::Result { + match candid::decode_one::>(&response) { + Ok(decoded) => match decoded { + Ok(()) => Ok(()), + Err(err_msg) => eyre::bail!("unexpected response {:?}", err_msg), + }, + Err(e) => { + eyre::bail!("unexpected response {:?}", e) + } + } + } +} + +impl<'a, T: Transport> ContextConfigMutateRequest<'a, T> { pub async fn send(self, signing_key: [u8; 32]) -> Result<(), ClientError> { let request = Mutate { signing_key, diff --git a/crates/context/config/src/client/env/config/query/application.rs b/crates/context/config/src/client/env/config/query/application.rs index c91c51ee6..bc361d4e2 100644 --- a/crates/context/config/src/client/env/config/query/application.rs +++ b/crates/context/config/src/client/env/config/query/application.rs @@ -1,13 +1,17 @@ +use candid::{Decode, Encode}; use serde::Serialize; -use starknet::core::codec::{Decode, Encode}; +use starknet::core::codec::{Decode as StarknetDecode, Encode as StarknetEncode}; use starknet_crypto::Felt; use crate::client::env::config::types::starknet::{ Application as StarknetApplication, CallData, FeltPair, }; use crate::client::env::Method; +use crate::client::protocol::icp::Icp; use crate::client::protocol::near::Near; use crate::client::protocol::starknet::Starknet; +use crate::icp::repr::ICRepr; +use crate::icp::types::ICApplication; use crate::repr::Repr; use crate::types::{Application, ApplicationMetadata, ApplicationSource, ContextId}; @@ -91,3 +95,19 @@ impl Method for ApplicationRequest { Ok(application.into()) } } + +impl Method for ApplicationRequest { + type Returns = Application<'static>; + + const METHOD: &'static str = "application"; + + fn encode(self) -> eyre::Result> { + let context_id = ICRepr::new(self.context_id); + Encode!(&context_id).map_err(Into::into) + } + + fn decode(response: Vec) -> eyre::Result { + let decoded = Decode!(&response, ICApplication)?; + Ok(decoded.into()) + } +} diff --git a/crates/context/config/src/client/env/config/query/application_revision.rs b/crates/context/config/src/client/env/config/query/application_revision.rs index 70f39162b..1229ed6f3 100644 --- a/crates/context/config/src/client/env/config/query/application_revision.rs +++ b/crates/context/config/src/client/env/config/query/application_revision.rs @@ -1,10 +1,13 @@ +use candid::{Decode, Encode}; use serde::Serialize; -use starknet::core::codec::Encode; +use starknet::core::codec::Encode as StarknetEncode; use crate::client::env::config::types::starknet::{CallData, FeltPair}; use crate::client::env::Method; +use crate::client::protocol::icp::Icp; use crate::client::protocol::near::Near; use crate::client::protocol::starknet::Starknet; +use crate::icp::repr::ICRepr; use crate::repr::Repr; use crate::types::{ContextId, Revision}; @@ -54,3 +57,18 @@ impl Method for ApplicationRevisionRequest { Ok(revision) } } + +impl Method for ApplicationRevisionRequest { + type Returns = u64; + + const METHOD: &'static str = "application_revision"; + + fn encode(self) -> eyre::Result> { + let context_id = ICRepr::new(*self.context_id); + Encode!(&context_id).map_err(Into::into) + } + + fn decode(response: Vec) -> eyre::Result { + Decode!(&response, Revision).map_err(Into::into) + } +} diff --git a/crates/context/config/src/client/env/config/query/has_member.rs b/crates/context/config/src/client/env/config/query/has_member.rs index beea351e6..20714de13 100644 --- a/crates/context/config/src/client/env/config/query/has_member.rs +++ b/crates/context/config/src/client/env/config/query/has_member.rs @@ -1,10 +1,13 @@ +use candid::{Decode, Encode}; use serde::Serialize; -use starknet::core::codec::Encode; +use starknet::core::codec::Encode as StarknetEncode; use crate::client::env::config::types::starknet::{CallData, FeltPair}; use crate::client::env::Method; +use crate::client::protocol::icp::Icp; use crate::client::protocol::near::Near; use crate::client::protocol::starknet::Starknet; +use crate::icp::repr::ICRepr; use crate::repr::Repr; use crate::types::{ContextId, ContextIdentity}; @@ -70,3 +73,22 @@ impl Method for HasMemberRequest { } } } + +impl Method for HasMemberRequest { + type Returns = bool; + + const METHOD: &'static str = "has_member"; + + fn encode(self) -> eyre::Result> { + let context_id = ICRepr::new(*self.context_id); + let identity = ICRepr::new(*self.identity); + let payload = (context_id, identity); + + Encode!(&payload).map_err(Into::into) + } + + fn decode(response: Vec) -> eyre::Result { + let value = Decode!(&response, Self::Returns)?; + Ok(value) + } +} diff --git a/crates/context/config/src/client/env/config/query/members.rs b/crates/context/config/src/client/env/config/query/members.rs index e688bece3..c96b935f0 100644 --- a/crates/context/config/src/client/env/config/query/members.rs +++ b/crates/context/config/src/client/env/config/query/members.rs @@ -1,15 +1,18 @@ use core::mem; +use candid::{Decode, Encode}; use serde::Serialize; -use starknet::core::codec::{Decode, Encode}; +use starknet::core::codec::{Decode as StarknetDecode, Encode as StarknetEncode}; use starknet_crypto::Felt; use crate::client::env::config::types::starknet::{ CallData, StarknetMembers, StarknetMembersRequest, }; use crate::client::env::Method; +use crate::client::protocol::icp::Icp; use crate::client::protocol::near::Near; use crate::client::protocol::starknet::Starknet; +use crate::icp::repr::ICRepr; use crate::repr::Repr; use crate::types::{ContextId, ContextIdentity}; @@ -96,3 +99,30 @@ impl Method for MembersRequest { Ok(members.into()) } } + +impl Method for MembersRequest { + type Returns = Vec; + + const METHOD: &'static str = "members"; + + fn encode(self) -> eyre::Result> { + let context_id = ICRepr::new(*self.context_id); + + Encode!(&context_id, &self.offset, &self.length).map_err(Into::into) + } + + fn decode(response: Vec) -> eyre::Result { + let members = Decode!(&response, Vec>)?; + + // safety: `ICRepr` is a transparent wrapper around `T` + #[expect( + clippy::transmute_undefined_repr, + reason = "ICRepr is a transparent wrapper around T" + )] + let members = unsafe { + mem::transmute::>, Vec>(members) + }; + + Ok(members) + } +} diff --git a/crates/context/config/src/client/env/config/query/members_revision.rs b/crates/context/config/src/client/env/config/query/members_revision.rs index dd2738fef..9771d2e3c 100644 --- a/crates/context/config/src/client/env/config/query/members_revision.rs +++ b/crates/context/config/src/client/env/config/query/members_revision.rs @@ -1,10 +1,13 @@ +use candid::{Decode, Encode}; use serde::Serialize; -use starknet::core::codec::Encode; +use starknet::core::codec::Encode as StarknetEncode; use crate::client::env::config::types::starknet::{CallData, ContextId as StarknetContextId}; use crate::client::env::Method; +use crate::client::protocol::icp::Icp; use crate::client::protocol::near::Near; use crate::client::protocol::starknet::Starknet; +use crate::icp::repr::ICRepr; use crate::repr::Repr; use crate::types::{ContextId, Revision}; @@ -63,3 +66,19 @@ impl Method for MembersRevisionRequest { Ok(revision) } } + +impl Method for MembersRevisionRequest { + type Returns = Revision; + + const METHOD: &'static str = "members_revision"; + + fn encode(self) -> eyre::Result> { + let context_id = ICRepr::new(*self.context_id); + Encode!(&context_id).map_err(Into::into) + } + + fn decode(response: Vec) -> eyre::Result { + let value = Decode!(&response, Self::Returns)?; + Ok(value) + } +} diff --git a/crates/context/config/src/client/env/config/query/privileges.rs b/crates/context/config/src/client/env/config/query/privileges.rs index 496a97319..c97af3a45 100644 --- a/crates/context/config/src/client/env/config/query/privileges.rs +++ b/crates/context/config/src/client/env/config/query/privileges.rs @@ -1,8 +1,9 @@ use core::{mem, ptr}; use std::collections::BTreeMap; +use candid::{Decode, Encode}; use serde::Serialize; -use starknet::core::codec::{Decode, Encode, FeltWriter}; +use starknet::core::codec::{Decode as StarknetDecode, Encode as StarknetEncode, FeltWriter}; use starknet_crypto::Felt; use crate::client::env::config::types::starknet::{ @@ -10,8 +11,11 @@ use crate::client::env::config::types::starknet::{ StarknetPrivileges, }; use crate::client::env::Method; +use crate::client::protocol::icp::Icp; use crate::client::protocol::near::Near; use crate::client::protocol::starknet::Starknet; +use crate::icp::repr::ICRepr; +use crate::icp::types::ICCapability; use crate::repr::Repr; use crate::types::{Capability, ContextId, ContextIdentity, SignerId}; @@ -23,6 +27,7 @@ pub(super) struct PrivilegesRequest<'a> { impl<'a> PrivilegesRequest<'a> { pub const fn new(context_id: ContextId, identities: &'a [ContextIdentity]) -> Self { + // safety: `Repr` is a transparent wrapper around `T` let identities = unsafe { &*(ptr::from_ref::<[ContextIdentity]>(identities) as *const [Repr]) }; @@ -126,3 +131,35 @@ impl<'a> Method for PrivilegesRequest<'a> { Ok(privileges.into()) } } + +impl<'a> Method for PrivilegesRequest<'a> { + type Returns = BTreeMap>; + + const METHOD: &'static str = "privileges"; + + fn encode(self) -> eyre::Result> { + let context_id = ICRepr::new(*self.context_id); + + // safety: + // `Repr` is a transparent wrapper around `T` and + // `ICRepr` is a transparent wrapper around `T` + + let identities = unsafe { + &*(ptr::from_ref::<[Repr]>(self.identities) + as *const [ICRepr]) + }; + + let payload = (context_id, identities); + + Encode!(&payload).map_err(Into::into) + } + + fn decode(response: Vec) -> eyre::Result { + let decoded = Decode!(&response, BTreeMap, Vec>)?; + + Ok(decoded + .into_iter() + .map(|(k, v)| (*k, v.into_iter().map(Into::into).collect())) + .collect()) + } +} diff --git a/crates/context/config/src/client/env/config/query/proxy_contract.rs b/crates/context/config/src/client/env/config/query/proxy_contract.rs index 06fc9ed12..79c087af7 100644 --- a/crates/context/config/src/client/env/config/query/proxy_contract.rs +++ b/crates/context/config/src/client/env/config/query/proxy_contract.rs @@ -1,11 +1,14 @@ +use candid::{Decode, Encode, Principal}; use serde::Serialize; -use starknet::core::codec::Encode; +use starknet::core::codec::Encode as StarknetEncode; use starknet_crypto::Felt; use crate::client::env::config::types::starknet::{CallData, FeltPair}; use crate::client::env::Method; +use crate::client::protocol::icp::Icp; use crate::client::protocol::near::Near; use crate::client::protocol::starknet::Starknet; +use crate::icp::repr::ICRepr; use crate::repr::Repr; use crate::types::ContextId; @@ -57,3 +60,20 @@ impl Method for ProxyContractRequest { Ok(format!("0x{:x}", felt)) } } + +impl Method for ProxyContractRequest { + const METHOD: &'static str = "proxy_contract"; + + type Returns = String; + + fn encode(self) -> eyre::Result> { + let context_id = ICRepr::new(*self.context_id); + Encode!(&context_id).map_err(Into::into) + } + + fn decode(response: Vec) -> eyre::Result { + let value: Principal = Decode!(&response, Principal)?; + let value_as_string = value.to_text(); + Ok(value_as_string) + } +} diff --git a/crates/context/config/src/client/env/config/types/starknet.rs b/crates/context/config/src/client/env/config/types/starknet.rs index 23224f989..b221e9ea8 100644 --- a/crates/context/config/src/client/env/config/types/starknet.rs +++ b/crates/context/config/src/client/env/config/types/starknet.rs @@ -1,6 +1,5 @@ use std::collections::BTreeMap; -use hex; use starknet::core::codec::{Decode, Encode, Error, FeltWriter}; use starknet::core::types::Felt; diff --git a/crates/context/config/src/client/env/proxy/mutate.rs b/crates/context/config/src/client/env/proxy/mutate.rs index 70620d404..da54490c6 100644 --- a/crates/context/config/src/client/env/proxy/mutate.rs +++ b/crates/context/config/src/client/env/proxy/mutate.rs @@ -1,3 +1,4 @@ +use candid::Decode; use ed25519_dalek::{Signer, SigningKey}; use starknet::core::codec::Encode; use starknet::signers::SigningKey as StarknetSigningKey; @@ -5,10 +6,13 @@ use starknet_crypto::{poseidon_hash_many, Felt}; use super::types::starknet::{StarknetProxyMutateRequest, StarknetSignedRequest}; use crate::client::env::{utils, Method}; +use crate::client::protocol::icp::Icp; use crate::client::protocol::near::Near; use crate::client::protocol::starknet::Starknet; use crate::client::transport::Transport; use crate::client::{CallClient, ClientError, Operation}; +use crate::icp::types::ICSigned; +use crate::icp::{ICProposalWithApprovals, ICProxyMutateRequest}; use crate::repr::ReprTransmute; use crate::types::Signed; use crate::{ProposalWithApprovals, ProxyMutateRequest, Repr}; @@ -118,6 +122,30 @@ impl Method for Mutate { } } +impl Method for Mutate { + type Returns = Option; + + const METHOD: &'static str = "mutate"; + + fn encode(self) -> eyre::Result> { + let signer_sk = SigningKey::from_bytes(&self.signing_key); + + let payload: ICProxyMutateRequest = + self.raw_request.try_into().map_err(eyre::Report::msg)?; + + let signed = ICSigned::new(payload, |b| signer_sk.sign(b))?; + + let encoded = candid::encode_one(&signed)?; + + Ok(encoded) + } + + fn decode(response: Vec) -> eyre::Result { + let decoded = Decode!(&response, Option)?; + Ok(decoded.map(Into::into)) + } +} + impl<'a, T: Transport> ContextProxyMutateRequest<'a, T> { pub async fn send( self, diff --git a/crates/context/config/src/client/env/proxy/query/active_proposals.rs b/crates/context/config/src/client/env/proxy/query/active_proposals.rs index 5e087f715..59aa23b6d 100644 --- a/crates/context/config/src/client/env/proxy/query/active_proposals.rs +++ b/crates/context/config/src/client/env/proxy/query/active_proposals.rs @@ -1,10 +1,12 @@ +use candid::{CandidType, Decode, Encode}; use serde::Serialize; use crate::client::env::Method; +use crate::client::protocol::icp::Icp; use crate::client::protocol::near::Near; use crate::client::protocol::starknet::Starknet; -#[derive(Copy, Clone, Debug, Serialize)] +#[derive(Copy, Clone, Debug, Serialize, CandidType)] pub(super) struct ActiveProposalRequest; impl Method for ActiveProposalRequest { @@ -52,3 +54,18 @@ impl Method for ActiveProposalRequest { Ok(value) } } + +impl Method for ActiveProposalRequest { + const METHOD: &'static str = "get_active_proposals_limit"; + + type Returns = u16; + + fn encode(self) -> eyre::Result> { + Encode!(&self).map_err(Into::into) + } + + fn decode(response: Vec) -> eyre::Result { + let value = Decode!(&response, Self::Returns)?; + Ok(value) + } +} diff --git a/crates/context/config/src/client/env/proxy/query/proposal.rs b/crates/context/config/src/client/env/proxy/query/proposal.rs index 136bef02c..0d539d29b 100644 --- a/crates/context/config/src/client/env/proxy/query/proposal.rs +++ b/crates/context/config/src/client/env/proxy/query/proposal.rs @@ -1,14 +1,18 @@ +use candid::{Decode, Encode}; use serde::Serialize; -use starknet::core::codec::{Decode, Encode}; +use starknet::core::codec::{Decode as StarknetDecode, Encode as StarknetEncode}; use starknet_crypto::Felt; -use super::ProposalId; use crate::client::env::proxy::starknet::CallData; use crate::client::env::proxy::types::starknet::{StarknetProposal, StarknetProposalId}; use crate::client::env::Method; +use crate::client::protocol::icp::Icp; use crate::client::protocol::near::Near; use crate::client::protocol::starknet::Starknet; +use crate::icp::repr::ICRepr; +use crate::icp::ICProposal; use crate::repr::Repr; +use crate::types::ProposalId; use crate::Proposal; #[derive(Clone, Debug, Serialize)] @@ -89,3 +93,19 @@ impl Method for ProposalRequest { } } } + +impl Method for ProposalRequest { + const METHOD: &'static str = "proposals"; + + type Returns = Option; + + fn encode(self) -> eyre::Result> { + let payload = ICRepr::new(*self.proposal_id); + Encode!(&payload).map_err(Into::into) + } + + fn decode(response: Vec) -> eyre::Result { + let decoded = Decode!(&response, Option)?; + Ok(decoded.map(Into::into)) + } +} diff --git a/crates/context/config/src/client/env/proxy/query/proposal_approvals.rs b/crates/context/config/src/client/env/proxy/query/proposal_approvals.rs index 96c73c5ed..a7cc35bee 100644 --- a/crates/context/config/src/client/env/proxy/query/proposal_approvals.rs +++ b/crates/context/config/src/client/env/proxy/query/proposal_approvals.rs @@ -1,19 +1,22 @@ -use serde::Serialize; -use starknet::core::codec::{Decode, Encode}; +use candid::{Decode, Encode}; +use serde::{Deserialize, Serialize}; +use starknet::core::codec::{Decode as StarknetDecode, Encode as StarknetEncode}; use starknet::core::types::Felt; -use super::ProposalId; use crate::client::env::proxy::starknet::CallData; use crate::client::env::proxy::types::starknet::{ StarknetProposalId, StarknetProposalWithApprovals, }; use crate::client::env::Method; +use crate::client::protocol::icp::Icp; use crate::client::protocol::near::Near; use crate::client::protocol::starknet::Starknet; -use crate::repr::Repr; -use crate::ProposalWithApprovals; +use crate::icp::repr::ICRepr; +use crate::icp::ICProposalWithApprovals; +use crate::types::ProposalId; +use crate::{ProposalWithApprovals, Repr}; -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub(super) struct ProposalApprovalsRequest { pub(super) proposal_id: Repr, } @@ -81,3 +84,19 @@ impl Method for ProposalApprovalsRequest { Ok(approvals.into()) } } + +impl Method for ProposalApprovalsRequest { + const METHOD: &'static str = "get_confirmations_count"; + + type Returns = ProposalWithApprovals; + + fn encode(self) -> eyre::Result> { + let payload = ICRepr::new(*self.proposal_id); + Encode!(&payload).map_err(Into::into) + } + + fn decode(response: Vec) -> eyre::Result { + let decoded = Decode!(&response, ICProposalWithApprovals)?; + Ok(decoded.into()) + } +} diff --git a/crates/context/config/src/client/env/proxy/query/proposal_approvers.rs b/crates/context/config/src/client/env/proxy/query/proposal_approvers.rs index 03e07124b..d4667340b 100644 --- a/crates/context/config/src/client/env/proxy/query/proposal_approvers.rs +++ b/crates/context/config/src/client/env/proxy/query/proposal_approvers.rs @@ -1,17 +1,19 @@ use std::mem; +use candid::{Decode, Encode}; use serde::Serialize; -use starknet::core::codec::{Decode, Encode}; +use starknet::core::codec::{Decode as StarknetDecode, Encode as StarknetEncode}; use starknet::core::types::Felt; -use super::ProposalId; use crate::client::env::proxy::starknet::CallData; use crate::client::env::proxy::types::starknet::{StarknetApprovers, StarknetProposalId}; use crate::client::env::Method; +use crate::client::protocol::icp::Icp; use crate::client::protocol::near::Near; use crate::client::protocol::starknet::Starknet; +use crate::icp::repr::ICRepr; use crate::repr::Repr; -use crate::types::ContextIdentity; +use crate::types::{ContextIdentity, ProposalId}; #[derive(Clone, Debug, Serialize)] pub(super) struct ProposalApproversRequest { @@ -90,3 +92,29 @@ impl Method for ProposalApproversRequest { Ok(approvers.into()) } } + +impl Method for ProposalApproversRequest { + const METHOD: &'static str = "proposal_approvers"; + + type Returns = Vec; + + fn encode(self) -> eyre::Result> { + let payload = ICRepr::new(*self.proposal_id); + Encode!(&payload).map_err(Into::into) + } + + fn decode(response: Vec) -> eyre::Result { + let identities = Decode!(&response, Vec>)?; + + // safety: `ICRepr` is a transparent wrapper around `T` + #[expect( + clippy::transmute_undefined_repr, + reason = "ICRepr is a transparent wrapper around T" + )] + let identities = unsafe { + mem::transmute::>, Vec>(identities) + }; + + Ok(identities) + } +} diff --git a/crates/context/config/src/client/env/proxy/query/proposals.rs b/crates/context/config/src/client/env/proxy/query/proposals.rs index 1b8ddba91..f76d482dc 100644 --- a/crates/context/config/src/client/env/proxy/query/proposals.rs +++ b/crates/context/config/src/client/env/proxy/query/proposals.rs @@ -1,11 +1,14 @@ +use candid::{Decode, Encode}; use serde::Serialize; -use starknet::core::codec::{Decode, Encode}; +use starknet::core::codec::{Decode as StarknetDecode, Encode as StarknetEncode}; use starknet_crypto::Felt; use crate::client::env::proxy::starknet::{CallData, StarknetProposals, StarknetProposalsRequest}; use crate::client::env::Method; +use crate::client::protocol::icp::Icp; use crate::client::protocol::near::Near; use crate::client::protocol::starknet::Starknet; +use crate::icp::ICProposal; use crate::Proposal; #[derive(Copy, Clone, Debug, Serialize)] @@ -82,3 +85,21 @@ impl Method for ProposalsRequest { Ok(proposals.into()) } } + +impl Method for ProposalsRequest { + const METHOD: &'static str = "proposals"; + + type Returns = Vec; + + fn encode(self) -> eyre::Result> { + Encode!(&self.offset, &self.length).map_err(Into::into) + } + + fn decode(response: Vec) -> eyre::Result { + let proposals = Decode!(&response, Vec)?; + + let proposals = proposals.into_iter().map(|id| id.into()).collect(); + + Ok(proposals) + } +} diff --git a/crates/context/config/src/client/protocol.rs b/crates/context/config/src/client/protocol.rs index ceb4b50f5..e3c3ad7b4 100644 --- a/crates/context/config/src/client/protocol.rs +++ b/crates/context/config/src/client/protocol.rs @@ -1,3 +1,4 @@ +pub mod icp; pub mod near; pub mod starknet; diff --git a/crates/context/config/src/client/protocol/icp.rs b/crates/context/config/src/client/protocol/icp.rs new file mode 100644 index 000000000..11af161e0 --- /dev/null +++ b/crates/context/config/src/client/protocol/icp.rs @@ -0,0 +1,247 @@ +use std::borrow::Cow; +use std::collections::BTreeMap; + +use ed25519_consensus::SigningKey; +use ic_agent::export::Principal; +use ic_agent::identity::BasicIdentity; +use ic_agent::Agent; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +use url::Url; + +use super::Protocol; +use crate::client::transport::{ + AssociatedTransport, Operation, ProtocolTransport, TransportRequest, +}; + +#[derive(Copy, Clone, Debug)] +pub enum Icp {} + +impl Protocol for Icp { + const PROTOCOL: &'static str = "icp"; +} + +impl AssociatedTransport for IcpTransport<'_> { + type Protocol = Icp; +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(try_from = "serde_creds::Credentials")] +pub struct Credentials { + pub account_id: Principal, + pub public_key: String, + pub secret_key: String, +} + +mod serde_creds { + use candid::Principal; + use hex::FromHexError; + use serde::{Deserialize, Serialize}; + use thiserror::Error; + + #[derive(Debug, Deserialize, Serialize)] + pub struct Credentials { + account_id: Principal, + public_key: String, + secret_key: String, + } + + #[derive(Clone, Debug, Error)] + pub enum CredentialsError { + #[error("failed to parse SigningKey from hex")] + ParseError(#[from] FromHexError), + #[error("failed to parse SigningKey from string")] + IntoError(String), + } + + impl TryFrom for super::Credentials { + type Error = CredentialsError; + + fn try_from(creds: Credentials) -> Result { + Ok(Self { + account_id: creds.account_id, + public_key: creds.public_key, + secret_key: creds.secret_key, + }) + } + } +} + +#[derive(Debug)] +pub struct NetworkConfig { + pub rpc_url: Url, + pub account_id: Principal, + pub secret_key: String, +} + +#[derive(Debug)] +pub struct IcpConfig<'a> { + pub networks: BTreeMap, NetworkConfig>, +} + +#[derive(Clone, Debug)] +struct Network { + client: Agent, + _account_id: Principal, + _secret_key: String, +} + +#[derive(Clone, Debug)] +pub struct IcpTransport<'a> { + networks: BTreeMap, Network>, +} + +impl<'a> IcpTransport<'a> { + #[must_use] + pub fn new(config: &IcpConfig<'a>) -> Self { + let mut networks: BTreeMap, Network> = BTreeMap::new(); + + for (network_id, network_config) in &config.networks { + let secret_key_byes = hex::decode(network_config.secret_key.clone()).unwrap(); + let secret_key_array: [u8; 32] = secret_key_byes.try_into().unwrap(); + let secret_key: SigningKey = secret_key_array.into(); + + let identity = BasicIdentity::from_signing_key(secret_key.clone()); + + let client = Agent::builder() + .with_url(network_config.rpc_url.clone()) + .with_identity(identity) + .build() + .unwrap(); + + let _ignored = networks.insert( + network_id.clone(), + Network { + client, + _account_id: network_config.account_id.clone(), + _secret_key: network_config.secret_key.clone(), + }, + ); + } + + Self { networks } + } +} + +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum IcpError { + #[error("unknown network `{0}`")] + UnknownNetwork(String), + #[error("invalid canister id `{0}`")] + InvalidCanisterId(String), + #[error("invalid response from RPC while {operation}")] + InvalidResponse { operation: ErrorOperation }, + #[error( + "access key does not have permission to call method `{method}` on canister {canister}" + )] + NotPermittedToCallMethod { canister: String, method: String }, + #[error("transaction timed out")] + TransactionTimeout, + #[error("error while {operation}: {reason}")] + Custom { + operation: ErrorOperation, + reason: String, + }, +} + +#[derive(Copy, Clone, Debug, Error)] +#[non_exhaustive] +pub enum ErrorOperation { + #[error("querying canister")] + Query, + #[error("updating canister")] + Mutate, +} + +impl ProtocolTransport for IcpTransport<'_> { + type Error = IcpError; + + async fn send( + &self, + request: TransportRequest<'_>, + payload: Vec, + ) -> Result, Self::Error> { + let Some(network) = self.networks.get(&request.network_id) else { + return Err(IcpError::UnknownNetwork(request.network_id.into_owned())); + }; + + let canister_id = Principal::from_text(&request.contract_id) + .map_err(|_| IcpError::InvalidCanisterId(request.contract_id.into_owned()))?; + + match request.operation { + Operation::Read { method } => { + network + .query(&canister_id, method.into_owned(), payload) + .await + } + Operation::Write { method } => { + network + .mutate(&canister_id, method.into_owned(), payload) + .await + } + } + } +} + +impl Network { + async fn query( + &self, + canister_id: &Principal, + method: String, + args: Vec, + ) -> Result, IcpError> { + self.client + .fetch_root_key() + .await + .map_err(|_| IcpError::Custom { + operation: ErrorOperation::Query, + reason: "Failed to fetch root key".to_owned(), + })?; + + let response = self + .client + .query(canister_id, method) + .with_arg(args) + .call() + .await; + + response.map_or( + Err(IcpError::Custom { + operation: ErrorOperation::Query, + reason: "Error while quering".to_owned(), + }), + |response| Ok(response), + ) + } + + async fn mutate( + &self, + canister_id: &Principal, + method: String, + args: Vec, + ) -> Result, IcpError> { + self.client + .fetch_root_key() + .await + .map_err(|_| IcpError::Custom { + operation: ErrorOperation::Mutate, + reason: "Failed to fetch root key".to_owned(), + })?; + + let response = self + .client + .update(canister_id, method) + .with_arg(args) + .call_and_wait() + .await; + + match response { + Ok(data) => Ok(data), + Err(err) => Err(IcpError::Custom { + operation: ErrorOperation::Mutate, + reason: err.to_string(), + }), + } + } +} diff --git a/crates/context/config/src/icp.rs b/crates/context/config/src/icp.rs index 46f7b383d..028301993 100644 --- a/crates/context/config/src/icp.rs +++ b/crates/context/config/src/icp.rs @@ -6,7 +6,9 @@ pub mod types; use repr::ICRepr; +use crate::repr::ReprTransmute; use crate::types::{ProposalId, SignerId}; +use crate::{Proposal, ProposalAction, ProposalWithApprovals, ProxyMutateRequest}; #[derive(CandidType, Deserialize, Clone, Debug, PartialEq)] pub enum ICProposalAction { @@ -32,6 +34,90 @@ pub enum ICProposalAction { }, } +impl TryFrom for ICProposalAction { + type Error = String; + + fn try_from(action: ProposalAction) -> Result { + let action = match action { + ProposalAction::ExternalFunctionCall { + receiver_id, + method_name, + args, + deposit, + gas: _, + } => ICProposalAction::ExternalFunctionCall { + receiver_id: receiver_id + .parse::() + .map_err(|e| e.to_string())?, + method_name, + args, + deposit, + }, + ProposalAction::Transfer { + receiver_id, + amount, + } => ICProposalAction::Transfer { + receiver_id: receiver_id + .parse::() + .map_err(|e| e.to_string())?, + amount, + }, + ProposalAction::SetNumApprovals { num_approvals } => { + ICProposalAction::SetNumApprovals { num_approvals } + } + ProposalAction::SetActiveProposalsLimit { + active_proposals_limit, + } => ICProposalAction::SetActiveProposalsLimit { + active_proposals_limit, + }, + ProposalAction::SetContextValue { key, value } => ICProposalAction::SetContextValue { + key: key.into(), + value: value.into(), + }, + }; + + Ok(action) + } +} + +impl From for ProposalAction { + fn from(action: ICProposalAction) -> Self { + match action { + ICProposalAction::ExternalFunctionCall { + receiver_id, + method_name, + args, + deposit, + } => ProposalAction::ExternalFunctionCall { + receiver_id: receiver_id.to_text(), + method_name, + args, + deposit, + gas: 0, + }, + ICProposalAction::Transfer { + receiver_id, + amount, + } => ProposalAction::Transfer { + receiver_id: receiver_id.to_text(), + amount, + }, + ICProposalAction::SetNumApprovals { num_approvals } => { + ProposalAction::SetNumApprovals { num_approvals } + } + ICProposalAction::SetActiveProposalsLimit { + active_proposals_limit, + } => ProposalAction::SetActiveProposalsLimit { + active_proposals_limit, + }, + ICProposalAction::SetContextValue { key, value } => ProposalAction::SetContextValue { + key: key.into_boxed_slice(), + value: value.into_boxed_slice(), + }, + } + } +} + #[derive(CandidType, Deserialize, Clone, Debug, PartialEq)] pub struct ICProposal { pub id: ICRepr, @@ -39,12 +125,31 @@ pub struct ICProposal { pub actions: Vec, } +impl From for Proposal { + fn from(proposal: ICProposal) -> Self { + Proposal { + id: proposal.id.rt().expect("infallible conversion"), + author_id: proposal.author_id.rt().expect("infallible conversion"), + actions: proposal.actions.into_iter().map(Into::into).collect(), + } + } +} + #[derive(CandidType, Deserialize, Copy, Clone, Debug)] pub struct ICProposalWithApprovals { pub proposal_id: ICRepr, pub num_approvals: usize, } +impl From for ProposalWithApprovals { + fn from(proposal: ICProposalWithApprovals) -> Self { + ProposalWithApprovals { + proposal_id: proposal.proposal_id.rt().expect("infallible conversion"), + num_approvals: proposal.num_approvals, + } + } +} + #[derive(CandidType, Deserialize, Copy, Clone, Debug)] pub struct ICProposalApprovalWithSigner { pub proposal_id: ICRepr, @@ -61,3 +166,32 @@ pub enum ICProxyMutateRequest { approval: ICProposalApprovalWithSigner, }, } + +impl TryFrom for ICProxyMutateRequest { + type Error = String; + + fn try_from(request: ProxyMutateRequest) -> Result { + let request = match request { + ProxyMutateRequest::Propose { proposal } => ICProxyMutateRequest::Propose { + proposal: ICProposal { + id: proposal.id.rt().map_err(|e| e.to_string())?, + author_id: proposal.author_id.rt().map_err(|e| e.to_string())?, + actions: proposal + .actions + .into_iter() + .map(TryInto::try_into) + .collect::>()?, + }, + }, + ProxyMutateRequest::Approve { approval } => ICProxyMutateRequest::Approve { + approval: ICProposalApprovalWithSigner { + proposal_id: approval.proposal_id.rt().map_err(|e| e.to_string())?, + signer_id: approval.signer_id.rt().map_err(|e| e.to_string())?, + added_timestamp: approval.added_timestamp, + }, + }, + }; + + Ok(request) + } +} diff --git a/crates/context/config/src/icp/repr.rs b/crates/context/config/src/icp/repr.rs index 9da560334..68c0fd1f5 100644 --- a/crates/context/config/src/icp/repr.rs +++ b/crates/context/config/src/icp/repr.rs @@ -9,6 +9,7 @@ use crate::repr::{self, ReprBytes}; #[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd)] #[serde(transparent)] +#[repr(transparent)] pub struct ICRepr { #[serde(bound = "for<'a> T: ReprBytes>")] #[serde(deserialize_with = "repr_deserialize")] diff --git a/crates/context/config/src/icp/types.rs b/crates/context/config/src/icp/types.rs index 0373af479..94ab5b7bb 100644 --- a/crates/context/config/src/icp/types.rs +++ b/crates/context/config/src/icp/types.rs @@ -1,5 +1,6 @@ use std::borrow::Cow; use std::marker::PhantomData; +use std::time::{SystemTime, UNIX_EPOCH}; use candid::CandidType; use ed25519_dalek::{Verifier, VerifyingKey}; @@ -13,6 +14,7 @@ use crate::types::{ Application, ApplicationId, ApplicationMetadata, ApplicationSource, BlobId, Capability, ContextId, ContextIdentity, IntoResult, SignerId, }; +use crate::{ContextRequest, ContextRequestKind, RequestKind}; #[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)] pub struct ICApplication { @@ -53,6 +55,15 @@ pub struct ICContextRequest { pub kind: ICContextRequestKind, } +impl<'a> From> for ICContextRequest { + fn from(value: ContextRequest<'a>) -> Self { + Self { + context_id: value.context_id.rt().expect("infallible conversion"), + kind: value.kind.into(), + } + } +} + #[derive(CandidType, Deserialize, Debug, Copy, Clone, PartialEq, Eq)] pub enum ICCapability { ManageApplication, @@ -84,11 +95,67 @@ pub enum ICContextRequestKind { UpdateProxyContract, } +impl From> for ICContextRequestKind { + fn from(value: ContextRequestKind<'_>) -> Self { + match value { + ContextRequestKind::Add { + author_id, + application, + } => ICContextRequestKind::Add { + author_id: author_id.rt().expect("infallible conversion"), + application: application.into(), + }, + ContextRequestKind::UpdateApplication { application } => { + ICContextRequestKind::UpdateApplication { + application: application.into(), + } + } + ContextRequestKind::AddMembers { members } => ICContextRequestKind::AddMembers { + members: members + .into_owned() + .into_iter() + .map(|m| m.rt().expect("infallible conversion")) + .collect(), + }, + ContextRequestKind::RemoveMembers { members } => ICContextRequestKind::RemoveMembers { + members: members + .into_owned() + .into_iter() + .map(|m| m.rt().expect("infallible conversion")) + .collect(), + }, + ContextRequestKind::Grant { capabilities } => ICContextRequestKind::Grant { + capabilities: capabilities + .into_owned() + .into_iter() + .map(|(id, cap)| (id.rt().expect("infallible conversion"), cap.into())) + .collect(), + }, + ContextRequestKind::Revoke { capabilities } => ICContextRequestKind::Revoke { + capabilities: capabilities + .into_owned() + .into_iter() + .map(|(id, cap)| (id.rt().expect("infallible conversion"), cap.into())) + .collect(), + }, + ContextRequestKind::UpdateProxyContract => ICContextRequestKind::UpdateProxyContract, + } + } +} + #[derive(CandidType, Deserialize, Debug, Clone)] pub enum ICRequestKind { Context(ICContextRequest), } +impl<'a> From> for ICRequestKind { + fn from(value: RequestKind<'a>) -> Self { + match value { + RequestKind::Context(context) => ICRequestKind::Context(context.into()), + } + } +} + #[derive(CandidType, Deserialize, Debug, Clone)] pub struct ICRequest { pub kind: ICRequestKind, @@ -101,7 +168,10 @@ impl ICRequest { Self { signer_id: ICRepr::new(signer_id), kind, - timestamp_ms: 0, // Default timestamp for tests + timestamp_ms: SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_millis() as u64, } } } diff --git a/crates/context/config/src/types.rs b/crates/context/config/src/types.rs index a193e3547..5c0ed74e7 100644 --- a/crates/context/config/src/types.rs +++ b/crates/context/config/src/types.rs @@ -56,9 +56,20 @@ impl<'a> Application<'a> { } #[derive( - Eq, Ord, Copy, Debug, Clone, PartialEq, PartialOrd, BorshSerialize, BorshDeserialize, Hash, + Eq, + Ord, + Copy, + Debug, + Clone, + PartialEq, + PartialOrd, + BorshSerialize, + BorshDeserialize, + Hash, + Serialize, + Deserialize, )] -pub struct Identity([u8; 32]); +pub struct Identity(pub(crate) [u8; 32]); impl ReprBytes for Identity { type EncodeBytes<'a> = [u8; 32]; @@ -78,10 +89,16 @@ impl ReprBytes for Identity { } } +impl From<[u8; 32]> for Identity { + fn from(value: [u8; 32]) -> Self { + Identity(value) + } +} + #[derive( Eq, Ord, Copy, Debug, Clone, PartialEq, PartialOrd, BorshSerialize, BorshDeserialize, Hash, )] -pub struct SignerId(Identity); +pub struct SignerId(pub(crate) Identity); impl ReprBytes for SignerId { type EncodeBytes<'a> = [u8; 32]; @@ -250,7 +267,7 @@ impl ReprBytes for Signature { } #[derive(Eq, Ord, Copy, Debug, Clone, PartialEq, PartialOrd, BorshSerialize, BorshDeserialize)] -pub struct ProposalId(Identity); +pub struct ProposalId(pub(crate) Identity); impl ReprBytes for ProposalId { type EncodeBytes<'a> = [u8; 32]; diff --git a/crates/merod/Cargo.toml b/crates/merod/Cargo.toml index fe7f58a8a..f7eb8d2f1 100644 --- a/crates/merod/Cargo.toml +++ b/crates/merod/Cargo.toml @@ -14,13 +14,16 @@ camino = { workspace = true, features = ["serde1"] } clap = { workspace = true, features = ["env", "derive"] } color-eyre.workspace = true const_format.workspace = true +ic-agent.workspace = true dirs.workspace = true +ed25519-consensus.workspace = true eyre.workspace = true futures-util.workspace = true hex.workspace = true libp2p.workspace = true multiaddr.workspace = true near-crypto.workspace = true +rand.workspace = true starknet.workspace = true tokio = { workspace = true, features = ["io-std", "macros"] } toml_edit.workspace = true diff --git a/crates/merod/src/cli/init.rs b/crates/merod/src/cli/init.rs index 751a32532..78e92dc38 100644 --- a/crates/merod/src/cli/init.rs +++ b/crates/merod/src/cli/init.rs @@ -12,7 +12,7 @@ use calimero_context_config::client::config::{ ClientSigner, Credentials, LocalConfig, }; use calimero_context_config::client::protocol::{ - near as near_protocol, starknet as starknet_protocol, + icp as icp_protocol, near as near_protocol, starknet as starknet_protocol, }; use calimero_network::config::{ BootstrapConfig, BootstrapNodes, DiscoveryConfig, RelayConfig, RendezvousConfig, SwarmConfig, @@ -24,10 +24,15 @@ use calimero_store::config::StoreConfig; use calimero_store::db::RocksDB; use calimero_store::Store; use clap::{Parser, ValueEnum}; +use ed25519_consensus::SigningKey as IcpSigningKey; use eyre::{bail, Result as EyreResult, WrapErr}; +use hex::encode; +use ic_agent::export::Principal; +use ic_agent::identity::{BasicIdentity, Identity}; use libp2p::identity::Keypair; use multiaddr::{Multiaddr, Protocol}; use near_crypto::{KeyType, SecretKey}; +use rand::rngs::OsRng; use starknet::signers::SigningKey; use tracing::{info, warn}; use url::Url; @@ -38,6 +43,7 @@ use crate::{cli, defaults}; pub enum ConfigProtocol { Near, Starknet, + Icp, } impl ConfigProtocol { @@ -45,6 +51,7 @@ impl ConfigProtocol { match self { ConfigProtocol::Near => "near", ConfigProtocol::Starknet => "starknet", + ConfigProtocol::Icp => "icp", } } } @@ -255,12 +262,31 @@ impl InitCommand { ] .into_iter() .collect(), + icp: [ + ( + "ic".to_owned(), + generate_local_signer( + "https://ic0.app".parse()?, + ConfigProtocol::Icp, + )?, + ), + ( + "local".to_owned(), + generate_local_signer( + "http://127.0.0.1:4943".parse()?, + ConfigProtocol::Icp, + )?, + ), + ] + .into_iter() + .collect(), }, }, new: ClientNew { network: match self.protocol { ConfigProtocol::Near => "testnet".into(), ConfigProtocol::Starknet => "sepolia".into(), + ConfigProtocol::Icp => "ic".into(), }, protocol: self.protocol.as_str().to_owned(), contract_id: match self.protocol { @@ -269,6 +295,7 @@ impl InitCommand { "0x1b991ee006e2d1e372ab96d0a957401fa200358f317b681df2948f30e17c29c" .parse()? } + ConfigProtocol::Icp => "br5f7-7uaaa-aaaaa-qaaca-cai".parse()?, }, }, }, @@ -300,7 +327,7 @@ fn generate_local_signer( Ok(ClientLocalSigner { rpc_url, credentials: Credentials::Near(near_protocol::Credentials { - account_id: hex::encode(account_id).parse()?, + account_id: encode(account_id).parse()?, public_key, secret_key, }), @@ -320,5 +347,23 @@ fn generate_local_signer( }), }) } + ConfigProtocol::Icp => { + let mut rng = OsRng; + + let signing_key = IcpSigningKey::new(&mut rng); + let identity = BasicIdentity::from_signing_key(signing_key.clone()); + + let public_key = identity.public_key().unwrap(); + let account_id = Principal::self_authenticating(&public_key); + + Ok(ClientLocalSigner { + rpc_url, + credentials: Credentials::Icp(icp_protocol::Credentials { + account_id, + public_key: encode(&account_id), + secret_key: encode(&signing_key), + }), + }) + } } } diff --git a/crates/merod/src/cli/relay.rs b/crates/merod/src/cli/relay.rs index 764417871..059f18177 100644 --- a/crates/merod/src/cli/relay.rs +++ b/crates/merod/src/cli/relay.rs @@ -1,5 +1,4 @@ use core::net::{AddrParseError, IpAddr, Ipv4Addr, SocketAddr}; -use std::borrow::Cow; use std::env; use axum::extract::State; @@ -8,12 +7,9 @@ use axum::response::IntoResponse; use axum::routing::post; use axum::{Json, Router}; use calimero_config::ConfigFile; -use calimero_context_config::client::config::Credentials; -use calimero_context_config::client::protocol::{near, starknet}; use calimero_context_config::client::relayer::{RelayRequest, ServerError}; -use calimero_context_config::client::transport::{ - Both, Transport, TransportArguments, TransportRequest, -}; +use calimero_context_config::client::transport::{Transport, TransportArguments, TransportRequest}; +use calimero_context_config::client::Client; use clap::{Parser, ValueEnum}; use eyre::{bail, Result as EyreResult}; use futures_util::FutureExt; @@ -55,69 +51,7 @@ impl RelayCommand { let (tx, mut rx) = mpsc::channel::(32); - let near_transport = near::NearTransport::new(&near::NearConfig { - networks: config - .context - .client - .signer - .local - .near - .iter() - .map(|(network, config)| { - let (account_id, access_key) = match &config.credentials { - Credentials::Near(credentials) => ( - credentials.account_id.clone(), - credentials.secret_key.clone(), - ), - Credentials::Starknet(_) => { - bail!("Expected NEAR credentials, but got Starknet credentials.") - } - _ => bail!("Expected NEAR credentials."), - }; - Ok(( - Cow::from(network.clone()), - near::NetworkConfig { - rpc_url: config.rpc_url.clone(), - account_id, - access_key, - }, - )) - }) - .collect::>()?, - }); - - let starknet_transport = starknet::StarknetTransport::new(&starknet::StarknetConfig { - networks: config - .context - .client - .signer - .local - .starknet - .iter() - .map(|(network, config)| { - let (account_id, access_key) = match &config.credentials { - Credentials::Starknet(credentials) => { - (credentials.account_id, credentials.secret_key) - } - Credentials::Near(_) => bail!("Expected Starknet credentials."), - _ => bail!("Expected NEAR credentials."), - }; - Ok(( - Cow::from(network.clone()), - starknet::NetworkConfig { - rpc_url: config.rpc_url.clone(), - account_id, - access_key, - }, - )) - }) - .collect::>()?, - }); - - let both_transport = Both { - left: near_transport, - right: starknet_transport, - }; + let transports = Client::from_local_config(&config.context.client.signer.local)?; let handle = async move { while let Some((request, res_tx)) = rx.recv().await { @@ -131,7 +65,7 @@ impl RelayCommand { payload: request.payload, }; - let res = both_transport + let res = transports .try_send(args) .await .map(|res| res.map_err(Into::into))