diff --git a/.cargo/config.toml b/.cargo/config.toml index 5704896780..861d0669fb 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -2,7 +2,7 @@ JEMALLOC_SYS_WITH_MALLOC_CONF = "background_thread:true,narenas:1,tcache:false,dirty_decay_ms:0,muzzy_decay_ms:0,metadata_thp:auto" [target.'cfg(all())'] -rustflags = [ "-Zshare-generics=y" ] +rustflags = [ "-Zshare-generics=y", '--cfg=curve25519_dalek_backend="fiat"' ] # # Install lld using package manager # [target.x86_64-unknown-linux-gnu] diff --git a/Cargo.lock b/Cargo.lock index 1e0790f40f..4dab14ced2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,25 +35,14 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aead" -version = "0.4.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "922b33332f54fc0ad13fa3e514601e8d30fb54e1f3eadc36643f6526db645621" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ + "crypto-common", "generic-array", ] -[[package]] -name = "aes" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" -dependencies = [ - "cfg-if 1.0.0", - "cipher 0.3.0", - "cpufeatures 0.2.11", - "opaque-debug", -] - [[package]] name = "aes" version = "0.8.3" @@ -61,19 +50,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" dependencies = [ "cfg-if 1.0.0", - "cipher 0.4.4", - "cpufeatures 0.2.11", + "cipher", + "cpufeatures", ] [[package]] name = "aes-gcm" -version = "0.9.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc3be92e19a7ef47457b8e6f90707e12b6ac5d20c6f3866584fa3be0787d839f" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" dependencies = [ "aead", - "aes 0.7.5", - "cipher 0.3.0", + "aes", + "cipher", "ctr", "ghash", "subtle", @@ -130,9 +119,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.42" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595d3cfa7a60d4555cb5067b99f07142a08ea778de5cf993f7b75c7d8fabc486" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" [[package]] name = "argon2" @@ -142,7 +131,7 @@ checksum = "17ba4cac0a46bc1d2912652a751c47f2a9f3a7fe89bcae2275d418f5270402f9" dependencies = [ "base64ct", "blake2", - "cpufeatures 0.2.11", + "cpufeatures", "password-hash", "zeroize", ] @@ -241,7 +230,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -258,8 +247,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "531b97fb4cd3dfdce92c35dedbfdc1f0b9d8091c8ca943d6dae340ef5012d514" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -706,7 +695,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" dependencies = [ - "cipher 0.4.4", + "cipher", ] [[package]] @@ -729,25 +718,24 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chacha20" -version = "0.8.2" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c80e5460aa66fe3b91d40bcbdab953a597b60053e34d684ac6903f863b680a6" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if 1.0.0", - "cipher 0.3.0", - "cpufeatures 0.2.11", - "zeroize", + "cipher", + "cpufeatures", ] [[package]] name = "chacha20poly1305" -version = "0.9.1" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18446b09be63d457bbec447509e85f662f32952b035ce892290396bc0b0cff5" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ "aead", "chacha20", - "cipher 0.3.0", + "cipher", "poly1305", "zeroize", ] @@ -780,15 +768,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "cipher" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" -dependencies = [ - "generic-array", -] - [[package]] name = "cipher" version = "0.4.4" @@ -797,6 +776,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", + "zeroize", ] [[package]] @@ -843,7 +823,7 @@ dependencies = [ "db_common", "derive_more", "dirs", - "ed25519-dalek", + "ed25519-dalek 1.0.1", "enum_derives", "ethabi", "ethcore-transaction", @@ -865,6 +845,7 @@ dependencies = [ "js-sys", "jsonrpc-core", "jubjub", + "kdf_walletconnect", "keys", "lazy_static", "libc", @@ -923,7 +904,7 @@ dependencies = [ "time 0.3.20", "tokio", "tokio-rustls 0.24.1", - "tokio-tungstenite-wasm", + "tokio-tungstenite-wasm 0.1.1-alpha.0 (git+https://github.com/KomodoPlatform/tokio-tungstenite-wasm?rev=d20abdb)", "tonic", "tonic-build", "tower-service", @@ -958,6 +939,7 @@ dependencies = [ "ethereum-types", "futures 0.3.28", "hex", + "kdf_walletconnect", "lightning", "lightning-background-processor", "lightning-invoice", @@ -1131,15 +1113,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "cpufeatures" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed00c67cb5d0a7d64a44f6ad2668db7e7530311dd53ea79bcd4fb022c64911c8" -dependencies = [ - "libc", -] - [[package]] name = "cpufeatures" version = "0.2.11" @@ -1307,7 +1280,7 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" name = "crypto" version = "1.0.0" dependencies = [ - "aes 0.8.3", + "aes", "argon2", "arrayref", "async-trait", @@ -1318,7 +1291,7 @@ dependencies = [ "bs58 0.4.0", "cbc", "cfg-if 1.0.0", - "cipher 0.4.4", + "cipher", "common", "derive_more", "enum-primitive-derive", @@ -1337,6 +1310,7 @@ dependencies = [ "num-traits", "parking_lot", "primitives", + "rand 0.8.5", "rpc", "rpc_task", "rustc-hex", @@ -1373,6 +1347,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -1413,11 +1388,11 @@ dependencies = [ [[package]] name = "ctr" -version = "0.7.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a232f92a03f37dd7d7dd2adc67166c77e9cd88de5b019b9a9eecfaeaf7bfd481" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ - "cipher 0.3.0", + "cipher", ] [[package]] @@ -1446,18 +1421,31 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.0.0-rc.1" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d4ba9852b42210c7538b75484f9daa0655e9a3ac04f693747bb0f02cf3cfe16" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if 1.0.0", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", "fiat-crypto", - "packed_simd_2", - "platforms", + "rustc_version 0.4.0", "subtle", "zeroize", ] +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote 1.0.37", + "syn 2.0.77", +] + [[package]] name = "curve25519-dalek-ng" version = "4.1.1" @@ -1493,7 +1481,7 @@ dependencies = [ "codespan-reporting", "lazy_static", "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "scratch", "syn 1.0.95", ] @@ -1511,7 +1499,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b846f081361125bfc8dc9d3940c84e1fd83ba54bbca7b17cd29483c828be0704" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -1534,7 +1522,7 @@ dependencies = [ "fnv", "ident_case", "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "strsim", "syn 1.0.95", ] @@ -1546,7 +1534,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ "darling_core", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -1608,7 +1596,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41cb0e6161ad61ed084a36ba71fbba9e3ac5aee3606fb607fe08da6acbcf3d8c" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -1686,12 +1674,12 @@ dependencies = [ [[package]] name = "ed25519" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" dependencies = [ "serde", - "signature 1.4.0", + "signature 1.6.4", ] [[package]] @@ -1724,7 +1712,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" dependencies = [ "curve25519-dalek 3.2.0", - "ed25519 1.5.2", + "ed25519 1.5.3", "rand 0.7.3", "serde", "serde_bytes", @@ -1732,6 +1720,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek 4.1.3", + "ed25519 2.2.3", + "rand_core 0.6.4", + "serde", + "sha2 0.10.7", + "subtle", + "zeroize", +] + [[package]] name = "edit-distance" version = "2.1.0" @@ -1784,9 +1787,9 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" dependencies = [ - "heck", + "heck 0.4.0", "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -1797,7 +1800,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c375b9c5eadb68d0a6efee2999fef292f45854c3444c86f09d8ab086ba942b0e" dependencies = [ "num-traits", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -1807,7 +1810,7 @@ version = "0.1.0" dependencies = [ "itertools", "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -2006,9 +2009,9 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.1.20" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "findshlibs" @@ -2027,7 +2030,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" dependencies = [ "byteorder", - "rand 0.8.4", + "rand 0.8.5", "rustc-hex", "static_assertions", ] @@ -2081,8 +2084,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26c4b37de5ae15812a764c958297cfc50f5c010438f60c6ce75d11b802abd404" dependencies = [ "cbc", - "cipher 0.4.4", - "libm 0.2.7", + "cipher", + "libm", "num-bigint", "num-integer", "num-traits", @@ -2199,8 +2202,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -2314,9 +2317,9 @@ dependencies = [ [[package]] name = "ghash" -version = "0.4.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bbd60caa311237d508927dbba7594b483db3ef05faa55172fcf89b1bcda7853" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" dependencies = [ "opaque-debug", "polyval", @@ -2491,6 +2494,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.1.14" @@ -2524,6 +2533,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + [[package]] name = "hmac" version = "0.8.1" @@ -2597,6 +2615,17 @@ dependencies = [ "itoa 1.0.10", ] +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes 1.4.0", + "fnv", + "itoa 1.0.10", +] + [[package]] name = "http-body" version = "0.1.0" @@ -2837,7 +2866,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5dacb10c5b3bb92d46ba347505a9041e676bb20ad220101326bffb0c93031ee" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -2992,6 +3021,20 @@ dependencies = [ "serde_json", ] +[[package]] +name = "jsonwebtoken" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" +dependencies = [ + "base64 0.21.7", + "pem", + "ring 0.16.20", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "jubjub" version = "0.5.1" @@ -3018,6 +3061,46 @@ dependencies = [ "sha2 0.10.7", ] +[[package]] +name = "kdf_walletconnect" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "base64 0.21.7", + "cfg-if 1.0.0", + "chrono", + "common", + "db_common", + "derive_more", + "enum_derives", + "futures 0.3.28", + "hex", + "hkdf", + "js-sys", + "mm2_core", + "mm2_db", + "mm2_err_handle", + "mm2_test_helpers", + "pairing_api", + "parking_lot", + "rand 0.8.5", + "relay_client", + "relay_rpc", + "secp256k1 0.20.3", + "serde", + "serde_json", + "sha2 0.10.7", + "thiserror", + "tokio", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", + "wc_common", + "web-sys", + "x25519-dalek 2.0.1", +] + [[package]] name = "keccak" version = "0.1.0" @@ -3085,12 +3168,6 @@ version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" -[[package]] -name = "libm" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a" - [[package]] name = "libm" version = "0.2.7" @@ -3170,7 +3247,7 @@ dependencies = [ "parking_lot", "pin-project", "quick-protobuf", - "rand 0.8.4", + "rand 0.8.5", "rw-stream-sink", "smallvec 1.6.1", "thiserror", @@ -3207,7 +3284,7 @@ dependencies = [ "log", "quick-protobuf", "quick-protobuf-codec", - "rand 0.8.4", + "rand 0.8.5", "smallvec 1.6.1", "thiserror", ] @@ -3235,7 +3312,7 @@ dependencies = [ "prometheus-client", "quick-protobuf", "quick-protobuf-codec", - "rand 0.8.4", + "rand 0.8.5", "regex", "sha2 0.10.7", "smallvec 1.6.1", @@ -3272,12 +3349,12 @@ checksum = "d2874d9c6575f1d7a151022af5c42bb0ffdcdfbafe0a6fd039de870b384835a2" dependencies = [ "asn1_der", "bs58 0.5.0", - "ed25519-dalek", + "ed25519-dalek 1.0.1", "libsecp256k1", "log", "multihash", "quick-protobuf", - "rand 0.8.4", + "rand 0.8.5", "sha2 0.10.7", "thiserror", "zeroize", @@ -3295,7 +3372,7 @@ dependencies = [ "libp2p-identity", "libp2p-swarm", "log", - "rand 0.8.4", + "rand 0.8.5", "smallvec 1.6.1", "socket2 0.5.3", "tokio", @@ -3334,12 +3411,12 @@ dependencies = [ "multihash", "once_cell", "quick-protobuf", - "rand 0.8.4", + "rand 0.8.5", "sha2 0.10.7", "snow", "static_assertions", "thiserror", - "x25519-dalek", + "x25519-dalek 1.1.0", "zeroize", ] @@ -3356,7 +3433,7 @@ dependencies = [ "libp2p-identity", "libp2p-swarm", "log", - "rand 0.8.4", + "rand 0.8.5", "void", ] @@ -3372,7 +3449,7 @@ dependencies = [ "libp2p-identity", "libp2p-swarm", "log", - "rand 0.8.4", + "rand 0.8.5", "smallvec 1.6.1", "void", ] @@ -3393,7 +3470,7 @@ dependencies = [ "log", "multistream-select", "once_cell", - "rand 0.8.4", + "rand 0.8.5", "smallvec 1.6.1", "tokio", "void", @@ -3404,11 +3481,11 @@ name = "libp2p-swarm-derive" version = "0.33.0" source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ - "heck", + "heck 0.4.0", "proc-macro-warning", "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -3486,7 +3563,7 @@ dependencies = [ "libsecp256k1-core", "libsecp256k1-gen-ecmult", "libsecp256k1-gen-genmult", - "rand 0.8.4", + "rand 0.8.5", "serde", "sha2 0.9.9", "typenum", @@ -3780,8 +3857,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddece26afd34c31585c74a4db0630c376df271c285d682d1e55012197830b6df" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -4057,6 +4134,7 @@ dependencies = [ "instant", "itertools", "js-sys", + "kdf_walletconnect", "keys", "lazy_static", "libc", @@ -4249,7 +4327,7 @@ dependencies = [ "serde_json", "sha2 0.10.7", "smallvec 1.6.1", - "syn 2.0.38", + "syn 2.0.77", "timed-map", "tokio", "void", @@ -4328,7 +4406,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3048ef3680533a27f9f8e7d6a0bce44dc61e4895ea0f42709337fa1c8616fefe" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -4515,8 +4593,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -4597,16 +4675,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "packed_simd_2" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1914cd452d8fccd6f9db48147b29fd4ae05bea9dc5d9ad578509f72415de282" -dependencies = [ - "cfg-if 1.0.0", - "libm 0.1.4", -] - [[package]] name = "pairing" version = "0.18.0" @@ -4617,6 +4685,27 @@ dependencies = [ "group 0.8.0", ] +[[package]] +name = "pairing_api" +version = "0.1.0" +source = "git+https://github.com/komodoplatform/walletconnectrust?tag=k-0.1.2#b9314b8fe61cb2537885babb0585ad387926c268" +dependencies = [ + "anyhow", + "chrono", + "hex", + "lazy_static", + "paste", + "rand 0.8.5", + "regex", + "relay_client", + "relay_rpc", + "serde", + "serde_json", + "thiserror", + "url", + "wc_common", +] + [[package]] name = "parity-scale-codec" version = "3.1.2" @@ -4639,7 +4728,7 @@ checksum = "c45ed1f39709f5a89338fab50e59816b2e8815f5bb58276e7ddf9afd495f73f8" dependencies = [ "proc-macro-crate", "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -4735,9 +4824,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.7" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "peg" @@ -4757,7 +4846,7 @@ checksum = "bdad6a1d9cf116a059582ce415d5f5566aabcd4008646779dab7fdc2a9a9d426" dependencies = [ "peg-runtime", "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", ] [[package]] @@ -4807,8 +4896,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -4845,12 +4934,6 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" -[[package]] -name = "platforms" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d7ddaed09e0eb771a79ab0fd64609ba0afb0a8366421957936ad14cbd13630" - [[package]] name = "polling" version = "2.8.0" @@ -4869,23 +4952,23 @@ dependencies = [ [[package]] name = "poly1305" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fe800695325da85083cd23b56826fccb2e2dc29b218e7811a6f33bc93f414be" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "cpufeatures 0.1.4", + "cpufeatures", "opaque-debug", "universal-hash", ] [[package]] name = "polyval" -version = "0.5.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e597450cbf209787f0e6de80bf3795c6b2356a380ee87837b545aded8dbc1823" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if 1.0.0", - "cpufeatures 0.1.4", + "cpufeatures", "opaque-debug", "universal-hash", ] @@ -4909,7 +4992,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" dependencies = [ "proc-macro2", - "syn 2.0.38", + "syn 2.0.77", ] [[package]] @@ -4953,15 +5036,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70550716265d1ec349c41f70dd4f964b4fd88394efe4405f0c1da679c4799a07" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" dependencies = [ "unicode-ident", ] @@ -4985,7 +5068,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b6a5217beb0ad503ee7fa752d451c905113d70721b937126158f3106a48cc1" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -5006,7 +5089,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ "bytes 1.4.0", - "heck", + "heck 0.5.0", "itertools", "log", "multimap", @@ -5016,7 +5099,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.38", + "syn 2.0.77", "tempfile", ] @@ -5029,8 +5112,8 @@ dependencies = [ "anyhow", "itertools", "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -5141,9 +5224,9 @@ checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" [[package]] name = "quote" -version = "1.0.33" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -5211,14 +5294,13 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha 0.3.1", "rand_core 0.6.4", - "rand_hc 0.3.1", ] [[package]] @@ -5302,15 +5384,6 @@ dependencies = [ "rand_core 0.5.1", ] -[[package]] -name = "rand_hc" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" -dependencies = [ - "rand_core 0.6.4", -] - [[package]] name = "rand_isaac" version = "0.1.1" @@ -5456,7 +5529,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c523ccaed8ac4b0288948849a350b37d3035827413c458b6a40ddb614bb4f72" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -5477,6 +5550,61 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +[[package]] +name = "relay_client" +version = "0.1.0" +source = "git+https://github.com/komodoplatform/walletconnectrust?tag=k-0.1.2#b9314b8fe61cb2537885babb0585ad387926c268" +dependencies = [ + "chrono", + "data-encoding", + "futures-util", + "getrandom 0.2.9", + "http 1.1.0", + "js-sys", + "pin-project", + "rand 0.8.5", + "relay_rpc", + "serde", + "serde_json", + "serde_qs", + "thiserror", + "tokio", + "tokio-tungstenite-wasm 0.1.1-alpha.0 (git+https://github.com/KomodoPlatform/tokio-tungstenite-wasm.git?rev=8fc7e2f)", + "tokio-util", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", + "web-sys", +] + +[[package]] +name = "relay_rpc" +version = "0.1.0" +source = "git+https://github.com/komodoplatform/walletconnectrust?tag=k-0.1.2#b9314b8fe61cb2537885babb0585ad387926c268" +dependencies = [ + "anyhow", + "bs58 0.4.0", + "chrono", + "data-encoding", + "derive_more", + "ed25519-dalek 2.1.1", + "getrandom 0.2.9", + "hex", + "jsonwebtoken", + "once_cell", + "paste", + "rand 0.8.5", + "regex", + "serde", + "serde-aux", + "serde_json", + "sha2 0.10.7", + "strum", + "thiserror", + "url", +] + [[package]] name = "reqwest" version = "0.11.9" @@ -5895,7 +6023,7 @@ checksum = "50e334bb10a245e28e5fd755cabcafd96cfcd167c99ae63a46924ca8d8703a3c" dependencies = [ "proc-macro-crate", "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -6079,20 +6207,30 @@ name = "ser_error_derive" version = "0.1.0" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "ser_error", "syn 1.0.95", ] [[package]] name = "serde" -version = "1.0.189" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] +[[package]] +name = "serde-aux" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d2e8bfba469d06512e11e3311d4d051a4a387a5b42d010404fecf3200321c95" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "serde-wasm-bindgen" version = "0.4.3" @@ -6115,27 +6253,39 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.189" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] name = "serde_json" -version = "1.0.79" +version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" +checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" dependencies = [ - "indexmap 1.9.3", + "indexmap 2.2.3", "itoa 1.0.10", + "memchr", "ryu", "serde", ] +[[package]] +name = "serde_qs" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cac3f1e2ca2fe333923a1ae72caca910b98ed0630bb35ef6f8c8517d6e81afa" +dependencies = [ + "percent-encoding", + "serde", + "thiserror", +] + [[package]] name = "serde_repr" version = "0.1.6" @@ -6143,7 +6293,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dc6b7951b17b051f3210b063f12cc17320e2fe30ae05b0fe2a3abb068551c76" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -6186,7 +6336,7 @@ checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" dependencies = [ "darling", "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -6217,7 +6367,7 @@ checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" dependencies = [ "block-buffer 0.9.0", "cfg-if 1.0.0", - "cpufeatures 0.2.11", + "cpufeatures", "digest 0.9.0", "opaque-debug", ] @@ -6229,7 +6379,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if 1.0.0", - "cpufeatures 0.2.11", + "cpufeatures", "digest 0.10.7", ] @@ -6241,7 +6391,7 @@ checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", "cfg-if 1.0.0", - "cpufeatures 0.2.11", + "cpufeatures", "digest 0.9.0", "opaque-debug", ] @@ -6253,7 +6403,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" dependencies = [ "cfg-if 1.0.0", - "cpufeatures 0.2.11", + "cpufeatures", "digest 0.10.7", ] @@ -6295,7 +6445,7 @@ dependencies = [ "blake2b_simd", "chrono", "derive_more", - "ed25519-dalek", + "ed25519-dalek 1.0.1", "hex", "nom", "reqwest", @@ -6317,9 +6467,9 @@ dependencies = [ [[package]] name = "signature" -version = "1.4.0" +version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02658e48d89f2bec991f9a78e69cfa4c316f8d6a6c4ec12fae1aeb263d486788" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" [[package]] name = "signature" @@ -6331,6 +6481,18 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time 0.3.20", +] + [[package]] name = "siphasher" version = "0.1.3" @@ -6390,16 +6552,16 @@ dependencies = [ [[package]] name = "snow" -version = "0.9.2" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ccba027ba85743e09d15c03296797cad56395089b832b48b5a5217880f57733" +checksum = "850948bee068e713b8ab860fe1adc4d109676ab4c3b621fd8147f06b261f2f85" dependencies = [ "aes-gcm", "blake2", "chacha20poly1305", - "curve25519-dalek 4.0.0-rc.1", + "curve25519-dalek 4.1.3", "rand_core 0.6.4", - "ring 0.16.20", + "ring 0.17.3", "rustc_version 0.4.0", "sha2 0.10.7", "subtle", @@ -6447,7 +6609,7 @@ dependencies = [ "futures 0.3.28", "httparse", "log", - "rand 0.8.4", + "rand 0.8.5", "sha-1", ] @@ -6483,7 +6645,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d676664972e22a0796176e81e7bec41df461d1edf52090955cdab55f2c956ff2" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -6513,7 +6675,7 @@ dependencies = [ "Inflector", "proc-macro-crate", "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -6634,7 +6796,7 @@ checksum = "2f9799e6d412271cb2414597581128b03f3285f260ea49f5363d07df6a332b3e" dependencies = [ "Inflector", "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "serde", "serde_json", "unicode-xid 0.2.0", @@ -6652,6 +6814,28 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote 1.0.37", + "rustversion", + "syn 2.0.77", +] + [[package]] name = "subtle" version = "2.4.0" @@ -6691,18 +6875,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "unicode-ident", ] [[package]] name = "syn" -version = "2.0.38" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "unicode-ident", ] @@ -6728,7 +6912,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", "unicode-xid 0.2.0", ] @@ -6848,7 +7032,7 @@ dependencies = [ "getrandom 0.2.9", "peg", "pin-project", - "rand 0.8.4", + "rand 0.8.5", "semver 1.0.6", "serde", "serde_bytes", @@ -6892,7 +7076,7 @@ dependencies = [ "hex", "hmac 0.12.1", "log", - "rand 0.8.4", + "rand 0.8.5", "serde", "serde_json", "sha2 0.10.7", @@ -6914,8 +7098,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -7044,8 +7228,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -7096,6 +7280,23 @@ dependencies = [ "webpki", ] +[[package]] +name = "tokio-tungstenite-wasm" +version = "0.1.1-alpha.0" +source = "git+https://github.com/KomodoPlatform/tokio-tungstenite-wasm.git?rev=8fc7e2f#8fc7e2ff4c970bee0c0867399cb9a941881ea183" +dependencies = [ + "futures-channel", + "futures-util", + "http 0.2.12", + "httparse", + "js-sys", + "thiserror", + "tokio", + "tokio-tungstenite", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "tokio-tungstenite-wasm" version = "0.1.1-alpha.0" @@ -7211,8 +7412,8 @@ dependencies = [ "prettyplease", "proc-macro2", "prost-build", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -7226,7 +7427,7 @@ dependencies = [ "indexmap 1.9.3", "pin-project", "pin-project-lite 0.2.9", - "rand 0.8.4", + "rand 0.8.5", "slab", "tokio", "tokio-util", @@ -7266,8 +7467,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -7374,7 +7575,7 @@ dependencies = [ "idna 0.2.3", "ipnet", "lazy_static", - "rand 0.8.4", + "rand 0.8.5", "smallvec 1.6.1", "socket2 0.4.9", "thiserror", @@ -7422,7 +7623,7 @@ dependencies = [ "http 0.2.12", "httparse", "log", - "rand 0.8.4", + "rand 0.8.5", "rustls 0.20.4", "sha-1", "thiserror", @@ -7433,9 +7634,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.15.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "uint" @@ -7495,11 +7696,11 @@ checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" [[package]] name = "universal-hash" -version = "0.4.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "generic-array", + "crypto-common", "subtle", ] @@ -7568,7 +7769,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom 0.2.9", - "rand 0.8.4", + "rand 0.8.5", "serde", ] @@ -7705,8 +7906,8 @@ dependencies = [ "log", "once_cell", "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", "wasm-bindgen-shared", ] @@ -7728,7 +7929,7 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ - "quote 1.0.33", + "quote 1.0.37", "wasm-bindgen-macro-support", ] @@ -7739,8 +7940,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -7772,7 +7973,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c2e18093f11c19ca4e188c177fecc7c372304c311189f12c2f9bea5b7324ac7" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", +] + +[[package]] +name = "wc_common" +version = "0.1.0" +source = "git+https://github.com/komodoplatform/walletconnectrust?tag=k-0.1.2#b9314b8fe61cb2537885babb0585ad387926c268" +dependencies = [ + "base64 0.21.7", + "chacha20poly1305", + "thiserror", ] [[package]] @@ -7817,7 +8028,7 @@ dependencies = [ "log", "parking_lot", "pin-project", - "rand 0.8.4", + "rand 0.8.5", "reqwest", "rlp", "serde", @@ -8186,6 +8397,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek 4.1.3", + "rand_core 0.6.4", + "serde", + "zeroize", +] + [[package]] name = "yamux" version = "0.12.1" @@ -8197,7 +8420,7 @@ dependencies = [ "nohash-hasher", "parking_lot", "pin-project", - "rand 0.8.4", + "rand 0.8.5", "static_assertions", ] @@ -8213,7 +8436,7 @@ dependencies = [ "nohash-hasher", "parking_lot", "pin-project", - "rand 0.8.4", + "rand 0.8.5", "static_assertions", ] @@ -8314,7 +8537,7 @@ name = "zcash_primitives" version = "0.5.0" source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.1#e92443a7bbd1c5e92e00e6deb45b5a33af14cea4" dependencies = [ - "aes 0.8.3", + "aes", "bitvec 0.18.5", "blake2b_simd", "blake2s_simd", @@ -8373,7 +8596,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", "synstructure", ] diff --git a/Cargo.toml b/Cargo.toml index 507c2e5c31..d8648ad96f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "mm2src/derives/ser_error_derive", "mm2src/derives/ser_error", "mm2src/hw_common", + "mm2src/kdf_walletconnect", "mm2src/mm2_bin_lib", "mm2src/mm2_bitcoin/chain", "mm2src/mm2_bitcoin/crypto", diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index 49616d1fa4..45fd7a3988 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -57,6 +57,7 @@ hex = "0.4.2" http = "0.2" itertools = { version = "0.10", features = ["use_std"] } jsonrpc-core = "18.0.0" +kdf_walletconnect = { path = "../kdf_walletconnect" } keys = { path = "../mm2_bitcoin/keys" } lazy_static = "1.4" libc = "0.2" @@ -90,7 +91,7 @@ script = { path = "../mm2_bitcoin/script" } secp256k1 = { version = "0.20" } ser_error = { path = "../derives/ser_error" } ser_error_derive = { path = "../derives/ser_error_derive" } -serde = "1.0" +serde = { version = "1.0", features = ["derive"] } serde_derive = "1.0" serde_json = { version = "1", features = ["preserve_order", "raw_value"] } serde_with = "1.14.0" @@ -112,7 +113,7 @@ web3 = { git = "https://github.com/KomodoPlatform/rust-web3", tag = "v0.20.0", d zbase32 = "0.1.2" zcash_client_backend = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.1" } zcash_extras = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.1" } -zcash_primitives = {features = ["transparent-inputs"], git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.1" } +zcash_primitives = { features = ["transparent-inputs"], git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.1" } [target.'cfg(target_arch = "wasm32")'.dependencies] blake2b_simd = "0.5" diff --git a/mm2src/coins/coin_errors.rs b/mm2src/coins/coin_errors.rs index 6075317bfc..5dc6f04b57 100644 --- a/mm2src/coins/coin_errors.rs +++ b/mm2src/coins/coin_errors.rs @@ -119,3 +119,10 @@ pub enum MyAddressError { UnexpectedDerivationMethod(String), InternalError(String), } + +impl std::error::Error for MyAddressError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + // This error doesn't wrap another error, so we return None + None + } +} diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index b8db8c7c12..c7430a5a98 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -20,6 +20,7 @@ // // Copyright © 2023 Pampex LTD and TillyHK LTD. All rights reserved. // +use self::wallet_connect::{send_transaction_with_walletconnect, WcEthTxParams}; use super::eth::Action::{Call, Create}; use super::watcher_common::{validate_watcher_reward, REWARD_GAS_AMOUNT}; use super::*; @@ -58,6 +59,7 @@ use common::executor::{abortable_queue::AbortableQueue, AbortOnDropHandle, Abort AbortedError, SpawnAbortable, Timer}; use common::log::{debug, error, info, warn}; use common::number_type_casting::SafeTypeCastingNumbers; +use common::wait_until_sec; use common::{now_sec, small_rng, DEX_FEE_ADDR_RAW_PUBKEY}; use crypto::privkey::key_pair_from_secret; use crypto::{Bip44Chain, CryptoCtx, CryptoCtxError, GlobalHDAccountArc, KeyPairPolicy}; @@ -76,6 +78,7 @@ use futures::future::{join, join_all, select_ok, try_join_all, Either, FutureExt use futures01::Future; use http::Uri; use instant::Instant; +use kdf_walletconnect::{WalletConnectCtx, WalletConnectOps}; use mm2_core::mm_ctx::{MmArc, MmWeak}; use mm2_event_stream::behaviour::{EventBehaviour, EventInitStatus}; use mm2_number::bigdecimal_custom::CheckedDivision; @@ -101,9 +104,8 @@ use web3::types::{Action as TraceAction, BlockId, BlockNumber, Bytes, CallReques use web3::{self, Web3}; cfg_wasm32! { - use common::{now_ms, wait_until_ms}; use crypto::MetamaskArc; - use ethereum_types::{H264, H520}; + use ethereum_types::H520; use mm2_metamask::MetamaskError; use web3::types::TransactionRequest; } @@ -138,6 +140,7 @@ mod eth_rpc; #[cfg(target_arch = "wasm32")] mod eth_wasm_tests; #[cfg(any(test, target_arch = "wasm32"))] mod for_tests; pub(crate) mod nft_swap_v2; +pub mod wallet_connect; mod web3_transport; use web3_transport::{http_transport::HttpTransportNode, Web3Transport}; @@ -786,6 +789,11 @@ pub enum EthPrivKeyBuildPolicy { #[cfg(target_arch = "wasm32")] Metamask(MetamaskArc), Trezor, + WalletConnect { + address: Address, + public_key_uncompressed: H520, + session_topic: String, + }, } impl EthPrivKeyBuildPolicy { @@ -1573,7 +1581,7 @@ impl SwapOps for EthCoin { activated_key: ref key_pair, .. } => key_pair_from_secret(key_pair.secret().as_bytes()).expect("valid key"), - EthPrivKeyPolicy::Trezor => todo!(), + EthPrivKeyPolicy::Trezor | EthPrivKeyPolicy::WalletConnect { .. } => todo!(), #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(_) => todo!(), } @@ -1590,6 +1598,7 @@ impl SwapOps for EthCoin { .expect("valid key") .public_slice() .to_vec(), + EthPrivKeyPolicy::WalletConnect { public_key, .. } => public_key.as_bytes().to_vec(), EthPrivKeyPolicy::Trezor => todo!(), #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(ref metamask_policy) => metamask_policy.public_key.as_bytes().to_vec(), @@ -2372,6 +2381,10 @@ impl MarketCoinOps for EthCoin { EthPrivKeyPolicy::Metamask(ref metamask_policy) => { Ok(format!("{:02x}", metamask_policy.public_key_uncompressed)) }, + EthPrivKeyPolicy::WalletConnect { + public_key_uncompressed, + .. + } => Ok(format!("{public_key_uncompressed:02x}")), } } @@ -2685,6 +2698,7 @@ impl MarketCoinOps for EthCoin { EthPrivKeyPolicy::Trezor => ERR!("'display_priv_key' is not supported for Hardware Wallets"), #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(_) => ERR!("'display_priv_key' is not supported for MetaMask"), + EthPrivKeyPolicy::WalletConnect { .. } => ERR!("'display_priv_key' is not supported for WalletConnect"), } } @@ -2739,15 +2753,14 @@ async fn sign_transaction_with_keypair<'a>( if !coin.is_tx_type_supported(&tx_type) { return Err(TransactionErr::Plain("Eth transaction type not supported".into())); } + let tx_builder = UnSignedEthTxBuilder::new(tx_type, nonce, gas, action, value, data); let tx_builder = tx_builder_with_pay_for_gas_option(coin, tx_builder, pay_for_gas_option) .map_err(|e| TransactionErr::Plain(e.get_inner().to_string()))?; let tx = tx_builder.build()?; + let signed_tx = tx.sign(key_pair.secret(), Some(coin.chain_id))?; - Ok(( - tx.sign(key_pair.secret(), Some(coin.chain_id))?, - web3_instances_with_latest_nonce, - )) + Ok((signed_tx, web3_instances_with_latest_nonce)) } /// Sign and send eth transaction with provided keypair, @@ -2821,7 +2834,7 @@ async fn sign_and_send_transaction_with_metamask( // It's important to return the transaction hex for the swap, // so wait up to 60 seconds for the transaction to appear on the RPC node. - let wait_rpc_timeout = 60_000; + let wait_rpc_timeout = 60; let check_every = 1.; // Please note that this method may take a long time @@ -2888,13 +2901,61 @@ async fn sign_raw_eth_tx(coin: &EthCoin, args: &SignEthTransactionParams) -> Raw }) .map_to_mm(|err| RawTransactionError::TransactionError(err.get_plain_text_format())) }, + EthPrivKeyPolicy::WalletConnect { .. } => { + // NOTE: doesn't work with wallets that doesn't support `eth_signTransaction`. + // e.g Metamask + let wc = { + let ctx = MmArc::from_weak(&coin.ctx).expect("No context"); + WalletConnectCtx::from_ctx(&ctx) + .expect("TODO: handle error when enable kdf initialization without key.") + }; + let my_address = coin + .derivation_method + .single_addr_or_err() + .await + .mm_err(|e| RawTransactionError::InternalError(e.to_string()))?; + let address_lock = coin.get_address_lock(my_address.to_string()).await; + let _nonce_lock = address_lock.lock().await; + let pay_for_gas_option = if let Some(ref pay_for_gas) = args.pay_for_gas { + pay_for_gas.clone().try_into()? + } else { + // use legacy gas_price() if not set + info!(target: "sign-and-send", "get_gas_price…"); + let gas_price = coin.get_gas_price().await?; + PayForGasOption::Legacy(LegacyGasPrice { gas_price }) + }; + let (nonce, _) = coin + .clone() + .get_addr_nonce(my_address) + .compat() + .await + .map_to_mm(RawTransactionError::InvalidParam)?; + + info!(target: "sign-and-send", "WalletConnect signing and sending tx…"); + let (signed_tx, _) = coin + .wc_sign_tx(&wc, WcEthTxParams { + my_address, + gas_price: pay_for_gas_option.get_gas_price(), + action, + value, + gas: args.gas_limit, + data: &data, + nonce, + }) + .await + .mm_err(|err| RawTransactionError::TransactionError(err.to_string()))?; + + Ok(RawTransactionRes { + tx_hex: signed_tx.tx_hex().into(), + }) + }, + EthPrivKeyPolicy::Trezor => MmError::err(RawTransactionError::InvalidParam( + "sign raw eth tx not implemented for Trezor".into(), + )), #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(_) => MmError::err(RawTransactionError::InvalidParam( "sign raw eth tx not implemented for Metamask".into(), )), - EthPrivKeyPolicy::Trezor => MmError::err(RawTransactionError::InvalidParam( - "sign raw eth tx not implemented for Trezor".into(), - )), } } @@ -3837,8 +3898,23 @@ impl EthCoin { .single_addr_or_err() .await .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; + sign_and_send_transaction_with_keypair(&coin, key_pair, address, value, action, data, gas).await }, + EthPrivKeyPolicy::WalletConnect { .. } => { + let wc = { + let ctx = MmArc::from_weak(&coin.ctx).expect("No context"); + WalletConnectCtx::from_ctx(&ctx) + .expect("TODO: handle error when enable kdf initialization without key.") + }; + let address = coin + .derivation_method + .single_addr_or_err() + .await + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; + + send_transaction_with_walletconnect(coin, &wc, address, value, action, &data, gas).await + }, EthPrivKeyPolicy::Trezor => Err(TransactionErr::Plain(ERRL!("Trezor is not supported for swaps yet!"))), #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(_) => { @@ -5451,15 +5527,14 @@ impl EthCoin { } /// Returns `None` if the transaction hasn't appeared on the RPC nodes at the specified time. - #[cfg(target_arch = "wasm32")] async fn wait_for_tx_appears_on_rpc( &self, tx_hash: H256, - wait_rpc_timeout_ms: u64, + wait_rpc_timeout_s: u64, check_every: f64, ) -> Web3RpcResult> { - let wait_until = wait_until_ms(wait_rpc_timeout_ms); - while now_ms() < wait_until { + let wait_until = wait_until_sec(wait_rpc_timeout_s); + while now_sec() < wait_until { let maybe_tx = self.transaction(TransactionId::Hash(tx_hash)).await?; if let Some(tx) = maybe_tx { let signed_tx = signed_tx_from_web3_tx(tx).map_to_mm(Web3RpcError::InvalidResponse)?; @@ -5469,10 +5544,10 @@ impl EthCoin { Timer::sleep(check_every).await; } - let timeout_s = wait_rpc_timeout_ms / 1000; warn!( - "Couldn't fetch the '{tx_hash:02x}' transaction hex as it hasn't appeared on the RPC node in {timeout_s}s" + "Couldn't fetch the '{tx_hash:02x}' transaction hex as it hasn't appeared on the RPC node in {wait_rpc_timeout_s}s" ); + Ok(None) } @@ -7377,6 +7452,15 @@ impl CommonSwapOpsV2 for EthCoin { .expect("slice with incorrect length"); Public::from_slice(&pubkey_bytes) }, + EthPrivKeyPolicy::WalletConnect { + public_key_uncompressed, + .. + } => { + let pubkey_bytes: [u8; 64] = public_key_uncompressed[1..65] + .try_into() + .expect("slice with incorrect length"); + Public::from_slice(&pubkey_bytes) + }, } } diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index a02aa9eaf8..9f24869c77 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -226,6 +226,7 @@ fn test_withdraw_impl_manual_fee() { }), memo: None, ibc_source_channel: None, + broadcast: false, }; block_on_f01(coin.get_balance()).unwrap(); @@ -275,6 +276,7 @@ fn test_withdraw_impl_fee_details() { }), memo: None, ibc_source_channel: None, + broadcast: false, }; block_on_f01(coin.get_balance()).unwrap(); diff --git a/mm2src/coins/eth/eth_withdraw.rs b/mm2src/coins/eth/eth_withdraw.rs index b3de177a89..037666c238 100644 --- a/mm2src/coins/eth/eth_withdraw.rs +++ b/mm2src/coins/eth/eth_withdraw.rs @@ -1,5 +1,6 @@ use super::{checksum_address, u256_to_big_decimal, wei_from_big_decimal, EthCoinType, EthDerivationMethod, EthPrivKeyPolicy, Public, WithdrawError, WithdrawRequest, WithdrawResult, ERC20_CONTRACT, H160, H256}; +use crate::eth::wallet_connect::WcEthTxParams; use crate::eth::{calc_total_fee, get_eth_gas_details_from_withdraw_fee, tx_builder_with_pay_for_gas_option, tx_type_from_pay_for_gas_option, Action, Address, EthTxFeeDetails, KeyPair, PayForGasOption, SignedEthTx, TransactionWrapper, UnSignedEthTxBuilder}; @@ -16,6 +17,7 @@ use crypto::trezor::trezor_rpc_task::{TrezorRequestStatuses, TrezorRpcTaskProces use crypto::{CryptoCtx, HwRpcError}; use ethabi::Token; use futures::compat::Future01CompatExt; +use kdf_walletconnect::{WalletConnectCtx, WalletConnectOps}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::map_mm_error::MapMmError; use mm2_err_handle::mm_error::MmResult; @@ -139,6 +141,9 @@ where let bytes = rlp::encode(&signed); Ok((signed.tx_hash(), BytesJson::from(bytes.to_vec()))) }, + EthPrivKeyPolicy::WalletConnect { .. } => { + MmError::err(WithdrawError::InternalError("invalid policy".to_owned())) + }, #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(_) => MmError::err(WithdrawError::InternalError("invalid policy".to_owned())), } @@ -162,7 +167,7 @@ where } // Wait for 10 seconds for the transaction to appear on the RPC node. - let wait_rpc_timeout = 10_000; + let wait_rpc_timeout = 10; let check_every = 1.; // Please note that this method may take a long time @@ -178,7 +183,10 @@ where .unwrap_or_default(); Ok((tx_hash, tx_hex)) }, - EthPrivKeyPolicy::Iguana(_) | EthPrivKeyPolicy::HDWallet { .. } | EthPrivKeyPolicy::Trezor => { + EthPrivKeyPolicy::Iguana(_) + | EthPrivKeyPolicy::HDWallet { .. } + | EthPrivKeyPolicy::Trezor + | EthPrivKeyPolicy::WalletConnect { .. } => { MmError::err(WithdrawError::InternalError("invalid policy".to_owned())) }, } @@ -292,6 +300,43 @@ where }; self.send_withdraw_tx(&req, tx_to_send).await? }, + EthPrivKeyPolicy::WalletConnect { .. } => { + let ctx = MmArc::from_weak(&coin.ctx).expect("No context"); + let wc = WalletConnectCtx::from_ctx(&ctx) + .expect("TODO: handle error when enable kdf initialization without key."); + + let gas_price = pay_for_gas_option.get_gas_price(); + let (nonce, _) = coin + .clone() + .get_addr_nonce(my_address) + .compat() + .timeout_secs(30.) + .await? + .map_to_mm(WithdrawError::Transport)?; + let params = WcEthTxParams { + gas, + nonce, + data: &data, + my_address, + action: Action::Call(to_addr), + value: eth_value, + gas_price, + }; + + let (tx, bytes) = if req.broadcast { + self.coin() + .wc_send_tx(&wc, params) + .await + .mm_err(|err| WithdrawError::SigningError(err.to_string()))? + } else { + self.coin() + .wc_sign_tx(&wc, params) + .await + .mm_err(|err| WithdrawError::SigningError(err.to_string()))? + }; + + (tx.tx_hash(), bytes) + }, }; self.on_finishing()?; diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index cee2313ba2..d3106420d9 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -7,10 +7,13 @@ use crate::nft::get_nfts_for_activation; use crate::nft::nft_errors::{GetNftInfoError, ParseChainTypeError}; use crate::nft::nft_structs::Chain; #[cfg(target_arch = "wasm32")] use crate::EthMetamaskPolicy; + use common::executor::AbortedError; use crypto::{trezor::TrezorError, Bip32Error, CryptoCtxError, HwError}; use enum_derives::EnumFromTrait; +use ethereum_types::H264; use instant::Instant; +use kdf_walletconnect::error::WalletConnectError; use mm2_err_handle::common_errors::WithInternal; #[cfg(target_arch = "wasm32")] use mm2_metamask::{from_metamask_error, MetamaskError, MetamaskRpcError, WithMetamaskRpcError}; @@ -21,7 +24,9 @@ use std::sync::atomic::Ordering; use url::Url; use web3_transport::websocket_transport::WebsocketTransport; -#[derive(Clone, Debug, Deserialize, Display, EnumFromTrait, PartialEq, Serialize, SerializeErrorType)] +#[derive( + Clone, Debug, Deserialize, Display, EnumFromTrait, EnumFromStringify, PartialEq, Serialize, SerializeErrorType, +)] #[serde(tag = "error_type", content = "error_data")] pub enum EthActivationV2Error { InvalidPayload(String), @@ -65,6 +70,8 @@ pub enum EthActivationV2Error { InvalidHardwareWalletCall, #[display(fmt = "Custom token error: {}", _0)] CustomTokenError(CustomTokenError), + #[from_stringify("WalletConnectError")] + WalletConnectError(String), } impl From for EthActivationV2Error { @@ -161,6 +168,10 @@ pub enum EthPrivKeyActivationPolicy { Trezor, #[cfg(target_arch = "wasm32")] Metamask, + #[serde(rename = "wallet_connect")] + WalletConnect { + session_topic: String, + }, } impl EthPrivKeyActivationPolicy { @@ -581,6 +592,7 @@ pub async fn eth_coin_from_conf_and_request_v2( req: EthActivationV2Request, priv_key_build_policy: EthPrivKeyBuildPolicy, ) -> MmResult { + let chain_id = conf["chain_id"].as_u64().ok_or(EthActivationV2Error::ChainIdNotSet)?; if req.swap_contract_address == Address::default() { return Err(EthActivationV2Error::InvalidSwapContractAddr( "swap_contract_address can't be zero address".to_string(), @@ -624,10 +636,10 @@ pub async fn eth_coin_from_conf_and_request_v2( ) .await?; - let chain_id = conf["chain_id"].as_u64().ok_or(EthActivationV2Error::ChainIdNotSet)?; let web3_instances = match (req.rpc_mode, &priv_key_policy) { (EthRpcMode::Default, EthPrivKeyPolicy::Iguana(_) | EthPrivKeyPolicy::HDWallet { .. }) - | (EthRpcMode::Default, EthPrivKeyPolicy::Trezor) => { + | (EthRpcMode::Default, EthPrivKeyPolicy::Trezor) + | (EthRpcMode::Default, EthPrivKeyPolicy::WalletConnect { .. }) => { build_web3_instances(ctx, ticker.to_string(), req.nodes.clone()).await? }, #[cfg(target_arch = "wasm32")] @@ -813,6 +825,21 @@ pub(crate) async fn build_address_and_priv_key_policy( DerivationMethod::SingleAddress(address), )) }, + EthPrivKeyBuildPolicy::WalletConnect { + address, + public_key_uncompressed, + session_topic, + } => { + let public_key = compress_public_key(public_key_uncompressed)?; + Ok(( + EthPrivKeyPolicy::WalletConnect { + public_key, + public_key_uncompressed, + session_topic, + }, + DerivationMethod::SingleAddress(address), + )) + }, } } @@ -829,7 +856,7 @@ async fn build_web3_instances( eth_nodes.as_mut_slice().shuffle(&mut rng); drop_mutability!(eth_nodes); - let event_handlers = rpc_event_handlers_for_eth_transport(ctx, coin_ticker.clone()); + let event_handlers = rpc_event_handlers_for_eth_transport(ctx, coin_ticker); let mut web3_instances = Vec::with_capacity(eth_nodes.len()); for eth_node in eth_nodes { @@ -984,7 +1011,6 @@ async fn check_metamask_supports_chain_id( } } -#[cfg(target_arch = "wasm32")] fn compress_public_key(uncompressed: H520) -> MmResult { let public_key = PublicKey::from_slice(uncompressed.as_bytes()) .map_to_mm(|e| EthActivationV2Error::InternalError(e.to_string()))?; diff --git a/mm2src/coins/eth/wallet_connect.rs b/mm2src/coins/eth/wallet_connect.rs new file mode 100644 index 0000000000..c9ff6e347f --- /dev/null +++ b/mm2src/coins/eth/wallet_connect.rs @@ -0,0 +1,289 @@ +/// https://docs.reown.com/advanced/multichain/rpc-reference/ethereum-rpc +use crate::common::Future01CompatExt; +use crate::Eip1559Ops; +use crate::{BytesJson, MarketCoinOps, TransactionErr}; + +use common::log::info; +use common::u256_to_hex; +use derive_more::Display; +use enum_derives::EnumFromStringify; +use ethcore_transaction::{Action, SignedTransaction}; +use ethereum_types::H256; +use ethereum_types::{Address, Public, H160, H520, U256}; +use ethkey::{public_to_address, Message, Signature}; +use kdf_walletconnect::WalletConnectOps; +use kdf_walletconnect::{chain::{WcChainId, WcRequestMethods}, + error::WalletConnectError, + WalletConnectCtx}; +use mm2_err_handle::prelude::*; +use secp256k1::PublicKey; +use secp256k1::{recovery::{RecoverableSignature, RecoveryId}, + Secp256k1}; +use std::str::FromStr; +use web3::signing::hash_message; + +use super::{EthCoin, EthPrivKeyPolicy}; + +// Wait for 60 seconds for the transaction to appear on the RPC node. +const WAIT_RPC_TIMEOUT_SECS: u64 = 60; + +#[derive(Display, Debug, EnumFromStringify)] +pub enum EthWalletConnectError { + UnsupportedChainId(WcChainId), + InvalidSignature(String), + AccoountMisMatch(String), + #[from_stringify("rlp::DecoderError", "hex::FromHexError")] + TxDecodingFailed(String), + #[from_stringify("ethkey::Error")] + InternalError(String), + InvalidTxData(String), + SessionError(String), + WalletConnectError(WalletConnectError), +} + +impl From for EthWalletConnectError { + fn from(value: WalletConnectError) -> Self { Self::WalletConnectError(value) } +} + +pub struct WcEthTxParams<'a> { + pub(crate) gas: U256, + pub(crate) nonce: U256, + pub(crate) data: &'a [u8], + pub(crate) my_address: H160, + pub(crate) action: Action, + pub(crate) value: U256, + pub(crate) gas_price: Option, +} + +impl<'a> WcEthTxParams<'a> { + fn prepare_wc_tx_format(&self) -> MmResult { + let mut tx_json = json!({ + "nonce": u256_to_hex(self.nonce), + "from": format!("{:x}", self.my_address), + "gas": u256_to_hex(self.gas), + "value": u256_to_hex(self.value), + "data": format!("0x{}", hex::encode(self.data)) + }); + + if let Some(gas_price) = self.gas_price { + tx_json + .as_object_mut() + .unwrap() + .insert("gasPrice".to_string(), json!(u256_to_hex(gas_price))); + } + + let to_addr = match self.action { + Action::Create => None, + Action::Call(addr) => Some(addr), + }; + if let Some(to) = to_addr { + tx_json + .as_object_mut() + .unwrap() + .insert("to".to_string(), json!(format!("0x{}", hex::encode(to.as_bytes())))); + } + + Ok(json!(vec![tx_json])) + } +} + +#[async_trait::async_trait] +impl WalletConnectOps for EthCoin { + type Error = MmError; + type Params<'a> = WcEthTxParams<'a>; + type SignTxData = (SignedTransaction, BytesJson); + type SendTxData = (SignedTransaction, BytesJson); + + async fn wc_chain_id(&self, wc: &WalletConnectCtx) -> Result { + let chain_id = WcChainId::new_eip155(self.chain_id.to_string()); + let session_topic = self.session_topic()?; + wc.validate_update_active_chain_id(session_topic, &chain_id).await?; + + Ok(chain_id) + } + + async fn wc_sign_tx<'a>( + &self, + wc: &WalletConnectCtx, + params: Self::Params<'a>, + ) -> Result { + let bytes = { + let chain_id = self.wc_chain_id(wc).await?; + let tx_json = params.prepare_wc_tx_format()?; + let session_topic = self.session_topic()?; + let tx_hex: String = wc + .send_session_request_and_wait( + session_topic, + &chain_id, + WcRequestMethods::EthSignTransaction, + tx_json, + Ok, + ) + .await?; + // First 4 bytes from WalletConnect represents Protoc info + hex::decode(&tx_hex[4..])? + }; + let unverified = rlp::decode(&bytes)?; + let signed = SignedTransaction::new(unverified)?; + let bytes = rlp::encode(&signed); + + Ok((signed, BytesJson::from(bytes.to_vec()))) + } + + async fn wc_send_tx<'a>( + &self, + wc: &WalletConnectCtx, + params: Self::Params<'a>, + ) -> Result { + let tx_hash: String = { + let chain_id = self.wc_chain_id(wc).await?; + let tx_json = params.prepare_wc_tx_format()?; + let session_topic = self.session_topic()?; + wc.send_session_request_and_wait( + session_topic, + &chain_id, + WcRequestMethods::EthSendTransaction, + tx_json, + Ok, + ) + .await? + }; + + let tx_hash = tx_hash.strip_prefix("0x").unwrap_or(&tx_hash); + let maybe_signed_tx = { + self.wait_for_tx_appears_on_rpc(H256::from_slice(&hex::decode(tx_hash)?), WAIT_RPC_TIMEOUT_SECS, 1.) + .await + .mm_err(|err| EthWalletConnectError::InternalError(err.to_string()))? + }; + let signed_tx = match maybe_signed_tx { + Some(signed_tx) => signed_tx, + None => { + return MmError::err(EthWalletConnectError::InternalError(format!( + "Waited too long until the transaction {:?} appear on the RPC node", + tx_hash + ))) + }, + }; + let tx_hex = BytesJson::from(rlp::encode(&signed_tx).to_vec()); + + Ok((signed_tx, tx_hex)) + } + + fn session_topic(&self) -> Result<&str, Self::Error> { + if let EthPrivKeyPolicy::WalletConnect { ref session_topic, .. } = &self.priv_key_policy { + return Ok(session_topic); + } + + MmError::err(EthWalletConnectError::SessionError(format!( + "{} is not activated via WalletConnect", + self.ticker() + ))) + } +} + +pub async fn eth_request_wc_personal_sign( + wc: &WalletConnectCtx, + session_topic: &str, + chain_id: u64, +) -> MmResult<(H520, Address), EthWalletConnectError> { + let chain_id = WcChainId::new_eip155(chain_id.to_string()); + wc.validate_update_active_chain_id(session_topic, &chain_id).await?; + + let (account_str, _) = wc.get_account_and_properties_for_chain_id(session_topic, &chain_id)?; + let message = "Authenticate with Komodefi"; + let params = { + let message_hex = format!("0x{}", hex::encode(message)); + json!(&[&message_hex, &account_str]) + }; + let data = wc + .send_session_request_and_wait( + session_topic, + &chain_id, + WcRequestMethods::PersonalSign, + params, + |data: String| { + extract_pubkey_from_signature(&data, message, &account_str) + .mm_err(|err| WalletConnectError::SessionError(err.to_string())) + }, + ) + .await?; + + Ok(data) +} + +fn extract_pubkey_from_signature( + signature_str: &str, + message: impl ToString, + account: &str, +) -> MmResult<(H520, Address), EthWalletConnectError> { + let account = + H160::from_str(&account[2..]).map_to_mm(|err| EthWalletConnectError::InternalError(err.to_string()))?; + let uncompressed: H520 = { + let message_hash = hash_message(message.to_string()); + let signature = Signature::from_str(&signature_str[2..]) + .map_to_mm(|err| EthWalletConnectError::InvalidSignature(err.to_string()))?; + let pubkey = recover(&signature, &message_hash).map_to_mm(|err| { + let error = format!("Couldn't recover a public key from the signature: '{signature:?}, error: {err:?}'"); + EthWalletConnectError::InvalidSignature(error) + })?; + pubkey.serialize_uncompressed().into() + }; + + let mut public = Public::default(); + public.as_mut().copy_from_slice(&uncompressed[1..65]); + + let recovered_address = public_to_address(&public); + if account != recovered_address { + let error = format!("Recovered address '{recovered_address:?}' should be the same as '{account:?}'"); + return MmError::err(EthWalletConnectError::AccoountMisMatch(error)); + } + + Ok((uncompressed, recovered_address)) +} + +pub(crate) fn recover(signature: &Signature, message: &Message) -> Result { + let recovery_id = { + let recovery_id = (signature[64] as i32) + .checked_sub(27) + .ok_or_else(|| ethkey::Error::InvalidSignature)?; + RecoveryId::from_i32(recovery_id)? + }; + let sig = RecoverableSignature::from_compact(&signature[0..64], recovery_id)?; + let pubkey = Secp256k1::new().recover(&secp256k1::Message::from_slice(&message[..])?, &sig)?; + + Ok(pubkey) +} + +/// Sign and send eth transaction with WalletConnect, +/// This fn is primarily for swap transactions so it uses swap tx fee policy +pub(crate) async fn send_transaction_with_walletconnect( + coin: EthCoin, + wc: &WalletConnectCtx, + my_address: Address, + value: U256, + action: Action, + data: &[u8], + gas: U256, +) -> Result { + info!(target: "WalletConnect: sign-and-send", "get_gas_price…"); + let pay_for_gas_option = try_tx_s!( + coin.get_swap_pay_for_gas_option(coin.get_swap_transaction_fee_policy()) + .await + ); + let (nonce, _) = try_tx_s!(coin.clone().get_addr_nonce(my_address).compat().await); + let params = WcEthTxParams { + gas, + nonce, + data, + my_address, + action, + value, + gas_price: pay_for_gas_option.get_gas_price(), + }; + // Please note that this method may take a long time + // due to `eth_sendTransaction` requests. + info!(target: "WalletConnect: sign-and-send", "signing and sending tx…"); + let (signed_tx, _) = try_tx_s!(coin.wc_send_tx(wc, params).await); + + Ok(signed_tx) +} diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 16ade17a6b..a55aaa8beb 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -54,7 +54,7 @@ use crypto::{derive_secp256k1_secret, Bip32Error, Bip44Chain, CryptoCtx, CryptoC Secp256k1ExtendedPublicKey, Secp256k1Secret, WithHwRpcError}; use derive_more::Display; use enum_derives::{EnumFromStringify, EnumFromTrait}; -use ethereum_types::{H256, U256}; +use ethereum_types::{H256, H264, H520, U256}; use futures::compat::Future01CompatExt; use futures::lock::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; use futures::{FutureExt, TryFutureExt}; @@ -99,7 +99,7 @@ cfg_native! { } cfg_wasm32! { - use ethereum_types::{H264 as EthH264, H520 as EthH520}; + use ethereum_types::{H264 as EthH264}; use hd_wallet::HDWalletDb; use mm2_db::indexed_db::{ConstructibleDb, DbLocked, SharedDb}; use tx_history_storage::wasm::{clear_tx_history, load_tx_history, save_tx_history, TxHistoryDb}; @@ -2125,8 +2125,7 @@ pub struct WithdrawRequest { memo: Option, /// Tendermint specific field used for manually providing the IBC channel IDs. ibc_source_channel: Option, - /// Currently, this flag is used by ETH/ERC20 coins activated with MetaMask **only**. - #[cfg(target_arch = "wasm32")] + /// Currently, this flag is used by ETH/ERC20 coins activated with MetaMask/WalletConnect(Some wallets e.g Metamask) **only**. #[serde(default)] broadcast: bool, } @@ -2180,7 +2179,6 @@ impl WithdrawRequest { fee: None, memo: None, ibc_source_channel: None, - #[cfg(target_arch = "wasm32")] broadcast: false, } } @@ -3875,7 +3873,7 @@ impl PrivKeyActivationPolicy { /// a hardware device like Trezor, or even external sources like Metamask. #[derive(Clone, Debug)] pub enum PrivKeyPolicy { - /// The legacy private key policy. + /// Legacy private key policy. /// /// This policy corresponds to a one-to-one mapping of private keys to addresses. /// In this scheme, only a single key and corresponding address is activated per coin, @@ -3903,19 +3901,32 @@ pub enum PrivKeyPolicy { /// Details about how the keys are managed with the Trezor device /// are abstracted away and are not directly managed by this policy. Trezor, - /// The Metamask private key policy, specific to the WASM target architecture. + /// Metamask private key policy, specific to the WASM target architecture. /// /// This variant encapsulates details about how keys are managed when interfacing /// with the Metamask extension, especially within web-based contexts. #[cfg(target_arch = "wasm32")] Metamask(EthMetamaskPolicy), + /// WalletConnect private key policy. + /// + /// This variant represents the key management details for connections + /// established via WalletConnect. It includes both compressed and uncompressed + /// public keys. + /// - `public_key`: Compressed public key, represented as [H264]. + /// - `public_key_uncompressed`: Uncompressed public key, represented as [H520]. + /// - `session_topic`: WalletConnect session that was used to activate this coin. + WalletConnect { + public_key: H264, + public_key_uncompressed: H520, + session_topic: String, + }, } #[cfg(target_arch = "wasm32")] #[derive(Clone, Debug)] pub struct EthMetamaskPolicy { pub(crate) public_key: EthH264, - pub(crate) public_key_uncompressed: EthH520, + pub(crate) public_key_uncompressed: H520, } impl From for PrivKeyPolicy { @@ -3930,7 +3941,7 @@ impl PrivKeyPolicy { activated_key: activated_key_pair, .. } => Some(activated_key_pair), - PrivKeyPolicy::Trezor => None, + PrivKeyPolicy::WalletConnect { .. } | PrivKeyPolicy::Trezor => None, #[cfg(target_arch = "wasm32")] PrivKeyPolicy::Metamask(_) => None, } @@ -3950,7 +3961,7 @@ impl PrivKeyPolicy { PrivKeyPolicy::HDWallet { bip39_secp_priv_key, .. } => Some(bip39_secp_priv_key), - PrivKeyPolicy::Iguana(_) | PrivKeyPolicy::Trezor => None, + PrivKeyPolicy::Iguana(_) | PrivKeyPolicy::Trezor | PrivKeyPolicy::WalletConnect { .. } => None, #[cfg(target_arch = "wasm32")] PrivKeyPolicy::Metamask(_) => None, } @@ -3972,8 +3983,7 @@ impl PrivKeyPolicy { path_to_coin: derivation_path, .. } => Some(derivation_path), - PrivKeyPolicy::Trezor => None, - PrivKeyPolicy::Iguana(_) => None, + PrivKeyPolicy::Iguana(_) | PrivKeyPolicy::Trezor | PrivKeyPolicy::WalletConnect { .. } => None, #[cfg(target_arch = "wasm32")] PrivKeyPolicy::Metamask(_) => None, } @@ -5703,6 +5713,7 @@ pub mod for_tests { fee, memo: None, ibc_source_channel: None, + broadcast: false, }; let init = init_withdraw(ctx.clone(), withdraw_req).await.unwrap(); let timeout = wait_until_ms(150000); diff --git a/mm2src/coins/qrc20/qrc20_tests.rs b/mm2src/coins/qrc20/qrc20_tests.rs index 3e6dbc94dd..b954b44824 100644 --- a/mm2src/coins/qrc20/qrc20_tests.rs +++ b/mm2src/coins/qrc20/qrc20_tests.rs @@ -95,6 +95,7 @@ fn test_withdraw_to_p2sh_address_should_fail() { fee: None, memo: None, ibc_source_channel: None, + broadcast: false, }; let err = block_on_f01(coin.withdraw(req)).unwrap_err().into_inner(); let expect = WithdrawError::InvalidAddress("QRC20 can be sent to P2PKH addresses only".to_owned()); @@ -142,6 +143,7 @@ fn test_withdraw_impl_fee_details() { }), memo: None, ibc_source_channel: None, + broadcast: false, }; let tx_details = block_on_f01(coin.withdraw(withdraw_req)).unwrap(); diff --git a/mm2src/coins/siacoin.rs b/mm2src/coins/siacoin.rs index bc57aaaf10..51d73590c1 100644 --- a/mm2src/coins/siacoin.rs +++ b/mm2src/coins/siacoin.rs @@ -312,6 +312,12 @@ impl MarketCoinOps for SiaCoin { ) .into()); }, + &PrivKeyPolicy::WalletConnect { .. } => { + return Err(MyAddressError::UnexpectedDerivationMethod( + "WalletConnect not yet supported. Must use iguana seed.".to_string(), + ) + .into()) + }, }; let address = SpendPolicy::PublicKey(key_pair.public).address(); Ok(address.to_string()) diff --git a/mm2src/coins/tendermint/mod.rs b/mm2src/coins/tendermint/mod.rs index 78009b5db8..87943930f1 100644 --- a/mm2src/coins/tendermint/mod.rs +++ b/mm2src/coins/tendermint/mod.rs @@ -10,11 +10,13 @@ mod tendermint_balance_events; mod tendermint_coin; mod tendermint_token; pub mod tendermint_tx_history_v2; +pub mod wallet_connect; pub use cosmrs::tendermint::PublicKey as TendermintPublicKey; pub use cosmrs::AccountId; pub use tendermint_coin::*; pub use tendermint_token::*; +pub use wallet_connect::*; pub(crate) const TENDERMINT_COIN_PROTOCOL_TYPE: &str = "TENDERMINT"; pub(crate) const TENDERMINT_ASSET_PROTOCOL_TYPE: &str = "TENDERMINTTOKEN"; diff --git a/mm2src/coins/tendermint/rpc/tendermint_native_rpc.rs b/mm2src/coins/tendermint/rpc/tendermint_native_rpc.rs index 5da40d622d..73eb8b97e6 100644 --- a/mm2src/coins/tendermint/rpc/tendermint_native_rpc.rs +++ b/mm2src/coins/tendermint/rpc/tendermint_native_rpc.rs @@ -429,7 +429,9 @@ mod sealed { .await .map_err(|e| Error::client_internal(e.to_string()))?; let response_body = response_to_string(response).await?; + debug!("Incoming response: {}", response_body); + R::Response::from_string(&response_body) } } diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 323637599d..93fe71993b 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -30,6 +30,8 @@ use crate::{big_decimal_from_sat_unsigned, BalanceError, BalanceFut, BigDecimal, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest}; use async_std::prelude::FutureExt as AsyncStdFutureExt; use async_trait::async_trait; +use base64::engine::general_purpose; +use base64::Engine; use bip32::DerivationPath; use bitcrypto::{dhash160, sha256}; use common::executor::{abortable_queue::AbortableQueue, AbortableSystem}; @@ -61,6 +63,7 @@ use futures01::Future; use hex::FromHexError; use instant::Duration; use itertools::Itertools; +use kdf_walletconnect::{WalletConnectCtx, WalletConnectOps}; use keys::{KeyPair, Public}; use mm2_core::mm_ctx::{MmArc, MmWeak}; use mm2_err_handle::prelude::*; @@ -178,7 +181,7 @@ pub struct TendermintProtocolInfo { decimals: u8, denom: String, pub account_prefix: String, - chain_id: String, + pub chain_id: String, gas_price: Option, chain_registry_name: Option, } @@ -269,13 +272,10 @@ impl TendermintActivationPolicy { .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Couldn't generate public key")) }, - PrivKeyPolicy::Trezor => Err(io::Error::new( + _ => Err(io::Error::new( io::ErrorKind::Unsupported, - "Trezor is not supported yet!", + "UnsupportedModeForPrivKeyPolicy", )), - - #[cfg(target_arch = "wasm32")] - PrivKeyPolicy::Metamask(_) => unreachable!(), }, Self::PublicKey(account_public_key) => Ok(*account_public_key), } @@ -354,6 +354,19 @@ impl RpcCommonOps for TendermintCoin { } } +#[derive(PartialEq)] +pub enum TendermintWalletConnectionType { + Wc(String), + WcLedger(String), + KeplrLedger, + Keplr, + Native, +} + +impl Default for TendermintWalletConnectionType { + fn default() -> Self { Self::Native } +} + pub struct TendermintCoinImpl { ticker: String, /// As seconds @@ -364,7 +377,7 @@ pub struct TendermintCoinImpl { pub(super) activation_policy: TendermintActivationPolicy, pub(crate) decimals: u8, pub(super) denom: Denom, - chain_id: ChainId, + pub(crate) chain_id: ChainId, gas_price: Option, pub tokens_info: PaMutex>, /// This spawner is used to spawn coin's related futures that should be aborted on coin deactivation @@ -374,7 +387,7 @@ pub struct TendermintCoinImpl { client: TendermintRpcClient, pub(crate) chain_registry_name: Option, pub(crate) ctx: MmWeak, - pub(crate) is_keplr_from_ledger: bool, + pub(crate) wallet_type: TendermintWalletConnectionType, } #[derive(Clone)] @@ -421,6 +434,11 @@ pub enum TendermintInitErrorKind { BalanceStreamInitError(String), #[display(fmt = "Watcher features can not be used with pubkey-only activation policy.")] CantUseWatchersWithPubkeyPolicy, + #[display( + fmt = "Unable to fetch chain account from WalletConnect. Please try again or reconnect your session - {}", + _0 + )] + UnableToFetchChainAccount(String), } #[derive(Display, Debug, Serialize, SerializeErrorType)] @@ -637,7 +655,7 @@ impl TendermintCoin { nodes: Vec, tx_history: bool, activation_policy: TendermintActivationPolicy, - is_keplr_from_ledger: bool, + wallet_type: Option, ) -> MmResult { if nodes.is_empty() { return MmError::err(TendermintInitError { @@ -702,7 +720,7 @@ impl TendermintCoin { client: TendermintRpcClient(AsyncMutex::new(client_impl)), chain_registry_name: protocol_info.chain_registry_name, ctx: ctx.weak(), - is_keplr_from_ledger, + wallet_type: wallet_type.unwrap_or_default(), }))) } @@ -831,7 +849,10 @@ impl TendermintCoin { // As there wouldn't be enough time to process the data, to mitigate potential edge problems (such as attempting to send transaction // bytes half a second before expiration, which may take longer to send and result in the transaction amount being wasted due to a timeout), // reduce the expiration time by 5 seconds. - let expiration = timeout - Duration::from_secs(5); + const SAFETY_MARGIN: Duration = Duration::from_secs(5); + let expiration = try_tx_s!(timeout + .checked_sub(SAFETY_MARGIN) + .ok_or("Timeout duration is too short")); match self.activation_policy { TendermintActivationPolicy::PrivateKey(_) => { @@ -842,6 +863,14 @@ impl TendermintCoin { ) }, TendermintActivationPolicy::PublicKey(_) => { + if self.is_wallet_connect() { + return try_tx_s!( + self.seq_safe_send_raw_tx_bytes(tx_payload, fee, timeout_height, memo) + .timeout(expiration) + .await + ); + }; + try_tx_s!( self.send_unsigned_tx_externally(tx_payload, fee, timeout_height, memo, expiration) .timeout(expiration) @@ -851,6 +880,38 @@ impl TendermintCoin { } } + async fn get_tx_raw( + &self, + account_info: &BaseAccount, + tx_payload: Any, + fee: Fee, + timeout_height: u64, + memo: String, + ) -> Result { + if self.is_wallet_connect() { + let ctx = try_tx_s!(MmArc::from_weak(&self.ctx).ok_or(ERRL!("ctx must be initialized already"))); + let wc = try_tx_s!(WalletConnectCtx::from_ctx(&ctx).map_err(|e| e.to_string())); + let SerializedUnsignedTx { tx_json, .. } = if self.is_ledger_connection() { + try_tx_s!(self.any_to_legacy_amino_json(account_info, tx_payload, fee, timeout_height, memo)) + } else { + try_tx_s!(self.any_to_serialized_sign_doc(account_info, tx_payload, fee, timeout_height, memo)) + }; + + return Ok(try_tx_s!(self.wc_sign_tx(&wc, tx_json).await.map_err(|err| err.to_string())).into()); + } + + let tx_raw = try_tx_s!(self.any_to_signed_raw_tx( + try_tx_s!(self.activation_policy.activated_key_or_err()), + account_info, + tx_payload, + fee, + timeout_height, + memo, + )); + + Ok(tx_raw) + } + async fn seq_safe_send_raw_tx_bytes( &self, tx_payload: Any, @@ -859,31 +920,36 @@ impl TendermintCoin { memo: String, ) -> Result<(String, Raw), TransactionErr> { let mut account_info = try_tx_s!(self.account_info(&self.account_id).await); - let (tx_id, tx_raw) = loop { - let tx_raw = try_tx_s!(self.any_to_signed_raw_tx( - try_tx_s!(self.activation_policy.activated_key_or_err()), - &account_info, - tx_payload.clone(), - fee.clone(), - timeout_height, - memo.clone(), - )); - match self.send_raw_tx_bytes(&try_tx_s!(tx_raw.to_bytes())).compat().await { - Ok(tx_id) => break (tx_id, tx_raw), + loop { + let tx_raw = try_tx_s!( + self.get_tx_raw( + &account_info, + tx_payload.clone(), + fee.clone(), + timeout_height, + memo.clone(), + ) + .await + ); + + // Attempt to send the transaction bytes + match self.send_raw_tx_bytes(try_tx_s!(&tx_raw.to_bytes())).compat().await { + Ok(tx_id) => { + return Ok((tx_id, tx_raw)); + }, Err(e) => { + // Handle sequence number mismatch and retry if e.contains(ACCOUNT_SEQUENCE_ERR) { account_info.sequence = try_tx_s!(parse_expected_sequence_number(&e)); - debug!("Got wrong account sequence, trying again."); + debug!("Account sequence mismatch, retrying..."); continue; } - return Err(crate::TransactionErr::Plain(ERRL!("{}", e))); + return Err(TransactionErr::Plain(ERRL!("Transaction failed: {}", e))); }, - }; - }; - - Ok((tx_id, tx_raw)) + } + } } async fn send_unsigned_tx_externally( @@ -902,32 +968,32 @@ impl TendermintCoin { let ctx = try_tx_s!(MmArc::from_weak(&self.ctx).ok_or(ERRL!("ctx must be initialized already"))); let account_info = try_tx_s!(self.account_info(&self.account_id).await); - let SerializedUnsignedTx { tx_json, body_bytes } = if self.is_keplr_from_ledger { + let SerializedUnsignedTx { tx_json, body_bytes } = if self.is_ledger_connection() { try_tx_s!(self.any_to_legacy_amino_json(&account_info, tx_payload, fee, timeout_height, memo)) } else { try_tx_s!(self.any_to_serialized_sign_doc(&account_info, tx_payload, fee, timeout_height, memo)) }; let data: TxHashData = try_tx_s!(ctx - .ask_for_data(&format!("TX_HASH:{}", self.ticker()), tx_json, timeout) + .ask_for_data(&format!("TX_HASH:{}", self.ticker()), tx_json.clone(), timeout) .await .map_err(|e| ERRL!("{}", e))); let tx = try_tx_s!(self.request_tx(data.hash.clone()).await.map_err(|e| ERRL!("{}", e))); - let tx_raw_inner = TxRaw { + let tx_raw = TxRaw { body_bytes: tx.body.as_ref().map(Message::encode_to_vec).unwrap_or_default(), auth_info_bytes: tx.auth_info.as_ref().map(Message::encode_to_vec).unwrap_or_default(), signatures: tx.signatures, }; - if body_bytes != tx_raw_inner.body_bytes { + if body_bytes != tx_raw.body_bytes { return Err(crate::TransactionErr::Plain(ERRL!( "Unsigned transaction don't match with the externally provided transaction." ))); } - Ok((data.hash, Raw::from(tx_raw_inner))) + Ok((data.hash, Raw::from(tx_raw))) } #[allow(deprecated)] @@ -1180,7 +1246,7 @@ impl TendermintCoin { } } - pub(super) fn any_to_transaction_data( + pub(super) async fn any_to_transaction_data( &self, maybe_pk: Option, message: Any, @@ -1194,19 +1260,34 @@ impl TendermintCoin { let tx_bytes = tx_raw.to_bytes()?; let hash = sha256(&tx_bytes); - Ok(TransactionData::new_signed( + return Ok(TransactionData::new_signed( tx_bytes.into(), hex::encode_upper(hash.as_slice()), - )) + )); + }; + + let SerializedUnsignedTx { tx_json, .. } = if self.is_ledger_connection() { + self.any_to_legacy_amino_json(account_info, message, fee, timeout_height, memo) } else { - let SerializedUnsignedTx { tx_json, .. } = if self.is_keplr_from_ledger { - self.any_to_legacy_amino_json(account_info, message, fee, timeout_height, memo) - } else { - self.any_to_serialized_sign_doc(account_info, message, fee, timeout_height, memo) - }?; + self.any_to_serialized_sign_doc(account_info, message, fee, timeout_height, memo) + }?; - Ok(TransactionData::Unsigned(tx_json)) - } + if self.is_wallet_connect() { + let ctx = MmArc::from_weak(&self.ctx) + .ok_or(MyAddressError::InternalError(ERRL!("ctx must be initialized already")))?; + let wallet_connect = WalletConnectCtx::from_ctx(&ctx)?; + + let tx_raw: Raw = self.wc_sign_tx(&wallet_connect, tx_json).await?.into(); + let tx_bytes = tx_raw.to_bytes()?; + let hash = sha256(&tx_bytes); + + return Ok(TransactionData::new_signed( + tx_bytes.into(), + hex::encode_upper(hash.as_slice()), + )); + }; + + Ok(TransactionData::Unsigned(tx_json)) } fn gen_create_htlc_tx( @@ -1294,14 +1375,27 @@ impl TendermintCoin { let auth_info = SignerInfo::single_direct(Some(pubkey), account_info.sequence).auth_info(fee); let sign_doc = SignDoc::new(&tx_body, &auth_info, &self.chain_id, account_info.account_number)?; - let tx_json = json!({ - "sign_doc": { - "body_bytes": sign_doc.body_bytes, - "auth_info_bytes": sign_doc.auth_info_bytes, - "chain_id": sign_doc.chain_id, - "account_number": sign_doc.account_number, - } - }); + let tx_json = if self.is_wallet_connect() { + // if wallet_type is WalletConnect, update tx_json to use WalletConnect type. + json!({ + "signerAddress": self.my_address()?, + "signDoc": { + "accountNumber": sign_doc.account_number.to_string(), + "chainId": sign_doc.chain_id, + "bodyBytes": general_purpose::STANDARD.encode(&sign_doc.body_bytes), + "authInfoBytes": general_purpose::STANDARD.encode(&sign_doc.auth_info_bytes) + } + }) + } else { + json!({ + "sign_doc": { + "body_bytes": &sign_doc.body_bytes, + "auth_info_bytes": sign_doc.auth_info_bytes, + "chain_id": sign_doc.chain_id, + "account_number": sign_doc.account_number, + } + }) + }; Ok(SerializedUnsignedTx { tx_json, @@ -1309,7 +1403,7 @@ impl TendermintCoin { }) } - /// This should only be used for Keplr from Ledger! + /// This should only be used for Keplr/WalletConnect from Ledger! /// When using Keplr from Ledger, they don't accept `SING_MODE_DIRECT` transactions. /// /// Visit https://docs.cosmos.network/main/build/architecture/adr-050-sign-mode-textual#context for more context. @@ -1337,8 +1431,6 @@ impl TendermintCoin { let msg_send = MsgSend::from_any(&tx_payload)?; let timeout_height = u32::try_from(timeout_height)?; - let original_tx_type_url = tx_payload.type_url.clone(); - let body_bytes = tx::Body::new(vec![tx_payload], &memo, timeout_height).into_bytes()?; let amount: Vec = msg_send .amount @@ -1375,20 +1467,52 @@ impl TendermintCoin { }) .collect(); - let tx_json = serde_json::json!({ - "legacy_amino_json": { - "account_number": account_info.account_number.to_string(), - "chain_id": self.chain_id.to_string(), - "fee": { - "amount": fee_amount, - "gas": fee.gas_limit.to_string() - }, - "memo": memo, - "msgs": [msg], - "sequence": account_info.sequence.to_string(), + let (tx_json, body_bytes) = match self.wallet_type { + TendermintWalletConnectionType::WcLedger(_) => { + let signer_address = self.my_address().unwrap(); + let body_bytes = tx::Body::new(vec![tx_payload], &memo, timeout_height).into_bytes()?; + let json = serde_json::json!({ + "signerAddress": signer_address, + "signDoc": { + "account_number": account_info.account_number.to_string(), + "chain_id": self.chain_id.to_string(), + "fee": { + "amount": fee_amount, + "gas": fee.gas_limit.to_string() + }, + "memo": memo, + "msgs": [msg], + "sequence": account_info.sequence.to_string(), + }, + }); + (json, body_bytes) }, - "original_tx_type_url": original_tx_type_url, - }); + TendermintWalletConnectionType::KeplrLedger => { + let original_tx_type_url = tx_payload.type_url.clone(); + let body_bytes = tx::Body::new(vec![tx_payload], &memo, timeout_height).into_bytes()?; + let json = serde_json::json!({ + "legacy_amino_json": { + "account_number": account_info.account_number.to_string(), + "chain_id": self.chain_id.to_string(), + "fee": { + "amount": fee_amount, + "gas": fee.gas_limit.to_string() + }, + "memo": memo, + "msgs": [msg], + "sequence": account_info.sequence.to_string(), + }, + "original_tx_type_url": original_tx_type_url, + }); + (json, body_bytes) + }, + _ => { + return Err(ErrorReport::new(io::Error::new( + io::ErrorKind::InvalidInput, + "Only WalletConnect activated with Ledger can call this funciton", + ))) + }, + }; Ok(SerializedUnsignedTx { tx_json, body_bytes }) } @@ -2080,6 +2204,28 @@ impl TendermintCoin { None } + + pub fn is_ledger_connection(&self) -> bool { + matches!( + self.wallet_type, + TendermintWalletConnectionType::WcLedger(_) | TendermintWalletConnectionType::KeplrLedger + ) + } + + pub fn is_wallet_connect(&self) -> bool { + matches!( + self.wallet_type, + TendermintWalletConnectionType::WcLedger(_) | TendermintWalletConnectionType::Wc(_) + ) + } + + pub fn try_wallet_connect_session(&self) -> Option<&str> { + match self.wallet_type { + TendermintWalletConnectionType::WcLedger(ref session_topic) + | TendermintWalletConnectionType::Wc(ref session_topic) => Some(session_topic), + _ => None, + } + } } fn clients_from_urls(ctx: &MmArc, nodes: Vec) -> MmResult, TendermintInitErrorKind> { @@ -2168,7 +2314,7 @@ impl MmCoin for TendermintCoin { } let wallet_only_conf = coin_conf["wallet_only"].as_bool().unwrap_or(false); - wallet_only_conf || self.is_keplr_from_ledger + wallet_only_conf || self.is_ledger_connection() } fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.abortable_system) } @@ -2252,7 +2398,7 @@ impl MmCoin for TendermintCoin { ) .await?; - let fee_amount_u64 = if coin.is_keplr_from_ledger { + let fee_amount_u64 = if coin.is_ledger_connection() { // When using `SIGN_MODE_LEGACY_AMINO_JSON`, Keplr ignores the fee we calculated // and calculates another one which is usually double what we calculate. // To make sure the transaction doesn't fail on the Keplr side (because if Keplr @@ -2308,6 +2454,7 @@ impl MmCoin for TendermintCoin { let tx = coin .any_to_transaction_data(maybe_pk, msg_payload, &account_info, fee, timeout_height, memo.clone()) + .await .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; let internal_id = { @@ -3318,7 +3465,7 @@ fn parse_expected_sequence_number(e: &str) -> MmResult CoinFutSpawner { CoinFutSpawner::new(&self.abortable_system) } @@ -621,6 +621,7 @@ impl MmCoin for TendermintToken { let tx = platform .any_to_transaction_data(maybe_pk, msg_payload, &account_info, fee, timeout_height, memo.clone()) + .await .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; let internal_id = { diff --git a/mm2src/coins/tendermint/wallet_connect.rs b/mm2src/coins/tendermint/wallet_connect.rs new file mode 100644 index 0000000000..bdfe85a25f --- /dev/null +++ b/mm2src/coins/tendermint/wallet_connect.rs @@ -0,0 +1,313 @@ +/// https://docs.reown.com/advanced/multichain/rpc-reference/cosmos-rpc +use base64::engine::general_purpose; +use base64::Engine; +use cosmrs::proto::cosmos::tx::v1beta1::TxRaw; +use kdf_walletconnect::chain::WcChainId; +use kdf_walletconnect::error::WalletConnectError; +use kdf_walletconnect::WalletConnectOps; +use kdf_walletconnect::{chain::WcRequestMethods, WalletConnectCtx}; +use mm2_err_handle::prelude::*; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::str::FromStr; + +use super::{CosmosTransaction, TendermintCoin}; +use crate::MarketCoinOps; + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub(crate) struct CosmosTxSignedData { + pub(crate) signature: CosmosTxSignature, + pub(crate) signed: CosmosSignData, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub(crate) struct CosmosTxSignature { + pub(crate) pub_key: CosmosTxPublicKey, + pub(crate) signature: String, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub(crate) struct CosmosTxPublicKey { + #[serde(rename = "type")] + pub(crate) key_type: String, + pub(crate) value: String, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct CosmosSignData { + pub(crate) chain_id: String, + pub(crate) account_number: String, + #[serde(deserialize_with = "deserialize_vec_field")] + pub(crate) auth_info_bytes: Vec, + #[serde(deserialize_with = "deserialize_vec_field")] + pub(crate) body_bytes: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum CosmosAccountAlgo { + #[serde(rename = "secp256k1")] + Secp256k1, + #[serde(rename = "tendermint/PubKeySecp256k1")] + TendermintSecp256k1, +} + +impl FromStr for CosmosAccountAlgo { + type Err = String; + fn from_str(s: &str) -> Result { + match s { + "secp256k1" => Ok(Self::Secp256k1), + "tendermint/PubKeySecp256k1" => Ok(Self::TendermintSecp256k1), + _ => Err(format!("Unknown pubkey type: {s}")), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct CosmosAccount { + pub address: String, + #[serde(deserialize_with = "deserialize_vec_field")] + pub pubkey: Vec, + pub algo: CosmosAccountAlgo, + #[serde(default)] + pub is_ledger: Option, +} + +#[async_trait::async_trait] +impl WalletConnectOps for TendermintCoin { + type Error = MmError; + type Params<'a> = serde_json::Value; + type SignTxData = TxRaw; + type SendTxData = CosmosTransaction; + + async fn wc_chain_id(&self, wc: &WalletConnectCtx) -> Result { + let chain_id = WcChainId::new_cosmos(self.chain_id.to_string()); + let session_topic = self.session_topic()?; + wc.validate_update_active_chain_id(session_topic, &chain_id).await?; + Ok(chain_id) + } + + async fn wc_sign_tx<'a>( + &self, + wc: &WalletConnectCtx, + params: Self::Params<'a>, + ) -> Result { + let chain_id = self.wc_chain_id(wc).await?; + let session_topic = self + .session_topic() + .expect("TODO: handle after updating tendermint coin init"); + let method = if wc.is_ledger_connection(session_topic) { + WcRequestMethods::CosmosSignAmino + } else { + WcRequestMethods::CosmosSignDirect + }; + + wc.send_session_request_and_wait(session_topic, &chain_id, method, params, |data: CosmosTxSignedData| { + let signature = general_purpose::STANDARD + .decode(data.signature.signature) + .map_to_mm(|err| WalletConnectError::PayloadError(err.to_string()))?; + + Ok(TxRaw { + body_bytes: data.signed.body_bytes, + auth_info_bytes: data.signed.auth_info_bytes, + signatures: vec![signature], + }) + }) + .await + } + + async fn wc_send_tx<'a>( + &self, + _ctx: &WalletConnectCtx, + _params: Self::Params<'a>, + ) -> Result { + todo!() + } + + fn session_topic(&self) -> Result<&str, Self::Error> { + self.try_wallet_connect_session() + .ok_or(MmError::new(WalletConnectError::SessionError(format!( + "{} is not activated via WalletConnect", + self.ticker() + )))) + } +} + +pub async fn cosmos_get_accounts_impl( + wc: &WalletConnectCtx, + session_topic: &str, + chain_id: &str, +) -> MmResult { + let chain_id = WcChainId::new_cosmos(chain_id.to_string()); + wc.validate_update_active_chain_id(session_topic, &chain_id).await?; + + let (account, properties) = wc.get_account_and_properties_for_chain_id(session_topic, &chain_id)?; + + // Check if session has session_properties and return wallet account; + if let Some(props) = properties { + if let Some(keys) = &props.keys { + if let Some(key) = keys.iter().next() { + let pubkey = decode_data(&key.pub_key).map_to_mm(|err| { + WalletConnectError::PayloadError(format!("error decoding pubkey payload: {err:?}")) + })?; + let address = decode_data(&key.address).map_to_mm(|err| { + WalletConnectError::PayloadError(format!("error decoding address payload: {err:?}")) + })?; + let address = hex::encode(address); + let algo = CosmosAccountAlgo::from_str(&key.algo).map_to_mm(|err| { + WalletConnectError::PayloadError(format!("error decoding algo payload: {err:?}")) + })?; + + return Ok(CosmosAccount { + address, + pubkey, + algo, + is_ledger: Some(key.is_nano_ledger), + }); + } + } + } + + let params = serde_json::to_value(&account).unwrap(); + wc.send_session_request_and_wait( + session_topic, + &chain_id, + WcRequestMethods::CosmosGetAccounts, + params, + |accounts: Vec| { + if accounts.is_empty() { + return MmError::err(WalletConnectError::EmptyAccount(chain_id.to_string())); + }; + + Ok(accounts[0].clone()) + }, + ) + .await +} + +fn deserialize_vec_field<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let value = Value::deserialize(deserializer)?; + + match value { + Value::Object(map) => map + .iter() + .map(|(_, value)| { + value + .as_u64() + .ok_or_else(|| serde::de::Error::custom("Invalid byte value")) + .and_then(|n| { + if n <= 0xff { + Ok(n as u8) + } else { + Err(serde::de::Error::custom("Invalid byte value")) + } + }) + }) + .collect(), + Value::Array(arr) => arr + .into_iter() + .map(|v| { + v.as_u64() + .ok_or_else(|| serde::de::Error::custom("Invalid byte value")) + .map(|n| n as u8) + }) + .collect(), + Value::String(data) => { + let data = decode_data(&data).map_err(|err| serde::de::Error::custom(err.to_string()))?; + Ok(data) + }, + _ => Err(serde::de::Error::custom("Pubkey must be an string, object or array")), + } +} + +fn decode_data(encoded: &str) -> Result, &'static str> { + if encoded.chars().all(|c| c.is_ascii_hexdigit()) && encoded.len() % 2 == 0 { + hex::decode(encoded).map_err(|_| "Invalid hex encoding") + } else if encoded.contains('=') || encoded.contains('/') || encoded.contains('+') || encoded.len() % 4 == 0 { + general_purpose::STANDARD + .decode(encoded) + .map_err(|_| "Invalid base64 encoding") + } else { + Err("Unknown encoding format") + } +} + +#[cfg(test)] +mod test_cosmos_walletconnect { + use serde_json::json; + + use super::{decode_data, CosmosSignData, CosmosTxPublicKey, CosmosTxSignature, CosmosTxSignedData}; + + #[test] + fn test_decode_base64() { + // "Hello world" in base64 + let base64_data = "SGVsbG8gd29ybGQ="; + let expected = b"Hello world".to_vec(); + let result = decode_data(base64_data); + assert_eq!(result.unwrap(), expected, "Base64 decoding failed"); + } + + #[test] + fn test_decode_hex() { + // "Hello world" in hex + let hex_data = "48656c6c6f20776f726c64"; + let expected = b"Hello world".to_vec(); + let result = decode_data(hex_data); + assert_eq!(result.unwrap(), expected, "Hex decoding failed"); + } + + #[test] + fn test_deserialize_sign_message_response() { + let json = json!({ + "signature": { + "signature": "eGrmDGKTmycxJO56yTQORDzTFjBEBgyBmHc8ey6FbHh9WytzgsJilYBywz5uludhyKePZdRwznamg841fXw50Q==", + "pub_key": { + "type": "tendermint/PubKeySecp256k1", + "value": "AjqZ1rq/EsPAb4SA6l0qjpVMHzqXotYXz23D5kOceYYu" + } + }, + "signed": { + "chainId": "cosmoshub-4", + "authInfoBytes": "0a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21023a99d6babf12c3c06f8480ea5d2a8e954c1f3a97a2d617cf6dc3e6439c79862e12040a020801180212140a0e0a057561746f6d1205313837353010c8d007", + "bodyBytes": "0a8e010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e64126e0a2d636f736d6f7331376c386432737973646e3667683636786d366664666b6575333634703836326a68396c6e6667122d636f736d6f7331376c386432737973646e3667683636786d366664666b6575333634703836326a68396c6e66671a0e0a057561746f6d12053430303030189780e00a", + "accountNumber": "2934714" + } + }); + let expected_tx = CosmosTxSignedData { + signature: CosmosTxSignature { + pub_key: CosmosTxPublicKey { + key_type: "tendermint/PubKeySecp256k1".to_owned(), + value: "AjqZ1rq/EsPAb4SA6l0qjpVMHzqXotYXz23D5kOceYYu".to_owned(), + }, + signature: "eGrmDGKTmycxJO56yTQORDzTFjBEBgyBmHc8ey6FbHh9WytzgsJilYBywz5uludhyKePZdRwznamg841fXw50Q==" + .to_owned(), + }, + signed: CosmosSignData { + chain_id: "cosmoshub-4".to_owned(), + account_number: "2934714".to_owned(), + auth_info_bytes: vec![ + 10, 80, 10, 70, 10, 31, 47, 99, 111, 115, 109, 111, 115, 46, 99, 114, 121, 112, 116, 111, 46, 115, + 101, 99, 112, 50, 53, 54, 107, 49, 46, 80, 117, 98, 75, 101, 121, 18, 35, 10, 33, 2, 58, 153, 214, + 186, 191, 18, 195, 192, 111, 132, 128, 234, 93, 42, 142, 149, 76, 31, 58, 151, 162, 214, 23, 207, + 109, 195, 230, 67, 156, 121, 134, 46, 18, 4, 10, 2, 8, 1, 24, 2, 18, 20, 10, 14, 10, 5, 117, 97, + 116, 111, 109, 18, 5, 49, 56, 55, 53, 48, 16, 200, 208, 7, + ], + body_bytes: vec![ + 10, 142, 1, 10, 28, 47, 99, 111, 115, 109, 111, 115, 46, 98, 97, 110, 107, 46, 118, 49, 98, 101, + 116, 97, 49, 46, 77, 115, 103, 83, 101, 110, 100, 18, 110, 10, 45, 99, 111, 115, 109, 111, 115, 49, + 55, 108, 56, 100, 50, 115, 121, 115, 100, 110, 54, 103, 104, 54, 54, 120, 109, 54, 102, 100, 102, + 107, 101, 117, 51, 54, 52, 112, 56, 54, 50, 106, 104, 57, 108, 110, 102, 103, 18, 45, 99, 111, 115, + 109, 111, 115, 49, 55, 108, 56, 100, 50, 115, 121, 115, 100, 110, 54, 103, 104, 54, 54, 120, 109, + 54, 102, 100, 102, 107, 101, 117, 51, 54, 52, 112, 56, 54, 50, 106, 104, 57, 108, 110, 102, 103, + 26, 14, 10, 5, 117, 97, 116, 111, 109, 18, 5, 52, 48, 48, 48, 48, 24, 151, 128, 224, 10, + ], + }, + }; + + let actual_tx = serde_json::from_value::(json).unwrap(); + assert_eq!(expected_tx, actual_tx); + } +} diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 70c8522b58..dff5288971 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -380,6 +380,9 @@ pub fn my_public_key(coin: &UtxoCoinFields) -> Result<&Public, MmError MmError::err(UnexpectedDerivationMethod::UnsupportedError( "`PrivKeyPolicy::Metamask` is not supported in this context".to_string(), )), + PrivKeyPolicy::WalletConnect { .. } => MmError::err(UnexpectedDerivationMethod::UnsupportedError( + "`PrivKeyPolicy::WalletConnect` is not supported in this context".to_string(), + )), } } @@ -2951,6 +2954,7 @@ pub fn display_priv_key(coin: &UtxoCoinFields) -> Result { PrivKeyPolicy::Trezor => ERR!("'display_priv_key' is not supported for Hardware Wallets"), #[cfg(target_arch = "wasm32")] PrivKeyPolicy::Metamask(_) => ERR!("'display_priv_key' doesn't support Metamask"), + PrivKeyPolicy::WalletConnect { .. } => ERR!("'display_priv_key' doesn't support WalletConnect"), } } @@ -4741,9 +4745,10 @@ pub fn derive_htlc_key_pair(coin: &UtxoCoinFields, _swap_unique_data: &[u8]) -> activated_key: activated_key_pair, .. } => activated_key_pair, - PrivKeyPolicy::Trezor => todo!(), + PrivKeyPolicy::Trezor => panic!("`PrivKeyPolicy::Trezor` is not supported for UTXO coins"), #[cfg(target_arch = "wasm32")] PrivKeyPolicy::Metamask(_) => panic!("`PrivKeyPolicy::Metamask` is not supported for UTXO coins"), + PrivKeyPolicy::WalletConnect { .. } => panic!("`PrivKeyPolicy::WalletConnect` is not supported for UTXO coins"), } } diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index bcd7cc991f..909fdcb230 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -612,6 +612,7 @@ fn test_withdraw_impl_set_fixed_fee() { }), memo: None, ibc_source_channel: None, + broadcast: false, }; let expected = Some( UtxoFeeDetails { @@ -661,6 +662,7 @@ fn test_withdraw_impl_sat_per_kb_fee() { }), memo: None, ibc_source_channel: None, + broadcast: false, }; // The resulting transaction size might be 244 or 245 bytes depending on signature size // MM2 always expects the worst case during fee calculation @@ -713,6 +715,7 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_equal_to_max() { }), memo: None, ibc_source_channel: None, + broadcast: false, }; let tx_details = block_on_f01(coin.withdraw(withdraw_req)).unwrap(); // The resulting transaction size might be 210 or 211 bytes depending on signature size @@ -767,6 +770,7 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_equal_to_max_dust_included_to_fee() }), memo: None, ibc_source_channel: None, + broadcast: false, }; let tx_details = block_on_f01(coin.withdraw(withdraw_req)).unwrap(); // The resulting transaction size might be 210 or 211 bytes depending on signature size @@ -821,6 +825,7 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_over_max() { }), memo: None, ibc_source_channel: None, + broadcast: false, }; block_on_f01(coin.withdraw(withdraw_req)).unwrap_err(); } @@ -862,6 +867,7 @@ fn test_withdraw_impl_sat_per_kb_fee_max() { }), memo: None, ibc_source_channel: None, + broadcast: false, }; // The resulting transaction size might be 210 or 211 bytes depending on signature size // MM2 always expects the worst case during fee calculation @@ -929,6 +935,7 @@ fn test_withdraw_kmd_rewards_impl( fee: None, memo: None, ibc_source_channel: None, + broadcast: false, }; let expected_fee = TxFeeDetails::Utxo(UtxoFeeDetails { coin: Some("KMD".into()), @@ -1011,6 +1018,7 @@ fn test_withdraw_rick_rewards_none() { fee: None, memo: None, ibc_source_channel: None, + broadcast: false, }; let expected_fee = TxFeeDetails::Utxo(UtxoFeeDetails { coin: Some(TEST_COIN_NAME.into()), @@ -3211,6 +3219,7 @@ fn test_withdraw_to_p2pk_fails() { fee: None, memo: None, ibc_source_channel: None, + broadcast: false, }; assert!(matches!( @@ -3269,6 +3278,7 @@ fn test_withdraw_to_p2pkh() { fee: None, memo: None, ibc_source_channel: None, + broadcast: false, }; let tx_details = block_on_f01(coin.withdraw(withdraw_req)).unwrap(); let transaction: UtxoTx = deserialize(tx_details.tx.tx_hex().unwrap().as_slice()).unwrap(); @@ -3329,6 +3339,7 @@ fn test_withdraw_to_p2sh() { fee: None, memo: None, ibc_source_channel: None, + broadcast: false, }; let tx_details = block_on_f01(coin.withdraw(withdraw_req)).unwrap(); let transaction: UtxoTx = deserialize(tx_details.tx.tx_hex().unwrap().as_slice()).unwrap(); @@ -3389,6 +3400,7 @@ fn test_withdraw_to_p2wpkh() { fee: None, memo: None, ibc_source_channel: None, + broadcast: false, }; let tx_details = block_on_f01(coin.withdraw(withdraw_req)).unwrap(); let transaction: UtxoTx = deserialize(tx_details.tx.tx_hex().unwrap().as_slice()).unwrap(); @@ -3444,6 +3456,7 @@ fn test_withdraw_p2pk_balance() { fee: None, memo: None, ibc_source_channel: None, + broadcast: false, }; let tx_details = block_on_f01(coin.withdraw(withdraw_req)).unwrap(); let transaction: UtxoTx = deserialize(tx_details.tx.tx_hex().unwrap().as_slice()).unwrap(); diff --git a/mm2src/coins/utxo/utxo_withdraw.rs b/mm2src/coins/utxo/utxo_withdraw.rs index 8721a6a433..4d2e265b64 100644 --- a/mm2src/coins/utxo/utxo_withdraw.rs +++ b/mm2src/coins/utxo/utxo_withdraw.rs @@ -342,6 +342,11 @@ where "`PrivKeyPolicy::Metamask` is not supported for UTXO coins!".to_string(), )) }, + PrivKeyPolicy::WalletConnect { .. } => { + return MmError::err(WithdrawError::UnsupportedError( + "`PrivKeyPolicy::WalletConnect` is not supported for UTXO coins!".to_string(), + )) + }, }; self.task_handle diff --git a/mm2src/coins_activation/Cargo.toml b/mm2src/coins_activation/Cargo.toml index be951c67d4..b1df2dded1 100644 --- a/mm2src/coins_activation/Cargo.toml +++ b/mm2src/coins_activation/Cargo.toml @@ -20,6 +20,7 @@ derive_more = "0.99" ethereum-types = { version = "0.13", default-features = false, features = ["std", "serialize"] } futures = { version = "0.3", package = "futures", features = ["compat", "async-await"] } hex = "0.4.2" +kdf_walletconnect = { path = "../kdf_walletconnect" } mm2_core = { path = "../mm2_core" } mm2_err_handle = { path = "../mm2_err_handle" } mm2_event_stream = { path = "../mm2_event_stream" } diff --git a/mm2src/coins_activation/src/eth_with_token_activation.rs b/mm2src/coins_activation/src/eth_with_token_activation.rs index 7bc62b444a..7a6af55d7e 100644 --- a/mm2src/coins_activation/src/eth_with_token_activation.rs +++ b/mm2src/coins_activation/src/eth_with_token_activation.rs @@ -10,14 +10,16 @@ use crate::prelude::*; use async_trait::async_trait; use coins::coin_balance::{CoinBalanceReport, EnableCoinBalanceOps}; use coins::eth::v2_activation::{eth_coin_from_conf_and_request_v2, Erc20Protocol, Erc20TokenActivationRequest, - EthActivationV2Error, EthActivationV2Request, EthPrivKeyActivationPolicy}; -use coins::eth::v2_activation::{EthTokenActivationError, NftActivationRequest, NftProviderEnum}; + EthActivationV2Error, EthActivationV2Request, EthPrivKeyActivationPolicy, + EthTokenActivationError, NftActivationRequest, NftProviderEnum}; +use coins::eth::wallet_connect::eth_request_wc_personal_sign; use coins::eth::{display_eth_address, Erc20TokenDetails, EthCoin, EthCoinType, EthPrivKeyBuildPolicy}; use coins::hd_wallet::RpcTaskXPubExtractor; use coins::my_tx_history_v2::TxHistoryStorage; use coins::nft::nft_structs::NftInfo; use coins::{CoinBalance, CoinBalanceMap, CoinProtocol, CoinWithDerivationMethod, DerivationMethod, MarketCoinOps, MmCoin, MmCoinEnum}; +use kdf_walletconnect::WalletConnectCtx; use crate::platform_coin_with_tokens::InitPlatformCoinWithTokensTask; use common::Future01CompatExt; @@ -62,7 +64,9 @@ impl From for EnablePlatformCoinWithTokensError { EthActivationV2Error::FailedSpawningBalanceEvents(e) => { EnablePlatformCoinWithTokensError::FailedSpawningBalanceEvents(e) }, - EthActivationV2Error::HDWalletStorageError(e) => EnablePlatformCoinWithTokensError::Internal(e), + EthActivationV2Error::HDWalletStorageError(e) | EthActivationV2Error::WalletConnectError(e) => { + EnablePlatformCoinWithTokensError::Internal(e) + }, #[cfg(target_arch = "wasm32")] EthActivationV2Error::MetamaskError(metamask) => { EnablePlatformCoinWithTokensError::Transport(metamask.to_string()) @@ -279,7 +283,12 @@ impl PlatformCoinWithTokensActivationOps for EthCoin { activation_request: Self::ActivationRequest, _protocol: Self::PlatformProtocolInfo, ) -> Result> { - let priv_key_policy = eth_priv_key_build_policy(&ctx, &activation_request.platform_request.priv_key_policy)?; + let priv_key_policy = eth_priv_key_build_policy( + &ctx, + &activation_request.platform_request.priv_key_policy, + platform_conf, + ) + .await?; let platform_coin = eth_coin_from_conf_and_request_v2( &ctx, @@ -463,9 +472,10 @@ impl PlatformCoinWithTokensActivationOps for EthCoin { } } -fn eth_priv_key_build_policy( +async fn eth_priv_key_build_policy( ctx: &MmArc, activation_policy: &EthPrivKeyActivationPolicy, + conf: &Json, ) -> MmResult { match activation_policy { EthPrivKeyActivationPolicy::ContextPrivKey => Ok(EthPrivKeyBuildPolicy::detect_priv_key_policy(ctx)?), @@ -477,5 +487,20 @@ fn eth_priv_key_build_policy( Ok(EthPrivKeyBuildPolicy::Metamask(metamask_ctx)) }, EthPrivKeyActivationPolicy::Trezor => Ok(EthPrivKeyBuildPolicy::Trezor), + EthPrivKeyActivationPolicy::WalletConnect { session_topic } => { + let wc = WalletConnectCtx::from_ctx(ctx) + .expect("TODO: handle error when enable kdf initialization without key."); + let chain_id = conf["chain_id"].as_u64().ok_or(EthActivationV2Error::ChainIdNotSet)?; + let (public_key_uncompressed, address) = + eth_request_wc_personal_sign(&wc, session_topic, chain_id) + .await + .mm_err(|err| EthActivationV2Error::WalletConnectError(err.to_string()))?; + + Ok(EthPrivKeyBuildPolicy::WalletConnect { + address, + public_key_uncompressed, + session_topic: session_topic.clone(), + }) + }, } } diff --git a/mm2src/coins_activation/src/platform_coin_with_tokens.rs b/mm2src/coins_activation/src/platform_coin_with_tokens.rs index d5ee5cfbf0..dd44e1989c 100644 --- a/mm2src/coins_activation/src/platform_coin_with_tokens.rs +++ b/mm2src/coins_activation/src/platform_coin_with_tokens.rs @@ -84,7 +84,7 @@ pub trait TokenAsMmCoinInitializer: Send + Sync { async fn enable_tokens_as_mm_coins( &self, - ctx: MmArc, + ctx: &MmArc, request: &Self::ActivationRequest, ) -> Result, MmError>; } @@ -135,15 +135,14 @@ where async fn enable_tokens_as_mm_coins( &self, - ctx: MmArc, + ctx: &MmArc, request: &Self::ActivationRequest, ) -> Result, MmError> { let tokens_requests = T::tokens_requests_from_platform_request(request); let token_params = tokens_requests .into_iter() .map(|req| -> Result<_, MmError> { - let (token_conf, protocol): (_, T::TokenProtocol) = - coin_conf_with_protocol(&ctx, &req.ticker, req.protocol.clone())?; + let (token_conf, protocol) = coin_conf_with_protocol(ctx, &req.ticker, req.protocol.clone())?; Ok(TokenActivationParams { ticker: req.ticker, conf: token_conf, @@ -399,7 +398,7 @@ where { let mut mm_tokens = Vec::new(); for initializer in platform_coin.token_initializers() { - let tokens = initializer.enable_tokens_as_mm_coins(ctx.clone(), &req.request).await?; + let tokens = initializer.enable_tokens_as_mm_coins(&ctx, &req.request).await?; mm_tokens.extend(tokens); } @@ -469,7 +468,7 @@ where let mut mm_tokens = Vec::new(); for initializer in platform_coin.token_initializers() { - let tokens = initializer.enable_tokens_as_mm_coins(ctx.clone(), &req.request).await?; + let tokens = initializer.enable_tokens_as_mm_coins(&ctx, &req.request).await?; mm_tokens.extend(tokens); } diff --git a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs index 349e37b23d..2e74c66f63 100644 --- a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs +++ b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs @@ -11,13 +11,15 @@ use async_trait::async_trait; use coins::hd_wallet::HDPathAccountToAddressId; use coins::my_tx_history_v2::TxHistoryStorage; use coins::tendermint::tendermint_tx_history_v2::tendermint_history_loop; -use coins::tendermint::{tendermint_priv_key_policy, RpcNode, TendermintActivationPolicy, TendermintCoin, - TendermintCommons, TendermintConf, TendermintInitError, TendermintInitErrorKind, - TendermintProtocolInfo, TendermintPublicKey, TendermintToken, TendermintTokenActivationParams, - TendermintTokenInitError, TendermintTokenProtocolInfo}; +use coins::tendermint::{cosmos_get_accounts_impl, tendermint_priv_key_policy, CosmosAccountAlgo, RpcNode, + TendermintActivationPolicy, TendermintCoin, TendermintCommons, TendermintConf, + TendermintInitError, TendermintInitErrorKind, TendermintProtocolInfo, TendermintPublicKey, + TendermintToken, TendermintTokenActivationParams, TendermintTokenInitError, + TendermintTokenProtocolInfo, TendermintWalletConnectionType}; use coins::{CoinBalance, CoinProtocol, MarketCoinOps, MmCoin, MmCoinEnum, PrivKeyBuildPolicy}; use common::executor::{AbortSettings, SpawnAbortable}; use common::{true_f, Future01CompatExt}; +use kdf_walletconnect::WalletConnectCtx; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_event_stream::behaviour::{EventBehaviour, EventInitStatus}; @@ -38,6 +40,19 @@ impl RegisterTokenInfo for TendermintCoin { } } +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum TendermintPubkeyActivationParams { + /// Activation via public key + WithPubkey { + #[serde(deserialize_with = "deserialize_account_public_key")] + pubkey: TendermintPublicKey, + is_ledger_connection: bool, + }, + /// Activation via WalletConnect + WalletConnect { session_topic: String }, +} + #[derive(Clone, Deserialize)] pub struct TendermintActivationParams { nodes: Vec, @@ -50,13 +65,10 @@ pub struct TendermintActivationParams { #[serde(default)] pub path_to_address: HDPathAccountToAddressId, #[serde(default)] - #[serde(deserialize_with = "deserialize_account_public_key")] - with_pubkey: Option, - #[serde(default)] - is_keplr_from_ledger: bool, + pub activation_params: Option, } -fn deserialize_account_public_key<'de, D>(deserializer: D) -> Result, D::Error> +fn deserialize_account_public_key<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { @@ -74,7 +86,7 @@ where .iter() .map(|i| i.as_u64().unwrap() as u8) .collect(); - Ok(Some(TendermintPublicKey::from_raw_ed25519(&value).unwrap())) + Ok(TendermintPublicKey::from_raw_ed25519(&value).unwrap()) }, Some("secp256k1") => { let value: Vec = value @@ -83,7 +95,7 @@ where .iter() .map(|i| i.as_u64().unwrap() as u8) .collect(); - Ok(Some(TendermintPublicKey::from_raw_secp256k1(&value).unwrap())) + Ok(TendermintPublicKey::from_raw_secp256k1(&value).unwrap()) }, _ => Err(serde::de::Error::custom( "Unsupported pubkey algorithm. Use one of ['ed25519', 'secp256k1']", @@ -217,6 +229,37 @@ impl From for EnablePlatformCoinWithTokensError { } } +async fn activate_with_walletconnect( + ctx: &MmArc, + session_topic: String, + chain_id: &str, + ticker: &str, +) -> MmResult<(TendermintActivationPolicy, TendermintWalletConnectionType), TendermintInitError> { + let wc = WalletConnectCtx::from_ctx(ctx).expect("TODO: handle error when enable kdf initialization without key."); + let account = cosmos_get_accounts_impl(&wc, &session_topic, chain_id) + .await + .mm_err(|err| TendermintInitError { + ticker: ticker.to_string(), + kind: TendermintInitErrorKind::UnableToFetchChainAccount(err.to_string()), + })?; + let wallet_type = if wc.is_ledger_connection(&session_topic) { + TendermintWalletConnectionType::WcLedger(session_topic) + } else { + TendermintWalletConnectionType::Wc(session_topic) + }; + + let pubkey = match account.algo { + CosmosAccountAlgo::Secp256k1 | CosmosAccountAlgo::TendermintSecp256k1 => { + TendermintPublicKey::from_raw_secp256k1(&account.pubkey).ok_or(TendermintInitError { + ticker: ticker.to_string(), + kind: TendermintInitErrorKind::Internal("Invalid secp256k1 pubkey".to_owned()), + })? + }, + }; + + Ok((TendermintActivationPolicy::with_public_key(pubkey), wallet_type)) +} + #[async_trait] impl PlatformCoinWithTokensActivationOps for TendermintCoin { type ActivationRequest = TendermintActivationParams; @@ -236,17 +279,35 @@ impl PlatformCoinWithTokensActivationOps for TendermintCoin { protocol_conf: Self::PlatformProtocolInfo, ) -> Result> { let conf = TendermintConf::try_from_json(&ticker, coin_conf)?; - let is_keplr_from_ledger = activation_request.is_keplr_from_ledger && activation_request.with_pubkey.is_some(); - let activation_policy = if let Some(pubkey) = activation_request.with_pubkey { + let (activation_policy, wallet_connection_type) = if let Some(params) = activation_request.activation_params { if ctx.is_watcher() || ctx.use_watchers() { return MmError::err(TendermintInitError { ticker: ticker.clone(), kind: TendermintInitErrorKind::CantUseWatchersWithPubkeyPolicy, }); + }; + + match params { + TendermintPubkeyActivationParams::WithPubkey { + pubkey, + is_ledger_connection, + } => { + let wallet_connection_type = if is_ledger_connection { + TendermintWalletConnectionType::KeplrLedger + } else { + TendermintWalletConnectionType::Keplr + }; + + ( + TendermintActivationPolicy::with_public_key(pubkey), + wallet_connection_type, + ) + }, + TendermintPubkeyActivationParams::WalletConnect { session_topic } => { + activate_with_walletconnect(&ctx, session_topic, protocol_conf.chain_id.as_ref(), &ticker).await? + }, } - - TendermintActivationPolicy::with_public_key(pubkey) } else { let private_key_policy = PrivKeyBuildPolicy::detect_priv_key_policy(&ctx).mm_err(|e| TendermintInitError { @@ -257,7 +318,10 @@ impl PlatformCoinWithTokensActivationOps for TendermintCoin { let tendermint_private_key_policy = tendermint_priv_key_policy(&conf, &ticker, private_key_policy, activation_request.path_to_address)?; - TendermintActivationPolicy::with_private_key_policy(tendermint_private_key_policy) + ( + TendermintActivationPolicy::with_private_key_policy(tendermint_private_key_policy), + TendermintWalletConnectionType::Native, + ) }; TendermintCoin::init( @@ -268,7 +332,7 @@ impl PlatformCoinWithTokensActivationOps for TendermintCoin { activation_request.nodes, activation_request.tx_history, activation_policy, - is_keplr_from_ledger, + Some(wallet_connection_type), ) .await } diff --git a/mm2src/common/common.rs b/mm2src/common/common.rs index 288882d0ae..7b05b21fc7 100644 --- a/mm2src/common/common.rs +++ b/mm2src/common/common.rs @@ -141,6 +141,7 @@ pub mod wio; #[cfg(target_arch = "wasm32")] pub mod wasm; +use primitive_types::U256; #[cfg(target_arch = "wasm32")] pub use wasm::*; use backtrace::SymbolName; @@ -1155,6 +1156,10 @@ pub fn http_uri_to_ws_address(uri: http::Uri) -> String { format!("{}{}{}{}", address_prefix, host_address, port, path) } +/// Converts a U256 value to a lowercase hexadecimal string with "0x" prefix +#[inline] +pub fn u256_to_hex(value: U256) -> String { format!("0x{:x}", value) } + /// If 0x prefix exists in an str strip it or return the str as-is #[macro_export] macro_rules! str_strip_0x { diff --git a/mm2src/crypto/Cargo.toml b/mm2src/crypto/Cargo.toml index dd3bbec752..6e3b67d518 100644 --- a/mm2src/crypto/Cargo.toml +++ b/mm2src/crypto/Cargo.toml @@ -34,6 +34,7 @@ mm2_err_handle = { path = "../mm2_err_handle" } num-traits = "0.2" parking_lot = { version = "0.12.0", features = ["nightly"] } primitives = { path = "../mm2_bitcoin/primitives" } +rand = "0.8.5" rpc = { path = "../mm2_bitcoin/rpc" } rpc_task = { path = "../rpc_task" } rustc-hex = "2" @@ -60,3 +61,4 @@ tokio = { version = "1.20", default-features = false } [features] trezor-udp = ["trezor/trezor-udp"] + diff --git a/mm2src/db_common/src/async_sql_conn.rs b/mm2src/db_common/src/async_sql_conn.rs index 78357405a1..f9e8747fd7 100644 --- a/mm2src/db_common/src/async_sql_conn.rs +++ b/mm2src/db_common/src/async_sql_conn.rs @@ -43,6 +43,10 @@ impl std::error::Error for AsyncConnError { } } +impl From for AsyncConnError { + fn from(err: String) -> Self { Self::Internal(InternalError(err)) } +} + #[derive(Debug)] pub struct InternalError(pub String); diff --git a/mm2src/kdf_walletconnect/Cargo.toml b/mm2src/kdf_walletconnect/Cargo.toml new file mode 100644 index 0000000000..ecc68f85ba --- /dev/null +++ b/mm2src/kdf_walletconnect/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "kdf_walletconnect" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0.89" +async-trait = "0.1.52" +base64 = "0.21.2" +chrono = { version = "0.4.23", "features" = ["serde"] } +common = { path = "../common" } +hex = "0.4.2" +cfg-if = "1.0" +db_common = { path = "../db_common" } +derive_more = "0.99" +enum_derives = { path = "../derives/enum_derives" } +futures = { version = "0.3", package = "futures", features = [ + "compat", + "async-await", +] } +hkdf = "0.12.4" +mm2_core = { path = "../mm2_core" } +mm2_db = { path = "../mm2_db" } +mm2_err_handle = { path = "../mm2_err_handle" } +parking_lot = { version = "0.12.0", features = ["nightly"] } +pairing_api = { git = "https://github.com/komodoplatform/walletconnectrust", tag = "k-0.1.2" } +rand = "0.8" +relay_client = { git = "https://github.com/komodoplatform/walletconnectrust", tag = "k-0.1.2" } +relay_rpc = { git = "https://github.com/komodoplatform/walletconnectrust", tag = "k-0.1.2" } +thiserror = "1.0.40" +tokio = { version = "1.20" } +wc_common = { git = "https://github.com/komodoplatform/walletconnectrust", tag = "k-0.1.2" } +secp256k1 = { version = "0.20" } +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1", features = ["preserve_order", "raw_value"] } +sha2 = "0.10.7" +x25519-dalek = { version = "2.0", features = ["static_secrets"] } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +js-sys = { version = "0.3.27" } +mm2_db = { path = "../mm2_db" } +mm2_test_helpers = { path = "../mm2_test_helpers" } +wasm-bindgen = "0.2.86" +wasm-bindgen-test = { version = "0.3.2" } +wasm-bindgen-futures = "0.4.21" +web-sys = { version = "0.3.55", features = ["console", "CloseEvent", "DomException", "ErrorEvent", "IdbDatabase", + "IdbCursor", "IdbCursorWithValue", "IdbFactory", "IdbIndex", "IdbIndexParameters", "IdbObjectStore", + "IdbObjectStoreParameters", "IdbOpenDbRequest", "IdbKeyRange", "IdbTransaction", "IdbTransactionMode", + "IdbVersionChangeEvent", "MessageEvent", "MessagePort", "ReadableStreamDefaultReader", "ReadableStream"]} + +[dev-dependencies] +mm2_test_helpers = { path = "../mm2_test_helpers" } diff --git a/mm2src/kdf_walletconnect/src/chain.rs b/mm2src/kdf_walletconnect/src/chain.rs new file mode 100644 index 0000000000..20e1acd6a8 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/chain.rs @@ -0,0 +1,107 @@ +use mm2_err_handle::prelude::{MmError, MmResult}; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; + +use crate::error::WalletConnectError; + +pub(crate) const SUPPORTED_PROTOCOL: &str = "irn"; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum WcChain { + Eip155, + Cosmos, +} + +impl FromStr for WcChain { + type Err = MmError; + fn from_str(s: &str) -> Result { + match s { + "eip155" => Ok(WcChain::Eip155), + "cosmos" => Ok(WcChain::Cosmos), + _ => MmError::err(WalletConnectError::InvalidChainId(format!( + "chain_id not supported: {s}" + ))), + } + } +} + +impl AsRef for WcChain { + fn as_ref(&self) -> &str { + match self { + Self::Eip155 => "eip155", + Self::Cosmos => "cosmos", + } + } +} + +impl WcChain { + pub(crate) fn derive_chain_id(&self, id: String) -> WcChainId { + WcChainId { + chain: self.clone(), + id, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct WcChainId { + pub chain: WcChain, + pub id: String, +} + +impl std::fmt::Display for WcChainId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{}", self.chain.as_ref(), self.id) + } +} + +impl WcChainId { + pub fn new_eip155(id: String) -> Self { + Self { + chain: WcChain::Eip155, + id, + } + } + + pub fn new_cosmos(id: String) -> Self { + Self { + chain: WcChain::Cosmos, + id, + } + } + + pub fn try_from_str(chain_id: &str) -> MmResult { + let sp = chain_id.split(':').collect::>(); + if sp.len() != 2 { + return MmError::err(WalletConnectError::InvalidChainId(chain_id.to_string())); + }; + + Ok(Self { + chain: WcChain::from_str(sp[0])?, + id: sp[1].to_owned(), + }) + } +} + +#[derive(Debug, Clone)] +pub enum WcRequestMethods { + CosmosSignDirect, + CosmosSignAmino, + CosmosGetAccounts, + EthSignTransaction, + EthSendTransaction, + PersonalSign, +} + +impl AsRef for WcRequestMethods { + fn as_ref(&self) -> &str { + match self { + Self::CosmosSignDirect => "cosmos_signDirect", + Self::CosmosSignAmino => "cosmos_signAmino", + Self::CosmosGetAccounts => "cosmos_getAccounts", + Self::EthSignTransaction => "eth_signTransaction", + Self::EthSendTransaction => "eth_sendTransaction", + Self::PersonalSign => "personal_sign", + } + } +} diff --git a/mm2src/kdf_walletconnect/src/connection_handler.rs b/mm2src/kdf_walletconnect/src/connection_handler.rs new file mode 100644 index 0000000000..2ac7d7b7c3 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/connection_handler.rs @@ -0,0 +1,98 @@ +use crate::WalletConnectCtxImpl; + +use common::executor::Timer; +use common::log::{debug, error, info}; +use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; +use futures::StreamExt; +use relay_client::error::ClientError; +use relay_client::websocket::{CloseFrame, ConnectionHandler, PublishedMessage}; + +pub(crate) const MAX_BACKOFF: u64 = 60; + +pub struct Handler { + name: &'static str, + msg_sender: UnboundedSender, + conn_live_sender: UnboundedSender>, +} + +impl Handler { + pub fn new( + name: &'static str, + msg_sender: UnboundedSender, + conn_live_sender: UnboundedSender>, + ) -> Self { + Self { + name, + msg_sender, + conn_live_sender, + } + } +} + +impl ConnectionHandler for Handler { + fn connected(&mut self) { + debug!("[{}] connection to WalletConnect relay server successful", self.name); + } + + fn disconnected(&mut self, frame: Option>) { + debug!("[{}] connection closed: frame={frame:?}", self.name); + + if let Err(e) = self.conn_live_sender.unbounded_send(frame.map(|f| f.to_string())) { + error!("[{}] failed to send to the receiver: {e}", self.name); + } + } + + fn message_received(&mut self, message: PublishedMessage) { + debug!( + "[{}] inbound message: message_id={} topic={} tag={} message={}", + self.name, message.message_id, message.topic, message.tag, message.message, + ); + + if let Err(e) = self.msg_sender.unbounded_send(message) { + error!("[{}] failed to send to the receiver: {e}", self.name); + } + } + + fn inbound_error(&mut self, error: ClientError) { + debug!("[{}] inbound error: {error}", self.name); + if let Err(e) = self.conn_live_sender.unbounded_send(Some(error.to_string())) { + error!("[{}] failed to send to the receiver: {e}", self.name); + } + } + + fn outbound_error(&mut self, error: ClientError) { + debug!("[{}] outbound error: {error}", self.name); + if let Err(e) = self.conn_live_sender.unbounded_send(Some(error.to_string())) { + error!("[{}] failed to send to the receiver: {e}", self.name); + } + } +} + +/// Handles unexpected disconnections from WalletConnect relay server. +/// Implements exponential backoff retry mechanism for reconnection attempts. +/// After successful reconnection, resubscribes to previous topics to restore full functionality. +pub(crate) async fn handle_disconnections( + this: &WalletConnectCtxImpl, + mut connection_live_rx: UnboundedReceiver>, +) { + let mut backoff = 1; + + while let Some(msg) = connection_live_rx.next().await { + info!("WalletConnect disconnected with message: {msg:?}. Attempting to reconnect..."); + loop { + match this.reconnect_and_subscribe().await { + Ok(_) => { + info!("Reconnection process complete."); + backoff = 1; + break; + }, + Err(e) => { + error!("Reconnection attempt failed: {:?}. Retrying in {:?}...", e, backoff); + Timer::sleep(backoff as f64).await; + // Exponentially increase backoff, but cap it at MAX_BACKOFF + backoff = std::cmp::min(backoff * 2, MAX_BACKOFF); + }, + } + } + } +} diff --git a/mm2src/kdf_walletconnect/src/error.rs b/mm2src/kdf_walletconnect/src/error.rs new file mode 100644 index 0000000000..1d8b6bcf93 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/error.rs @@ -0,0 +1,186 @@ +use enum_derives::EnumFromStringify; +#[cfg(target_arch = "wasm32")] +use mm2_db::indexed_db::cursor_prelude::*; +#[cfg(target_arch = "wasm32")] +use mm2_db::indexed_db::{DbTransactionError, InitDbError}; +use pairing_api::PairingClientError; +use relay_client::error::{ClientError, Error}; +use relay_rpc::rpc::{PublishError, SubscriptionError}; +use serde::{Deserialize, Serialize}; + +// Error codes for various cases +pub(crate) const INVALID_METHOD: i32 = 1001; +pub(crate) const INVALID_EVENT: i32 = 1002; +pub(crate) const INVALID_UPDATE_REQUEST: i32 = 1003; +pub(crate) const INVALID_EXTEND_REQUEST: i32 = 1004; +pub(crate) const INVALID_SESSION_SETTLE_REQUEST: i32 = 1005; + +// Unauthorized error codes +pub(crate) const UNAUTHORIZED_METHOD: i32 = 3001; +pub(crate) const UNAUTHORIZED_EVENT: i32 = 3002; +pub(crate) const UNAUTHORIZED_UPDATE_REQUEST: i32 = 3003; +pub(crate) const UNAUTHORIZED_EXTEND_REQUEST: i32 = 3004; +pub(crate) const UNAUTHORIZED_CHAIN: i32 = 3005; + +// EIP-1193 error code +pub(crate) const USER_REJECTED_REQUEST: i32 = 4001; + +// Rejected (CAIP-25) error codes +pub(crate) const USER_REJECTED: i32 = 5000; +pub(crate) const USER_REJECTED_CHAINS: i32 = 5001; +pub(crate) const USER_REJECTED_METHODS: i32 = 5002; +pub(crate) const USER_REJECTED_EVENTS: i32 = 5003; + +// Unsupported error codes +pub(crate) const UNSUPPORTED_CHAINS: i32 = 5100; +pub(crate) const UNSUPPORTED_METHODS: i32 = 5101; +pub(crate) const UNSUPPORTED_EVENTS: i32 = 5102; +pub(crate) const UNSUPPORTED_ACCOUNTS: i32 = 5103; +pub(crate) const UNSUPPORTED_NAMESPACE_KEY: i32 = 5104; + +pub(crate) const USER_REQUESTED: i64 = 6000; + +#[derive(Debug, Serialize, Deserialize, EnumFromStringify, thiserror::Error)] +pub enum WalletConnectError { + #[error("Pairing Error: {0}")] + #[from_stringify("PairingClientError")] + PairingError(String), + #[error("Publish Error: {0}")] + PublishError(String), + #[error("Client Error: {0}")] + #[from_stringify("ClientError")] + ClientError(String), + #[error("Subscription Error: {0}")] + SubscriptionError(String), + #[error("Internal Error: {0}")] + InternalError(String), + #[error("Serde Error: {0}")] + #[from_stringify("serde_json::Error")] + SerdeError(String), + #[error("UnSuccessfulResponse Error: {0}")] + UnSuccessfulResponse(String), + #[error("Session Error: {0}")] + #[from_stringify("SessionError")] + SessionError(String), + #[error("Unknown params")] + InvalidRequest, + #[error("Request is not yet implemented")] + NotImplemented, + #[error("Hex Error: {0}")] + #[from_stringify("hex::FromHexError")] + HexError(String), + #[error("Payload Error: {0}")] + #[from_stringify("wc_common::PayloadError")] + PayloadError(String), + #[error("Account not found for chain_id: {0}")] + NoAccountFound(String), + #[error("Account not found for index: {0}")] + NoAccountFoundForIndex(usize), + #[error("Empty account approved for chain_id: {0}")] + EmptyAccount(String), + #[error("WalletConnect is not initaliazed yet!")] + NotInitialized, + #[error("Storage Error: {0}")] + StorageError(String), + #[error("ChainId mismatch")] + ChainIdMismatch, + #[error("No feedback from wallet")] + NoWalletFeedback, + #[error("Invalid ChainId Error: {0}")] + InvalidChainId(String), + #[error("ChainId not supported: {0}")] + ChainIdNotSupported(String), + #[error("Request timeout error")] + TimeoutError, +} + +impl From> for WalletConnectError { + fn from(error: Error) -> Self { WalletConnectError::PublishError(format!("{error:?}")) } +} + +impl From> for WalletConnectError { + fn from(error: Error) -> Self { WalletConnectError::SubscriptionError(format!("{error:?}")) } +} + +/// Session key and topic derivation errors. +#[derive(Debug, Clone, thiserror::Error)] +pub enum SessionError { + #[error("Failed to generate symmetric session key: {0}")] + SymKeyGeneration(String), +} + +#[cfg(target_arch = "wasm32")] +#[derive(Debug, Clone, thiserror::Error)] +pub enum WcIndexedDbError { + #[error("Internal Error: {0}")] + InternalError(String), + #[error("Not supported: {0}")] + NotSupported(String), + #[error("Delete Error: {0}")] + DeletionError(String), + #[error("Insert Error: {0}")] + AddToStorageErr(String), + #[error("GetFromStorage Error: {0}")] + GetFromStorageError(String), + #[error("Decoding Error: {0}")] + DecodingError(String), +} + +#[cfg(target_arch = "wasm32")] +impl From for WcIndexedDbError { + fn from(e: InitDbError) -> Self { + match &e { + InitDbError::NotSupported(_) => WcIndexedDbError::NotSupported(e.to_string()), + InitDbError::EmptyTableList + | InitDbError::DbIsOpenAlready { .. } + | InitDbError::InvalidVersion(_) + | InitDbError::OpeningError(_) + | InitDbError::TypeMismatch { .. } + | InitDbError::UnexpectedState(_) + | InitDbError::UpgradingError { .. } => WcIndexedDbError::InternalError(e.to_string()), + } + } +} + +#[cfg(target_arch = "wasm32")] +impl From for WcIndexedDbError { + fn from(e: DbTransactionError) -> Self { + match e { + DbTransactionError::ErrorSerializingItem(_) | DbTransactionError::ErrorDeserializingItem(_) => { + WcIndexedDbError::DecodingError(e.to_string()) + }, + DbTransactionError::ErrorUploadingItem(_) => WcIndexedDbError::AddToStorageErr(e.to_string()), + DbTransactionError::ErrorGettingItems(_) | DbTransactionError::ErrorCountingItems(_) => { + WcIndexedDbError::GetFromStorageError(e.to_string()) + }, + DbTransactionError::ErrorDeletingItems(_) => WcIndexedDbError::DeletionError(e.to_string()), + DbTransactionError::NoSuchTable { .. } + | DbTransactionError::ErrorCreatingTransaction(_) + | DbTransactionError::ErrorOpeningTable { .. } + | DbTransactionError::ErrorSerializingIndex { .. } + | DbTransactionError::UnexpectedState(_) + | DbTransactionError::TransactionAborted + | DbTransactionError::MultipleItemsByUniqueIndex { .. } + | DbTransactionError::NoSuchIndex { .. } + | DbTransactionError::InvalidIndex { .. } => WcIndexedDbError::InternalError(e.to_string()), + } + } +} + +#[cfg(target_arch = "wasm32")] +impl From for WcIndexedDbError { + fn from(value: CursorError) -> Self { + match value { + CursorError::ErrorSerializingIndexFieldValue { .. } + | CursorError::ErrorDeserializingIndexValue { .. } + | CursorError::ErrorDeserializingItem(_) => Self::DecodingError(value.to_string()), + CursorError::ErrorOpeningCursor { .. } + | CursorError::AdvanceError { .. } + | CursorError::InvalidKeyRange { .. } + | CursorError::IncorrectNumberOfKeysPerIndex { .. } + | CursorError::UnexpectedState(_) + | CursorError::IncorrectUsage { .. } + | CursorError::TypeMismatch { .. } => Self::InternalError(value.to_string()), + } + } +} diff --git a/mm2src/kdf_walletconnect/src/inbound_message.rs b/mm2src/kdf_walletconnect/src/inbound_message.rs new file mode 100644 index 0000000000..f2ac60036a --- /dev/null +++ b/mm2src/kdf_walletconnect/src/inbound_message.rs @@ -0,0 +1,98 @@ +use crate::{error::WalletConnectError, + pairing::{reply_pairing_delete_response, reply_pairing_extend_response, reply_pairing_ping_response}, + session::rpc::{delete::reply_session_delete_request, + event::handle_session_event, + extend::reply_session_extend_request, + ping::reply_session_ping_request, + propose::{process_session_propose_response, reply_session_proposal_request}, + settle::reply_session_settle_request, + update::reply_session_update_request}, + WalletConnectCtxImpl}; + +use common::log::{info, LogOnError}; +use mm2_err_handle::prelude::*; +use relay_rpc::domain::{MessageId, Topic}; +use relay_rpc::rpc::{params::ResponseParamsSuccess, Params, Request, Response}; + +pub(crate) type SessionMessageType = MmResult; + +#[derive(Debug)] +pub struct SessionMessage { + pub message_id: MessageId, + pub topic: Topic, + pub data: ResponseParamsSuccess, +} + +/// Processes an inbound WalletConnect request and performs the appropriate action based on the request type. +/// +/// Handles various session and pairing requests, routing them to their corresponding handlers. +pub(crate) async fn process_inbound_request( + ctx: &WalletConnectCtxImpl, + request: Request, + topic: &Topic, +) -> MmResult<(), WalletConnectError> { + let message_id = request.id; + match request.params { + Params::SessionPropose(proposal) => reply_session_proposal_request(ctx, proposal, topic, &message_id).await?, + Params::SessionExtend(param) => reply_session_extend_request(ctx, topic, &message_id, param).await?, + Params::SessionDelete(param) => reply_session_delete_request(ctx, topic, &message_id, param).await?, + Params::SessionPing(()) => reply_session_ping_request(ctx, topic, &message_id).await?, + Params::SessionSettle(param) => reply_session_settle_request(ctx, topic, param).await?, + Params::SessionUpdate(param) => reply_session_update_request(ctx, topic, &message_id, param).await?, + Params::SessionEvent(param) => handle_session_event(ctx, topic, &message_id, param).await?, + Params::SessionRequest(_param) => { + // TODO: Implement when integrating KDF as a Dapp. + return MmError::err(WalletConnectError::NotImplemented); + }, + + Params::PairingPing(_param) => reply_pairing_ping_response(ctx, topic, &message_id).await?, + Params::PairingDelete(param) => reply_pairing_delete_response(ctx, topic, &message_id, param).await?, + Params::PairingExtend(param) => reply_pairing_extend_response(ctx, topic, &message_id, param).await?, + _ => { + info!("Unknown request params received."); + return MmError::err(WalletConnectError::InvalidRequest); + }, + }; + + Ok(()) +} + +/// Processes an inbound WalletConnect response and sends the result to the provided message channel. +/// +/// Handles successful responses, errors, and specific session proposal processing. +pub(crate) async fn process_inbound_response(ctx: &WalletConnectCtxImpl, response: Response, topic: &Topic) { + let message_id = response.id(); + let result = match &response { + Response::Success(value) => match serde_json::from_value::(value.result.clone()) { + Ok(ResponseParamsSuccess::SessionPropose(propose)) => { + // If this is a session propose response, process it right away and return. + // Session proposal responses are not waited for since it might take a long time + // for the proposal to be accepted (user interaction). So they are handled in async fashion. + ctx.pending_requests + .lock() + .expect("pending request lock shouldn't fail!") + .remove(&message_id); + return process_session_propose_response(ctx, topic, &propose) + .await + .error_log_with_msg("Failed to process session propose response"); + }, + Ok(data) => Ok(SessionMessage { + message_id, + topic: topic.clone(), + data, + }), + Err(err) => MmError::err(WalletConnectError::SerdeError(err.to_string())), + }, + Response::Error(err) => MmError::err(WalletConnectError::UnSuccessfulResponse(format!("{err:?}"))), + }; + + let mut pending_requests = ctx + .pending_requests + .lock() + .expect("pending request lock shouldn't fail!"); + if let Some(tx) = pending_requests.remove(&message_id) { + tx.send(result).ok(); + } else { + common::log::error!("[{topic}] unrecognized inbound response/message: {response:?}"); + }; +} diff --git a/mm2src/kdf_walletconnect/src/lib.rs b/mm2src/kdf_walletconnect/src/lib.rs new file mode 100644 index 0000000000..8ff938f2d1 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/lib.rs @@ -0,0 +1,637 @@ +pub mod chain; +mod connection_handler; +#[allow(unused)] pub mod error; +pub mod inbound_message; +mod metadata; +#[allow(unused)] mod pairing; +pub mod session; +mod storage; + +use crate::connection_handler::{handle_disconnections, MAX_BACKOFF}; +use crate::session::rpc::propose::send_proposal_request; + +use chain::{WcChainId, WcRequestMethods, SUPPORTED_PROTOCOL}; +use common::custom_futures::timeout::FutureTimerExt; +use common::executor::abortable_queue::AbortableQueue; +use common::executor::{AbortableSystem, Timer}; +use common::log::{debug, info, LogOnError}; +use common::{executor::SpawnFuture, log::error}; +use connection_handler::Handler; +use error::WalletConnectError; +use futures::channel::mpsc::{unbounded, UnboundedReceiver}; +use futures::StreamExt; +use inbound_message::{process_inbound_request, process_inbound_response, SessionMessageType}; +use metadata::{generate_metadata, AUTH_TOKEN_DURATION, AUTH_TOKEN_SUB, PROJECT_ID, RELAY_ADDRESS}; +use mm2_core::mm_ctx::{from_ctx, MmArc}; +use mm2_err_handle::prelude::*; +use pairing_api::PairingClient; +use relay_client::websocket::{connection_event_loop as client_event_loop, Client, PublishedMessage}; +use relay_client::{ConnectionOptions, MessageIdGenerator}; +use relay_rpc::auth::{ed25519_dalek::SigningKey, AuthToken}; +use relay_rpc::domain::{MessageId, Topic}; +use relay_rpc::rpc::params::session::Namespace; +use relay_rpc::rpc::params::session_request::SessionRequestRequest; +use relay_rpc::rpc::params::{session_request::Request as SessionRequest, IrnMetadata, Metadata, Relay, + RelayProtocolMetadata, RequestParams, ResponseParamsError, ResponseParamsSuccess}; +use relay_rpc::rpc::{ErrorResponse, Payload, Request, Response, SuccessfulResponse}; +use serde::de::DeserializeOwned; +use session::rpc::delete::send_session_delete_request; +use session::{key::SymKeyPair, SessionManager}; +use session::{Session, SessionProperties}; +use std::collections::{BTreeSet, HashMap}; +use std::ops::Deref; +use std::{sync::{Arc, Mutex}, + time::Duration}; +use storage::SessionStorageDb; +use storage::WalletConnectStorageOps; +use tokio::sync::oneshot; +use wc_common::{decode_and_decrypt_type0, encrypt_and_encode, EnvelopeType, SymKey}; + +const PUBLISH_TIMEOUT_SECS: f64 = 6.; +const MAX_RETRIES: usize = 5; + +#[async_trait::async_trait] +pub trait WalletConnectOps { + type Error; + type Params<'a>; + type SignTxData; + type SendTxData; + + async fn wc_chain_id(&self, ctx: &WalletConnectCtx) -> Result; + + async fn wc_sign_tx<'a>( + &self, + wc: &WalletConnectCtx, + params: Self::Params<'a>, + ) -> Result; + + async fn wc_send_tx<'a>( + &self, + wc: &WalletConnectCtx, + params: Self::Params<'a>, + ) -> Result; + + fn session_topic(&self) -> Result<&str, Self::Error>; +} + +pub struct WalletConnectCtxImpl { + pub(crate) client: Client, + pub(crate) pairing: PairingClient, + pub(crate) key_pair: SymKeyPair, + pub session_manager: SessionManager, + relay: Relay, + metadata: Metadata, + message_id_generator: MessageIdGenerator, + pending_requests: Mutex>>, + abortable_system: AbortableQueue, +} + +pub struct WalletConnectCtx(pub Arc); +impl Deref for WalletConnectCtx { + type Target = WalletConnectCtxImpl; + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl WalletConnectCtx { + pub fn try_init(ctx: &MmArc) -> MmResult { + let abortable_system = ctx + .abortable_system + .create_subsystem::() + .map_to_mm(|err| WalletConnectError::InternalError(err.to_string()))?; + let storage = SessionStorageDb::new(ctx)?; + let pairing = PairingClient::new(); + let relay = Relay { + protocol: SUPPORTED_PROTOCOL.to_string(), + data: None, + }; + let (inbound_message_tx, inbound_message_rx) = unbounded(); + let (conn_live_sender, conn_live_receiver) = unbounded(); + let (client, _) = Client::new_with_callback( + Handler::new("Komodefi", inbound_message_tx, conn_live_sender), + |receiver, handler| { + abortable_system + .weak_spawner() + .spawn(client_event_loop(receiver, handler)) + }, + ); + + let message_id_generator = MessageIdGenerator::new(); + let context = Arc::new(WalletConnectCtxImpl { + client, + pairing, + relay, + metadata: generate_metadata(), + key_pair: SymKeyPair::new(), + session_manager: SessionManager::new(storage), + pending_requests: Default::default(), + message_id_generator, + abortable_system, + }); + + // Connect to relayer client and spawn a watcher loop for disconnection. + context + .abortable_system + .weak_spawner() + .spawn(context.clone().spawn_connection_initialization_fut(conn_live_receiver)); + + // spawn message handler event loop + context + .abortable_system + .weak_spawner() + .spawn(context.clone().spawn_published_message_fut(inbound_message_rx)); + + Ok(Self(context)) + } + + pub fn from_ctx(ctx: &MmArc) -> MmResult, WalletConnectError> { + from_ctx(&ctx.wallet_connect, move || { + Self::try_init(ctx).map_err(|err| err.to_string()) + }) + .map_to_mm(WalletConnectError::InternalError) + } +} + +impl WalletConnectCtxImpl { + /// Establishes initial connection to WalletConnect relay server with linear retry mechanism. + /// Uses increasing delay between retry attempts starting from 1sec and increase exponentially. + /// After successful connection, attempts to restore previous session state from storage. + pub(crate) async fn spawn_connection_initialization_fut( + self: Arc, + connection_live_rx: UnboundedReceiver>, + ) { + info!("Initializing WalletConnect connection"); + let mut retry_count = 0; + let mut retry_secs = 1; + + // Connect to WalletConnect relay client(retry until successful) before proceeeding with other initializations. + while let Err(err) = self.connect_client().await { + retry_count += 1; + error!( + "Error during initial connection attempt {}: {:?}. Retrying in {retry_secs} seconds...", + retry_count, err + ); + Timer::sleep(retry_secs as f64).await; + retry_secs = std::cmp::min(retry_secs * 2, MAX_BACKOFF); + } + + // Initialize storage + if let Err(err) = self.session_manager.storage().init().await { + error!("Failed to initialize WalletConnect storage, shutting down: {err:?}"); + self.abortable_system.abort_all().error_log(); + }; + + // load session from storage + if let Err(err) = self.load_session_from_storage().await { + error!("Failed to load sessions from storage, shutting down: {err:?}"); + self.abortable_system.abort_all().error_log(); + }; + + // Spawn session disconnection watcher. + handle_disconnections(&self, connection_live_rx).await; + } + + pub async fn connect_client(&self) -> MmResult<(), WalletConnectError> { + let auth = { + let key = SigningKey::generate(&mut rand::thread_rng()); + AuthToken::new(AUTH_TOKEN_SUB) + .aud(RELAY_ADDRESS) + .ttl(AUTH_TOKEN_DURATION) + .as_jwt(&key) + .map_to_mm(|err| WalletConnectError::InternalError(err.to_string()))? + }; + let opts = ConnectionOptions::new(PROJECT_ID, auth).with_address(RELAY_ADDRESS); + self.client.connect(&opts).await?; + + Ok(()) + } + + /// Re-connect to WalletConnect relayer and re-subscribes to previously active session topics after reconnection. + pub(crate) async fn reconnect_and_subscribe(&self) -> MmResult<(), WalletConnectError> { + self.connect_client().await?; + let sessions = self + .session_manager + .get_sessions() + .flat_map(|s| vec![s.topic, s.pairing_topic]) + .collect::>(); + + if !sessions.is_empty() { + self.client.batch_subscribe(sessions).await?; + } + + Ok(()) + } + + /// Create a WalletConnect pairing connection url. + pub async fn new_connection( + &self, + required_namespaces: serde_json::Value, + optional_namespaces: Option, + ) -> MmResult { + let required_namespaces = serde_json::from_value(required_namespaces)?; + let optional_namespaces = match optional_namespaces { + Some(value) => Some(serde_json::from_value(value)?), + None => None, + }; + let (topic, url) = self.pairing.create(self.metadata.clone(), None)?; + + info!("[{topic}] Subscribing to topic"); + + for attempt in 0..MAX_RETRIES { + match self + .client + .subscribe(topic.clone()) + .timeout_secs(PUBLISH_TIMEOUT_SECS) + .await + { + Ok(res) => { + res.map_to_mm(|err| err.into())?; + info!("[{topic}] Subscribed to topic"); + send_proposal_request(self, &topic, required_namespaces, optional_namespaces).await?; + return Ok(url); + }, + Err(_) => self.wait_until_client_is_online_loop(attempt).await, + } + } + + MmError::err(WalletConnectError::InternalError( + "client connection timeout".to_string(), + )) + } + + /// Retrieves the symmetric key associated with a given `topic`. + fn sym_key(&self, topic: &Topic) -> MmResult { + self.session_manager + .sym_key(topic) + .or_else(|| self.pairing.sym_key(topic).ok()) + .ok_or_else(|| { + error!("Failed to find sym_key for topic: {topic}"); + MmError::new(WalletConnectError::InternalError(format!( + "topic sym_key not found: {topic}" + ))) + }) + } + + /// Handles an inbound published message by decrypting, decoding, and processing it. + async fn handle_published_message(&self, msg: PublishedMessage) -> MmResult<(), WalletConnectError> { + let message = { + let key = self.sym_key(&msg.topic)?; + decode_and_decrypt_type0(msg.message.as_bytes(), &key)? + }; + + debug!("[{}] Inbound message payload={message}", msg.topic); + + match serde_json::from_str(&message)? { + Payload::Request(request) => process_inbound_request(self, request, &msg.topic).await?, + Payload::Response(response) => process_inbound_response(self, response, &msg.topic).await, + } + + debug!("[{}] Inbound message was handled successfully", msg.topic); + + Ok(()) + } + + // Spawns a task that continuously processes published messages from inbound message channel. + async fn spawn_published_message_fut(self: Arc, mut recv: UnboundedReceiver) { + while let Some(msg) = recv.next().await { + self.handle_published_message(msg) + .await + .error_log_with_msg("Error processing message"); + } + } + + /// Loads sessions from storage, activates valid ones, and deletes expired ones. + async fn load_session_from_storage(&self) -> MmResult<(), WalletConnectError> { + info!("Loading WalletConnect session from storage"); + let now = chrono::Utc::now().timestamp() as u64; + let sessions = self + .session_manager + .storage() + .get_all_sessions() + .await + .mm_err(|err| WalletConnectError::StorageError(err.to_string()))?; + let mut valid_topics = Vec::with_capacity(sessions.len()); + let mut pairing_topics = Vec::with_capacity(sessions.len()); + + // bring most recent active session to the back. + for session in sessions.into_iter().rev() { + // delete expired session + if now > session.expiry { + debug!("Session {} expired, trying to delete from storage", session.topic); + self.session_manager + .storage() + .delete_session(&session.topic) + .await + .error_log_with_msg(&format!("[{}] Unable to delete session from storage", session.topic)); + continue; + }; + + let topic = session.topic.clone(); + let pairing_topic = session.pairing_topic.clone(); + debug!("[{topic}] Session found! activating"); + self.session_manager.add_session(session); + + valid_topics.push(topic); + pairing_topics.push(pairing_topic); + } + + let all_topics = valid_topics.into_iter().chain(pairing_topics).collect::>(); + + if !all_topics.is_empty() { + self.client.batch_subscribe(all_topics).await?; + } + + Ok(()) + } + + /// function to publish a request. + pub(crate) async fn publish_request( + &self, + topic: &Topic, + param: RequestParams, + ) -> MmResult<(oneshot::Receiver, Duration), WalletConnectError> { + let irn_metadata = param.irn_metadata(); + let ttl = irn_metadata.ttl; + let message_id = self.message_id_generator.next(); + let request = Request::new(message_id, param.into()); + + self.publish_payload(topic, irn_metadata, Payload::Request(request)) + .await?; + + let (tx, rx) = oneshot::channel(); + self.pending_requests + .lock() + .expect("pending request lock shouldn't fail!") + .insert(message_id, tx); + + Ok((rx, Duration::from_secs(ttl))) + } + + /// Private function to publish a success request response. + pub(crate) async fn publish_response_ok( + &self, + topic: &Topic, + result: ResponseParamsSuccess, + message_id: &MessageId, + ) -> MmResult<(), WalletConnectError> { + let irn_metadata = result.irn_metadata(); + let value = serde_json::to_value(result)?; + let response = Response::Success(SuccessfulResponse::new(*message_id, value)); + + self.publish_payload(topic, irn_metadata, Payload::Response(response)) + .await + } + + /// Private function to publish an error request response. + pub(crate) async fn publish_response_err( + &self, + topic: &Topic, + error_data: ResponseParamsError, + message_id: &MessageId, + ) -> MmResult<(), WalletConnectError> { + let error = error_data.error(); + let irn_metadata = error_data.irn_metadata(); + let response = Response::Error(ErrorResponse::new(*message_id, error)); + + self.publish_payload(topic, irn_metadata, Payload::Response(response)) + .await + } + + /// Private function to publish a payload. + pub(crate) async fn publish_payload( + &self, + topic: &Topic, + irn_metadata: IrnMetadata, + payload: Payload, + ) -> MmResult<(), WalletConnectError> { + info!("[{topic}] Publishing message={payload:?}"); + let message = { + let sym_key = self.sym_key(topic)?; + let payload = serde_json::to_string(&payload)?; + encrypt_and_encode(EnvelopeType::Type0, payload, &sym_key)? + }; + + for attempt in 0..MAX_RETRIES { + match self + .client + .publish( + topic.clone(), + &*message, + None, + irn_metadata.tag, + Duration::from_secs(irn_metadata.ttl), + irn_metadata.prompt, + ) + .timeout_secs(PUBLISH_TIMEOUT_SECS) + .await + { + Ok(Ok(_)) => { + info!("[{topic}] Message published successfully"); + return Ok(()); + }, + Ok(Err(err)) => return MmError::err(err.into()), + Err(_) => self.wait_until_client_is_online_loop(attempt).await, + } + } + + MmError::err(WalletConnectError::InternalError( + "[{topic}] client connection timeout".to_string(), + )) + } + + /// This persistent reconnection and retry strategy keeps the WebSocket connection active, + /// allowing the client to automatically resume operations after network interruptions or disconnections. + /// Since TCP handles connection timeouts (which can be lengthy), we're using a shorter timeout here + /// to detect issues quickly and reconnect as needed. + async fn wait_until_client_is_online_loop(&self, attempt: usize) { + debug!("Attempt {} failed due to timeout. Reconnecting...", attempt + 1); + loop { + match self.reconnect_and_subscribe().await { + Ok(_) => { + info!("Reconnected and subscribed successfully."); + break; + }, + Err(reconnect_err) => { + error!("Reconnection attempt failed: {reconnect_err:?}. Retrying..."); + Timer::sleep(1.5).await; + }, + } + } + } + + /// Checks if the current session is connected to a Ledger device. + /// NOTE: for COSMOS chains only. + pub fn is_ledger_connection(&self, session_topic: &str) -> bool { + let session_topic = session_topic.into(); + self.session_manager + .get_session(&session_topic) + .and_then(|session| session.session_properties) + .and_then(|props| props.keys.as_ref().cloned()) + .and_then(|keys| keys.first().cloned()) + .map(|key| key.is_nano_ledger) + .unwrap_or(false) + } + + /// Checks if a given chain ID is supported. + pub(crate) fn validate_chain_id( + &self, + session: &Session, + chain_id: &WcChainId, + ) -> MmResult<(), WalletConnectError> { + if let Some(Namespace { + chains: Some(chains), .. + }) = session.namespaces.get(chain_id.chain.as_ref()) + { + if chains.contains(&chain_id.to_string()) { + return Ok(()); + }; + } + + // https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#13-chains-might-be-omitted-if-the-caip-2-is-defined-in-the-index + if session.namespaces.contains_key(&chain_id.to_string()) { + return Ok(()); + } + + MmError::err(WalletConnectError::ChainIdNotSupported(chain_id.to_string())) + } + + pub async fn validate_update_active_chain_id( + &self, + session_topic: &str, + chain_id: &WcChainId, + ) -> MmResult<(), WalletConnectError> { + let session_topic = session_topic.into(); + let session = + self.session_manager + .get_session(&session_topic) + .ok_or(MmError::new(WalletConnectError::SessionError( + "No active WalletConnect session found".to_string(), + )))?; + + self.validate_chain_id(&session, chain_id)?; + + // TODO: uncomment when WalletConnect wallets start listening to chainChanged event + // if WcChain::Eip155 != chain_id.chain { + // return Ok(()); + // }; + // + // if let Some(active_chain_id) = session.get_active_chain_id().await { + // if chain_id == active_chain_id { + // return Ok(()); + // } + // }; + // + // let event = SessionEventRequest { + // event: Event { + // name: "chainChanged".to_string(), + // data: serde_json::to_value(&chain_id.id)?, + // }, + // chain_id: chain_id.to_string(), + // }; + // self.publish_request(&session.topic, RequestParams::SessionEvent(event)) + // .await?; + // + // let wait_duration = Duration::from_secs(60); + // if let Ok(Some(resp)) = self.message_rx.lock().await.next().timeout(wait_duration).await { + // let result = resp.mm_err(WalletConnectError::InternalError)?; + // if let ResponseParamsSuccess::SessionEvent(data) = result.data { + // if !data { + // return MmError::err(WalletConnectError::PayloadError( + // "Please approve chain id change".to_owned(), + // )); + // } + // + // self.session + // .get_session_mut(&session.topic) + // .ok_or(MmError::new(WalletConnectError::SessionError( + // "No active WalletConnect session found".to_string(), + // )))? + // .set_active_chain_id(chain_id.clone()) + // .await; + // } + // } + + Ok(()) + } + + /// Retrieves the available account for a given chain ID. + pub fn get_account_and_properties_for_chain_id( + &self, + session_topic: &str, + chain_id: &WcChainId, + ) -> MmResult<(String, Option), WalletConnectError> { + let session_topic = session_topic.into(); + let session = + self.session_manager + .get_session(&session_topic) + .ok_or(MmError::new(WalletConnectError::SessionError( + "No active WalletConnect session found".to_string(), + )))?; + + if let Some(Namespace { + accounts: Some(accounts), + .. + }) = &session.namespaces.get(chain_id.chain.as_ref()) + { + if let Some(account) = find_account_in_namespace(accounts, &chain_id.id) { + return Ok((account, session.session_properties)); + } + }; + + MmError::err(WalletConnectError::NoAccountFound(chain_id.to_string())) + } + + /// Waits for and handles a WalletConnect session response with arbitrary data. + /// https://specs.walletconnect.com/2.0/specs/clients/sign/session-events#session_request + pub async fn send_session_request_and_wait( + &self, + session_topic: &str, + chain_id: &WcChainId, + method: WcRequestMethods, + params: serde_json::Value, + callback: F, + ) -> MmResult + where + T: DeserializeOwned, + F: Fn(T) -> MmResult, + { + let session_topic = session_topic.into(); + self.session_manager.validate_session_exists(&session_topic)?; + + let request = SessionRequestRequest { + chain_id: chain_id.to_string(), + request: SessionRequest { + method: method.as_ref().to_string(), + expiry: None, + params, + }, + }; + let (rx, ttl) = self + .publish_request(&session_topic, RequestParams::SessionRequest(request)) + .await?; + + let response = rx + .timeout(ttl) + .await + .map_to_mm(|_| WalletConnectError::TimeoutError)? + .map_to_mm(|err| WalletConnectError::InternalError(err.to_string()))??; + match response.data { + ResponseParamsSuccess::Arbitrary(data) => callback(serde_json::from_value::(data)?), + _ => MmError::err(WalletConnectError::PayloadError("Unexpected response type".to_string())), + } + } + + pub async fn drop_session(&self, topic: &Topic) -> MmResult<(), WalletConnectError> { + send_session_delete_request(self, topic).await + } +} + +fn find_account_in_namespace<'a>(accounts: &'a BTreeSet, chain_id: &'a str) -> Option { + accounts.iter().find_map(move |account_name| { + let parts: Vec<&str> = account_name.split(':').collect(); + if parts.len() >= 3 && parts[1] == chain_id { + Some(parts[2].to_string()) + } else { + None + } + }) +} diff --git a/mm2src/kdf_walletconnect/src/metadata.rs b/mm2src/kdf_walletconnect/src/metadata.rs new file mode 100644 index 0000000000..4ede6579c7 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/metadata.rs @@ -0,0 +1,20 @@ +use std::time::Duration; + +use relay_rpc::rpc::params::Metadata; + +pub(crate) const RELAY_ADDRESS: &str = "wss://relay.walletconnect.com"; +pub(crate) const PROJECT_ID: &str = "86e916bcbacee7f98225dde86b697f5b"; +pub(crate) const AUTH_TOKEN_SUB: &str = "http://127.0.0.1:8000"; +pub(crate) const AUTH_TOKEN_DURATION: Duration = Duration::from_secs(5 * 60 * 60); +pub(crate) const APP_NAME: &str = "Komodefi Framework"; +pub(crate) const APP_DESCRIPTION: &str = "WallectConnect Komodefi Framework Playground"; + +#[inline] +pub(crate) fn generate_metadata() -> Metadata { + Metadata { + description: APP_DESCRIPTION.to_owned(), + url: AUTH_TOKEN_SUB.to_owned(), + icons: vec!["https://avatars.githubusercontent.com/u/21276113?s=200&v=4".to_owned()], + name: APP_NAME.to_owned(), + } +} diff --git a/mm2src/kdf_walletconnect/src/pairing.rs b/mm2src/kdf_walletconnect/src/pairing.rs new file mode 100644 index 0000000000..4990dd197a --- /dev/null +++ b/mm2src/kdf_walletconnect/src/pairing.rs @@ -0,0 +1,48 @@ +use crate::session::{WcRequestResponseResult, THIRTY_DAYS}; +use crate::{error::WalletConnectError, WalletConnectCtxImpl}; + +use chrono::Utc; +use mm2_err_handle::prelude::MmResult; +use relay_rpc::domain::MessageId; +use relay_rpc::rpc::params::pairing_ping::PairingPingRequest; +use relay_rpc::rpc::params::{RelayProtocolMetadata, RequestParams}; +use relay_rpc::{domain::Topic, + rpc::params::{pairing_delete::PairingDeleteRequest, pairing_extend::PairingExtendRequest, + ResponseParamsSuccess}}; + +pub(crate) async fn reply_pairing_ping_response( + ctx: &WalletConnectCtxImpl, + topic: &Topic, + message_id: &MessageId, +) -> MmResult<(), WalletConnectError> { + let param = ResponseParamsSuccess::PairingPing(true); + ctx.publish_response_ok(topic, param, message_id).await?; + + Ok(()) +} + +pub(crate) async fn reply_pairing_extend_response( + ctx: &WalletConnectCtxImpl, + topic: &Topic, + message_id: &MessageId, + extend: PairingExtendRequest, +) -> MmResult<(), WalletConnectError> { + ctx.pairing.activate(topic)?; + let param = ResponseParamsSuccess::PairingExtend(true); + ctx.publish_response_ok(topic, param, message_id).await?; + + Ok(()) +} + +pub(crate) async fn reply_pairing_delete_response( + ctx: &WalletConnectCtxImpl, + topic: &Topic, + message_id: &MessageId, + _delete: PairingDeleteRequest, +) -> MmResult<(), WalletConnectError> { + ctx.pairing.disconnect_rpc(topic, &ctx.client).await?; + let param = ResponseParamsSuccess::PairingDelete(true); + ctx.publish_response_ok(topic, param, message_id).await?; + + Ok(()) +} diff --git a/mm2src/kdf_walletconnect/src/session/key.rs b/mm2src/kdf_walletconnect/src/session/key.rs new file mode 100644 index 0000000000..7ac299cae6 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/session/key.rs @@ -0,0 +1,197 @@ +use crate::error::SessionError; + +use serde::{Deserialize, Serialize}; +use wc_common::SymKey; +use x25519_dalek::{PublicKey, SharedSecret, StaticSecret}; +use {hkdf::Hkdf, + rand::{rngs::OsRng, CryptoRng, RngCore}, + sha2::{Digest, Sha256}}; + +pub(crate) struct SymKeyPair { + pub(crate) secret: StaticSecret, + pub(crate) public_key: PublicKey, +} + +impl SymKeyPair { + pub(crate) fn new() -> Self { + let static_secret = StaticSecret::random_from_rng(OsRng); + let public_key = PublicKey::from(&static_secret); + Self { + secret: static_secret, + public_key, + } + } +} + +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct SessionKey { + pub(crate) sym_key: SymKey, + pub(crate) public_key: SymKey, +} + +impl std::fmt::Debug for SessionKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SessionKey") + .field("sym_key", &"*******") + .field("public_key", &self.public_key) + .finish() + } +} + +impl SessionKey { + /// Creates a new `SessionKey` with the given public key and an empty symmetric key. + pub fn new(public_key: PublicKey) -> Self { + Self { + sym_key: [0u8; 32], + public_key: public_key.to_bytes(), + } + } + + /// Creates a new `SessionKey` using a random number generator and a peer's public key. + pub fn from_osrng(other_public_key: &SymKey) -> Result { + SessionKey::diffie_hellman(OsRng, other_public_key) + } + + /// Performs Diffie-Hellman key exchange to derive a symmetric key. + pub fn diffie_hellman(csprng: T, other_public_key: &SymKey) -> Result + where + T: RngCore + CryptoRng, + { + let static_private_key = StaticSecret::random_from_rng(csprng); + let public_key = PublicKey::from(&static_private_key); + let shared_secret = static_private_key.diffie_hellman(&PublicKey::from(*other_public_key)); + + let mut session_key = Self { + sym_key: [0u8; 32], + public_key: public_key.to_bytes(), + }; + session_key.derive_symmetric_key(&shared_secret)?; + + Ok(session_key) + } + + /// Generates the symmetric key using the static secret and the peer's public key. + pub fn generate_symmetric_key( + &mut self, + static_secret: &StaticSecret, + peer_public_key: &SymKey, + ) -> Result<(), SessionError> { + let shared_secret = static_secret.diffie_hellman(&PublicKey::from(*peer_public_key)); + self.derive_symmetric_key(&shared_secret) + } + + /// Derives the symmetric key from a shared secret. + fn derive_symmetric_key(&mut self, shared_secret: &SharedSecret) -> Result<(), SessionError> { + let hk = Hkdf::::new(None, shared_secret.as_bytes()); + hk.expand(&[], &mut self.sym_key) + .map_err(|e| SessionError::SymKeyGeneration(e.to_string())) + } + + /// Gets symmetic key reference. + pub fn symmetric_key(&self) -> SymKey { self.sym_key } + + /// Gets "our" public key used in symmetric key derivation. + pub fn diffie_public_key(&self) -> SymKey { self.public_key } + + /// Generates new session topic. + pub fn generate_topic(&self) -> String { + let mut hasher = Sha256::new(); + hasher.update(self.sym_key); + hex::encode(hasher.finalize()) + } +} + +#[cfg(test)] +mod session_key_tests { + use super::*; + use anyhow::Result; + use rand::rngs::OsRng; + use x25519_dalek::{PublicKey, StaticSecret}; + + #[test] + fn test_diffie_hellman_key_exchange() -> Result<()> { + // Alice's key pair + let alice_static_secret = StaticSecret::random_from_rng(OsRng); + let alice_public_key = PublicKey::from(&alice_static_secret); + + // Bob's key pair + let bob_static_secret = StaticSecret::random_from_rng(OsRng); + let bob_public_key = PublicKey::from(&bob_static_secret); + + // Alice computes shared secret and session key + let alice_shared_secret = alice_static_secret.diffie_hellman(&bob_public_key); + let mut alice_session_key = SessionKey::new(alice_public_key); + alice_session_key.derive_symmetric_key(&alice_shared_secret)?; + + // Bob computes shared secret and session key + let bob_shared_secret = bob_static_secret.diffie_hellman(&alice_public_key); + let mut bob_session_key = SessionKey::new(bob_public_key); + bob_session_key.derive_symmetric_key(&bob_shared_secret)?; + + // Both symmetric keys should be the same + assert_eq!(alice_session_key.symmetric_key(), bob_session_key.symmetric_key()); + + // Ensure public keys are different + assert_ne!(alice_session_key.public_key, bob_session_key.public_key); + + Ok(()) + } + + #[test] + fn test_generate_symmetric_key() -> Result<()> { + // Alice's key pair + let alice_static_secret = StaticSecret::random_from_rng(OsRng); + let alice_public_key = PublicKey::from(&alice_static_secret); + + // Bob's public key + let bob_static_secret = StaticSecret::random_from_rng(OsRng); + let bob_public_key = PublicKey::from(&bob_static_secret); + + // Alice initializes session key + let mut alice_session_key = SessionKey::new(alice_public_key); + + // Alice generates symmetric key using Bob's public key + alice_session_key.generate_symmetric_key(&alice_static_secret, &bob_public_key.to_bytes())?; + + // Bob computes shared secret and session key + let bob_shared_secret = bob_static_secret.diffie_hellman(&alice_public_key); + let mut bob_session_key = SessionKey::new(bob_public_key); + bob_session_key.derive_symmetric_key(&bob_shared_secret)?; + + // Both symmetric keys should be the same + assert_eq!(alice_session_key.symmetric_key(), bob_session_key.symmetric_key()); + + Ok(()) + } + + #[test] + fn test_from_osrng() -> Result<()> { + // Bob's public key + let bob_static_secret = StaticSecret::random_from_rng(OsRng); + let bob_public_key = PublicKey::from(&bob_static_secret); + + // Alice creates session key using from_osrng + let alice_session_key = SessionKey::from_osrng(&bob_public_key.to_bytes())?; + + // Bob computes shared secret and session key + let bob_shared_secret = bob_static_secret.diffie_hellman(&PublicKey::from(alice_session_key.public_key)); + let mut bob_session_key = SessionKey::new(bob_public_key); + bob_session_key.derive_symmetric_key(&bob_shared_secret)?; + + // Both symmetric keys should be the same + assert_eq!(alice_session_key.symmetric_key(), bob_session_key.symmetric_key()); + + Ok(()) + } + + #[test] + fn test_debug_trait() { + let static_secret = StaticSecret::random_from_rng(OsRng); + let public_key = PublicKey::from(&static_secret); + let session_key = SessionKey::new(public_key); + + let debug_str = format!("{:?}", session_key); + assert!(debug_str.contains("SessionKey")); + assert!(debug_str.contains("sym_key: \"*******\"")); + } +} diff --git a/mm2src/kdf_walletconnect/src/session/mod.rs b/mm2src/kdf_walletconnect/src/session/mod.rs new file mode 100644 index 0000000000..4cbedfacd6 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/session/mod.rs @@ -0,0 +1,335 @@ +pub(crate) mod key; +pub mod rpc; + +use crate::chain::WcChainId; +use crate::storage::SessionStorageDb; +use crate::{error::WalletConnectError, WalletConnectCtxImpl}; + +use chrono::Utc; +use common::log::info; +use derive_more::Display; +use key::SessionKey; +use mm2_err_handle::prelude::{MmError, MmResult}; +use relay_rpc::domain::Topic; +use relay_rpc::rpc::params::session::Namespace; +use relay_rpc::rpc::params::session_propose::Proposer; +use relay_rpc::rpc::params::IrnMetadata; +use relay_rpc::{domain::SubscriptionId, + rpc::params::{session::ProposeNamespaces, session_settle::Controller, Metadata, Relay}}; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_json::Value; +use std::collections::{BTreeMap, HashMap}; +use std::fmt::Debug; +use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; +use wc_common::SymKey; + +pub(crate) const FIVE_MINUTES: u64 = 5 * 60; +pub(crate) const THIRTY_DAYS: u64 = 30 * 24 * 60 * 60; + +pub(crate) type WcRequestResponseResult = MmResult<(Value, IrnMetadata), WalletConnectError>; + +/// In the WalletConnect protocol, a session involves two parties: a controller +/// (typically a wallet) and a proposer (typically a dApp). This enum is used +/// to distinguish between these two roles. +#[derive(Debug, Display, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum SessionType { + /// Represents the controlling party in a session, typically a wallet. + Controller, + /// Represents the proposing party in a session, typically a dApp. + Proposer, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub struct SessionRpcInfo { + pub topic: Topic, + pub metadata: Metadata, + pub peer_pubkey: String, + pub pairing_topic: Topic, + pub namespaces: BTreeMap, + pub subscription_id: SubscriptionId, + pub properties: Option, + pub expiry: u64, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct KeyInfo { + pub chain_id: String, + pub name: String, + pub algo: String, + pub pub_key: String, + pub address: String, + pub bech32_address: String, + pub ethereum_hex_address: String, + pub is_nano_ledger: bool, + pub is_keystone: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct SessionProperties { + #[serde(default, deserialize_with = "deserialize_keys_from_string")] + pub keys: Option>, +} + +fn deserialize_keys_from_string<'de, D>(deserializer: D) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum KeysField { + String(String), + Vec(Vec), + None, + } + + match KeysField::deserialize(deserializer)? { + KeysField::String(key_string) => serde_json::from_str(&key_string) + .map(Some) + .map_err(serde::de::Error::custom), + KeysField::Vec(keys) => Ok(Some(keys)), + KeysField::None => Ok(None), + } +} + +/// This struct is typically used in the core session management logic of a WalletConnect +/// implementation. It's used to store, retrieve, and update session information throughout +/// the lifecycle of a WalletConnect connection. +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub struct Session { + /// Session topic + pub topic: Topic, + /// Pairing subscription id. + pub subscription_id: SubscriptionId, + /// Session symmetric key + pub session_key: SessionKey, + /// Information about the controlling party (typically a wallet). + pub controller: Controller, + /// Information about the proposing party (typically a dApp). + pub proposer: Proposer, + /// Details about the relay used for communication. + pub relay: Relay, + /// Agreed-upon namespaces for the session, mapping namespace strings to their definitions. + pub namespaces: BTreeMap, + /// Namespaces proposed for the session, may differ from agreed namespaces. + pub propose_namespaces: ProposeNamespaces, + /// Unix timestamp (in seconds) when the session expires. + pub expiry: u64, + /// Topic used for the initial pairing process. + pub pairing_topic: Topic, + /// Indicates whether this session info represents a Controller or Proposer perspective. + pub session_type: SessionType, + pub session_properties: Option, + /// Session active chain_id + pub active_chain_id: Option, +} + +impl Session { + pub fn new( + ctx: &WalletConnectCtxImpl, + session_topic: Topic, + subscription_id: SubscriptionId, + session_key: SessionKey, + pairing_topic: Topic, + metadata: Metadata, + session_type: SessionType, + ) -> Self { + let (proposer, controller) = match session_type { + SessionType::Proposer => ( + Proposer { + public_key: hex::encode(session_key.diffie_public_key()), + metadata, + }, + Controller::default(), + ), + SessionType::Controller => (Proposer::default(), Controller { + public_key: hex::encode(session_key.diffie_public_key()), + metadata, + }), + }; + + Self { + subscription_id, + session_key, + controller, + namespaces: BTreeMap::new(), + proposer, + propose_namespaces: ProposeNamespaces::default(), + relay: ctx.relay.clone(), + expiry: Utc::now().timestamp() as u64 + FIVE_MINUTES, + pairing_topic, + session_type, + topic: session_topic, + session_properties: None, + active_chain_id: Default::default(), + } + } + + pub(crate) fn extend(&mut self, till: u64) { self.expiry = till; } + + /// Get the active chain ID for the current session. + pub fn get_active_chain_id(&self) -> &Option { &self.active_chain_id } + + /// Sets the active chain ID for the current session. + pub fn set_active_chain_id(&mut self, chain_id: WcChainId) { self.active_chain_id = Some(chain_id); } +} + +/// Internal implementation of session management. +struct SessionManagerImpl { + /// A thread-safe map of sessions indexed by topic. + sessions: Arc>>, + pub(crate) storage: SessionStorageDb, +} + +pub struct SessionManager(Arc); + +impl From for SessionRpcInfo { + fn from(value: Session) -> Self { + Self { + topic: value.topic, + metadata: value.controller.metadata, + peer_pubkey: value.controller.public_key, + pairing_topic: value.pairing_topic, + namespaces: value.namespaces, + subscription_id: value.subscription_id, + properties: value.session_properties, + expiry: value.expiry, + } + } +} + +#[allow(unused)] +impl SessionManager { + pub(crate) fn new(storage: SessionStorageDb) -> Self { + Self( + SessionManagerImpl { + sessions: Default::default(), + storage, + } + .into(), + ) + } + + pub(crate) fn read(&self) -> RwLockReadGuard> { + self.0.sessions.read().expect("read shouldn't fail") + } + + pub(crate) fn write(&self) -> RwLockWriteGuard> { + self.0.sessions.write().expect("read shouldn't fail") + } + + pub(crate) fn storage(&self) -> &SessionStorageDb { &self.0.storage } + + /// Inserts `Session` into the session store, associated with the specified topic. + /// If a session with the same topic already exists, it will be overwritten. + pub(crate) fn add_session(&self, session: Session) { + // insert session + self.write().insert(session.topic.clone(), session); + } + + /// Removes session corresponding to the specified topic from the session store. + /// If the session does not exist, this method does nothing. + pub(crate) fn delete_session(&self, topic: &Topic) -> Option { + info!("[{topic}] Deleting session with topic"); + // Remove the session and return the removed session (if any) + self.write().remove(topic) + } + + /// Retrieves a cloned session associated with a given topic. + pub fn get_session(&self, topic: &Topic) -> Option { self.read().get(topic).cloned() } + + /// Retrieves all sessions(active and inactive) + pub fn get_sessions(&self) -> impl Iterator { + self.read().clone().into_values().map(|session| session.into()) + } + + pub(crate) fn get_sessions_full(&self) -> impl Iterator { self.read().clone().into_values() } + + /// Updates the expiry time of the session associated with the given topic to the specified timestamp. + /// If the session does not exist, this method does nothing. + pub(crate) fn extend_session(&self, topic: &Topic, till: u64) { + info!("[{topic}] Extending session with topic"); + if let Some(mut session) = self.write().get_mut(topic) { + session.extend(till); + } + } + + /// Retrieves the symmetric key associated with a given topic. + pub(crate) fn sym_key(&self, topic: &Topic) -> Option { + self.get_session(topic).map(|sess| sess.session_key.symmetric_key()) + } + + /// Check if a session exists. + pub(crate) fn validate_session_exists(&self, topic: &Topic) -> Result<(), MmError> { + if self.read().contains_key(topic) { + return Ok(()); + }; + + MmError::err(WalletConnectError::SessionError( + "No active WalletConnect session found".to_string(), + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn create_sample_key_info() -> KeyInfo { + KeyInfo { + chain_id: "test-chain".to_string(), + name: "Test Key".to_string(), + algo: "secp256k1".to_string(), + pub_key: "0123456789ABCDEF".to_string(), + address: "test_address".to_string(), + bech32_address: "bech32_test_address".to_string(), + ethereum_hex_address: "0xtest_eth_address".to_string(), + is_nano_ledger: false, + is_keystone: false, + } + } + + #[test] + fn test_deserialize_keys_from_string() { + let key_info = create_sample_key_info(); + let key_json = serde_json::to_string(&vec![key_info.clone()]).unwrap(); + let json = format!(r#"{{"keys": "{}"}}"#, key_json.replace('\"', "\\\"")); + let session: SessionProperties = serde_json::from_str(&json).unwrap(); + assert!(session.keys.is_some()); + assert_eq!(session.keys.unwrap(), vec![key_info]); + } + + #[test] + fn test_deserialize_keys_from_vec() { + let key_info = create_sample_key_info(); + let json = format!(r#"{{"keys": [{}]}}"#, serde_json::to_string(&key_info).unwrap()); + let session: SessionProperties = serde_json::from_str(&json).unwrap(); + assert!(session.keys.is_some()); + assert_eq!(session.keys.unwrap(), vec![key_info]); + } + + #[test] + fn test_deserialize_empty_keys() { + let json = r#"{"keys": []}"#; + let session: SessionProperties = serde_json::from_str(json).unwrap(); + assert_eq!(session.keys, Some(vec![])); + } + + #[test] + fn test_deserialize_no_keys() { + let json = r#"{}"#; + let session: SessionProperties = serde_json::from_str(json).unwrap(); + assert_eq!(session.keys, None); + } + + #[test] + fn test_serialize_deserialize_roundtrip() { + let key_info = create_sample_key_info(); + let original = SessionProperties { + keys: Some(vec![key_info]), + }; + let serialized = serde_json::to_string(&original).unwrap(); + let deserialized: SessionProperties = serde_json::from_str(&serialized).unwrap(); + assert_eq!(original, deserialized); + } +} diff --git a/mm2src/kdf_walletconnect/src/session/rpc/delete.rs b/mm2src/kdf_walletconnect/src/session/rpc/delete.rs new file mode 100644 index 0000000000..aa004cc437 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/session/rpc/delete.rs @@ -0,0 +1,63 @@ +use crate::{error::{WalletConnectError, USER_REQUESTED}, + storage::WalletConnectStorageOps, + WalletConnectCtxImpl}; + +use common::{custom_futures::timeout::FutureTimerExt, log::debug}; +use mm2_err_handle::map_to_mm::MapToMmResult; +use mm2_err_handle::prelude::{MapMmError, MmResult}; +use relay_rpc::domain::{MessageId, Topic}; +use relay_rpc::rpc::params::{session_delete::SessionDeleteRequest, RequestParams, ResponseParamsSuccess}; + +pub(crate) async fn reply_session_delete_request( + ctx: &WalletConnectCtxImpl, + topic: &Topic, + message_id: &MessageId, + _delete_params: SessionDeleteRequest, +) -> MmResult<(), WalletConnectError> { + let param = ResponseParamsSuccess::SessionDelete(true); + ctx.publish_response_ok(topic, param, message_id).await?; + + session_delete_cleanup(ctx, topic).await +} + +pub(crate) async fn send_session_delete_request( + ctx: &WalletConnectCtxImpl, + session_topic: &Topic, +) -> MmResult<(), WalletConnectError> { + let delete_request = SessionDeleteRequest { + code: USER_REQUESTED, + message: "User Disconnected".to_owned(), + }; + let param = RequestParams::SessionDelete(delete_request); + let (rx, ttl) = ctx.publish_request(session_topic, param).await?; + + rx.timeout(ttl) + .await + .map_to_mm(|_| WalletConnectError::TimeoutError)? + .map_to_mm(|err| WalletConnectError::InternalError(err.to_string()))??; + + session_delete_cleanup(ctx, session_topic).await +} + +async fn session_delete_cleanup(ctx: &WalletConnectCtxImpl, topic: &Topic) -> MmResult<(), WalletConnectError> { + ctx.client.unsubscribe(topic.clone()).await?; + + if let Some(session) = ctx.session_manager.delete_session(topic) { + debug!( + "[{}] No active sessions for pairing disconnecting", + session.pairing_topic + ); + //Attempt to unsubscribe from topic + ctx.client.unsubscribe(session.pairing_topic.clone()).await?; + // Attempt to delete/disconnect the pairing + ctx.pairing.delete(&session.pairing_topic); + // delete session from storage as well. + ctx.session_manager + .storage() + .delete_session(topic) + .await + .mm_err(|err| WalletConnectError::StorageError(err.to_string()))?; + }; + + Ok(()) +} diff --git a/mm2src/kdf_walletconnect/src/session/rpc/event.rs b/mm2src/kdf_walletconnect/src/session/rpc/event.rs new file mode 100644 index 0000000000..62159e6b91 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/session/rpc/event.rs @@ -0,0 +1,73 @@ +use crate::{chain::{WcChain, WcChainId}, + error::{WalletConnectError, UNSUPPORTED_CHAINS}, + WalletConnectCtxImpl}; + +use common::log::{error, info}; +use mm2_err_handle::prelude::*; +use relay_rpc::{domain::{MessageId, Topic}, + rpc::{params::{session_event::SessionEventRequest, ResponseParamsError}, + ErrorData}}; + +pub async fn handle_session_event( + ctx: &WalletConnectCtxImpl, + topic: &Topic, + message_id: &MessageId, + event: SessionEventRequest, +) -> MmResult<(), WalletConnectError> { + let chain_id = WcChainId::try_from_str(&event.chain_id)?; + let event_name = event.event.name.as_str(); + + match event_name { + "chainChanged" => { + let session = + ctx.session_manager + .get_session(topic) + .ok_or(MmError::new(WalletConnectError::SessionError( + "No active WalletConnect session found".to_string(), + )))?; + + if WcChain::Eip155 != chain_id.chain { + return Ok(()); + }; + + ctx.validate_chain_id(&session, &chain_id)?; + + if session.get_active_chain_id().as_ref().map_or(false, |c| c == &chain_id) { + return Ok(()); + }; + + // check if if new chain_id is supported. + let new_id = serde_json::from_value::(event.event.data)?; + let new_chain = chain_id.chain.derive_chain_id(new_id.to_string()); + if let Err(err) = ctx.validate_chain_id(&session, &new_chain) { + error!("[{topic}] {err:?}"); + let error_data = ErrorData { + code: UNSUPPORTED_CHAINS, + message: "Unsupported chain id".to_string(), + data: None, + }; + let params = ResponseParamsError::SessionEvent(error_data); + ctx.publish_response_err(topic, params, message_id).await?; + } else { + { + ctx.session_manager + .write() + .get_mut(topic) + .ok_or(MmError::new(WalletConnectError::SessionError( + "No active WalletConnect session found".to_string(), + )))? + .set_active_chain_id(chain_id); + } + }; + }, + "accountsChanged" => { + // TODO: Handle accountsChanged event logic. + }, + _ => { + // TODO: Handle other event logic., + }, + }; + + info!("[{topic}] {event_name} event handled successfully"); + Ok(()) +} diff --git a/mm2src/kdf_walletconnect/src/session/rpc/extend.rs b/mm2src/kdf_walletconnect/src/session/rpc/extend.rs new file mode 100644 index 0000000000..0574277af1 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/session/rpc/extend.rs @@ -0,0 +1,20 @@ +use crate::{error::WalletConnectError, WalletConnectCtxImpl}; + +use mm2_err_handle::prelude::MmResult; +use relay_rpc::{domain::{MessageId, Topic}, + rpc::params::{session_extend::SessionExtendRequest, ResponseParamsSuccess}}; + +/// Process session extend request. +pub(crate) async fn reply_session_extend_request( + ctx: &WalletConnectCtxImpl, + topic: &Topic, + message_id: &MessageId, + extend: SessionExtendRequest, +) -> MmResult<(), WalletConnectError> { + ctx.session_manager.extend_session(topic, extend.expiry); + + let param = ResponseParamsSuccess::SessionExtend(true); + ctx.publish_response_ok(topic, param, message_id).await?; + + Ok(()) +} diff --git a/mm2src/kdf_walletconnect/src/session/rpc/mod.rs b/mm2src/kdf_walletconnect/src/session/rpc/mod.rs new file mode 100644 index 0000000000..b94443c191 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/session/rpc/mod.rs @@ -0,0 +1,9 @@ +pub mod delete; +pub(crate) mod event; +pub(crate) mod extend; +pub mod ping; +pub(crate) mod propose; +pub(crate) mod settle; +pub(crate) mod update; + +pub use ping::*; diff --git a/mm2src/kdf_walletconnect/src/session/rpc/ping.rs b/mm2src/kdf_walletconnect/src/session/rpc/ping.rs new file mode 100644 index 0000000000..c1b9b6bf53 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/session/rpc/ping.rs @@ -0,0 +1,28 @@ +use crate::{error::WalletConnectError, WalletConnectCtxImpl}; + +use common::custom_futures::timeout::FutureTimerExt; +use mm2_err_handle::prelude::*; +use relay_rpc::{domain::{MessageId, Topic}, + rpc::params::{RequestParams, ResponseParamsSuccess}}; + +pub(crate) async fn reply_session_ping_request( + ctx: &WalletConnectCtxImpl, + topic: &Topic, + message_id: &MessageId, +) -> MmResult<(), WalletConnectError> { + let param = ResponseParamsSuccess::SessionPing(true); + ctx.publish_response_ok(topic, param, message_id).await?; + + Ok(()) +} + +pub async fn send_session_ping_request(ctx: &WalletConnectCtxImpl, topic: &Topic) -> MmResult<(), WalletConnectError> { + let param = RequestParams::SessionPing(()); + let (rx, ttl) = ctx.publish_request(topic, param).await?; + rx.timeout(ttl) + .await + .map_to_mm(|_| WalletConnectError::TimeoutError)? + .map_to_mm(|err| WalletConnectError::InternalError(err.to_string()))??; + + Ok(()) +} diff --git a/mm2src/kdf_walletconnect/src/session/rpc/propose.rs b/mm2src/kdf_walletconnect/src/session/rpc/propose.rs new file mode 100644 index 0000000000..5180fe7ff9 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/session/rpc/propose.rs @@ -0,0 +1,152 @@ +use super::settle::send_session_settle_request; +use crate::storage::WalletConnectStorageOps; +use crate::{error::WalletConnectError, + metadata::generate_metadata, + session::{Session, SessionKey, SessionType, THIRTY_DAYS}, + WalletConnectCtxImpl}; + +use chrono::Utc; +use mm2_err_handle::map_to_mm::MapToMmResult; +use mm2_err_handle::prelude::*; +use relay_rpc::rpc::params::session::ProposeNamespaces; +use relay_rpc::{domain::{MessageId, Topic}, + rpc::params::{session_propose::{Proposer, SessionProposeRequest, SessionProposeResponse}, + RequestParams, ResponseParamsSuccess}}; + +/// Creates a new session proposal from topic and metadata. +pub(crate) async fn send_proposal_request( + ctx: &WalletConnectCtxImpl, + topic: &Topic, + required_namespaces: ProposeNamespaces, + optional_namespaces: Option, +) -> MmResult<(), WalletConnectError> { + let proposer = Proposer { + metadata: ctx.metadata.clone(), + public_key: hex::encode(ctx.key_pair.public_key.as_bytes()), + }; + let session_proposal = RequestParams::SessionPropose(SessionProposeRequest { + relays: vec![ctx.relay.clone()], + proposer, + required_namespaces, + optional_namespaces, + }); + let _ = ctx.publish_request(topic, session_proposal).await?; + + Ok(()) +} + +/// Process session proposal request +/// https://specs.walletconnect.com/2.0/specs/clients/sign/session-proposal +pub async fn reply_session_proposal_request( + ctx: &WalletConnectCtxImpl, + proposal: SessionProposeRequest, + topic: &Topic, + message_id: &MessageId, +) -> MmResult<(), WalletConnectError> { + let session = { + let sender_public_key = hex::decode(&proposal.proposer.public_key)? + .as_slice() + .try_into() + .map_to_mm(|_| WalletConnectError::InternalError("Invalid sender_public_key".to_owned()))?; + let session_key = SessionKey::from_osrng(&sender_public_key)?; + let session_topic: Topic = session_key.generate_topic().into(); + let subscription_id = ctx + .client + .subscribe(session_topic.clone()) + .await + .map_to_mm(|err| WalletConnectError::SubscriptionError(err.to_string()))?; + + Session::new( + ctx, + session_topic.clone(), + subscription_id, + session_key, + topic.clone(), + proposal.proposer.metadata, + SessionType::Controller, + ) + }; + session + .propose_namespaces + .supported(&proposal.required_namespaces) + .map_to_mm(|err| WalletConnectError::InternalError(err.to_string()))?; + + { + // save session to storage + ctx.session_manager + .storage() + .save_session(&session) + .await + .mm_err(|err| WalletConnectError::StorageError(err.to_string()))?; + + // Add session to session lists + ctx.session_manager.add_session(session.clone()); + } + + send_session_settle_request(ctx, &session).await?; + + // Respond to incoming session propose. + let param = ResponseParamsSuccess::SessionPropose(SessionProposeResponse { + relay: ctx.relay.clone(), + responder_public_key: proposal.proposer.public_key, + }); + + ctx.publish_response_ok(topic, param, message_id).await?; + + Ok(()) +} + +/// Process session propose reponse. +pub(crate) async fn process_session_propose_response( + ctx: &WalletConnectCtxImpl, + pairing_topic: &Topic, + response: &SessionProposeResponse, +) -> MmResult<(), WalletConnectError> { + let session_key = { + let other_public_key = hex::decode(&response.responder_public_key)? + .as_slice() + .try_into() + .unwrap(); + let mut session_key = SessionKey::new(ctx.key_pair.public_key); + session_key.generate_symmetric_key(&ctx.key_pair.secret, &other_public_key)?; + session_key + }; + + let session = { + let session_topic: Topic = session_key.generate_topic().into(); + let subscription_id = ctx + .client + .subscribe(session_topic.clone()) + .await + .map_to_mm(|err| WalletConnectError::SubscriptionError(err.to_string()))?; + + let mut session = Session::new( + ctx, + session_topic.clone(), + subscription_id, + session_key, + pairing_topic.clone(), + generate_metadata(), + SessionType::Proposer, + ); + session.relay = response.relay.clone(); + session.expiry = Utc::now().timestamp() as u64 + THIRTY_DAYS; + session.controller.public_key = response.responder_public_key.clone(); + session + }; + + // save session to storage + ctx.session_manager + .storage() + .save_session(&session) + .await + .mm_err(|err| WalletConnectError::StorageError(err.to_string()))?; + + // Add session to session lists + ctx.session_manager.add_session(session.clone()); + + // Activate pairing_topic + ctx.pairing.activate(pairing_topic)?; + + Ok(()) +} diff --git a/mm2src/kdf_walletconnect/src/session/rpc/settle.rs b/mm2src/kdf_walletconnect/src/session/rpc/settle.rs new file mode 100644 index 0000000000..901e7885ed --- /dev/null +++ b/mm2src/kdf_walletconnect/src/session/rpc/settle.rs @@ -0,0 +1,81 @@ +use crate::session::{Session, SessionProperties}; +use crate::storage::WalletConnectStorageOps; +use crate::{error::WalletConnectError, WalletConnectCtxImpl}; + +use common::log::{debug, info}; +use mm2_err_handle::prelude::{MapMmError, MmError, MmResult}; +use relay_rpc::domain::Topic; +use relay_rpc::rpc::params::session_settle::SessionSettleRequest; + +/// TODO: Finish when implementing KDF as a Wallet. +pub(crate) async fn send_session_settle_request( + _ctx: &WalletConnectCtxImpl, + _session_info: &Session, +) -> MmResult<(), WalletConnectError> { + // let mut settled_namespaces = BTreeMap::::new(); + // let nam + // settled_namespaces.insert("eip155".to_string(), Namespace { + // chains: Some(SUPPORTED_CHAINS.iter().map(|c| c.to_string()).collect()), + // methods: SUPPORTED_METHODS.iter().map(|m| m.to_string()).collect(), + // events: SUPPORTED_EVENTS.iter().map(|e| e.to_string()).collect(), + // accounts: None, + // }); + // + // let request = RequestParams::SessionSettle(SessionSettleRequest { + // relay: session_info.relay.clone(), + // controller: session_info.controller.clone(), + // namespaces: SettleNamespaces(settled_namespaces), + // expiry: Utc::now().timestamp() as u64 + THIRTY_DAYS, + // session_properties: None, + // }); + // + // ctx.publish_request(&session_info.topic, request).await?; + + Ok(()) +} + +/// Process session settle request. +pub(crate) async fn reply_session_settle_request( + ctx: &WalletConnectCtxImpl, + topic: &Topic, + settle: SessionSettleRequest, +) -> MmResult<(), WalletConnectError> { + let (session, session_controller_exists) = { + let mut sessions = ctx.session_manager.write(); + let Some(session) = sessions.get_mut(topic) else { + return MmError::err(WalletConnectError::SessionError(format!("No session found for topic: {topic}"))); + }; + let session_controller_exists = session.controller == settle.controller; + if let Some(value) = settle.session_properties { + let session_properties = serde_json::from_value::(value)?; + session.session_properties = Some(session_properties); + }; + session.namespaces = settle.namespaces.0; + session.controller = settle.controller; + session.relay = settle.relay; + session.expiry = settle.expiry; + + (session.clone(), session_controller_exists) + }; + + // Update storage session. + ctx.session_manager + .storage() + .update_session(&session) + .await + .mm_err(|err| WalletConnectError::StorageError(err.to_string()))?; + + info!("[{topic}] Session successfully settled for topic"); + + // Delete other sessions with same controller + // NOTE: we might not want to do this! + let all_sessions = ctx.session_manager.get_sessions_full(); + for session in all_sessions { + if session_controller_exists && session.topic.as_ref() != topic.as_ref() { + ctx.drop_session(&session.topic).await?; + debug!("[{}] session deleted", session.topic); + } + } + + Ok(()) +} diff --git a/mm2src/kdf_walletconnect/src/session/rpc/update.rs b/mm2src/kdf_walletconnect/src/session/rpc/update.rs new file mode 100644 index 0000000000..dee6d9e39f --- /dev/null +++ b/mm2src/kdf_walletconnect/src/session/rpc/update.rs @@ -0,0 +1,46 @@ +use crate::storage::WalletConnectStorageOps; +use crate::{error::WalletConnectError, WalletConnectCtxImpl}; + +use common::log::info; +use mm2_err_handle::prelude::*; +use relay_rpc::domain::{MessageId, Topic}; +use relay_rpc::rpc::params::{session_update::SessionUpdateRequest, ResponseParamsSuccess}; + +pub(crate) async fn reply_session_update_request( + ctx: &WalletConnectCtxImpl, + topic: &Topic, + message_id: &MessageId, + update: SessionUpdateRequest, +) -> MmResult<(), WalletConnectError> { + { + let mut session = ctx.session_manager.write(); + let Some(session) = session.get_mut(topic) else { + return MmError::err(WalletConnectError::SessionError(format!("No session found for topic: {topic}"))); + }; + update + .namespaces + .caip2_validate() + .map_to_mm(|err| WalletConnectError::InternalError(err.to_string()))?; + session.namespaces = update.namespaces.0; + let session = session; + info!("Updated extended, info: {:?}", session.topic); + } + + // Update storage session. + let session = ctx + .session_manager + .get_session(topic) + .ok_or(MmError::new(WalletConnectError::SessionError(format!( + "session not foun topic: {topic}" + ))))?; + ctx.session_manager + .storage() + .update_session(&session) + .await + .mm_err(|err| WalletConnectError::StorageError(err.to_string()))?; + + let param = ResponseParamsSuccess::SessionUpdate(true); + ctx.publish_response_ok(topic, param, message_id).await?; + + Ok(()) +} diff --git a/mm2src/kdf_walletconnect/src/storage/indexed_db.rs b/mm2src/kdf_walletconnect/src/storage/indexed_db.rs new file mode 100644 index 0000000000..ac9205600f --- /dev/null +++ b/mm2src/kdf_walletconnect/src/storage/indexed_db.rs @@ -0,0 +1,129 @@ +use super::WalletConnectStorageOps; +use crate::error::WcIndexedDbError; +use crate::session::Session; +use async_trait::async_trait; +use common::log::debug; +use mm2_core::mm_ctx::MmArc; +use mm2_db::indexed_db::{ConstructibleDb, DbIdentifier, DbInstance, DbLocked, DbUpgrader, IndexedDb, IndexedDbBuilder, + InitDbResult, OnUpgradeResult, SharedDb, TableSignature}; +use mm2_err_handle::prelude::MmResult; +use mm2_err_handle::prelude::*; +use relay_rpc::domain::Topic; + +const DB_VERSION: u32 = 1; + +pub type IDBSessionStorageLocked<'a> = DbLocked<'a, IDBSessionStorageInner>; + +impl TableSignature for Session { + const TABLE_NAME: &'static str = "sessions"; + + fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { + if let (0, 1) = (old_version, new_version) { + let table = upgrader.create_table(Self::TABLE_NAME)?; + table.create_index("topic", false)?; + } + Ok(()) + } +} + +pub struct IDBSessionStorageInner(IndexedDb); + +#[async_trait] +impl DbInstance for IDBSessionStorageInner { + const DB_NAME: &'static str = "wc_session_storage"; + + async fn init(db_id: DbIdentifier) -> InitDbResult { + let inner = IndexedDbBuilder::new(db_id) + .with_version(DB_VERSION) + .with_table::() + .build() + .await?; + + Ok(Self(inner)) + } +} + +impl IDBSessionStorageInner { + pub(crate) fn get_inner(&self) -> &IndexedDb { &self.0 } +} + +#[derive(Clone)] +pub struct IDBSessionStorage(SharedDb); + +impl IDBSessionStorage { + pub(crate) fn new(ctx: &MmArc) -> MmResult { + Ok(Self(ConstructibleDb::new(ctx).into_shared())) + } + + async fn lock_db(&self) -> MmResult, WcIndexedDbError> { + self.0 + .get_or_initialize() + .await + .mm_err(|err| WcIndexedDbError::InternalError(err.to_string())) + } +} + +#[async_trait::async_trait] +impl WalletConnectStorageOps for IDBSessionStorage { + type Error = WcIndexedDbError; + + async fn init(&self) -> MmResult<(), Self::Error> { + debug!("Initializing WalletConnect session storage"); + Ok(()) + } + + async fn is_initialized(&self) -> MmResult { Ok(true) } + + async fn save_session(&self, session: &Session) -> MmResult<(), Self::Error> { + debug!("[{}] Saving WalletConnect session to storage", session.topic); + let lock_db = self.lock_db().await?; + let transaction = lock_db.get_inner().transaction().await?; + let session_table = transaction.table::().await?; + session_table + .replace_item_by_unique_index("topic", session.topic.clone(), session) + .await?; + + Ok(()) + } + + async fn get_session(&self, topic: &Topic) -> MmResult, Self::Error> { + debug!("[{topic}] Retrieving WalletConnect session from storage"); + let lock_db = self.lock_db().await?; + let transaction = lock_db.get_inner().transaction().await?; + let session_table = transaction.table::().await?; + + Ok(session_table + .get_item_by_unique_index("topic", topic) + .await? + .map(|s| s.1)) + } + + async fn get_all_sessions(&self) -> MmResult, Self::Error> { + debug!("Loading WalletConnect sessions from storage"); + let lock_db = self.lock_db().await?; + let transaction = lock_db.get_inner().transaction().await?; + let session_table = transaction.table::().await?; + + Ok(session_table + .get_all_items() + .await? + .into_iter() + .map(|s| s.1) + .collect::>()) + } + + async fn delete_session(&self, topic: &Topic) -> MmResult<(), Self::Error> { + debug!("[{topic}] Deleting WalletConnect session from storage"); + let lock_db = self.lock_db().await?; + let transaction = lock_db.get_inner().transaction().await?; + let session_table = transaction.table::().await?; + + session_table.delete_item_by_unique_index("topic", topic).await?; + Ok(()) + } + + async fn update_session(&self, session: &Session) -> MmResult<(), Self::Error> { + debug!("[{}] Updating WalletConnect session in storage", session.topic); + self.save_session(session).await + } +} diff --git a/mm2src/kdf_walletconnect/src/storage/mod.rs b/mm2src/kdf_walletconnect/src/storage/mod.rs new file mode 100644 index 0000000000..088e052e2f --- /dev/null +++ b/mm2src/kdf_walletconnect/src/storage/mod.rs @@ -0,0 +1,202 @@ +use std::ops::Deref; + +use async_trait::async_trait; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::MmResult; +use mm2_err_handle::prelude::*; +use relay_rpc::domain::Topic; + +use crate::{error::WalletConnectError, session::Session}; + +#[cfg(target_arch = "wasm32")] pub(crate) mod indexed_db; +#[cfg(not(target_arch = "wasm32"))] pub(crate) mod sqlite; + +#[async_trait] +pub(crate) trait WalletConnectStorageOps { + type Error: std::fmt::Debug + NotMmError + NotEqual + Send; + + async fn init(&self) -> MmResult<(), Self::Error>; + async fn is_initialized(&self) -> MmResult; + async fn save_session(&self, session: &Session) -> MmResult<(), Self::Error>; + async fn get_session(&self, topic: &Topic) -> MmResult, Self::Error>; + async fn get_all_sessions(&self) -> MmResult, Self::Error>; + async fn delete_session(&self, topic: &Topic) -> MmResult<(), Self::Error>; + async fn update_session(&self, session: &Session) -> MmResult<(), Self::Error>; +} + +#[cfg(target_arch = "wasm32")] +type DB = indexed_db::IDBSessionStorage; +#[cfg(not(target_arch = "wasm32"))] +type DB = sqlite::SqliteSessionStorage; + +pub(crate) struct SessionStorageDb(DB); + +impl Deref for SessionStorageDb { + type Target = DB; + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl SessionStorageDb { + pub(crate) fn new(ctx: &MmArc) -> MmResult { + let db = DB::new(ctx).mm_err(|err| WalletConnectError::StorageError(err.to_string()))?; + + Ok(SessionStorageDb(db)) + } +} + +#[cfg(test)] +pub(crate) mod session_storage_tests { + common::cfg_wasm32! { + use wasm_bindgen_test::*; + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + } + use common::cross_test; + use mm2_test_helpers::for_tests::mm_ctx_with_custom_async_db; + use relay_rpc::{domain::SubscriptionId, rpc::params::Metadata}; + + use crate::{session::key::SessionKey, + session::{Session, SessionType}, + WalletConnectCtx}; + + use super::WalletConnectStorageOps; + + fn sample_test_session(wc_ctx: &WalletConnectCtx) -> Session { + let session_key = SessionKey { + sym_key: [ + 115, 159, 247, 31, 199, 84, 88, 59, 158, 252, 98, 225, 51, 125, 201, 239, 142, 34, 9, 201, 128, 114, + 144, 166, 102, 131, 87, 191, 33, 24, 153, 7, + ], + public_key: [ + 115, 159, 247, 31, 199, 84, 88, 59, 158, 252, 98, 225, 51, 125, 201, 239, 142, 34, 9, 201, 128, 114, + 144, 166, 102, 131, 87, 191, 33, 24, 153, 7, + ], + }; + + Session::new( + wc_ctx, + "bb89e3bae8cb89e5549f4d9bcc5a1ac2aae6dd90ef37eb2f59d80c5773f36343".into(), + SubscriptionId::generate(), + session_key, + "5af44bdf8d6b11f4635c964a15e9e2d50942534824791757b2c26528e8feef39".into(), + Metadata::default(), + SessionType::Controller, + ) + } + + cross_test!(save_and_get_session_test, { + let mm_ctx = mm_ctx_with_custom_async_db().await; + let wc_ctx = WalletConnectCtx::try_init(&mm_ctx).unwrap(); + wc_ctx.session_manager.storage().init().await.unwrap(); + + let sample_session = sample_test_session(&wc_ctx); + + // try save session + wc_ctx + .session_manager + .storage() + .save_session(&sample_session) + .await + .unwrap(); + + // try get session + let db_session = wc_ctx + .session_manager + .storage() + .get_session(&sample_session.topic) + .await + .unwrap(); + assert_eq!(sample_session, db_session.unwrap()); + }); + + cross_test!(delete_session_test, { + let mm_ctx = mm_ctx_with_custom_async_db().await; + let wc_ctx = WalletConnectCtx::try_init(&mm_ctx).unwrap(); + wc_ctx.session_manager.storage().init().await.unwrap(); + + let sample_session = sample_test_session(&wc_ctx); + + // try save session + wc_ctx + .session_manager + .storage() + .save_session(&sample_session) + .await + .unwrap(); + + // try get session + let db_session = wc_ctx + .session_manager + .storage() + .get_session(&sample_session.topic) + .await + .unwrap() + .unwrap(); + assert_eq!(sample_session, db_session); + + // try delete session + wc_ctx + .session_manager + .storage() + .delete_session(&db_session.topic) + .await + .unwrap(); + + // try get_session deleted again + let db_session = wc_ctx.session_manager.storage().get_session(&db_session.topic).await; + assert!(db_session.is_err()); + }); + + cross_test!(update_session_test, { + let mm_ctx = mm_ctx_with_custom_async_db().await; + let wc_ctx = WalletConnectCtx::try_init(&mm_ctx).unwrap(); + wc_ctx.session_manager.storage().init().await.unwrap(); + + let sample_session = sample_test_session(&wc_ctx); + + // try save session + wc_ctx + .session_manager + .storage() + .save_session(&sample_session) + .await + .unwrap(); + + // try get session + let db_session = wc_ctx + .session_manager + .storage() + .get_session(&sample_session.topic) + .await + .unwrap() + .unwrap(); + assert_eq!(sample_session, db_session); + + // modify sample_session + let mut modified_sample_session = sample_session.clone(); + modified_sample_session.expiry = 100; + + // assert that original session expiry isn't the same as our new expiry. + assert_ne!(sample_session.expiry, modified_sample_session.expiry); + + // try update session + wc_ctx + .session_manager + .storage() + .update_session(&modified_sample_session) + .await + .unwrap(); + + // try get_session again with new updated expiry + let db_session = wc_ctx + .session_manager + .storage() + .get_session(&sample_session.topic) + .await + .unwrap() + .unwrap(); + assert_ne!(sample_session.expiry, db_session.expiry); + + assert_eq!(modified_sample_session, db_session); + assert_eq!(100, db_session.expiry); + }); +} diff --git a/mm2src/kdf_walletconnect/src/storage/sqlite.rs b/mm2src/kdf_walletconnect/src/storage/sqlite.rs new file mode 100644 index 0000000000..40bfcf9431 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/storage/sqlite.rs @@ -0,0 +1,176 @@ +use async_trait::async_trait; +use common::log::debug; +use db_common::async_sql_conn::InternalError; +use db_common::sqlite::rusqlite::Result as SqlResult; +use db_common::sqlite::{query_single_row, string_from_row, CHECK_TABLE_EXISTS_SQL}; +use db_common::{async_sql_conn::{AsyncConnError, AsyncConnection}, + sqlite::validate_table_name}; +use futures::lock::{Mutex, MutexGuard}; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use relay_rpc::domain::Topic; +use std::sync::Arc; + +use super::WalletConnectStorageOps; +use crate::session::Session; + +const SESSION_TABLE_NAME: &str = "wc_session"; + +/// Sessions table +fn create_sessions_table() -> SqlResult { + validate_table_name(SESSION_TABLE_NAME)?; + Ok(format!( + "CREATE TABLE IF NOT EXISTS {SESSION_TABLE_NAME} ( + topic char(32) PRIMARY KEY, + data TEXT NOT NULL, + expiry BIGINT NOT NULL + );" + )) +} + +#[derive(Clone, Debug)] +pub(crate) struct SqliteSessionStorage { + pub conn: Arc>, +} + +impl SqliteSessionStorage { + pub(crate) fn new(ctx: &MmArc) -> MmResult { + let conn = ctx + .async_sqlite_connection + .get() + .ok_or(AsyncConnError::Internal(InternalError( + "async_sqlite_connection is not initialized".to_owned(), + )))?; + + Ok(Self { conn: conn.clone() }) + } + + pub(crate) async fn lock_db(&self) -> MutexGuard<'_, AsyncConnection> { self.conn.lock().await } +} + +#[async_trait] +impl WalletConnectStorageOps for SqliteSessionStorage { + type Error = AsyncConnError; + + async fn init(&self) -> MmResult<(), Self::Error> { + debug!("Initializing WalletConnect session storage"); + let lock = self.lock_db().await; + lock.call(move |conn| { + conn.execute(&create_sessions_table()?, []).map(|_| ())?; + Ok(()) + }) + .await + .map_to_mm(AsyncConnError::from) + } + + async fn is_initialized(&self) -> MmResult { + let lock = self.lock_db().await; + validate_table_name(SESSION_TABLE_NAME).map_err(AsyncConnError::from)?; + lock.call(move |conn| { + let initialized = query_single_row(conn, CHECK_TABLE_EXISTS_SQL, [SESSION_TABLE_NAME], string_from_row)?; + Ok(initialized.is_some()) + }) + .await + .map_to_mm(AsyncConnError::from) + } + + async fn save_session(&self, session: &Session) -> MmResult<(), Self::Error> { + debug!("[{}] Saving WalletConnect session to storage", session.topic); + let lock = self.lock_db().await; + + let session = session.clone(); + lock.call(move |conn| { + let sql = format!( + "INSERT INTO {} (topic, data, expiry) VALUES (?1, ?2, ?3);", + SESSION_TABLE_NAME + ); + let transaction = conn.transaction()?; + + let session_data = serde_json::to_string(&session).map_err(|err| AsyncConnError::from(err.to_string()))?; + + let params = [session.topic.to_string(), session_data, session.expiry.to_string()]; + + transaction.execute(&sql, params)?; + transaction.commit()?; + + Ok(()) + }) + .await + .map_to_mm(AsyncConnError::from) + } + + async fn get_session(&self, topic: &Topic) -> MmResult, Self::Error> { + debug!("[{topic}] Retrieving WalletConnect session from storage"); + let lock = self.lock_db().await; + let topic = topic.clone(); + let session_str = lock + .call(move |conn| { + let sql = format!("SELECT topic, data, expiry FROM {} WHERE topic=?1;", SESSION_TABLE_NAME); + let mut stmt = conn.prepare(&sql)?; + let session: String = stmt.query_row([topic.to_string()], |row| row.get::<_, String>(1))?; + Ok(session) + }) + .await + .map_to_mm(AsyncConnError::from)?; + + let session = serde_json::from_str(&session_str).map_to_mm(|err| AsyncConnError::from(err.to_string()))?; + Ok(session) + } + + async fn get_all_sessions(&self) -> MmResult, Self::Error> { + debug!("Loading WalletConnect sessions from storage"); + let lock = self.lock_db().await; + let sessions_str = lock + .call(move |conn| { + let sql = format!("SELECT topic, data, expiry FROM {};", SESSION_TABLE_NAME); + let mut stmt = conn.prepare(&sql)?; + let sessions = stmt.query_map([], |row| row.get::<_, String>(1))?.collect::>(); + Ok(sessions) + }) + .await + .map_to_mm(AsyncConnError::from)?; + + let mut sessions = Vec::with_capacity(sessions_str.len()); + for session in sessions_str { + let session = serde_json::from_str(&session.map_to_mm(AsyncConnError::from)?) + .map_to_mm(|err| AsyncConnError::from(err.to_string()))?; + sessions.push(session); + } + + Ok(sessions) + } + + async fn delete_session(&self, topic: &Topic) -> MmResult<(), Self::Error> { + debug!("[{topic}] Deleting WalletConnect session from storage"); + let topic = topic.clone(); + let lock = self.lock_db().await; + lock.call(move |conn| { + let sql = format!("DELETE FROM {} WHERE topic = ?1", SESSION_TABLE_NAME); + let mut stmt = conn.prepare(&sql)?; + let _ = stmt.execute([topic.to_string()])?; + + Ok(()) + }) + .await + .map_to_mm(AsyncConnError::from) + } + + async fn update_session(&self, session: &Session) -> MmResult<(), Self::Error> { + debug!("[{}] Updating WalletConnect session in storage", session.topic); + let session = session.clone(); + let lock = self.lock_db().await; + lock.call(move |conn| { + let sql = format!( + "UPDATE {} SET data = ?1, expiry = ?2 WHERE topic = ?3", + SESSION_TABLE_NAME + ); + let session_data = serde_json::to_string(&session).map_err(|err| AsyncConnError::from(err.to_string()))?; + let params = [session_data, session.expiry.to_string(), session.topic.to_string()]; + let _row = conn.prepare(&sql)?.execute(params)?; + + Ok(()) + }) + .await + .map_to_mm(AsyncConnError::from) + } +} diff --git a/mm2src/mm2_core/Cargo.toml b/mm2src/mm2_core/Cargo.toml index 365592bffe..d7286cb82e 100644 --- a/mm2src/mm2_core/Cargo.toml +++ b/mm2src/mm2_core/Cargo.toml @@ -33,7 +33,7 @@ uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } [target.'cfg(target_arch = "wasm32")'.dependencies] instant = { version = "0.1.12", features = ["wasm-bindgen"] } -mm2_rpc = { path = "../mm2_rpc", features = [ "rpc_facilities" ] } +mm2_rpc = { path = "../mm2_rpc", features = ["rpc_facilities"] } wasm-bindgen-test = { version = "0.3.2" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index 0a1afb2eea..1a3da1ec52 100644 --- a/mm2src/mm2_core/src/mm_ctx.rs +++ b/mm2src/mm2_core/src/mm_ctx.rs @@ -147,6 +147,7 @@ pub struct MmCtx { pub async_sqlite_connection: OnceLock>>, /// Links the RPC context to the P2P context to handle health check responses. pub healthcheck_response_handler: AsyncMutex>>, + pub wallet_connect: Mutex>>, } impl MmCtx { @@ -197,6 +198,7 @@ impl MmCtx { #[cfg(not(target_arch = "wasm32"))] async_sqlite_connection: OnceLock::default(), healthcheck_response_handler: AsyncMutex::new(ExpirableMap::default()), + wallet_connect: Mutex::new(None), } } @@ -301,7 +303,7 @@ impl MmCtx { self.db_root().join(wallet_name.to_string() + ".dat") } - /// MM database path. + /// MM database path. /// Defaults to a relative "DB". /// /// Can be changed via the "dbdir" configuration field, for example: @@ -597,7 +599,7 @@ impl MmArc { } } - /// Tries getting access to the MM context. + /// Tries getting access to the MM context. /// Fails if an invalid MM context handler is passed (no such context or dropped context). #[track_caller] pub fn from_ffi_handle(ffi_handle: u32) -> Result { diff --git a/mm2src/mm2_main/Cargo.toml b/mm2src/mm2_main/Cargo.toml index b80377a822..6e04e1a8f8 100644 --- a/mm2src/mm2_main/Cargo.toml +++ b/mm2src/mm2_main/Cargo.toml @@ -13,13 +13,16 @@ path = "src/mm2.rs" doctest = false [features] -custom-swap-locktime = [] # only for testing purposes, should never be activated on release builds. +custom-swap-locktime = [ +] # only for testing purposes, should never be activated on release builds. native = [] # Deprecated track-ctx-pointer = ["common/track-ctx-pointer"] zhtlc-native-tests = ["coins/zhtlc-native-tests"] run-docker-tests = ["coins/run-docker-tests"] default = [] -trezor-udp = ["crypto/trezor-udp"] # use for tests to connect to trezor emulator over udp +trezor-udp = [ + "crypto/trezor-udp", +] # use for tests to connect to trezor emulator over udp run-device-tests = [] enable-sia = ["coins/enable-sia", "coins_activation/enable-sia"] sepolia-maker-swap-v2-tests = [] @@ -44,11 +47,17 @@ crypto = { path = "../crypto" } db_common = { path = "../db_common" } derive_more = "0.99" either = "1.6" -ethereum-types = { version = "0.13", default-features = false, features = ["std", "serialize"] } +ethereum-types = { version = "0.13", default-features = false, features = [ + "std", + "serialize", +] } enum_derives = { path = "../derives/enum_derives" } enum-primitive-derive = "0.2" futures01 = { version = "0.1", package = "futures" } -futures = { version = "0.3.1", package = "futures", features = ["compat", "async-await"] } +futures = { version = "0.3.1", package = "futures", features = [ + "compat", + "async-await", +] } gstuff = { version = "0.7", features = ["nightly"] } hash256-std-hasher = "0.15.2" hash-db = "0.15.2" @@ -57,6 +66,7 @@ http = "0.2" hw_common = { path = "../hw_common" } instant = { version = "0.1.12" } itertools = "0.10" +kdf_walletconnect = { path = "../kdf_walletconnect" } keys = { path = "../mm2_bitcoin/keys" } lazy_static = "1.4" # ledger = { path = "../ledger" } @@ -70,7 +80,7 @@ mm2_libp2p = { path = "../mm2_p2p", package = "mm2_p2p" } mm2_metrics = { path = "../mm2_metrics" } mm2_net = { path = "../mm2_net" } mm2_number = { path = "../mm2_number" } -mm2_rpc = { path = "../mm2_rpc", features = ["rpc_facilities"]} +mm2_rpc = { path = "../mm2_rpc", features = ["rpc_facilities"] } mm2_state_machine = { path = "../mm2_state_machine" } trading_api = { path = "../trading_api" } num-traits = "0.2" @@ -94,7 +104,9 @@ ser_error_derive = { path = "../derives/ser_error_derive" } serialization = { path = "../mm2_bitcoin/serialization" } serialization_derive = { path = "../mm2_bitcoin/serialization_derive" } spv_validation = { path = "../mm2_bitcoin/spv_validation" } -sp-runtime-interface = { version = "6.0.0", default-features = false, features = ["disable_target_static_assertions"] } +sp-runtime-interface = { version = "6.0.0", default-features = false, features = [ + "disable_target_static_assertions", +] } sp-trie = { version = "6.0", default-features = false } trie-db = { version = "0.23.1", default-features = false } trie-root = "0.16.0" @@ -129,7 +141,9 @@ mm2_test_helpers = { path = "../mm2_test_helpers" } trading_api = { path = "../trading_api", features = ["mocktopus"] } mocktopus = "0.8.0" testcontainers = "0.15.0" -web3 = { git = "https://github.com/KomodoPlatform/rust-web3", tag = "v0.20.0", default-features = false, features = ["http-rustls-tls"] } +web3 = { git = "https://github.com/KomodoPlatform/rust-web3", tag = "v0.20.0", default-features = false, features = [ + "http-rustls-tls", +] } ethabi = { version = "17.0.0" } rlp = { version = "0.5" } ethcore-transaction = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git", rev = "mm2-v2.1.1" } diff --git a/mm2src/mm2_main/src/lp_native_dex.rs b/mm2src/mm2_main/src/lp_native_dex.rs index 1e5f7feff0..ec0f3a9f67 100644 --- a/mm2src/mm2_main/src/lp_native_dex.rs +++ b/mm2src/mm2_main/src/lp_native_dex.rs @@ -25,6 +25,7 @@ use common::log::{info, warn}; use crypto::{from_hw_error, CryptoCtx, HwError, HwProcessingError, HwRpcError, WithHwRpcError}; use derive_more::Display; use enum_derives::EnumFromTrait; +use kdf_walletconnect::WalletConnectCtx; use mm2_core::mm_ctx::{MmArc, MmCtx}; use mm2_err_handle::common_errors::InternalError; use mm2_err_handle::prelude::*; @@ -493,6 +494,10 @@ pub async fn lp_init_continue(ctx: MmArc) -> MmInitResult<()> { #[cfg(target_arch = "wasm32")] init_wasm_event_streaming(&ctx); + // This function spwans related WalletConnect related tasks and needed initialization before + // WalletConnect can be usable in KDF. + WalletConnectCtx::from_ctx(&ctx).mm_err(|err| MmInitError::WalletInitError(err.to_string()))?; + ctx.spawner().spawn(clean_memory_loop(ctx.weak())); Ok(()) diff --git a/mm2src/mm2_main/src/lp_swap/taker_swap.rs b/mm2src/mm2_main/src/lp_swap/taker_swap.rs index c7b1cf59a9..32b4e2f9bc 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap.rs @@ -1256,6 +1256,7 @@ impl TakerSwap { NEGOTIATE_TIMEOUT_SEC as f64 / 6., self.p2p_privkey, ); + let recv_fut = recv_swap_msg( self.ctx.clone(), |store| store.negotiated.take(), diff --git a/mm2src/mm2_main/src/rpc.rs b/mm2src/mm2_main/src/rpc.rs index 4e4947e151..3343777b72 100644 --- a/mm2src/mm2_main/src/rpc.rs +++ b/mm2src/mm2_main/src/rpc.rs @@ -46,8 +46,9 @@ cfg_native! { mod dispatcher_legacy; pub mod lp_commands; mod rate_limiter; +pub mod wc_commands; -/// Lists the RPC method not requiring the "userpass" authentication. +/// Lists the RPC method not requiring the "userpass" authentication. /// None is also public to skip auth and display proper error in case of method is missing const PUBLIC_METHODS: &[Option<&str>] = &[ // Sorted alphanumerically (on the first letter) for readability. @@ -71,7 +72,6 @@ const PUBLIC_METHODS: &[Option<&str>] = &[ ]; pub type DispatcherResult = Result>; - #[derive(Display, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum DispatcherError { diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index 937db9631b..dab16f672c 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -1,3 +1,4 @@ +use super::wc_commands::{disconnect_session, get_all_sessions, get_session}; use super::{DispatcherError, DispatcherResult, PUBLIC_METHODS}; use crate::lp_healthcheck::peer_connection_healthcheck_rpc; use crate::lp_native_dex::init_hw::{cancel_init_trezor, init_trezor, init_trezor_status, init_trezor_user_action}; @@ -21,6 +22,7 @@ use crate::rpc::lp_commands::tokens::get_token_info; use crate::rpc::lp_commands::tokens::{approve_token_rpc, get_token_allowance_rpc}; use crate::rpc::lp_commands::trezor::trezor_connection_status; use crate::rpc::rate_limiter::{process_rate_limit, RateLimitContext}; +use crate::rpc::wc_commands::{new_connection, ping_session}; use coins::eth::EthCoin; use coins::my_tx_history_v2::my_tx_history_v2_rpc; use coins::rpc_command::tendermint::{ibc_chains, ibc_transfer_channels}; @@ -236,6 +238,11 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, one_inch_v6_0_classic_swap_tokens_rpc).await, + "wc_new_connection" => handle_mmrpc(ctx, request, new_connection).await, + "wc_get_session" => handle_mmrpc(ctx, request, get_session).await, + "wc_get_sessions" => handle_mmrpc(ctx, request, get_all_sessions).await, + "wc_delete_session" => handle_mmrpc(ctx, request, disconnect_session).await, + "wc_ping_session" => handle_mmrpc(ctx, request, ping_session).await, _ => MmError::err(DispatcherError::NoSuchMethod), } } diff --git a/mm2src/mm2_main/src/rpc/wc_commands/mod.rs b/mm2src/mm2_main/src/rpc/wc_commands/mod.rs new file mode 100644 index 0000000000..08df545f8a --- /dev/null +++ b/mm2src/mm2_main/src/rpc/wc_commands/mod.rs @@ -0,0 +1,34 @@ +mod new_connection; +mod sessions; + +use common::HttpStatusCode; +use derive_more::Display; +use http::StatusCode; +pub use new_connection::new_connection; +use serde::Deserialize; +pub use sessions::*; + +#[derive(Deserialize)] +pub struct EmptyRpcRequst {} + +#[derive(Debug, Serialize)] +pub struct EmptyRpcResponse {} + +#[derive(Serialize, Display, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum WalletConnectRpcError { + InternalError(String), + InitializationError(String), + SessionRequestError(String), +} + +impl HttpStatusCode for WalletConnectRpcError { + fn status_code(&self) -> StatusCode { + match self { + WalletConnectRpcError::InitializationError(_) => StatusCode::BAD_REQUEST, + WalletConnectRpcError::SessionRequestError(_) | WalletConnectRpcError::InternalError(_) => { + StatusCode::INTERNAL_SERVER_ERROR + }, + } + } +} diff --git a/mm2src/mm2_main/src/rpc/wc_commands/new_connection.rs b/mm2src/mm2_main/src/rpc/wc_commands/new_connection.rs new file mode 100644 index 0000000000..410deae5d7 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/wc_commands/new_connection.rs @@ -0,0 +1,32 @@ +use kdf_walletconnect::WalletConnectCtx; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use serde::{Deserialize, Serialize}; + +use super::WalletConnectRpcError; + +#[derive(Debug, PartialEq, Serialize)] +pub struct CreateConnectionResponse { + pub url: String, +} + +#[derive(Deserialize)] +pub struct NewConnectionRequest { + required_namespaces: serde_json::Value, + optional_namespaces: Option, +} + +/// `new_connection` RPC command implementation. +pub async fn new_connection( + ctx: MmArc, + req: NewConnectionRequest, +) -> MmResult { + let ctx = + WalletConnectCtx::from_ctx(&ctx).mm_err(|err| WalletConnectRpcError::InitializationError(err.to_string()))?; + let url = ctx + .new_connection(req.required_namespaces, req.optional_namespaces) + .await + .mm_err(|err| WalletConnectRpcError::SessionRequestError(err.to_string()))?; + + Ok(CreateConnectionResponse { url }) +} diff --git a/mm2src/mm2_main/src/rpc/wc_commands/sessions.rs b/mm2src/mm2_main/src/rpc/wc_commands/sessions.rs new file mode 100644 index 0000000000..7aede2c81f --- /dev/null +++ b/mm2src/mm2_main/src/rpc/wc_commands/sessions.rs @@ -0,0 +1,83 @@ +use kdf_walletconnect::session::rpc::send_session_ping_request; +use kdf_walletconnect::session::SessionRpcInfo; +use kdf_walletconnect::WalletConnectCtx; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use serde::Serialize; + +use super::{EmptyRpcRequst, EmptyRpcResponse, WalletConnectRpcError}; + +#[derive(Debug, PartialEq, Serialize)] +pub struct SessionResponse { + pub result: String, +} + +#[derive(Debug, PartialEq, Serialize)] +pub struct GetSessionsResponse { + pub sessions: Vec, +} + +/// `Get all sessions connection` RPC command implementation. +pub async fn get_all_sessions( + ctx: MmArc, + _req: EmptyRpcRequst, +) -> MmResult { + let ctx = + WalletConnectCtx::from_ctx(&ctx).mm_err(|err| WalletConnectRpcError::InitializationError(err.to_string()))?; + let sessions = ctx + .session_manager + .get_sessions() + .map(SessionRpcInfo::from) + .collect::>(); + + Ok(GetSessionsResponse { sessions }) +} + +#[derive(Debug, Serialize)] +pub struct GetSessionResponse { + pub session: Option, +} + +#[derive(Deserialize)] +pub struct GetSessionRequest { + topic: String, +} + +/// `Get session connection` RPC command implementation. +pub async fn get_session(ctx: MmArc, req: GetSessionRequest) -> MmResult { + let ctx = + WalletConnectCtx::from_ctx(&ctx).mm_err(|err| WalletConnectRpcError::InitializationError(err.to_string()))?; + let session = ctx + .session_manager + .get_session(&req.topic.into()) + .map(SessionRpcInfo::from); + + Ok(GetSessionResponse { session }) +} + +/// `Delete session connection` RPC command implementation. +pub async fn disconnect_session( + ctx: MmArc, + req: GetSessionRequest, +) -> MmResult { + let ctx = + WalletConnectCtx::from_ctx(&ctx).mm_err(|err| WalletConnectRpcError::InitializationError(err.to_string()))?; + ctx.drop_session(&req.topic.into()) + .await + .mm_err(|err| WalletConnectRpcError::SessionRequestError(err.to_string()))?; + + Ok(EmptyRpcResponse {}) +} + +/// `ping session` RPC command implementation. +pub async fn ping_session(ctx: MmArc, req: GetSessionRequest) -> MmResult { + let ctx = + WalletConnectCtx::from_ctx(&ctx).mm_err(|err| WalletConnectRpcError::InitializationError(err.to_string()))?; + send_session_ping_request(&ctx, &req.topic.into()) + .await + .mm_err(|err| WalletConnectRpcError::SessionRequestError(err.to_string()))?; + + Ok(SessionResponse { + result: "Ping successful".to_owned(), + }) +} diff --git a/mm2src/mm2_net/Cargo.toml b/mm2src/mm2_net/Cargo.toml index a720327d96..1f2b61551b 100644 --- a/mm2src/mm2_net/Cargo.toml +++ b/mm2src/mm2_net/Cargo.toml @@ -42,10 +42,7 @@ tower-service = "0.3" wasm-bindgen = "0.2.86" wasm-bindgen-test = { version = "0.3.2" } wasm-bindgen-futures = "0.4.21" -web-sys = { version = "0.3.55", features = ["console", "CloseEvent", "DomException", "ErrorEvent", "IdbDatabase", - "IdbCursor", "IdbCursorWithValue", "IdbFactory", "IdbIndex", "IdbIndexParameters", "IdbObjectStore", - "IdbObjectStoreParameters", "IdbOpenDbRequest", "IdbKeyRange", "IdbTransaction", "IdbTransactionMode", - "IdbVersionChangeEvent", "MessageEvent", "MessagePort", "ReadableStreamDefaultReader", "ReadableStream", "SharedWorker", "Url", "WebSocket"] } +web-sys = { version = "0.3.55", features = ["console", "CloseEvent", "DomException", "ErrorEvent", "IdbDatabase", "IdbCursor", "IdbCursorWithValue", "IdbFactory", "IdbIndex", "IdbIndexParameters", "IdbObjectStore", "IdbObjectStoreParameters", "IdbOpenDbRequest", "IdbKeyRange", "IdbTransaction", "IdbTransactionMode", "Request", "RequestInit", "RequestMode", "Response", "Window","Headers", "IdbVersionChangeEvent", "MessageEvent", "MessagePort", "ReadableStreamDefaultReader", "ReadableStream", "SharedWorker", "Url", "WebSocket"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/mm2src/mm2_number/Cargo.toml b/mm2src/mm2_number/Cargo.toml index 0303fd20d8..2449ca3360 100644 --- a/mm2src/mm2_number/Cargo.toml +++ b/mm2src/mm2_number/Cargo.toml @@ -11,6 +11,6 @@ bigdecimal = { version = "0.3", features = ["serde"] } num-bigint = { version = "0.4", features = ["serde", "std"] } num-rational = { version = "0.4", features = ["serde"] } num-traits = "0.2" -paste = "1.0" +paste = "1.0.7" serde = { version = "1", features = ["serde_derive"] } serde_json = { version = "1", features = ["preserve_order", "raw_value"] } diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 7592384696..86d3b85d02 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -1164,6 +1164,9 @@ pub async fn mm_ctx_with_custom_async_db() -> MmArc { ctx } +#[cfg(target_arch = "wasm32")] +pub async fn mm_ctx_with_custom_async_db() -> MmArc { MmCtxBuilder::new().with_test_db_namespace().into_mm_arc() } + /// Automatically kill a wrapped process. pub struct RaiiKill { pub handle: Child,