From 4649072ab7445722f7153c8d453463fc7c0c577e Mon Sep 17 00:00:00 2001 From: michael1011 Date: Mon, 9 Dec 2024 13:36:09 +0100 Subject: [PATCH] feat: support multiple contract versions (#739) --- boltzr/Cargo.lock | 315 +++++++++++------ boltzr/Cargo.toml | 24 +- boltzr/protos/boltzr.proto | 5 + boltzr/src/config.rs | 11 +- boltzr/src/evm/contracts/erc20_swap.rs | 71 +++- boltzr/src/evm/contracts/ether_swap.rs | 67 +++- boltzr/src/evm/contracts/mod.rs | 10 + boltzr/src/evm/manager.rs | 247 +++++++++++++ boltzr/src/evm/mod.rs | 32 +- boltzr/src/evm/refund_signer.rs | 191 +++++----- boltzr/src/grpc/server.rs | 12 +- boltzr/src/grpc/service.rs | 68 +++- boltzr/src/main.rs | 2 +- lib/Config.ts | 12 +- lib/cli/ethereum/EthereumUtils.ts | 4 +- lib/cli/ethereum/commands/Claim.ts | 2 +- lib/cli/ethereum/commands/Refund.ts | 2 +- lib/proto/sidecar/boltzr_pb.d.ts | 21 ++ lib/proto/sidecar/boltzr_pb.js | 127 ++++++- lib/service/Renegotiator.ts | 35 +- lib/service/cooperative/DeferredClaimer.ts | 33 +- lib/service/cooperative/EipSigner.ts | 6 + lib/sidecar/Sidecar.ts | 2 + lib/swap/SwapManager.ts | 5 +- lib/swap/SwapNursery.ts | 117 +++--- .../ethereum/ConsolidatedEventHandler.ts | 30 ++ lib/wallet/ethereum/Errors.ts | 8 + lib/wallet/ethereum/EthereumManager.ts | 147 ++++---- .../{ => contracts}/ContractEventHandler.ts | 33 +- .../{ => contracts}/ContractHandler.ts | 38 +- .../ethereum/{ => contracts}/ContractUtils.ts | 6 +- lib/wallet/ethereum/contracts/Contracts.ts | 114 ++++++ lib/wallet/providers/ERC20WalletProvider.ts | 2 + test/integration/service/Renegotiator.spec.ts | 11 +- .../cooperative/DeferredClaimer.spec.ts | 41 ++- .../service/cooperative/EipSigner.spec.ts | 12 + .../wallet/ethereum/EthereumManager.spec.ts | 99 +++--- .../ContractEventHandler.spec.ts | 47 ++- .../{ => contracts}/ContractHandler.spec.ts | 333 ++++++++++-------- .../{ => contracts}/ContractUtils.spec.ts | 9 +- .../ethereum/contracts/Contracts.spec.ts | 98 ++++++ .../ethereum/ConsolidatedEventHandler.spec.ts | 68 ++++ 42 files changed, 1832 insertions(+), 685 deletions(-) create mode 100644 boltzr/src/evm/manager.rs create mode 100644 lib/wallet/ethereum/ConsolidatedEventHandler.ts rename lib/wallet/ethereum/{ => contracts}/ContractEventHandler.ts (87%) rename lib/wallet/ethereum/{ => contracts}/ContractHandler.ts (87%) rename lib/wallet/ethereum/{ => contracts}/ContractUtils.ts (95%) create mode 100644 lib/wallet/ethereum/contracts/Contracts.ts rename test/integration/wallet/ethereum/{ => contracts}/ContractEventHandler.spec.ts (84%) rename test/integration/wallet/ethereum/{ => contracts}/ContractHandler.spec.ts (65%) rename test/integration/wallet/ethereum/{ => contracts}/ContractUtils.spec.ts (92%) create mode 100644 test/integration/wallet/ethereum/contracts/Contracts.spec.ts create mode 100644 test/unit/wallet/ethereum/ConsolidatedEventHandler.spec.ts diff --git a/boltzr/Cargo.lock b/boltzr/Cargo.lock index 3dc9e46d..f10ced48 100644 --- a/boltzr/Cargo.lock +++ b/boltzr/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -47,9 +47,9 @@ checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] name = "alloy" -version = "0.6.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b524b8c28a7145d1fe4950f84360b5de3e307601679ff0558ddc20ea229399" +checksum = "02b0561294ccedc6181e5528b850b4579e3fbde696507baa00109bfd9054c5bb" dependencies = [ "alloy-consensus", "alloy-contract", @@ -81,25 +81,40 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "0.6.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae09ffd7c29062431dd86061deefe4e3c6f07fa0d674930095f8dcedb0baf02c" +checksum = "a101d4d016f47f13890a74290fdd17b05dd175191d9337bc600791fb96e4dea8" dependencies = [ "alloy-eips", "alloy-primitives", "alloy-rlp", "alloy-serde", + "alloy-trie", "auto_impl", "c-kzg", "derive_more", "serde", ] +[[package]] +name = "alloy-consensus-any" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa60357dda9a3d0f738f18844bd6d0f4a5924cc5cf00bfad2ff1369897966123" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "serde", +] + [[package]] name = "alloy-contract" -version = "0.6.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66430a72d5bf5edead101c8c2f0a24bada5ec9f3cf9909b3e08b6d6899b4803e" +checksum = "2869e4fb31331d3b8c58c7db567d1e4e4e94ef64640beda3b6dd9b7045690941" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -112,7 +127,7 @@ dependencies = [ "alloy-transport", "futures", "futures-util", - "thiserror", + "thiserror 2.0.4", ] [[package]] @@ -171,9 +186,9 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "0.6.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b6aa3961694b30ba53d41006131a2fca3bdab22e4c344e46db2c639e7c2dfdd" +checksum = "8b6755b093afef5925f25079dd5a7c8d096398b804ba60cb5275397b06b31689" dependencies = [ "alloy-eip2930", "alloy-eip7702", @@ -189,12 +204,13 @@ dependencies = [ [[package]] name = "alloy-genesis" -version = "0.6.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e53f7877ded3921d18a0a9556d55bedf84535567198c9edab2aa23106da91855" +checksum = "aeec8e6eab6e52b7c9f918748c9b811e87dbef7312a2e3a2ca1729a92966a6af" dependencies = [ "alloy-primitives", "alloy-serde", + "alloy-trie", "serde", ] @@ -212,29 +228,31 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "0.6.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3694b7e480728c0b3e228384f223937f14c10caef5a4c766021190fc8f283d35" +checksum = "4fa077efe0b834bcd89ff4ba547f48fb081e4fdc3673dd7da1b295a2cf2bb7b7" dependencies = [ "alloy-primitives", "alloy-sol-types", "serde", "serde_json", - "thiserror", + "thiserror 2.0.4", "tracing", ] [[package]] name = "alloy-network" -version = "0.6.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea94b8ceb5c75d7df0a93ba0acc53b55a22b47b532b600a800a87ef04eb5b0b4" +checksum = "209a1882a08e21aca4aac6e2a674dc6fcf614058ef8cb02947d63782b1899552" dependencies = [ "alloy-consensus", + "alloy-consensus-any", "alloy-eips", "alloy-json-rpc", "alloy-network-primitives", "alloy-primitives", + "alloy-rpc-types-any", "alloy-rpc-types-eth", "alloy-serde", "alloy-signer", @@ -244,14 +262,14 @@ dependencies = [ "futures-utils-wasm", "serde", "serde_json", - "thiserror", + "thiserror 2.0.4", ] [[package]] name = "alloy-network-primitives" -version = "0.6.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9f3e281005943944d15ee8491534a1c7b3cbf7a7de26f8c433b842b93eb5f9" +checksum = "c20219d1ad261da7a6331c16367214ee7ded41d001fabbbd656fbf71898b2773" dependencies = [ "alloy-consensus", "alloy-eips", @@ -262,9 +280,9 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "0.8.13" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aeeb5825c2fc8c2662167058347cd0cafc3cb15bcb5cdb1758a63c2dca0409e" +checksum = "9db948902dfbae96a73c2fbf1f7abec62af034ab883e4c777c3fd29702bd6e2c" dependencies = [ "alloy-rlp", "bytes", @@ -290,9 +308,9 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "0.6.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c1f9eede27bf4c13c099e8e64d54efd7ce80ef6ea47478aa75d5d74e2dba3b" +checksum = "9eefa6f4c798ad01f9b4202d02cea75f5ec11fa180502f4701e2b47965a8c0bb" dependencies = [ "alloy-chains", "alloy-consensus", @@ -318,7 +336,7 @@ dependencies = [ "schnellru", "serde", "serde_json", - "thiserror", + "thiserror 2.0.4", "tokio", "tracing", "url", @@ -349,9 +367,9 @@ dependencies = [ [[package]] name = "alloy-rpc-client" -version = "0.6.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374dbe0dc3abdc2c964f36b3d3edf9cdb3db29d16bda34aa123f03d810bec1dd" +checksum = "ed30bf1041e84cabc5900f52978ca345dd9969f2194a945e6fdec25b0620705c" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -370,13 +388,25 @@ dependencies = [ "wasmtimer", ] +[[package]] +name = "alloy-rpc-types-any" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200661999b6e235d9840be5d60a6e8ae2f0af9eb2a256dd378786744660e36ec" +dependencies = [ + "alloy-consensus-any", + "alloy-rpc-types-eth", + "alloy-serde", +] + [[package]] name = "alloy-rpc-types-eth" -version = "0.6.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a477281940d82d29315846c7216db45b15e90bcd52309da9f54bcf7ad94a11" +checksum = "a0600b8b5e2dc0cab12cbf91b5a885c35871789fb7b3a57b434bd4fced5b7a8b" dependencies = [ "alloy-consensus", + "alloy-consensus-any", "alloy-eips", "alloy-network-primitives", "alloy-primitives", @@ -391,9 +421,9 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "0.6.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dfa4a7ccf15b2492bb68088692481fd6b2604ccbee1d0d6c44c21427ae4df83" +checksum = "9afa753a97002a33b2ccb707d9f15f31c81b8c1b786c95b73cc62bb1d1fd0c3f" dependencies = [ "alloy-primitives", "serde", @@ -402,9 +432,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "0.6.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e10aec39d60dc27edcac447302c7803d2371946fb737245320a05b78eb2fafd" +checksum = "9b2cbff01a673936c2efd7e00d4c0e9a4dbbd6d600e2ce298078d33efbb19cd7" dependencies = [ "alloy-dyn-abi", "alloy-primitives", @@ -413,14 +443,14 @@ dependencies = [ "auto_impl", "elliptic-curve", "k256", - "thiserror", + "thiserror 2.0.4", ] [[package]] name = "alloy-signer-aws" -version = "0.6.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0109e5b18079aec2a022e4bc9db1d74bcc046f8b66274ffa8b0e4322b44b2b44" +checksum = "71ce77227fdb9059fd7a3b38a8679c0dae95d81886ee8c13ef8ad99d74866bbd" dependencies = [ "alloy-consensus", "alloy-network", @@ -430,15 +460,15 @@ dependencies = [ "aws-sdk-kms", "k256", "spki", - "thiserror", + "thiserror 2.0.4", "tracing", ] [[package]] name = "alloy-signer-gcp" -version = "0.6.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "558651eb0d76bcf2224de694481e421112fa2cbc6fe6a413cc76fd67e14cf0d7" +checksum = "7622438a51e1fa6379cad6bff52e0cde88b0d4e5e3f2f15e5feebdee527ef5f2" dependencies = [ "alloy-consensus", "alloy-network", @@ -448,15 +478,15 @@ dependencies = [ "gcloud-sdk", "k256", "spki", - "thiserror", + "thiserror 2.0.4", "tracing", ] [[package]] name = "alloy-signer-ledger" -version = "0.6.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29781b6a064b6235de4ec3cc0810f59fe227b8d31258f23a077570fc9525d7a6" +checksum = "b7b56789cbd13bace37acd7afd080aa7002ed65ab84f0220cd0c32e162b0afd6" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -468,15 +498,15 @@ dependencies = [ "coins-ledger", "futures-util", "semver 1.0.23", - "thiserror", + "thiserror 2.0.4", "tracing", ] [[package]] name = "alloy-signer-local" -version = "0.6.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8396f6dff60700bc1d215ee03d86ff56de268af96e2bf833a14d0bafcab9882" +checksum = "bd6d988cb6cd7d2f428a74476515b1a6e901e08c796767f9f93311ab74005c8b" dependencies = [ "alloy-consensus", "alloy-network", @@ -487,7 +517,7 @@ dependencies = [ "coins-bip39", "k256", "rand", - "thiserror", + "thiserror 2.0.4", ] [[package]] @@ -565,9 +595,9 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "0.6.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f99acddb34000d104961897dbb0240298e8b775a7efffb9fda2a1a3efedd65b3" +checksum = "d69d36982b9e46075ae6b792b0f84208c6c2c15ad49f6c500304616ef67b70e0" dependencies = [ "alloy-json-rpc", "base64 0.22.1", @@ -575,7 +605,7 @@ dependencies = [ "futures-utils-wasm", "serde", "serde_json", - "thiserror", + "thiserror 2.0.4", "tokio", "tower 0.5.1", "tracing", @@ -585,9 +615,9 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "0.6.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dc013132e34eeadaa0add7e74164c1503988bfba8bae885b32e0918ba85a8a6" +checksum = "2e02ffd5d93ffc51d72786e607c97de3b60736ca3e636ead0ec1f7dce68ea3fd" dependencies = [ "alloy-json-rpc", "alloy-transport", @@ -598,6 +628,22 @@ dependencies = [ "url", ] +[[package]] +name = "alloy-trie" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a5fd8fea044cc9a8c8a50bb6f28e31f0385d820f116c5b98f6f4e55d6e5590b" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "arrayvec", + "derive_more", + "nybbles", + "serde", + "smallvec", + "tracing", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -664,9 +710,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.92" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74f37166d7d48a0284b99dd824694c26119c700b53bf0d1540cdb147dbdaaf13" +checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "ark-ff" @@ -797,6 +843,9 @@ name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +dependencies = [ + "serde", +] [[package]] name = "asn1-rs" @@ -810,7 +859,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror", + "thiserror 1.0.63", "time", ] @@ -960,7 +1009,7 @@ dependencies = [ "quick-xml", "rust-ini", "serde", - "thiserror", + "thiserror 1.0.63", "time", "url", ] @@ -971,7 +1020,7 @@ version = "0.25.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9aed3f9c7eac9be28662fdb3b0f4d1951e812f7c64fed4f0327ba702f459b3b" dependencies = [ - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -1391,9 +1440,9 @@ checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] name = "bitcoin" -version = "0.32.4" +version = "0.32.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "788902099d47c8682efe6a7afb01c8d58b9794ba66c06affd81c3d6b560743eb" +checksum = "ce6bc65742dea50536e35ad42492b234c27904a27f0abdcbce605015cb4ea026" dependencies = [ "base58ck", "bech32 0.11.0", @@ -1557,7 +1606,7 @@ dependencies = [ "opentelemetry-otlp", "opentelemetry-semantic-conventions", "opentelemetry_sdk", - "prost 0.13.3", + "prost 0.13.4", "r2d2", "rand", "rcgen", @@ -1711,9 +1760,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.20" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" dependencies = [ "clap_builder", "clap_derive", @@ -1721,9 +1770,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.20" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" dependencies = [ "anstream", "anstyle", @@ -1745,9 +1794,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "coins-bip32" @@ -1762,7 +1811,7 @@ dependencies = [ "k256", "serde", "sha2", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -1778,7 +1827,7 @@ dependencies = [ "pbkdf2", "rand", "sha2", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -1797,7 +1846,7 @@ dependencies = [ "serde", "sha2", "sha3", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -1816,7 +1865,7 @@ dependencies = [ "log", "nix 0.26.4", "once_cell", - "thiserror", + "thiserror 1.0.63", "tokio", "tracing", "wasm-bindgen", @@ -1831,9 +1880,9 @@ checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" [[package]] name = "const-hex" -version = "1.12.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8a24a26d37e1ffd45343323dc9fe6654ceea44c12f2fcb3d7ac29e610bc6" +checksum = "4b0485bab839b018a8f1723fc5391819fea5f8f0f32288ef8a735fd096b6160c" dependencies = [ "cfg-if", "cpufeatures", @@ -2099,9 +2148,9 @@ dependencies = [ [[package]] name = "diesel" -version = "2.2.4" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "158fe8e2e68695bd615d7e4f3227c0727b151330d3e253b525086c348d055d5e" +checksum = "ccf1bedf64cdb9643204a36dd15b19a6ce8e7aa7f7b105868e9f1fad5ffa7d12" dependencies = [ "bitflags 2.6.0", "byteorder", @@ -2576,7 +2625,7 @@ dependencies = [ "hyper 1.5.0", "jsonwebtoken", "once_cell", - "prost 0.13.3", + "prost 0.13.4", "prost-types 0.13.2", "reqwest", "secret-vault-value", @@ -3477,7 +3526,7 @@ dependencies = [ "metrics 0.22.3", "metrics-util 0.16.3", "quanta", - "thiserror", + "thiserror 1.0.63", "tokio", ] @@ -3492,7 +3541,7 @@ dependencies = [ "metrics 0.23.0", "metrics-util 0.17.0", "quanta", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -3771,6 +3820,19 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "nybbles" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95f06be0417d97f81fe4e5c86d7d01b392655a9cac9c19a848aa033e18937b23" +dependencies = [ + "alloy-rlp", + "const-hex", + "proptest", + "serde", + "smallvec", +] + [[package]] name = "object" version = "0.36.4" @@ -3850,7 +3912,7 @@ dependencies = [ "js-sys", "once_cell", "pin-project-lite", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -3865,8 +3927,8 @@ dependencies = [ "opentelemetry", "opentelemetry-proto", "opentelemetry_sdk", - "prost 0.13.3", - "thiserror", + "prost 0.13.4", + "thiserror 1.0.63", "tokio", "tonic 0.12.3", ] @@ -3879,7 +3941,7 @@ checksum = "2c43620e8f93359eb7e627a3b16ee92d8585774986f24f2ab010817426c5ce61" dependencies = [ "opentelemetry", "opentelemetry_sdk", - "prost 0.13.3", + "prost 0.13.4", "tonic 0.12.3", ] @@ -3905,7 +3967,7 @@ dependencies = [ "percent-encoding", "rand", "serde_json", - "thiserror", + "thiserror 1.0.63", "tokio", "tokio-stream", ] @@ -4035,7 +4097,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c73c26c01b8c87956cea613c907c9d6ecffd8d18a2a5908e5de0adfaa185cea" dependencies = [ "memchr", - "thiserror", + "thiserror 1.0.63", "ucd-trie", ] @@ -4289,12 +4351,12 @@ dependencies = [ [[package]] name = "prost" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0487d90e047de87f984913713b85c601c05609aad5b0df4b4573fbf69aa13f" +checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec" dependencies = [ "bytes", - "prost-derive 0.13.3", + "prost-derive 0.13.4", ] [[package]] @@ -4332,7 +4394,7 @@ dependencies = [ "once_cell", "petgraph", "prettyplease", - "prost 0.13.3", + "prost 0.13.4", "prost-types 0.13.2", "regex", "syn 2.0.87", @@ -4354,9 +4416,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5" +checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3" dependencies = [ "anyhow", "itertools 0.13.0", @@ -4380,7 +4442,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60caa6738c7369b940c3d49246a8d1749323674c65cb13010134f5c9bad5b519" dependencies = [ - "prost 0.13.3", + "prost 0.13.4", ] [[package]] @@ -4427,7 +4489,7 @@ dependencies = [ "rustc-hash 2.0.0", "rustls 0.23.16", "socket2", - "thiserror", + "thiserror 1.0.63", "tokio", "tracing", ] @@ -4444,7 +4506,7 @@ dependencies = [ "rustc-hash 2.0.0", "rustls 0.23.16", "slab", - "thiserror", + "thiserror 1.0.63", "tinyvec", "tracing", ] @@ -4568,7 +4630,7 @@ checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom", "libredox", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -4842,7 +4904,7 @@ dependencies = [ "serde_derive", "serde_json", "sha2", - "thiserror", + "thiserror 1.0.63", "time", "tokio", "tokio-native-tls", @@ -5175,7 +5237,7 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc32a777b53b3433b974c9c26b6d502a50037f8da94e46cb8ce2ced2cfdfaea0" dependencies = [ - "prost 0.13.3", + "prost 0.13.4", "prost-types 0.13.2", "serde", "serde_json", @@ -5231,18 +5293,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.214" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.214" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", @@ -5251,9 +5313,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", "memchr", @@ -5401,7 +5463,7 @@ checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" dependencies = [ "num-bigint", "num-traits", - "thiserror", + "thiserror 1.0.63", "time", ] @@ -5425,6 +5487,9 @@ name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +dependencies = [ + "serde", +] [[package]] name = "smartstring" @@ -5621,7 +5686,16 @@ version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.63", +] + +[[package]] +name = "thiserror" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f49a1853cf82743e3b7950f77e0f4d622ca36cf4317cba00c767838bac8d490" +dependencies = [ + "thiserror-impl 2.0.4", ] [[package]] @@ -5635,6 +5709,17 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "thiserror-impl" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8381894bb3efe0c4acac3ded651301ceee58a15d47c2e34885ed1908ad667061" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "thread_local" version = "1.1.8" @@ -5793,9 +5878,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", @@ -5888,7 +5973,7 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project 1.1.5", - "prost 0.13.3", + "prost 0.13.4", "rustls-native-certs 0.8.0", "rustls-pemfile 2.1.3", "socket2", @@ -6006,9 +6091,9 @@ dependencies = [ [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "pin-project-lite", @@ -6018,9 +6103,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", @@ -6029,9 +6114,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", "valuable", @@ -6110,9 +6195,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "matchers", "nu-ansi-term", @@ -6153,7 +6238,7 @@ dependencies = [ "native-tls", "rand", "sha1", - "thiserror", + "thiserror 1.0.63", "utf-8", ] @@ -6714,7 +6799,7 @@ dependencies = [ "oid-registry", "ring", "rusticata-macros", - "thiserror", + "thiserror 1.0.63", "time", ] diff --git a/boltzr/Cargo.toml b/boltzr/Cargo.toml index f0dba1fc..f98bb462 100644 --- a/boltzr/Cargo.toml +++ b/boltzr/Cargo.toml @@ -34,22 +34,22 @@ panic = "abort" [dependencies] axum = "0.7.7" bitcoin_hashes = "0.15.0" -clap = { version = "4.5.20", features = ["derive"] } +clap = { version = "4.5.23", features = ["derive"] } crossbeam-channel = "0.5.13" ctrlc = { version = "3.4.5", features = ["termination"] } dirs = "5.0.1" num_cpus = "1.16.0" -prost = "0.13.3" +prost = "0.13.4" rcgen = { version = "0.13.1", features = ["x509-parser"] } reqwest = { version = "0.12.9", features = ["json"] } -serde = { version = "1.0.214", features = ["derive"] } -serde_json = "1.0.132" +serde = { version = "1.0.215", features = ["derive"] } +serde_json = "1.0.133" tokio = { version = "1.38.1", features = ["rt-multi-thread", "macros", "process"] } toml = "0.8.19" tonic = { version = "0.12.3", features = ["prost", "tls"] } -tracing = "0.1.40" -tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } -tokio-util = "0.7.12" +tracing = "0.1.41" +tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } +tokio-util = "0.7.13" tracing-loki = { version = "0.2.5", optional = true } futures = "0.3.31" metrics-process = { version = "2.1.0", optional = true } @@ -58,7 +58,7 @@ axum-prometheus = { version = "0.6.1", default-features = false, optional = true metrics = { version = "0.23.0", optional = true } diesel_migrations = "2.2.0" r2d2 = "0.8.10" -diesel = { version = "2.2.4", default-features = false, features = ["postgres", "r2d2", "chrono"] } +diesel = { version = "2.2.6", default-features = false, features = ["postgres", "r2d2", "chrono"] } strum_macros = "0.26.4" strum = "0.26.3" dashmap = "6.1.0" @@ -68,17 +68,17 @@ opentelemetry_sdk = { version = "0.25.0", optional = true, features = ["rt-tokio opentelemetry-otlp = { version = "0.25.0", optional = true } tracing-opentelemetry = { version = "0.26.0", optional = true } diesel-tracing = { version = "0.3.0", optional = true, features = ["postgres", "r2d2", "statement-fields"] } -alloy = { version = "0.6.4", features = ["reqwest", "sol-types", "serde", "eip712", "signer-local", "signer-mnemonic", "providers", "transports", "contract", "json"] } -alloy-transport-http = "0.6.4" +alloy = { version = "0.7.3", features = ["reqwest", "sol-types", "serde", "eip712", "signer-local", "signer-mnemonic", "providers", "transports", "contract", "json"] } +alloy-transport-http = "0.7.3" async-tungstenite = { version = "0.28.0", features = ["tokio-native-tls", "tokio-runtime"] } async-trait = "0.1.83" futures-util = "0.3.31" async-stream = "0.3.6" -anyhow = "1.0.92" +anyhow = "1.0.94" lightning = { version = "0.0.125", features = ["std"] } lightning-invoice = { version = "0.32.0", features = ["std"] } bech32 = "0.9.1" -bitcoin = "0.32.4" +bitcoin = "0.32.5" elements = "0.25.1" base64 = "0.22.1" rust-s3 = "0.35.1" diff --git a/boltzr/protos/boltzr.proto b/boltzr/protos/boltzr.proto index 01f3b7a1..d967ad46 100644 --- a/boltzr/protos/boltzr.proto +++ b/boltzr/protos/boltzr.proto @@ -118,6 +118,11 @@ message SignEvmRefundRequest { // When populated, an ERC20 refund signature will be signed optional string token_address = 3; uint64 timeout = 4; + + oneof contract { + string address = 5; + uint64 version = 6; + } } message SignEvmRefundResponse { diff --git a/boltzr/src/config.rs b/boltzr/src/config.rs index 50cd446a..33c322d6 100644 --- a/boltzr/src/config.rs +++ b/boltzr/src/config.rs @@ -193,8 +193,9 @@ lokiNetwork = "someNetwork" [rsk] providerEndpoint = "http://127.0.0.1:8545" -etherSwapAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3" -erc20SwapAddress = "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" + [[rsk.contracts]] + etherSwap = "0x5FbDB2315678afecb367f032d93F642f64180aa3" + erc20Swap = "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" [sidecar] [sidecar.grpc] @@ -242,8 +243,10 @@ erc20SwapAddress = "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" config.rsk.unwrap(), crate::evm::Config { provider_endpoint: "http://127.0.0.1:8545".to_string(), - ether_swap_address: "0x5FbDB2315678afecb367f032d93F642f64180aa3".to_string(), - erc20_swap_address: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512".to_string(), + contracts: vec![crate::evm::ContractAddresses { + ether_swap: "0x5FbDB2315678afecb367f032d93F642f64180aa3".to_string(), + erc20_swap: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512".to_string(), + }], } ); diff --git a/boltzr/src/evm/contracts/erc20_swap.rs b/boltzr/src/evm/contracts/erc20_swap.rs index 65092fb5..912a8913 100644 --- a/boltzr/src/evm/contracts/erc20_swap.rs +++ b/boltzr/src/evm/contracts/erc20_swap.rs @@ -1,13 +1,11 @@ -use std::error::Error; - +use crate::evm::contracts::erc20_swap::ERC20Swap::ERC20SwapInstance; +use crate::evm::contracts::SwapContract; use alloy::primitives::{Address, U256}; use alloy::providers::Provider; use alloy::sol; use alloy::sol_types::Eip712Domain; use tracing::{debug, info}; -use crate::evm::contracts::erc20_swap::ERC20Swap::ERC20SwapInstance; - sol!( #[allow(clippy::too_many_arguments)] #[sol(rpc)] @@ -31,6 +29,7 @@ pub struct ERC20SwapContract { #[allow(dead_code)] contract: ERC20SwapInstance, + version: u8, eip712domain: Eip712Domain, } @@ -40,15 +39,28 @@ impl< N: alloy::providers::network::Network, > ERC20SwapContract { - pub async fn new(address: Address, provider: P) -> Result> { - info!("Using {}: {}", NAME, address.to_string()); + pub async fn new(address: Address, provider: P) -> anyhow::Result { + debug!("Using {}: {}", NAME, address.to_string()); + let code = provider.get_code_at(address).await?; + if code.is_empty() { + return Err(anyhow::anyhow!( + "no contract at address: {}", + address.to_string() + )); + } let erc20_swap = ERC20Swap::new(address, provider.clone()); let chain_id = provider.get_chain_id().await?; let version = erc20_swap.version().call().await?._0; - debug!("Found {} version: {}", NAME, version); + info!( + "Found {} ({}) version: {}", + NAME, + address.to_string(), + version + ); Ok(ERC20SwapContract { + version, contract: erc20_swap, eip712domain: Eip712Domain::new( Some(NAME.into()), @@ -59,8 +71,23 @@ impl< ), }) } +} + +impl< + T: alloy::transports::Transport + Sync + Send + Clone, + P: Provider + Clone + 'static, + N: alloy::providers::network::Network, + > SwapContract for ERC20SwapContract +{ + fn address(&self) -> &Address { + self.contract.address() + } - pub fn eip712_domain(&self) -> &Eip712Domain { + fn version(&self) -> u8 { + self.version + } + + fn eip712_domain(&self) -> &Eip712Domain { &self.eip712domain } } @@ -68,7 +95,35 @@ impl< #[cfg(test)] mod test { use crate::evm::contracts::erc20_swap::ERC20SwapContract; + use crate::evm::contracts::SwapContract; use crate::evm::refund_signer::test::ERC20_SWAP_ADDRESS; + use alloy::primitives::Address; + + #[tokio::test] + async fn test_address() { + let (_, _, _, provider) = crate::evm::refund_signer::test::setup().await; + let contract = ERC20SwapContract::new(ERC20_SWAP_ADDRESS.parse().unwrap(), provider) + .await + .unwrap(); + + assert_eq!( + contract.address(), + &ERC20_SWAP_ADDRESS.parse::
().unwrap() + ); + } + + #[tokio::test] + async fn test_version() { + let (_, _, _, provider) = crate::evm::refund_signer::test::setup().await; + let contract = ERC20SwapContract::new(ERC20_SWAP_ADDRESS.parse().unwrap(), provider) + .await + .unwrap(); + + assert_eq!( + contract.version(), + contract.contract.version().call().await.unwrap()._0 + ); + } #[tokio::test] async fn test_eip712_domain() { diff --git a/boltzr/src/evm/contracts/ether_swap.rs b/boltzr/src/evm/contracts/ether_swap.rs index 1f078723..2fbfc3eb 100644 --- a/boltzr/src/evm/contracts/ether_swap.rs +++ b/boltzr/src/evm/contracts/ether_swap.rs @@ -1,9 +1,9 @@ use crate::evm::contracts::ether_swap::EtherSwap::EtherSwapInstance; +use crate::evm::contracts::SwapContract; use alloy::primitives::{Address, U256}; use alloy::providers::Provider; use alloy::sol; use alloy::sol_types::Eip712Domain; -use std::error::Error; use tracing::{debug, info}; sol!( @@ -28,6 +28,7 @@ pub struct EtherSwapContract { #[allow(dead_code)] contract: EtherSwapInstance, + version: u8, eip712domain: Eip712Domain, } @@ -37,15 +38,28 @@ impl< N: alloy::providers::network::Network, > EtherSwapContract { - pub async fn new(address: Address, provider: P) -> Result> { - info!("Using {}: {}", NAME, address.to_string()); + pub async fn new(address: Address, provider: P) -> anyhow::Result { + debug!("Using {}: {}", NAME, address.to_string()); + let code = provider.get_code_at(address).await?; + if code.is_empty() { + return Err(anyhow::anyhow!( + "no contract at address: {}", + address.to_string() + )); + } let ether_swap = EtherSwap::new(address, provider.clone()); let chain_id = provider.get_chain_id().await?; let version = ether_swap.version().call().await?._0; - debug!("Found {} version: {}", NAME, version); + info!( + "Found {} ({}) version: {}", + NAME, + address.to_string(), + version + ); Ok(EtherSwapContract { + version, contract: ether_swap, eip712domain: Eip712Domain::new( Some(NAME.into()), @@ -56,8 +70,23 @@ impl< ), }) } +} + +impl< + T: alloy::transports::Transport + Sync + Send + Clone, + P: Provider + Clone + 'static, + N: alloy::providers::network::Network, + > SwapContract for EtherSwapContract +{ + fn address(&self) -> &Address { + self.contract.address() + } + + fn version(&self) -> u8 { + self.version + } - pub fn eip712_domain(&self) -> &Eip712Domain { + fn eip712_domain(&self) -> &Eip712Domain { &self.eip712domain } } @@ -65,7 +94,35 @@ impl< #[cfg(test)] mod test { use crate::evm::contracts::ether_swap::EtherSwapContract; + use crate::evm::contracts::SwapContract; use crate::evm::refund_signer::test::ETHER_SWAP_ADDRESS; + use alloy::primitives::Address; + + #[tokio::test] + async fn test_address() { + let (_, _, _, provider) = crate::evm::refund_signer::test::setup().await; + let contract = EtherSwapContract::new(ETHER_SWAP_ADDRESS.parse().unwrap(), provider) + .await + .unwrap(); + + assert_eq!( + contract.address(), + ÐER_SWAP_ADDRESS.parse::
().unwrap() + ); + } + + #[tokio::test] + async fn test_version() { + let (_, _, _, provider) = crate::evm::refund_signer::test::setup().await; + let contract = EtherSwapContract::new(ETHER_SWAP_ADDRESS.parse().unwrap(), provider) + .await + .unwrap(); + + assert_eq!( + contract.version(), + contract.contract.version().call().await.unwrap()._0 + ); + } #[tokio::test] async fn test_eip712_domain() { diff --git a/boltzr/src/evm/contracts/mod.rs b/boltzr/src/evm/contracts/mod.rs index 73b565de..92b200ae 100644 --- a/boltzr/src/evm/contracts/mod.rs +++ b/boltzr/src/evm/contracts/mod.rs @@ -1,2 +1,12 @@ +use alloy::dyn_abi::Eip712Domain; +use alloy::primitives::Address; + pub mod erc20_swap; pub mod ether_swap; + +pub trait SwapContract { + fn address(&self) -> &Address; + fn version(&self) -> u8; + + fn eip712_domain(&self) -> &Eip712Domain; +} diff --git a/boltzr/src/evm/manager.rs b/boltzr/src/evm/manager.rs new file mode 100644 index 00000000..dd43334f --- /dev/null +++ b/boltzr/src/evm/manager.rs @@ -0,0 +1,247 @@ +use crate::evm::refund_signer::LocalRefundSigner; +use crate::evm::RefundSigner; +use alloy::network::{AnyNetwork, EthereumWallet}; +use alloy::primitives::{Address, FixedBytes, Signature, U256}; +use alloy::providers::{Provider, ProviderBuilder}; +use alloy::signers::local::coins_bip39::English; +use alloy::signers::local::{MnemonicBuilder, PrivateKeySigner}; +use anyhow::anyhow; +use async_trait::async_trait; +use std::collections::HashMap; +use std::fs; +use tracing::{debug, info, instrument, warn}; + +pub struct Manager { + signer: PrivateKeySigner, + + address_versions: HashMap, + refund_signers: HashMap, +} + +impl Manager { + pub async fn from_mnemonic_file( + mnemonic_path: String, + config: &crate::evm::Config, + ) -> anyhow::Result { + let mnemonic = fs::read_to_string(mnemonic_path)?; + debug!("Read mnemonic"); + + let signer = MnemonicBuilder::::default() + .phrase(mnemonic.strip_suffix("\n").unwrap_or(mnemonic.as_str())) + .index(0)? + .build()?; + + Self::new(signer, config).await + } + + #[instrument(name = "Manager::new", skip_all)] + pub async fn new( + signer: PrivateKeySigner, + config: &crate::evm::Config, + ) -> anyhow::Result { + info!("Using address: {}", signer.address()); + + let provider = ProviderBuilder::new() + .network::() + .with_recommended_fillers() + .wallet(EthereumWallet::from(signer.clone())) + .on_http(config.provider_endpoint.parse()?); + + let chain_id = provider.get_chain_id().await?; + info!("Connected to EVM chain with id: {}", chain_id); + + if config.contracts.is_empty() { + warn!("No contracts are configured"); + } + + let mut refund_signers = HashMap::new(); + for contracts in &config.contracts { + let refund_signer = LocalRefundSigner::new(provider.clone(), contracts).await?; + refund_signers.insert(refund_signer.version(), refund_signer); + } + + let mut address_versions = HashMap::new(); + refund_signers.iter().for_each(|(version, signer)| { + let addresses = signer.addresses(); + address_versions.insert(*addresses.0, *version); + address_versions.insert(*addresses.1, *version); + }); + + Ok(Self { + signer, + refund_signers, + address_versions, + }) + } +} + +#[async_trait] +impl RefundSigner for Manager { + fn version_for_address(&self, contract_address: &Address) -> anyhow::Result { + match self.address_versions.get(contract_address) { + Some(version) => Ok(*version), + None => Err(anyhow!( + "no signer for contract address {}", + contract_address + )), + } + } + + async fn sign_cooperative_refund( + &self, + contract_version: u8, + preimage_hash: FixedBytes<32>, + amount: U256, + token_address: Option
, + timeout: u64, + ) -> anyhow::Result { + match self.refund_signers.get(&contract_version) { + Some(signer) => { + signer + .sign(&self.signer, preimage_hash, amount, token_address, timeout) + .await + } + None => Err(anyhow!( + "no refund signers for contracts version {}", + contract_version + )), + } + } +} + +#[cfg(test)] +mod test { + use crate::evm::manager::Manager; + use crate::evm::refund_signer::test::{ + ERC20_SWAP_ADDRESS, ETHER_SWAP_ADDRESS, MNEMONIC, PROVIDER, + }; + use crate::evm::{Config, ContractAddresses, RefundSigner}; + use alloy::primitives::{Address, FixedBytes}; + use alloy::signers::local::coins_bip39::English; + use alloy::signers::local::MnemonicBuilder; + use serial_test::serial; + use std::fs; + use std::path::Path; + + const EXPECTED_ADDRESS: &str = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; + + #[tokio::test] + #[serial(mnemonic)] + async fn test_from_mnemonic_file() { + let mnemonic_file = Path::new(env!("CARGO_MANIFEST_DIR")).join("mnemonic"); + fs::write(mnemonic_file.clone(), MNEMONIC).unwrap(); + + let signer = Manager::from_mnemonic_file( + mnemonic_file.to_str().unwrap().to_string(), + &Config { + provider_endpoint: PROVIDER.to_string(), + contracts: vec![ContractAddresses { + ether_swap: ETHER_SWAP_ADDRESS.to_string(), + erc20_swap: ERC20_SWAP_ADDRESS.to_string(), + }], + }, + ) + .await + .unwrap(); + assert_eq!( + signer.signer.address(), + EXPECTED_ADDRESS.parse::
().unwrap() + ); + + fs::remove_file(mnemonic_file.clone()).unwrap(); + } + + #[tokio::test] + #[serial(mnemonic)] + async fn test_from_mnemonic_file_trailing_whitespace() { + let mnemonic_file = Path::new(env!("CARGO_MANIFEST_DIR")).join("mnemonic"); + fs::write(mnemonic_file.clone(), format!("{}\n", MNEMONIC)).unwrap(); + + let signer = Manager::from_mnemonic_file( + mnemonic_file.to_str().unwrap().to_string(), + &Config { + provider_endpoint: PROVIDER.to_string(), + contracts: vec![ContractAddresses { + ether_swap: ETHER_SWAP_ADDRESS.to_string(), + erc20_swap: ERC20_SWAP_ADDRESS.to_string(), + }], + }, + ) + .await + .unwrap(); + assert_eq!( + signer.signer.address(), + EXPECTED_ADDRESS.parse::
().unwrap() + ); + + fs::remove_file(mnemonic_file.clone()).unwrap(); + } + + #[tokio::test] + async fn test_version_for_address() { + let manager = new_manager().await; + + let contract_version = 4; + assert_eq!( + manager + .version_for_address(ÐER_SWAP_ADDRESS.parse().unwrap()) + .unwrap(), + contract_version + ); + assert_eq!( + manager + .version_for_address(&ERC20_SWAP_ADDRESS.parse().unwrap()) + .unwrap(), + contract_version + ); + + assert!(manager.version_for_address(&Address::default()).is_err()); + } + + #[tokio::test] + async fn test_sign_cooperative_refund() { + let manager = new_manager().await; + + assert!(manager + .sign_cooperative_refund( + 4, + FixedBytes::<32>::default(), + crate::evm::utils::parse_wei("1").unwrap(), + None, + 1, + ) + .await + .is_ok()); + + assert!(manager + .sign_cooperative_refund( + 0, + FixedBytes::<32>::default(), + crate::evm::utils::parse_wei("1").unwrap(), + None, + 1, + ) + .await + .is_err()); + } + + async fn new_manager() -> Manager { + Manager::new( + MnemonicBuilder::::default() + .phrase(MNEMONIC) + .index(0) + .unwrap() + .build() + .unwrap(), + &Config { + provider_endpoint: PROVIDER.to_string(), + contracts: vec![ContractAddresses { + ether_swap: ETHER_SWAP_ADDRESS.to_string(), + erc20_swap: ERC20_SWAP_ADDRESS.to_string(), + }], + }, + ) + .await + .unwrap() + } +} diff --git a/boltzr/src/evm/mod.rs b/boltzr/src/evm/mod.rs index 0c870ef9..273d8d24 100644 --- a/boltzr/src/evm/mod.rs +++ b/boltzr/src/evm/mod.rs @@ -1,17 +1,39 @@ +use alloy::primitives::{Address, FixedBytes, Signature, U256}; use serde::{Deserialize, Serialize}; mod contracts; -pub mod refund_signer; +pub mod manager; +mod refund_signer; pub mod utils; +#[derive(Deserialize, Serialize, PartialEq, Clone, Debug)] +pub struct ContractAddresses { + #[serde(rename = "etherSwap")] + pub ether_swap: String, + + #[serde(rename = "erc20Swap")] + pub erc20_swap: String, +} + #[derive(Deserialize, Serialize, PartialEq, Clone, Debug)] pub struct Config { #[serde(rename = "providerEndpoint")] pub(crate) provider_endpoint: String, - #[serde(rename = "etherSwapAddress")] - pub ether_swap_address: String, + #[serde(rename = "contracts")] + pub(crate) contracts: Vec, +} + +#[tonic::async_trait] +pub trait RefundSigner { + fn version_for_address(&self, contract_address: &Address) -> anyhow::Result; - #[serde(rename = "erc20SwapAddress")] - pub erc20_swap_address: String, + async fn sign_cooperative_refund( + &self, + contract_version: u8, + preimage_hash: FixedBytes<32>, + amount: U256, + token_address: Option
, + timeout: u64, + ) -> anyhow::Result; } diff --git a/boltzr/src/evm/refund_signer.rs b/boltzr/src/evm/refund_signer.rs index 8379a0c7..85d7874f 100644 --- a/boltzr/src/evm/refund_signer.rs +++ b/boltzr/src/evm/refund_signer.rs @@ -1,20 +1,21 @@ -use alloy::primitives::{Address, FixedBytes, U256}; +use alloy::primitives::{Address, FixedBytes, Signature, U256}; use alloy::providers::fillers::{ BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller, WalletFiller, }; use alloy::providers::network::{AnyNetwork, EthereumWallet}; -use alloy::providers::{Provider, ProviderBuilder, RootProvider}; -use alloy::signers::local::coins_bip39::English; -use alloy::signers::local::{MnemonicBuilder, PrivateKeySigner}; -use alloy::signers::{Signature, Signer}; +use alloy::providers::RootProvider; +use alloy::signers::local::PrivateKeySigner; +use alloy::signers::Signer; use alloy::sol_types::SolStruct; -use std::error::Error; -use std::fs; -use tracing::{debug, info, instrument}; +use anyhow::anyhow; +use tracing::info; use crate::evm::contracts::erc20_swap::ERC20SwapContract; use crate::evm::contracts::ether_swap::EtherSwapContract; -use crate::evm::contracts::{erc20_swap, ether_swap}; +use crate::evm::contracts::{erc20_swap, ether_swap, SwapContract}; + +const MIN_VERSION: u8 = 3; +const MAX_VERSION: u8 = 4; type AlloyTransport = alloy_transport_http::Http; type AlloyProvider = FillProvider< @@ -30,78 +31,62 @@ type AlloyProvider = FillProvider< AnyNetwork, >; -#[tonic::async_trait] -pub trait RefundSigner { - async fn sign( - &self, - preimage_hash: FixedBytes<32>, - amount: U256, - token_address: Option
, - timeout: u64, - ) -> Result>; -} - pub struct LocalRefundSigner { - signer: PrivateKeySigner, + version: u8, ether_swap: EtherSwapContract, erc20_swap: ERC20SwapContract, } impl LocalRefundSigner { - pub async fn new_mnemonic_file( - mnemonic_path: String, - config: &crate::evm::Config, - ) -> Result> { - let mnemonic = fs::read_to_string(mnemonic_path)?; - debug!("Read mnemonic"); - - let signer = MnemonicBuilder::::default() - .phrase(mnemonic.strip_suffix("\n").unwrap_or(mnemonic.as_str())) - .index(0)? - .build()?; - - Self::new(signer, config).await - } - - #[instrument(name = "RefundSigner::new", skip_all)] pub async fn new( - signer: PrivateKeySigner, - config: &crate::evm::Config, - ) -> Result> { - info!("Using address: {}", signer.address()); - - let provider = ProviderBuilder::new() - .network::() - .with_recommended_fillers() - .wallet(EthereumWallet::from(signer.clone())) - .on_http(config.provider_endpoint.parse()?); + provider: AlloyProvider, + config: &crate::evm::ContractAddresses, + ) -> anyhow::Result { + let (ether_swap, erc20_swap) = match tokio::try_join!( + EtherSwapContract::new(config.ether_swap.parse()?, provider.clone()), + ERC20SwapContract::new(config.erc20_swap.parse()?, provider) + ) { + Ok(res) => res, + Err(err) => return Err(anyhow!("{}", err)), + }; - let chain_id = provider.get_chain_id().await?; - info!("Connected to EVM chain with id: {}", chain_id); + if ether_swap.version() != erc20_swap.version() { + return Err(anyhow::anyhow!( + "EtherSwap and ERC20Swap contracts have different versions" + )); + } - let (ether_swap, erc20_swap) = tokio::try_join!( - EtherSwapContract::new(config.ether_swap_address.parse()?, provider.clone()), - ERC20SwapContract::new(config.erc20_swap_address.parse()?, provider) - )?; + if ether_swap.version() < MIN_VERSION || ether_swap.version() > MAX_VERSION { + return Err(anyhow::anyhow!( + "unsupported contract version {}", + ether_swap.version() + )); + } Ok(LocalRefundSigner { - signer, + version: ether_swap.version(), ether_swap, erc20_swap, }) } -} -#[tonic::async_trait] -impl RefundSigner for LocalRefundSigner { - async fn sign( + pub fn addresses(&self) -> (&Address, &Address) { + (self.ether_swap.address(), self.erc20_swap.address()) + } + + pub fn version(&self) -> u8 { + self.version + } + + pub async fn sign( &self, + signer: &PrivateKeySigner, preimage_hash: FixedBytes<32>, amount: U256, token_address: Option
, timeout: u64, - ) -> Result> { + ) -> anyhow::Result { info!( "Signing cooperative {} refund", if token_address.is_none() { @@ -116,7 +101,7 @@ impl RefundSigner for LocalRefundSigner { amount, preimageHash: preimage_hash, tokenAddress: token_address, - claimAddress: self.signer.address(), + claimAddress: signer.address(), timeout: U256::from(timeout), } .eip712_signing_hash(self.erc20_swap.eip712_domain()) @@ -124,13 +109,13 @@ impl RefundSigner for LocalRefundSigner { ether_swap::Refund { amount, preimageHash: preimage_hash, - claimAddress: self.signer.address(), + claimAddress: signer.address(), timeout: U256::from(timeout), } .eip712_signing_hash(self.ether_swap.eip712_domain()) }; - let sig = self.signer.sign_hash(&hash).await?; + let sig = signer.sign_hash(&hash).await?; Ok(Signature::from_bytes_and_parity(&sig.as_bytes(), sig.v())?) } } @@ -139,8 +124,9 @@ impl RefundSigner for LocalRefundSigner { pub mod test { use crate::evm::contracts::erc20_swap::ERC20Swap; use crate::evm::contracts::ether_swap::EtherSwap; - use crate::evm::refund_signer::{AlloyProvider, LocalRefundSigner, RefundSigner}; - use crate::evm::Config; + use crate::evm::contracts::SwapContract; + use crate::evm::refund_signer::{AlloyProvider, LocalRefundSigner}; + use crate::evm::ContractAddresses; use alloy::primitives::{Address, FixedBytes, U256}; use alloy::providers::network::{AnyNetwork, EthereumWallet, ReceiptResponse}; use alloy::providers::{Provider, ProviderBuilder}; @@ -149,11 +135,9 @@ pub mod test { use alloy::sol; use rand::Rng; use serial_test::serial; - use std::fs; - use std::path::Path; - const MNEMONIC: &str = "test test test test test test test test test test test junk"; - const PROVIDER: &str = "http://127.0.0.1:8545"; + pub const MNEMONIC: &str = "test test test test test test test test test test test junk"; + pub const PROVIDER: &str = "http://127.0.0.1:8545"; pub const ETHER_SWAP_ADDRESS: &str = "0x5FbDB2315678afecb367f032d93F642f64180aa3"; pub const ERC20_SWAP_ADDRESS: &str = "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512"; @@ -167,39 +151,24 @@ pub mod test { ); #[tokio::test] - async fn test_new_mnemonic_file() { - let config = Config { - provider_endpoint: PROVIDER.to_string(), - ether_swap_address: ETHER_SWAP_ADDRESS.to_string(), - erc20_swap_address: ERC20_SWAP_ADDRESS.to_string(), - }; - let expected_address = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" - .parse::
() - .unwrap(); - - let mnemonic_file = Path::new(env!("CARGO_MANIFEST_DIR")).join("mnemonic"); - fs::write(mnemonic_file.clone(), MNEMONIC).unwrap(); + async fn test_addresses() { + let (_, _, signer, _) = setup().await; - let signer = LocalRefundSigner::new_mnemonic_file( - mnemonic_file.to_str().unwrap().to_string(), - &config, - ) - .await - .unwrap(); - assert_eq!(signer.signer.address(), expected_address); - - // With a trailing newline - fs::write(mnemonic_file.clone(), format!("{}\n", MNEMONIC)).unwrap(); + assert_eq!( + signer.addresses().0, + ÐER_SWAP_ADDRESS.parse::
().unwrap() + ); + assert_eq!( + signer.addresses().1, + &ERC20_SWAP_ADDRESS.parse::
().unwrap() + ); + } - let signer = LocalRefundSigner::new_mnemonic_file( - mnemonic_file.to_str().unwrap().to_string(), - &config, - ) - .await - .unwrap(); - assert_eq!(signer.signer.address(), expected_address); + #[tokio::test] + async fn test_version() { + let (_, _, signer, _) = setup().await; - fs::remove_file(mnemonic_file.clone()).unwrap(); + assert_eq!(signer.version(), signer.ether_swap.version()); } #[tokio::test] @@ -225,7 +194,10 @@ pub mod test { .await .unwrap(); - let refund_sig = signer.sign(preimage_hash, amount, None, 1).await.unwrap(); + let refund_sig = signer + .sign(&claim_keys, preimage_hash, amount, None, 1) + .await + .unwrap(); let refund_tx_hash = contract .refundCooperative( @@ -309,7 +281,13 @@ pub mod test { .unwrap(); let refund_sig = signer - .sign(preimage_hash, amount, Some(*token.address()), 1) + .sign( + &claim_keys, + preimage_hash, + amount, + Some(*token.address()), + 1, + ) .await .unwrap(); @@ -348,11 +326,14 @@ pub mod test { let mnemonic_builder = MnemonicBuilder::::default().phrase(MNEMONIC); let claim_keys = mnemonic_builder.clone().index(0).unwrap().build().unwrap(); let signer = LocalRefundSigner::new( - claim_keys.clone(), - &Config { - provider_endpoint: PROVIDER.to_string(), - ether_swap_address: ETHER_SWAP_ADDRESS.to_string(), - erc20_swap_address: ERC20_SWAP_ADDRESS.to_string(), + ProviderBuilder::new() + .network::() + .with_recommended_fillers() + .wallet(EthereumWallet::from(claim_keys.clone())) + .on_http(PROVIDER.parse().unwrap()), + &ContractAddresses { + ether_swap: ETHER_SWAP_ADDRESS.to_string(), + erc20_swap: ERC20_SWAP_ADDRESS.to_string(), }, ) .await diff --git a/boltzr/src/grpc/server.rs b/boltzr/src/grpc/server.rs index 8c0344c9..e92bc966 100644 --- a/boltzr/src/grpc/server.rs +++ b/boltzr/src/grpc/server.rs @@ -1,6 +1,6 @@ use crate::api::ws::types::SwapStatus; use crate::db::helpers::web_hook::WebHookHelper; -use crate::evm::refund_signer::RefundSigner; +use crate::evm::RefundSigner; use crate::grpc::service::boltzr::boltz_r_server::BoltzRServer; use crate::grpc::service::BoltzService; use crate::grpc::status_fetcher::StatusFetcher; @@ -156,7 +156,7 @@ mod server_test { use crate::db::helpers::web_hook::WebHookHelper; use crate::db::helpers::QueryResponse; use crate::db::models::{WebHook, WebHookState}; - use crate::evm::refund_signer::RefundSigner; + use crate::evm::RefundSigner; use crate::grpc::server::{Config, Server}; use crate::grpc::service::boltzr::boltz_r_client::BoltzRClient; use crate::grpc::service::boltzr::GetInfoRequest; @@ -168,7 +168,6 @@ mod server_test { use async_trait::async_trait; use mockall::{mock, predicate::*}; use std::collections::HashMap; - use std::error::Error; use std::fs; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -198,13 +197,16 @@ mod server_test { #[async_trait] impl RefundSigner for RefundSigner { - async fn sign( + fn version_for_address(&self, contract_address: &Address) -> anyhow::Result; + + async fn sign_cooperative_refund( &self, + contract_version: u8, preimage_hash: FixedBytes<32>, amount: U256, token_address: Option
, timeout: u64, - ) -> Result>; + ) -> anyhow::Result; } } diff --git a/boltzr/src/grpc/service.rs b/boltzr/src/grpc/service.rs index 224bebd5..1770675a 100644 --- a/boltzr/src/grpc/service.rs +++ b/boltzr/src/grpc/service.rs @@ -1,9 +1,10 @@ use crate::api::ws::types::SwapStatus; use crate::db::helpers::web_hook::WebHookHelper; use crate::db::models::{WebHook, WebHookState}; -use crate::evm::refund_signer::RefundSigner; +use crate::evm::RefundSigner; use crate::grpc::service::boltzr::boltz_r_server::BoltzR; use crate::grpc::service::boltzr::scan_mempool_response::Transactions; +use crate::grpc::service::boltzr::sign_evm_refund_request::Contract; use crate::grpc::service::boltzr::{ bolt11_invoice, bolt12_invoice, decode_invoice_or_offer_response, Bolt11Invoice, Bolt12Invoice, Bolt12Offer, CreateWebHookRequest, CreateWebHookResponse, DecodeInvoiceOrOfferRequest, @@ -395,8 +396,40 @@ where None => None, }; + let contract_version = match params.contract { + Some(contract) => match contract { + Contract::Address(address) => match address.parse::
() { + Ok(address) => match refund_signer.version_for_address(&address) { + Ok(version) => version, + Err(err) => { + return Err(Status::new( + Code::NotFound, + format!("no refund signer for contract: {}", err), + )); + } + }, + Err(err) => { + return Err(Status::new( + Code::InvalidArgument, + format!("could not parse contract address: {}", err), + )); + } + }, + Contract::Version(version) => version as u8, + }, + None => { + return Err(Status::new(Code::InvalidArgument, "contract not specified")); + } + }; + let signature = match refund_signer - .sign(preimage_hash, amount, token_address, params.timeout) + .sign_cooperative_refund( + contract_version, + preimage_hash, + amount, + token_address, + params.timeout, + ) .await { Ok(res) => res, @@ -625,8 +658,9 @@ mod test { use crate::db::helpers::web_hook::WebHookHelper; use crate::db::helpers::QueryResponse; use crate::db::models::{WebHook, WebHookState}; - use crate::evm::refund_signer::RefundSigner; + use crate::evm::RefundSigner; use crate::grpc::service::boltzr::boltz_r_server::BoltzR; + use crate::grpc::service::boltzr::sign_evm_refund_request::Contract; use crate::grpc::service::boltzr::{ CreateWebHookRequest, CreateWebHookResponse, GetInfoRequest, GetInfoResponse, SendWebHookRequest, SendWebHookResponse, SignEvmRefundRequest, StartWebHookRetriesRequest, @@ -639,12 +673,11 @@ mod test { use crate::tracing_setup::ReloadHandler; use crate::webhook::caller::{Caller, Config}; use alloy::primitives::{Address, FixedBytes, Signature, U256}; - use alloy::signers::k256; + use anyhow::anyhow; use async_trait::async_trait; use mockall::mock; use rand::Rng; use std::collections::HashMap; - use std::error::Error; use std::str::FromStr; use std::sync::Arc; use tokio_util::sync::CancellationToken; @@ -671,13 +704,16 @@ mod test { #[async_trait] impl RefundSigner for RefundSigner { - async fn sign( + fn version_for_address(&self, contract_address: &Address) -> anyhow::Result; + + async fn sign_cooperative_refund( &self, + contract_version: u8, preimage_hash: FixedBytes<32>, amount: U256, token_address: Option
, timeout: u64, - ) -> Result>; + ) -> anyhow::Result; } } @@ -828,13 +864,16 @@ mod test { amount: "321".to_string(), token_address: Some("0xB65828B4729754fD7d3ce72344DAF00fC3F5E06B".to_string()), timeout: 123, + contract: Some(Contract::Version(4)), }; let mut signer = MockRefundSigner::new(); let sig_str = "0xd247cfedc0c62ea93f4f3093a3b2941c329773f140ab0cdc04a641376982d34e0aa7152cb2dd9036fad543646a3fdc8b22c8d83e62e13684d61f630afdd08b0f1c"; signer - .expect_sign() - .returning(|_, _, _, _| Ok(alloy::signers::Signature::from_str(sig_str).unwrap())); + .expect_sign_cooperative_refund() + .returning( + |_, _, _, _, _| Ok(alloy::primitives::Signature::from_str(sig_str).unwrap()), + ); svc.refund_signer = Some(Arc::new(signer)); let res = svc @@ -864,17 +903,18 @@ mod test { amount: "321".to_string(), token_address: Some("0xB65828B4729754fD7d3ce72344DAF00fC3F5E06B".to_string()), timeout: 123, + contract: Some(Contract::Version(4)), }; let mut signer = MockRefundSigner::new(); signer - .expect_sign() - .returning(|_, _, _, _| Err(Box::new(k256::ecdsa::Error::new()))); + .expect_sign_cooperative_refund() + .returning(|_, _, _, _, _| Err(anyhow!("fail"))); svc.refund_signer = Some(Arc::new(signer)); let err = svc.sign_evm_refund(Request::new(req)).await.err().unwrap(); assert_eq!(err.code(), Code::Internal); - assert_eq!(err.message(), "signing failed: signature error"); + assert_eq!(err.message(), "signing failed: fail"); } #[tokio::test] @@ -888,6 +928,7 @@ mod test { amount: "".to_string(), token_address: None, timeout: 0, + contract: Some(Contract::Version(4)), })) .await .err() @@ -909,6 +950,7 @@ mod test { amount: "".to_string(), token_address: None, timeout: 0, + contract: Some(Contract::Version(4)), })) .await .err() @@ -933,6 +975,7 @@ mod test { amount: "-1".to_string(), token_address: None, timeout: 0, + contract: Some(Contract::Version(4)), })) .await .err() @@ -957,6 +1000,7 @@ mod test { amount: "21".to_string(), token_address: Some("clearly not an address".to_string()), timeout: 0, + contract: Some(Contract::Version(4)), })) .await .err() diff --git a/boltzr/src/main.rs b/boltzr/src/main.rs index 5be7b494..604bc4bc 100644 --- a/boltzr/src/main.rs +++ b/boltzr/src/main.rs @@ -89,7 +89,7 @@ async fn main() { // TODO: move to currencies let refund_signer = if let Some(rsk_config) = config.rsk { Some( - evm::refund_signer::LocalRefundSigner::new_mnemonic_file( + evm::manager::Manager::from_mnemonic_file( config.mnemonic_path_evm.unwrap(), &rsk_config, ) diff --git a/lib/Config.ts b/lib/Config.ts index 5534909c..9b324424 100644 --- a/lib/Config.ts +++ b/lib/Config.ts @@ -104,12 +104,16 @@ type EthProviderServiceConfig = { apiKey: string; }; +type ContractsConfig = { + etherSwap: string; + erc20Swap: string; +}; + type RskConfig = { networkName?: string; providerEndpoint: string; - etherSwapAddress: string; - erc20SwapAddress: string; + contracts: ContractsConfig[]; tokens: TokenConfig[]; }; @@ -407,8 +411,7 @@ class Config { network: 'rinkeby', }, - etherSwapAddress: '', - erc20SwapAddress: '', + contracts: [], tokens: [], }, @@ -531,6 +534,7 @@ export { EthereumConfig, PostgresConfig, CurrencyConfig, + ContractsConfig, PreferredWallet, OverPaymentConfig, LiquidChainConfig, diff --git a/lib/cli/ethereum/EthereumUtils.ts b/lib/cli/ethereum/EthereumUtils.ts index 8c179c37..a3ec9c27 100644 --- a/lib/cli/ethereum/EthereumUtils.ts +++ b/lib/cli/ethereum/EthereumUtils.ts @@ -55,11 +55,11 @@ export const getContracts = ( Object.entries({ etherSwap: { abi: ContractABIs.EtherSwap, - address: config.etherSwapAddress, + address: config.contracts[0].etherSwap, }, erc20Swap: { abi: ContractABIs.ERC20Swap, - address: config.erc20SwapAddress, + address: config.contracts[0].erc20Swap, }, token: { abi: ContractABIs.ERC20, diff --git a/lib/cli/ethereum/commands/Claim.ts b/lib/cli/ethereum/commands/Claim.ts index 34e1c7ac..aa1d882f 100644 --- a/lib/cli/ethereum/commands/Claim.ts +++ b/lib/cli/ethereum/commands/Claim.ts @@ -5,7 +5,7 @@ import { getHexBuffer } from '../../../Utils'; import { queryERC20SwapValues, queryEtherSwapValues, -} from '../../../wallet/ethereum/ContractUtils'; +} from '../../../wallet/ethereum/contracts/ContractUtils'; import BuilderComponents from '../../BuilderComponents'; import { connectEthereum, diff --git a/lib/cli/ethereum/commands/Refund.ts b/lib/cli/ethereum/commands/Refund.ts index 9c9d7f7c..4c53f9c4 100644 --- a/lib/cli/ethereum/commands/Refund.ts +++ b/lib/cli/ethereum/commands/Refund.ts @@ -4,7 +4,7 @@ import { getHexBuffer } from '../../../Utils'; import { queryERC20SwapValues, queryEtherSwapValues, -} from '../../../wallet/ethereum/ContractUtils'; +} from '../../../wallet/ethereum/contracts/ContractUtils'; import BuilderComponents from '../../BuilderComponents'; import { connectEthereum, diff --git a/lib/proto/sidecar/boltzr_pb.d.ts b/lib/proto/sidecar/boltzr_pb.d.ts index bcbf03f9..c3d74c81 100644 --- a/lib/proto/sidecar/boltzr_pb.d.ts +++ b/lib/proto/sidecar/boltzr_pb.d.ts @@ -483,6 +483,18 @@ export class SignEvmRefundRequest extends jspb.Message { getTimeout(): number; setTimeout(value: number): SignEvmRefundRequest; + hasAddress(): boolean; + clearAddress(): void; + getAddress(): string; + setAddress(value: string): SignEvmRefundRequest; + + hasVersion(): boolean; + clearVersion(): void; + getVersion(): number; + setVersion(value: number): SignEvmRefundRequest; + + getContractCase(): SignEvmRefundRequest.ContractCase; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): SignEvmRefundRequest.AsObject; static toObject(includeInstance: boolean, msg: SignEvmRefundRequest): SignEvmRefundRequest.AsObject; @@ -499,7 +511,16 @@ export namespace SignEvmRefundRequest { amount: string, tokenAddress?: string, timeout: number, + address: string, + version: number, } + + export enum ContractCase { + CONTRACT_NOT_SET = 0, + ADDRESS = 5, + VERSION = 6, + } + } export class SignEvmRefundResponse extends jspb.Message { diff --git a/lib/proto/sidecar/boltzr_pb.js b/lib/proto/sidecar/boltzr_pb.js index 9cd6a20a..44a319d2 100644 --- a/lib/proto/sidecar/boltzr_pb.js +++ b/lib/proto/sidecar/boltzr_pb.js @@ -51,6 +51,7 @@ goog.exportSymbol('proto.boltzr.SendWebHookResponse', null, global); goog.exportSymbol('proto.boltzr.SetLogLevelRequest', null, global); goog.exportSymbol('proto.boltzr.SetLogLevelResponse', null, global); goog.exportSymbol('proto.boltzr.SignEvmRefundRequest', null, global); +goog.exportSymbol('proto.boltzr.SignEvmRefundRequest.ContractCase', null, global); goog.exportSymbol('proto.boltzr.SignEvmRefundResponse', null, global); goog.exportSymbol('proto.boltzr.StartWebHookRetriesRequest', null, global); goog.exportSymbol('proto.boltzr.StartWebHookRetriesResponse', null, global); @@ -491,7 +492,7 @@ if (goog.DEBUG && !COMPILED) { * @constructor */ proto.boltzr.SignEvmRefundRequest = function(opt_data) { - jspb.Message.initialize(this, opt_data, 0, -1, null, null); + jspb.Message.initialize(this, opt_data, 0, -1, null, proto.boltzr.SignEvmRefundRequest.oneofGroups_); }; goog.inherits(proto.boltzr.SignEvmRefundRequest, jspb.Message); if (goog.DEBUG && !COMPILED) { @@ -3926,6 +3927,32 @@ proto.boltzr.SendWebHookResponse.prototype.setOk = function(value) { +/** + * Oneof group definitions for this message. Each group defines the field + * numbers belonging to that group. When of these fields' value is set, all + * other fields in the group are cleared. During deserialization, if multiple + * fields are encountered for a group, only the last value seen will be kept. + * @private {!Array>} + * @const + */ +proto.boltzr.SignEvmRefundRequest.oneofGroups_ = [[5,6]]; + +/** + * @enum {number} + */ +proto.boltzr.SignEvmRefundRequest.ContractCase = { + CONTRACT_NOT_SET: 0, + ADDRESS: 5, + VERSION: 6 +}; + +/** + * @return {proto.boltzr.SignEvmRefundRequest.ContractCase} + */ +proto.boltzr.SignEvmRefundRequest.prototype.getContractCase = function() { + return /** @type {proto.boltzr.SignEvmRefundRequest.ContractCase} */(jspb.Message.computeOneofCase(this, proto.boltzr.SignEvmRefundRequest.oneofGroups_[0])); +}; + if (jspb.Message.GENERATE_TO_OBJECT) { @@ -3960,7 +3987,9 @@ proto.boltzr.SignEvmRefundRequest.toObject = function(includeInstance, msg) { preimageHash: msg.getPreimageHash_asB64(), amount: jspb.Message.getFieldWithDefault(msg, 2, ""), tokenAddress: jspb.Message.getFieldWithDefault(msg, 3, ""), - timeout: jspb.Message.getFieldWithDefault(msg, 4, 0) + timeout: jspb.Message.getFieldWithDefault(msg, 4, 0), + address: jspb.Message.getFieldWithDefault(msg, 5, ""), + version: jspb.Message.getFieldWithDefault(msg, 6, 0) }; if (includeInstance) { @@ -4013,6 +4042,14 @@ proto.boltzr.SignEvmRefundRequest.deserializeBinaryFromReader = function(msg, re var value = /** @type {number} */ (reader.readUint64()); msg.setTimeout(value); break; + case 5: + var value = /** @type {string} */ (reader.readString()); + msg.setAddress(value); + break; + case 6: + var value = /** @type {number} */ (reader.readUint64()); + msg.setVersion(value); + break; default: reader.skipField(); break; @@ -4070,6 +4107,20 @@ proto.boltzr.SignEvmRefundRequest.serializeBinaryToWriter = function(message, wr f ); } + f = /** @type {string} */ (jspb.Message.getField(message, 5)); + if (f != null) { + writer.writeString( + 5, + f + ); + } + f = /** @type {number} */ (jspb.Message.getField(message, 6)); + if (f != null) { + writer.writeUint64( + 6, + f + ); + } }; @@ -4187,6 +4238,78 @@ proto.boltzr.SignEvmRefundRequest.prototype.setTimeout = function(value) { }; +/** + * optional string address = 5; + * @return {string} + */ +proto.boltzr.SignEvmRefundRequest.prototype.getAddress = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 5, "")); +}; + + +/** + * @param {string} value + * @return {!proto.boltzr.SignEvmRefundRequest} returns this + */ +proto.boltzr.SignEvmRefundRequest.prototype.setAddress = function(value) { + return jspb.Message.setOneofField(this, 5, proto.boltzr.SignEvmRefundRequest.oneofGroups_[0], value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.boltzr.SignEvmRefundRequest} returns this + */ +proto.boltzr.SignEvmRefundRequest.prototype.clearAddress = function() { + return jspb.Message.setOneofField(this, 5, proto.boltzr.SignEvmRefundRequest.oneofGroups_[0], undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.boltzr.SignEvmRefundRequest.prototype.hasAddress = function() { + return jspb.Message.getField(this, 5) != null; +}; + + +/** + * optional uint64 version = 6; + * @return {number} + */ +proto.boltzr.SignEvmRefundRequest.prototype.getVersion = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 6, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.boltzr.SignEvmRefundRequest} returns this + */ +proto.boltzr.SignEvmRefundRequest.prototype.setVersion = function(value) { + return jspb.Message.setOneofField(this, 6, proto.boltzr.SignEvmRefundRequest.oneofGroups_[0], value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.boltzr.SignEvmRefundRequest} returns this + */ +proto.boltzr.SignEvmRefundRequest.prototype.clearVersion = function() { + return jspb.Message.setOneofField(this, 6, proto.boltzr.SignEvmRefundRequest.oneofGroups_[0], undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.boltzr.SignEvmRefundRequest.prototype.hasVersion = function() { + return jspb.Message.getField(this, 6) != null; +}; + + diff --git a/lib/service/Renegotiator.ts b/lib/service/Renegotiator.ts index b4529e6a..0e4aa18c 100644 --- a/lib/service/Renegotiator.ts +++ b/lib/service/Renegotiator.ts @@ -18,10 +18,11 @@ import RateProvider from '../rates/RateProvider'; import ErrorsSwap from '../swap/Errors'; import SwapNursery from '../swap/SwapNursery'; import WalletManager, { Currency } from '../wallet/WalletManager'; +import EthereumErrors from '../wallet/ethereum/Errors'; import { formatERC20SwapValues, formatEtherSwapValues, -} from '../wallet/ethereum/ContractUtils'; +} from '../wallet/ethereum/contracts/ContractUtils'; import BalanceCheck from './BalanceCheck'; import Errors from './Errors'; import TimeoutDeltaProvider from './TimeoutDeltaProvider'; @@ -107,11 +108,15 @@ class Renegotiator { } const isEther = receivingCurrency.type === CurrencyType.Ether; + const contracts = await nursery.ethereumManager.contractsForAddress( + swap.receivingData.lockupAddress, + ); + if (contracts === undefined) { + throw EthereumErrors.UNSUPPORTED_CONTRACT(); + } const topicHash = ( - isEther - ? nursery.ethereumManager.contractHandler.etherSwap - : nursery.ethereumManager.contractHandler.erc20Swap + isEther ? contracts.etherSwap : contracts.erc20Swap ).interface.getEvent('Lockup').topicHash; const lockupEvent = receipt.logs.find( (log) => log.topics.length > 0 && log.topics[0] === topicHash, @@ -121,12 +126,11 @@ class Renegotiator { } if (isEther) { - const values = - nursery.ethereumManager.contractHandler.etherSwap.interface.decodeEventLog( - 'Lockup', - lockupEvent.data, - lockupEvent.topics, - ); + const values = contracts.etherSwap.interface.decodeEventLog( + 'Lockup', + lockupEvent.data, + lockupEvent.topics, + ); await nursery.checkEtherSwapLockup( await ChainSwapRepository.setExpectedAmounts( @@ -139,12 +143,11 @@ class Renegotiator { formatEtherSwapValues(values), ); } else { - const values = - nursery.ethereumManager.contractHandler.erc20Swap.interface.decodeEventLog( - 'Lockup', - lockupEvent.data, - lockupEvent.topics, - ); + const values = contracts.erc20Swap.interface.decodeEventLog( + 'Lockup', + lockupEvent.data, + lockupEvent.topics, + ); await nursery.checkErc20SwapLockup( await ChainSwapRepository.setExpectedAmounts( diff --git a/lib/service/cooperative/DeferredClaimer.ts b/lib/service/cooperative/DeferredClaimer.ts index 53aa3ded..55bf526c 100644 --- a/lib/service/cooperative/DeferredClaimer.ts +++ b/lib/service/cooperative/DeferredClaimer.ts @@ -35,7 +35,8 @@ import WalletManager, { Currency } from '../../wallet/WalletManager'; import { queryERC20SwapValuesFromLock, queryEtherSwapValuesFromLock, -} from '../../wallet/ethereum/ContractUtils'; +} from '../../wallet/ethereum/contracts/ContractUtils'; +import Contracts from '../../wallet/ethereum/contracts/Contracts'; import ERC20WalletProvider from '../../wallet/providers/ERC20WalletProvider'; import Errors from '../Errors'; import TimeoutDeltaProvider from '../TimeoutDeltaProvider'; @@ -161,7 +162,7 @@ class DeferredClaimer extends CoopSignerBase< ): Promise => { const { base, quote } = splitPairId(swap.pair); const chainCurrency = getChainCurrency(base, quote, swap.orderSide, false); - if (!this.shouldBeDeferred(chainCurrency, swap)) { + if (!(await this.shouldBeDeferred(chainCurrency, swap))) { return false; } @@ -345,19 +346,20 @@ class DeferredClaimer extends CoopSignerBase< case CurrencyType.Ether: { const manager = this.getEthereumManager(symbol); + const contracts = manager.highestContractsVersion(); const swapValues: EtherSwapValues[] = []; for (const swap of swaps) { swapValues.push( await queryEtherSwapValuesFromLock( manager.provider, - manager.etherSwap, + contracts.etherSwap, swap.swap.lockupTransactionId!, ), ); } - const tx = await manager.contractHandler.claimBatchEther( + const tx = await contracts.contractHandler.claimBatchEther( swaps.map((s) => s.swap.id), swaps .map((s, i) => ({ swap: s, values: swapValues[i] })) @@ -377,19 +379,20 @@ class DeferredClaimer extends CoopSignerBase< case CurrencyType.ERC20: { const manager = this.getEthereumManager(symbol); + const contracts = manager.highestContractsVersion(); const swapValues: ERC20SwapValues[] = []; for (const swap of swaps) { swapValues.push( await queryERC20SwapValuesFromLock( manager.provider, - manager.erc20Swap, + contracts.erc20Swap, swap.swap.lockupTransactionId!, ), ); } - const tx = await manager.contractHandler.claimBatchToken( + const tx = await contracts.contractHandler.claimBatchToken( swaps.map((s) => s.swap.id), this.walletManager.wallets.get(symbol)! .walletProvider as ERC20WalletProvider, @@ -434,7 +437,7 @@ class DeferredClaimer extends CoopSignerBase< return swaps.map((toClaim) => toClaim.swap.id); }; - private shouldBeDeferred = (chainCurrency: string, swap: Swap) => { + private shouldBeDeferred = async (chainCurrency: string, swap: Swap) => { if (!this.config.deferredClaimSymbols.includes(chainCurrency)) { this.logNotDeferringReason( swap, @@ -448,6 +451,22 @@ class DeferredClaimer extends CoopSignerBase< return false; } + const currency = this.currencies.get(chainCurrency)!; + if ( + currency.type === CurrencyType.Ether || + currency.type === CurrencyType.ERC20 + ) { + const manager = this.getEthereumManager(chainCurrency); + const contracts = (await manager.contractsForAddress( + swap.lockupAddress, + ))!; + + if (contracts.version !== Contracts.maxVersion) { + this.logNotDeferringReason(swap, 'not using the latest contracts'); + return false; + } + } + return true; }; diff --git a/lib/service/cooperative/EipSigner.ts b/lib/service/cooperative/EipSigner.ts index 7ee91766..7e388d2a 100644 --- a/lib/service/cooperative/EipSigner.ts +++ b/lib/service/cooperative/EipSigner.ts @@ -72,7 +72,13 @@ class EipSigner { ? (swap as Swap).onchainAmount : (swap as ChainSwapInfo).receivingData.amount; + const contractAddress = + swap.type === SwapType.Submarine + ? (swap as Swap).lockupAddress + : (swap as ChainSwapInfo).receivingData.lockupAddress; + const sidecarRes = await this.sidecar.signEvmRefund( + contractAddress, getHexBuffer(swap.preimageHash), isEtherSwap ? BigInt(onchainAmount!) * etherDecimals diff --git a/lib/sidecar/Sidecar.ts b/lib/sidecar/Sidecar.ts index 83f085fa..c5a1702a 100644 --- a/lib/sidecar/Sidecar.ts +++ b/lib/sidecar/Sidecar.ts @@ -256,12 +256,14 @@ class Sidecar extends BaseClient< }; public signEvmRefund = async ( + contractAddress: string, preimageHash: Buffer, amount: bigint, tokenAddress: string | undefined, timeout: number, ) => { const req = new sidecarrpc.SignEvmRefundRequest(); + req.setAddress(contractAddress); req.setPreimageHash(preimageHash); req.setAmount(amount.toString()); req.setTimeout(timeout); diff --git a/lib/swap/SwapManager.ts b/lib/swap/SwapManager.ts index bc12d760..a865ba28 100644 --- a/lib/swap/SwapManager.ts +++ b/lib/swap/SwapManager.ts @@ -1335,10 +1335,11 @@ class SwapManager { const ethereumManager = this.walletManager.ethereumManagers.find( (manager) => manager.hasSymbol(symbol), )!; + const contracts = ethereumManager.highestContractsVersion(); return type === CurrencyType.Ether - ? ethereumManager.etherSwap.getAddress() - : ethereumManager.erc20Swap.getAddress(); + ? contracts.etherSwap.getAddress() + : contracts.erc20Swap.getAddress(); }; } diff --git a/lib/swap/SwapNursery.ts b/lib/swap/SwapNursery.ts index a500eba6..3600f021 100644 --- a/lib/swap/SwapNursery.ts +++ b/lib/swap/SwapNursery.ts @@ -69,12 +69,13 @@ import DeferredClaimer from '../service/cooperative/DeferredClaimer'; import Sidecar from '../sidecar/Sidecar'; import Wallet from '../wallet/Wallet'; import WalletManager, { Currency } from '../wallet/WalletManager'; -import ContractHandler from '../wallet/ethereum/ContractHandler'; +import EthereumManager from '../wallet/ethereum/EthereumManager'; +import ContractHandler from '../wallet/ethereum/contracts/ContractHandler'; import { queryERC20SwapValuesFromLock, queryEtherSwapValuesFromLock, -} from '../wallet/ethereum/ContractUtils'; -import EthereumManager from '../wallet/ethereum/EthereumManager'; +} from '../wallet/ethereum/contracts/ContractUtils'; +import Contracts from '../wallet/ethereum/contracts/Contracts'; import ERC20WalletProvider from '../wallet/providers/ERC20WalletProvider'; import ChannelNursery from './ChannelNursery'; import Errors from './Errors'; @@ -650,13 +651,19 @@ class SwapNursery extends TypedEventEmitter { const manager = this.findEthereumNursery( currency.symbol, )!.ethereumManager; + const lockupAddress = + swap.type === SwapType.Submarine + ? (swap as Swap).lockupAddress + : (swap as ChainSwapInfo).receivingData.lockupAddress; + const contracts = (await manager.contractsForAddress(lockupAddress))!; await this.claimEther( manager, + contracts, swap, await queryEtherSwapValuesFromLock( manager.provider, - manager.etherSwap, + contracts.etherSwap, txToClaim!, ), payRes.preimage, @@ -669,13 +676,18 @@ class SwapNursery extends TypedEventEmitter { const manager = this.findEthereumNursery( currency.symbol, )!.ethereumManager; + const lockupAddress = + swap.type === SwapType.Submarine + ? (swap as Swap).lockupAddress + : (swap as ChainSwapInfo).receivingData.lockupAddress; + const contracts = (await manager.contractsForAddress(lockupAddress))!; await this.claimERC20( - manager.contractHandler, + contracts.contractHandler, swap, await queryERC20SwapValuesFromLock( manager.provider, - manager.erc20Swap, + contracts.erc20Swap, txToClaim!, ), payRes.preimage, @@ -1003,11 +1015,13 @@ class SwapNursery extends TypedEventEmitter { ) => { try { const nursery = this.findEthereumNursery(wallet.symbol)!; - const lockupDetails = swap.type === SwapType.ReverseSubmarine ? (swap as ReverseSwap) : (swap as ChainSwapInfo).sendingData; + const contracts = (await nursery.ethereumManager.contractsForAddress( + lockupDetails.lockupAddress, + ))!; let contractTransaction: ContractTransactionResponse; @@ -1016,7 +1030,7 @@ class SwapNursery extends TypedEventEmitter { (swap as ReverseSwap).minerFeeOnchainAmount ) { contractTransaction = - await nursery.ethereumManager.contractHandler.lockupEtherPrepayMinerfee( + await contracts.contractHandler.lockupEtherPrepayMinerfee( swap, getHexBuffer(swap.preimageHash), BigInt(lockupDetails.expectedAmount) * etherDecimals, @@ -1026,14 +1040,13 @@ class SwapNursery extends TypedEventEmitter { lockupDetails.timeoutBlockHeight, ); } else { - contractTransaction = - await nursery.ethereumManager.contractHandler.lockupEther( - swap, - getHexBuffer(swap.preimageHash), - BigInt(lockupDetails.expectedAmount) * etherDecimals, - lockupDetails.claimAddress!, - lockupDetails.timeoutBlockHeight, - ); + contractTransaction = await contracts.contractHandler.lockupEther( + swap, + getHexBuffer(swap.preimageHash), + BigInt(lockupDetails.expectedAmount) * etherDecimals, + lockupDetails.claimAddress!, + lockupDetails.timeoutBlockHeight, + ); } nursery.listenContractTransaction(swap, contractTransaction); @@ -1076,6 +1089,9 @@ class SwapNursery extends TypedEventEmitter { swap.type === SwapType.ReverseSubmarine ? (swap as ReverseSwap) : (swap as ChainSwapInfo).sendingData; + const contracts = (await nursery.ethereumManager.contractsForAddress( + lockupDetails.lockupAddress, + ))!; let contractTransaction: ContractTransactionResponse; @@ -1084,7 +1100,7 @@ class SwapNursery extends TypedEventEmitter { (swap as ReverseSwap).minerFeeOnchainAmount ) { contractTransaction = - await nursery.ethereumManager.contractHandler.lockupTokenPrepayMinerfee( + await contracts.contractHandler.lockupTokenPrepayMinerfee( swap, walletProvider, getHexBuffer(swap.preimageHash), @@ -1095,15 +1111,14 @@ class SwapNursery extends TypedEventEmitter { lockupDetails.timeoutBlockHeight, ); } else { - contractTransaction = - await nursery.ethereumManager.contractHandler.lockupToken( - swap, - walletProvider, - getHexBuffer(swap.preimageHash), - walletProvider.formatTokenAmount(lockupDetails.expectedAmount), - lockupDetails.claimAddress!, - lockupDetails.timeoutBlockHeight, - ); + contractTransaction = await contracts.contractHandler.lockupToken( + swap, + walletProvider, + getHexBuffer(swap.preimageHash), + walletProvider.formatTokenAmount(lockupDetails.expectedAmount), + lockupDetails.claimAddress!, + lockupDetails.timeoutBlockHeight, + ); } nursery.listenContractTransaction(swap, contractTransaction); @@ -1215,12 +1230,13 @@ class SwapNursery extends TypedEventEmitter { private claimEther = async ( manager: EthereumManager, + contracts: Contracts, swap: Swap | ChainSwapInfo, etherSwapValues: EtherSwapValues, preimage: Buffer, channelCreation?: ChannelCreation | null, ) => { - const contractTransaction = await manager.contractHandler.claimEther( + const contractTransaction = await contracts.contractHandler.claimEther( swap, preimage, etherSwapValues.amount, @@ -1563,6 +1579,11 @@ class SwapNursery extends TypedEventEmitter { chainSymbol: string, ) => { const nursery = this.findEthereumNursery(chainSymbol)!; + const contracts = (await nursery.ethereumManager.contractsForAddress( + swap.type === SwapType.ReverseSubmarine + ? (swap as ReverseSwap).lockupAddress + : (swap as ChainSwapInfo).sendingData.lockupAddress, + ))!; const lockupTransactionId = swap.type === SwapType.ReverseSubmarine @@ -1571,17 +1592,16 @@ class SwapNursery extends TypedEventEmitter { const etherSwapValues = await queryEtherSwapValuesFromLock( nursery.ethereumManager!.provider, - nursery.ethereumManager.etherSwap, + contracts.etherSwap, lockupTransactionId!, ); - const contractTransaction = - await nursery.ethereumManager.contractHandler.refundEther( - swap, - getHexBuffer(swap.preimageHash), - etherSwapValues.amount, - etherSwapValues.claimAddress, - etherSwapValues.timelock, - ); + const contractTransaction = await contracts.contractHandler.refundEther( + swap, + getHexBuffer(swap.preimageHash), + etherSwapValues.amount, + etherSwapValues.claimAddress, + etherSwapValues.timelock, + ); this.logger.info( `Refunded ${nursery.ethereumManager.networkDetails.name} of ${swapTypeToPrettyString(swap.type)} Swap ${swap.id} in: ${contractTransaction.hash}`, @@ -1602,6 +1622,12 @@ class SwapNursery extends TypedEventEmitter { chainSymbol: string, ) => { const nursery = this.findEthereumNursery(chainSymbol)!; + const contracts = (await nursery.ethereumManager.contractsForAddress( + swap.type === SwapType.ReverseSubmarine + ? (swap as ReverseSwap).lockupAddress + : (swap as ChainSwapInfo).sendingData.lockupAddress, + ))!; + const walletProvider = this.walletManager.wallets.get(chainSymbol)! .walletProvider as ERC20WalletProvider; @@ -1612,18 +1638,17 @@ class SwapNursery extends TypedEventEmitter { const erc20SwapValues = await queryERC20SwapValuesFromLock( nursery.ethereumManager.provider, - nursery.ethereumManager.erc20Swap, + contracts.erc20Swap, lockupTransactionId!, ); - const contractTransaction = - await nursery.ethereumManager.contractHandler.refundToken( - swap, - walletProvider, - getHexBuffer(swap.preimageHash), - erc20SwapValues.amount, - erc20SwapValues.claimAddress, - erc20SwapValues.timelock, - ); + const contractTransaction = await contracts.contractHandler.refundToken( + swap, + walletProvider, + getHexBuffer(swap.preimageHash), + erc20SwapValues.amount, + erc20SwapValues.claimAddress, + erc20SwapValues.timelock, + ); this.logger.info( `Refunded ${chainSymbol} of ${swapTypeToPrettyString(swap.type)} Swap ${swap.id} in: ${contractTransaction.hash}`, diff --git a/lib/wallet/ethereum/ConsolidatedEventHandler.ts b/lib/wallet/ethereum/ConsolidatedEventHandler.ts new file mode 100644 index 00000000..626e76f9 --- /dev/null +++ b/lib/wallet/ethereum/ConsolidatedEventHandler.ts @@ -0,0 +1,30 @@ +import TypedEventEmitter from '../../consts/TypedEventEmitter'; +import ContractEventHandler, { Events } from './contracts/ContractEventHandler'; + +class ConsolidatedEventHandler extends TypedEventEmitter { + private readonly handlers: ContractEventHandler[] = []; + + constructor() { + super(); + } + + public register = (handler: ContractEventHandler) => { + this.handlers.push(handler); + + handler.on('eth.lockup', (lockup) => this.emit('eth.lockup', lockup)); + handler.on('eth.claim', (claim) => this.emit('eth.claim', claim)); + handler.on('eth.refund', (refund) => this.emit('eth.refund', refund)); + + handler.on('erc20.lockup', (lockup) => this.emit('erc20.lockup', lockup)); + handler.on('erc20.claim', (claim) => this.emit('erc20.claim', claim)); + handler.on('erc20.refund', (refund) => this.emit('erc20.refund', refund)); + }; + + public rescan = async (startHeight: number): Promise => { + await Promise.all( + this.handlers.map((handler) => handler.rescan(startHeight)), + ); + }; +} + +export default ConsolidatedEventHandler; diff --git a/lib/wallet/ethereum/Errors.ts b/lib/wallet/ethereum/Errors.ts index 5cee04f2..b1e2dacd 100644 --- a/lib/wallet/ethereum/Errors.ts +++ b/lib/wallet/ethereum/Errors.ts @@ -33,4 +33,12 @@ export default { message: `requests to all providers failed:\n - ${errors.join('\n - ')}`, code: concatErrorCode(ErrorCodePrefix.Ethereum, 4), }), + NOT_SUPPORTED_BY_CONTRACT_VERSION: (): Error => ({ + message: 'not supported by contract version', + code: concatErrorCode(ErrorCodePrefix.Ethereum, 5), + }), + UNSUPPORTED_CONTRACT: (): Error => ({ + message: 'unsupported contract', + code: concatErrorCode(ErrorCodePrefix.Ethereum, 6), + }), }; diff --git a/lib/wallet/ethereum/EthereumManager.ts b/lib/wallet/ethereum/EthereumManager.ts index c8423c64..a2f6816e 100644 --- a/lib/wallet/ethereum/EthereumManager.ts +++ b/lib/wallet/ethereum/EthereumManager.ts @@ -1,7 +1,5 @@ import { ContractABIs } from 'boltz-core'; import { ERC20 } from 'boltz-core/typechain/ERC20'; -import { ERC20Swap } from 'boltz-core/typechain/ERC20Swap'; -import { EtherSwap } from 'boltz-core/typechain/EtherSwap'; import { Contract, Wallet as EthersWallet, @@ -18,11 +16,11 @@ import Errors from '../Errors'; import Wallet from '../Wallet'; import ERC20WalletProvider from '../providers/ERC20WalletProvider'; import EtherWalletProvider from '../providers/EtherWalletProvider'; -import ContractEventHandler from './ContractEventHandler'; -import ContractHandler from './ContractHandler'; +import ConsolidatedEventHandler from './ConsolidatedEventHandler'; import EthereumTransactionTracker from './EthereumTransactionTracker'; import { Ethereum, NetworkDetails, Rsk } from './EvmNetworks'; import InjectedProvider from './InjectedProvider'; +import Contracts from './contracts/Contracts'; type Network = { name: string; @@ -30,18 +28,8 @@ type Network = { }; class EthereumManager { - private static supportedContractVersions = { - EtherSwap: 4, - ERC20Swap: 4, - }; - public readonly provider: InjectedProvider; - - public readonly contractHandler: ContractHandler; - public readonly contractEventHandler: ContractEventHandler; - - public etherSwap: EtherSwap; - public erc20Swap: ERC20Swap; + public readonly contractEventHandler = new ConsolidatedEventHandler(); public signer!: Signer; public address!: string; @@ -50,6 +38,8 @@ class EthereumManager { public readonly tokenAddresses = new Map(); + private contracts: Contracts[] = []; + constructor( private readonly logger: Logger, isRsk: boolean, @@ -58,9 +48,8 @@ class EthereumManager { if ( config === null || config === undefined || - [config.etherSwapAddress, config.erc20SwapAddress].some( - (value) => value === undefined || value === '', - ) + config.contracts === undefined || + config.contracts.length === 0 ) { throw Errors.MISSING_SWAP_CONTRACTS(); } @@ -80,26 +69,6 @@ class EthereumManager { `${this.networkDetails.name} network name not configured`, ); } - - this.logger.debug( - `Using ${this.networkDetails.name} EtherSwap contract: ${this.config.etherSwapAddress}`, - ); - this.logger.debug( - `Using ${this.networkDetails.name} ERC20Swap contract: ${this.config.erc20SwapAddress}`, - ); - - this.etherSwap = new Contract( - config.etherSwapAddress, - ContractABIs.EtherSwap as any, - ) as any as EtherSwap; - - this.erc20Swap = new Contract( - config.erc20SwapAddress, - ContractABIs.ERC20Swap as any, - ) as any as ERC20Swap; - - this.contractHandler = new ContractHandler(this.networkDetails); - this.contractEventHandler = new ContractEventHandler(this.logger); } public init = async (mnemonic: string): Promise> => { @@ -115,22 +84,6 @@ class EthereumManager { this.signer = EthersWallet.fromPhrase(mnemonic).connect(this.provider); this.address = await this.signer.getAddress(); - this.etherSwap = this.etherSwap.connect(this.signer) as EtherSwap; - this.erc20Swap = this.erc20Swap.connect(this.signer) as ERC20Swap; - - await Promise.all([ - this.checkContractVersion( - `${this.networkDetails.name} EtherSwap`, - this.etherSwap, - BigInt(EthereumManager.supportedContractVersions.EtherSwap), - ), - this.checkContractVersion( - `${this.networkDetails.name} ERC20Swap`, - this.erc20Swap, - BigInt(EthereumManager.supportedContractVersions.ERC20Swap), - ), - ]); - this.logger.verbose( `Using ${this.networkDetails.name} signer: ${this.address}`, ); @@ -141,13 +94,6 @@ class EthereumManager { currentBlock, ); - this.contractHandler.init(this.provider, this.etherSwap, this.erc20Swap); - await this.contractEventHandler.init( - this.networkDetails, - this.etherSwap, - this.erc20Swap, - ); - this.logger.verbose( `${this.networkDetails.name} chain status: ${stringify({ blockNumber: currentBlock, @@ -162,6 +108,12 @@ class EthereumManager { this.signer, ); + for (const contracts of this.config.contracts) { + const c = new Contracts(this.logger, this.networkDetails, contracts); + await c.init(this.provider, this.signer, this.contractEventHandler); + this.contracts.push(c); + } + await transactionTracker.init(); await this.provider.on('block', async (blockNumber: number) => { @@ -200,7 +152,18 @@ class EthereumManager { new Wallet(this.logger, CurrencyType.ERC20, provider), ); - await this.checkERC20Allowance(provider); + let nonce = await this.signer.getNonce(); + for (const c of this.contracts) { + if ( + await this.checkERC20Allowance( + provider, + await c.erc20Swap.getAddress(), + nonce, + ) + ) { + nonce += 1; + } + } } else { throw Errors.INVALID_ETHEREUM_CONFIGURATION( `duplicate ${token.symbol} token config`, @@ -247,6 +210,8 @@ class EthereumManager { }; public getContractDetails = async () => { + const bestContracts = this.highestContractsVersion(); + return { network: { chainId: Number(this.network.chainId), @@ -254,8 +219,8 @@ class EthereumManager { }, tokens: this.tokenAddresses, swapContracts: new Map([ - ['EtherSwap', await this.etherSwap.getAddress()], - ['ERC20Swap', await this.erc20Swap.getAddress()], + ['EtherSwap', await bestContracts.etherSwap.getAddress()], + ['ERC20Swap', await bestContracts.erc20Swap.getAddress()], ]), }; }; @@ -263,44 +228,52 @@ class EthereumManager { public hasSymbol = (symbol: string): boolean => this.networkDetails.symbol === symbol || this.tokenAddresses.has(symbol); - private checkERC20Allowance = async (erc20Wallet: ERC20WalletProvider) => { - const allowance = await erc20Wallet.getAllowance( - this.config.erc20SwapAddress, + public highestContractsVersion = (): Contracts => + this.contracts.reduce( + (max, c) => (max.version > c.version ? max : c), + this.contracts[0], ); + public contractsForAddress = async (address: string) => { + for (const c of this.contracts) { + if ( + (await c.etherSwap.getAddress()) === address || + (await c.erc20Swap.getAddress()) === address + ) { + return c; + } + } + + return undefined; + }; + + private checkERC20Allowance = async ( + erc20Wallet: ERC20WalletProvider, + erc20SwapAddress: string, + nonce: number, + ) => { + const allowance = await erc20Wallet.getAllowance(erc20SwapAddress); + this.logger.debug( - `Allowance of ${erc20Wallet.symbol} is ${allowance.toString()}`, + `Allowance of ${erc20Wallet.symbol} for ${erc20SwapAddress} is ${allowance.toString()}`, ); - if (allowance == BigInt(0)) { + if (allowance == 0n) { this.logger.verbose(`Setting allowance of ${erc20Wallet.symbol}`); const { transactionId } = await erc20Wallet.approve( - this.config.erc20SwapAddress, + erc20SwapAddress, MaxUint256, + nonce, ); this.logger.info( - `Set allowance of token ${erc20Wallet.symbol}: ${transactionId}`, + `Set allowance of token ${erc20Wallet.symbol} for ${erc20SwapAddress}: ${transactionId}`, ); + return true; } - }; - private checkContractVersion = async ( - name: string, - contract: EtherSwap | ERC20Swap, - supportedVersion: bigint, - ) => { - const contractVersion = await contract.version(); - - if (contractVersion !== supportedVersion) { - throw Errors.UNSUPPORTED_CONTRACT_VERSION( - name, - await contract.getAddress(), - contractVersion, - supportedVersion, - ); - } + return false; }; } diff --git a/lib/wallet/ethereum/ContractEventHandler.ts b/lib/wallet/ethereum/contracts/ContractEventHandler.ts similarity index 87% rename from lib/wallet/ethereum/ContractEventHandler.ts rename to lib/wallet/ethereum/contracts/ContractEventHandler.ts index 01f5d4ba..e3892db8 100644 --- a/lib/wallet/ethereum/ContractEventHandler.ts +++ b/lib/wallet/ethereum/contracts/ContractEventHandler.ts @@ -1,46 +1,54 @@ import { ERC20Swap } from 'boltz-core/typechain/ERC20Swap'; import { EtherSwap } from 'boltz-core/typechain/EtherSwap'; import { ContractEventPayload, Transaction, TransactionResponse } from 'ethers'; -import Logger from '../../Logger'; -import TypedEventEmitter from '../../consts/TypedEventEmitter'; -import { ERC20SwapValues, EtherSwapValues } from '../../consts/Types'; +import Logger from '../../../Logger'; +import TypedEventEmitter from '../../../consts/TypedEventEmitter'; +import { ERC20SwapValues, EtherSwapValues } from '../../../consts/Types'; +import { parseBuffer } from '../EthereumUtils'; +import { NetworkDetails } from '../EvmNetworks'; import { formatERC20SwapValues, formatEtherSwapValues } from './ContractUtils'; -import { parseBuffer } from './EthereumUtils'; -import { NetworkDetails } from './EvmNetworks'; type Events = { // EtherSwap contract events 'eth.lockup': { + version: bigint; transaction: Transaction | TransactionResponse; etherSwapValues: EtherSwapValues; }; 'eth.claim': { + version: bigint; transactionHash: string; preimageHash: Buffer; preimage: Buffer; }; 'eth.refund': { + version: bigint; transactionHash: string; preimageHash: Buffer; }; // ERC20Swap contract events 'erc20.lockup': { + version: bigint; transaction: Transaction | TransactionResponse; erc20SwapValues: ERC20SwapValues; }; 'erc20.claim': { + version: bigint; transactionHash: string; preimageHash: Buffer; preimage: Buffer; }; 'erc20.refund': { + version: bigint; transactionHash: string; preimageHash: Buffer; }; }; class ContractEventHandler extends TypedEventEmitter { + private version!: bigint; + private etherSwap!: EtherSwap; private erc20Swap!: ERC20Swap; @@ -49,10 +57,12 @@ class ContractEventHandler extends TypedEventEmitter { } public init = async ( + version: bigint, networkDetails: NetworkDetails, etherSwap: EtherSwap, erc20Swap: ERC20Swap, ): Promise => { + this.version = version; this.etherSwap = etherSwap; this.erc20Swap = erc20Swap; @@ -72,6 +82,7 @@ class ContractEventHandler extends TypedEventEmitter { for (const event of etherLockups) { this.emit('eth.lockup', { + version: this.version, transaction: await event.getTransaction(), etherSwapValues: formatEtherSwapValues(event.args!), }); @@ -79,6 +90,7 @@ class ContractEventHandler extends TypedEventEmitter { for (const event of etherClaims) { this.emit('eth.claim', { + version: this.version, transactionHash: event.transactionHash, preimageHash: parseBuffer(event.topics[1]), preimage: parseBuffer(event.args!.preimage), @@ -87,6 +99,7 @@ class ContractEventHandler extends TypedEventEmitter { for (const event of etherRefunds) { this.emit('eth.refund', { + version: this.version, transactionHash: event.transactionHash, preimageHash: parseBuffer(event.topics[1]), }); @@ -100,6 +113,7 @@ class ContractEventHandler extends TypedEventEmitter { for (const event of erc20Lockups) { this.emit('erc20.lockup', { + version: this.version, transaction: await event.getTransaction(), erc20SwapValues: formatERC20SwapValues(event.args!), }); @@ -107,6 +121,7 @@ class ContractEventHandler extends TypedEventEmitter { for (const event of erc20Claims) { this.emit('erc20.claim', { + version: this.version, transactionHash: event.transactionHash, preimageHash: parseBuffer(event.topics[1]), preimage: parseBuffer(event.args!.preimage), @@ -115,6 +130,7 @@ class ContractEventHandler extends TypedEventEmitter { for (const event of erc20Refunds) { this.emit('erc20.refund', { + version: this.version, transactionHash: event.transactionHash, preimageHash: parseBuffer(event.topics[1]), }); @@ -133,6 +149,7 @@ class ContractEventHandler extends TypedEventEmitter { event: ContractEventPayload, ) => { this.emit('eth.lockup', { + version: this.version, transaction: await event.log.getTransaction(), etherSwapValues: { amount, @@ -149,6 +166,7 @@ class ContractEventHandler extends TypedEventEmitter { 'Claim' as any, (preimageHash: string, preimage: string, event: ContractEventPayload) => { this.emit('eth.claim', { + version: this.version, transactionHash: event.log.transactionHash, preimageHash: parseBuffer(preimageHash), preimage: parseBuffer(preimage), @@ -160,6 +178,7 @@ class ContractEventHandler extends TypedEventEmitter { 'Refund' as any, (preimageHash: string, event: ContractEventPayload) => { this.emit('eth.refund', { + version: this.version, transactionHash: event.log.transactionHash, preimageHash: parseBuffer(preimageHash), }); @@ -178,6 +197,7 @@ class ContractEventHandler extends TypedEventEmitter { event: ContractEventPayload, ) => { this.emit('erc20.lockup', { + version: this.version, transaction: await event.log.getTransaction(), erc20SwapValues: { amount, @@ -195,6 +215,7 @@ class ContractEventHandler extends TypedEventEmitter { 'Claim' as any, (preimageHash: string, preimage: string, event: ContractEventPayload) => { this.emit('erc20.claim', { + version: this.version, transactionHash: event.log.transactionHash, preimageHash: parseBuffer(preimageHash), preimage: parseBuffer(preimage), @@ -206,6 +227,7 @@ class ContractEventHandler extends TypedEventEmitter { 'Refund' as any, (preimageHash: string, event: ContractEventPayload) => { this.emit('erc20.refund', { + version: this.version, transactionHash: event.log.transactionHash, preimageHash: parseBuffer(preimageHash), }); @@ -215,3 +237,4 @@ class ContractEventHandler extends TypedEventEmitter { } export default ContractEventHandler; +export { Events }; diff --git a/lib/wallet/ethereum/ContractHandler.ts b/lib/wallet/ethereum/contracts/ContractHandler.ts similarity index 87% rename from lib/wallet/ethereum/ContractHandler.ts rename to lib/wallet/ethereum/contracts/ContractHandler.ts index cb64bd04..12df083a 100644 --- a/lib/wallet/ethereum/ContractHandler.ts +++ b/lib/wallet/ethereum/contracts/ContractHandler.ts @@ -1,13 +1,15 @@ import { ERC20Swap } from 'boltz-core/typechain/ERC20Swap'; import { EtherSwap } from 'boltz-core/typechain/EtherSwap'; import { ContractTransactionResponse, Provider } from 'ethers'; -import { ethereumPrepayMinerFeeGasLimit } from '../../consts/Consts'; -import { swapTypeToPrettyString } from '../../consts/Enums'; -import { AnySwap } from '../../consts/Types'; -import TransactionLabelRepository from '../../db/repositories/TransactionLabelRepository'; -import ERC20WalletProvider from '../providers/ERC20WalletProvider'; -import { getGasPrices } from './EthereumUtils'; -import { NetworkDetails } from './EvmNetworks'; +import { ethereumPrepayMinerFeeGasLimit } from '../../../consts/Consts'; +import { swapTypeToPrettyString } from '../../../consts/Enums'; +import { AnySwap } from '../../../consts/Types'; +import TransactionLabelRepository from '../../../db/repositories/TransactionLabelRepository'; +import ERC20WalletProvider from '../../providers/ERC20WalletProvider'; +import Errors from '../Errors'; +import { getGasPrices } from '../EthereumUtils'; +import { NetworkDetails } from '../EvmNetworks'; +import { Feature } from './Contracts'; export type BatchClaimValues = { preimage: Buffer; @@ -22,13 +24,17 @@ class ContractHandler { public etherSwap!: EtherSwap; public erc20Swap!: ERC20Swap; + private features: Set = new Set(); + constructor(private readonly networkDetails: NetworkDetails) {} public init = ( + features: Set, provider: Provider, etherSwap: EtherSwap, erc20Swap: ERC20Swap, ): void => { + this.features = features; this.provider = provider; this.etherSwap = etherSwap; this.erc20Swap = erc20Swap; @@ -113,8 +119,12 @@ class ContractHandler { public claimBatchEther = async ( swapsIds: string[], values: BatchClaimValues[], - ): Promise => - this.annotateLabel( + ): Promise => { + if (!this.features.has(Feature.BatchClaim)) { + throw Errors.NOT_SUPPORTED_BY_CONTRACT_VERSION(); + } + + return this.annotateLabel( TransactionLabelRepository.claimBatchLabel(swapsIds), this.networkDetails.symbol, this.etherSwap.claimBatch( @@ -127,6 +137,7 @@ class ContractHandler { }, ), ); + }; public refundEther = async ( swap: AnySwap, @@ -230,8 +241,12 @@ class ContractHandler { swapsIds: string[], token: ERC20WalletProvider, values: BatchClaimValues[], - ): Promise => - this.annotateLabel( + ): Promise => { + if (!this.features.has(Feature.BatchClaim)) { + throw Errors.NOT_SUPPORTED_BY_CONTRACT_VERSION(); + } + + return this.annotateLabel( TransactionLabelRepository.claimBatchLabel(swapsIds), token.symbol, this.erc20Swap.claimBatch( @@ -245,6 +260,7 @@ class ContractHandler { }, ), ); + }; public refundToken = async ( swap: AnySwap, diff --git a/lib/wallet/ethereum/ContractUtils.ts b/lib/wallet/ethereum/contracts/ContractUtils.ts similarity index 95% rename from lib/wallet/ethereum/ContractUtils.ts rename to lib/wallet/ethereum/contracts/ContractUtils.ts index 4c961fdf..bd0c5615 100644 --- a/lib/wallet/ethereum/ContractUtils.ts +++ b/lib/wallet/ethereum/contracts/ContractUtils.ts @@ -7,9 +7,9 @@ import { LockupEvent as EtherSwapLockupEvent, } from 'boltz-core/typechain/EtherSwap'; import { Provider, Result } from 'ethers'; -import { ERC20SwapValues, EtherSwapValues } from '../../consts/Types'; -import Errors from './Errors'; -import { parseBuffer } from './EthereumUtils'; +import { ERC20SwapValues, EtherSwapValues } from '../../../consts/Types'; +import Errors from '../Errors'; +import { parseBuffer } from '../EthereumUtils'; // TODO: what happens if the hash doesn't exist or the transaction isn't confirmed yet? diff --git a/lib/wallet/ethereum/contracts/Contracts.ts b/lib/wallet/ethereum/contracts/Contracts.ts new file mode 100644 index 00000000..ee030732 --- /dev/null +++ b/lib/wallet/ethereum/contracts/Contracts.ts @@ -0,0 +1,114 @@ +import { ContractABIs } from 'boltz-core'; +import { ERC20Swap } from 'boltz-core/typechain/ERC20Swap'; +import { EtherSwap } from 'boltz-core/typechain/EtherSwap'; +import { Contract, Signer } from 'ethers'; +import { ContractsConfig } from '../../../Config'; +import Logger from '../../../Logger'; +import DefaultMap from '../../../consts/DefaultMap'; +import Errors from '../../../wallet/Errors'; +import ConsolidatedEventHandler from '../ConsolidatedEventHandler'; +import { NetworkDetails } from '../EvmNetworks'; +import InjectedProvider from '../InjectedProvider'; +import ContractEventHandler from './ContractEventHandler'; +import ContractHandler from './ContractHandler'; + +enum Feature { + BatchClaim, +} + +class Contracts { + public static readonly minVersion = 3n; + public static readonly maxVersion = 4n; + + public static readonly supportedFeatures = new DefaultMap< + bigint, + Set + >(() => new Set(), [[4n, new Set([Feature.BatchClaim])]]); + + public features: Set = new Set(); + public version!: bigint; + + public readonly contractHandler: ContractHandler; + public readonly contractEventHandler: ContractEventHandler; + + public etherSwap!: EtherSwap; + public erc20Swap!: ERC20Swap; + + constructor( + private readonly logger: Logger, + private readonly network: NetworkDetails, + private readonly contracts: ContractsConfig, + ) { + this.contractHandler = new ContractHandler(this.network); + this.contractEventHandler = new ContractEventHandler(this.logger); + } + + public init = async ( + provider: InjectedProvider, + signer: Signer, + eventHandler: ConsolidatedEventHandler, + ) => { + if ( + [this.contracts.etherSwap, this.contracts.erc20Swap].some( + (value) => value === undefined || value === '', + ) + ) { + throw Errors.MISSING_SWAP_CONTRACTS(); + } + + this.etherSwap = new Contract( + this.contracts.etherSwap, + ContractABIs.EtherSwap as any, + signer, + ) as any as EtherSwap; + + this.erc20Swap = new Contract( + this.contracts.erc20Swap, + ContractABIs.ERC20Swap as any, + signer, + ) as any as ERC20Swap; + + const versions = await Promise.all( + [this.etherSwap, this.erc20Swap].map((c) => c.version()), + ); + if (versions.some((v) => v !== versions[0])) { + throw Errors.INVALID_ETHEREUM_CONFIGURATION('contract version mismatch'); + } + + if ( + versions[0] < Contracts.minVersion || + versions[0] > Contracts.maxVersion + ) { + throw Errors.INVALID_ETHEREUM_CONFIGURATION( + `unsupported contract version ${versions[0].toString()}`, + ); + } + + this.version = versions[0]; + this.features = Contracts.supportedFeatures.get(versions[0]); + + eventHandler.register(this.contractEventHandler); + this.contractHandler.init( + this.features, + provider, + this.etherSwap, + this.erc20Swap, + ); + await this.contractEventHandler.init( + this.version, + this.network, + this.etherSwap, + this.erc20Swap, + ); + + this.logger.debug( + `Using ${this.network.name} EtherSwap v${versions[0]} contract: ${this.contracts.etherSwap}`, + ); + this.logger.debug( + `Using ${this.network.name} ERC20Swap v${versions[0]} contract: ${this.contracts.erc20Swap}`, + ); + }; +} + +export default Contracts; +export { Feature }; diff --git a/lib/wallet/providers/ERC20WalletProvider.ts b/lib/wallet/providers/ERC20WalletProvider.ts index 4b98aa8c..f07edea2 100644 --- a/lib/wallet/providers/ERC20WalletProvider.ts +++ b/lib/wallet/providers/ERC20WalletProvider.ts @@ -104,8 +104,10 @@ class ERC20WalletProvider implements WalletProviderInterface { public approve = async ( spender: string, amount: bigint, + nonce?: number, ): Promise => { const transaction = await this.token.contract.approve(spender, amount, { + nonce, ...(await getGasPrices(this.signer.provider!)), }); diff --git a/test/integration/service/Renegotiator.spec.ts b/test/integration/service/Renegotiator.spec.ts index d2865028..4a45c1c4 100644 --- a/test/integration/service/Renegotiator.spec.ts +++ b/test/integration/service/Renegotiator.spec.ts @@ -22,6 +22,7 @@ import EthereumNursery from '../../../lib/swap/EthereumNursery'; import SwapNursery from '../../../lib/swap/SwapNursery'; import UtxoNursery from '../../../lib/swap/UtxoNursery'; import WalletManager from '../../../lib/wallet/WalletManager'; +import Contracts from '../../../lib/wallet/ethereum/contracts/Contracts'; import { bitcoinClient } from '../Nodes'; import { getContracts, getSigner } from '../wallet/EthereumTools'; @@ -43,6 +44,7 @@ describe('Renegotiator', () => { wallets: new Map([['BTC', { the: 'BTC wallet' }]]), } as any as WalletManager; + const contracts = {} as Contracts; const swapNursery = { utxoNursery: { checkChainSwapTransaction: jest.fn().mockImplementation(async () => {}), @@ -51,6 +53,7 @@ describe('Renegotiator', () => { { ethereumManager: { hasSymbol: jest.fn().mockReturnValue(true), + contractsForAddress: jest.fn().mockResolvedValue(contracts), }, checkEtherSwapLockup: jest.fn(), @@ -150,11 +153,9 @@ describe('Renegotiator', () => { (swapNursery.ethereumNurseries[0].ethereumManager as any).provider = provider; - (swapNursery.ethereumNurseries[0].ethereumManager as any).contractHandler = - { - etherSwap, - erc20Swap, - }; + + contracts.etherSwap = etherSwap; + contracts.erc20Swap = erc20Swap; await bitcoinClient.connect(); }); diff --git a/test/integration/service/cooperative/DeferredClaimer.spec.ts b/test/integration/service/cooperative/DeferredClaimer.spec.ts index 7e3b90e3..2bd70adb 100644 --- a/test/integration/service/cooperative/DeferredClaimer.spec.ts +++ b/test/integration/service/cooperative/DeferredClaimer.spec.ts @@ -41,8 +41,11 @@ import DeferredClaimer from '../../../../lib/service/cooperative/DeferredClaimer import SwapOutputType from '../../../../lib/swap/SwapOutputType'; import Wallet from '../../../../lib/wallet/Wallet'; import WalletManager, { Currency } from '../../../../lib/wallet/WalletManager'; -import ContractHandler from '../../../../lib/wallet/ethereum/ContractHandler'; import { Ethereum } from '../../../../lib/wallet/ethereum/EvmNetworks'; +import ContractHandler from '../../../../lib/wallet/ethereum/contracts/ContractHandler'; +import Contracts, { + Feature, +} from '../../../../lib/wallet/ethereum/contracts/Contracts'; import CoreWalletProvider from '../../../../lib/wallet/providers/CoreWalletProvider'; import ERC20WalletProvider from '../../../../lib/wallet/providers/ERC20WalletProvider'; import { wait } from '../../../Utils'; @@ -268,18 +271,37 @@ describe('DeferredClaimer', () => { const contractHandler = new ContractHandler(Ethereum); contractHandler.init( + new Set([Feature.BatchClaim]), ethereumSetup.provider, contracts.etherSwap, contracts.erc20Swap, ); + const cts = { + contractHandler, + version: Contracts.maxVersion, + etherSwap: contracts.etherSwap, + erc20Swap: contracts.erc20Swap, + }; + walletManager.ethereumManagers = [ { contractHandler, - etherSwap: contracts.etherSwap, - erc20Swap: contracts.erc20Swap, provider: ethereumSetup.provider, hasSymbol: jest.fn().mockReturnValue(true), + contractsForAddress: jest + .fn() + .mockImplementation(async (address: string) => { + if (address == 'outdated') { + return { + ...cts, + version: Contracts.maxVersion - 1n, + }; + } else { + return cts; + } + }), + highestContractsVersion: jest.fn().mockReturnValue(cts), } as any, ]; claimer['currencies'].set('RBTC', { @@ -427,6 +449,19 @@ describe('DeferredClaimer', () => { await expect(claimer.deferClaim(swap, preimage)).resolves.toEqual(false); }); + + test('should not defer claim transactions of swaps to outdated contracts', async () => { + const swap = { + pair: 'RBTC/BTC', + orderSide: OrderSide.SELL, + version: SwapVersion.Taproot, + lockupAddress: 'outdated', + } as Partial as Swap; + + await expect(claimer.deferClaim(swap, randomBytes(32))).resolves.toEqual( + false, + ); + }); }); test('should get ids of pending sweeps', async () => { diff --git a/test/integration/service/cooperative/EipSigner.spec.ts b/test/integration/service/cooperative/EipSigner.spec.ts index 961b4b9a..5d7e011a 100644 --- a/test/integration/service/cooperative/EipSigner.spec.ts +++ b/test/integration/service/cooperative/EipSigner.spec.ts @@ -141,8 +141,10 @@ describe('EipSigner', () => { const preimageHash = randomBytes(32); const amount = BigInt(10) ** BigInt(17); const timelock = (await setup.provider.getBlockNumber()) + 21; + const lockupAddress = '0xfbd623a70f5D6d50d2935071b5c4cd0E5a9772Ad'; SwapRepository.getSwap = jest.fn().mockResolvedValue({ + lockupAddress, orderSide: 1, pair: 'RBTC/BTC', type: SwapType.Submarine, @@ -157,6 +159,7 @@ describe('EipSigner', () => { expect(sidecar.signEvmRefund).toHaveBeenCalledTimes(1); expect(sidecar.signEvmRefund).toHaveBeenCalledWith( + lockupAddress, preimageHash, amount, undefined, @@ -167,6 +170,7 @@ describe('EipSigner', () => { test('should refund chain EtherSwap cooperatively', async () => { const preimageHash = randomBytes(32); const amount = BigInt(10) ** BigInt(17); + const lockupAddress = '0xfbd623a70f5D6d50d2935071b5c4cd0E5a9772Ad'; const timelock = (await setup.provider.getBlockNumber()) + 21; const id = 'asdf'; @@ -177,6 +181,7 @@ describe('EipSigner', () => { preimageHash: getHexString(preimageHash), status: SwapUpdateEvent.InvoiceFailedToPay, receivingData: { + lockupAddress, symbol: 'RBTC', timeoutBlockHeight: timelock, amount: Number(amount / etherDecimals), @@ -188,6 +193,7 @@ describe('EipSigner', () => { expect(sidecar.signEvmRefund).toHaveBeenCalledTimes(1); expect(sidecar.signEvmRefund).toHaveBeenCalledWith( + lockupAddress, preimageHash, amount, undefined, @@ -205,9 +211,11 @@ describe('EipSigner', () => { test('should refund submarine ERC20Swap cooperatively', async () => { const preimageHash = randomBytes(32); const amount = BigInt(10); + const lockupAddress = '0xfbd623a70f5D6d50d2935071b5c4cd0E5a9772Ad'; const timelock = (await setup.provider.getBlockNumber()) + 21; SwapRepository.getSwap = jest.fn().mockResolvedValue({ + lockupAddress, orderSide: 1, pair: 'TOKEN/BTC', type: SwapType.Submarine, @@ -222,6 +230,7 @@ describe('EipSigner', () => { expect(sidecar.signEvmRefund).toHaveBeenCalledTimes(1); expect(sidecar.signEvmRefund).toHaveBeenCalledWith( + lockupAddress, preimageHash, amount, await token.getAddress(), @@ -232,6 +241,7 @@ describe('EipSigner', () => { test('should refund chain ERC20Swap cooperatively', async () => { const preimageHash = randomBytes(32); const amount = BigInt(10); + const lockupAddress = '0xfbd623a70f5D6d50d2935071b5c4cd0E5a9772Ad'; const timelock = (await setup.provider.getBlockNumber()) + 21; const id = 'erc20'; @@ -242,6 +252,7 @@ describe('EipSigner', () => { preimageHash: getHexString(preimageHash), status: SwapUpdateEvent.InvoiceFailedToPay, receivingData: { + lockupAddress, symbol: 'TOKEN', amount: Number(amount), timeoutBlockHeight: timelock, @@ -253,6 +264,7 @@ describe('EipSigner', () => { expect(sidecar.signEvmRefund).toHaveBeenCalledTimes(1); expect(sidecar.signEvmRefund).toHaveBeenCalledWith( + lockupAddress, preimageHash, amount, await token.getAddress(), diff --git a/test/integration/wallet/ethereum/EthereumManager.spec.ts b/test/integration/wallet/ethereum/EthereumManager.spec.ts index 8af935a3..38f79602 100644 --- a/test/integration/wallet/ethereum/EthereumManager.spec.ts +++ b/test/integration/wallet/ethereum/EthereumManager.spec.ts @@ -1,4 +1,3 @@ -import { generateMnemonic } from 'bip39'; import { MaxUint256 } from 'ethers'; import Logger from '../../../../lib/Logger'; import Database from '../../../../lib/db/Database'; @@ -6,6 +5,7 @@ import Errors from '../../../../lib/wallet/Errors'; import Wallet from '../../../../lib/wallet/Wallet'; import EthereumManager from '../../../../lib/wallet/ethereum/EthereumManager'; import { Ethereum, Rsk } from '../../../../lib/wallet/ethereum/EvmNetworks'; +import Contracts from '../../../../lib/wallet/ethereum/contracts/Contracts'; import ERC20WalletProvider from '../../../../lib/wallet/providers/ERC20WalletProvider'; import { EthereumSetup, @@ -31,6 +31,20 @@ describe('EthereumManager', () => { let manager: EthereumManager; let wallets: Map; + const oldContracts = { + version: 1, + etherSwap: { + getAddress: jest + .fn() + .mockResolvedValue('0x8F78a4f8A9931FE8F7CBc6B5fD4976dcBe8f1832'), + }, + erc20Swap: { + getAddress: jest + .fn() + .mockResolvedValue('0xBf074e2a0b85c975b41F33C99b14992EFba62776'), + }, + } as unknown as Contracts; + beforeAll(async () => { database = new Database(Logger.disabledLogger, Database.memoryDatabase); await database.init(); @@ -42,8 +56,12 @@ describe('EthereumManager', () => { manager = new EthereumManager(Logger.disabledLogger, false, { providerEndpoint, networkName: 'Anvil', - etherSwapAddress: await contracts.etherSwap.getAddress(), - erc20SwapAddress: await contracts.erc20Swap.getAddress(), + contracts: [ + { + etherSwap: await contracts.etherSwap.getAddress(), + erc20Swap: await contracts.erc20Swap.getAddress(), + }, + ], tokens: [ { symbol: Ethereum.symbol, @@ -57,6 +75,8 @@ describe('EthereumManager', () => { }, ], }); + manager['contracts'].push(oldContracts); + wallets = await manager.init(setup.mnemonic); }); @@ -79,48 +99,6 @@ describe('EthereumManager', () => { ).toThrow(Errors.MISSING_SWAP_CONTRACTS().message); }); - test.each` - versions | isEtherSwap - ${{ - EtherSwap: 2, - ERC20Swap: 3, -}} | ${true} - ${{ - EtherSwap: 5, - ERC20Swap: 3, -}} | ${true} - ${{ - EtherSwap: 4, - ERC20Swap: 2, -}} | ${false} - ${{ - EtherSwap: 4, - ERC20Swap: 5, -}} | ${false} - `( - 'should throw for invalid contract versions $versions', - async ({ versions, isEtherSwap }) => { - EthereumManager['supportedContractVersions'] = versions; - const throwManager = new EthereumManager(Logger.disabledLogger, false, { - providerEndpoint, - tokens: [], - etherSwapAddress: await manager.etherSwap.getAddress(), - erc20SwapAddress: await manager.erc20Swap.getAddress(), - }); - - await expect(throwManager.init(generateMnemonic())).rejects.toEqual( - Errors.UNSUPPORTED_CONTRACT_VERSION( - `${Ethereum.name} ${isEtherSwap ? 'EtherSwap' : 'ERC20Swap'}`, - await ( - isEtherSwap ? manager.etherSwap : manager.erc20Swap - ).getAddress(), - BigInt(4), - isEtherSwap ? versions.EtherSwap : versions.ERC20Swap, - ), - ); - }, - ); - test('should use network name in config', async () => { expect((await manager.getContractDetails()).network).toEqual({ name: manager['config'].networkName, @@ -132,7 +110,9 @@ describe('EthereumManager', () => { expect( await ( wallets.get('USDT')!.walletProvider as ERC20WalletProvider - ).getAllowance(await manager.erc20Swap.getAddress()), + ).getAllowance( + await manager.highestContractsVersion().erc20Swap.getAddress(), + ), ).toEqual(MaxUint256); }); @@ -145,4 +125,31 @@ describe('EthereumManager', () => { `('should have symbol $symbol -> $expected', ({ symbol, expected }) => { expect(manager.hasSymbol(symbol)).toEqual(expected); }); + + test('should get highest contracts version', async () => { + expect(manager.highestContractsVersion()).not.toEqual(oldContracts); + expect(manager.highestContractsVersion()).toEqual(manager['contracts'][1]); + }); + + test('should get contracts for address', async () => { + await expect( + manager.contractsForAddress( + await manager.highestContractsVersion().etherSwap.getAddress(), + ), + ).resolves.toEqual(manager['contracts'][1]); + await expect( + manager.contractsForAddress( + await manager.highestContractsVersion().erc20Swap.getAddress(), + ), + ).resolves.toEqual(manager['contracts'][1]); + + await expect( + manager.contractsForAddress(await oldContracts.etherSwap.getAddress()), + ).resolves.toEqual(oldContracts); + await expect( + manager.contractsForAddress(await oldContracts.erc20Swap.getAddress()), + ).resolves.toEqual(oldContracts); + + await expect(manager.contractsForAddress('0x')).resolves.toBeUndefined(); + }); }); diff --git a/test/integration/wallet/ethereum/ContractEventHandler.spec.ts b/test/integration/wallet/ethereum/contracts/ContractEventHandler.spec.ts similarity index 84% rename from test/integration/wallet/ethereum/ContractEventHandler.spec.ts rename to test/integration/wallet/ethereum/contracts/ContractEventHandler.spec.ts index c36f2f0c..1e2705d2 100644 --- a/test/integration/wallet/ethereum/ContractEventHandler.spec.ts +++ b/test/integration/wallet/ethereum/contracts/ContractEventHandler.spec.ts @@ -4,15 +4,15 @@ import { ERC20Swap } from 'boltz-core/typechain/ERC20Swap'; import { EtherSwap } from 'boltz-core/typechain/EtherSwap'; import { randomBytes } from 'crypto'; import { MaxUint256 } from 'ethers'; -import Logger from '../../../../lib/Logger'; -import ContractEventHandler from '../../../../lib/wallet/ethereum/ContractEventHandler'; -import { Ethereum } from '../../../../lib/wallet/ethereum/EvmNetworks'; +import Logger from '../../../../../lib/Logger'; +import { Ethereum } from '../../../../../lib/wallet/ethereum/EvmNetworks'; +import ContractEventHandler from '../../../../../lib/wallet/ethereum/contracts/ContractEventHandler'; import { EthereumSetup, fundSignerWallet, getContracts, getSigner, -} from '../EthereumTools'; +} from '../../EthereumTools'; type Transactions = { lockup?: string; @@ -23,6 +23,8 @@ type Transactions = { describe('ContractEventHandler', () => { const contractEventHandler = new ContractEventHandler(Logger.disabledLogger); + const contractsVersion = 4n; + const preimage = randomBytes(32); const timelock = 1; const amount = BigInt(123_321); @@ -63,6 +65,7 @@ describe('ContractEventHandler', () => { test('should init', async () => { await contractEventHandler.init( + contractsVersion, Ethereum, contracts.etherSwap, contracts.erc20Swap, @@ -83,7 +86,8 @@ describe('ContractEventHandler', () => { const lockupPromise = new Promise((resolve) => { contractEventHandler.once( 'eth.lockup', - async ({ transaction, etherSwapValues }) => { + async ({ version, transaction, etherSwapValues }) => { + expect(version).toEqual(contractsVersion); expect(transaction).toEqual( await setup.provider.getTransaction(tx.hash), ); @@ -115,7 +119,8 @@ describe('ContractEventHandler', () => { const claimPromise = new Promise((resolve) => { contractEventHandler.once( 'eth.claim', - async ({ transactionHash, preimageHash, preimage }) => { + async ({ version, transactionHash, preimageHash, preimage }) => { + expect(version).toEqual(contractsVersion); expect(transactionHash).toEqual(tx.hash); expect(preimageHash).toEqual(preimageHash); expect(preimage).toEqual(preimage); @@ -151,7 +156,8 @@ describe('ContractEventHandler', () => { const refundPromise = new Promise((resolve) => { contractEventHandler.once( 'eth.refund', - async ({ transactionHash, preimageHash }) => { + async ({ version, transactionHash, preimageHash }) => { + expect(version).toEqual(contractsVersion); expect(transactionHash).toEqual(tx.hash); expect(preimageHash).toEqual(preimageHash); resolve(); @@ -177,7 +183,8 @@ describe('ContractEventHandler', () => { const lockupPromise = new Promise((resolve) => { contractEventHandler.once( 'erc20.lockup', - async ({ transaction, erc20SwapValues }) => { + async ({ version, transaction, erc20SwapValues }) => { + expect(version).toEqual(contractsVersion); expect(transaction).toEqual( await setup.provider.getTransaction(tx.hash), ); @@ -210,7 +217,8 @@ describe('ContractEventHandler', () => { const claimPromise = new Promise((resolve) => { contractEventHandler.once( 'erc20.claim', - async ({ transactionHash, preimageHash, preimage }) => { + async ({ version, transactionHash, preimageHash, preimage }) => { + expect(version).toEqual(contractsVersion); expect(transactionHash).toEqual(tx.hash); expect(preimageHash).toEqual(preimageHash); expect(preimage).toEqual(preimage); @@ -246,7 +254,8 @@ describe('ContractEventHandler', () => { const refundPromise = new Promise((resolve) => { contractEventHandler.once( 'erc20.refund', - async ({ transactionHash, preimageHash }) => { + async ({ version, transactionHash, preimageHash }) => { + expect(version).toEqual(contractsVersion); expect(transactionHash).toEqual(tx.hash); expect(preimageHash).toEqual(preimageHash); resolve(); @@ -263,7 +272,8 @@ describe('ContractEventHandler', () => { const lockupPromise = new Promise((resolve) => { contractEventHandler.once( 'eth.lockup', - async ({ transaction, etherSwapValues }) => { + async ({ version, transaction, etherSwapValues }) => { + expect(version).toEqual(contractsVersion); expect(transaction).toEqual( await setup.provider.getTransaction(transactions.etherSwap.lockup!), ); @@ -282,7 +292,8 @@ describe('ContractEventHandler', () => { const claimPromise = new Promise((resolve) => { contractEventHandler.once( 'eth.claim', - async ({ transactionHash, preimageHash, preimage }) => { + async ({ version, transactionHash, preimageHash, preimage }) => { + expect(version).toEqual(contractsVersion); expect(transactionHash).toEqual(transactions.etherSwap.claim); expect(preimageHash).toEqual(preimageHash); expect(preimage).toEqual(preimage); @@ -294,7 +305,8 @@ describe('ContractEventHandler', () => { const refundPromise = new Promise((resolve) => { contractEventHandler.once( 'eth.refund', - async ({ transactionHash, preimageHash }) => { + async ({ version, transactionHash, preimageHash }) => { + expect(version).toEqual(contractsVersion); expect(transactionHash).toEqual(transactions.etherSwap.refund); expect(preimageHash).toEqual(preimageHash); resolve(); @@ -310,7 +322,8 @@ describe('ContractEventHandler', () => { const lockupPromise = new Promise((resolve) => { contractEventHandler.once( 'erc20.lockup', - async ({ transaction, erc20SwapValues }) => { + async ({ version, transaction, erc20SwapValues }) => { + expect(version).toEqual(contractsVersion); expect(transaction).toEqual( await setup.provider.getTransaction(transactions.erc20Swap.lockup!), ); @@ -330,7 +343,8 @@ describe('ContractEventHandler', () => { const claimPromise = new Promise((resolve) => { contractEventHandler.once( 'erc20.claim', - async ({ transactionHash, preimageHash, preimage }) => { + async ({ version, transactionHash, preimageHash, preimage }) => { + expect(version).toEqual(contractsVersion); expect(transactionHash).toEqual(transactions.erc20Swap.claim); expect(preimageHash).toEqual(preimageHash); expect(preimage).toEqual(preimage); @@ -342,7 +356,8 @@ describe('ContractEventHandler', () => { const refundPromise = new Promise((resolve) => { contractEventHandler.once( 'erc20.refund', - async ({ transactionHash, preimageHash }) => { + async ({ version, transactionHash, preimageHash }) => { + expect(version).toEqual(contractsVersion); expect(transactionHash).toEqual(transactions.erc20Swap.refund); expect(preimageHash).toEqual(preimageHash); resolve(); diff --git a/test/integration/wallet/ethereum/ContractHandler.spec.ts b/test/integration/wallet/ethereum/contracts/ContractHandler.spec.ts similarity index 65% rename from test/integration/wallet/ethereum/ContractHandler.spec.ts rename to test/integration/wallet/ethereum/contracts/ContractHandler.spec.ts index dc88da75..6adc69d9 100644 --- a/test/integration/wallet/ethereum/ContractHandler.spec.ts +++ b/test/integration/wallet/ethereum/contracts/ContractHandler.spec.ts @@ -4,24 +4,26 @@ import { ERC20Swap } from 'boltz-core/typechain/ERC20Swap'; import { EtherSwap } from 'boltz-core/typechain/EtherSwap'; import { randomBytes } from 'crypto'; import { Wallet } from 'ethers'; -import Logger from '../../../../lib/Logger'; -import { SwapType } from '../../../../lib/consts/Enums'; -import { AnySwap } from '../../../../lib/consts/Types'; -import Database from '../../../../lib/db/Database'; -import TransactionLabel from '../../../../lib/db/models/TransactionLabel'; -import TransactionLabelRepository from '../../../../lib/db/repositories/TransactionLabelRepository'; +import Logger from '../../../../../lib/Logger'; +import { SwapType } from '../../../../../lib/consts/Enums'; +import { AnySwap } from '../../../../../lib/consts/Types'; +import Database from '../../../../../lib/db/Database'; +import TransactionLabel from '../../../../../lib/db/models/TransactionLabel'; +import TransactionLabelRepository from '../../../../../lib/db/repositories/TransactionLabelRepository'; +import Errors from '../../../../../lib/wallet/ethereum/Errors'; +import { Ethereum } from '../../../../../lib/wallet/ethereum/EvmNetworks'; import ContractHandler, { BatchClaimValues, -} from '../../../../lib/wallet/ethereum/ContractHandler'; -import { Ethereum } from '../../../../lib/wallet/ethereum/EvmNetworks'; -import ERC20WalletProvider from '../../../../lib/wallet/providers/ERC20WalletProvider'; -import { wait } from '../../../Utils'; +} from '../../../../../lib/wallet/ethereum/contracts/ContractHandler'; +import { Feature } from '../../../../../lib/wallet/ethereum/contracts/Contracts'; +import ERC20WalletProvider from '../../../../../lib/wallet/providers/ERC20WalletProvider'; +import { wait } from '../../../../Utils'; import { EthereumSetup, fundSignerWallet, getContracts, getSigner, -} from '../EthereumTools'; +} from '../../EthereumTools'; describe('ContractHandler', () => { let database: Database; @@ -130,8 +132,11 @@ describe('ContractHandler', () => { }, ); - contractHandler.init(setup.provider, etherSwap, erc20Swap); + const features = new Set([Feature.BatchClaim]); + + contractHandler.init(features, setup.provider, etherSwap, erc20Swap); contractHandlerEtherBase.init( + features, setup.provider, etherSwap.connect(setup.etherBase) as EtherSwap, erc20Swap.connect(setup.etherBase) as ERC20Swap, @@ -243,85 +248,96 @@ describe('ContractHandler', () => { expect(label!.label).toEqual(TransactionLabelRepository.claimLabel(swap)); }); - test('should batch claim Ether', async () => { - const values: BatchClaimValues[] = [ - { - amount: 21n, - timelock: 1, - preimage: randomBytes(32), - refundAddress: await setup.signer.getAddress(), - }, - { - amount: 42n, - timelock: 2, - preimage: randomBytes(32), - refundAddress: await setup.signer.getAddress(), - }, - { - amount: 50n, - timelock: 3, - preimage: randomBytes(32), - refundAddress: await setup.signer.getAddress(), - }, - { - amount: 2121n, - timelock: 21, - preimage: randomBytes(32), - refundAddress: await setup.signer.getAddress(), - }, - { - amount: 2142n, - timelock: 32, - preimage: randomBytes(32), - refundAddress: await setup.signer.getAddress(), - }, - ]; - const valuesWithPreimageHash = values.map((v) => ({ - ...v, - preimageHash: crypto.sha256(v.preimage), - })); - - let nonce = await setup.signer.getNonce(); - for (const v of valuesWithPreimageHash) { - const tx = await etherSwap.lock( - v.preimageHash, - await setup.signer.getAddress(), - v.timelock, + describe('claimBatchEther', () => { + test('should batch claim Ether', async () => { + const values: BatchClaimValues[] = [ { - nonce, - value: v.amount, + amount: 21n, + timelock: 1, + preimage: randomBytes(32), + refundAddress: await setup.signer.getAddress(), }, - ); + { + amount: 42n, + timelock: 2, + preimage: randomBytes(32), + refundAddress: await setup.signer.getAddress(), + }, + { + amount: 50n, + timelock: 3, + preimage: randomBytes(32), + refundAddress: await setup.signer.getAddress(), + }, + { + amount: 2121n, + timelock: 21, + preimage: randomBytes(32), + refundAddress: await setup.signer.getAddress(), + }, + { + amount: 2142n, + timelock: 32, + preimage: randomBytes(32), + refundAddress: await setup.signer.getAddress(), + }, + ]; + const valuesWithPreimageHash = values.map((v) => ({ + ...v, + preimageHash: crypto.sha256(v.preimage), + })); + + let nonce = await setup.signer.getNonce(); + for (const v of valuesWithPreimageHash) { + const tx = await etherSwap.lock( + v.preimageHash, + await setup.signer.getAddress(), + v.timelock, + { + nonce, + value: v.amount, + }, + ); + await tx.wait(1); + + nonce += 1; + } + + await wait(150); + const tx = await contractHandler.claimBatchEther([swap.id], values); await tx.wait(1); - nonce += 1; - } - - await wait(150); - const tx = await contractHandler.claimBatchEther([swap.id], values); - await tx.wait(1); - - for (const v of valuesWithPreimageHash) { - await expect( - etherSwap.swaps( - await etherSwap.hashValues( - v.preimageHash, - v.amount, - await setup.signer.getAddress(), - v.refundAddress, - v.timelock, + for (const v of valuesWithPreimageHash) { + await expect( + etherSwap.swaps( + await etherSwap.hashValues( + v.preimageHash, + v.amount, + await setup.signer.getAddress(), + v.refundAddress, + v.timelock, + ), ), - ), - ).resolves.toEqual(false); - } + ).resolves.toEqual(false); + } + + const label = await TransactionLabelRepository.getLabel(tx!.hash); + expect(label!).not.toBeNull(); + expect(label!.id).toEqual(tx!.hash); + expect(label!.symbol).toEqual(Ethereum.symbol); + expect(label!.label).toEqual( + TransactionLabelRepository.claimBatchLabel([swap.id]), + ); + }); - const label = await TransactionLabelRepository.getLabel(tx!.hash); - expect(label!).not.toBeNull(); - expect(label!.id).toEqual(tx!.hash); - expect(label!.symbol).toEqual(Ethereum.symbol); - expect(label!.label).toEqual( - TransactionLabelRepository.claimBatchLabel([swap.id]), - ); + test('should not batch claim when contract does not support it', async () => { + const handler = new ContractHandler(Ethereum); + handler.init(new Set(), setup.provider, etherSwap, erc20Swap); + + await expect(handler.claimBatchEther([], [])).rejects.toEqual( + Errors.NOT_SUPPORTED_BY_CONTRACT_VERSION(), + ); + }); }); test('should refund Ether', async () => { @@ -457,83 +473,94 @@ describe('ContractHandler', () => { expect(label!.label).toEqual(TransactionLabelRepository.claimLabel(swap)); }); - test('should batch claim ERC20 tokens', async () => { - const values: BatchClaimValues[] = [ - { - amount: 21n, - timelock: 1, - preimage: randomBytes(32), - refundAddress: await setup.signer.getAddress(), - }, - { - amount: 42n, - timelock: 2, - preimage: randomBytes(32), - refundAddress: await setup.signer.getAddress(), - }, - ]; - const valuesWithPreimageHash = values.map((v) => ({ - ...v, - preimageHash: crypto.sha256(v.preimage), - })); - - let nonce = await setup.signer.getNonce(); - const approveTx = await tokenContract.approve( - await erc20Swap.getAddress(), - values.reduce((sum, { amount }) => sum + amount, 0n), - { - nonce, - }, - ); - await approveTx.wait(1); - nonce += 1; - - for (const v of valuesWithPreimageHash) { - const tx = await erc20Swap.lock( - v.preimageHash, - v.amount, - await tokenContract.getAddress(), - await setup.signer.getAddress(), - v.timelock, + describe('claimBatchToken', () => { + test('should batch claim ERC20 tokens', async () => { + const values: BatchClaimValues[] = [ + { + amount: 21n, + timelock: 1, + preimage: randomBytes(32), + refundAddress: await setup.signer.getAddress(), + }, + { + amount: 42n, + timelock: 2, + preimage: randomBytes(32), + refundAddress: await setup.signer.getAddress(), + }, + ]; + const valuesWithPreimageHash = values.map((v) => ({ + ...v, + preimageHash: crypto.sha256(v.preimage), + })); + + let nonce = await setup.signer.getNonce(); + const approveTx = await tokenContract.approve( + await erc20Swap.getAddress(), + values.reduce((sum, { amount }) => sum + amount, 0n), { nonce, }, ); - await tx.wait(1); - + await approveTx.wait(1); nonce += 1; - } - await wait(150); - const tx = await contractHandler.claimBatchToken( - [swap.id], - erc20WalletProvider, - values, - ); - await tx.wait(1); + for (const v of valuesWithPreimageHash) { + const tx = await erc20Swap.lock( + v.preimageHash, + v.amount, + await tokenContract.getAddress(), + await setup.signer.getAddress(), + v.timelock, + { + nonce, + }, + ); + await tx.wait(1); + + nonce += 1; + } + + await wait(150); + const tx = await contractHandler.claimBatchToken( + [swap.id], + erc20WalletProvider, + values, + ); + await tx.wait(1); - for (const v of valuesWithPreimageHash) { - await expect( - erc20Swap.swaps( - await erc20Swap.hashValues( - v.preimageHash, - v.amount, - await tokenContract.getAddress(), - await setup.signer.getAddress(), - v.refundAddress, - v.timelock, + for (const v of valuesWithPreimageHash) { + await expect( + erc20Swap.swaps( + await erc20Swap.hashValues( + v.preimageHash, + v.amount, + await tokenContract.getAddress(), + await setup.signer.getAddress(), + v.refundAddress, + v.timelock, + ), ), - ), - ).resolves.toEqual(false); - } + ).resolves.toEqual(false); + } + + const label = await TransactionLabelRepository.getLabel(tx!.hash); + expect(label!).not.toBeNull(); + expect(label!.id).toEqual(tx!.hash); + expect(label!.symbol).toEqual(erc20WalletProvider.symbol); + expect(label!.label).toEqual( + TransactionLabelRepository.claimBatchLabel([swap.id]), + ); + }); - const label = await TransactionLabelRepository.getLabel(tx!.hash); - expect(label!).not.toBeNull(); - expect(label!.id).toEqual(tx!.hash); - expect(label!.symbol).toEqual(erc20WalletProvider.symbol); - expect(label!.label).toEqual( - TransactionLabelRepository.claimBatchLabel([swap.id]), - ); + test('should not batch claim when contract does not support it', async () => { + const handler = new ContractHandler(Ethereum); + handler.init(new Set(), setup.provider, etherSwap, erc20Swap); + + await expect( + handler.claimBatchToken([], erc20WalletProvider, []), + ).rejects.toEqual(Errors.NOT_SUPPORTED_BY_CONTRACT_VERSION()); + }); }); test('should refund ERC20 tokens', async () => { diff --git a/test/integration/wallet/ethereum/ContractUtils.spec.ts b/test/integration/wallet/ethereum/contracts/ContractUtils.spec.ts similarity index 92% rename from test/integration/wallet/ethereum/ContractUtils.spec.ts rename to test/integration/wallet/ethereum/contracts/ContractUtils.spec.ts index ae8c546e..4855e784 100644 --- a/test/integration/wallet/ethereum/ContractUtils.spec.ts +++ b/test/integration/wallet/ethereum/contracts/ContractUtils.spec.ts @@ -3,14 +3,17 @@ import { ERC20Swap } from 'boltz-core/typechain/ERC20Swap'; import { EtherSwap } from 'boltz-core/typechain/EtherSwap'; import { randomBytes } from 'crypto'; import { MaxUint256 } from 'ethers'; -import { ERC20SwapValues, EtherSwapValues } from '../../../../lib/consts/Types'; +import { + ERC20SwapValues, + EtherSwapValues, +} from '../../../../../lib/consts/Types'; import { queryERC20SwapValues, queryERC20SwapValuesFromLock, queryEtherSwapValues, queryEtherSwapValuesFromLock, -} from '../../../../lib/wallet/ethereum/ContractUtils'; -import { EthereumSetup, getContracts, getSigner } from '../EthereumTools'; +} from '../../../../../lib/wallet/ethereum/contracts/ContractUtils'; +import { EthereumSetup, getContracts, getSigner } from '../../EthereumTools'; describe('ContractUtils', () => { let setup: EthereumSetup; diff --git a/test/integration/wallet/ethereum/contracts/Contracts.spec.ts b/test/integration/wallet/ethereum/contracts/Contracts.spec.ts new file mode 100644 index 00000000..d8389d89 --- /dev/null +++ b/test/integration/wallet/ethereum/contracts/Contracts.spec.ts @@ -0,0 +1,98 @@ +import Logger from '../../../../../lib/Logger'; +import Errors from '../../../../../lib/wallet/Errors'; +import ConsolidatedEventHandler from '../../../../../lib/wallet/ethereum/ConsolidatedEventHandler'; +import InjectedProvider from '../../../../../lib/wallet/ethereum/InjectedProvider'; +import Contracts, { + Feature, +} from '../../../../../lib/wallet/ethereum/contracts/Contracts'; +import { EthereumSetup, getContracts, getSigner } from '../../EthereumTools'; + +describe('Contracts', () => { + let setup: EthereumSetup; + let setupContracts: Awaited>; + + beforeAll(async () => { + setup = await getSigner(); + setupContracts = await getContracts(setup.signer); + }); + + afterAll(() => { + setup.provider.removeAllListeners(); + + setup.provider.destroy(); + }); + + describe('init', () => { + let contracts: Contracts; + + beforeEach(async () => { + contracts = new Contracts( + Logger.disabledLogger, + { + name: 'RSK', + symbol: 'RBTC', + decimals: 18n, + }, + { + etherSwap: await setupContracts.etherSwap.getAddress(), + erc20Swap: await setupContracts.erc20Swap.getAddress(), + }, + ); + }); + + test('should init', async () => { + const eventHandler = { + register: jest.fn(), + } as unknown as ConsolidatedEventHandler; + await contracts.init( + setup.provider as unknown as InjectedProvider, + setup.signer, + eventHandler, + ); + + expect(contracts.version).toEqual( + await setupContracts.etherSwap.version(), + ); + expect(contracts.features).toEqual(new Set([Feature.BatchClaim])); + + expect(contracts.etherSwap).toBeDefined(); + expect(contracts.erc20Swap).toBeDefined(); + + expect(eventHandler.register).toHaveBeenCalledTimes(1); + }); + + test('should throw for missing contracts', async () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + contracts['contracts'] = { + etherSwap: undefined, + erc20Swap: undefined, + }; + await expect( + contracts.init( + setup.provider as unknown as InjectedProvider, + setup.signer, + {} as any, + ), + ).rejects.toEqual(Errors.MISSING_SWAP_CONTRACTS()); + }); + + test('should throw when contract version is not supported', async () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + Contracts.maxVersion = -1n; + + await expect( + contracts.init( + setup.provider as unknown as InjectedProvider, + setup.signer, + {} as any, + ), + ).rejects.toEqual( + Errors.INVALID_ETHEREUM_CONFIGURATION( + `unsupported contract version ${await setupContracts.etherSwap.version()}`, + ), + ); + }); + }); +}); diff --git a/test/unit/wallet/ethereum/ConsolidatedEventHandler.spec.ts b/test/unit/wallet/ethereum/ConsolidatedEventHandler.spec.ts new file mode 100644 index 00000000..6112fda8 --- /dev/null +++ b/test/unit/wallet/ethereum/ConsolidatedEventHandler.spec.ts @@ -0,0 +1,68 @@ +import ConsolidatedEventHandler from '../../../../lib/wallet/ethereum/ConsolidatedEventHandler'; +import ContractEventHandler from '../../../../lib/wallet/ethereum/contracts/ContractEventHandler'; + +describe('ConsolidatedEventHandler', () => { + const emittersV3 = {}; + const eventsV3 = { + rescan: jest.fn(), + on: jest.fn().mockImplementation((event, callback) => { + emittersV3[event] = callback; + }), + } as unknown as ContractEventHandler; + + const emittersV4 = {}; + const eventsV4 = { + rescan: jest.fn(), + on: jest.fn().mockImplementation((event, callback) => { + emittersV4[event] = callback; + }), + } as unknown as ContractEventHandler; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test.each` + event + ${'eth.lockup'} + ${'eth.claim'} + ${'eth.refund'} + ${'erc20.lockup'} + ${'erc20.claim'} + ${'erc20.refund'} + `('should forward event $event', ({ event }) => { + expect.assertions(6); + + const handler = new ConsolidatedEventHandler(); + handler.register(eventsV3); + handler.register(eventsV4); + + expect(eventsV3.on).toHaveBeenCalledTimes(6); + expect(eventsV3.on).toHaveBeenCalledWith(event, expect.any(Function)); + + expect(emittersV3[event]).toBeDefined(); + expect(emittersV4[event]).toBeDefined(); + + const dataV3 = 'dataV3'; + const dataV4 = 'dataV4'; + + handler.on(event, (emittedData) => { + expect(emittedData === dataV3 || emittedData === dataV4).toEqual(true); + }); + + emittersV3[event](dataV3); + emittersV4[event](dataV4); + }); + + test('should rescan all event handlers', async () => { + const handler = new ConsolidatedEventHandler(); + handler.register(eventsV3); + handler.register(eventsV4); + + const startHeight = 21; + await handler.rescan(startHeight); + + expect(eventsV3.rescan).toHaveBeenCalledWith(startHeight); + expect(eventsV4.rescan).toHaveBeenCalledWith(startHeight); + }); +});