From 78bfaaea8bd6e03bb4ecc68b195132746e21d09f Mon Sep 17 00:00:00 2001 From: alenmestrov Date: Wed, 4 Dec 2024 14:44:38 +0100 Subject: [PATCH] Feat icp proxy (#993) --- Cargo.lock | 334 +++-- Cargo.toml | 3 + .../icp/context-config/context_contract.did | 3 +- contracts/icp/context-config/src/lib.rs | 14 +- contracts/icp/context-config/src/mutate.rs | 108 +- contracts/icp/context-config/src/query.rs | 3 +- contracts/icp/context-config/src/sys.rs | 60 + .../icp/context-config/tests/integration.rs | 363 +++--- .../icp/proxy-contract/.cargo/config.toml | 2 + contracts/icp/proxy-contract/.gitignore | 28 + contracts/icp/proxy-contract/Cargo.toml | 24 + contracts/icp/proxy-contract/build.sh | 18 + .../proxy-contract/build_mock_contracts.sh | 16 + contracts/icp/proxy-contract/dfx.json | 29 + .../mock/external/.cargo/config.toml | 2 + .../proxy-contract/mock/external/Cargo.toml | 12 + .../icp/proxy-contract/mock/external/build.sh | 18 + .../mock/external/mock_external.did | 1 + .../proxy-contract/mock/external/src/lib.rs | 20 + .../mock/ledger/.cargo/config.toml | 2 + .../icp/proxy-contract/mock/ledger/Cargo.toml | 14 + .../icp/proxy-contract/mock/ledger/build.sh | 18 + .../mock/ledger/mock_ledger.did | 1 + .../icp/proxy-contract/mock/ledger/src/lib.rs | 62 + .../icp/proxy-contract/proxy_contract.did | 42 + contracts/icp/proxy-contract/src/lib.rs | 58 + contracts/icp/proxy-contract/src/mutate.rs | 286 +++++ contracts/icp/proxy-contract/src/query.rs | 111 ++ contracts/icp/proxy-contract/src/sys.rs | 47 + contracts/icp/proxy-contract/src/types.rs | 327 +++++ .../icp/proxy-contract/tests/context_types.rs | 60 + .../icp/proxy-contract/tests/integration.rs | 1125 +++++++++++++++++ scripts/test.sh | 2 +- 33 files changed, 2858 insertions(+), 355 deletions(-) create mode 100644 contracts/icp/context-config/src/sys.rs create mode 100644 contracts/icp/proxy-contract/.cargo/config.toml create mode 100644 contracts/icp/proxy-contract/.gitignore create mode 100644 contracts/icp/proxy-contract/Cargo.toml create mode 100755 contracts/icp/proxy-contract/build.sh create mode 100755 contracts/icp/proxy-contract/build_mock_contracts.sh create mode 100644 contracts/icp/proxy-contract/dfx.json create mode 100644 contracts/icp/proxy-contract/mock/external/.cargo/config.toml create mode 100644 contracts/icp/proxy-contract/mock/external/Cargo.toml create mode 100755 contracts/icp/proxy-contract/mock/external/build.sh create mode 100644 contracts/icp/proxy-contract/mock/external/mock_external.did create mode 100644 contracts/icp/proxy-contract/mock/external/src/lib.rs create mode 100644 contracts/icp/proxy-contract/mock/ledger/.cargo/config.toml create mode 100644 contracts/icp/proxy-contract/mock/ledger/Cargo.toml create mode 100755 contracts/icp/proxy-contract/mock/ledger/build.sh create mode 100644 contracts/icp/proxy-contract/mock/ledger/mock_ledger.did create mode 100644 contracts/icp/proxy-contract/mock/ledger/src/lib.rs create mode 100644 contracts/icp/proxy-contract/proxy_contract.did create mode 100644 contracts/icp/proxy-contract/src/lib.rs create mode 100644 contracts/icp/proxy-contract/src/mutate.rs create mode 100644 contracts/icp/proxy-contract/src/query.rs create mode 100644 contracts/icp/proxy-contract/src/sys.rs create mode 100644 contracts/icp/proxy-contract/src/types.rs create mode 100644 contracts/icp/proxy-contract/tests/context_types.rs create mode 100644 contracts/icp/proxy-contract/tests/integration.rs diff --git a/Cargo.lock b/Cargo.lock index 6d4f7e1ff..c3a5b1202 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -267,7 +267,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", "synstructure 0.13.1", ] @@ -290,7 +290,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -352,7 +352,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -363,7 +363,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -417,7 +417,7 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -454,7 +454,7 @@ dependencies = [ "serde_path_to_error", "serde_urlencoded", "sha1", - "sync_wrapper 1.0.1", + "sync_wrapper 1.0.2", "tokio", "tokio-tungstenite", "tower 0.5.1", @@ -478,7 +478,7 @@ dependencies = [ "mime", "pin-project-lite", "rustversion", - "sync_wrapper 1.0.1", + "sync_wrapper 1.0.2", "tower-layer", "tower-service", "tracing", @@ -611,7 +611,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -720,7 +720,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -780,9 +780,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "bytesize" @@ -1045,7 +1045,7 @@ dependencies = [ "prettyplease 0.2.25", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", "thiserror 1.0.69", ] @@ -1152,7 +1152,7 @@ dependencies = [ "calimero-sdk", "calimero-storage", "quote", - "syn 2.0.87", + "syn 2.0.89", "trybuild", ] @@ -1164,7 +1164,7 @@ dependencies = [ "calimero-primitives", "camino", "eyre", - "generic-array 1.1.0", + "generic-array 1.1.1", "rocksdb", "serde", "serde_json", @@ -1215,14 +1215,14 @@ dependencies = [ "lazy_static", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] name = "cc" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" +checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc" dependencies = [ "jobserver", "libc", @@ -1357,7 +1357,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -1530,9 +1530,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca741a962e1b0bff6d724a1a0958b686406e853bb14061f218562e1896f95e6" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] @@ -1733,7 +1733,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -1757,7 +1757,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -1768,7 +1768,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -1878,7 +1878,7 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -1891,7 +1891,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -1964,7 +1964,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -2050,7 +2050,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -2090,7 +2090,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -2111,7 +2111,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -2131,12 +2131,12 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2475,7 +2475,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -2495,7 +2495,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" dependencies = [ "futures-io", - "rustls 0.23.17", + "rustls 0.23.19", "rustls-pki-types", ] @@ -2576,9 +2576,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96512db27971c2c3eece70a1e106fbe6c87760234e31e8f7e5634912fe52794a" +checksum = "2cb8bc4c28d15ade99c7e90b219f30da4be5c88e586277e8cbe886beeb868ab2" dependencies = [ "typenum", ] @@ -2705,9 +2705,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" dependencies = [ "allocator-api2", "equivalent", @@ -2919,9 +2919,9 @@ dependencies = [ [[package]] name = "http-range-header" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08a397c49fec283e3d6211adbe480be95aae5f304cfb923e9970e08956d5168a" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" [[package]] name = "httparse" @@ -3004,13 +3004,13 @@ dependencies = [ "http 1.1.0", "hyper 1.5.1", "hyper-util", - "rustls 0.23.17", + "rustls 0.23.19", "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls 0.26.0", "tower-service", - "webpki-roots 0.26.6", + "webpki-roots 0.26.7", ] [[package]] @@ -3129,6 +3129,19 @@ dependencies = [ "serde_bytes", ] +[[package]] +name = "ic-cdk" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2abdf9341da9f9f6b451a40609cb69645a05a8e9eb7784c16209f16f2c0f76f" +dependencies = [ + "candid", + "ic-cdk-macros 0.17.0", + "ic0", + "serde", + "serde_bytes", +] + [[package]] name = "ic-cdk-macros" version = "0.14.0" @@ -3140,7 +3153,7 @@ dependencies = [ "quote", "serde", "serde_tokenstream", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -3154,7 +3167,21 @@ dependencies = [ "quote", "serde", "serde_tokenstream", - "syn 2.0.87", + "syn 2.0.89", +] + +[[package]] +name = "ic-cdk-macros" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8df41980e95dead28735ab0f748c75477b0c5eab37a09a5641c78ec406a1db0" +dependencies = [ + "candid", + "proc-macro2", + "quote", + "serde", + "serde_tokenstream", + "syn 2.0.89", ] [[package]] @@ -3169,6 +3196,21 @@ dependencies = [ "sha2", ] +[[package]] +name = "ic-ledger-types" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7103dea96107c8b1e49d6e80ab452ab48e9f3594d618dbb46449381f4f0a01" +dependencies = [ + "candid", + "crc32fast", + "hex", + "ic-cdk 0.17.0", + "serde", + "serde_bytes", + "sha2", +] + [[package]] name = "ic-representation-independent-hash" version = "2.6.0" @@ -3373,7 +3415,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -3494,13 +3536,13 @@ dependencies = [ [[package]] name = "impl-trait-for-tuples" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d7a9f6330b71fea57921c9b61c47ee6e84f72d394754eff6163ae67e7395eb" +checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.89", ] [[package]] @@ -3527,7 +3569,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.15.1", + "hashbrown 0.15.2", "serde", ] @@ -3622,9 +3664,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.12" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a73e9fe3c49d7afb2ace819fa181a287ce54a0983eda4e0eb05c22f82ffe534" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jobserver" @@ -3785,9 +3827,9 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" -version = "0.2.164" +version = "0.2.167" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" +checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" [[package]] name = "libloading" @@ -3985,9 +4027,9 @@ dependencies = [ [[package]] name = "libp2p-identity" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cca1eb2bc1fd29f099f3daaab7effd01e1a54b7c577d0ed082521034d912e8" +checksum = "257b5621d159b32282eac446bed6670c39c7dc68a200a992d8f056afa0066f6d" dependencies = [ "bs58 0.5.1", "ed25519-dalek", @@ -4134,7 +4176,7 @@ dependencies = [ "quinn", "rand 0.8.5", "ring 0.17.8", - "rustls 0.23.17", + "rustls 0.23.19", "socket2", "thiserror 1.0.69", "tokio", @@ -4258,7 +4300,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -4309,7 +4351,7 @@ dependencies = [ "libp2p-identity", "rcgen 0.11.3", "ring 0.17.8", - "rustls 0.23.17", + "rustls 0.23.19", "rustls-webpki 0.101.7", "thiserror 1.0.69", "x509-parser 0.16.0", @@ -4399,9 +4441,9 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "litemap" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] name = "local-ip-address" @@ -4447,7 +4489,7 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.15.1", + "hashbrown 0.15.2", ] [[package]] @@ -4653,6 +4695,26 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "mock_external" +version = "0.1.0" +dependencies = [ + "candid", + "ic-cdk 0.16.0", + "ic-cdk-macros 0.16.0", +] + +[[package]] +name = "mock_ledger" +version = "0.1.0" +dependencies = [ + "candid", + "ic-cdk 0.16.0", + "ic-cdk-macros 0.16.0", + "ic-ledger-types", + "serde", +] + [[package]] name = "more-asserts" version = "0.2.2" @@ -5066,7 +5128,7 @@ dependencies = [ "serde_json", "strum 0.26.3", "strum_macros 0.26.4", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -5492,7 +5554,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -5553,7 +5615,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -5601,7 +5663,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -5695,7 +5757,7 @@ checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -5836,7 +5898,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" dependencies = [ "proc-macro2", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -5897,9 +5959,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.89" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -5912,7 +5974,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", "version_check", "yansi", ] @@ -5937,7 +5999,7 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -5957,6 +6019,24 @@ dependencies = [ "tokio", ] +[[package]] +name = "proxy_contract" +version = "0.1.0" +dependencies = [ + "bs58 0.5.1", + "calimero-context-config", + "candid", + "ed25519-dalek", + "hex", + "ic-cdk 0.16.0", + "ic-cdk-macros 0.16.0", + "ic-ledger-types", + "pocket-ic", + "rand 0.8.5", + "serde", + "thiserror 1.0.69", +] + [[package]] name = "psm" version = "0.1.24" @@ -6039,7 +6119,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.0.0", - "rustls 0.23.17", + "rustls 0.23.19", "socket2", "thiserror 2.0.3", "tokio", @@ -6057,7 +6137,7 @@ dependencies = [ "rand 0.8.5", "ring 0.17.8", "rustc-hash 2.0.0", - "rustls 0.23.17", + "rustls 0.23.19", "rustls-pki-types", "slab", "thiserror 2.0.3", @@ -6389,14 +6469,14 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.17", + "rustls 0.23.19", "rustls-native-certs", "rustls-pemfile 2.2.0", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 1.0.1", + "sync_wrapper 1.0.2", "system-configuration 0.6.1", "tokio", "tokio-native-tls", @@ -6409,7 +6489,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 0.26.6", + "webpki-roots 0.26.7", "windows-registry", ] @@ -6560,7 +6640,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.87", + "syn 2.0.89", "walkdir", ] @@ -6644,9 +6724,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.17" +version = "0.23.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f1a745511c54ba6d4465e8d5dfbd81b45791756de28d4981af70d6dca128f1e" +checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1" dependencies = [ "log", "once_cell", @@ -6788,7 +6868,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -6939,7 +7019,7 @@ checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -6950,7 +7030,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -6994,7 +7074,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -7015,7 +7095,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -7057,7 +7137,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -7245,9 +7325,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", @@ -7379,7 +7459,7 @@ source = "git+https://github.com/xJonathanLEI/starknet-rs?rev=5c676a6#5c676a6403 dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -7442,7 +7522,7 @@ version = "0.2.1" source = "git+https://github.com/xJonathanLEI/starknet-rs?rev=5c676a6#5c676a64031901b5a203168fd8ef8d6b40a5862f" dependencies = [ "starknet-core", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -7548,7 +7628,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -7570,9 +7650,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.87" +version = "2.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" dependencies = [ "proc-macro2", "quote", @@ -7587,9 +7667,9 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "sync_wrapper" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" dependencies = [ "futures-core", ] @@ -7614,7 +7694,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -7756,7 +7836,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -7767,7 +7847,7 @@ checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -7886,7 +7966,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -7926,7 +8006,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.17", + "rustls 0.23.19", "rustls-pki-types", "tokio", ] @@ -8165,9 +8245,9 @@ dependencies = [ [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "pin-project-lite", @@ -8189,20 +8269,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", "valuable", @@ -8416,10 +8496,10 @@ dependencies = [ "flate2", "log", "once_cell", - "rustls 0.23.17", + "rustls 0.23.19", "rustls-pki-types", "url", - "webpki-roots 0.26.6", + "webpki-roots 0.26.7", ] [[package]] @@ -8434,9 +8514,9 @@ dependencies = [ [[package]] name = "url" -version = "2.5.3" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna 1.0.3", @@ -8593,7 +8673,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", "wasm-bindgen-shared", ] @@ -8627,7 +8707,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -8905,9 +8985,9 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webpki-roots" -version = "0.26.6" +version = "0.26.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958" +checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" dependencies = [ "rustls-pki-types", ] @@ -9392,9 +9472,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", @@ -9404,13 +9484,13 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", "synstructure 0.13.1", ] @@ -9432,27 +9512,27 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] name = "zerofrom" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", "synstructure 0.13.1", ] @@ -9473,7 +9553,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -9508,7 +9588,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 4e5d8e3d0..8537a3438 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,9 @@ members = [ "./contracts/proxy-lib", "./contracts/test-counter", "./contracts/icp/context-config", + "./contracts/icp/proxy-contract", + "./contracts/icp/proxy-contract/mock/ledger", + "./contracts/icp/proxy-contract/mock/external", "./e2e-tests", ] diff --git a/contracts/icp/context-config/context_contract.did b/contracts/icp/context-config/context_contract.did index 87b94cfe2..af9edb43c 100644 --- a/contracts/icp/context-config/context_contract.did +++ b/contracts/icp/context-config/context_contract.did @@ -18,5 +18,6 @@ service : () -> { privileges : (blob, vec blob) -> ( vec record { blob; vec ICCapability }, ) query; - proxy_contract : (blob) -> (text) query; + proxy_contract : (blob) -> (principal) query; + set_proxy_code : (blob) -> (Result); } diff --git a/contracts/icp/context-config/src/lib.rs b/contracts/icp/context-config/src/lib.rs index ed3a1bb84..b3283e6d9 100644 --- a/contracts/icp/context-config/src/lib.rs +++ b/contracts/icp/context-config/src/lib.rs @@ -1,7 +1,7 @@ use std::cell::RefCell; use std::collections::{BTreeMap, HashMap}; -use candid::CandidType; +use candid::{CandidType, Principal}; use guard::Guard; use serde::{Deserialize, Serialize}; @@ -12,25 +12,31 @@ use crate::types::{ pub mod guard; pub mod mutate; pub mod query; +pub mod sys; pub mod types; #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] pub struct Context { pub application: Guard, pub members: Guard>, - pub proxy: Guard, + pub proxy: Guard, } +#[derive(CandidType, Deserialize, Clone, Debug)] pub struct ContextConfigs { pub contexts: HashMap, - pub next_proxy_id: u64, + pub proxy_code: Option>, + pub owner: Principal, + pub ledger_id: Principal, } impl Default for ContextConfigs { fn default() -> Self { Self { contexts: HashMap::new(), - next_proxy_id: 0, + proxy_code: None, + owner: ic_cdk::api::caller(), + ledger_id: Principal::anonymous(), } } } diff --git a/contracts/icp/context-config/src/mutate.rs b/contracts/icp/context-config/src/mutate.rs index e1aba06b4..8e5c4148f 100644 --- a/contracts/icp/context-config/src/mutate.rs +++ b/contracts/icp/context-config/src/mutate.rs @@ -1,6 +1,10 @@ use std::ops::Deref; use calimero_context_config::repr::{ReprBytes, ReprTransmute}; +use candid::Principal; +use ic_cdk::api::management_canister::main::{ + create_canister, install_code, CanisterSettings, CreateCanisterArgument, InstallCodeArgument, +}; use crate::guard::Guard; use crate::types::{ @@ -10,7 +14,7 @@ use crate::types::{ use crate::{Context, CONTEXT_CONFIGS}; #[ic_cdk::update] -pub fn mutate(signed_request: ICPSigned) -> Result<(), String> { +pub async fn mutate(signed_request: ICPSigned) -> Result<(), String> { let request = signed_request .parse(|r| r.signer_id) .map_err(|e| format!("Failed to verify signature: {}", e))?; @@ -27,7 +31,7 @@ pub fn mutate(signed_request: ICPSigned) -> Result<(), String> { ContextRequestKind::Add { author_id, application, - } => add_context(&request.signer_id, context_id, author_id, application), + } => add_context(&request.signer_id, context_id, author_id, application).await, ContextRequestKind::UpdateApplication { application } => { update_application(&request.signer_id, &context_id, application) } @@ -44,24 +48,26 @@ pub fn mutate(signed_request: ICPSigned) -> Result<(), String> { revoke(&request.signer_id, &context_id, capabilities) } ContextRequestKind::UpdateProxyContract => { - // TODO: Implement update_proxy_contract - Ok(()) + update_proxy_contract(&request.signer_id, context_id).await } }, } } -fn add_context( +async fn add_context( signer_id: &ICSignerId, context_id: ICContextId, author_id: ICContextIdentity, application: ICApplication, ) -> Result<(), String> { - // 1. Verify signer is the context itself - direct array comparison if signer_id.as_bytes() != context_id.as_bytes() { return Err("context addition must be signed by the context itself".into()); } + let proxy_canister_id = deploy_proxy_contract(&context_id) + .await + .unwrap_or_else(|e| panic!("Failed to deploy proxy contract: {}", e)); + CONTEXT_CONFIGS.with(|configs| { let mut configs = configs.borrow_mut(); @@ -74,7 +80,7 @@ fn add_context( ), proxy: Guard::new( author_id.rt().expect("infallible conversion"), - format!("{}.{}", configs.next_proxy_id, ic_cdk::api::id()), + proxy_canister_id, ), }; @@ -83,12 +89,55 @@ fn add_context( return Err("context already exists".into()); } - configs.next_proxy_id += 1; - Ok(()) }) } +async fn deploy_proxy_contract(context_id: &ICContextId) -> Result { + // Get the proxy code + let proxy_code = CONTEXT_CONFIGS + .with(|configs| configs.borrow().proxy_code.clone()) + .ok_or("proxy code not set")?; + + // Get the ledger ID + let ledger_id = CONTEXT_CONFIGS.with(|configs| configs.borrow().ledger_id.clone()); + // Create canister with cycles + let create_args = CreateCanisterArgument { + settings: Some(CanisterSettings { + controllers: Some(vec![ic_cdk::api::id()]), + compute_allocation: None, + memory_allocation: None, + freezing_threshold: None, + reserved_cycles_limit: None, + log_visibility: None, + wasm_memory_limit: None, + }), + }; + + let (canister_record,) = create_canister(create_args, 500_000_000_000_000u128) + .await + .map_err(|e| format!("Failed to create canister: {:?}", e))?; + + let canister_id = canister_record.canister_id; + + // Encode init args matching the proxy's init(context_id: ICContextId, ledger_id: Principal) + let init_args = candid::encode_args((context_id.clone(), ledger_id)) + .map_err(|e| format!("Failed to encode init args: {}", e))?; + + let install_args = InstallCodeArgument { + mode: ic_cdk::api::management_canister::main::CanisterInstallMode::Install, + canister_id, + wasm_module: proxy_code, + arg: init_args, + }; + + install_code(install_args) + .await + .map_err(|e| format!("Failed to install code: {:?}", e))?; + + Ok(canister_id) +} + fn update_application( signer_id: &ICSignerId, context_id: &ICContextId, @@ -282,3 +331,44 @@ fn revoke( Ok(()) }) } + +async fn update_proxy_contract( + signer_id: &ICSignerId, + context_id: ICContextId, +) -> Result<(), String> { + let mut context = CONTEXT_CONFIGS.with(|configs| { + let configs = configs.borrow(); + configs + .contexts + .get(&context_id) + .ok_or_else(|| "context does not exist".to_string()) + .cloned() + })?; + + // Get proxy canister ID + let proxy_canister_id = context + .proxy + .get(signer_id) + .map_err(|_| "unauthorized: Proxy capability required".to_string())? + .get_mut() + .clone(); + + // Get the proxy code + let proxy_code = CONTEXT_CONFIGS + .with(|configs| configs.borrow().proxy_code.clone()) + .ok_or("proxy code not set")?; + + // Update the proxy contract code + let install_args = InstallCodeArgument { + mode: ic_cdk::api::management_canister::main::CanisterInstallMode::Upgrade(None), + canister_id: proxy_canister_id, + wasm_module: proxy_code, + arg: candid::encode_one(&context_id).map_err(|e| format!("Encoding error: {}", e))?, + }; + + install_code(install_args) + .await + .map_err(|e| format!("Failed to update proxy contract: {:?}", e))?; + + Ok(()) +} diff --git a/contracts/icp/context-config/src/query.rs b/contracts/icp/context-config/src/query.rs index 79ad2a7b9..47205de9f 100644 --- a/contracts/icp/context-config/src/query.rs +++ b/contracts/icp/context-config/src/query.rs @@ -1,6 +1,7 @@ use std::collections::BTreeMap; use calimero_context_config::repr::ReprTransmute; +use candid::Principal; use ic_cdk_macros::query; use crate::types::*; @@ -33,7 +34,7 @@ fn application_revision(context_id: ICContextId) -> u64 { } #[query] -fn proxy_contract(context_id: ICContextId) -> String { +fn proxy_contract(context_id: ICContextId) -> Principal { CONTEXT_CONFIGS.with(|configs| { let configs = configs.borrow(); let context = configs diff --git a/contracts/icp/context-config/src/sys.rs b/contracts/icp/context-config/src/sys.rs new file mode 100644 index 000000000..67437d930 --- /dev/null +++ b/contracts/icp/context-config/src/sys.rs @@ -0,0 +1,60 @@ +use candid::{CandidType, Deserialize, Principal}; +use ic_cdk; + +use crate::CONTEXT_CONFIGS; + +#[derive(CandidType, Deserialize)] +struct StableStorage { + configs: crate::ContextConfigs, +} + +#[ic_cdk::pre_upgrade] +fn pre_upgrade() { + // Verify caller is the owner + CONTEXT_CONFIGS.with(|configs| { + let configs = configs.borrow(); + if ic_cdk::api::caller() != configs.owner { + ic_cdk::trap("unauthorized: only owner can upgrade context contract"); + } + }); + + // Store the contract state + let state = CONTEXT_CONFIGS.with(|configs| StableStorage { + configs: configs.borrow().clone(), + }); + + // Write state to stable storage + match ic_cdk::storage::stable_save((state,)) { + Ok(_) => (), + Err(err) => ic_cdk::trap(&format!("Failed to save stable storage: {}", err)), + } +} + +#[ic_cdk::post_upgrade] +fn post_upgrade() { + // Restore the contract state + match ic_cdk::storage::stable_restore::<(StableStorage,)>() { + Ok((state,)) => { + CONTEXT_CONFIGS.with(|configs| { + *configs.borrow_mut() = state.configs; + }); + } + Err(err) => ic_cdk::trap(&format!("Failed to restore stable storage: {}", err)), + } +} + +#[ic_cdk::update] +pub fn set_proxy_code(proxy_code: Vec, ledger_id: Principal) -> Result<(), String> { + CONTEXT_CONFIGS.with(|configs| { + let mut configs = configs.borrow_mut(); + + // Check if caller is the owner + if ic_cdk::api::caller() != configs.owner { + return Err("Unauthorized: only owner can set proxy code".to_string()); + } + + configs.ledger_id = ledger_id; + configs.proxy_code = Some(proxy_code); + Ok(()) + }) +} diff --git a/contracts/icp/context-config/tests/integration.rs b/contracts/icp/context-config/tests/integration.rs index 07090e510..24cce2e18 100644 --- a/contracts/icp/context-config/tests/integration.rs +++ b/contracts/icp/context-config/tests/integration.rs @@ -7,20 +7,32 @@ use context_contract::types::{ ICContextId, ICContextIdentity, ICPSigned, ICSignerId, Request, RequestKind, }; use ed25519_dalek::{Signer, SigningKey}; -use pocket_ic::{PocketIc, WasmResult}; +use pocket_ic::{PocketIc, UserError, WasmResult}; use rand::Rng; fn setup() -> (PocketIc, Principal) { let pic = PocketIc::new(); let wasm = std::fs::read("res/context_contract.wasm").expect("failed to read wasm"); let canister = pic.create_canister(); - pic.add_cycles(canister, 2_000_000_000_000); + pic.add_cycles(canister, 1_000_000_000_000_000); pic.install_canister( canister, wasm, vec![], None, // No controller ); + + // Set the proxy code + let proxy_code = std::fs::read("../proxy-contract/res/proxy_contract.wasm") + .expect("failed to read proxy wasm"); + pic.update_call( + canister, + Principal::anonymous(), + "set_proxy_code", + candid::encode_one(proxy_code).unwrap(), + ) + .expect("Failed to set proxy code"); + (pic, canister) } @@ -36,6 +48,128 @@ fn get_time_nanos(pic: &PocketIc) -> u64 { .as_nanos() as u64 } +fn handle_response( + response: Result, + expected_success: bool, + operation_name: &str, +) { + match response { + Ok(WasmResult::Reply(bytes)) => { + let result: Result<(), String> = candid::decode_one(&bytes).unwrap_or_else(|e| { + panic!("Failed to decode response for {}: {}", operation_name, e) + }); + + match (result, expected_success) { + (Ok(_), true) => println!("{} succeeded as expected", operation_name), + (Ok(_), false) => panic!("{} succeeded when it should have failed", operation_name), + (Err(e), true) => panic!( + "{} failed when it should have succeeded: {}", + operation_name, e + ), + (Err(e), false) => println!("{} failed as expected: {}", operation_name, e), + } + } + Ok(WasmResult::Reject(msg)) => { + if expected_success { + panic!("{}: Unexpected canister rejection: {}", operation_name, msg); + } else { + println!("{}: Expected canister rejection: {}", operation_name, msg); + } + } + Err(e) => panic!("{}: Call failed: {:?}", operation_name, e), + } +} + +#[test] +fn test_proxy_management() { + let (pic, canister) = setup(); + let mut rng = rand::thread_rng(); + + // Advance IC time + let current_nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() as u64; + pic.advance_time(Duration::from_nanos(current_nanos)); + + // Create test identities + let context_sk = SigningKey::from_bytes(&rng.gen()); + let context_pk = context_sk.verifying_key(); + let context_id = ICContextId::new(context_pk.to_bytes()); + + let alice_sk = SigningKey::from_bytes(&rng.gen()); + let alice_pk = alice_sk.verifying_key(); + let alice_id = ICContextIdentity::new(alice_pk.to_bytes()); + + // Create context with initial application + let create_request = Request { + kind: RequestKind::Context(ContextRequest { + context_id: context_id.clone(), + kind: ContextRequestKind::Add { + author_id: alice_id.clone(), + application: ICApplication { + id: ICApplicationId::new(rng.gen()), + blob: ICBlobId::new(rng.gen()), + size: 0, + source: String::new(), + metadata: vec![], + }, + }, + }), + signer_id: ICSignerId::new(context_id.as_bytes()), + timestamp_ms: get_time_nanos(&pic), + }; + + let signed_request = create_signed_request(&context_sk, create_request); + let response = pic.update_call( + canister, + Principal::anonymous(), + "mutate", + candid::encode_one(signed_request).unwrap(), + ); + handle_response(response, true, "mutate"); + + // Try to update proxy contract without Proxy capability (should fail) + let bob_sk = SigningKey::from_bytes(&rng.gen()); + let bob_pk = bob_sk.verifying_key(); + let update_request = Request { + kind: RequestKind::Context(ContextRequest { + context_id: context_id.clone(), + kind: ContextRequestKind::UpdateProxyContract, + }), + signer_id: ICSignerId::new(bob_pk.to_bytes()), + timestamp_ms: get_time_nanos(&pic), + }; + + let signed_request = create_signed_request(&bob_sk, update_request); + let response = pic.update_call( + canister, + Principal::anonymous(), + "mutate", + candid::encode_one(signed_request).unwrap(), + ); + handle_response(response, false, "mutate"); + + // Update proxy contract with proper capability (Alice has it by default) + let update_request = Request { + kind: RequestKind::Context(ContextRequest { + context_id: context_id.clone(), + kind: ContextRequestKind::UpdateProxyContract, + }), + signer_id: ICSignerId::new(alice_pk.to_bytes()), + timestamp_ms: get_time_nanos(&pic), + }; + + let signed_request = create_signed_request(&alice_sk, update_request); + let response = pic.update_call( + canister, + Principal::anonymous(), + "mutate", + candid::encode_one(signed_request).unwrap(), + ); + handle_response(response, true, "mutate"); +} + #[test] fn test_mutate_success_cases() { let (pic, canister) = setup(); @@ -82,25 +216,7 @@ fn test_mutate_success_cases() { "mutate", candid::encode_one(signed_request).unwrap(), ); - - match response { - Ok(WasmResult::Reply(bytes)) => { - // Decode the actual response - let result: Result<(), String> = - candid::decode_one(&bytes).expect("Failed to decode response"); - assert!( - result.is_ok(), - "Context addition failed: {:?}", - result.err() - ); - } - Ok(WasmResult::Reject(msg)) => { - panic!("Canister rejected the call: {}", msg); - } - Err(err) => { - panic!("Failed to call canister: {}", err); - } - } + handle_response(response, true, "Context creation"); } #[test] @@ -154,7 +270,7 @@ fn test_member_management() { "mutate", candid::encode_one(signed_request).unwrap(), ); - assert!(response.is_ok(), "Context creation should succeed"); + handle_response(response, true, "Context creation"); // Add Bob as a member (signed by Alice who has management rights) let add_member_request = Request { @@ -175,20 +291,7 @@ fn test_member_management() { "mutate", candid::encode_one(signed_request).unwrap(), ); - - match response { - Ok(WasmResult::Reply(bytes)) => { - let result: Result<(), String> = - candid::decode_one(&bytes).expect("Failed to decode response"); - assert!(result.is_ok(), "Adding member failed: {:?}", result.err()); - } - Ok(WasmResult::Reject(msg)) => { - panic!("Canister rejected the call: {}", msg); - } - Err(err) => { - panic!("Failed to call canister: {}", err); - } - } + handle_response(response, true, "Member addition"); // Verify members through query call let query_response = pic.query_call( @@ -232,20 +335,7 @@ fn test_member_management() { "mutate", candid::encode_one(signed_request).unwrap(), ); - - match response { - Ok(WasmResult::Reply(bytes)) => { - let result: Result<(), String> = - candid::decode_one(&bytes).expect("Failed to decode response"); - assert!(result.is_ok(), "Removing member failed: {:?}", result.err()); - } - Ok(WasmResult::Reject(msg)) => { - panic!("Canister rejected the call: {}", msg); - } - Err(err) => { - panic!("Failed to call canister: {}", err); - } - } + handle_response(response, true, "Member removal"); // Verify members again let query_response = pic.query_call( @@ -319,23 +409,7 @@ fn test_capability_management() { "mutate", candid::encode_one(signed_request).unwrap(), ); - match response { - Ok(WasmResult::Reply(bytes)) => { - let result: Result<(), String> = - candid::decode_one(&bytes).expect("Failed to decode response"); - assert!( - result.is_ok(), - "Context creation failed: {:?}", - result.err() - ); - } - Ok(WasmResult::Reject(msg)) => { - panic!("Canister rejected the call: {}", msg); - } - Err(err) => { - panic!("Failed to call canister: {}", err); - } - } + handle_response(response, true, "Context creation"); // Add Bob as a member before granting capabilities let add_member_request = Request { @@ -356,19 +430,7 @@ fn test_capability_management() { "mutate", candid::encode_one(signed_request).unwrap(), ); - match response { - Ok(WasmResult::Reply(bytes)) => { - let result: Result<(), String> = - candid::decode_one(&bytes).expect("Failed to decode response"); - assert!(result.is_ok(), "Adding member failed: {:?}", result.err()); - } - Ok(WasmResult::Reject(msg)) => { - panic!("Canister rejected the call: {}", msg); - } - Err(err) => { - panic!("Failed to call canister: {}", err); - } - } + handle_response(response, true, "Member addition"); // Grant capabilities to Bob let grant_request = Request { @@ -389,23 +451,7 @@ fn test_capability_management() { "mutate", candid::encode_one(signed_request).unwrap(), ); - match response { - Ok(WasmResult::Reply(bytes)) => { - let result: Result<(), String> = - candid::decode_one(&bytes).expect("Failed to decode response"); - assert!( - result.is_ok(), - "Granting capability failed: {:?}", - result.err() - ); - } - Ok(WasmResult::Reject(msg)) => { - panic!("Canister rejected the call: {}", msg); - } - Err(err) => { - panic!("Failed to call canister: {}", err); - } - } + handle_response(response, true, "Capability granting"); // Verify Bob's capabilities let query_response = pic.query_call( @@ -443,23 +489,7 @@ fn test_capability_management() { "mutate", candid::encode_one(signed_request).unwrap(), ); - match response { - Ok(WasmResult::Reply(bytes)) => { - let result: Result<(), String> = - candid::decode_one(&bytes).expect("Failed to decode response"); - assert!( - result.is_ok(), - "Revoking capability failed: {:?}", - result.err() - ); - } - Ok(WasmResult::Reject(msg)) => { - panic!("Canister rejected the call: {}", msg); - } - Err(err) => { - panic!("Failed to call canister: {}", err); - } - } + handle_response(response, true, "Capability revoking"); // Verify Bob's capabilities are gone let query_response = pic.query_call( @@ -539,23 +569,7 @@ fn test_application_update() { "mutate", candid::encode_one(signed_request).unwrap(), ); - match response { - Ok(WasmResult::Reply(bytes)) => { - let result: Result<(), String> = - candid::decode_one(&bytes).expect("Failed to decode response"); - assert!( - result.is_ok(), - "Context creation failed: {:?}", - result.err() - ); - } - Ok(WasmResult::Reject(msg)) => { - panic!("Canister rejected the call: {}", msg); - } - Err(err) => { - panic!("Failed to call canister: {}", err); - } - } + handle_response(response, true, "Context creation"); // Verify initial application state let query_response = pic.query_call( @@ -664,23 +678,7 @@ fn test_application_update() { "mutate", candid::encode_one(signed_request).unwrap(), ); - match response { - Ok(WasmResult::Reply(bytes)) => { - let result: Result<(), String> = - candid::decode_one(&bytes).expect("Failed to decode response"); - assert!( - result.is_ok(), - "Authorized update failed: {:?}", - result.err() - ); - } - Ok(WasmResult::Reject(msg)) => { - panic!("Canister rejected the call: {}", msg); - } - Err(err) => { - panic!("Failed to call canister: {}", err); - } - } + handle_response(response, true, "Authorized update"); // Verify application has been updated let query_response = pic.query_call( @@ -749,23 +747,7 @@ fn test_edge_cases() { "mutate", candid::encode_one(signed_request).unwrap(), ); - match response { - Ok(WasmResult::Reply(bytes)) => { - let result: Result<(), String> = - candid::decode_one(&bytes).expect("Failed to decode response"); - assert!( - result.is_ok(), - "Context creation failed: {:?}", - result.err() - ); - } - Ok(WasmResult::Reject(msg)) => { - panic!("Canister rejected the call: {}", msg); - } - Err(err) => { - panic!("Failed to call canister: {}", err); - } - } + handle_response(response, true, "Context creation"); // Test 1: Adding empty member list let add_empty_members = Request { @@ -784,23 +766,7 @@ fn test_edge_cases() { "mutate", candid::encode_one(signed_request).unwrap(), ); - match response { - Ok(WasmResult::Reply(bytes)) => { - let result: Result<(), String> = - candid::decode_one(&bytes).expect("Failed to decode response"); - assert!( - result.is_ok(), - "Empty member list failed: {:?}", - result.err() - ); - } - Ok(WasmResult::Reject(msg)) => { - panic!("Canister rejected the call: {}", msg); - } - Err(err) => { - panic!("Failed to call canister: {}", err); - } - } + handle_response(response, true, "Empty member list addition"); // Test 2: Adding duplicate members let bob_id = ICContextIdentity::new(rng.gen()); @@ -822,23 +788,7 @@ fn test_edge_cases() { "mutate", candid::encode_one(signed_request).unwrap(), ); - match response { - Ok(WasmResult::Reply(bytes)) => { - let result: Result<(), String> = - candid::decode_one(&bytes).expect("Failed to decode response"); - assert!( - result.is_ok(), - "Duplicate members failed: {:?}", - result.err() - ); - } - Ok(WasmResult::Reject(msg)) => { - panic!("Canister rejected the call: {}", msg); - } - Err(err) => { - panic!("Failed to call canister: {}", err); - } - } + handle_response(response, true, "Duplicate member addition"); // Verify only one instance was added let query_response = pic.query_call( @@ -898,7 +848,7 @@ fn test_timestamp_scenarios() { "mutate", candid::encode_one(signed_request).unwrap(), ); - assert!(response.is_ok(), "Context creation should succeed"); + handle_response(response, true, "Context creation"); // Try with expired timestamp (more than 5 seconds old) let expired_request = Request { @@ -919,18 +869,7 @@ fn test_timestamp_scenarios() { "mutate", candid::encode_one(signed_request).unwrap(), ); - - match response { - Ok(WasmResult::Reply(bytes)) => { - let result: Result<(), String> = candid::decode_one(&bytes).unwrap(); - assert!(result.is_err(), "Expired timestamp should be rejected"); - assert!( - result.unwrap_err().contains("expired"), - "Should contain 'expired' in error message" - ); - } - _ => panic!("Expected error response for expired timestamp"), - } + handle_response(response, false, "Expired timestamp request"); } #[test] diff --git a/contracts/icp/proxy-contract/.cargo/config.toml b/contracts/icp/proxy-contract/.cargo/config.toml new file mode 100644 index 000000000..a6b014e95 --- /dev/null +++ b/contracts/icp/proxy-contract/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target-dir = "target" diff --git a/contracts/icp/proxy-contract/.gitignore b/contracts/icp/proxy-contract/.gitignore new file mode 100644 index 000000000..0730dc676 --- /dev/null +++ b/contracts/icp/proxy-contract/.gitignore @@ -0,0 +1,28 @@ +# Various IDEs and Editors +.vscode/ +.idea/ +**/*~ + +# Mac OSX temporary files +.DS_Store +**/.DS_Store + +# dfx temporary files +.dfx/ + +# generated files +**/declarations/ + +# rust +target/ + +# frontend code +node_modules/ +dist/ +.svelte-kit/ + +# environment variables +.env + +# testing library +pocket-ic diff --git a/contracts/icp/proxy-contract/Cargo.toml b/contracts/icp/proxy-contract/Cargo.toml new file mode 100644 index 000000000..a851e8d8d --- /dev/null +++ b/contracts/icp/proxy-contract/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "proxy_contract" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +bs58.workspace = true +calimero-context-config = { path = "../../../crates/context/config" } +candid = { version = "0.10", features = ["value"] } +ed25519-dalek.workspace = true +hex.workspace = true +ic-cdk = "0.16" +ic-cdk-macros = "0.16" +ic-ledger-types = "0.14.0" +serde = { version = "1.0", features = ["derive"] } +thiserror.workspace = true + +[dev-dependencies] +pocket-ic = "6.0.0" +rand = "0.8" + \ No newline at end of file diff --git a/contracts/icp/proxy-contract/build.sh b/contracts/icp/proxy-contract/build.sh new file mode 100755 index 000000000..32fda01a6 --- /dev/null +++ b/contracts/icp/proxy-contract/build.sh @@ -0,0 +1,18 @@ +#!/bin/sh +set -e + +cd "$(dirname $0)" + +TARGET="${CARGO_TARGET_DIR:-./target}" + +rustup target add wasm32-unknown-unknown + +cargo build --target wasm32-unknown-unknown --profile app-release + +mkdir -p res + +cp $TARGET/wasm32-unknown-unknown/app-release/proxy_contract.wasm ./res/proxy_contract.wasm + +if command -v wasm-opt > /dev/null; then + wasm-opt -Oz ./res/proxy_contract.wasm -o ./res/proxy_contract.wasm +fi diff --git a/contracts/icp/proxy-contract/build_mock_contracts.sh b/contracts/icp/proxy-contract/build_mock_contracts.sh new file mode 100755 index 000000000..a8d9cecd8 --- /dev/null +++ b/contracts/icp/proxy-contract/build_mock_contracts.sh @@ -0,0 +1,16 @@ +#!/bin/sh +set -e + +cd "$(dirname $0)" + +echo "Building proxy contract..." +./build.sh + +echo "Building mock ledger contract..." +./mock/ledger/build.sh + +echo "Building mock external contract..." +./mock/external/build.sh + +echo "Building context-config contract..." +../context-config/build.sh diff --git a/contracts/icp/proxy-contract/dfx.json b/contracts/icp/proxy-contract/dfx.json new file mode 100644 index 000000000..6f1cf6bfb --- /dev/null +++ b/contracts/icp/proxy-contract/dfx.json @@ -0,0 +1,29 @@ +{ + "canisters": { + "proxy_contract": { + "candid": "proxy_contract.did", + "package": "proxy_contract", + "type": "rust" + }, + "mock_ledger": { + "type": "rust", + "package": "mock_ledger", + "candid": "mock/ledger/ledger.did", + "path": "mock/ledger" + }, + "mock_external": { + "type": "rust", + "package": "mock_external", + "candid": "mock/external/external.did", + "path": "mock/external" + } + }, + "defaults": { + "build": { + "args": "", + "packtool": "" + } + }, + "output_env_file": ".env", + "version": 1 +} \ No newline at end of file diff --git a/contracts/icp/proxy-contract/mock/external/.cargo/config.toml b/contracts/icp/proxy-contract/mock/external/.cargo/config.toml new file mode 100644 index 000000000..a6b014e95 --- /dev/null +++ b/contracts/icp/proxy-contract/mock/external/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target-dir = "target" diff --git a/contracts/icp/proxy-contract/mock/external/Cargo.toml b/contracts/icp/proxy-contract/mock/external/Cargo.toml new file mode 100644 index 000000000..4b14dd556 --- /dev/null +++ b/contracts/icp/proxy-contract/mock/external/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "mock_external" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +candid = "0.10" +ic-cdk = "0.16" +ic-cdk-macros = "0.16" \ No newline at end of file diff --git a/contracts/icp/proxy-contract/mock/external/build.sh b/contracts/icp/proxy-contract/mock/external/build.sh new file mode 100755 index 000000000..f331d6cd7 --- /dev/null +++ b/contracts/icp/proxy-contract/mock/external/build.sh @@ -0,0 +1,18 @@ +#!/bin/sh +set -e + +cd "$(dirname $0)" + +TARGET="${CARGO_TARGET_DIR:-../../../../../target}" + +rustup target add wasm32-unknown-unknown + +cargo build --target wasm32-unknown-unknown --profile app-release + +mkdir -p res + +cp $TARGET/wasm32-unknown-unknown/app-release/mock_external.wasm ./res/ + +if command -v wasm-opt > /dev/null; then + wasm-opt -Oz ./res/mock_external.wasm -o ./res/mock_external.wasm +fi diff --git a/contracts/icp/proxy-contract/mock/external/mock_external.did b/contracts/icp/proxy-contract/mock/external/mock_external.did new file mode 100644 index 000000000..608f976b7 --- /dev/null +++ b/contracts/icp/proxy-contract/mock/external/mock_external.did @@ -0,0 +1 @@ +service : { get_calls : () -> (vec blob) query; test_method : (blob) -> () } diff --git a/contracts/icp/proxy-contract/mock/external/src/lib.rs b/contracts/icp/proxy-contract/mock/external/src/lib.rs new file mode 100644 index 000000000..ca10b9152 --- /dev/null +++ b/contracts/icp/proxy-contract/mock/external/src/lib.rs @@ -0,0 +1,20 @@ +use std::cell::RefCell; + +thread_local! { + static CALLS: RefCell>> = RefCell::new(Vec::new()); +} + +#[ic_cdk::update] +fn test_method(args: Vec) -> Vec { + CALLS.with(|calls| { + calls.borrow_mut().push(args.clone()); + args // Return the same args back + }) +} + +#[ic_cdk::query] +fn get_calls() -> Vec> { + CALLS.with(|calls| calls.borrow().clone()) +} + +ic_cdk::export_candid!(); diff --git a/contracts/icp/proxy-contract/mock/ledger/.cargo/config.toml b/contracts/icp/proxy-contract/mock/ledger/.cargo/config.toml new file mode 100644 index 000000000..a6b014e95 --- /dev/null +++ b/contracts/icp/proxy-contract/mock/ledger/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target-dir = "target" diff --git a/contracts/icp/proxy-contract/mock/ledger/Cargo.toml b/contracts/icp/proxy-contract/mock/ledger/Cargo.toml new file mode 100644 index 000000000..3736d2fff --- /dev/null +++ b/contracts/icp/proxy-contract/mock/ledger/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "mock_ledger" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +candid = "0.10" +serde = { version = "1.0", features = ["derive"] } +ic-cdk = "0.16" +ic-cdk-macros = "0.16" +ic-ledger-types = "0.14.0" diff --git a/contracts/icp/proxy-contract/mock/ledger/build.sh b/contracts/icp/proxy-contract/mock/ledger/build.sh new file mode 100755 index 000000000..e9951e81c --- /dev/null +++ b/contracts/icp/proxy-contract/mock/ledger/build.sh @@ -0,0 +1,18 @@ +#!/bin/sh +set -e + +cd "$(dirname $0)" + +TARGET="${CARGO_TARGET_DIR:-../../../../../target}" + +rustup target add wasm32-unknown-unknown + +cargo build --target wasm32-unknown-unknown --profile app-release + +mkdir -p res + +cp $TARGET/wasm32-unknown-unknown/app-release/mock_ledger.wasm ./res/mock_ledger.wasm + +if command -v wasm-opt > /dev/null; then + wasm-opt -Oz ./res/mock_ledger.wasm -o ./res/mock_ledger.wasm +fi diff --git a/contracts/icp/proxy-contract/mock/ledger/mock_ledger.did b/contracts/icp/proxy-contract/mock/ledger/mock_ledger.did new file mode 100644 index 000000000..20313456b --- /dev/null +++ b/contracts/icp/proxy-contract/mock/ledger/mock_ledger.did @@ -0,0 +1 @@ +service : { balance : () -> (nat) query; transfer : (blob) -> () } diff --git a/contracts/icp/proxy-contract/mock/ledger/src/lib.rs b/contracts/icp/proxy-contract/mock/ledger/src/lib.rs new file mode 100644 index 000000000..2c60efed8 --- /dev/null +++ b/contracts/icp/proxy-contract/mock/ledger/src/lib.rs @@ -0,0 +1,62 @@ +use std::cell::RefCell; + +use candid::{CandidType, Deserialize, Principal}; +use ic_ledger_types::{ + AccountIdentifier, BlockIndex, Memo, Timestamp, Tokens, TransferArgs, TransferError, +}; + +thread_local! { + static BALANCE: RefCell = RefCell::new(1_000_000_000); +} + +type TransferResult = Result; + +#[ic_cdk::update] +fn transfer(args: TransferArgs) -> TransferResult { + ic_cdk::println!( + "Mock ledger received transfer: to={:?}, amount={}", + args.to, + args.amount + ); + + // Verify fee + if args.fee.e8s() != 10_000 { + return Err(TransferError::BadFee { + expected_fee: Tokens::from_e8s(10_000), + }); + } + + let amount_e8s = args.amount.e8s(); + + BALANCE.with(|balance| { + let mut bal = balance.borrow_mut(); + + // Check if we have enough balance + if amount_e8s > *bal { + return Err(TransferError::InsufficientFunds { + balance: Tokens::from_e8s(*bal), + }); + } + + // Subtract amount and fee + *bal = bal.saturating_sub(amount_e8s); + *bal = bal.saturating_sub(args.fee.e8s()); + + ic_cdk::println!("New balance: {}", *bal); + + // Return mock block index + Ok(1) + }) +} + +#[ic_cdk::query] +fn account_balance(args: AccountBalanceArgs) -> Tokens { + BALANCE.with(|balance| Tokens::from_e8s(*balance.borrow())) +} + +#[derive(CandidType, Deserialize)] +struct AccountBalanceArgs { + account: AccountIdentifier, +} + +ic_cdk::export_candid!(); diff --git a/contracts/icp/proxy-contract/proxy_contract.did b/contracts/icp/proxy-contract/proxy_contract.did new file mode 100644 index 000000000..fe3387d2d --- /dev/null +++ b/contracts/icp/proxy-contract/proxy_contract.did @@ -0,0 +1,42 @@ +type ICPSigned = record { signature : blob; _phantom : null; payload : blob }; +type ICProposal = record { + id : blob; + actions : vec ICProposalAction; + author_id : blob; +}; +type ICProposalAction = variant { + SetNumApprovals : record { num_approvals : nat32 }; + SetContextValue : record { key : blob; value : blob }; + Transfer : record { receiver_id : principal; amount : nat }; + SetActiveProposalsLimit : record { active_proposals_limit : nat32 }; + ExternalFunctionCall : record { + receiver_id : principal; + args : text; + deposit : nat; + method_name : text; + }; +}; +type ICProposalApprovalWithSigner = record { + added_timestamp : nat64; + signer_id : blob; + proposal_id : blob; +}; +type ICProposalWithApprovals = record { + num_approvals : nat64; + proposal_id : blob; +}; +type Result = variant { Ok : opt ICProposalWithApprovals; Err : text }; +service : (blob, principal) -> { + context_storage_entries : (nat64, nat64) -> (vec record { blob; blob }) query; + get_active_proposals_limit : () -> (nat32) query; + get_confirmations_count : (blob) -> (opt ICProposalWithApprovals) query; + get_context_value : (blob) -> (opt blob) query; + get_num_approvals : () -> (nat32) query; + get_proposal_approvals_with_signer : (blob) -> ( + vec ICProposalApprovalWithSigner, + ) query; + get_proposal_approvers : (blob) -> (opt vec blob) query; + mutate : (ICPSigned) -> (Result); + proposal : (blob) -> (opt ICProposal) query; + proposals : (nat64, nat64) -> (vec ICProposal) query; +} diff --git a/contracts/icp/proxy-contract/src/lib.rs b/contracts/icp/proxy-contract/src/lib.rs new file mode 100644 index 000000000..4904704b2 --- /dev/null +++ b/contracts/icp/proxy-contract/src/lib.rs @@ -0,0 +1,58 @@ +use std::cell::RefCell; +use std::collections::{BTreeMap, BTreeSet, HashMap}; + +use candid::{CandidType, Principal}; +use serde::Deserialize; +use types::{ICContextId, LedgerId}; + +use crate::types::{ + ICPSigned, ICProposal, ICProposalApprovalWithSigner, ICProposalId, ICProposalWithApprovals, + ICRequest, ICSignerId, +}; + +pub mod mutate; +pub mod query; +pub mod sys; +pub mod types; + +#[derive(Default, CandidType, Deserialize, Clone)] +pub struct ICProxyContract { + pub context_id: ICContextId, + pub context_config_id: String, + pub num_approvals: u32, + pub proposals: BTreeMap, + pub approvals: BTreeMap>, + pub num_proposals_pk: BTreeMap, + pub active_proposals_limit: u32, + pub context_storage: HashMap, Vec>, + pub ledger_id: LedgerId, +} + +impl ICProxyContract { + pub fn new(context_id: ICContextId, ledger_id: Principal) -> Self { + Self { + context_id, + context_config_id: ic_cdk::caller().to_string(), + num_approvals: 3, + proposals: BTreeMap::new(), + approvals: BTreeMap::new(), + num_proposals_pk: BTreeMap::new(), + active_proposals_limit: 10, + context_storage: HashMap::new(), + ledger_id: ledger_id.into(), + } + } +} + +thread_local! { + static PROXY_CONTRACT: RefCell = RefCell::new(ICProxyContract::default()); +} + +#[ic_cdk::init] +fn init(context_id: types::ICContextId, ledger_id: Principal) { + PROXY_CONTRACT.with(|contract| { + *contract.borrow_mut() = ICProxyContract::new(context_id, ledger_id); + }); +} + +ic_cdk::export_candid!(); diff --git a/contracts/icp/proxy-contract/src/mutate.rs b/contracts/icp/proxy-contract/src/mutate.rs new file mode 100644 index 000000000..eaa267eb5 --- /dev/null +++ b/contracts/icp/proxy-contract/src/mutate.rs @@ -0,0 +1,286 @@ +use std::collections::BTreeSet; + +use calimero_context_config::repr::ReprTransmute; +use candid::Principal; +use ic_cdk::api::call::CallResult; +use ic_ledger_types::{AccountIdentifier, Memo, Subaccount, Tokens, TransferArgs, TransferError}; + +use crate::types::*; +use crate::{ICProxyContract, PROXY_CONTRACT}; + +async fn check_member(signer_id: &ICSignerId) -> Result { + let (context_canister_id, context_id) = PROXY_CONTRACT.with(|contract| { + ( + contract.borrow().context_config_id.clone(), + contract.borrow().context_id.clone(), + ) + }); + + let identity = ICContextIdentity::new(signer_id.rt().expect("Invalid signer id")); + + let call_result: CallResult<(bool,)> = ic_cdk::call( + Principal::from_text(&context_canister_id) + .map_err(|e| format!("Invalid context canister ID: {}", e))?, + "has_member", + (context_id, identity), + ) + .await; + + match call_result { + Ok((is_member,)) => Ok(is_member), + Err(e) => Err(format!("Error checking membership: {:?}", e)), + } +} + +#[ic_cdk::update] +async fn mutate( + signed_request: ICPSigned, +) -> Result, String> { + let request = signed_request + .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 { + return Err("request expired".to_string()); + } + + // Check membership + if !check_member(&request.signer_id).await? { + return Err("signer is not a member".to_string()); + } + + match request.kind { + ICRequestKind::Propose { proposal } => internal_create_proposal(proposal), + ICRequestKind::Approve { approval } => { + internal_approve_proposal( + approval.signer_id, + approval.proposal_id, + approval.added_timestamp, + ) + .await + } + } +} + +async fn internal_approve_proposal( + signer_id: ICSignerId, + proposal_id: ICProposalId, + _added_timestamp: u64, +) -> Result, String> { + // First phase: Update approvals and check if we need to execute + let should_execute = PROXY_CONTRACT.with(|contract| { + let mut contract = contract.borrow_mut(); + + // Check if proposal exists + if !contract.proposals.contains_key(&proposal_id) { + return Err("proposal does not exist".to_string()); + } + + let approvals = contract.approvals.entry(proposal_id).or_default(); + + if !approvals.insert(signer_id) { + return Err("proposal already approved".to_string()); + } + + Ok(approvals.len() as u32 >= contract.num_approvals) + })?; + + // Execute if needed + if should_execute { + execute_proposal(&proposal_id).await?; + } + + // Build final response + PROXY_CONTRACT.with(|contract| { + let contract = contract.borrow(); + build_proposal_response(&*contract, proposal_id) + }) +} + +async fn execute_proposal(proposal_id: &ICProposalId) -> Result<(), String> { + let proposal = PROXY_CONTRACT.with(|contract| { + let contract = contract.borrow(); + contract + .proposals + .get(proposal_id) + .cloned() + .ok_or_else(|| "proposal does not exist".to_string()) + })?; + + // Execute each action + for action in proposal.actions { + match action { + ICProposalAction::ExternalFunctionCall { + receiver_id, + method_name, + args, + deposit: _, + } => { + let args_bytes = candid::encode_one(args) + .map_err(|e| format!("Failed to encode args: {}", e))?; + + let _: () = ic_cdk::call(receiver_id, method_name.as_str(), (args_bytes,)) + .await + .map_err(|e| format!("Inter-canister call failed: {:?}", e))?; + } + ICProposalAction::Transfer { + receiver_id, + amount, + } => { + let ledger_id = PROXY_CONTRACT.with(|contract| contract.borrow().ledger_id.clone()); + + let transfer_args = TransferArgs { + memo: Memo(0), + amount: Tokens::from_e8s( + amount + .try_into() + .map_err(|e| format!("Amount conversion error: {}", e))?, + ), + fee: Tokens::from_e8s(10_000), // Standard fee is 0.0001 ICP + from_subaccount: None, + to: AccountIdentifier::new(&receiver_id, &Subaccount([0; 32])), + created_at_time: None, + }; + + let _: (Result,) = + ic_cdk::call(Principal::from(ledger_id), "transfer", (transfer_args,)) + .await + .map_err(|e| format!("Transfer failed: {:?}", e))?; + } + ICProposalAction::SetNumApprovals { num_approvals } => { + PROXY_CONTRACT.with(|contract| { + let mut contract = contract.borrow_mut(); + contract.num_approvals = num_approvals; + }); + } + ICProposalAction::SetActiveProposalsLimit { + active_proposals_limit, + } => { + PROXY_CONTRACT.with(|contract| { + let mut contract = contract.borrow_mut(); + contract.active_proposals_limit = active_proposals_limit; + }); + } + ICProposalAction::SetContextValue { key, value } => { + PROXY_CONTRACT.with(|contract| { + let mut contract = contract.borrow_mut(); + contract.context_storage.insert(key.clone(), value.clone()); + }); + } + } + } + + remove_proposal(proposal_id); + Ok(()) +} + +fn internal_create_proposal( + proposal: ICProposal, +) -> Result, String> { + if proposal.actions.is_empty() { + return Err("proposal cannot have empty actions".to_string()); + } + + PROXY_CONTRACT.with(|contract| { + let mut contract = contract.borrow_mut(); + + let num_proposals = contract + .num_proposals_pk + .get(&proposal.author_id) + .copied() + .unwrap_or(0); + + // Check proposal limit + if num_proposals >= contract.active_proposals_limit { + return Err( + "Account has too many active proposals. Confirm or delete some.".to_string(), + ); + } + + // Validate proposal actions + for action in &proposal.actions { + validate_proposal_action(action)?; + } + + // Store proposal + let proposal_id = proposal.id; + let author_id = proposal.author_id; + contract.proposals.insert(proposal_id, proposal); + + // Initialize approvals set with author's approval + let approvals = BTreeSet::from([author_id]); + contract.approvals.insert(proposal_id, approvals); + + // Update proposal count + *contract.num_proposals_pk.entry(author_id).or_insert(0) += 1; + + build_proposal_response(&*contract, proposal_id) + }) +} + +fn validate_proposal_action(action: &ICProposalAction) -> Result<(), String> { + match action { + ICProposalAction::ExternalFunctionCall { + receiver_id: _, + method_name, + args: _, + deposit: _, + } => { + if method_name.is_empty() { + return Err("method name cannot be empty".to_string()); + } + } + ICProposalAction::Transfer { + receiver_id: _, + amount, + } => { + if *amount == 0 { + return Err("transfer amount cannot be zero".to_string()); + } + } + ICProposalAction::SetNumApprovals { num_approvals } => { + if *num_approvals == 0 { + return Err("num approvals cannot be zero".to_string()); + } + } + ICProposalAction::SetActiveProposalsLimit { + active_proposals_limit, + } => { + if *active_proposals_limit == 0 { + return Err("active proposals limit cannot be zero".to_string()); + } + } + ICProposalAction::SetContextValue { .. } => {} + } + Ok(()) +} + +fn remove_proposal(proposal_id: &ICProposalId) { + PROXY_CONTRACT.with(|contract| { + let mut contract = contract.borrow_mut(); + contract.approvals.remove(&proposal_id); + if let Some(proposal) = contract.proposals.remove(&proposal_id) { + let author_id = proposal.author_id; + if let Some(count) = contract.num_proposals_pk.get_mut(&author_id) { + *count = count.saturating_sub(1); + if *count == 0 { + contract.num_proposals_pk.remove(&author_id); + } + } + } + }); +} + +fn build_proposal_response( + contract: &ICProxyContract, + proposal_id: ICProposalId, +) -> Result, String> { + let approvals = contract.approvals.get(&proposal_id); + + Ok(approvals.map(|approvals| ICProposalWithApprovals { + proposal_id, + num_approvals: approvals.len(), + })) +} diff --git a/contracts/icp/proxy-contract/src/query.rs b/contracts/icp/proxy-contract/src/query.rs new file mode 100644 index 000000000..c9e4eaba6 --- /dev/null +++ b/contracts/icp/proxy-contract/src/query.rs @@ -0,0 +1,111 @@ +use crate::types::*; +use crate::PROXY_CONTRACT; + +#[ic_cdk::query] +pub fn get_num_approvals() -> u32 { + PROXY_CONTRACT.with(|contract| { + let contract = contract.borrow(); + contract.num_approvals + }) +} + +#[ic_cdk::query] +pub fn get_active_proposals_limit() -> u32 { + PROXY_CONTRACT.with(|contract| { + let contract = contract.borrow(); + contract.active_proposals_limit + }) +} + +#[ic_cdk::query] +pub fn proposal(proposal_id: ICProposalId) -> Option { + PROXY_CONTRACT.with(|contract| { + let contract = contract.borrow(); + contract.proposals.get(&proposal_id).cloned() + }) +} + +#[ic_cdk::query] +pub fn proposals(from_index: usize, limit: usize) -> Vec { + PROXY_CONTRACT.with(|contract| { + let contract = contract.borrow(); + contract + .proposals + .values() + .skip(from_index) + .take(limit) + .cloned() + .collect() + }) +} + +#[ic_cdk::query] +pub fn get_confirmations_count(proposal_id: ICProposalId) -> Option { + PROXY_CONTRACT.with(|contract| { + let contract = contract.borrow(); + contract.proposals.get(&proposal_id).map(|_| { + let num_approvals = contract + .approvals + .get(&proposal_id) + .map_or(0, |approvals| approvals.len()); + ICProposalWithApprovals { + proposal_id, + num_approvals, + } + }) + }) +} + +#[ic_cdk::query] +pub fn get_proposal_approvers(proposal_id: ICProposalId) -> Option> { + PROXY_CONTRACT.with(|contract| { + let contract = contract.borrow(); + contract + .approvals + .get(&proposal_id) + .map(|approvals| approvals.iter().cloned().collect()) + }) +} + +#[ic_cdk::query] +pub fn get_proposal_approvals_with_signer( + proposal_id: ICProposalId, +) -> Vec { + PROXY_CONTRACT.with(|contract| { + let contract = contract.borrow(); + if let Some(approvals) = contract.approvals.get(&proposal_id) { + approvals + .iter() + .map(|signer_id| ICProposalApprovalWithSigner { + proposal_id: proposal_id.clone(), + signer_id: signer_id.clone(), + added_timestamp: 0, // TODO: We need to store approval timestamps + }) + .collect() + } else { + vec![] + } + }) +} + +#[ic_cdk::query] +pub fn get_context_value(key: Vec) -> Option> { + PROXY_CONTRACT.with(|contract| { + let contract = contract.borrow(); + contract.context_storage.get(&key).cloned() + }) +} + +#[ic_cdk::query] +pub fn context_storage_entries(from_index: usize, limit: usize) -> Vec<(Vec, Vec)> { + PROXY_CONTRACT.with(|contract| { + let contract = contract.borrow(); + contract + .context_storage + .iter() + .skip(from_index) + .take(limit) + .map(|(k, v)| (k.clone(), v.clone())) + .collect() + }) +} diff --git a/contracts/icp/proxy-contract/src/sys.rs b/contracts/icp/proxy-contract/src/sys.rs new file mode 100644 index 000000000..dcc6d2de5 --- /dev/null +++ b/contracts/icp/proxy-contract/src/sys.rs @@ -0,0 +1,47 @@ +use candid::{CandidType, Deserialize, Principal}; +use ic_cdk; + +use crate::{ICProxyContract, PROXY_CONTRACT}; + +#[derive(CandidType, Deserialize)] +struct StableStorage { + proxy_contract: ICProxyContract, +} + +#[ic_cdk::pre_upgrade] +fn pre_upgrade() { + // Verify caller is the context contract that created this proxy + let caller = ic_cdk::caller(); + let context_canister = PROXY_CONTRACT.with(|contract| { + Principal::from_text(&contract.borrow().context_config_id) + .expect("Invalid context canister ID") + }); + + if caller != context_canister { + ic_cdk::trap("unauthorized: only context contract can upgrade proxy"); + } + + // Store the contract state + let state = PROXY_CONTRACT.with(|contract| StableStorage { + proxy_contract: contract.borrow().clone(), + }); + + // Write state to stable storage + match ic_cdk::storage::stable_save((state,)) { + Ok(_) => (), + Err(err) => ic_cdk::trap(&format!("Failed to save stable storage: {}", err)), + } +} + +#[ic_cdk::post_upgrade] +fn post_upgrade() { + // Restore the contract state + match ic_cdk::storage::stable_restore::<(StableStorage,)>() { + Ok((state,)) => { + PROXY_CONTRACT.with(|contract| { + *contract.borrow_mut() = state.proxy_contract; + }); + } + Err(err) => ic_cdk::trap(&format!("Failed to restore stable storage: {}", err)), + } +} diff --git a/contracts/icp/proxy-contract/src/types.rs b/contracts/icp/proxy-contract/src/types.rs new file mode 100644 index 000000000..ca9d9bf97 --- /dev/null +++ b/contracts/icp/proxy-contract/src/types.rs @@ -0,0 +1,327 @@ +use std::marker::PhantomData; + +use bs58::decode::Result as Bs58Result; +use calimero_context_config::repr; +use calimero_context_config::repr::{LengthMismatch, ReprBytes, ReprTransmute}; +use calimero_context_config::types::IntoResult; +use candid::{CandidType, Principal}; +use ed25519_dalek::{Signature, Verifier, VerifyingKey}; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use thiserror::Error as ThisError; + +/// Base identity type +#[derive( + CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Hash, +)] +pub struct Identity([u8; 32]); + +impl Identity { + pub fn new(bytes: [u8; 32]) -> Self { + Self(bytes) + } + + pub fn as_bytes(&self) -> [u8; 32] { + self.0 + } + + pub fn as_slice(&self) -> &[u8] { + &self.0[..] + } +} + +impl Default for Identity { + fn default() -> Self { + Self([0; 32]) + } +} + +impl ReprBytes for Identity { + type EncodeBytes<'a> = [u8; 32]; + type DecodeBytes = [u8; 32]; + type Error = LengthMismatch; + + fn as_bytes(&self) -> Self::EncodeBytes<'_> { + self.0 + } + + fn from_bytes(f: F) -> repr::Result + where + F: FnOnce(&mut Self::DecodeBytes) -> Bs58Result, + { + Self::DecodeBytes::from_bytes(f).map(Self) + } +} + +#[derive( + CandidType, Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq, Copy, Ord, PartialOrd, +)] +pub struct ICSignerId(Identity); + +impl ICSignerId { + pub fn new(bytes: [u8; 32]) -> Self { + Self(Identity(bytes)) + } +} + +impl Default for ICSignerId { + fn default() -> Self { + Self(Identity::default()) + } +} + +impl ReprBytes for ICSignerId { + type EncodeBytes<'a> = [u8; 32]; + type DecodeBytes = [u8; 32]; + type Error = LengthMismatch; + + fn as_bytes(&self) -> Self::EncodeBytes<'_> { + self.0.as_bytes() + } + + fn from_bytes(f: F) -> repr::Result + where + F: FnOnce(&mut Self::DecodeBytes) -> Bs58Result, + { + Identity::from_bytes(f).map(Self) + } +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)] +pub struct ICContextId(Identity); + +impl ICContextId { + pub fn new(bytes: [u8; 32]) -> Self { + Self(Identity(bytes)) + } +} + +impl Default for ICContextId { + fn default() -> Self { + Self(Identity::default()) + } +} + +impl ReprBytes for ICContextId { + type EncodeBytes<'a> = [u8; 32]; + type DecodeBytes = [u8; 32]; + type Error = LengthMismatch; + + fn as_bytes(&self) -> Self::EncodeBytes<'_> { + self.0.as_bytes() + } + + fn from_bytes(f: F) -> repr::Result + where + F: FnOnce(&mut Self::DecodeBytes) -> Bs58Result, + { + Identity::from_bytes(f).map(Self) + } +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)] +pub struct ICContextIdentity(Identity); + +impl ICContextIdentity { + pub fn new(bytes: [u8; 32]) -> Self { + Self(Identity(bytes)) + } +} + +impl ReprBytes for ICContextIdentity { + type EncodeBytes<'a> = [u8; 32]; + type DecodeBytes = [u8; 32]; + type Error = LengthMismatch; + + fn as_bytes(&self) -> Self::EncodeBytes<'_> { + self.0.as_bytes() + } + + fn from_bytes(f: F) -> repr::Result + where + F: FnOnce(&mut Self::DecodeBytes) -> Bs58Result, + { + Identity::from_bytes(f).map(Self) + } +} + +#[derive( + CandidType, Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, +)] +pub struct ICProposalId(pub [u8; 32]); + +impl ICProposalId { + pub fn new(bytes: [u8; 32]) -> Self { + Self(bytes) + } +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq)] +pub enum ICProposalAction { + ExternalFunctionCall { + receiver_id: Principal, + method_name: String, + args: String, + deposit: u128, + }, + Transfer { + receiver_id: Principal, + amount: u128, + }, + SetNumApprovals { + num_approvals: u32, + }, + SetActiveProposalsLimit { + active_proposals_limit: u32, + }, + SetContextValue { + key: Vec, + value: Vec, + }, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct ICProposal { + pub id: ICProposalId, + pub author_id: ICSignerId, + pub actions: Vec, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct ICProposalWithApprovals { + pub proposal_id: ICProposalId, + pub num_approvals: usize, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct ICProposalApprovalWithSigner { + pub proposal_id: ICProposalId, + pub signer_id: ICSignerId, + pub added_timestamp: u64, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub enum ICRequestKind { + Propose { + proposal: ICProposal, + }, + Approve { + approval: ICProposalApprovalWithSigner, + }, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct ICRequest { + pub kind: ICRequestKind, + pub signer_id: ICSignerId, + pub timestamp_ms: u64, +} + +#[derive(CandidType, Deserialize, Debug, Clone)] +pub struct ICPSigned { + payload: Vec, + signature: Vec, + _phantom: Phantom, +} + +impl ICPSigned { + pub fn new(payload: T, sign: F) -> Result> + where + R: IntoResult, + F: FnOnce(&[u8]) -> R, + { + let bytes = candid::encode_one(payload) + .map_err(|e| ICPSignedError::SerializationError(e.to_string()))?; + + let signature = sign(&bytes) + .into_result() + .map_err(ICPSignedError::DerivationError)?; + + Ok(Self { + payload: bytes, + signature: signature.to_vec(), + _phantom: Phantom(PhantomData), + }) + } + + pub fn parse(&self, f: F) -> Result> + where + R: IntoResult, + F: FnOnce(&T) -> R, + { + let parsed: T = candid::decode_one(&self.payload) + .map_err(|e| ICPSignedError::DeserializationError(e.to_string()))?; + + let signer_id = f(&parsed) + .into_result() + .map_err(ICPSignedError::DerivationError)?; + + let key = signer_id + .rt::() + .map_err(|_| ICPSignedError::InvalidPublicKey)?; + + let signature_bytes: [u8; 64] = + self.signature.as_slice().try_into().map_err(|_| { + ICPSignedError::SignatureError(ed25519_dalek::ed25519::Error::new()) + })?; + let signature = ed25519_dalek::Signature::from_bytes(&signature_bytes); + + key.verify(&self.payload, &signature) + .map_err(|_| ICPSignedError::InvalidSignature)?; + + Ok(parsed) + } +} + +#[derive(Debug, ThisError)] +pub enum ICPSignedError { + #[error("invalid signature")] + InvalidSignature, + #[error("derivation error: {0}")] + DerivationError(E), + #[error("invalid public key")] + InvalidPublicKey, + #[error("signature error: {0}")] + SignatureError(#[from] ed25519_dalek::ed25519::Error), + #[error("serialization error: {0}")] + SerializationError(String), + #[error("deserialization error: {0}")] + DeserializationError(String), +} + +#[derive(Deserialize, Debug, Clone)] +struct Phantom(#[serde(skip)] std::marker::PhantomData); + +impl CandidType for Phantom { + fn _ty() -> candid::types::Type { + candid::types::TypeInner::Null.into() + } + + fn idl_serialize(&self, serializer: S) -> Result<(), S::Error> + where + S: candid::types::Serializer, + { + serializer.serialize_null(()) + } +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct LedgerId(Principal); + +impl Default for LedgerId { + fn default() -> Self { + Self(Principal::anonymous()) + } +} + +impl From for LedgerId { + fn from(p: Principal) -> Self { + Self(p) + } +} + +impl From for Principal { + fn from(id: LedgerId) -> Self { + id.0 + } +} diff --git a/contracts/icp/proxy-contract/tests/context_types.rs b/contracts/icp/proxy-contract/tests/context_types.rs new file mode 100644 index 000000000..ea7b964d4 --- /dev/null +++ b/contracts/icp/proxy-contract/tests/context_types.rs @@ -0,0 +1,60 @@ +use candid::CandidType; +use proxy_contract::types::{ICContextId, ICContextIdentity, ICSignerId}; +use serde::{Deserialize, Serialize}; + +#[derive(CandidType, Serialize, Deserialize, Debug, Clone)] +pub struct Request { + pub kind: RequestKind, + pub signer_id: ICSignerId, + pub timestamp_ms: u64, +} + +#[derive(CandidType, Serialize, Deserialize, Debug, Clone)] +pub enum RequestKind { + Context(ContextRequest), +} + +#[derive(CandidType, Serialize, Deserialize, Debug, Clone)] +pub struct ContextRequest { + pub context_id: ICContextId, + pub kind: ContextRequestKind, +} + +#[derive(CandidType, Serialize, Deserialize, Debug, Clone)] +pub enum ContextRequestKind { + Add { + author_id: ICContextIdentity, + application: ICApplication, + }, + AddMembers { + members: Vec, + }, + UpdateProxyContract, +} + +#[derive(CandidType, Serialize, Deserialize, Debug, Clone)] +pub struct ICApplication { + pub id: ICApplicationId, + pub blob: ICBlobId, + pub size: u64, + pub source: String, + pub metadata: Vec, +} + +#[derive(CandidType, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct ICApplicationId(pub [u8; 32]); + +#[derive(CandidType, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct ICBlobId(pub [u8; 32]); + +impl ICApplicationId { + pub fn new(bytes: [u8; 32]) -> Self { + Self(bytes) + } +} + +impl ICBlobId { + pub fn new(bytes: [u8; 32]) -> Self { + Self(bytes) + } +} diff --git a/contracts/icp/proxy-contract/tests/integration.rs b/contracts/icp/proxy-contract/tests/integration.rs new file mode 100644 index 000000000..0da011745 --- /dev/null +++ b/contracts/icp/proxy-contract/tests/integration.rs @@ -0,0 +1,1125 @@ +mod context_types; +use context_types::*; + +#[cfg(test)] +mod tests { + use std::cell::RefCell; + use std::time::UNIX_EPOCH; + + use calimero_context_config::repr::ReprBytes; + use candid::Principal; + use ed25519_dalek::{Signer, SigningKey}; + use ic_ledger_types::{AccountBalanceArgs, AccountIdentifier, Subaccount, Tokens}; + use pocket_ic::{PocketIc, WasmResult}; + use proxy_contract::types::{ + ICContextId, ICContextIdentity, ICPSigned, ICProposal, ICProposalAction, + ICProposalApprovalWithSigner, ICProposalId, ICProposalWithApprovals, ICRequest, + ICRequestKind, ICSignerId, + }; + use rand::Rng; + + use crate::{ + ContextRequest, ContextRequestKind, ICApplication, ICApplicationId, ICBlobId, Request, + RequestKind, + }; + + // Mock canister states + thread_local! { + static MOCK_LEDGER_BALANCE: RefCell = RefCell::new(1_000_000_000); + static MOCK_EXTERNAL_CALLS: RefCell)>> = RefCell::new(Vec::new()); + } + + fn create_signed_request(signer_key: &SigningKey, request: ICRequest) -> ICPSigned { + ICPSigned::new(request, |bytes| signer_key.sign(bytes)) + .expect("Failed to create signed request") + } + + fn create_signed_context_request( + signer_key: &SigningKey, + request: Request, + ) -> ICPSigned { + ICPSigned::new(request, |bytes| signer_key.sign(bytes)) + .expect("Failed to create signed request") + } + + fn get_time_nanos(pic: &PocketIc) -> u64 { + pic.get_time() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_nanos() as u64 + } + + // Helper function to create a proposal and verify response + fn create_and_verify_proposal( + pic: &PocketIc, + canister: Principal, + signer_sk: &SigningKey, + signer_id: &ICSignerId, + proposal: ICProposal, + ) -> Result { + let request = ICRequest { + signer_id: signer_id.clone(), + timestamp_ms: get_time_nanos(pic), + kind: ICRequestKind::Propose { + proposal: proposal.clone(), + }, + }; + + let signed_request = create_signed_request(signer_sk, request); + let response = pic + .update_call( + canister, + Principal::anonymous(), + "mutate", + candid::encode_one(signed_request).unwrap(), + ) + .map_err(|e| format!("Failed to call canister: {}", e))?; + + match response { + WasmResult::Reply(bytes) => { + let result: Result, String> = + candid::decode_one(&bytes) + .map_err(|e| format!("Failed to decode response: {}", e))?; + + match result { + Ok(Some(proposal_with_approvals)) => Ok(proposal_with_approvals), + Ok(None) => Err("No proposal returned".to_string()), + Err(e) => Err(e), + } + } + WasmResult::Reject(msg) => Err(format!("Canister rejected the call: {}", msg)), + } + } + + struct ProxyTestContext { + pic: PocketIc, + proxy_canister: Principal, + context_canister: Principal, + mock_ledger: Principal, + mock_external: Principal, + author_sk: SigningKey, + context_id: ICContextId, + } + + fn setup() -> ProxyTestContext { + let pic = PocketIc::new(); + let mut rng = rand::thread_rng(); + + // Setup context contract first + let context_canister = pic.create_canister(); + pic.add_cycles(context_canister, 100_000_000_000_000_000); + let context_wasm = std::fs::read("../context-config/res/context_contract.wasm") + .expect("failed to read context wasm"); + pic.install_canister(context_canister, context_wasm, vec![], None); + + // Setup mock ledger + let mock_ledger = pic.create_canister(); + pic.add_cycles(mock_ledger, 100_000_000_000_000); + let mock_ledger_wasm = std::fs::read("mock/ledger/res/mock_ledger.wasm") + .expect("failed to read mock ledger wasm"); + pic.install_canister(mock_ledger, mock_ledger_wasm, vec![], None); + + // Set proxy code in context contract + set_proxy_code(&pic, context_canister, mock_ledger).expect("Failed to set proxy code"); + + // Setup mock external + let mock_external = pic.create_canister(); + pic.add_cycles(mock_external, 100_000_000_000_000); + let mock_external_wasm = std::fs::read("mock/external/res/mock_external.wasm") + .expect("failed to read mock external wasm"); + pic.install_canister(mock_external, mock_external_wasm, vec![], None); + + // Create initial author key + let author_sk = SigningKey::from_bytes(&rng.gen()); + + // Create context and get proxy canister + let (proxy_canister, context_id) = + create_context_with_proxy(&pic, context_canister, &author_sk) + .expect("Failed to create context and proxy"); + + ProxyTestContext { + pic, + proxy_canister, + context_canister, + mock_ledger, + mock_external, + author_sk, + context_id, + } + } + + // Helper function to set proxy code in context contract + fn set_proxy_code( + pic: &PocketIc, + context_canister: Principal, + ledger_id: Principal, + ) -> Result<(), String> { + // Read proxy contract wasm + let proxy_wasm = + std::fs::read("res/proxy_contract.wasm").expect("failed to read proxy wasm"); + + let response = pic.update_call( + context_canister, + Principal::anonymous(), + "set_proxy_code", + candid::encode_args((proxy_wasm, ledger_id)).unwrap(), + ); + + match response { + Ok(WasmResult::Reply(bytes)) => { + let result: Result<(), String> = candid::decode_one(&bytes) + .map_err(|e| format!("Failed to decode response: {}", e))?; + result + } + Ok(WasmResult::Reject(msg)) => Err(format!("Setting proxy code rejected: {}", msg)), + Err(e) => Err(format!("Setting proxy code failed: {}", e)), + } + } + + // Helper function to create context and deploy proxy + fn create_context_with_proxy( + pic: &PocketIc, + context_canister: Principal, + author_sk: &SigningKey, + ) -> Result<(Principal, ICContextId), String> { + let mut rng = rand::thread_rng(); + + // Generate context ID + let context_sk = SigningKey::from_bytes(&rng.gen()); + let context_pk = context_sk.verifying_key(); + let context_id = ICContextId::new(context_pk.to_bytes()); + + // Create author identity + let author_pk = author_sk.verifying_key(); + let author_id = ICContextIdentity::new(author_pk.to_bytes()); + + // Create context with initial application + let create_request = Request { + kind: RequestKind::Context(ContextRequest { + context_id: context_id.clone(), + kind: ContextRequestKind::Add { + author_id: author_id.clone(), + application: ICApplication { + id: ICApplicationId::new(rng.gen()), + blob: ICBlobId::new(rng.gen()), + size: 0, + source: String::new(), + metadata: vec![], + }, + }, + }), + signer_id: ICSignerId::new(context_id.as_bytes()), + timestamp_ms: get_time_nanos(pic), + }; + + let signed_request = create_signed_context_request(&context_sk, create_request); + let response = pic.update_call( + context_canister, + Principal::anonymous(), + "mutate", + candid::encode_one(signed_request).unwrap(), + ); + + // Check if context creation succeeded + match response { + Ok(WasmResult::Reply(bytes)) => { + let result: Result<(), String> = candid::decode_one(&bytes) + .map_err(|e| format!("Failed to decode response: {}", e))?; + result.map_err(|e| format!("Context creation failed: {}", e))?; + } + Ok(WasmResult::Reject(msg)) => { + return Err(format!("Context creation rejected: {}", msg)) + } + Err(e) => return Err(format!("Context creation failed: {}", e)), + } + + // Query for proxy canister ID + let query_response = pic.query_call( + context_canister, + Principal::anonymous(), + "proxy_contract", + candid::encode_one(context_id.clone()).unwrap(), + ); + + match query_response { + Ok(WasmResult::Reply(bytes)) => { + let proxy_canister: Principal = candid::decode_one(&bytes) + .map_err(|e| format!("Failed to decode proxy canister ID: {}", e))?; + Ok((proxy_canister, context_id)) + } + Ok(WasmResult::Reject(msg)) => Err(format!("Query rejected: {}", msg)), + Err(e) => Err(format!("Query failed: {}", e)), + } + } + + // Helper function to add members to context + fn add_members_to_context( + pic: &PocketIc, + context_canister: Principal, + context_id: &ICContextId, + author_sk: &SigningKey, + members: Vec, + ) -> Result<(), String> { + let author_pk = author_sk.verifying_key(); + let request = Request { + kind: RequestKind::Context(ContextRequest { + context_id: context_id.clone(), + kind: ContextRequestKind::AddMembers { members }, + }), + signer_id: ICSignerId::new(author_pk.to_bytes()), + timestamp_ms: get_time_nanos(pic), + }; + + let signed_request = create_signed_context_request(author_sk, request); + let response = pic.update_call( + context_canister, + Principal::anonymous(), + "mutate", + candid::encode_one(signed_request).unwrap(), + ); + + match response { + Ok(WasmResult::Reply(bytes)) => { + candid::decode_one(&bytes).map_err(|e| format!("Failed to decode response: {}", e)) + } + Ok(WasmResult::Reject(msg)) => Err(format!("Adding members rejected: {}", msg)), + Err(e) => Err(format!("Adding members failed: {}", e)), + } + } + + #[test] + fn test_update_proxy_contract() { + let ProxyTestContext { + pic, + proxy_canister, + context_canister, + author_sk, + context_id, + .. + } = setup(); + + // First test: Try direct upgrade (should fail) + let proxy_wasm = + std::fs::read("res/proxy_contract.wasm").expect("failed to read proxy wasm"); + + let unauthorized_result = pic.upgrade_canister( + proxy_canister, + proxy_wasm.clone(), + candid::encode_one::>(vec![]).unwrap(), + Some(Principal::anonymous()), + ); + match unauthorized_result { + Ok(_) => panic!("Direct upgrade should fail"), + Err(e) => { + println!("Got expected unauthorized error: {:?}", e); + } + } + + // Now continue with the rest of the test (authorized upgrade through context) + let author_pk = author_sk.verifying_key(); + let author_id = ICSignerId::new(author_pk.to_bytes()); + + let proposal = ICProposal { + id: ICProposalId::new([1; 32]), + author_id: author_id.clone(), + actions: vec![ICProposalAction::Transfer { + receiver_id: Principal::anonymous(), + amount: 1000000, + }], + }; + + create_and_verify_proposal(&pic, proxy_canister, &author_sk, &author_id, proposal) + .expect("Transfer proposal creation should succeed"); + + // Query initial state - get the proposal + let initial_proposal = pic + .query_call( + proxy_canister, + Principal::anonymous(), + "proposal", + candid::encode_one(ICProposalId::new([1; 32])).unwrap(), + ) + .and_then(|r| match r { + WasmResult::Reply(bytes) => { + Ok(candid::decode_one::>(&bytes).unwrap()) + } + _ => panic!("Unexpected response type"), + }) + .expect("Query failed") + .expect("Proposal not found"); + + // Create update request to context contract + let update_request = Request { + kind: RequestKind::Context(ContextRequest { + context_id: context_id.clone(), + kind: ContextRequestKind::UpdateProxyContract, + }), + signer_id: ICSignerId::new(author_pk.to_bytes()), + timestamp_ms: get_time_nanos(&pic), + }; + + let signed_update_request = create_signed_context_request(&author_sk, update_request); + let response = pic.update_call( + context_canister, + Principal::anonymous(), + "mutate", + candid::encode_one(signed_update_request).unwrap(), + ); + + // Handle the response directly + match response { + Ok(WasmResult::Reply(bytes)) => { + let result: Result<(), String> = + candid::decode_one(&bytes).expect("Failed to decode response"); + assert!(result.is_ok(), "Context update should succeed"); + } + Ok(WasmResult::Reject(msg)) => panic!("Context update was rejected: {}", msg), + Err(e) => panic!("Context update failed: {}", e), + } + + // Verify state was preserved after upgrade + let final_proposal = pic + .query_call( + proxy_canister, + Principal::anonymous(), + "proposal", + candid::encode_one(ICProposalId::new([1; 32])).unwrap(), + ) + .and_then(|r| match r { + WasmResult::Reply(bytes) => { + Ok(candid::decode_one::>(&bytes).unwrap()) + } + _ => panic!("Unexpected response type"), + }) + .expect("Query failed") + .expect("Proposal not found"); + + assert_eq!( + initial_proposal, final_proposal, + "Proposal state not preserved after upgrade" + ); + } + + #[test] + fn test_create_proposal_transfer() { + let ProxyTestContext { + pic, + proxy_canister, + author_sk, + .. + } = setup(); + + let author_pk = author_sk.verifying_key(); + let author_id = ICSignerId::new(author_pk.to_bytes()); + + let proposal = ICProposal { + id: ICProposalId::new([1; 32]), + author_id: author_id.clone(), + actions: vec![ICProposalAction::Transfer { + receiver_id: Principal::anonymous(), + amount: 1000000, + }], + }; + + create_and_verify_proposal(&pic, proxy_canister, &author_sk, &author_id, proposal) + .expect("Transfer proposal creation should succeed"); + } + + #[test] + fn test_create_proposal_external_call() { + let ProxyTestContext { + pic, + proxy_canister, + author_sk, + .. + } = setup(); + + let author_pk = author_sk.verifying_key(); + let author_id = ICSignerId::new(author_pk.to_bytes()); + + let proposal = ICProposal { + id: ICProposalId::new([3; 32]), + author_id: author_id.clone(), + actions: vec![ICProposalAction::ExternalFunctionCall { + receiver_id: Principal::anonymous(), + method_name: "test_method".to_string(), + args: "deadbeef".to_string(), + deposit: 0, + }], + }; + + create_and_verify_proposal(&pic, proxy_canister, &author_sk, &author_id, proposal) + .expect("External call proposal creation should succeed"); + } + + #[test] + fn test_create_proposal_set_context() { + let ProxyTestContext { + pic, + proxy_canister, + author_sk, + .. + } = setup(); + + let author_pk = author_sk.verifying_key(); + let author_id = ICSignerId::new(author_pk.to_bytes()); + + let proposal = ICProposal { + id: ICProposalId::new([5; 32]), + author_id: author_id.clone(), + actions: vec![ICProposalAction::SetContextValue { + key: vec![1, 2, 3], + value: vec![4, 5, 6], + }], + }; + + create_and_verify_proposal(&pic, proxy_canister, &author_sk, &author_id, proposal) + .expect("Setting context value should succeed"); + } + + #[test] + fn test_create_proposal_multiple_actions() { + let ProxyTestContext { + pic, + proxy_canister, + author_sk, + .. + } = setup(); + + let author_pk = author_sk.verifying_key(); + let author_id = ICSignerId::new(author_pk.to_bytes()); + + let proposal = ICProposal { + id: ICProposalId::new([6; 32]), + author_id: author_id.clone(), + actions: vec![ + ICProposalAction::SetNumApprovals { num_approvals: 2 }, + ICProposalAction::SetActiveProposalsLimit { + active_proposals_limit: 5, + }, + ], + }; + + create_and_verify_proposal(&pic, proxy_canister, &author_sk, &author_id, proposal) + .expect("Multiple actions proposal creation should succeed"); + } + + #[test] + fn test_create_proposal_invalid_transfer_amount() { + let ProxyTestContext { + pic, + proxy_canister, + author_sk, + .. + } = setup(); + + let author_pk = author_sk.verifying_key(); + let author_id = ICSignerId::new(author_pk.to_bytes()); + + let proposal = ICProposal { + id: ICProposalId::new([8; 32]), + author_id: author_id.clone(), + actions: vec![ICProposalAction::Transfer { + receiver_id: Principal::anonymous(), + amount: 0, // Invalid amount + }], + }; + + let request = ICRequest { + signer_id: author_id.clone(), + timestamp_ms: get_time_nanos(&pic), + kind: ICRequestKind::Propose { proposal }, + }; + + let signed_request = create_signed_request(&author_sk, request); + let response = pic.update_call( + proxy_canister, + Principal::anonymous(), + "mutate", + candid::encode_one(signed_request).unwrap(), + ); + + match response { + Ok(WasmResult::Reply(bytes)) => { + let result: Result, String> = + candid::decode_one(&bytes).expect("Failed to decode response"); + assert!( + result.is_err(), + "Expected error for invalid transfer amount" + ); + } + Ok(WasmResult::Reject(msg)) => { + panic!("Canister rejected the call: {}", msg); + } + Err(err) => { + panic!("Failed to call canister: {}", err); + } + } + } + + #[test] + fn test_create_proposal_invalid_method_name() { + let ProxyTestContext { + pic, + proxy_canister, + author_sk, + .. + } = setup(); + + let author_pk = author_sk.verifying_key(); + let author_id = ICSignerId::new(author_pk.to_bytes()); + + let proposal = ICProposal { + id: ICProposalId::new([9; 32]), + author_id: author_id.clone(), + actions: vec![ICProposalAction::ExternalFunctionCall { + receiver_id: Principal::anonymous(), + method_name: "".to_string(), // Invalid method name + args: "deadbeef".to_string(), + deposit: 0, + }], + }; + + let request = ICRequest { + signer_id: author_id.clone(), + timestamp_ms: get_time_nanos(&pic), + kind: ICRequestKind::Propose { proposal }, + }; + + let signed_request = create_signed_request(&author_sk, request); + let response = pic.update_call( + proxy_canister, + Principal::anonymous(), + "mutate", + candid::encode_one(signed_request).unwrap(), + ); + + match response { + Ok(WasmResult::Reply(bytes)) => { + let result: Result, String> = + candid::decode_one(&bytes).expect("Failed to decode response"); + assert!(result.is_err(), "Expected error for invalid method name"); + } + Ok(WasmResult::Reject(msg)) => { + panic!("Canister rejected the call: {}", msg); + } + Err(err) => { + panic!("Failed to call canister: {}", err); + } + } + } + + #[test] + fn test_approve_own_proposal() { + let ProxyTestContext { + pic, + proxy_canister, + author_sk, + .. + } = setup(); + + let author_pk = author_sk.verifying_key(); + let author_id = ICSignerId::new(author_pk.to_bytes()); + + // Create proposal + let proposal = ICProposal { + id: ICProposalId::new([10; 32]), + author_id: author_id.clone(), + actions: vec![ICProposalAction::SetNumApprovals { num_approvals: 2 }], + }; + + let _ = create_and_verify_proposal( + &pic, + proxy_canister, + &author_sk, + &author_id, + proposal.clone(), + ); + + // Try to approve own proposal + let approval = ICProposalApprovalWithSigner { + signer_id: author_id.clone(), + proposal_id: proposal.id, + added_timestamp: get_time_nanos(&pic), + }; + + let request = ICRequest { + signer_id: author_id.clone(), + timestamp_ms: get_time_nanos(&pic), + kind: ICRequestKind::Approve { approval }, + }; + + let signed_request = create_signed_request(&author_sk, request); + let result = pic.update_call( + proxy_canister, + Principal::anonymous(), + "mutate", + candid::encode_one(signed_request).unwrap(), + ); + + match result { + Ok(WasmResult::Reply(bytes)) => { + let result: Result, String> = + candid::decode_one(&bytes).expect("Failed to decode response"); + assert!( + matches!(result, Err(e) if e.contains("already approved")), + "Should not be able to approve own proposal twice" + ); + } + _ => panic!("Unexpected response type"), + } + } + + #[test] + fn test_approve_non_existent_proposal() { + let ProxyTestContext { + pic, + proxy_canister, + author_sk, + .. + } = setup(); + + let author_pk = author_sk.verifying_key(); + let author_id = ICSignerId::new(author_pk.to_bytes()); + + let approval = ICProposalApprovalWithSigner { + signer_id: author_id.clone(), + proposal_id: ICProposalId::new([11; 32]), + added_timestamp: get_time_nanos(&pic), + }; + + let request = ICRequest { + signer_id: author_id.clone(), + timestamp_ms: get_time_nanos(&pic), + kind: ICRequestKind::Approve { approval }, + }; + + let signed_request = create_signed_request(&author_sk, request); + let response = pic.update_call( + proxy_canister, + Principal::anonymous(), + "mutate", + candid::encode_one(signed_request).unwrap(), + ); + + match response { + Ok(WasmResult::Reply(bytes)) => { + let result: Result, String> = + candid::decode_one(&bytes).expect("Failed to decode response"); + assert!( + result.is_err(), + "Should not be able to approve non-existent proposal" + ); + } + _ => panic!("Unexpected response type"), + } + } + + #[test] + fn test_create_proposal_empty_actions() { + let ProxyTestContext { + pic, + proxy_canister, + author_sk, + .. + } = setup(); + + let author_pk = author_sk.verifying_key(); + let author_id = ICSignerId::new(author_pk.to_bytes()); + + let proposal = ICProposal { + id: ICProposalId::new([12; 32]), + author_id: author_id.clone(), + actions: vec![], // Empty actions + }; + + let request = ICRequest { + signer_id: author_id.clone(), + timestamp_ms: get_time_nanos(&pic), + kind: ICRequestKind::Propose { proposal }, + }; + + let signed_request = create_signed_request(&author_sk, request); + let response = pic.update_call( + proxy_canister, + Principal::anonymous(), + "mutate", + candid::encode_one(signed_request).unwrap(), + ); + + match response { + Ok(WasmResult::Reply(bytes)) => { + let result: Result, String> = + candid::decode_one(&bytes).expect("Failed to decode response"); + assert!(result.is_err(), "Should fail with empty actions"); + assert!( + matches!(result, Err(e) if e.contains("empty actions")), + "Error should mention empty actions" + ); + } + _ => panic!("Unexpected response type"), + } + } + + #[test] + fn test_create_proposal_exceeds_limit() { + let ProxyTestContext { + pic, + proxy_canister, + author_sk, + .. + } = setup(); + + let author_pk = author_sk.verifying_key(); + let author_id = ICSignerId::new(author_pk.to_bytes()); + + // Create max number of proposals + for i in 0..10 { + let proposal = ICProposal { + id: ICProposalId::new([i as u8; 32]), + author_id: author_id.clone(), + actions: vec![ICProposalAction::SetNumApprovals { num_approvals: 2 }], + }; + let _ = + create_and_verify_proposal(&pic, proxy_canister, &author_sk, &author_id, proposal); + } + + // Try to create one more + let proposal = ICProposal { + id: ICProposalId::new([11; 32]), + author_id: author_id.clone(), + actions: vec![ICProposalAction::SetNumApprovals { num_approvals: 2 }], + }; + + let request = ICRequest { + signer_id: author_id.clone(), + timestamp_ms: get_time_nanos(&pic), + kind: ICRequestKind::Propose { proposal }, + }; + + let signed_request = create_signed_request(&author_sk, request); + let response = pic.update_call( + proxy_canister, + Principal::anonymous(), + "mutate", + candid::encode_one(signed_request).unwrap(), + ); + + match response { + Ok(WasmResult::Reply(bytes)) => { + let result: Result, String> = + candid::decode_one(&bytes).expect("Failed to decode response"); + assert!( + result.is_err(), + "Should not be able to exceed proposal limit" + ); + } + _ => panic!("Unexpected response type"), + } + } + + #[test] + fn test_proposal_execution_transfer() { + let ProxyTestContext { + pic, + proxy_canister, + mock_external, + mock_ledger, + author_sk, + context_canister, + context_id, + .. + } = setup(); + + let mut rng = rand::thread_rng(); + + let initial_balance = MOCK_LEDGER_BALANCE.with(|b| *b.borrow()); + + // Setup signers + let author_pk = author_sk.verifying_key(); + let author_id = ICSignerId::new(author_pk.to_bytes()); + + let signer2_sk = SigningKey::from_bytes(&rng.gen()); + let signer2_pk = signer2_sk.verifying_key(); + let signer2_id = ICSignerId::new(signer2_pk.to_bytes()); + + let signer3_sk = SigningKey::from_bytes(&rng.gen()); + let signer3_pk = signer3_sk.verifying_key(); + let signer3_id = ICSignerId::new(signer3_pk.to_bytes()); + + let transfer_amount = 1_000; + + // let receiver_id = Principal::from_text("bnz7o-iuaaa-aaaaa-qaaaa-cai").unwrap(); + // Create transfer proposal + let proposal = ICProposal { + id: ICProposalId::new([14; 32]), + author_id: author_id.clone(), + actions: vec![ICProposalAction::Transfer { + receiver_id: mock_external, + amount: transfer_amount, + }], + }; + + // Create and verify initial proposal + let _ = create_and_verify_proposal( + &pic, + proxy_canister, + &author_sk, + &author_id, + proposal.clone(), + ); + + let context_members = vec![ + ICContextIdentity::new(signer2_id.as_bytes()), + ICContextIdentity::new(signer3_id.as_bytes()), + ]; + + let _ = add_members_to_context( + &pic, + context_canister, + &context_id, + &author_sk, + context_members, + ); + + // Add approvals to trigger execution + for (signer_sk, signer_id) in [(signer2_sk, signer2_id), (signer3_sk, signer3_id)] { + let approval = ICProposalApprovalWithSigner { + signer_id: signer_id.clone(), + proposal_id: proposal.id.clone(), + added_timestamp: get_time_nanos(&pic), + }; + + let request = ICRequest { + signer_id, + timestamp_ms: get_time_nanos(&pic), + kind: ICRequestKind::Approve { approval }, + }; + + let signed_request = create_signed_request(&signer_sk, request); + let response = pic.update_call( + proxy_canister, + Principal::anonymous(), + "mutate", + candid::encode_one(signed_request).unwrap(), + ); + + // Last approval should trigger execution + match response { + Ok(WasmResult::Reply(bytes)) => { + let result: Result, String> = + candid::decode_one(&bytes).expect("Failed to decode response"); + match result { + Ok(Some(_proposal_with_approvals)) => {} + Ok(None) => { + // Proposal was executed and removed + // Verify proposal no longer exists + let query_response = pic + .query_call( + proxy_canister, + Principal::anonymous(), + "proposal", + candid::encode_one(proposal.id.clone()).unwrap(), + ) + .expect("Query failed"); + + match query_response { + WasmResult::Reply(bytes) => { + let stored_proposal: Option = + candid::decode_one(&bytes) + .expect("Failed to decode stored proposal"); + assert!( + stored_proposal.is_none(), + "Proposal should be removed after execution" + ); + } + WasmResult::Reject(msg) => { + panic!("Query rejected: {}", msg); + } + } + } + Err(e) => panic!("Unexpected error: {}", e), + } + } + _ => panic!("Unexpected response type"), + } + } + + let args = AccountBalanceArgs { + account: AccountIdentifier::new(&Principal::anonymous(), &Subaccount([0; 32])), + }; + + let response = pic + .query_call( + mock_ledger, + Principal::anonymous(), + "account_balance", + candid::encode_one(args).unwrap(), + ) + .expect("Failed to query balance"); + + match response { + WasmResult::Reply(bytes) => { + let balance: Tokens = candid::decode_one(&bytes).expect("Failed to decode balance"); + let final_balance = balance.e8s(); + // Verify the transfer was executed + assert_eq!( + final_balance, + initial_balance + .saturating_sub(transfer_amount as u64) + .saturating_sub(10_000), // Subtract both transfer amount and fee + "Transfer amount should be deducted from ledger balance" + ); + } + _ => panic!("Unexpected response type"), + } + } + + #[test] + fn test_proposal_execution_external_call() { + let ProxyTestContext { + pic, + proxy_canister, + mock_external, + author_sk, + context_canister, + context_id, + .. + } = setup(); + + let mut rng = rand::thread_rng(); + + let author_pk = author_sk.verifying_key(); + let author_id = ICSignerId::new(author_pk.to_bytes()); + + let signer2_sk = SigningKey::from_bytes(&rng.gen()); + let signer2_pk = signer2_sk.verifying_key(); + let signer2_id = ICSignerId::new(signer2_pk.to_bytes()); + + let signer3_sk = SigningKey::from_bytes(&rng.gen()); + let signer3_pk = signer3_sk.verifying_key(); + let signer3_id = ICSignerId::new(signer3_pk.to_bytes()); + + // Create external call proposal + let test_args = "01020304".to_string(); // Test arguments as string + let proposal = ICProposal { + id: ICProposalId::new([14; 32]), + author_id: author_id.clone(), + actions: vec![ICProposalAction::ExternalFunctionCall { + receiver_id: mock_external, + method_name: "test_method".to_string(), + args: test_args.clone(), + deposit: 0, + }], + }; + + // Create and verify initial proposal + let _ = create_and_verify_proposal( + &pic, + proxy_canister, + &author_sk, + &author_id, + proposal.clone(), + ); + + let context_members = vec![ + ICContextIdentity::new(signer2_id.as_bytes()), + ICContextIdentity::new(signer3_id.as_bytes()), + ]; + + let _ = add_members_to_context( + &pic, + context_canister, + &context_id, + &author_sk, + context_members, + ); + + // Add approvals to trigger execution + for (signer_sk, signer_id) in [(signer2_sk, signer2_id), (signer3_sk, signer3_id)] { + let approval = ICProposalApprovalWithSigner { + signer_id: signer_id.clone(), + proposal_id: proposal.id.clone(), + added_timestamp: get_time_nanos(&pic), + }; + + let request = ICRequest { + signer_id, + timestamp_ms: get_time_nanos(&pic), + kind: ICRequestKind::Approve { approval }, + }; + + let signed_request = create_signed_request(&signer_sk, request); + let response = pic.update_call( + proxy_canister, + Principal::anonymous(), + "mutate", + candid::encode_one(signed_request).unwrap(), + ); + + // Last approval should trigger execution + match response { + Ok(WasmResult::Reply(bytes)) => { + let result: Result, String> = + candid::decode_one(&bytes).expect("Failed to decode response"); + match result { + Ok(Some(_proposal_with_approvals)) => {} + Ok(None) => { + // Proposal was executed and removed + // Verify proposal no longer exists + let query_response = pic + .query_call( + proxy_canister, + Principal::anonymous(), + "proposal", + candid::encode_one(proposal.id.clone()).unwrap(), + ) + .expect("Query failed"); + + match query_response { + WasmResult::Reply(bytes) => { + let stored_proposal: Option = + candid::decode_one(&bytes) + .expect("Failed to decode stored proposal"); + assert!( + stored_proposal.is_none(), + "Proposal should be removed after execution" + ); + } + WasmResult::Reject(msg) => { + panic!("Query rejected: {}", msg); + } + } + } + Err(e) => panic!("Unexpected error: {}", e), + } + } + _ => panic!("Unexpected response type"), + } + } + + // Verify the external call was executed + let response = pic + .query_call( + mock_external, + Principal::anonymous(), + "get_calls", + candid::encode_args(()).unwrap(), + ) + .expect("Query failed"); + + match response { + WasmResult::Reply(bytes) => { + let calls: Vec> = + candid::decode_one(&bytes).expect("Failed to decode calls"); + assert_eq!(calls.len(), 1, "Should have exactly one call"); + + // Decode the Candid-encoded argument + let received_args: String = + candid::decode_one(&calls[0]).expect("Failed to decode call arguments"); + assert_eq!(received_args, test_args, "Call arguments should match"); + } + _ => panic!("Unexpected response type"), + } + } +} diff --git a/scripts/test.sh b/scripts/test.sh index 7da681894..5e31fbef4 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -13,6 +13,6 @@ cd "$(dirname $0)" ../contracts/context-config/build.sh ../contracts/proxy-lib/build-test-deps.sh ../contracts/icp/context-config/build.sh - +../contracts/icp/proxy-contract/build_mock_contracts.sh # Run cargo test cargo test