diff --git a/.cargo/config.toml b/.cargo/config.toml index b16c63ead9..015a4b34d2 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -13,8 +13,7 @@ # - mocktopus: nightly only dependency. # - mocktopus_macros: nightly only dependency. # - docker_tests_main: Depends on `custom_test_frameworks` and `test`. -# - docker_tests_sia_unique: Depends on `custom_test_frameworks` and `test`. -RUSTC_BOOTSTRAP = "mm2_state_machine,mm2_err_handle,mocktopus,mocktopus_macros,docker_tests_main,docker_tests_sia_unique" +RUSTC_BOOTSTRAP = "mm2_state_machine,mm2_err_handle,mocktopus,mocktopus_macros,docker_tests_main" JEMALLOC_SYS_WITH_MALLOC_CONF = "background_thread:true,narenas:1,tcache:false,dirty_decay_ms:0,muzzy_decay_ms:0,metadata_thp:auto" diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index e599dccc99..d3a171cd43 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -60,6 +60,7 @@ jobs: SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ + shasum -a 256 ./$SAFE_DIR_NAME/$NAME | tee ./$SAFE_DIR_NAME/$NAME.sha256 - name: Upload build artifact env: @@ -130,6 +131,7 @@ jobs: SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ + shasum -a 256 ./$SAFE_DIR_NAME/$NAME | tee ./$SAFE_DIR_NAME/$NAME.sha256 - name: Upload build artifact env: @@ -188,6 +190,7 @@ jobs: SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ + shasum -a 256 ./$SAFE_DIR_NAME/$NAME | tee ./$SAFE_DIR_NAME/$NAME.sha256 - name: Upload build artifact env: @@ -252,6 +255,7 @@ jobs: SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ + shasum -a 256 ./$SAFE_DIR_NAME/$NAME | tee ./$SAFE_DIR_NAME/$NAME.sha256 - name: Upload build artifact env: @@ -304,6 +308,12 @@ jobs: mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ + # Generate the SHA256 hash for the zip file + Get-FileHash ./$SAFE_DIR_NAME/$NAME -Algorithm SHA256 | Format-Table Hash | Out-File "./$SAFE_DIR_NAME/$NAME.sha256" -Encoding ascii + + # Display the SHA256 hash in Actions logs + Get-Content "./$SAFE_DIR_NAME/$NAME.sha256" + - name: Upload build artifact env: FILE_SERVER_HOST: ${{ secrets.FILE_SERVER_HOST }} @@ -356,6 +366,7 @@ jobs: SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ + shasum -a 256 ./$SAFE_DIR_NAME/$NAME | tee ./$SAFE_DIR_NAME/$NAME.sha256 - name: Upload build artifact env: @@ -421,6 +432,7 @@ jobs: SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ + shasum -a 256 ./$SAFE_DIR_NAME/$NAME | tee ./$SAFE_DIR_NAME/$NAME.sha256 - name: Upload build artifact env: @@ -474,6 +486,7 @@ jobs: SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ + shasum -a 256 ./$SAFE_DIR_NAME/$NAME | tee ./$SAFE_DIR_NAME/$NAME.sha256 - name: Upload build artifact env: @@ -541,6 +554,7 @@ jobs: SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ + shasum -a 256 ./$SAFE_DIR_NAME/$NAME | tee ./$SAFE_DIR_NAME/$NAME.sha256 - name: Upload build artifact env: @@ -608,6 +622,7 @@ jobs: SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ + shasum -a 256 ./$SAFE_DIR_NAME/$NAME | tee ./$SAFE_DIR_NAME/$NAME.sha256 - name: Upload build artifact env: diff --git a/Cargo.lock b/Cargo.lock index 1145166c8f..94db1b05a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -316,8 +316,8 @@ dependencies = [ "bytes", "futures-util", "http 0.2.12", - "http-body", - "hyper", + "http-body 0.4.5", + "hyper 0.14.26", "itoa", "matchit", "memchr", @@ -342,7 +342,7 @@ dependencies = [ "bytes", "futures-util", "http 0.2.12", - "http-body", + "http-body 0.4.5", "mime", "rustversion", "tower-layer", @@ -465,12 +465,14 @@ dependencies = [ [[package]] name = "bip39" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f2635620bf0b9d4576eb7bb9a38a55df78bd1205d26fa994b25911a69f212f" +checksum = "43d193de1f7487df1914d3a568b772458861d33f9c54249612cc2893d6915054" dependencies = [ - "bitcoin_hashes", + "bitcoin_hashes 0.13.0", "rand_core 0.6.4", + "serde", + "unicode-normalization", "zeroize", ] @@ -481,16 +483,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0694ea59225b0c5f3cb405ff3f670e4828358ed26aec49dc352f730f0cb1a8a3" dependencies = [ "bech32", - "bitcoin_hashes", + "bitcoin_hashes 0.11.0", "secp256k1 0.24.3", ] +[[package]] +name = "bitcoin-internals" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" + [[package]] name = "bitcoin_hashes" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90064b8dee6815a6470d60bad07bbbaee885c0e12d04177138fa3291a01b7bc4" +[[package]] +name = "bitcoin_hashes" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" +dependencies = [ + "bitcoin-internals", + "hex-conservative", +] + [[package]] name = "bitcrypto" version = "0.1.0" @@ -644,13 +662,53 @@ dependencies = [ "subtle", ] +[[package]] +name = "bollard" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aed08d3adb6ebe0eff737115056652670ae290f177759aac19c30456135f94c" +dependencies = [ + "base64 0.22.1", + "bollard-stubs", + "bytes", + "futures-core", + "futures-util", + "hex", + "home", + "http 1.1.0", + "http-body-util", + "hyper 1.6.0", + "hyper-named-pipe", + "hyper-rustls 0.26.0", + "hyper-util", + "hyperlocal-next", + "log", + "pin-project-lite 0.2.16", + "rustls 0.22.4", + "rustls-native-certs 0.7.3", + "rustls-pemfile 2.2.0", + "rustls-pki-types", + "serde", + "serde_derive", + "serde_json", + "serde_repr", + "serde_urlencoded", + "thiserror 1.0.69", + "tokio", + "tokio-util", + "tower-service", + "url", + "winapi", +] + [[package]] name = "bollard-stubs" -version = "1.42.0-rc.3" +version = "1.44.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed59b5c00048f48d7af971b71f800fdf23e858844a6f9e4d32ca72e9399e7864" +checksum = "709d9aa1c37abb89d40f19f5d0ad6f0d88cb1581264e571c9350fc5bb89cf1c5" dependencies = [ "serde", + "serde_repr", "serde_with", ] @@ -891,7 +949,7 @@ dependencies = [ "bincode", "bip32", "bitcoin", - "bitcoin_hashes", + "bitcoin_hashes 0.11.0", "bitcrypto", "blake2b_simd", "bs58 0.4.0", @@ -907,6 +965,7 @@ dependencies = [ "db_common", "derive_more", "ed25519-dalek 1.0.1", + "ed25519-dalek-bip32", "enum_derives", "ethabi", "ethcore-transaction", @@ -921,7 +980,7 @@ dependencies = [ "gstuff", "hex", "http 0.2.12", - "hyper", + "hyper 0.14.26", "hyper-rustls 0.24.2", "itertools", "js-sys", @@ -995,7 +1054,7 @@ dependencies = [ "spl-token", "spv_validation", "tendermint-rpc", - "thiserror 1.0.40", + "thiserror 1.0.69", "time", "timed-map", "tokio", @@ -1082,8 +1141,8 @@ dependencies = [ "gstuff", "hex", "http 0.2.12", - "http-body", - "hyper", + "http-body 0.4.5", + "hyper 0.14.26", "hyper-rustls 0.24.2", "itertools", "js-sys", @@ -1205,7 +1264,7 @@ dependencies = [ "signature 2.2.0", "subtle-encoding", "tendermint", - "thiserror 1.0.40", + "thiserror 1.0.69", ] [[package]] @@ -1378,6 +1437,7 @@ dependencies = [ "cipher", "common", "derive_more", + "ed25519-dalek-bip32", "enum-primitive-derive", "enum_derives", "futures 0.3.31", @@ -1404,6 +1464,7 @@ dependencies = [ "serde_derive", "serde_json", "sha2 0.10.9", + "thiserror 1.0.69", "tokio", "trezor", "wasm-bindgen-test", @@ -1589,9 +1650,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.13.4" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ "darling_core", "darling_macro", @@ -1599,27 +1660,27 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.13.4" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote 1.0.37", "strsim", - "syn 1.0.95", + "syn 2.0.87", ] [[package]] name = "darling_macro" -version = "0.13.4" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote 1.0.37", - "syn 1.0.95", + "syn 2.0.87", ] [[package]] @@ -1680,8 +1741,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", + "serde", ] +[[package]] +name = "derivation-path" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5c37193a1db1d8ed868c03ec7b152175f26160a5b740e5e484143877e0adf0" + [[package]] name = "derive_more" version = "0.99.20" @@ -1722,7 +1790,16 @@ version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e69600ff1703123957937708eb27f7a564e48885c537782722ed0ba3189ce1d7" dependencies = [ - "dirs-sys", + "dirs-sys 0.3.6", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", ] [[package]] @@ -1736,12 +1813,41 @@ dependencies = [ "winapi", ] +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "docker_credential" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d89dfcba45b4afad7450a99b39e751590463e45c04728cf555d36bb66940de8" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + [[package]] name = "dtoa" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5caaa75cbd2b960ff1e5392d2cfb1f44717fffe12fc1f32b7b5d1267f99732a6" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "ecdsa" version = "0.16.9" @@ -1819,6 +1925,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ed25519-dalek-bip32" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b49a684b133c4980d7ee783936af771516011c8cd15f429dbda77245e282f03" +dependencies = [ + "derivation-path", + "ed25519-dalek 2.1.1", + "hmac 0.12.1", + "sha2 0.10.9", +] + [[package]] name = "edit-distance" version = "2.1.0" @@ -1939,7 +2057,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1955,7 +2073,7 @@ dependencies = [ "serde", "serde_json", "sha3 0.10.8", - "thiserror 1.0.40", + "thiserror 1.0.69", "uint", ] @@ -2610,6 +2728,15 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] + +[[package]] +name = "hex-conservative" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212ab92002354b4819390025006c897e8140934349e8635c9b077f47b4dcbd20" [[package]] name = "hex_fmt" @@ -2678,6 +2805,15 @@ dependencies = [ "hmac 0.8.1", ] +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "hostname" version = "0.3.1" @@ -2722,6 +2858,29 @@ dependencies = [ "pin-project-lite 0.2.16", ] +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.1.0", + "http-body 1.0.1", + "pin-project-lite 0.2.16", +] + [[package]] name = "httparse" version = "1.10.1" @@ -2767,7 +2926,7 @@ dependencies = [ "futures-util", "h2", "http 0.2.12", - "http-body", + "http-body 0.4.5", "httparse", "httpdate", "itoa", @@ -2779,6 +2938,40 @@ dependencies = [ "want", ] +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "httparse", + "itoa", + "pin-project-lite 0.2.16", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-named-pipe" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" +dependencies = [ + "hex", + "hyper 1.6.0", + "hyper-util", + "pin-project-lite 0.2.16", + "tokio", + "tower-service", + "winapi", +] + [[package]] name = "hyper-rustls" version = "0.23.0" @@ -2786,7 +2979,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" dependencies = [ "http 0.2.12", - "hyper", + "hyper 0.14.26", "rustls 0.20.4", "tokio", "tokio-rustls 0.23.2", @@ -2800,25 +2993,80 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", "http 0.2.12", - "hyper", + "hyper 0.14.26", "rustls 0.21.10", "tokio", "tokio-rustls 0.24.1", "webpki-roots 0.25.4", ] +[[package]] +name = "hyper-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" +dependencies = [ + "futures-util", + "http 1.1.0", + "hyper 1.6.0", + "hyper-util", + "log", + "rustls 0.22.4", + "rustls-native-certs 0.7.3", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.25.0", + "tower-service", +] + [[package]] name = "hyper-timeout" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "hyper", + "hyper 0.14.26", "pin-project-lite 0.2.16", "tokio", "tokio-io-timeout", ] +[[package]] +name = "hyper-util" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "hyper 1.6.0", + "libc", + "pin-project-lite 0.2.16", + "socket2 0.6.0", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "hyperlocal-next" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acf569d43fa9848e510358c07b80f4adf34084ddc28c6a4a651ee8474c070dcc" +dependencies = [ + "hex", + "http-body-util", + "hyper 1.6.0", + "hyper-util", + "pin-project-lite 0.2.16", + "tokio", + "tower-service", +] + [[package]] name = "iana-time-zone" version = "0.1.53" @@ -2955,6 +3203,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg 1.1.0", "hashbrown 0.12.1", + "serde", ] [[package]] @@ -2965,6 +3214,7 @@ checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" dependencies = [ "equivalent", "hashbrown 0.14.3", + "serde", ] [[package]] @@ -3149,7 +3399,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.9", - "thiserror 1.0.40", + "thiserror 1.0.69", "timed-map", "tokio", "wasm-bindgen", @@ -3312,7 +3562,7 @@ dependencies = [ "rand 0.8.5", "rw-stream-sink", "smallvec", - "thiserror 1.0.40", + "thiserror 1.0.69", "unsigned-varint", "void", ] @@ -3348,7 +3598,7 @@ dependencies = [ "quick-protobuf-codec", "rand 0.8.5", "smallvec", - "thiserror 1.0.40", + "thiserror 1.0.69", ] [[package]] @@ -3399,7 +3649,7 @@ dependencies = [ "quick-protobuf", "quick-protobuf-codec", "smallvec", - "thiserror 1.0.40", + "thiserror 1.0.69", "void", ] @@ -3418,7 +3668,7 @@ dependencies = [ "quick-protobuf", "rand 0.8.5", "sha2 0.10.9", - "thiserror 1.0.40", + "thiserror 1.0.69", "zeroize", ] @@ -3477,7 +3727,7 @@ dependencies = [ "sha2 0.10.9", "snow", "static_assertions", - "thiserror 1.0.40", + "thiserror 1.0.69", "x25519-dalek 1.1.0", "zeroize", ] @@ -3606,7 +3856,7 @@ dependencies = [ "either", "futures 0.3.31", "libp2p-core", - "thiserror 1.0.40", + "thiserror 1.0.69", "tracing", "yamux 0.12.1", "yamux 0.13.1", @@ -3710,7 +3960,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9680857590c3529cf8c7d32b04501f215f2bf1e029fdfa22f4112f66c1741e4" dependencies = [ "bech32", - "bitcoin_hashes", + "bitcoin_hashes 0.11.0", "lightning", "num-traits", "secp256k1 0.24.3", @@ -3835,9 +4085,9 @@ source = "git+https://github.com/KomodoPlatform/mm2-parity-ethereum.git?rev=mm2- [[package]] name = "memchr" -version = "2.5.0" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memoffset" @@ -3877,13 +4127,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a4964177ddfdab1e3a2b37aec7cf320e14169abb0ed73999f558136409178d5" dependencies = [ "base64 0.21.7", - "hyper", + "hyper 0.14.26", "indexmap 1.9.3", "ipnet", "metrics", "metrics-util", "quanta", - "thiserror 1.0.40", + "thiserror 1.0.69", "tokio", "tracing", ] @@ -4150,6 +4400,7 @@ version = "0.1.0" dependencies = [ "async-std", "async-trait", + "base64 0.21.7", "bitcrypto", "blake2", "bytes", @@ -4181,7 +4432,7 @@ dependencies = [ "hex", "http 0.2.12", "hw_common", - "hyper", + "hyper 0.14.26", "instant", "itertools", "js-sys", @@ -4213,6 +4464,7 @@ dependencies = [ "rand 0.7.3", "rcgen", "regex", + "reqwest", "rlp", "rmp-serde", "rpc", @@ -4283,7 +4535,7 @@ dependencies = [ "common", "derive_more", "futures 0.3.31", - "hyper", + "hyper 0.14.26", "hyper-rustls 0.24.2", "itertools", "metrics", @@ -4311,9 +4563,9 @@ dependencies = [ "futures-util", "gstuff", "http 0.2.12", - "http-body", + "http-body 0.4.5", "httparse", - "hyper", + "hyper 0.14.26", "js-sys", "lazy_static", "mm2_core", @@ -4326,7 +4578,7 @@ dependencies = [ "rustls 0.21.10", "serde", "serde_json", - "thiserror 1.0.40", + "thiserror 1.0.69", "tokio", "tokio-rustls 0.24.1", "tonic", @@ -4557,7 +4809,7 @@ dependencies = [ "anyhow", "byteorder", "paste", - "thiserror 1.0.40", + "thiserror 1.0.69", ] [[package]] @@ -4571,7 +4823,7 @@ dependencies = [ "log", "netlink-packet-core", "netlink-sys", - "thiserror 1.0.40", + "thiserror 1.0.69", "tokio", ] @@ -4745,6 +4997,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "ordered-float" version = "3.7.0" @@ -4780,7 +5038,7 @@ dependencies = [ "relay_rpc", "serde", "serde_json", - "thiserror 1.0.40", + "thiserror 1.0.69", "url", "wc_common", ] @@ -4871,6 +5129,31 @@ dependencies = [ "windows-sys 0.32.0", ] +[[package]] +name = "parse-display" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914a1c2265c98e2446911282c6ac86d8524f495792c38c5bd884f80499c7538a" +dependencies = [ + "parse-display-derive", + "regex", + "regex-syntax", +] + +[[package]] +name = "parse-display-derive" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae7800a4c974efd12df917266338e79a7a74415173caf7e70aa0a0707345281" +dependencies = [ + "proc-macro2", + "quote 1.0.37", + "regex", + "regex-syntax", + "structmeta", + "syn 2.0.87", +] + [[package]] name = "password-hash" version = "0.5.0" @@ -5090,7 +5373,7 @@ dependencies = [ name = "primitives" version = "0.1.0" dependencies = [ - "bitcoin_hashes", + "bitcoin_hashes 0.11.0", "byteorder", "rustc-hex", "uint", @@ -5102,7 +5385,7 @@ version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" dependencies = [ - "thiserror 1.0.40", + "thiserror 1.0.69", "toml 0.5.7", ] @@ -5310,7 +5593,7 @@ dependencies = [ "asynchronous-codec", "bytes", "quick-protobuf", - "thiserror 1.0.40", + "thiserror 1.0.69", "unsigned-varint", ] @@ -5607,29 +5890,41 @@ dependencies = [ [[package]] name = "ref-cast" -version = "1.0.3" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17626b2f4bcf35b84bf379072a66e28cfe5c3c6ae58b38e4914bb8891dabece" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.3" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c523ccaed8ac4b0288948849a350b37d3035827413c458b6a40ddb614bb4f72" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote 1.0.37", - "syn 1.0.95", + "syn 2.0.87", ] [[package]] name = "regex" -version = "1.8.4" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick 1.0.2", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick 1.0.2", "memchr", @@ -5638,9 +5933,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.2" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "relay_client" @@ -5659,7 +5954,7 @@ dependencies = [ "serde", "serde_json", "serde_qs", - "thiserror 1.0.40", + "thiserror 1.0.69", "tokio", "tokio-tungstenite-wasm", "tokio-util", @@ -5693,7 +5988,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "strum", - "thiserror 1.0.40", + "thiserror 1.0.69", "url", ] @@ -5710,8 +6005,8 @@ dependencies = [ "futures-util", "h2", "http 0.2.12", - "http-body", - "hyper", + "http-body 0.4.5", + "hyper 0.14.26", "hyper-rustls 0.23.0", "ipnet", "js-sys", @@ -5884,7 +6179,7 @@ dependencies = [ "netlink-proto", "netlink-sys", "nix", - "thiserror 1.0.40", + "thiserror 1.0.69", "tokio", ] @@ -5956,7 +6251,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -5983,6 +6278,20 @@ dependencies = [ "sct", ] +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring 0.17.3", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + [[package]] name = "rustls-native-certs" version = "0.6.3" @@ -5995,6 +6304,19 @@ dependencies = [ "security-framework", ] +[[package]] +name = "rustls-native-certs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +dependencies = [ + "openssl-probe", + "rustls-pemfile 2.2.0", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "0.2.1" @@ -6013,11 +6335,23 @@ dependencies = [ "base64 0.21.7", ] +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" -version = "1.13.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] [[package]] name = "rustls-webpki" @@ -6039,6 +6373,17 @@ dependencies = [ "untrusted 0.9.0", ] +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring 0.17.3", + "rustls-pki-types", + "untrusted 0.9.0", +] + [[package]] name = "rustversion" version = "1.0.14" @@ -6103,6 +6448,30 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "scoped-tls" version = "1.0.0" @@ -6175,7 +6544,7 @@ version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b1629c9c557ef9b293568b338dddfc8208c98a18c59d722a9d53f859d9c9b62" dependencies = [ - "bitcoin_hashes", + "bitcoin_hashes 0.11.0", "secp256k1-sys 0.6.1", ] @@ -6344,7 +6713,7 @@ checksum = "8cac3f1e2ca2fe333923a1ae72caca910b98ed0630bb35ef6f8c8517d6e81afa" dependencies = [ "percent-encoding", "serde", - "thiserror 1.0.40", + "thiserror 1.0.69", ] [[package]] @@ -6381,24 +6750,34 @@ dependencies = [ [[package]] name = "serde_with" -version = "1.14.0" +version = "3.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" +checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e" dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.2.3", + "schemars 0.9.0", + "schemars 1.0.4", "serde", + "serde_derive", + "serde_json", "serde_with_macros", + "time", ] [[package]] name = "serde_with_macros" -version = "1.5.2" +version = "3.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" +checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e" dependencies = [ "darling", "proc-macro2", "quote 1.0.37", - "syn 1.0.95", + "syn 2.0.87", ] [[package]] @@ -6506,21 +6885,33 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "sia-rust" version = "0.1.0" -source = "git+https://github.com/KomodoPlatform/sia-rust?rev=9f188b80b3213bcb604e7619275251ce08fae808#9f188b80b3213bcb604e7619275251ce08fae808" +source = "git+https://github.com/KomodoPlatform/sia-rust?branch=refactor%2Frc-cleanup#934e4d03954be62b13db42a15e6c3c281d2ec893" dependencies = [ + "async-trait", "base64 0.21.7", "blake2b_simd", "chrono", + "curve25519-dalek 3.2.0", "derive_more", "ed25519-dalek 1.0.1", + "futures 0.3.31", + "getrandom 0.2.16", "hex", + "http 0.2.12", + "js-sys", + "log", "nom", + "percent-encoding", "reqwest", "rustc-hex", "serde", + "serde-wasm-bindgen", "serde_json", - "serde_with", + "thiserror 1.0.69", "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] @@ -6578,7 +6969,7 @@ checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" dependencies = [ "num-bigint", "num-traits", - "thiserror 1.0.40", + "thiserror 1.0.69", "time", ] @@ -7645,7 +8036,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1008d95d2ec2d062959352527be30e10fec42a1aa5e5a48d990a5ff0fb9bdc0" dependencies = [ "anyhow", - "thiserror 1.0.40", + "thiserror 1.0.69", ] [[package]] @@ -7670,9 +8061,32 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "structmeta" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e1575d8d40908d70f6fd05537266b90ae71b15dbbe7a8b7dffa2b759306d329" +dependencies = [ + "proc-macro2", + "quote 1.0.37", + "structmeta-derive", + "syn 2.0.87", +] + +[[package]] +name = "structmeta-derive" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" +dependencies = [ + "proc-macro2", + "quote 1.0.37", + "syn 2.0.87", +] [[package]] name = "strum" @@ -7901,7 +8315,7 @@ dependencies = [ "tendermint", "tendermint-config", "tendermint-proto", - "thiserror 1.0.40", + "thiserror 1.0.69", "time", "url", "uuid", @@ -7926,28 +8340,37 @@ dependencies = [ [[package]] name = "testcontainers" -version = "0.15.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d2931d7f521af5bae989f716c3fa43a6af9af7ec7a5e21b59ae40878cec00" +checksum = "025e0ac563d543e0354d984540e749859a83dbe5c0afb8d458dc48d91cef2d6a" dependencies = [ + "async-trait", + "bollard", "bollard-stubs", + "bytes", + "dirs", + "docker_credential", "futures 0.3.31", - "hex", - "hmac 0.12.1", "log", - "rand 0.8.5", + "memchr", + "parse-display", "serde", "serde_json", - "sha2 0.10.9", + "serde_with", + "thiserror 1.0.69", + "tokio", + "tokio-stream", + "tokio-util", + "url", ] [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl 1.0.40", + "thiserror-impl 1.0.69", ] [[package]] @@ -7961,9 +8384,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote 1.0.37", @@ -8117,11 +8540,22 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.4", + "rustls-pki-types", + "tokio", +] + [[package]] name = "tokio-stream" -version = "0.1.8" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite 0.2.16", @@ -8137,7 +8571,7 @@ dependencies = [ "futures-util", "log", "rustls 0.20.4", - "rustls-native-certs", + "rustls-native-certs 0.6.3", "tokio", "tokio-rustls 0.23.2", "tungstenite", @@ -8155,7 +8589,7 @@ dependencies = [ "http 0.2.12", "httparse", "js-sys", - "thiserror 1.0.40", + "thiserror 1.0.69", "tokio", "tokio-tungstenite", "wasm-bindgen", @@ -8232,8 +8666,8 @@ dependencies = [ "flate2", "h2", "http 0.2.12", - "http-body", - "hyper", + "http-body 0.4.5", + "hyper 0.14.26", "hyper-timeout", "percent-encoding", "pin-project", @@ -8426,7 +8860,7 @@ dependencies = [ "rand 0.8.5", "smallvec", "socket2 0.4.9", - "thiserror 1.0.40", + "thiserror 1.0.69", "tinyvec", "tokio", "tracing", @@ -8447,7 +8881,7 @@ dependencies = [ "parking_lot", "resolv-conf", "smallvec", - "thiserror 1.0.40", + "thiserror 1.0.69", "tokio", "tracing", "trust-dns-proto", @@ -8474,7 +8908,7 @@ dependencies = [ "rand 0.8.5", "rustls 0.20.4", "sha-1", - "thiserror 1.0.40", + "thiserror 1.0.69", "url", "utf-8", "webpki", @@ -8836,7 +9270,7 @@ source = "git+https://github.com/komodoplatform/walletconnectrust?tag=k-0.1.3#e2 dependencies = [ "base64 0.21.7", "chacha20poly1305", - "thiserror 1.0.40", + "thiserror 1.0.69", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8e664bf807..c0e111bf85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,7 +64,7 @@ bech32 = "0.9.1" bs58 = "0.4.0" bigdecimal = { version = "0.3", features = ["serde"] } bip32 = { version = "0.2.2", default-features = false, features = ["alloc", "secp256k1-ffi"] } -bip39 = { version = "2.0.0", features = ["rand_core", "zeroize"], default-features = false } +bip39 = { version = "2.1.0", features = ["rand_core", "zeroize", "std"], default-features = false } bitcoin = "0.29" bitcoin_hashes = "0.11" blake2 = "0.10.6" @@ -85,6 +85,7 @@ crc32fast = { version = "1.3.2", features = ["std", "nightly"] } derive_more = "0.99.20" directories = "5.0" ed25519-dalek = { version = "1.0.1", features = ["serde"] } +ed25519-dalek-bip32 = "0.3.0" either = "1.6" enum-primitive-derive = "0.2" env_logger = { version = "0.11", default-features = false } @@ -155,6 +156,7 @@ quote = "1.0" regex = "1" relay_client = { git = "https://github.com/komodoplatform/walletconnectrust", tag = "k-0.1.3" } relay_rpc = { git = "https://github.com/komodoplatform/walletconnectrust", tag = "k-0.1.3" } +reqwest = { version = "0.11.9", default-features = false } rand = { version = "0.7", default-features = false, features = ["std", "small_rng", "wasm-bindgen"] } rcgen = "0.10" ripemd160 = "0.9.0" @@ -173,13 +175,13 @@ serde = { version = "1", default-features = false } serde_bytes = "0.11.5" serde_derive = { version = "1", default-features = false } serde_json = { version = "1.0.140", features = ["preserve_order", "raw_value"] } -serde_with = "1.14.0" +serde_with = "3.14.1" serde_repr = "0.1.6" serde-wasm-bindgen = "0.4.3" sha-1 = "0.9" sha2 = "0.10" sha3 = "0.9" -sia-rust = { git = "https://github.com/KomodoPlatform/sia-rust", rev = "9f188b80b3213bcb604e7619275251ce08fae808" } +sia-rust = { git = "https://github.com/KomodoPlatform/sia-rust", branch = "refactor/rc-cleanup"} siphasher = "0.1.1" smallvec = "1.6.1" sp-runtime-interface = { version = "6.0.0", default-features = false, features = ["disable_target_static_assertions"] } @@ -190,7 +192,8 @@ sysinfo = "0.28" tempfile = "3.7.1" # using the same version as cosmrs tendermint-rpc = { version = "0.35", default-features = false } -testcontainers = "0.15.0" +# Todo: now that toolchain is updated, maybe this can be updated more and used with Sia tests +testcontainers = "0.17.0" tiny-bip39 = "0.8.0" thiserror = "1.0.40" time = "0.3.36" @@ -240,10 +243,11 @@ panic = 'unwind' [profile.dev] opt-level = 0 debug = 1 -debug-assertions = false +debug-assertions = false # TODO: Enable debug-assertions in the future panic = 'unwind' -incremental = true +incremental = true # TODO: this is probably wasting resources in Github Actions, `CARGO_INCREMENTAL` can be used there codegen-units = 256 + [profile.release.package.mocktopus] opt-level = 1 # TODO: MIR fails on optimizing this dependency, remove that.. diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index 4c1dc647e6..0a02126de3 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -5,10 +5,6 @@ edition = "2018" [features] zhtlc-native-tests = [] -enable-sia = [ - "dep:blake2b_simd", - "dep:sia-rust" -] default = [] run-docker-tests = [] for-tests = ["dep:mocktopus"] @@ -29,7 +25,7 @@ base64.workspace = true bip32.workspace = true bitcoin_hashes.workspace = true bitcrypto = { path = "../mm2_bitcoin/crypto" } -blake2b_simd = { workspace = true, optional = true } +blake2b_simd = { workspace = true } bs58.workspace = true byteorder.workspace = true bytes.workspace = true @@ -44,6 +40,7 @@ crypto = { path = "../crypto" } db_common = { path = "../db_common" } derive_more.workspace = true ed25519-dalek.workspace = true +ed25519-dalek-bip32 = { workspace = true } enum_derives = { path = "../derives/enum_derives" } kdf_walletconnect = { path = "../kdf_walletconnect" } ethabi.workspace = true @@ -88,6 +85,7 @@ rlp.workspace = true rmp-serde.workspace = true rpc = { path = "../mm2_bitcoin/rpc" } rpc_task = { path = "../rpc_task" } +sia-rust = { workspace = true } script = { path = "../mm2_bitcoin/script" } secp256k1.workspace = true ser_error = { path = "../derives/ser_error" } @@ -98,7 +96,6 @@ serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } serde_with.workspace = true serialization = { path = "../mm2_bitcoin/serialization" } serialization_derive = { path = "../mm2_bitcoin/serialization_derive" } -sia-rust = { git = "https://github.com/KomodoPlatform/sia-rust", rev = "9f188b80b3213bcb604e7619275251ce08fae808", optional = true } spv_validation = { path = "../mm2_bitcoin/spv_validation" } sha2.workspace = true sha3.workspace = true diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 27d6e487eb..1bb740ea6f 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -5894,7 +5894,7 @@ impl EthCoin { let ctx = MmArc::from_weak(&self.ctx).expect("No context"); let platform_coin = lp_coinfind_or_err(&ctx, platform).await?; match platform_coin { - MmCoinEnum::EthCoin(eth_coin) => Ok(eth_coin), + MmCoinEnum::EthCoinVariant(eth_coin) => Ok(eth_coin), _ => MmError::err(CoinFindError::NoSuchCoin { coin: platform.to_string(), }), @@ -6960,7 +6960,7 @@ fn get_valid_nft_addr_to_withdraw( token_add: &str, ) -> MmResult<(Address, Address, EthCoin), GetValidEthWithdrawAddError> { let eth_coin = match coin_enum { - MmCoinEnum::EthCoin(eth_coin) => eth_coin, + MmCoinEnum::EthCoinVariant(eth_coin) => eth_coin, _ => { return MmError::err(GetValidEthWithdrawAddError::CoinDoesntSupportNftWithdraw { coin: coin_enum.ticker().to_owned(), diff --git a/mm2src/coins/eth/erc20.rs b/mm2src/coins/eth/erc20.rs index 53c6b89cdf..35b0a534d9 100644 --- a/mm2src/coins/eth/erc20.rs +++ b/mm2src/coins/eth/erc20.rs @@ -123,7 +123,7 @@ pub async fn get_enabled_erc20_by_platform_and_contract( let coins = cctx.coins.lock().await; Ok(coins.values().find_map(|coin| match &coin.inner { - MmCoinEnum::EthCoin(eth_coin) + MmCoinEnum::EthCoinVariant(eth_coin) if eth_coin.platform_ticker() == platform && eth_coin.erc20_token_address().as_ref() == Some(contract_address) => { diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index 37790bddbf..b4cea9f129 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -1147,7 +1147,7 @@ fn test_gas_limit_conf() { }); let coin = block_on(lp_coininit(&ctx, ETH, &req)).unwrap(); let eth_coin = match coin { - MmCoinEnum::EthCoin(eth_coin) => eth_coin, + MmCoinEnum::EthCoinVariant(eth_coin) => eth_coin, _ => panic!("not eth coin"), }; assert!( diff --git a/mm2src/coins/eth/fee_estimation/rpc.rs b/mm2src/coins/eth/fee_estimation/rpc.rs index 0d8deab749..148f13366d 100644 --- a/mm2src/coins/eth/fee_estimation/rpc.rs +++ b/mm2src/coins/eth/fee_estimation/rpc.rs @@ -38,7 +38,7 @@ pub async fn get_eth_estimated_fee_per_gas( req: GetFeeEstimationRequest, ) -> MmResult { match lp_coinfind(&ctx, &req.coin).await { - Ok(Some(MmCoinEnum::EthCoin(coin))) => { + Ok(Some(MmCoinEnum::EthCoinVariant(coin))) => { let use_simple = matches!(req.estimator_type, EstimatorType::Simple); let fee = coin .get_eip1559_gas_fee(use_simple) diff --git a/mm2src/coins/hd_wallet/mod.rs b/mm2src/coins/hd_wallet/mod.rs index 2eee8c0749..abfb944771 100644 --- a/mm2src/coins/hd_wallet/mod.rs +++ b/mm2src/coins/hd_wallet/mod.rs @@ -301,7 +301,7 @@ where #[derive(Debug)] pub struct HDWallet where - HDAccount: HDAccountOps + Clone + Send + Sync, + HDAccount: HDAccountOps + Send + Sync, { /// A unique identifier for the HD wallet derived from the master public key. /// Specifically, it's the RIPEMD160 hash of the SHA256 hash of the master pubkey. diff --git a/mm2src/coins/hd_wallet/pubkey.rs b/mm2src/coins/hd_wallet/pubkey.rs index ac28de3cd5..07c80fed5d 100644 --- a/mm2src/coins/hd_wallet/pubkey.rs +++ b/mm2src/coins/hd_wallet/pubkey.rs @@ -1,5 +1,3 @@ -#[cfg(feature = "enable-sia")] -use crate::siacoin::sia_hd_wallet::Ed25519ExtendedPublicKey; use crate::CoinProtocol; use super::*; @@ -36,17 +34,6 @@ impl ExtendedPublicKeyOps for Secp256k1ExtendedPublicKey { } } -#[cfg(feature = "enable-sia")] -impl ExtendedPublicKeyOps for Ed25519ExtendedPublicKey { - fn derive_child(&self, child_number: ChildNumber) -> Result { - self.derive_child(child_number) - } - - fn to_string(&self, prefix: Prefix) -> String { - self.to_string(prefix) - } -} - /// This trait should be implemented for coins /// to support extracting extended public keys from any depth. /// The extraction can be from either an internal or external wallet. diff --git a/mm2src/coins/hd_wallet/wallet_ops.rs b/mm2src/coins/hd_wallet/wallet_ops.rs index 65b1de223e..a9ede8e4e1 100644 --- a/mm2src/coins/hd_wallet/wallet_ops.rs +++ b/mm2src/coins/hd_wallet/wallet_ops.rs @@ -6,7 +6,7 @@ use crypto::{Bip44Chain, HDPathToCoin}; #[async_trait] pub trait HDWalletOps { /// Any type that represents a Hierarchical Deterministic (HD) wallet account. - type HDAccount: HDAccountOps + Clone + Send + Sync; + type HDAccount: HDAccountOps + Send + Sync; /// Returns the coin type associated with this HD Wallet. /// diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 54cc41fa1d..76ed3117b1 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -275,10 +275,8 @@ pub use test_coin::TestCoin; pub mod tx_history_storage; -#[cfg(feature = "enable-sia")] pub mod siacoin; -#[cfg(feature = "enable-sia")] -use siacoin::SiaCoin; +use siacoin::{SiaCoin, SiaCoinActivationRequest, SiaFeeDetails, SiaTransaction, SiaTransactionTypes}; pub mod utxo; use utxo::bch::{bch_coin_with_policy, BchActivationRequest, BchCoin}; @@ -614,6 +612,7 @@ pub enum TransactionEnum { CosmosTransaction(CosmosTransaction), #[cfg(not(target_arch = "wasm32"))] LightningPayment(LightningPayment), + SiaTransaction(SiaTransaction), } ifrom!(TransactionEnum, UtxoTx); @@ -621,6 +620,7 @@ ifrom!(TransactionEnum, SignedEthTx); ifrom!(TransactionEnum, ZTransaction); #[cfg(not(target_arch = "wasm32"))] ifrom!(TransactionEnum, LightningPayment); +ifrom!(TransactionEnum, SiaTransaction); impl TransactionEnum { #[cfg(not(target_arch = "wasm32"))] @@ -645,6 +645,7 @@ impl Deref for TransactionEnum { TransactionEnum::CosmosTransaction(ref t) => t, #[cfg(not(target_arch = "wasm32"))] TransactionEnum::LightningPayment(ref p) => p, + TransactionEnum::SiaTransaction(ref t) => t, } } } @@ -671,6 +672,18 @@ pub enum TransactionErr { InternalError(String), } +impl From for TransactionErr { + fn from(e: String) -> Self { + TransactionErr::Plain(e) + } +} + +impl From<&str> for TransactionErr { + fn from(e: &str) -> Self { + TransactionErr::Plain(e.to_string()) + } +} + impl TransactionErr { /// Returns transaction if the error includes it. #[inline] @@ -1025,8 +1038,11 @@ pub struct CheckIfMyPaymentSentArgs<'a> { #[derive(Clone, Debug)] pub struct ValidateFeeArgs<'a> { pub fee_tx: &'a TransactionEnum, + /// Public key of the expected sender pub expected_sender: &'a [u8], pub dex_fee: &'a DexFee, + /// the minimum block number the fee transaction can be included in the blockchain + /// this can be the current height if the transaction is in mempool or the confirmed block height pub min_block_number: u64, pub uuid: &'a [u8], } @@ -1306,6 +1322,9 @@ pub trait SwapOps { } } +// FIXME Alright - implement defaults for all methods or remove trait bound from MmCoin +// This is only relevant to UTXO and ETH protocols and should not be forced to implement it otherwise +// I am told unimplemented!() is safe here, but it's safer to return errors #[async_trait] pub trait WatcherOps { fn send_maker_payment_spend_preimage(&self, _input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { @@ -2208,7 +2227,12 @@ pub trait MarketCoinOps { fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send>; /// Signs raw utxo transaction in hexadecimal format as input and returns signed transaction in hexadecimal format - async fn sign_raw_tx(&self, args: &SignRawTransactionRequest) -> RawTransactionResult; + /// This method is only used by the sign_raw_transaction RPC method. Optional to implement. + async fn sign_raw_tx(&self, _args: &SignRawTransactionRequest) -> RawTransactionResult { + MmError::err(RawTransactionError::NotImplemented { + coin: self.ticker().to_string(), + }) + } fn wait_for_confirmations(&self, input: ConfirmPaymentInput) -> Box + Send>; @@ -2453,6 +2477,7 @@ pub enum TxFeeDetails { Qrc20(Qrc20FeeDetails), Slp(SlpFeeDetails), Tendermint(TendermintFeeDetails), + Sia(SiaFeeDetails), Solana(SolanaFeeDetails), } @@ -2470,6 +2495,7 @@ impl<'de> Deserialize<'de> for TxFeeDetails { Qrc20(Qrc20FeeDetails), Slp(SlpFeeDetails), Tendermint(TendermintFeeDetails), + Sia(SiaFeeDetails), Solana(SolanaFeeDetails), } @@ -2479,6 +2505,7 @@ impl<'de> Deserialize<'de> for TxFeeDetails { TxFeeDetailsUnTagged::Qrc20(f) => Ok(TxFeeDetails::Qrc20(f)), TxFeeDetailsUnTagged::Slp(f) => Ok(TxFeeDetails::Slp(f)), TxFeeDetailsUnTagged::Tendermint(f) => Ok(TxFeeDetails::Tendermint(f)), + TxFeeDetailsUnTagged::Sia(f) => Ok(TxFeeDetails::Sia(f)), TxFeeDetailsUnTagged::Solana(f) => Ok(TxFeeDetails::Solana(f)), } } @@ -2502,6 +2529,12 @@ impl From for TxFeeDetails { } } +impl From for TxFeeDetails { + fn from(sia_details: SiaFeeDetails) -> Self { + TxFeeDetails::Sia(sia_details) + } +} + impl From for TxFeeDetails { fn from(tendermint_details: TendermintFeeDetails) -> Self { TxFeeDetails::Tendermint(tendermint_details) @@ -2536,6 +2569,9 @@ pub enum TransactionType { TendermintIBCTransfer { token_id: Option, }, + SiaV1Transaction, + SiaV2Transaction, + SiaMinerPayout, } /// Transaction details @@ -2578,9 +2614,10 @@ pub struct TransactionDetails { #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(untagged)] +#[allow(clippy::large_enum_variant)] pub enum TransactionData { Signed { - /// Raw bytes of signed transaction, this should be sent as is to `send_raw_transaction_bytes` RPC to broadcast the transaction + /// Raw bytes of signed transaction, this should be sent as is to `send_raw_transaction` RPC to broadcast the transaction tx_hex: BytesJson, /// Transaction hash in hexadecimal format tx_hash: String, @@ -2588,6 +2625,14 @@ pub enum TransactionData { /// This can contain entirely different data depending on the platform. /// TODO: Perhaps using generics would be more suitable here? Unsigned(Json), + // Todo: After implementing tx hash in sia-rust we can use Signed variant for sia as well but make tx_hex: BytesJson and enum or add another variant for sia/json + Sia { + /// SIA transactions are broadcasted in JSON format. + /// This is provided in case someone wants to broadcast the transaction JSON through other means than `send_raw_transaction`. + tx_json: SiaTransactionTypes, + /// Transaction hash in hexadecimal format + tx_hash: String, + }, } impl TransactionData { @@ -2603,6 +2648,7 @@ impl TransactionData { match self { TransactionData::Signed { tx_hex, .. } => Some(tx_hex), TransactionData::Unsigned(_) => None, + TransactionData::Sia { .. } => None, } } @@ -2610,6 +2656,7 @@ impl TransactionData { match self { TransactionData::Signed { tx_hash, .. } => Some(tx_hash), TransactionData::Unsigned(_) => None, + TransactionData::Sia { tx_hash, .. } => Some(tx_hash), } } } @@ -3599,7 +3646,9 @@ pub trait MmCoin: SwapOps + WatcherOps + MarketCoinOps + Send + Sync + 'static { // state serialization, to get full rewind and debugging information about the coins participating in a SWAP operation. // status/availability check: https://github.com/artemii235/SuperNET/issues/156#issuecomment-446501816 - fn is_asset_chain(&self) -> bool; + fn is_asset_chain(&self) -> bool { + false + } /// The coin can be initialized, but it cannot participate in the swaps. fn wallet_only(&self, ctx: &MmArc) -> bool { @@ -3620,16 +3669,26 @@ pub trait MmCoin: SwapOps + WatcherOps + MarketCoinOps + Send + Sync + 'static { fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut; + // TODO Alright: should be separated into a "OptionalDispatcherOps" trait. + // This trait can handle all methods that are only used by dispatcher methods. + // only used by "get_raw_transaction" dispatcher method. fn get_raw_transaction(&self, req: RawTransactionRequest) -> RawTransactionFut<'_>; + // TODO Alright: this method is only applicable to Watcher logic and could be moved to WatcherOps fn get_tx_hex_by_hash(&self, tx_hash: Vec) -> RawTransactionFut<'_>; /// Maximum number of digits after decimal point used to denominate integer coin units (satoshis, wei, etc.) fn decimals(&self) -> u8; /// Convert input address to the specified address format. + // TODO Alright: should be separated into a "OptionalDispatcherOps" trait. + // This trait can handle all methods that are only used by dispatcher methods. fn convert_to_address(&self, from: &str, to_address_format: Json) -> Result; + // TODO Alright: could be separated into a "OptionalDispatcherOps" trait. + // only used by "verify_message" and "validate_address" dispatcher methods. + // Consider using traits to track which methods are neccesary for which UIs + // eg, "KomodoWalletOps" for the Komodo wallet, "ReactWalletOps" for the react wallet, etc. fn validate_address(&self, address: &str) -> ValidateAddressResult; /// Loop collecting coin transaction history and saving it to local DB @@ -3822,109 +3881,107 @@ where #[derive(Clone)] #[allow(clippy::large_enum_variant)] pub enum MmCoinEnum { - UtxoCoin(UtxoStandardCoin), - QtumCoin(QtumCoin), - Qrc20Coin(Qrc20Coin), - EthCoin(EthCoin), - ZCoin(ZCoin), - Bch(BchCoin), - SlpToken(SlpToken), - Tendermint(TendermintCoin), - TendermintToken(TendermintToken), + UtxoCoinVariant(UtxoStandardCoin), + QtumCoinVariant(QtumCoin), + Qrc20CoinVariant(Qrc20Coin), + EthCoinVariant(EthCoin), + ZCoinVariant(ZCoin), + BchVariant(BchCoin), + SlpTokenVariant(SlpToken), + TendermintVariant(TendermintCoin), + TendermintTokenVariant(TendermintToken), #[cfg(not(target_arch = "wasm32"))] - LightningCoin(LightningCoin), - #[cfg(feature = "enable-sia")] - SiaCoin(SiaCoin), - Solana(solana::SolanaCoin), - SolanaToken(solana::SolanaToken), + LightningCoinVariant(LightningCoin), + SiaCoinVariant(SiaCoin), + SolanaCoinVariant(solana::SolanaCoin), + SolanaTokenVariant(solana::SolanaToken), #[cfg(any(test, feature = "for-tests"))] - Test(TestCoin), + TestVariant(TestCoin), } impl From for MmCoinEnum { fn from(c: UtxoStandardCoin) -> MmCoinEnum { - MmCoinEnum::UtxoCoin(c) + MmCoinEnum::UtxoCoinVariant(c) } } impl From for MmCoinEnum { fn from(c: EthCoin) -> MmCoinEnum { - MmCoinEnum::EthCoin(c) + MmCoinEnum::EthCoinVariant(c) } } #[cfg(any(test, feature = "for-tests"))] impl From for MmCoinEnum { fn from(c: TestCoin) -> MmCoinEnum { - MmCoinEnum::Test(c) + MmCoinEnum::TestVariant(c) } } impl From for MmCoinEnum { fn from(coin: QtumCoin) -> Self { - MmCoinEnum::QtumCoin(coin) + MmCoinEnum::QtumCoinVariant(coin) } } impl From for MmCoinEnum { fn from(c: Qrc20Coin) -> MmCoinEnum { - MmCoinEnum::Qrc20Coin(c) + MmCoinEnum::Qrc20CoinVariant(c) } } impl From for MmCoinEnum { fn from(c: BchCoin) -> MmCoinEnum { - MmCoinEnum::Bch(c) + MmCoinEnum::BchVariant(c) } } impl From for MmCoinEnum { fn from(c: SlpToken) -> MmCoinEnum { - MmCoinEnum::SlpToken(c) + MmCoinEnum::SlpTokenVariant(c) } } impl From for MmCoinEnum { fn from(c: TendermintCoin) -> Self { - MmCoinEnum::Tendermint(c) + MmCoinEnum::TendermintVariant(c) } } impl From for MmCoinEnum { fn from(c: TendermintToken) -> Self { - MmCoinEnum::TendermintToken(c) + MmCoinEnum::TendermintTokenVariant(c) } } #[cfg(not(target_arch = "wasm32"))] impl From for MmCoinEnum { fn from(c: LightningCoin) -> MmCoinEnum { - MmCoinEnum::LightningCoin(c) + MmCoinEnum::LightningCoinVariant(c) } } -impl From for MmCoinEnum { - fn from(c: solana::SolanaCoin) -> MmCoinEnum { - MmCoinEnum::Solana(c) +impl From for MmCoinEnum { + fn from(c: ZCoin) -> MmCoinEnum { + MmCoinEnum::ZCoinVariant(c) } } -impl From for MmCoinEnum { - fn from(c: solana::SolanaToken) -> MmCoinEnum { - MmCoinEnum::SolanaToken(c) +impl From for MmCoinEnum { + fn from(c: SiaCoin) -> MmCoinEnum { + MmCoinEnum::SiaCoinVariant(c) } } -impl From for MmCoinEnum { - fn from(c: ZCoin) -> MmCoinEnum { - MmCoinEnum::ZCoin(c) +impl From for MmCoinEnum { + fn from(c: solana::SolanaCoin) -> MmCoinEnum { + MmCoinEnum::SolanaCoinVariant(c) } } -#[cfg(feature = "enable-sia")] -impl From for MmCoinEnum { - fn from(c: SiaCoin) -> MmCoinEnum { - MmCoinEnum::SiaCoin(c) +impl From for MmCoinEnum { + fn from(c: solana::SolanaToken) -> MmCoinEnum { + MmCoinEnum::SolanaTokenVariant(c) } } @@ -3933,23 +3990,22 @@ impl Deref for MmCoinEnum { type Target = dyn MmCoin; fn deref(&self) -> &dyn MmCoin { match self { - MmCoinEnum::UtxoCoin(ref c) => c, - MmCoinEnum::QtumCoin(ref c) => c, - MmCoinEnum::Qrc20Coin(ref c) => c, - MmCoinEnum::EthCoin(ref c) => c, - MmCoinEnum::Bch(ref c) => c, - MmCoinEnum::SlpToken(ref c) => c, - MmCoinEnum::Tendermint(ref c) => c, - MmCoinEnum::TendermintToken(ref c) => c, + MmCoinEnum::UtxoCoinVariant(ref c) => c, + MmCoinEnum::QtumCoinVariant(ref c) => c, + MmCoinEnum::Qrc20CoinVariant(ref c) => c, + MmCoinEnum::EthCoinVariant(ref c) => c, + MmCoinEnum::BchVariant(ref c) => c, + MmCoinEnum::SlpTokenVariant(ref c) => c, + MmCoinEnum::TendermintVariant(ref c) => c, + MmCoinEnum::TendermintTokenVariant(ref c) => c, #[cfg(not(target_arch = "wasm32"))] - MmCoinEnum::LightningCoin(ref c) => c, - MmCoinEnum::ZCoin(ref c) => c, - #[cfg(feature = "enable-sia")] - MmCoinEnum::SiaCoin(ref c) => c, - MmCoinEnum::Solana(ref c) => c, - MmCoinEnum::SolanaToken(ref c) => c, + MmCoinEnum::LightningCoinVariant(ref c) => c, + MmCoinEnum::ZCoinVariant(ref c) => c, + MmCoinEnum::SiaCoinVariant(ref c) => c, + MmCoinEnum::SolanaCoinVariant(ref c) => c, + MmCoinEnum::SolanaTokenVariant(ref c) => c, #[cfg(any(test, feature = "for-tests"))] - MmCoinEnum::Test(ref c) => c, + MmCoinEnum::TestVariant(ref c) => c, } } } @@ -3957,19 +4013,19 @@ impl Deref for MmCoinEnum { impl MmCoinEnum { pub fn is_utxo_in_native_mode(&self) -> bool { match self { - MmCoinEnum::UtxoCoin(ref c) => c.as_ref().rpc_client.is_native(), - MmCoinEnum::QtumCoin(ref c) => c.as_ref().rpc_client.is_native(), - MmCoinEnum::Qrc20Coin(ref c) => c.as_ref().rpc_client.is_native(), - MmCoinEnum::Bch(ref c) => c.as_ref().rpc_client.is_native(), - MmCoinEnum::SlpToken(ref c) => c.as_ref().rpc_client.is_native(), + MmCoinEnum::UtxoCoinVariant(ref c) => c.as_ref().rpc_client.is_native(), + MmCoinEnum::QtumCoinVariant(ref c) => c.as_ref().rpc_client.is_native(), + MmCoinEnum::Qrc20CoinVariant(ref c) => c.as_ref().rpc_client.is_native(), + MmCoinEnum::BchVariant(ref c) => c.as_ref().rpc_client.is_native(), + MmCoinEnum::SlpTokenVariant(ref c) => c.as_ref().rpc_client.is_native(), #[cfg(not(target_arch = "wasm32"))] - MmCoinEnum::ZCoin(ref c) => c.as_ref().rpc_client.is_native(), + MmCoinEnum::ZCoinVariant(ref c) => c.as_ref().rpc_client.is_native(), _ => false, } } pub fn is_eth(&self) -> bool { - matches!(self, MmCoinEnum::EthCoin(_)) + matches!(self, MmCoinEnum::EthCoinVariant(_)) } fn is_platform_coin(&self) -> bool { @@ -3982,11 +4038,11 @@ impl MmCoinEnum { /// Otherwise, the function will default to `SecretHashAlgo::DHASH160`, which may not be correct for the new coin. pub fn secret_hash_algo_v2(&self) -> SecretHashAlgo { match self { - MmCoinEnum::Tendermint(_) | MmCoinEnum::TendermintToken(_) | MmCoinEnum::EthCoin(_) => { - SecretHashAlgo::SHA256 - }, + MmCoinEnum::TendermintVariant(_) + | MmCoinEnum::TendermintTokenVariant(_) + | MmCoinEnum::EthCoinVariant(_) => SecretHashAlgo::SHA256, #[cfg(not(target_arch = "wasm32"))] - MmCoinEnum::LightningCoin(_) => SecretHashAlgo::SHA256, + MmCoinEnum::LightningCoinVariant(_) => SecretHashAlgo::SHA256, _ => SecretHashAlgo::DHASH160, } } @@ -4048,6 +4104,7 @@ pub enum DexFeeBurnDestination { } /// Represents the different types of DEX fees. +/// WithBurn is a special case for KMD see: dex_fee_amount function #[derive(Clone, Debug, PartialEq)] pub enum DexFee { /// No dex fee is taken (if taker is dex pubkey) @@ -4874,7 +4931,6 @@ pub enum CoinProtocol { confirmation_targets: PlatformCoinConfirmationTargets, }, ZHTLC(ZcoinProtocolInfo), - #[cfg(feature = "enable-sia")] SIA, NFT { platform: String, @@ -4923,7 +4979,6 @@ impl CoinProtocol { | CoinProtocol::BCH { .. } | CoinProtocol::TENDERMINT(_) | CoinProtocol::ZHTLC(_) => None, - #[cfg(feature = "enable-sia")] CoinProtocol::SIA => None, CoinProtocol::SOLANA(_) => None, CoinProtocol::SOLANATOKEN(info) => Some(&info.platform), @@ -4948,7 +5003,6 @@ impl CoinProtocol { | CoinProtocol::NFT { .. } => None, #[cfg(not(target_arch = "wasm32"))] CoinProtocol::LIGHTNING { .. } => None, - #[cfg(feature = "enable-sia")] CoinProtocol::SIA => None, CoinProtocol::SOLANA(_) => None, CoinProtocol::SOLANATOKEN(info) => Some(info.mint_address.to_string()), @@ -5278,7 +5332,7 @@ pub async fn lp_coininit(ctx: &MmArc, ticker: &str, req: &Json) -> Result { let platform_coin = try_s!(lp_coinfind(ctx, platform).await); let platform_coin = match platform_coin { - Some(MmCoinEnum::Bch(coin)) => coin, + Some(MmCoinEnum::BchVariant(coin)) => coin, Some(_) => return ERR!("Platform coin {} is not BCH", platform), None => return ERR!("Platform coin {} is not activated", platform), }; @@ -5300,9 +5354,9 @@ pub async fn lp_coininit(ctx: &MmArc, ticker: &str, req: &Json) -> Result return ERR!("TRX protocol is not supported by lp_coininit"), #[cfg(not(target_arch = "wasm32"))] CoinProtocol::LIGHTNING { .. } => return ERR!("Lightning protocol is not supported by lp_coininit"), - #[cfg(feature = "enable-sia")] CoinProtocol::SIA => { - return ERR!("SIA protocol is not supported by lp_coininit. Use task::enable_sia::init"); + let params = try_s!(SiaCoinActivationRequest::from_legacy_req(req)); + try_s!(SiaCoin::new(ctx, coins_en, ¶ms, priv_key_policy).await).into() }, CoinProtocol::SOLANA(_) => return ERR!("SOLANA is not supported by lp_coininit"), CoinProtocol::SOLANATOKEN(_) => return ERR!("SOLANATOKEN is not supported by lp_coininit"), @@ -5363,7 +5417,8 @@ pub async fn lp_register_coin( Ok(()) } -fn lp_spawn_tx_history(ctx: MmArc, coin: MmCoinEnum) -> Result<(), String> { +/// Initiates the transaction history synchronization loop for fetching and processing transactions. +pub fn lp_spawn_tx_history(ctx: MmArc, coin: MmCoinEnum) -> Result<(), String> { let spawner = coin.spawner(); let fut = async move { let _res = coin.process_history_loop(ctx).compat().await; @@ -5446,7 +5501,7 @@ pub async fn convert_address(ctx: MmArc, req: Json) -> Result>, pub async fn kmd_rewards_info(ctx: MmArc) -> Result>, String> { let coin = match lp_coinfind(&ctx, "KMD").await { - Ok(Some(MmCoinEnum::UtxoCoin(t))) => t, + Ok(Some(MmCoinEnum::UtxoCoinVariant(t))) => t, Ok(Some(_)) => return ERR!("KMD was expected to be UTXO"), Ok(None) => return ERR!("KMD is not activated"), Err(err) => return ERR!("!lp_coinfind({}): KMD", err), @@ -5538,7 +5593,7 @@ pub async fn remove_delegation(ctx: MmArc, req: RemoveDelegateRequest) -> Delega }); } - let MmCoinEnum::Tendermint(tendermint) = coin else { + let MmCoinEnum::TendermintVariant(tendermint) = coin else { return MmError::err(DelegationError::CoinDoesntSupportDelegation { coin: coin.ticker().to_string(), }); @@ -5552,7 +5607,7 @@ pub async fn remove_delegation(ctx: MmArc, req: RemoveDelegateRequest) -> Delega }), None => match coin { - MmCoinEnum::QtumCoin(qtum) => qtum.remove_delegation().compat().await, + MmCoinEnum::QtumCoinVariant(qtum) => qtum.remove_delegation().compat().await, _ => MmError::err(DelegationError::CoinDoesntSupportDelegation { coin: coin.ticker().to_string(), }), @@ -5565,7 +5620,7 @@ pub async fn delegations_info(ctx: MmArc, req: DelegationsInfo) -> Result { - let MmCoinEnum::QtumCoin(qtum) = coin else { + let MmCoinEnum::QtumCoinVariant(qtum) = coin else { return MmError::err(StakingInfoError::InvalidPayload { reason: format!("{} is not a Qtum coin", req.coin), }); @@ -5575,8 +5630,10 @@ pub async fn delegations_info(ctx: MmArc, req: DelegationsInfo) -> Result match coin { - MmCoinEnum::Tendermint(t) => Ok(t.delegations_list(r.paging).await.map(|v| json!(v)).map_mm_err()?), - MmCoinEnum::TendermintToken(_) => MmError::err(StakingInfoError::InvalidPayload { + MmCoinEnum::TendermintVariant(t) => { + Ok(t.delegations_list(r.paging).await.map(|v| json!(v)).map_mm_err()?) + }, + MmCoinEnum::TendermintTokenVariant(_) => MmError::err(StakingInfoError::InvalidPayload { reason: "Tokens are not supported for delegation".into(), }), _ => MmError::err(StakingInfoError::InvalidPayload { @@ -5591,12 +5648,12 @@ pub async fn ongoing_undelegations_info(ctx: MmArc, req: UndelegationsInfo) -> R match req.info_details { UndelegationsInfoDetails::Cosmos(r) => match coin { - MmCoinEnum::Tendermint(t) => Ok(t + MmCoinEnum::TendermintVariant(t) => Ok(t .ongoing_undelegations_list(r.paging) .await .map(|v| json!(v)) .map_mm_err()?), - MmCoinEnum::TendermintToken(_) => MmError::err(StakingInfoError::InvalidPayload { + MmCoinEnum::TendermintTokenVariant(_) => MmError::err(StakingInfoError::InvalidPayload { reason: "Tokens are not supported for delegation".into(), }), _ => MmError::err(StakingInfoError::InvalidPayload { @@ -5621,7 +5678,7 @@ pub async fn add_delegation(ctx: MmArc, req: AddDelegateRequest) -> DelegationRe match req.staking_details { StakingDetails::Qtum(req) => { - let MmCoinEnum::QtumCoin(qtum) = coin else { + let MmCoinEnum::QtumCoinVariant(qtum) = coin else { return MmError::err(DelegationError::CoinDoesntSupportDelegation { coin: coin.ticker().to_string(), }); @@ -5630,7 +5687,7 @@ pub async fn add_delegation(ctx: MmArc, req: AddDelegateRequest) -> DelegationRe qtum.add_delegation(req).compat().await }, StakingDetails::Cosmos(req) => { - let MmCoinEnum::Tendermint(tendermint) = coin else { + let MmCoinEnum::TendermintVariant(tendermint) = coin else { return MmError::err(DelegationError::CoinDoesntSupportDelegation { coin: coin.ticker().to_string(), }); @@ -5646,7 +5703,7 @@ pub async fn claim_staking_rewards(ctx: MmArc, req: ClaimStakingRewardsRequest) ClaimingDetails::Cosmos(r) => { let coin = lp_coinfind_or_err(&ctx, &req.coin).await.map_mm_err()?; - let MmCoinEnum::Tendermint(tendermint) = coin else { + let MmCoinEnum::TendermintVariant(tendermint) = coin else { return MmError::err(DelegationError::InvalidPayload { reason: format!("{} is not a Cosmos coin", req.coin), }); @@ -5664,8 +5721,16 @@ pub async fn send_raw_transaction(ctx: MmArc, req: Json) -> Result return ERR!("No such coin: {}", ticker), Err(err) => return ERR!("!lp_coinfind({}): {}", ticker, err), }; - let bytes_string = try_s!(req["tx_hex"].as_str().ok_or("No 'tx_hex' field")); - let res = try_s!(coin.send_raw_tx(bytes_string).compat().await); + // tx_json parsing is required for siacoin because txes are never encoded in hex + let tx_string = if let Some(tx_hex) = req["tx_hex"].as_str() { + tx_hex.to_owned() + } else if let Some(tx_json) = req["tx_json"].as_object() { + let json_string = try_s!(json::to_string(tx_json)); + json_string + } else { + return ERR!("No 'tx_hex' or 'tx_json' field"); + }; + let res = try_s!(coin.send_raw_tx(&tx_string).compat().await); let body = try_s!(json::to_vec(&json!({ "tx_hash": res }))); Ok(try_s!(Response::builder().body(body))) } @@ -5880,7 +5945,7 @@ pub async fn convert_utxo_address(ctx: MmArc, req: Json) -> Result return ERR!("Coin {} is not activated", req.to_coin), }; let coin = match coin { - MmCoinEnum::UtxoCoin(utxo) => utxo, + MmCoinEnum::UtxoCoinVariant(utxo) => utxo, _ => return ERR!("Coin {} is not utxo", req.to_coin), }; addr.prefix = coin.as_ref().conf.address_prefixes.p2pkh.clone(); @@ -5947,8 +6012,10 @@ pub fn address_by_coin_conf_and_pubkey_str( ERR!("address_by_coin_conf_and_pubkey_str is not implemented for lightning protocol yet!") }, CoinProtocol::ZHTLC { .. } => ERR!("address_by_coin_conf_and_pubkey_str is not supported for ZHTLC protocol!"), - #[cfg(feature = "enable-sia")] - CoinProtocol::SIA => ERR!("address_by_coin_conf_and_pubkey_str is not supported for SIA protocol!"), // TODO Alright + // TODO Alright - generating a Sia address in this case requires including the ed25519 pubkey in the OrderbookItem + // this will require significant changes and this function is only called from "legacy" dispatcher's `orderbook` rpc + // so it's not a priority right now + CoinProtocol::SIA => ERR!("address_by_coin_conf_and_pubkey_str is not supported for SIA protocol!"), CoinProtocol::SOLANA(_) => ERR!("address_by_coin_conf_and_pubkey_str is not implemented for SOLANA yet."), CoinProtocol::SOLANATOKEN(_) => { ERR!("address_by_coin_conf_and_pubkey_str is not implemented for SOLANATOKEN yet.") @@ -6247,7 +6314,7 @@ pub trait Eip1559Ops { pub async fn get_swap_gas_fee_policy(ctx: MmArc, req: GetSwapGasFeePolicyRequest) -> SwapGasFeePolicyResult { let coin = lp_coinfind_or_err(&ctx, &req.coin).await.map_mm_err()?; match coin { - MmCoinEnum::EthCoin(eth_coin) => Ok(eth_coin.get_swap_gas_fee_policy().await.map_mm_err()?), + MmCoinEnum::EthCoinVariant(eth_coin) => Ok(eth_coin.get_swap_gas_fee_policy().await.map_mm_err()?), _ => MmError::err(SwapGasFeePolicyError::NotSupported(req.coin)), } } @@ -6256,7 +6323,7 @@ pub async fn get_swap_gas_fee_policy(ctx: MmArc, req: GetSwapGasFeePolicyRequest pub async fn set_swap_gas_fee_policy(ctx: MmArc, req: SetSwapGasFeePolicyRequest) -> SwapGasFeePolicyResult { let coin = lp_coinfind_or_err(&ctx, &req.coin).await.map_mm_err()?; match coin { - MmCoinEnum::EthCoin(eth_coin) => { + MmCoinEnum::EthCoinVariant(eth_coin) => { eth_coin .set_swap_gas_fee_policy(req.swap_gas_fee_policy) .await @@ -6359,7 +6426,7 @@ mod tests { fn test_lp_coinfind() { let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); let coins_ctx = CoinsContext::from_ctx(&ctx).unwrap(); - let coin = MmCoinEnum::Test(TestCoin::new(RICK)); + let coin = MmCoinEnum::TestVariant(TestCoin::new(RICK)); // Add test coin to coins context common::block_on(coins_ctx.add_platform_with_tokens(coin.clone(), vec![], None)).unwrap(); @@ -6384,7 +6451,7 @@ mod tests { fn test_lp_coinfind_any() { let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); let coins_ctx = CoinsContext::from_ctx(&ctx).unwrap(); - let coin = MmCoinEnum::Test(TestCoin::new(RICK)); + let coin = MmCoinEnum::TestVariant(TestCoin::new(RICK)); // Add test coin to coins context common::block_on(coins_ctx.add_platform_with_tokens(coin.clone(), vec![], None)).unwrap(); diff --git a/mm2src/coins/my_tx_history_v2.rs b/mm2src/coins/my_tx_history_v2.rs index da957e2179..04aeafc635 100644 --- a/mm2src/coins/my_tx_history_v2.rs +++ b/mm2src/coins/my_tx_history_v2.rs @@ -232,6 +232,9 @@ impl<'a, Addr: Clone + DisplayAddress + Eq + std::hash::Hash, Tx: Transaction> T | TransactionType::FeeForTokenTx | TransactionType::StandardTransfer | TransactionType::NftTransfer => tx_hash.clone(), + TransactionType::SiaV1Transaction | TransactionType::SiaV2Transaction | TransactionType::SiaMinerPayout => { + tx_hash.clone() + }, }; TransactionDetails { @@ -376,12 +379,14 @@ pub async fn my_tx_history_v2_rpc( request: MyTxHistoryRequestV2, ) -> Result, MmError> { match lp_coinfind_or_err(&ctx, &request.coin).await.map_mm_err()? { - MmCoinEnum::Bch(bch) => my_tx_history_v2_impl(ctx, &bch, request).await, - MmCoinEnum::SlpToken(slp_token) => my_tx_history_v2_impl(ctx, &slp_token, request).await, - MmCoinEnum::UtxoCoin(utxo) => my_tx_history_v2_impl(ctx, &utxo, request).await, - MmCoinEnum::QtumCoin(qtum) => my_tx_history_v2_impl(ctx, &qtum, request).await, - MmCoinEnum::Tendermint(tendermint) => my_tx_history_v2_impl(ctx, &tendermint, request).await, - MmCoinEnum::TendermintToken(tendermint_token) => my_tx_history_v2_impl(ctx, &tendermint_token, request).await, + MmCoinEnum::BchVariant(bch) => my_tx_history_v2_impl(ctx, &bch, request).await, + MmCoinEnum::SlpTokenVariant(slp_token) => my_tx_history_v2_impl(ctx, &slp_token, request).await, + MmCoinEnum::UtxoCoinVariant(utxo) => my_tx_history_v2_impl(ctx, &utxo, request).await, + MmCoinEnum::QtumCoinVariant(qtum) => my_tx_history_v2_impl(ctx, &qtum, request).await, + MmCoinEnum::TendermintVariant(tendermint) => my_tx_history_v2_impl(ctx, &tendermint, request).await, + MmCoinEnum::TendermintTokenVariant(tendermint_token) => { + my_tx_history_v2_impl(ctx, &tendermint_token, request).await + }, other => MmError::err(MyTxHistoryErrorV2::NotSupportedFor(other.ticker().to_owned())), } } @@ -512,7 +517,7 @@ pub async fn z_coin_tx_history_rpc( request: MyTxHistoryRequestV2, ) -> Result, MmError> { match lp_coinfind_or_err(&ctx, &request.coin).await.map_mm_err()? { - MmCoinEnum::ZCoin(z_coin) => z_coin.tx_history(request).await, + MmCoinEnum::ZCoinVariant(z_coin) => z_coin.tx_history(request).await, other => MmError::err(MyTxHistoryErrorV2::NotSupportedFor(other.ticker().to_owned())), } } diff --git a/mm2src/coins/nft.rs b/mm2src/coins/nft.rs index 2ee597ef30..65529b9e21 100644 --- a/mm2src/coins/nft.rs +++ b/mm2src/coins/nft.rs @@ -210,7 +210,7 @@ async fn process_transfers_confirmations( let ticker = chain.to_ticker(); let coin_enum = lp_coinfind_or_err(ctx, ticker).await.map_mm_err()?; match coin_enum { - MmCoinEnum::EthCoin(eth_coin) => { + MmCoinEnum::EthCoinVariant(eth_coin) => { let current_block = current_block_impl(eth_coin).await?; Ok((ticker, current_block)) }, @@ -267,7 +267,7 @@ pub async fn update_nft(ctx: MmArc, req: UpdateNftReq) -> MmResult<(), UpdateNft }; let coin_enum = lp_coinfind_or_err(&ctx, chain.to_nft_ticker()).await.map_mm_err()?; let global_nft = match coin_enum { - MmCoinEnum::EthCoin(eth_coin) => eth_coin, + MmCoinEnum::EthCoinVariant(eth_coin) => eth_coin, _ => { return MmError::err(UpdateNftError::CoinDoesntSupportNft { coin: coin_enum.ticker().to_owned(), @@ -359,7 +359,7 @@ where let ticker = chain.to_nft_ticker(); if let Some(MmCoinStruct { - inner: MmCoinEnum::EthCoin(nft_global), + inner: MmCoinEnum::EthCoinVariant(nft_global), .. }) = coins.get_mut(ticker) { diff --git a/mm2src/coins/rpc_command/account_balance.rs b/mm2src/coins/rpc_command/account_balance.rs index f3f224ef73..eb860470cd 100644 --- a/mm2src/coins/rpc_command/account_balance.rs +++ b/mm2src/coins/rpc_command/account_balance.rs @@ -63,13 +63,13 @@ pub async fn account_balance( req: HDAccountBalanceRequest, ) -> MmResult { match lp_coinfind_or_err(&ctx, &req.coin).await.map_mm_err()? { - MmCoinEnum::UtxoCoin(utxo) => Ok(HDAccountBalanceResponseEnum::Map( + MmCoinEnum::UtxoCoinVariant(utxo) => Ok(HDAccountBalanceResponseEnum::Map( utxo.account_balance_rpc(req.params).await?, )), - MmCoinEnum::QtumCoin(qtum) => Ok(HDAccountBalanceResponseEnum::Map( + MmCoinEnum::QtumCoinVariant(qtum) => Ok(HDAccountBalanceResponseEnum::Map( qtum.account_balance_rpc(req.params).await?, )), - MmCoinEnum::EthCoin(eth) => Ok(HDAccountBalanceResponseEnum::Map( + MmCoinEnum::EthCoinVariant(eth) => Ok(HDAccountBalanceResponseEnum::Map( eth.account_balance_rpc(req.params).await?, )), _ => MmError::err(HDAccountBalanceRpcError::CoinIsActivatedNotWithHDWallet), diff --git a/mm2src/coins/rpc_command/consolidate_utxos.rs b/mm2src/coins/rpc_command/consolidate_utxos.rs index 8a55fe6328..24a2278c00 100644 --- a/mm2src/coins/rpc_command/consolidate_utxos.rs +++ b/mm2src/coins/rpc_command/consolidate_utxos.rs @@ -87,7 +87,7 @@ pub async fn consolidate_utxos_rpc( ) -> MmResult { let coin = lp_coinfind_or_err(&ctx, &request.coin).await.map_mm_err()?; match coin { - MmCoinEnum::UtxoCoin(coin) => { + MmCoinEnum::UtxoCoinVariant(coin) => { let from_address = match &coin.as_ref().derivation_method { DerivationMethod::SingleAddress(my_address) => my_address.clone(), DerivationMethod::HDWallet(wallet) => { diff --git a/mm2src/coins/rpc_command/fetch_utxos.rs b/mm2src/coins/rpc_command/fetch_utxos.rs index 19d0265277..e21c06934a 100644 --- a/mm2src/coins/rpc_command/fetch_utxos.rs +++ b/mm2src/coins/rpc_command/fetch_utxos.rs @@ -75,7 +75,7 @@ pub async fn fetch_utxos_rpc(ctx: MmArc, req: FetchUtxosRequest) -> MmResult match &coin.as_ref().derivation_method { + MmCoinEnum::UtxoCoinVariant(coin) => match &coin.as_ref().derivation_method { DerivationMethod::SingleAddress(my_address) => { let addresses_utxos = get_utxos(&coin, UtxosFrom::Single(my_address.clone())).await?; let total_count = addresses_utxos.iter().map(|addr| addr.count).sum(); diff --git a/mm2src/coins/rpc_command/get_current_mtp.rs b/mm2src/coins/rpc_command/get_current_mtp.rs index f95c06d203..75634453dc 100644 --- a/mm2src/coins/rpc_command/get_current_mtp.rs +++ b/mm2src/coins/rpc_command/get_current_mtp.rs @@ -57,20 +57,20 @@ pub async fn get_current_mtp_rpc( req: GetCurrentMtpRequest, ) -> GetCurrentMtpRpcResult { match lp_coinfind_or_err(&ctx, &req.coin).await.map_mm_err()? { - MmCoinEnum::UtxoCoin(utxo) => Ok(GetCurrentMtpResponse { + MmCoinEnum::UtxoCoinVariant(utxo) => Ok(GetCurrentMtpResponse { mtp: utxo.get_current_mtp().await.map_mm_err()?, }), - MmCoinEnum::QtumCoin(qtum) => Ok(GetCurrentMtpResponse { + MmCoinEnum::QtumCoinVariant(qtum) => Ok(GetCurrentMtpResponse { mtp: qtum.get_current_mtp().await.map_mm_err()?, }), - MmCoinEnum::Qrc20Coin(qrc) => Ok(GetCurrentMtpResponse { + MmCoinEnum::Qrc20CoinVariant(qrc) => Ok(GetCurrentMtpResponse { mtp: qrc.get_current_mtp().await.map_mm_err()?, }), #[cfg(not(target_arch = "wasm32"))] - MmCoinEnum::ZCoin(zcoin) => Ok(GetCurrentMtpResponse { + MmCoinEnum::ZCoinVariant(zcoin) => Ok(GetCurrentMtpResponse { mtp: zcoin.get_current_mtp().await.map_mm_err()?, }), - MmCoinEnum::Bch(bch) => Ok(GetCurrentMtpResponse { + MmCoinEnum::BchVariant(bch) => Ok(GetCurrentMtpResponse { mtp: bch.get_current_mtp().await.map_mm_err()?, }), _ => Err(MmError::new(GetCurrentMtpError::NotSupportedCoin(req.coin))), diff --git a/mm2src/coins/rpc_command/get_new_address.rs b/mm2src/coins/rpc_command/get_new_address.rs index 9236421228..cabbed3a43 100644 --- a/mm2src/coins/rpc_command/get_new_address.rs +++ b/mm2src/coins/rpc_command/get_new_address.rs @@ -342,7 +342,7 @@ impl RpcTask for InitGetNewAddressTask { } match self.coin { - MmCoinEnum::UtxoCoin(ref utxo) => { + MmCoinEnum::UtxoCoinVariant(ref utxo) => { // Set script type to enable Trezor to correctly validate the derivation path let trezor_script_type = match utxo.addr_format() { AddressFormat::Standard | AddressFormat::CashAddress { .. } => { @@ -362,7 +362,7 @@ impl RpcTask for InitGetNewAddressTask { .await?, )) }, - MmCoinEnum::QtumCoin(ref qtum) => { + MmCoinEnum::QtumCoinVariant(ref qtum) => { // Set script type to enable Trezor to correctly validate the derivation path let trezor_script_type = match qtum.addr_format() { AddressFormat::Standard | AddressFormat::CashAddress { .. } => { @@ -382,7 +382,7 @@ impl RpcTask for InitGetNewAddressTask { .await?, )) }, - MmCoinEnum::EthCoin(ref eth) => Ok(GetNewAddressResponseEnum::Map( + MmCoinEnum::EthCoinVariant(ref eth) => Ok(GetNewAddressResponseEnum::Map( get_new_address_helper( &self.ctx, eth, @@ -405,13 +405,13 @@ pub async fn get_new_address( ) -> MmResult { let coin = lp_coinfind_or_err(&ctx, &req.coin).await.map_mm_err()?; match coin { - MmCoinEnum::UtxoCoin(utxo) => Ok(GetNewAddressResponseEnum::Map( + MmCoinEnum::UtxoCoinVariant(utxo) => Ok(GetNewAddressResponseEnum::Map( utxo.get_new_address_rpc_without_conf(req.params).await?, )), - MmCoinEnum::QtumCoin(qtum) => Ok(GetNewAddressResponseEnum::Map( + MmCoinEnum::QtumCoinVariant(qtum) => Ok(GetNewAddressResponseEnum::Map( qtum.get_new_address_rpc_without_conf(req.params).await?, )), - MmCoinEnum::EthCoin(eth) => Ok(GetNewAddressResponseEnum::Map( + MmCoinEnum::EthCoinVariant(eth) => Ok(GetNewAddressResponseEnum::Map( eth.get_new_address_rpc_without_conf(req.params).await?, )), _ => MmError::err(GetNewAddressRpcError::CoinIsActivatedNotWithHDWallet), diff --git a/mm2src/coins/rpc_command/init_account_balance.rs b/mm2src/coins/rpc_command/init_account_balance.rs index 3f26ac74a8..c14dd8bd1e 100644 --- a/mm2src/coins/rpc_command/init_account_balance.rs +++ b/mm2src/coins/rpc_command/init_account_balance.rs @@ -78,13 +78,13 @@ impl RpcTask for InitAccountBalanceTask { _task_handle: InitAccountBalanceTaskHandleShared, ) -> Result> { match self.coin { - MmCoinEnum::UtxoCoin(ref utxo) => Ok(HDAccountBalanceEnum::Map( + MmCoinEnum::UtxoCoinVariant(ref utxo) => Ok(HDAccountBalanceEnum::Map( utxo.init_account_balance_rpc(self.req.params.clone()).await?, )), - MmCoinEnum::QtumCoin(ref qtum) => Ok(HDAccountBalanceEnum::Map( + MmCoinEnum::QtumCoinVariant(ref qtum) => Ok(HDAccountBalanceEnum::Map( qtum.init_account_balance_rpc(self.req.params.clone()).await?, )), - MmCoinEnum::EthCoin(ref eth) => Ok(HDAccountBalanceEnum::Map( + MmCoinEnum::EthCoinVariant(ref eth) => Ok(HDAccountBalanceEnum::Map( eth.init_account_balance_rpc(self.req.params.clone()).await?, )), _ => MmError::err(HDAccountBalanceRpcError::CoinIsActivatedNotWithHDWallet), diff --git a/mm2src/coins/rpc_command/init_create_account.rs b/mm2src/coins/rpc_command/init_create_account.rs index 69d6686ba8..fb96006d62 100644 --- a/mm2src/coins/rpc_command/init_create_account.rs +++ b/mm2src/coins/rpc_command/init_create_account.rs @@ -261,9 +261,9 @@ impl RpcTask for InitCreateAccountTask { if let Some(account_id) = self.task_state.create_account_id() { // We created the account already, so need to revert the changes. match self.coin { - MmCoinEnum::UtxoCoin(utxo) => utxo.revert_creating_account(account_id).await, - MmCoinEnum::QtumCoin(qtum) => qtum.revert_creating_account(account_id).await, - MmCoinEnum::EthCoin(eth) => eth.revert_creating_account(account_id).await, + MmCoinEnum::UtxoCoinVariant(utxo) => utxo.revert_creating_account(account_id).await, + MmCoinEnum::QtumCoinVariant(qtum) => qtum.revert_creating_account(account_id).await, + MmCoinEnum::EthCoinVariant(eth) => eth.revert_creating_account(account_id).await, _ => (), } }; @@ -303,7 +303,7 @@ impl RpcTask for InitCreateAccountTask { } match self.coin { - MmCoinEnum::UtxoCoin(ref utxo) => Ok(HDAccountBalanceEnum::Map( + MmCoinEnum::UtxoCoinVariant(ref utxo) => Ok(HDAccountBalanceEnum::Map( create_new_account_helper( &self.ctx, utxo, @@ -316,7 +316,7 @@ impl RpcTask for InitCreateAccountTask { ) .await?, )), - MmCoinEnum::QtumCoin(ref qtum) => Ok(HDAccountBalanceEnum::Map( + MmCoinEnum::QtumCoinVariant(ref qtum) => Ok(HDAccountBalanceEnum::Map( create_new_account_helper( &self.ctx, qtum, @@ -328,7 +328,7 @@ impl RpcTask for InitCreateAccountTask { ) .await?, )), - MmCoinEnum::EthCoin(ref eth) => Ok(HDAccountBalanceEnum::Map( + MmCoinEnum::EthCoinVariant(ref eth) => Ok(HDAccountBalanceEnum::Map( create_new_account_helper( &self.ctx, eth, diff --git a/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs b/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs index d3d8a3d06d..c67adfcf5a 100644 --- a/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs +++ b/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs @@ -97,13 +97,13 @@ impl RpcTask for InitScanAddressesTask { async fn run(&mut self, _task_handle: ScanAddressesTaskHandleShared) -> Result> { match self.coin { - MmCoinEnum::UtxoCoin(ref utxo) => Ok(ScanAddressesResponseEnum::Map( + MmCoinEnum::UtxoCoinVariant(ref utxo) => Ok(ScanAddressesResponseEnum::Map( utxo.init_scan_for_new_addresses_rpc(self.req.params.clone()).await?, )), - MmCoinEnum::QtumCoin(ref qtum) => Ok(ScanAddressesResponseEnum::Map( + MmCoinEnum::QtumCoinVariant(ref qtum) => Ok(ScanAddressesResponseEnum::Map( qtum.init_scan_for_new_addresses_rpc(self.req.params.clone()).await?, )), - MmCoinEnum::EthCoin(ref eth) => Ok(ScanAddressesResponseEnum::Map( + MmCoinEnum::EthCoinVariant(ref eth) => Ok(ScanAddressesResponseEnum::Map( eth.init_scan_for_new_addresses_rpc(self.req.params.clone()).await?, )), _ => MmError::err(HDAccountBalanceRpcError::CoinIsActivatedNotWithHDWallet), diff --git a/mm2src/coins/rpc_command/init_withdraw.rs b/mm2src/coins/rpc_command/init_withdraw.rs index 976c916ecf..7c68235e89 100644 --- a/mm2src/coins/rpc_command/init_withdraw.rs +++ b/mm2src/coins/rpc_command/init_withdraw.rs @@ -141,10 +141,12 @@ impl RpcTask for WithdrawTask { let ctx = self.ctx.clone(); let request = self.request.clone(); match self.coin { - MmCoinEnum::UtxoCoin(ref standard_utxo) => standard_utxo.init_withdraw(ctx, request, task_handle).await, - MmCoinEnum::QtumCoin(ref qtum) => qtum.init_withdraw(ctx, request, task_handle).await, - MmCoinEnum::ZCoin(ref z) => z.init_withdraw(ctx, request, task_handle).await, - MmCoinEnum::EthCoin(ref eth) => eth.init_withdraw(ctx, request, task_handle).await, + MmCoinEnum::UtxoCoinVariant(ref standard_utxo) => { + standard_utxo.init_withdraw(ctx, request, task_handle).await + }, + MmCoinEnum::QtumCoinVariant(ref qtum) => qtum.init_withdraw(ctx, request, task_handle).await, + MmCoinEnum::ZCoinVariant(ref z) => z.init_withdraw(ctx, request, task_handle).await, + MmCoinEnum::EthCoinVariant(ref eth) => eth.init_withdraw(ctx, request, task_handle).await, _ => MmError::err(WithdrawError::CoinDoesntSupportInitWithdraw { coin: self.coin.ticker().to_owned(), }), diff --git a/mm2src/coins/rpc_command/lightning/close_channel.rs b/mm2src/coins/rpc_command/lightning/close_channel.rs index 865c74399d..c0696c7e9c 100644 --- a/mm2src/coins/rpc_command/lightning/close_channel.rs +++ b/mm2src/coins/rpc_command/lightning/close_channel.rs @@ -49,7 +49,7 @@ pub struct CloseChannelReq { pub async fn close_channel(ctx: MmArc, req: CloseChannelReq) -> CloseChannelResult { let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await.map_mm_err()? { - MmCoinEnum::LightningCoin(c) => c, + MmCoinEnum::LightningCoinVariant(c) => c, e => return MmError::err(CloseChannelError::UnsupportedCoin(e.ticker().to_string())), }; diff --git a/mm2src/coins/rpc_command/lightning/connect_to_node.rs b/mm2src/coins/rpc_command/lightning/connect_to_node.rs index a64a4f32bf..18f988270d 100644 --- a/mm2src/coins/rpc_command/lightning/connect_to_node.rs +++ b/mm2src/coins/rpc_command/lightning/connect_to_node.rs @@ -74,7 +74,7 @@ pub struct ConnectToNodeRequest { /// Connect to a certain node on the lightning network. pub async fn connect_to_node(ctx: MmArc, req: ConnectToNodeRequest) -> ConnectToNodeResult { let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await.map_mm_err()? { - MmCoinEnum::LightningCoin(c) => c, + MmCoinEnum::LightningCoinVariant(c) => c, e => return MmError::err(ConnectToNodeError::UnsupportedCoin(e.ticker().to_string())), }; diff --git a/mm2src/coins/rpc_command/lightning/generate_invoice.rs b/mm2src/coins/rpc_command/lightning/generate_invoice.rs index fb2f62bc15..7d5ab110a4 100644 --- a/mm2src/coins/rpc_command/lightning/generate_invoice.rs +++ b/mm2src/coins/rpc_command/lightning/generate_invoice.rs @@ -81,7 +81,7 @@ pub async fn generate_invoice( req: GenerateInvoiceRequest, ) -> GenerateInvoiceResult { let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await.map_mm_err()? { - MmCoinEnum::LightningCoin(c) => c, + MmCoinEnum::LightningCoinVariant(c) => c, e => return MmError::err(GenerateInvoiceError::UnsupportedCoin(e.ticker().to_string())), }; let open_channels_nodes = ln_coin.open_channels_nodes.lock().clone(); diff --git a/mm2src/coins/rpc_command/lightning/get_channel_details.rs b/mm2src/coins/rpc_command/lightning/get_channel_details.rs index efa17b6a36..06a532fa27 100644 --- a/mm2src/coins/rpc_command/lightning/get_channel_details.rs +++ b/mm2src/coins/rpc_command/lightning/get_channel_details.rs @@ -66,7 +66,7 @@ pub async fn get_channel_details( req: GetChannelDetailsRequest, ) -> GetChannelDetailsResult { let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await.map_mm_err()? { - MmCoinEnum::LightningCoin(c) => c, + MmCoinEnum::LightningCoinVariant(c) => c, e => return MmError::err(GetChannelDetailsError::UnsupportedCoin(e.ticker().to_string())), }; diff --git a/mm2src/coins/rpc_command/lightning/get_claimable_balances.rs b/mm2src/coins/rpc_command/lightning/get_claimable_balances.rs index 9b53ccd450..039b293811 100644 --- a/mm2src/coins/rpc_command/lightning/get_claimable_balances.rs +++ b/mm2src/coins/rpc_command/lightning/get_claimable_balances.rs @@ -46,7 +46,7 @@ pub async fn get_claimable_balances( req: ClaimableBalancesReq, ) -> ClaimableBalancesResult> { let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await.map_mm_err()? { - MmCoinEnum::LightningCoin(c) => c, + MmCoinEnum::LightningCoinVariant(c) => c, e => return MmError::err(ClaimableBalancesError::UnsupportedCoin(e.ticker().to_string())), }; let ignored_channels = if req.include_open_channels_balances { diff --git a/mm2src/coins/rpc_command/lightning/get_payment_details.rs b/mm2src/coins/rpc_command/lightning/get_payment_details.rs index bf166ad5bb..cba5e26440 100644 --- a/mm2src/coins/rpc_command/lightning/get_payment_details.rs +++ b/mm2src/coins/rpc_command/lightning/get_payment_details.rs @@ -65,7 +65,7 @@ pub async fn get_payment_details( req: GetPaymentDetailsRequest, ) -> GetPaymentDetailsResult { let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await.map_mm_err()? { - MmCoinEnum::LightningCoin(c) => c, + MmCoinEnum::LightningCoinVariant(c) => c, e => return MmError::err(GetPaymentDetailsError::UnsupportedCoin(e.ticker().to_string())), }; diff --git a/mm2src/coins/rpc_command/lightning/list_channels.rs b/mm2src/coins/rpc_command/lightning/list_channels.rs index c03c375981..7c1edebaa9 100644 --- a/mm2src/coins/rpc_command/lightning/list_channels.rs +++ b/mm2src/coins/rpc_command/lightning/list_channels.rs @@ -72,7 +72,7 @@ pub async fn list_open_channels_by_filter( req: ListOpenChannelsRequest, ) -> ListChannelsResult { let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await.map_mm_err()? { - MmCoinEnum::LightningCoin(c) => c, + MmCoinEnum::LightningCoinVariant(c) => c, e => return MmError::err(ListChannelsError::UnsupportedCoin(e.ticker().to_string())), }; @@ -115,7 +115,7 @@ pub async fn list_closed_channels_by_filter( req: ListClosedChannelsRequest, ) -> ListChannelsResult { let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await.map_mm_err()? { - MmCoinEnum::LightningCoin(c) => c, + MmCoinEnum::LightningCoinVariant(c) => c, e => return MmError::err(ListChannelsError::UnsupportedCoin(e.ticker().to_string())), }; let closed_channels_res = ln_coin diff --git a/mm2src/coins/rpc_command/lightning/list_payments_by_filter.rs b/mm2src/coins/rpc_command/lightning/list_payments_by_filter.rs index 47d4e76139..ad24fb37af 100644 --- a/mm2src/coins/rpc_command/lightning/list_payments_by_filter.rs +++ b/mm2src/coins/rpc_command/lightning/list_payments_by_filter.rs @@ -68,7 +68,7 @@ pub struct ListPaymentsResponse { pub async fn list_payments_by_filter(ctx: MmArc, req: ListPaymentsReq) -> ListPaymentsResult { let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await.map_mm_err()? { - MmCoinEnum::LightningCoin(c) => c, + MmCoinEnum::LightningCoinVariant(c) => c, e => return MmError::err(ListPaymentsError::UnsupportedCoin(e.ticker().to_string())), }; let get_payments_res = ln_coin diff --git a/mm2src/coins/rpc_command/lightning/open_channel.rs b/mm2src/coins/rpc_command/lightning/open_channel.rs index 66a713c989..8f4d25f378 100644 --- a/mm2src/coins/rpc_command/lightning/open_channel.rs +++ b/mm2src/coins/rpc_command/lightning/open_channel.rs @@ -154,7 +154,7 @@ pub struct OpenChannelResponse { /// Opens a channel on the lightning network. pub async fn open_channel(ctx: MmArc, req: OpenChannelRequest) -> OpenChannelResult { let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await.map_mm_err()? { - MmCoinEnum::LightningCoin(c) => c, + MmCoinEnum::LightningCoinVariant(c) => c, e => return MmError::err(OpenChannelError::UnsupportedCoin(e.ticker().to_string())), }; diff --git a/mm2src/coins/rpc_command/lightning/send_payment.rs b/mm2src/coins/rpc_command/lightning/send_payment.rs index 8d99615fdd..95c775645b 100644 --- a/mm2src/coins/rpc_command/lightning/send_payment.rs +++ b/mm2src/coins/rpc_command/lightning/send_payment.rs @@ -91,7 +91,7 @@ pub struct SendPaymentResponse { pub async fn send_payment(ctx: MmArc, req: SendPaymentReq) -> SendPaymentResult { let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await.map_mm_err()? { - MmCoinEnum::LightningCoin(c) => c, + MmCoinEnum::LightningCoinVariant(c) => c, e => return MmError::err(SendPaymentError::UnsupportedCoin(e.ticker().to_string())), }; let open_channels_nodes = ln_coin.open_channels_nodes.lock().clone(); diff --git a/mm2src/coins/rpc_command/lightning/trusted_nodes.rs b/mm2src/coins/rpc_command/lightning/trusted_nodes.rs index 3e143fb291..f8f371d259 100644 --- a/mm2src/coins/rpc_command/lightning/trusted_nodes.rs +++ b/mm2src/coins/rpc_command/lightning/trusted_nodes.rs @@ -57,7 +57,7 @@ pub struct AddTrustedNodeResponse { pub async fn add_trusted_node(ctx: MmArc, req: AddTrustedNodeReq) -> TrustedNodeResult { let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await.map_mm_err()? { - MmCoinEnum::LightningCoin(c) => c, + MmCoinEnum::LightningCoinVariant(c) => c, e => return MmError::err(TrustedNodeError::UnsupportedCoin(e.ticker().to_string())), }; @@ -86,7 +86,7 @@ pub async fn remove_trusted_node( req: RemoveTrustedNodeReq, ) -> TrustedNodeResult { let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await.map_mm_err()? { - MmCoinEnum::LightningCoin(c) => c, + MmCoinEnum::LightningCoinVariant(c) => c, e => return MmError::err(TrustedNodeError::UnsupportedCoin(e.ticker().to_string())), }; @@ -111,7 +111,7 @@ pub struct ListTrustedNodesResponse { pub async fn list_trusted_nodes(ctx: MmArc, req: ListTrustedNodesReq) -> TrustedNodeResult { let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await.map_mm_err()? { - MmCoinEnum::LightningCoin(c) => c, + MmCoinEnum::LightningCoinVariant(c) => c, e => return MmError::err(TrustedNodeError::UnsupportedCoin(e.ticker().to_string())), }; diff --git a/mm2src/coins/rpc_command/lightning/update_channel.rs b/mm2src/coins/rpc_command/lightning/update_channel.rs index 10b0a401c5..b9a657cd02 100644 --- a/mm2src/coins/rpc_command/lightning/update_channel.rs +++ b/mm2src/coins/rpc_command/lightning/update_channel.rs @@ -55,7 +55,7 @@ pub struct UpdateChannelResponse { /// Updates configuration for an open channel. pub async fn update_channel(ctx: MmArc, req: UpdateChannelReq) -> UpdateChannelResult { let ln_coin = match lp_coinfind_or_err(&ctx, &req.coin).await.map_mm_err()? { - MmCoinEnum::LightningCoin(c) => c, + MmCoinEnum::LightningCoinVariant(c) => c, e => return MmError::err(UpdateChannelError::UnsupportedCoin(e.ticker().to_string())), }; diff --git a/mm2src/coins/rpc_command/tendermint/staking.rs b/mm2src/coins/rpc_command/tendermint/staking.rs index 02a005c4c0..0a052e74ec 100644 --- a/mm2src/coins/rpc_command/tendermint/staking.rs +++ b/mm2src/coins/rpc_command/tendermint/staking.rs @@ -110,11 +110,11 @@ pub async fn validators_rpc( } let validators = match coin { - MmCoinEnum::Tendermint(coin) => coin + MmCoinEnum::TendermintVariant(coin) => coin .validators_list(req.filter_by_status, req.paging) .await .map_mm_err()?, - MmCoinEnum::TendermintToken(token) => token + MmCoinEnum::TendermintTokenVariant(token) => token .platform_coin .validators_list(req.filter_by_status, req.paging) .await diff --git a/mm2src/coins/siacoin.rs b/mm2src/coins/siacoin.rs index e6d4e6b4e6..cce1f0e02b 100644 --- a/mm2src/coins/siacoin.rs +++ b/mm2src/coins/siacoin.rs @@ -1,331 +1,681 @@ use super::{ - BalanceError, CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, RawTransactionFut, RawTransactionRequest, - SwapOps, TradeFee, TransactionEnum, + BalanceError, CoinBalance, CoinsContext, HistorySyncState, MarketCoinOps, MmCoin, RawTransactionError, + RawTransactionFut, RawTransactionRequest, RawTransactionResult, SignRawTransactionRequest, SignatureError, SwapOps, + SwapTxTypeWithSecretHash, TradeFee, TransactionData, TransactionDetails, TransactionEnum, TransactionErr, + TransactionType, VerificationError, }; use crate::hd_wallet::HDAddressSelector; +use crate::siacoin::sia_withdraw::SiaWithdrawBuilder; use crate::{ - coin_errors::MyAddressError, AddressFromPubkeyError, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, - ConfirmPaymentInput, DexFee, FeeApproxStage, FoundSwapTxSpend, NegotiateSwapContractAddrErr, PrivKeyBuildPolicy, - PrivKeyPolicy, RawTransactionResult, RefundPaymentArgs, SearchForSwapTxSpendInput, SendPaymentArgs, - SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, TradePreimageFut, TradePreimageResult, - TradePreimageValue, TransactionResult, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, - ValidateFeeArgs, ValidateOtherPubKeyErr, ValidatePaymentInput, ValidatePaymentResult, VerificationResult, - WaitForHTLCTxSpendArgs, WatcherOps, WeakSpawner, WithdrawFut, WithdrawRequest, + coin_errors::{AddressFromPubkeyError, MyAddressError}, + now_sec, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, DexFee, FeeApproxStage, + FoundSwapTxSpend, NegotiateSwapContractAddrErr, PrivKeyBuildPolicy, PrivKeyPolicy, RawTransactionRes, + RefundPaymentArgs, SearchForSwapTxSpendInput, SendPaymentArgs, SignatureResult, SpendPaymentArgs, TradePreimageFut, + TradePreimageResult, TradePreimageValue, Transaction, TransactionResult, TxMarshalingErr, + UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateOtherPubKeyErr, ValidatePaymentError, + ValidatePaymentInput, ValidatePaymentResult, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WeakSpawner, + WithdrawFut, WithdrawRequest, }; -use crate::{SignatureError, VerificationError}; use async_trait::async_trait; -use common::executor::AbortedError; -use derive_more::Display; -pub use ed25519_dalek::{Keypair, PublicKey, SecretKey, Signature}; +use bitcrypto::sha256; +use common::executor::abortable_queue::AbortableQueue; +use common::executor::{AbortableSystem, AbortedError, Timer}; +use common::log::{debug, info}; +use common::DEX_FEE_PUBKEY_ED25519; +use derive_more::{Display, From, Into}; +use rpc::v1::types::H264 as H264Json; +/* +TODO Alright — this is now the third type in our codebase representing BIP32 derivation paths. + +We currently have: +- `ed25519_dalek_bip32::DerivationPath` +- `bip32::DerivationPath` +- Type aliases like `StandardHDPath`, `HDPathToCoin` and `HDPathToAccount` in `standard_hd_path.rs` +- `RpcDerivationPath` + + +This is named "DalekDerivationPath" to avoid confusion with bip32::DerivationPath, but they represent +the same thing conceptually. + */ +use ed25519_dalek_bip32::DerivationPath as DalekDerivationPath; +use futures::compat::Future01CompatExt; use futures::{FutureExt, TryFutureExt}; use futures01::Future; +use hex; use keys::KeyPair; use mm2_core::mm_ctx::MmArc; -use mm2_err_handle::prelude::*; +use mm2_number::num_bigint::ToBigInt; use mm2_number::{BigDecimal, BigInt, MmNumber}; -use rpc::v1::types::{Bytes as BytesJson, H264 as H264Json}; +use num_traits::ToPrimitive; +use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json}; use serde_json::Value as Json; -use std::ops::Deref; -use std::sync::Arc; +// expose all of sia-rust so mm2_main can use it via coins::siacoin::sia_rust +pub use sia_rust; +pub use sia_rust::transport::client::{ApiClient as SiaApiClient, ApiClientHelpers}; +pub use sia_rust::transport::endpoints::{ + AddressesEventsRequest, ConsensusTipRequest, GetAddressUtxosRequest, GetEventRequest, TxpoolBroadcastRequest, + TxpoolTransactionsRequest, TxpoolTransactionsResponse, +}; +pub use sia_rust::types::{ + Address, Currency, Event, EventDataWrapper, EventPayout, EventType, Hash256, Hash256Error, Keypair as SiaKeypair, + KeypairError, Preimage, PreimageError, PublicKey, PublicKeyError, SiacoinElement, SiacoinOutput, SiacoinOutputId, + SpendPolicy, TransactionId, V1Transaction, V2Transaction, +}; +pub use sia_rust::utils::{V2TransactionBuilder, V2TransactionBuilderError}; +use std::collections::hash_map::Entry; +use std::collections::{HashMap, HashSet}; +use std::convert::TryFrom; +use std::fmt; +use std::str::FromStr; +use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering}; +use std::sync::{Arc, Mutex}; +use uuid::Uuid; -use sia_rust::http_client::{SiaApiClient, SiaApiClientError, SiaHttpConf}; -use sia_rust::spend_policy::SpendPolicy; +use mm2_err_handle::prelude::*; + +pub mod error; +pub use error::SiaCoinNewError; +use error::*; pub mod sia_hd_wallet; +mod sia_withdraw; + +/* +The wasm and native modules should act identically *except* the ClientError associated type as it +wraps some transport specific error types. + +Avoid doing any conditional logic on any of the client_error types. +*/ +pub use sia_rust::transport::client::{error as client_error, Client as SiaClient}; + +pub type SiaCoin = SiaCoinGeneric; +pub type SiaClientConf = ::Conf; + +lazy_static! { + pub static ref FEE_PUBLIC_KEY_BYTES: Vec = + hex::decode(DEX_FEE_PUBKEY_ED25519).expect("DEX_FEE_PUBKEY_ED25510 is a valid hex string"); + pub static ref FEE_PUBLIC_KEY: PublicKey = + PublicKey::from_bytes(&FEE_PUBLIC_KEY_BYTES).expect("DEX_FEE_PUBKEY_ED25510 is a valid PublicKey"); + pub static ref FEE_ADDR: Address = Address::from_public_key(&FEE_PUBLIC_KEY); + pub static ref SINGLE_ADDRESS_MODE_PATH: DalekDerivationPath = + DalekDerivationPath::from_str("m/44'/1991'/0'/0'/0'").expect("Valid single address mode path"); +} -#[derive(Clone)] -pub struct SiaCoin(SiaArc); -#[derive(Clone)] -pub struct SiaArc(Arc); +/// The index of the HTLC output in the transaction that locks the funds +/// u32 is used to because this is generally used as an index of a Vec or slice +/// Setting usize would result in a u64->u32 cast in some cases, and we want to avoid that. +const HTLC_VOUT_INDEX: u32 = 0; -#[derive(Debug, Display)] -pub enum SiaConfError { - #[display(fmt = "'foo' field is not found in config")] - Foo, - Bar(String), +// TODO see https://github.com/KomodoPlatform/komodo-defi-framework/pull/2086#discussion_r1521668313 +// for additional fields needed +#[derive(Clone)] +pub struct SiaCoinGeneric { + /// SIA coin config + pub conf: SiaCoinConf, + pub priv_key_policy: Arc>, + /// Client used to interact with the blockchain, most likely an HTTP(s) client + pub client: Arc, + /// State of the transaction history loop (enabled, started, in progress, etc.) + pub history_sync_state: Arc>, + /// This abortable system is used to spawn coin's related futures that should be aborted on coin deactivation + /// and on [`MmArc::stop`]. + pub abortable_system: Arc, + required_confirmations: Arc, } -pub type SiaConfResult = Result>; +impl WatcherOps for SiaCoin {} -#[derive(Debug)] +/// The JSON configuration loaded from `coins` file +#[derive(Clone, Debug, Deserialize)] pub struct SiaCoinConf { - ticker: String, - pub foo: u32, + #[serde(rename = "coin")] + pub ticker: String, + pub required_confirmations: u64, } // TODO see https://github.com/KomodoPlatform/komodo-defi-framework/pull/2086#discussion_r1521660384 // for additional fields needed -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct SiaCoinActivationParams { +/// SiaCoinActivationRequest represents the deserialized JSON body from the `enable` RPC command +#[derive(Clone, Debug, Deserialize)] +pub struct SiaCoinActivationRequest { #[serde(default)] pub tx_history: bool, pub required_confirmations: Option, pub gap_limit: Option, - pub http_conf: SiaHttpConf, + pub client_conf: SiaClientConf, } -pub struct SiaConfBuilder<'a> { - #[allow(dead_code)] - conf: &'a Json, - ticker: &'a str, +#[derive(Debug, Display)] +pub enum SiaCoinFromLegacyReqErr { + InvalidRequiredConfs(serde_json::Error), + InvalidGapLimit(serde_json::Error), + InvalidClientConf(serde_json::Error), } -impl<'a> SiaConfBuilder<'a> { - pub fn new(conf: &'a Json, ticker: &'a str) -> Self { - SiaConfBuilder { conf, ticker } - } - - pub fn build(&self) -> SiaConfResult { - Ok(SiaCoinConf { - ticker: self.ticker.to_owned(), - foo: 0, +impl SiaCoinActivationRequest { + pub fn from_legacy_req(req: &Json) -> Result> { + let tx_history = req["tx_history"].as_bool().unwrap_or_default(); + let required_confirmations = serde_json::from_value(req["required_confirmations"].clone()) + .map_to_mm(SiaCoinFromLegacyReqErr::InvalidRequiredConfs)?; + let gap_limit = + serde_json::from_value(req["gap_limit"].clone()).map_to_mm(SiaCoinFromLegacyReqErr::InvalidGapLimit)?; + let client_conf = + serde_json::from_value(req["client_conf"].clone()).map_to_mm(SiaCoinFromLegacyReqErr::InvalidClientConf)?; + + Ok(SiaCoinActivationRequest { + tx_history, + required_confirmations, + gap_limit, + client_conf, }) } } -// TODO see https://github.com/KomodoPlatform/komodo-defi-framework/pull/2086#discussion_r1521668313 -// for additional fields needed -pub struct SiaCoinFields { - /// SIA coin config - pub conf: SiaCoinConf, - pub priv_key_policy: PrivKeyPolicy, - /// HTTP(s) client - pub http_client: SiaApiClient, -} - -pub async fn sia_coin_from_conf_and_params( - ctx: &MmArc, - ticker: &str, - conf: &Json, - params: &SiaCoinActivationParams, - priv_key_policy: PrivKeyBuildPolicy, -) -> Result> { - let priv_key = match priv_key_policy { - PrivKeyBuildPolicy::IguanaPrivKey(priv_key) => priv_key, - _ => return Err(SiaCoinBuildError::UnsupportedPrivKeyPolicy.into()), - }; - let key_pair = generate_keypair_from_slice(priv_key.as_slice())?; - let builder = SiaCoinBuilder::new(ctx, ticker, conf, key_pair, params); - builder.build().await +impl SiaCoin { + pub async fn new( + ctx: &MmArc, + json_conf: Json, + request: &SiaCoinActivationRequest, + priv_key_policy: PrivKeyBuildPolicy, + ) -> Result> { + let key_pair = match priv_key_policy { + PrivKeyBuildPolicy::IguanaPrivKey(priv_key) => SiaKeypair::from_private_bytes(priv_key.as_slice())?, + PrivKeyBuildPolicy::GlobalHDAccount(global_hd_account) => { + // generate the keypair from SINGLE_ADDRESS_MODE_PATH to be used for a "single address mode" for now + let extended_key = global_hd_account + .derive_ed25519_signing_key(&SINGLE_ADDRESS_MODE_PATH) + // TODO this map_err shouldn't be neccesary but From MmError for MmError + // impl is broken only for wasm targets, why? + .map_err(|e| e.into_inner())?; + SiaKeypair::from_private_bytes(extended_key.signing_key.as_bytes())? + }, + _ => return Err(SiaCoinNewError::UnsupportedPrivKeyPolicy.into()), + }; + + // parse the "coins" file JSON configuration + let conf: SiaCoinConf = serde_json::from_value(json_conf)?; + + Ok(SiaCoinBuilder::new(ctx, conf, key_pair, request).build().await?) + } } pub struct SiaCoinBuilder<'a> { ctx: &'a MmArc, - ticker: &'a str, - conf: &'a Json, - key_pair: Keypair, - params: &'a SiaCoinActivationParams, + conf: SiaCoinConf, + key_pair: SiaKeypair, + request: &'a SiaCoinActivationRequest, } impl<'a> SiaCoinBuilder<'a> { - pub fn new( - ctx: &'a MmArc, - ticker: &'a str, - conf: &'a Json, - key_pair: Keypair, - params: &'a SiaCoinActivationParams, - ) -> Self { + pub fn new(ctx: &'a MmArc, conf: SiaCoinConf, key_pair: SiaKeypair, request: &'a SiaCoinActivationRequest) -> Self { SiaCoinBuilder { ctx, - ticker, conf, key_pair, - params, + request, } } -} - -fn generate_keypair_from_slice(priv_key: &[u8]) -> Result { - let secret_key = SecretKey::from_bytes(priv_key).map_err(SiaCoinBuildError::EllipticCurveError)?; - let public_key = PublicKey::from(&secret_key); - Ok(Keypair { - secret: secret_key, - public: public_key, - }) -} - -/// Convert hastings amount to siacoin amount -fn siacoin_from_hastings(hastings: u128) -> BigDecimal { - let hastings = BigInt::from(hastings); - let decimals = BigInt::from(10u128.pow(24)); - BigDecimal::from(hastings) / BigDecimal::from(decimals) -} - -impl From for SiaCoinBuildError { - fn from(e: SiaConfError) -> Self { - SiaCoinBuildError::ConfError(e) - } -} - -#[derive(Debug, Display)] -pub enum SiaCoinBuildError { - ConfError(SiaConfError), - UnsupportedPrivKeyPolicy, - ClientError(SiaApiClientError), - EllipticCurveError(ed25519_dalek::ed25519::Error), -} - -impl SiaCoinBuilder<'_> { - #[allow(dead_code)] - fn ctx(&self) -> &MmArc { - self.ctx - } - - #[allow(dead_code)] - fn conf(&self) -> &Json { - self.conf - } - - fn ticker(&self) -> &str { - self.ticker - } - async fn build(self) -> MmResult { - let conf = SiaConfBuilder::new(self.conf, self.ticker()).build().map_mm_err()?; - let sia_fields = SiaCoinFields { - conf, - http_client: SiaApiClient::new(self.params.http_conf.clone()) - .map_err(SiaCoinBuildError::ClientError) - .await?, - priv_key_policy: PrivKeyPolicy::Iguana(self.key_pair), + // TODO Alright - update to follow the new error handling pattern + async fn build(self) -> Result { + let abortable_queue: AbortableQueue = self + .ctx + .abortable_system + .create_subsystem() + .map_err(SiaCoinBuilderError::AbortableSystem)?; + let abortable_system = Arc::new(abortable_queue); + let history_sync_state = if self.request.tx_history { + HistorySyncState::NotStarted + } else { + HistorySyncState::NotEnabled }; - let sia_arc = SiaArc::new(sia_fields); - - Ok(SiaCoin::from(sia_arc)) - } -} -impl Deref for SiaArc { - type Target = SiaCoinFields; - fn deref(&self) -> &SiaCoinFields { - &self.0 + // Use required_confirmations from activation request if it's set, otherwise use the value from coins conf + let required_confirmations: AtomicU64 = self + .request + .required_confirmations + .unwrap_or(self.conf.required_confirmations) + .into(); + + Ok(SiaCoin { + conf: self.conf, + client: Arc::new( + SiaClient::new(self.request.client_conf.clone()) + .await + .map_err(SiaCoinBuilderError::Client)?, + ), + priv_key_policy: PrivKeyPolicy::Iguana(self.key_pair).into(), + history_sync_state: Mutex::new(history_sync_state).into(), + abortable_system, + required_confirmations: required_confirmations.into(), + }) } } -impl From for SiaArc { - fn from(coin: SiaCoinFields) -> SiaArc { - SiaArc::new(coin) - } +/// Convert hastings representation to "coin" amount +/// BigDecimal(1) == 1 SC == 10^24 hastings +/// 1 H == 0.000000000000000000000001 SC +fn hastings_to_siacoin(hastings: Currency) -> BigDecimal { + let hastings: u128 = hastings.into(); + BigDecimal::new(BigInt::from(hastings), 24) } -impl From> for SiaArc { - fn from(arc: Arc) -> SiaArc { - SiaArc(arc) - } +/// Convert "coin" representation to hastings amount +/// BigDecimal(1) == 1 SC == 10^24 hastings +// TODO Alright it's not ideal that we require these standalone helpers, but a newtype of Currency is even messier +fn siacoin_to_hastings(siacoin: BigDecimal) -> Result { + // Shift the decimal place to the right by 24 places (10^24) + let decimals = BigInt::from(10u128.pow(24)); + let hastings = siacoin.clone() * BigDecimal::from(decimals); + hastings + .to_bigint() + .ok_or(SiacoinToHastingsError::BigDecimalToBigInt(siacoin.clone()))? + .to_u128() + .ok_or(SiacoinToHastingsError::BigIntToU128(siacoin)) + .map(Currency) } -impl From for SiaCoin { - fn from(coin: SiaArc) -> SiaCoin { - SiaCoin(coin) - } -} +// TODO Alright - refactor and move to siacoin::error +// #[derive(Debug, Error)] +// pub enum FrameworkErrorWga { +// #[error( +// "Sia select_outputs insufficent amount, available: {:?} required: {:?}", +// available, +// required +// )] +// SelectOutputsInsufficientAmount { available: Currency, required: Currency }, +// #[error("Sia TransactionErr {:?}", _0)] +// MmTransactionErr(TransactionErr), +// #[error("Sia MyAddressError: `{0}`")] +// MyAddressError(MyAddressError), +// } + +// impl From for FrameworkError { +// fn from(e: TransactionErr) -> Self { FrameworkError::MmTransactionErr(e) } +// } + +// impl From for FrameworkError { +// fn from(e: MyAddressError) -> Self { FrameworkError::MyAddressError(e) } +// } -impl SiaArc { - pub fn new(fields: SiaCoinFields) -> SiaArc { - SiaArc(Arc::new(fields)) - } +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SiaCoinProtocolInfo; - pub fn with_arc(inner: Arc) -> SiaArc { - SiaArc(inner) - } +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum SiaFeePolicy { + Fixed, + HastingsPerByte(Currency), + Unknown, } -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct SiaCoinProtocolInfo; +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct SiaFeeDetails { + pub coin: String, + pub policy: SiaFeePolicy, + pub total_amount: BigDecimal, +} #[async_trait] impl MmCoin for SiaCoin { - fn is_asset_chain(&self) -> bool { - false - } - fn spawner(&self) -> WeakSpawner { - unimplemented!() + self.abortable_system.weak_spawner() } - fn get_raw_transaction(&self, _req: RawTransactionRequest) -> RawTransactionFut<'_> { - unimplemented!() + /* + TODO: refactor MmCoin to remove or better generalize this method + No Sia software ever presents the user with a hex representation of a transaction. Transactions + are always presented or taken as user input as JSON. + Ideally, we would use an associated type within the response to allow returning + the transaction as a JSON. For now, we encode JSON to hex and return this hex string. + */ + fn get_raw_transaction(&self, req: RawTransactionRequest) -> RawTransactionFut<'_> { + let fut = async move { + let txid = match Hash256::from_str(&req.tx_hash).map_err(|e| { + RawTransactionError::InternalError(format!("SiaCoin::get_raw_transaction: failed to parse txid: {}", e)) + }) { + Ok(tx_hash) => tx_hash, + Err(e) => return Err(e.into()), + }; + let tx = match self.client.get_transaction(&txid).await.map_err(|e| { + RawTransactionError::InternalError(format!( + "SiaCoin::get_raw_transaction: failed to fetch txid:{} :{}", + txid, e + )) + }) { + Ok(tx) => tx, + Err(e) => return Err(e.into()), + }; + let tx_hex = SiaTransaction(tx).tx_hex(); + Ok(RawTransactionRes { tx_hex: tx_hex.into() }) + }; + Box::new(fut.boxed().compat()) } + // TODO Alright - this is only applicable to Watcher logic and will be removed from MmCoin trait fn get_tx_hex_by_hash(&self, _tx_hash: Vec) -> RawTransactionFut<'_> { - unimplemented!() + let fut = async move { + Err(RawTransactionError::InternalError( + "SiaCoin::get_tx_hex_by_hash: Unsupported".to_string(), + ))? + }; + Box::new(fut.boxed().compat()) } - fn withdraw(&self, _req: WithdrawRequest) -> WithdrawFut { - unimplemented!() + fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { + let coin = self.clone(); + let fut = async move { + let builder = SiaWithdrawBuilder::new(&coin, req)?; + builder.build().await + }; + Box::new(fut.boxed().compat()) } fn decimals(&self) -> u8 { - unimplemented!() + 24 } fn convert_to_address(&self, _from: &str, _to_address_format: Json) -> Result { - unimplemented!() + Err("SiaCoin::convert_to_address: Unsupported".to_string()) } - fn validate_address(&self, _address: &str) -> ValidateAddressResult { - unimplemented!() + fn validate_address(&self, address: &str) -> ValidateAddressResult { + match Address::from_str(address) { + Ok(_) => ValidateAddressResult { + is_valid: true, + reason: None, + }, + Err(e) => ValidateAddressResult { + is_valid: false, + reason: Some(e.to_string()), + }, + } } - fn process_history_loop(&self, _ctx: MmArc) -> Box + Send> { - unimplemented!() + // Todo: deprecate this due to the use of attempts once tx_history_v2 is implemented + fn process_history_loop(&self, ctx: MmArc) -> Box + Send> { + if self.history_sync_status() == HistorySyncState::NotEnabled { + return Box::new(futures01::future::ok(())); + } + + let mut my_balance: Option = None; + let coin = self.clone(); + + let fut = async move { + let history = match coin.load_history_from_file(&ctx).compat().await { + Ok(history) => history, + Err(e) => { + log_tag!( + ctx, + "", + "tx_history", + "coin" => coin.conf.ticker; + fmt = "Error {} on 'load_history_from_file', stop the history loop", e + ); + return; + }, + }; + + let mut history_map: HashMap = history + .into_iter() + .filter_map(|tx| { + let tx_hash = H256Json::from_str(tx.tx.tx_hash()?).ok()?; + Some((tx_hash, tx)) + }) + .collect(); + + let mut success_iteration = 0i32; + let mut attempts = 0; + loop { + if ctx.is_stopping() { + break; + }; + { + let coins_ctx = CoinsContext::from_ctx(&ctx).unwrap(); + let coins = coins_ctx.coins.lock().await; + if !coins.contains_key(&coin.conf.ticker) { + log_tag!(ctx, "", "tx_history", "coin" => coin.conf.ticker; fmt = "Loop stopped"); + attempts += 1; + if attempts > 6 { + log_tag!( + ctx, + "", + "tx_history", + "coin" => coin.conf.ticker; + fmt = "Loop stopped after 6 attempts to find coin in coins context" + ); + break; + } + Timer::sleep(10.).await; + continue; + }; + } + + let actual_balance = match coin.my_balance().compat().await { + Ok(actual_balance) => Some(actual_balance), + Err(err) => { + log_tag!( + ctx, + "", + "tx_history", + "coin" => coin.conf.ticker; + fmt = "Error {:?} on getting balance", err + ); + None + }, + }; + + let need_update = history_map.iter().any(|(_, tx)| tx.should_update()); + match (&my_balance, &actual_balance) { + (Some(prev_balance), Some(actual_balance)) if prev_balance == actual_balance && !need_update => { + // my balance hasn't been changed, there is no need to reload tx_history + Timer::sleep(30.).await; + continue; + }, + _ => (), + } + + // Todo: get mempool transactions and update them once they have confirmations + let filtered_events: Vec = match coin.request_events_history().await { + Ok(events) => events + .into_iter() + .filter(|event| { + event.event_type == EventType::V2Transaction + || event.event_type == EventType::V1Transaction + || event.event_type == EventType::Miner + || event.event_type == EventType::Foundation + }) + .collect(), + Err(e) => { + log_tag!( + ctx, + "", + "tx_history", + "coin" => coin.conf.ticker; + fmt = "Error {} on 'request_events_history', stop the history loop", e + ); + + Timer::sleep(10.).await; + continue; + }, + }; + + // Remove transactions in the history_map that are not in the requested transaction list anymore + let history_length = history_map.len(); + let requested_ids: HashSet = filtered_events.iter().map(|x| H256Json(x.id.0)).collect(); + history_map.retain(|hash, _| requested_ids.contains(hash)); + + if history_map.len() < history_length { + let to_write: Vec = history_map.values().cloned().collect(); + if let Err(e) = coin.save_history_to_file(&ctx, to_write).compat().await { + log_tag!( + ctx, + "", + "tx_history", + "coin" => coin.conf.ticker; + fmt = "Error {} on 'save_history_to_file', stop the history loop", e + ); + return; + }; + } + + let mut transactions_left = if requested_ids.len() > history_map.len() { + *coin.history_sync_state.lock().unwrap() = HistorySyncState::InProgress(json!({ + "transactions_left": requested_ids.len() - history_map.len() + })); + requested_ids.len() - history_map.len() + } else { + *coin.history_sync_state.lock().unwrap() = HistorySyncState::InProgress(json!({ + "transactions_left": 0 + })); + 0 + }; + + for txid in requested_ids { + let mut updated = false; + match history_map.entry(txid) { + Entry::Vacant(e) => match filtered_events.iter().find(|event| H256Json(event.id.0) == txid) { + Some(event) => { + let tx_details = match coin.tx_details_from_event(event) { + Ok(tx_details) => tx_details, + Err(e) => { + log_tag!( + ctx, + "", + "tx_history", + "coin" => coin.conf.ticker; + fmt = "Error {} on 'tx_details_from_event', stop the history loop", e + ); + return; + }, + }; + e.insert(tx_details); + if transactions_left > 0 { + transactions_left -= 1; + *coin.history_sync_state.lock().unwrap() = + HistorySyncState::InProgress(json!({ "transactions_left": transactions_left })); + } + updated = true; + }, + None => log_tag!( + ctx, + "", + "tx_history", + "coin" => coin.conf.ticker; + fmt = "Transaction with id {} not found in the events list", txid + ), + }, + Entry::Occupied(_) => {}, + } + if updated { + let to_write: Vec = history_map.values().cloned().collect(); + if let Err(e) = coin.save_history_to_file(&ctx, to_write).compat().await { + log_tag!( + ctx, + "", + "tx_history", + "coin" => coin.conf.ticker; + fmt = "Error {} on 'save_history_to_file', stop the history loop", e + ); + return; + }; + } + } + *coin.history_sync_state.lock().unwrap() = HistorySyncState::Finished; + + if success_iteration == 0 { + log_tag!( + ctx, + "😅", + "tx_history", + "coin" => coin.conf.ticker; + fmt = "history has been loaded successfully" + ); + } + + my_balance = actual_balance; + success_iteration += 1; + Timer::sleep(30.).await; + } + }; + + Box::new(fut.map(|_| Ok(())).boxed().compat()) } fn history_sync_status(&self) -> HistorySyncState { - unimplemented!() + self.history_sync_state.lock().unwrap().clone() } - /// Get fee to be paid per 1 swap transaction + // This is only utilized by the now deprecated get_trade_fee RPC method and should be removed + // from the MmCoin trait fn get_trade_fee(&self) -> Box + Send> { - unimplemented!() + Box::new(futures01::future::err("SiaCoin::get_trade_fee: Unsupported".into())) } + // Todo: Modify this when not using `DEFAULT_FEE` async fn get_sender_trade_fee( &self, _value: TradePreimageValue, _stage: FeeApproxStage, ) -> TradePreimageResult { - unimplemented!() + Ok(TradeFee { + coin: self.conf.ticker.clone(), + amount: hastings_to_siacoin(Currency::DEFAULT_FEE).into(), + paid_from_trading_vol: false, + }) } + /// Get the transaction fee required to spend the HTLC output + // Todo: Modify this when not using `DEFAULT_FEE` fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { - unimplemented!() + let ticker = self.conf.ticker.clone(); + let fut = async move { + Ok(TradeFee { + coin: ticker, + amount: hastings_to_siacoin(Currency::DEFAULT_FEE).into(), + paid_from_trading_vol: true, + }) + }; + Box::new(fut.boxed().compat()) } + // Todo: Modify this when not using `DEFAULT_FEE` async fn get_fee_to_send_taker_fee( &self, _dex_fee_amount: DexFee, _stage: FeeApproxStage, ) -> TradePreimageResult { - unimplemented!() + Ok(TradeFee { + coin: self.conf.ticker.clone(), + amount: hastings_to_siacoin(Currency::DEFAULT_FEE).into(), + paid_from_trading_vol: false, + }) } fn required_confirmations(&self) -> u64 { - unimplemented!() + self.required_confirmations.load(AtomicOrdering::Relaxed) } fn requires_notarization(&self) -> bool { false } - fn set_required_confirmations(&self, _confirmations: u64) { - unimplemented!() + fn set_required_confirmations(&self, confirmations: u64) { + self.required_confirmations + .store(confirmations, AtomicOrdering::Relaxed); } - fn set_requires_notarization(&self, _requires_nota: bool) { - unimplemented!() - } + fn set_requires_notarization(&self, _requires_nota: bool) {} fn swap_contract_address(&self) -> Option { - unimplemented!() + None } fn fallback_swap_contract(&self) -> Option { - unimplemented!() + None } fn mature_confirmations(&self) -> Option { - unimplemented!() + None } fn coin_protocol_info(&self, _amount_to_receive: Option) -> Vec { @@ -343,39 +693,33 @@ impl MmCoin for SiaCoin { } fn on_disabled(&self) -> Result<(), AbortedError> { - Ok(()) + self.abortable_system.abort_all() } fn on_token_deactivated(&self, _ticker: &str) {} } -// TODO Alright - Dummy values for these functions allow minimal functionality to produce signatures #[async_trait] impl MarketCoinOps for SiaCoin { fn ticker(&self) -> &str { - &self.0.conf.ticker + &self.conf.ticker } - // needs test coverage FIXME COME BACK fn my_address(&self) -> MmResult { - let key_pair = match &self.0.priv_key_policy { + let key_pair = match &*self.priv_key_policy { PrivKeyPolicy::Iguana(key_pair) => key_pair, - PrivKeyPolicy::Trezor => { - return Err(MyAddressError::UnexpectedDerivationMethod( - "Trezor not yet supported. Must use iguana seed.".to_string(), - ) - .into()); - }, - PrivKeyPolicy::HDWallet { .. } => { + PrivKeyPolicy::Trezor | PrivKeyPolicy::HDWallet { .. } => { return Err(MyAddressError::UnexpectedDerivationMethod( - "HDWallet not yet supported. Must use iguana seed.".to_string(), + "SiaCoin::my_address: Unexpected Key Derivation Method.".to_string(), ) .into()); }, #[cfg(target_arch = "wasm32")] PrivKeyPolicy::Metamask(_) => { return Err(MyAddressError::UnexpectedDerivationMethod( - "Metamask not supported. Must use iguana seed.".to_string(), + "SiaCoin::my_address: Unexpected Key Derivation Method." + .to_string() + .to_string(), ) .into()); }, @@ -386,39 +730,61 @@ impl MarketCoinOps for SiaCoin { .into()) }, }; - let address = SpendPolicy::PublicKey(key_pair.public).address(); + let address = key_pair.public().address(); Ok(address.to_string()) } + // Todo: Implement in this PR, this was added to dev while work in this code was being done fn address_from_pubkey(&self, pubkey: &H264Json) -> MmResult { - let pubkey = PublicKey::from_bytes(&pubkey.0[..32]).map_err(|e| { - AddressFromPubkeyError::InternalError(format!("Couldn't parse bytes into ed25519 pubkey: {e:?}")) - })?; - let address = SpendPolicy::PublicKey(pubkey).address(); - Ok(address.to_string()) + let pubkey_bytes = &pubkey.0[..32]; + let pubkey = + PublicKey::from_bytes(pubkey_bytes).map_err(|e| AddressFromPubkeyError::InternalError(e.to_string()))?; + Ok(pubkey.address().to_string()) } async fn get_public_key(&self) -> Result> { - MmError::err(UnexpectedDerivationMethod::InternalError("Not implemented".into())) + let public_key = match &*self.priv_key_policy { + PrivKeyPolicy::Iguana(key_pair) => key_pair.public(), + PrivKeyPolicy::Trezor => { + return MmError::err(UnexpectedDerivationMethod::ExpectedSingleAddress); + }, + PrivKeyPolicy::HDWallet { .. } => { + return MmError::err(UnexpectedDerivationMethod::ExpectedSingleAddress); + }, + #[cfg(target_arch = "wasm32")] + PrivKeyPolicy::Metamask(_) => { + return MmError::err(UnexpectedDerivationMethod::ExpectedSingleAddress); + }, + PrivKeyPolicy::WalletConnect { .. } => { + return MmError::err(UnexpectedDerivationMethod::ExpectedSingleAddress); + }, + }; + Ok(public_key.to_string()) } + // TODO Alright - Unsupported and will be removed - see dev comments in trait declaration fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]> { None } + // Todo: needed as part of feature completion fn sign_message(&self, _message: &str, _address: Option) -> SignatureResult { - MmError::err(SignatureError::InternalError("Not implemented".into())) + MmError::err(SignatureError::InternalError( + "SiaCoin::sign_message: Unsupported".to_string(), + )) } fn verify_message(&self, _signature: &str, _message: &str, _address: &str) -> VerificationResult { - MmError::err(VerificationError::InternalError("Not implemented".into())) + MmError::err(VerificationError::InternalError( + "SiaCoin::verify_message: Unsupported".to_string(), + )) } fn my_balance(&self) -> BalanceFut { let coin = self.clone(); let fut = async move { - let my_address = match &coin.0.priv_key_policy { - PrivKeyPolicy::Iguana(key_pair) => SpendPolicy::PublicKey(key_pair.public).address(), + let my_address = match &*coin.priv_key_policy { + PrivKeyPolicy::Iguana(key_pair) => key_pair.public().address(), _ => { return MmError::err(BalanceError::UnexpectedDerivationMethod( UnexpectedDerivationMethod::ExpectedSingleAddress, @@ -426,34 +792,49 @@ impl MarketCoinOps for SiaCoin { }, }; let balance = coin - .0 - .http_client + .client .address_balance(my_address) .await .map_to_mm(|e| BalanceError::Transport(e.to_string()))?; Ok(CoinBalance { - spendable: siacoin_from_hastings(balance.siacoins.to_u128()), - unspendable: siacoin_from_hastings(balance.immature_siacoins.to_u128()), + spendable: hastings_to_siacoin(balance.siacoins), + unspendable: hastings_to_siacoin(balance.immature_siacoins), }) }; Box::new(fut.boxed().compat()) } fn platform_coin_balance(&self) -> BalanceFut { - unimplemented!() + Box::new(self.my_balance().map(|res| res.spendable)) } fn platform_ticker(&self) -> &str { - "FOO" - } // TODO Alright + self.ticker() + } /// Receives raw transaction bytes in hexadecimal format as input and returns tx hash in hexadecimal format - fn send_raw_tx(&self, _tx: &str) -> Box + Send> { - unimplemented!() + fn send_raw_tx(&self, tx: &str) -> Box + Send> { + let client = self.client.clone(); + let tx = tx.to_owned(); + + let fut = async move { + let tx: Json = serde_json::from_str(&tx).map_err(|e| e.to_string())?; + let transaction = serde_json::from_str::(&tx.to_string()).map_err(|e| e.to_string())?; + let txid = transaction.txid().to_string(); + + client + .broadcast_transaction(&transaction) + .await + .map_err(|e| e.to_string())?; + Ok(txid) + }; + Box::new(fut.boxed().compat()) } - fn send_raw_tx_bytes(&self, _tx: &[u8]) -> Box + Send> { - unimplemented!() + fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { + let tx: V2Transaction = try_fus!(serde_json::from_slice(tx).map_err(|e| e.to_string())); + let str_tx = try_fus!(serde_json::to_string(&tx).map_err(|e| e.to_string())); + self.send_raw_tx(&str_tx) } #[inline(always)] @@ -461,40 +842,75 @@ impl MarketCoinOps for SiaCoin { unimplemented!() } - fn wait_for_confirmations(&self, _input: ConfirmPaymentInput) -> Box + Send> { - unimplemented!() + // TODO Alright - match the standard convention of Tryfrom for SiaConfirmPaymentInput + fn wait_for_confirmations(&self, input: ConfirmPaymentInput) -> Box + Send> { + let tx: SiaTransaction = try_fus!(serde_json::from_slice(&input.payment_tx) + .map_err(|e| format!("siacoin wait_for_confirmations payment_tx deser failed: {}", e))); + let txid = tx.txid(); + let client = self.client.clone(); + let tx_request = GetEventRequest { txid: txid.clone() }; + + let fut = async move { + loop { + if now_sec() > input.wait_until { + return ERR!( + "Waited too long until {} for payment {} to be received", + input.wait_until, + tx.txid() + ); + } + + match client.dispatcher(tx_request.clone()).await { + Ok(event) => { + if event.confirmations >= input.confirmations { + return Ok(()); + } + }, + Err(e) => info!("Waiting for confirmation of Sia txid {}: {}", txid, e), + } + + Timer::sleep(input.check_every as f64).await; + } + }; + + Box::new(fut.boxed().compat()) } - async fn wait_for_htlc_tx_spend(&self, _args: WaitForHTLCTxSpendArgs<'_>) -> TransactionResult { - unimplemented!() + async fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionResult { + self.sia_wait_for_htlc_tx_spend(args) + .await + .map_err(|e| TransactionErr::Plain(e.to_string())) } - fn tx_enum_from_bytes(&self, _bytes: &[u8]) -> Result> { - MmError::err(TxMarshalingErr::NotSupported( - "tx_enum_from_bytes is not supported for Sia coin yet.".to_string(), - )) + fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result> { + let tx: V2Transaction = + serde_json::from_slice(bytes).map_to_mm(|e| TxMarshalingErr::InvalidInput(e.to_string()))?; + Ok(TransactionEnum::SiaTransaction(SiaTransaction(tx))) } fn current_block(&self) -> Box + Send> { - let http_client = self.0.http_client.clone(); // Clone the client + let client = self.client.clone(); // Clone the client - let height_fut = async move { http_client.current_height().await.map_err(|e| e.to_string()) } + let height_fut = async move { client.current_height().await.map_err(|e| e.to_string()) } .boxed() // Make the future 'static by boxing .compat(); // Convert to a futures 0.1-compatible future Box::new(height_fut) } + // This remains unimplemented because the response is meaningless to a typical sia user since + // all Sia software only ever presents or accepts seed phrases, never raw private keys. + // TODO Alright: provide useful import/export functionality prior to mainnet v2 activation fn display_priv_key(&self) -> Result { - unimplemented!() + Err("SiaCoin::display_priv_key: Unsupported".to_string()) } fn min_tx_amount(&self) -> BigDecimal { - unimplemented!() + hastings_to_siacoin(1u64.into()) } fn min_trading_vol(&self) -> MmNumber { - unimplemented!() + hastings_to_siacoin(1u64.into()).into() } fn should_burn_dex_fee(&self) -> bool { @@ -502,69 +918,920 @@ impl MarketCoinOps for SiaCoin { } fn is_trezor(&self) -> bool { - self.0.priv_key_policy.is_trezor() + self.priv_key_policy.is_trezor() + } +} + +// contains various helpers to account for subpar error handling trait method signatures +impl SiaCoin { + pub fn my_keypair(&self) -> Result<&SiaKeypair, SiaCoinMyKeypairError> { + match &*self.priv_key_policy { + PrivKeyPolicy::Iguana(keypair) => Ok(keypair), + _ => Err(SiaCoinMyKeypairError::PrivKeyPolicy), + } + } +} + +// contains imeplementations of the SwapOps trait methods with proper error handling +// Some of these methods are extremely verbose and can obviously be refactored to be more consise. +// However, the SwapOps trait is expected to be refactored to use associated types for types such as +// Address, PublicKey, Currency and Error types. +// TODO Alright : refactor error types of SwapOps methods to use associated types +impl SiaCoin { + /// Create a new transaction to send the taker fee to the fee address + async fn new_send_taker_fee( + &self, + dex_fee: DexFee, + uuid: &[u8], + _expire_at: u64, + ) -> Result { + // Check the Uuid provided is valid v4 as we will encode it into the transaction + let uuid_type_check = Uuid::from_slice(uuid)?; + + match uuid_type_check.get_version_num() { + 4 => (), + version => return Err(SendTakerFeeError::UuidVersion(version)), + } + + // Convert the DexFee to a Currency amount + let trade_fee_amount = match dex_fee { + DexFee::Standard(mm_num) => siacoin_to_hastings(BigDecimal::from(mm_num))?, + wrong_variant => return Err(SendTakerFeeError::DexFeeVariant(wrong_variant)), + }; + + let my_keypair = self.my_keypair()?; + + // Create a new transaction builder + let tx = V2TransactionBuilder::new() + // FIXME Alright: Calculate the miner fee amount + .miner_fee(Currency::DEFAULT_FEE) + // Add the trade fee output + .add_siacoin_output((FEE_ADDR.clone(), trade_fee_amount).into()) + // Fund the transaction + .fund_tx_single_source(&self.client, &my_keypair.public()) + .await? + // Embed swap uuid to provide better validation from maker + .arbitrary_data(uuid.to_vec().into()) + .add_change_output(&my_keypair.public().address()) + // Sign inputs and finalize the transaction + .sign_simple(vec![my_keypair]) + .build(); + + // Broadcast the transaction + self.client.broadcast_transaction(&tx).await?; + + Ok(TransactionEnum::SiaTransaction(tx.into())) + } + + async fn new_send_maker_payment( + &self, + args: SendPaymentArgs<'_>, + ) -> Result { + let my_keypair = self.my_keypair()?; + + let maker_public_key = my_keypair.public(); + + // TODO Alright - pubkey padding, see SiaCoin::derive_htlc_pubkey + if args.other_pubkey.len() != 33 { + return Err(SendMakerPaymentError::InvalidTakerPublicKeyLength( + args.other_pubkey.to_vec(), + )); + } + let taker_public_key = PublicKey::from_bytes(&args.other_pubkey[..32])?; + + let secret_hash = Hash256::try_from(args.secret_hash)?; + + // Generate HTLC SpendPolicy + let htlc_spend_policy = + SpendPolicy::atomic_swap(&taker_public_key, &maker_public_key, args.time_lock, &secret_hash); + + // Convert the trade amount to a Currency amount + let trade_amount = siacoin_to_hastings(args.amount)?; + + // Create a new transaction builder + let tx = V2TransactionBuilder::new() + // FIXME Alright: Calculate the miner fee amount + .miner_fee(Currency::DEFAULT_FEE) + // Add the HTLC output + .add_siacoin_output((htlc_spend_policy.address(), trade_amount).into()) + // Fund the transaction from my_keypair + .fund_tx_single_source(&self.client, &my_keypair.public()) + .await? + .add_change_output(&my_keypair.public().address()) + // Sign inputs + .sign_simple(vec![my_keypair]) + .build(); + + // Broadcast the transaction + self.client.broadcast_transaction(&tx).await?; + + Ok(TransactionEnum::SiaTransaction(tx.into())) + } + + async fn new_send_taker_payment( + &self, + args: SendPaymentArgs<'_>, + ) -> Result { + let my_keypair = self.my_keypair()?; + + let taker_public_key = my_keypair.public(); + + // TODO Alright - pubkey padding, see SiaCoin::derive_htlc_pubkey + if args.other_pubkey.len() != 33 { + return Err(SendTakerPaymentError::InvalidMakerPublicKeyLength( + args.other_pubkey.to_vec(), + )); + } + let maker_public_key = PublicKey::from_bytes(&args.other_pubkey[..32])?; + + let secret_hash = Hash256::try_from(args.secret_hash)?; + + // Generate HTLC SpendPolicy + let htlc_spend_policy = + SpendPolicy::atomic_swap(&maker_public_key, &taker_public_key, args.time_lock, &secret_hash); + + // Convert the trade amount to a Currency amount + let trade_amount = siacoin_to_hastings(args.amount)?; + + // Create a new transaction builder + let tx = V2TransactionBuilder::new() + // Set the miner fee amount + .miner_fee(Currency::DEFAULT_FEE) + // Add the HTLC output + .add_siacoin_output((htlc_spend_policy.address(), trade_amount).into()) + // Fund(add enough inputs) the transaction + .fund_tx_single_source(&self.client, &my_keypair.public()) + .await? + .add_change_output(&my_keypair.public().address()) + // Sign inputs and finalize the transaction + .sign_simple(vec![my_keypair]) + .build(); + + // Broadcast the transaction + self.client.broadcast_transaction(&tx).await?; + + Ok(TransactionEnum::SiaTransaction(tx.into())) + } + + // TODO Alright - this is logically the same as new_send_taker_spends_maker_payment except + // maker_public_key, taker_public being swapped. Refactor to reduce code duplication + async fn new_send_maker_spends_taker_payment( + &self, + args: SpendPaymentArgs<'_>, + ) -> Result { + let my_keypair = self.my_keypair()?; + + let maker_public_key = my_keypair.public(); + + // TODO Alright - pubkey padding, see SiaCoin::derive_htlc_pubkey + if args.other_pubkey.len() != 33 { + return Err(MakerSpendsTakerPaymentError::InvalidTakerPublicKeyLength( + args.other_pubkey.to_vec(), + )); + } + let taker_public_key = PublicKey::from_bytes(&args.other_pubkey[..32])?; + + let taker_payment_tx = SiaTransaction::try_from(args.other_payment_tx.to_vec())?; + let taker_payment_txid = taker_payment_tx.txid(); + + let secret = Preimage::try_from(args.secret)?; + let secret_hash = Hash256::try_from(args.secret_hash)?; + // TODO Alright could do `sha256(secret) == secret_hash`` sanity check here + + // Generate HTLC SpendPolicy as it will appear in the SiacoinInputV2 that spends taker payment + let input_spend_policy = + SpendPolicy::atomic_swap_success(&maker_public_key, &taker_public_key, args.time_lock, &secret_hash); + + // Fetch the HTLC UTXO from the taker payment transaction + let htlc_utxo = self + .client + .utxo_from_txid(&taker_payment_txid, 0) + .await + .map_err(Box::new)?; + + // FIXME Alright this transaction will have a fixed size, calculate the miner fee amount + // after we have the actual transaction size + let miner_fee = Currency::DEFAULT_FEE; + let htlc_utxo_amount = htlc_utxo.output.siacoin_output.value; + + // Create a new transaction builder + let tx = V2TransactionBuilder::new() + // Set the miner fee amount + .miner_fee(miner_fee) + // Add output of maker spending to self + .add_siacoin_output((maker_public_key.address(), htlc_utxo_amount - miner_fee).into()) + // Add input spending the HTLC output + .add_siacoin_input(htlc_utxo.output, input_spend_policy) + // Satisfy the HTLC by providing a signature and the secret + .satisfy_atomic_swap_success(my_keypair, secret, 0u32)? + .build(); + + // Broadcast the transaction + self.client.broadcast_transaction(&tx).await?; + + Ok(TransactionEnum::SiaTransaction(tx.into())) + } + + async fn new_send_taker_spends_maker_payment( + &self, + args: SpendPaymentArgs<'_>, + ) -> Result { + let my_keypair = self.my_keypair()?; + + let taker_public_key = my_keypair.public(); + + // TODO Alright - pubkey padding, see SiaCoin::derive_htlc_pubkey + if args.other_pubkey.len() != 33 { + return Err(TakerSpendsMakerPaymentError::InvalidMakerPublicKeyLength( + args.other_pubkey.to_vec(), + )); + }; + let maker_public_key = PublicKey::from_bytes(&args.other_pubkey[..32])?; + + let maker_payment_tx = SiaTransaction::try_from(args.other_payment_tx.to_vec())?; + let maker_payment_txid = maker_payment_tx.txid(); + + let secret = Preimage::try_from(args.secret)?; + let secret_hash = Hash256::try_from(args.secret_hash)?; + // TODO Alright could do `sha256(secret) == secret_hash`` sanity check here + + // Generate HTLC SpendPolicy as it will appear in the SiacoinInputV2 that spends taker payment + let input_spend_policy = + SpendPolicy::atomic_swap_success(&taker_public_key, &maker_public_key, args.time_lock, &secret_hash); + + // Fetch the HTLC UTXO from the taker payment transaction + let htlc_utxo = self + .client + .utxo_from_txid(&maker_payment_txid, 0) + .await + .map_err(Box::new)?; + + let miner_fee = Currency::DEFAULT_FEE; + let htlc_utxo_amount = htlc_utxo.output.siacoin_output.value; + + // Create a new transaction builder + let tx = V2TransactionBuilder::new() + // Set the miner fee amount + .miner_fee(miner_fee) + // Add output of taker spending to self + .add_siacoin_output((taker_public_key.address(), htlc_utxo_amount - miner_fee).into()) + // Add input spending the HTLC output + .add_siacoin_input_with_basis(htlc_utxo, input_spend_policy) + // Satisfy the HTLC by providing a signature and the secret + .satisfy_atomic_swap_success(my_keypair, secret, 0u32)? + .build(); + + // Broadcast the transaction + self.client.broadcast_transaction(&tx).await?; + + Ok(TransactionEnum::SiaTransaction(tx.into())) + } + + async fn new_validate_fee(&self, args: ValidateFeeArgs<'_>) -> Result<(), ValidateFeeError> { + let args = SiaValidateFeeArgs::try_from(args)?; + + // Transaction provided by peer via p2p stack + let peer_tx = args.fee_tx.0.clone(); + let fee_txid = peer_tx.txid(); + + let found_in_block = self.client.get_event(&fee_txid).await; + + // TODO Alright - this creates a significant mess in the Error type and can be simplified + // with a helper method for fetching transactions from chain or mempool + let fee_tx = match found_in_block { + Ok(event) => { + // check fetched event is V2Transaction + let tx = match event.data { + EventDataWrapper::V2Transaction(tx) => tx, + _ => return Err(ValidateFeeError::EventVariant(event)), + }; + + // check tx confirmed at or after min_block_number + let confirmed_at_height = event.index.height; + if confirmed_at_height < args.min_block_number { + return Err(ValidateFeeError::MininumConfirmedHeight { + txid: tx.txid(), + min_block_number: args.min_block_number, + }); + } + tx + }, + Err(e) => { + // Log the error incase of actual error rather than just not finding the tx + // TODO Alright - can be simplified, see get_transaction FIXME dev comment + debug!( + "SiaCoin::new_validate_fee: fee_tx not found on chain {}, checking mempool", + e + ); + match self.client.get_unconfirmed_transaction(&fee_txid).await? { + Some(tx) => { + let current_height = self.client.current_height().await?; + // if tx found in mempool, check that it would confirm at or after min_block_number + if current_height < args.min_block_number { + return Err(ValidateFeeError::MininumMempoolHeight { + txid: tx.txid(), + min_block_number: args.min_block_number, + }); + } + tx + }, + None => return Err(ValidateFeeError::TxNotFound(fee_txid.clone())), + } + }, + }; + + // check that all inputs originate from taker address + // This mimicks the behavior of KDF's utxo_standard protocol for consistency. + // TODO Alright - Logically there seems no reason to enforce this? Why would maker care + // where the fee comes from? + if !fee_tx + .siacoin_inputs + .into_iter() + .all(|input| input.satisfied_policy.policy.address() == args.taker_public_key.address()) + { + return Err(ValidateFeeError::InputsOrigin(fee_txid.clone())); + } + + // check that fee_tx has 1 or 2 outputs + match fee_tx.siacoin_outputs.len() { + 1 | 2 => (), + outputs_length => { + return Err(ValidateFeeError::VoutLength { + txid: fee_txid.clone(), + outputs_length, + }) + }, + } + + // check that output 0 pays the fee address + if fee_tx.siacoin_outputs[0].address != *FEE_ADDR { + return Err(ValidateFeeError::InvalidFeeAddress { + txid: fee_txid.clone(), + address: fee_tx.siacoin_outputs[0].address.clone(), + }); + } + + // check that output 0 is the correct amount, trade_fee_amount + if fee_tx.siacoin_outputs[0].value != args.dex_fee_amount { + return Err(ValidateFeeError::InvalidFeeAmount { + txid: fee_txid.clone(), + expected: args.dex_fee_amount, + actual: fee_tx.siacoin_outputs[0].value, + }); + } + + // check that arbitrary_data is the same as the uuid + let fee_tx_uuid = Uuid::from_slice(&fee_tx.arbitrary_data.0)?; + if fee_tx_uuid != args.uuid { + return Err(ValidateFeeError::InvalidUuid { + txid: fee_txid.clone(), + expected: args.uuid, + actual: fee_tx_uuid, + }); + } + + Ok(()) + } + + async fn send_refund_htlc(&self, args: RefundPaymentArgs<'_>) -> Result { + let my_keypair = self.my_keypair()?; + let refund_public_key = my_keypair.public(); + + // parse KDF provided data to Sia specific types + let sia_args = SiaRefundPaymentArgs::try_from(args)?; + + // Generate HTLC SpendPolicy as it will appear in the SiacoinInputV2 + let input_spend_policy = SpendPolicy::atomic_swap_refund( + &sia_args.success_public_key, + &refund_public_key, + sia_args.time_lock, + &sia_args.secret_hash, + ); + + // Fetch the HTLC UTXO from the payment_tx transaction + let htlc_utxo = self + .client + .utxo_from_txid(&sia_args.payment_tx.txid(), 0) + .await + .map_err(Box::new)?; + + let miner_fee = Currency::DEFAULT_FEE; + let htlc_utxo_amount = htlc_utxo.output.siacoin_output.value; + + // Create a new transaction builder + let tx = V2TransactionBuilder::new() + // Set the miner fee amount + .miner_fee(miner_fee) + // Add output of taker spending to self + .add_siacoin_output((my_keypair.public().address(), htlc_utxo_amount - miner_fee).into()) + // Add input spending the HTLC output + .add_siacoin_input_with_basis(htlc_utxo, input_spend_policy) + // Satisfy the HTLC by providing a signature and the secret + .satisfy_atomic_swap_refund(my_keypair, 0u32)? + .build(); + + // Broadcast the transaction + self.client.broadcast_transaction(&tx).await?; + + Ok(TransactionEnum::SiaTransaction(tx.into())) + } + + async fn new_check_if_my_payment_sent( + &self, + args: CheckIfMyPaymentSentArgs<'_>, + ) -> Result, SiaCheckIfMyPaymentSentError> { + // parse arguments to Sia specific types + let sia_args = SiaCheckIfMyPaymentSentArgs::try_from(args)?; + + // Get my_keypair.public() to use in HTLC SpendPolicy + let my_keypair = self.my_keypair()?; + let refund_public_key = my_keypair.public(); + + // Generate HTLC SpendPolicy and corresponding address + let spend_policy = SpendPolicy::atomic_swap( + &sia_args.success_public_key, + &refund_public_key, + sia_args.time_lock, + &sia_args.secret_hash, + ); + let htlc_address = spend_policy.address(); + + // Fetch all events for the HTLC address + let events_result = self.client.get_address_events(htlc_address).await; + let events = match events_result { + Ok(events) => events, + Err(_) => return Ok(None), + }; + + // return Ok(None) if no events found - This indicates the payment has not been sent. + let event = match events.len() { + 0 => return Ok(None), + _ => events[0].clone(), + }; + + let tx = match event.data { + EventDataWrapper::V2Transaction(tx) => tx, + wrong_variant => return Err(SiaCheckIfMyPaymentSentError::EventVariant(wrong_variant)), + }; + + // TODO Alright - check that vout index is correct, check amount is correct + // Unclear what the consequence of selecting the wrong transaction might have + // The current implementation matches the UtxoStandardCoin logic + Ok(Some(SiaTransaction(tx).into())) + } + + #[allow(clippy::result_large_err)] + fn sia_extract_secret( + &self, + expected_hash_slice: &[u8], + spend_tx: &[u8], + watcher_reward: bool, + ) -> Result<[u8; 32], SiaCoinSiaExtractSecretError> { + // Parse arguments to Sia specific types + let tx = SiaTransaction::try_from(spend_tx)?; + let expected_hash = Hash256::try_from(expected_hash_slice)?; + + // watcher_reward is irrelevant, but a true value indicates a bug within the swap protocol + // An error is not thrown as it would not be in the best interest of the swap participant + // if they are still able to extract the secret and spend the HTLC output + if watcher_reward { + debug!("SiaCoin::sia_extract_secret: expected watcher_reward false, found true"); + } + + // iterate over all inputs and search for preimage that hashes to expected_hash + let found_secret = + tx.0.siacoin_inputs + .iter() + // flat_map to iterate over all preimages of all inputs + .flat_map(|input| input.satisfied_policy.preimages.iter()) + // hash each included preimage and check if secret_hash==sha256(preimage) + .find(|extracted_secret| { + let check_secret_hash = Hash256(sha256(&extracted_secret.0).take()); + check_secret_hash == expected_hash + }); + + // Map Sia types to SwapOps expected types + found_secret + .map(|secret| secret.0) + .ok_or(SiaCoinSiaExtractSecretError::FailedToExtract { tx, expected_hash }) + } + + /// Determines if the HTLC output can be spent via refund path or if additional time must pass + async fn sia_can_refund_htlc(&self, locktime: u64) -> Result { + let median_timestamp = self.client.get_median_timestamp().await?; + + if locktime < median_timestamp { + return Ok(CanRefundHtlc::CanRefundNow); + } + Ok(CanRefundHtlc::HaveToWait(locktime - median_timestamp)) + } + + async fn sia_wait_for_htlc_tx_spend( + &self, + args: WaitForHTLCTxSpendArgs<'_>, + ) -> Result { + let sia_args = SiaWaitForHTLCTxSpendArgs::try_from(args)?; + + let htlc_lock_txid = sia_args.tx.txid(); + let output_id = SiacoinOutputId::new(htlc_lock_txid.clone(), HTLC_VOUT_INDEX); + loop { + // search the memory pool by txid first + let found_in_mempool = self + .client + .dispatcher(TxpoolTransactionsRequest) + .await + .unwrap_or({ + // log any client error here because we must continue to search for the tx + // in the blockchain regardless of the error + debug!("SiaCoin::sia_wait_for_htlc_tx_spend: failed to fetch mempool transactions"); + TxpoolTransactionsResponse::default() + }) + .v2transactions + .into_iter() + .find(|tx| tx.siacoin_inputs.iter().any(|input| input.parent.id == output_id)); + + if let Some(tx) = found_in_mempool { + return Ok(TransactionEnum::SiaTransaction(SiaTransaction(tx))); + } + + // Search confirmed blocks + let found_in_block = self.client.find_where_utxo_spent(&output_id).await; + + match found_in_block { + Ok(Some(tx)) => return Ok(TransactionEnum::SiaTransaction(SiaTransaction(tx))), + // An Err is expected if the UTXO is not found in the blockchain yet. + // FIXME Alright - An error may also be thrown if the server has dropped the spent + // UTXO from its index. Need to analyze when this may happen. The indexer node will + // generally keep ~24 hours of UTXO history. If we hit this case, we need to blindly + // attempt to spend the UTXO ourselves. + Err(e) => debug!( + "SiaCoin::sia_wait_for_htlc_tx_spend: find_where_utxo_spent failed, continue searching: {}", + e + ), + _ => (), + } + + // Check timeout + if now_sec() >= sia_args.wait_until { + return Err(SiaWaitForHTLCTxSpendError::Timeout { txid: htlc_lock_txid }); + } + + // Wait before trying again + Timer::sleep(sia_args.check_every).await; + } + } + + /// Validates that a given transaction has the expected HTLC output at HTLC_VOUT_INDEX + async fn validate_htlc_payment(&self, input: ValidatePaymentInput) -> Result<(), SiaValidateHtlcPaymentError> { + let sia_args = SiaValidatePaymentInput::try_from(input)?; + + let my_keypair = self.my_keypair()?; + let success_public_key = my_keypair.public(); + let refund_public_key = sia_args.other_pub; + + // Generate the expected HTLC address where funds should be locked + let htlc_address = SpendPolicy::atomic_swap( + &success_public_key, + &refund_public_key, + sia_args.time_lock, + &sia_args.secret_hash, + ) + .address(); + + // Build the expected HTLC output + let expected_htlc_output = SiacoinOutput { + value: sia_args.amount, + address: htlc_address, + }; + + // Check that the transaction has the expected output at HTLC_VOUT_INDEX + let htlc_output = match sia_args.payment_tx.0.siacoin_outputs.get(HTLC_VOUT_INDEX as usize) { + Some(output) => output, + None => { + return Err(SiaValidateHtlcPaymentError::InvalidOutputLength { + expected: HTLC_VOUT_INDEX + 1, + actual: sia_args.payment_tx.0.siacoin_outputs.len() as u32, + txid: sia_args.payment_tx.0.txid(), + }) + }, + }; + + if *htlc_output != expected_htlc_output { + return Err(SiaValidateHtlcPaymentError::InvalidOutput { + expected: expected_htlc_output, + actual: htlc_output.clone(), + txid: sia_args.payment_tx.0.txid(), + }); + } + + Ok(()) + } + + async fn sia_validate_maker_payment( + &self, + input: ValidatePaymentInput, + ) -> Result<(), SiaValidateMakerPaymentError> { + Ok(self.validate_htlc_payment(input).await?) + } + + async fn sia_validate_taker_payment( + &self, + input: ValidatePaymentInput, + ) -> Result<(), SiaValidateTakerPaymentError> { + Ok(self.validate_htlc_payment(input).await?) + } +} + +/// Sia typed equivalent of coins::ValidatePaymentInput +#[derive(Clone, Debug)] +struct SiaValidatePaymentInput { + payment_tx: SiaTransaction, + time_lock: u64, + other_pub: PublicKey, + secret_hash: Hash256, + amount: Currency, +} + +impl TryFrom for SiaValidatePaymentInput { + type Error = SiaValidatePaymentInputError; + + fn try_from(args: ValidatePaymentInput) -> Result { + let payment_tx = SiaTransaction::try_from(args.payment_tx.to_vec())?; + + // TODO Alright - pubkey padding, see SiaCoin::derive_htlc_pubkey + if args.other_pub.len() != 33 { + return Err(SiaValidatePaymentInputError::InvalidOtherPublicKeyLength( + args.other_pub, + )); + } + let other_pub = PublicKey::from_bytes(&args.other_pub[..32])?; + + let secret_hash = Hash256::try_from(args.secret_hash.as_slice())?; + let amount = siacoin_to_hastings(args.amount)?; + + Ok(SiaValidatePaymentInput { + payment_tx, + time_lock: args.time_lock, + other_pub, + secret_hash, + amount, + }) + } +} +/// Sia typed equivalent of coins::RefundPaymentArgs +pub struct SiaRefundPaymentArgs { + payment_tx: SiaTransaction, + time_lock: u64, + success_public_key: PublicKey, + secret_hash: Hash256, +} + +impl TryFrom> for SiaRefundPaymentArgs { + type Error = SiaRefundPaymentArgsError; + + fn try_from(args: RefundPaymentArgs<'_>) -> Result { + let payment_tx = SiaTransaction::try_from(args.payment_tx.to_vec())?; + + let time_lock = args.time_lock; + + // TODO Alright - pubkey padding, see SiaCoin::derive_htlc_pubkey + if args.other_pubkey.len() != 33 { + return Err(SiaRefundPaymentArgsError::InvalidOtherPublicKeyLength( + args.other_pubkey.to_vec(), + )); + } + let success_public_key = PublicKey::from_bytes(&args.other_pubkey[..32])?; + + let secret_hash_slice = match args.tx_type_with_secret_hash { + SwapTxTypeWithSecretHash::TakerOrMakerPayment { maker_secret_hash } => maker_secret_hash, + wrong_variant => { + return Err(SiaRefundPaymentArgsError::SwapTxTypeVariant(format!( + "{:?}", + wrong_variant + ))); + }, + }; + + let secret_hash = Hash256::try_from(secret_hash_slice)?; + + // TODO Alright - check watcher_reward=false, swap_unique_data and swap_contract_address are valid??? + // currently unclear what swap_unique_data and swap_contract_address are used for(if anything) + // in the context of Sia + + Ok(SiaRefundPaymentArgs { + payment_tx, + time_lock, + success_public_key, + secret_hash, + }) + } +} + +// +/// Sia typed equivalent of coins::ValidateFeeArgs +/// fee_addr from ValidateFeeArgs is not relevant to Sia because it is a secp256k1 public key +/// Sia requires a ed25519 public key, so FEE_ADDR is used instead +#[derive(Clone, Debug)] +struct SiaValidateFeeArgs { + fee_tx: SiaTransaction, + taker_public_key: PublicKey, + dex_fee_amount: Currency, + min_block_number: u64, + uuid: Uuid, +} + +impl TryFrom> for SiaValidateFeeArgs { + type Error = SiaValidateFeeArgsError; + + fn try_from(args: ValidateFeeArgs<'_>) -> Result { + // Extract the fee tx from TransactionEnum + let fee_tx = match args.fee_tx { + TransactionEnum::SiaTransaction(tx) => tx.clone(), + wrong_variant => return Err(SiaValidateFeeArgsError::TxEnumVariant(wrong_variant.clone())), + }; + + // TODO Alright - pubkey padding, see SiaCoin::derive_htlc_pubkey + if args.expected_sender.len() != 33 { + return Err(SiaValidateFeeArgsError::InvalidTakerPublicKeyLength( + args.expected_sender.to_vec(), + )); + } + + let expected_sender_public_key = PublicKey::from_bytes(&args.expected_sender[..32])?; + + // Convert the DexFee to a Currency amount + let dex_fee_amount = match args.dex_fee { + DexFee::Standard(mm_num) => siacoin_to_hastings(BigDecimal::from(mm_num.clone()))?, + wrong_variant => return Err(SiaValidateFeeArgsError::DexFeeVariant(wrong_variant.clone())), + }; + + // Check the Uuid provided is valid v4 + let uuid = Uuid::from_slice(args.uuid)?; + + match uuid.get_version_num() { + 4 => (), + version => return Err(SiaValidateFeeArgsError::UuidVersion(version)), + } + + Ok(SiaValidateFeeArgs { + fee_tx, + taker_public_key: expected_sender_public_key, + dex_fee_amount, + min_block_number: args.min_block_number, + uuid, + }) + } +} + +/// Sia typed equivalent of coins::WaitForHTLCTxSpendArgs +struct SiaWaitForHTLCTxSpendArgs { + pub tx: SiaTransaction, + pub wait_until: u64, + pub check_every: f64, +} + +impl TryFrom> for SiaWaitForHTLCTxSpendArgs { + type Error = SiaWaitForHTLCTxSpendArgsError; + + fn try_from(args: WaitForHTLCTxSpendArgs<'_>) -> Result { + // Convert tx_bytes to an owned type to prevent lifetime issues + let tx = SiaTransaction::try_from(args.tx_bytes.to_owned())?; + + // verify secret_hash is valid, but we don't need it otherwise + let secret_hash_slice: &[u8] = args.secret_hash; + let _secret_hash = Hash256::try_from(secret_hash_slice)?; + + Ok(SiaWaitForHTLCTxSpendArgs { + tx, + wait_until: args.wait_until, + check_every: args.check_every, + }) + } +} + +/// Sia typed equivalent of coins::CheckIfMyPaymentSentArgs +/// Does not include irrelevant fields swap_contract_address, swap_unique_data or payment_instructions +struct SiaCheckIfMyPaymentSentArgs { + time_lock: u64, + /// The PublicKey that appears in the HTLC SpendPolicy success branch + /// aka "other_pub" in coins::CheckIfMyPaymentSentArgs + success_public_key: PublicKey, + secret_hash: Hash256, + #[expect(dead_code)] + search_from_block: u64, + #[expect(dead_code)] + amount: Currency, +} + +impl TryFrom> for SiaCheckIfMyPaymentSentArgs { + type Error = SiaCheckIfMyPaymentSentArgsError; + + fn try_from(args: CheckIfMyPaymentSentArgs<'_>) -> Result { + let time_lock = args.time_lock; + + // TODO Alright - pubkey padding, see SiaCoin::derive_htlc_pubkey + if args.other_pub.len() != 33 { + return Err(SiaCheckIfMyPaymentSentArgsError::InvalidOtherPublicKeyLength( + args.other_pub.to_vec(), + )); + } + let success_public_key = PublicKey::from_bytes(&args.other_pub[..32])?; + let secret_hash = Hash256::try_from(args.secret_hash)?; + let search_from_block = args.search_from_block; + let amount = siacoin_to_hastings(args.amount.clone())?; + + Ok(SiaCheckIfMyPaymentSentArgs { + time_lock, + success_public_key, + secret_hash, + search_from_block, + amount, + }) } } #[async_trait] impl SwapOps for SiaCoin { - async fn send_taker_fee(&self, _dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionResult { - unimplemented!() + /* TODO Alright - refactor SwapOps to use associated types for error handling + TransactionErr is a very suboptimal structure for error handling, so we route to + new_send_taker_fee to allow for cleaner code patterns. The error is then converted to a + TransactionErr::Plain(String) for compatibility with the SwapOps trait + This may lose verbosity such as the full error chain/trace. */ + async fn send_taker_fee(&self, dex_fee: DexFee, uuid: &[u8], expire_at: u64) -> TransactionResult { + self.new_send_taker_fee(dex_fee, uuid, expire_at) + .await + .map_err(|e| e.to_string().into()) } - async fn send_maker_payment(&self, _maker_payment_args: SendPaymentArgs<'_>) -> TransactionResult { - unimplemented!() + async fn send_maker_payment(&self, maker_payment_args: SendPaymentArgs<'_>) -> TransactionResult { + self.new_send_maker_payment(maker_payment_args) + .await + .map_err(|e| e.to_string().into()) } - async fn send_taker_payment(&self, _taker_payment_args: SendPaymentArgs<'_>) -> TransactionResult { - unimplemented!() + async fn send_taker_payment(&self, taker_payment_args: SendPaymentArgs<'_>) -> TransactionResult { + self.new_send_taker_payment(taker_payment_args) + .await + .map_err(|e| e.to_string().into()) } async fn send_maker_spends_taker_payment( &self, - _maker_spends_payment_args: SpendPaymentArgs<'_>, + maker_spends_payment_args: SpendPaymentArgs<'_>, ) -> TransactionResult { - unimplemented!() + self.new_send_maker_spends_taker_payment(maker_spends_payment_args) + .await + .map_err(|e| e.to_string().into()) } async fn send_taker_spends_maker_payment( &self, - _taker_spends_payment_args: SpendPaymentArgs<'_>, + taker_spends_payment_args: SpendPaymentArgs<'_>, ) -> TransactionResult { - unimplemented!() + self.new_send_taker_spends_maker_payment(taker_spends_payment_args) + .await + .map_err(|e| e.to_string().into()) } - async fn send_taker_refunds_payment( - &self, - _taker_refunds_payment_args: RefundPaymentArgs<'_>, - ) -> TransactionResult { - unimplemented!() + async fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { + self.send_refund_htlc(taker_refunds_payment_args) + .await + .map_err(|e| SendRefundHltcMakerOrTakerError::Taker(e).to_string().into()) } - async fn send_maker_refunds_payment( - &self, - _maker_refunds_payment_args: RefundPaymentArgs<'_>, - ) -> TransactionResult { - unimplemented!() + async fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { + self.send_refund_htlc(maker_refunds_payment_args) + .await + .map_err(|e| SendRefundHltcMakerOrTakerError::Maker(e).to_string().into()) } - async fn validate_fee(&self, _validate_fee_args: ValidateFeeArgs<'_>) -> ValidatePaymentResult<()> { - unimplemented!() + async fn validate_fee(&self, validate_fee_args: ValidateFeeArgs<'_>) -> ValidatePaymentResult<()> { + self.new_validate_fee(validate_fee_args) + .await + .map_err(|e| MmError::new(ValidatePaymentError::InternalError(e.to_string()))) } - async fn validate_maker_payment(&self, _input: ValidatePaymentInput) -> ValidatePaymentResult<()> { - unimplemented!() + async fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { + self.sia_validate_maker_payment(input) + .await + .map_err(|e| MmError::new(ValidatePaymentError::InternalError(e.to_string()))) } - async fn validate_taker_payment(&self, _input: ValidatePaymentInput) -> ValidatePaymentResult<()> { - unimplemented!() + async fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { + self.sia_validate_taker_payment(input) + .await + .map_err(|e| MmError::new(ValidatePaymentError::InternalError(e.to_string()))) } + // return Ok(Some(tx)) if a transaction is found + // return Ok(None) if no transaction is found async fn check_if_my_payment_sent( &self, - _if_my_payment_sent_args: CheckIfMyPaymentSentArgs<'_>, + if_my_payment_sent_args: CheckIfMyPaymentSentArgs<'_>, ) -> Result, String> { - unimplemented!() + self.new_check_if_my_payment_sent(if_my_payment_sent_args) + .await + .map_err(|e| e.to_string()) } async fn search_for_swap_tx_spend_my( @@ -583,11 +1850,12 @@ impl SwapOps for SiaCoin { async fn extract_secret( &self, - _secret_hash: &[u8], - _spend_tx: &[u8], - _watcher_reward: bool, + secret_hash: &[u8], + spend_tx: &[u8], + watcher_reward: bool, ) -> Result<[u8; 32], String> { - unimplemented!() + self.sia_extract_secret(secret_hash, spend_tx, watcher_reward) + .map_err(|e| e.to_string()) } fn negotiate_swap_contract_addr( @@ -597,25 +1865,347 @@ impl SwapOps for SiaCoin { Ok(None) } + // Todo: This is only used for watchers so it's ok to use a default implementation as watchers are not supported for SIA yet fn derive_htlc_key_pair(&self, _swap_unique_data: &[u8]) -> KeyPair { - unimplemented!() + KeyPair::default() } + /// Return the iguana ed25519 public key + /// This is the public key that will be used inside the HTLC SpendPolicy + // TODO Alright - MakerSwapData is badly designed and assumes this is a 33 byte array aka H264 + // we pad it then drop the last byte when we use it for now fn derive_htlc_pubkey(&self, _swap_unique_data: &[u8]) -> [u8; 33] { - unimplemented!() + let my_keypair = self + .my_keypair() + .expect("SiaCoin::derive_htlc_pubkey: failed to get my_keypair"); + + let mut pubkey_bytes_padded = [0u8; 33]; + let pubkey_bytes = my_keypair.public().to_bytes(); + pubkey_bytes_padded[..32].copy_from_slice(&pubkey_bytes); + pubkey_bytes_padded + } + + /// Determines "Whether the refund transaction can be sent now" + /// /api/consensus/tipstate provides 11 timestamps, take the median + /// medianTimestamp = prevTimestamps[5] + /// SpendPolicy::After(time) evaluates to true when `time > medianTimestamp` + async fn can_refund_htlc(&self, locktime: u64) -> Result { + self.sia_can_refund_htlc(locktime).await.map_err(|e| e.to_string()) + } + + /// Validate the PublicKey the other party provided + /// The other party generates this PublicKey via SwapOps::derive_htlc_pubkey + fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { + // TODO Alright - pubkey padding, see SiaCoin::derive_htlc_pubkey + if raw_pubkey.len() != 33 { + return Err(ValidateOtherPubKeyErr::InvalidPubKey(format!( + "SiaCoin::validate_other_pubkey: invalid raw_pubkey, expected 33 bytes found: {:?}", + raw_pubkey.to_vec() + )) + .into()); + } + let _public_key = PublicKey::from_bytes(&raw_pubkey[..32]).map_err(|e| { + ValidateOtherPubKeyErr::InvalidPubKey(format!( + "SiaCoin::validate_other_pubkey: validate pubkey:{:?} failed: {}", + raw_pubkey, e + )) + })?; + Ok(()) } +} - async fn can_refund_htlc(&self, _locktime: u64) -> Result { - unimplemented!() +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, From, Into)] +#[serde(transparent)] +pub struct SiaTransaction(pub V2Transaction); + +impl fmt::Display for SiaTransaction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match serde_json::to_string(self) { + Ok(json) => write!(f, "{}", json), + Err(err) => write!(f, "Failed to serialize SiaTransaction:{:?} to JSON: {}", self, err), + } } +} - fn validate_other_pubkey(&self, _raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { - unimplemented!() +impl SiaTransaction { + pub fn txid(&self) -> Hash256 { + self.0.txid() } } -#[async_trait] -impl WatcherOps for SiaCoin {} +impl TryFrom for Vec { + type Error = SiaTransactionError; + + fn try_from(tx: SiaTransaction) -> Result { + serde_json::ser::to_vec(&tx).map_err(SiaTransactionError::ToVec) + } +} + +impl TryFrom<&[u8]> for SiaTransaction { + type Error = SiaTransactionError; + + fn try_from(tx_slice: &[u8]) -> Result { + serde_json::de::from_slice(tx_slice).map_err(SiaTransactionError::FromVec) + } +} + +impl TryFrom> for SiaTransaction { + type Error = SiaTransactionError; + + fn try_from(tx: Vec) -> Result { + serde_json::de::from_slice(&tx).map_err(SiaTransactionError::FromVec) + } +} + +impl Transaction for SiaTransaction { + // serde should always be succesful but write an empty vec just in case. + fn tx_hex(&self) -> Vec { + serde_json::ser::to_vec(self).unwrap_or_default() + } + + fn tx_hash_as_bytes(&self) -> BytesJson { + BytesJson(self.txid().0.to_vec()) + } +} + +/// Represents the different types of transactions that can be sent to a wallet. +/// This enum is generally only useful for displaying wallet history. +/// We do not support any operations for any type other than V2Transaction, but we want the ability +/// to display other event types within the wallet history. +/// Use SiaTransaction type instead. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(untagged)] +pub enum SiaTransactionTypes { + V1Transaction(V1Transaction), + V2Transaction(V2Transaction), + EventPayout(EventPayout), +} + +impl SiaCoin { + pub async fn request_events_history(&self) -> Result, MmError> { + let my_address = match &*self.priv_key_policy { + PrivKeyPolicy::Iguana(key_pair) => key_pair.public().address(), + _ => { + return MmError::err(ERRL!("Unexpected derivation method. Expected single address.")); + }, + }; + + let address_events = self + .client + .get_address_events(my_address) + .await + .map_err(|e| e.to_string())?; + + Ok(address_events) + } + + // TODO this was written prior to Currency arithmetic traits being added; refactor to use those + fn tx_details_from_event(&self, event: &Event) -> Result> { + match &event.data { + EventDataWrapper::V2Transaction(tx) => { + let txid = tx.txid().to_string(); + let internal_id = hex::decode(&txid).map_to_mm(|e| e.to_string())?.into(); + + let from: Vec = tx + .siacoin_inputs + .iter() + .map(|input| input.parent.siacoin_output.address.to_string()) + .collect(); + + let to: Vec = tx + .siacoin_outputs + .iter() + .map(|output| output.address.to_string()) + .collect(); + + let total_input: u128 = tx + .siacoin_inputs + .iter() + .map(|input| *input.parent.siacoin_output.value) + .sum(); + + let total_output: u128 = tx.siacoin_outputs.iter().map(|output| *output.value).sum(); + + let fee = total_input - total_output; + + let my_address = self.my_address().mm_err(|e| e.to_string())?; + + let spent_by_me: u128 = tx + .siacoin_inputs + .iter() + .filter(|input| input.parent.siacoin_output.address.to_string() == my_address) + .map(|input| *input.parent.siacoin_output.value) + .sum(); + + let received_by_me: u128 = tx + .siacoin_outputs + .iter() + .filter(|output| output.address.to_string() == my_address) + .map(|output| *output.value) + .sum(); + + let my_balance_change = hastings_to_siacoin(received_by_me.into()) - hastings_to_siacoin(spent_by_me.into()); + + Ok(TransactionDetails { + tx: TransactionData::Sia { + tx_json: SiaTransactionTypes::V2Transaction(tx.clone()), + tx_hash: txid, + }, + from, + to, + total_amount: hastings_to_siacoin(total_input.into()), + spent_by_me: hastings_to_siacoin(spent_by_me.into()), + received_by_me: hastings_to_siacoin(received_by_me.into()), + my_balance_change, + block_height: event.index.height, + timestamp: event.timestamp.timestamp() as u64, + fee_details: Some( + SiaFeeDetails { + coin: self.ticker().to_string(), + policy: SiaFeePolicy::Unknown, + total_amount: hastings_to_siacoin(fee.into()), + } + .into(), + ), + coin: self.ticker().to_string(), + internal_id, + kmd_rewards: None, + transaction_type: TransactionType::SiaV2Transaction, + memo: None, + }) + }, + EventDataWrapper::V1Transaction(tx) => { + let txid = tx.transaction.txid().to_string(); + let internal_id = hex::decode(&txid).map_to_mm(|e| e.to_string())?.into(); + + let from: Vec = tx + .spent_siacoin_elements + .iter() + .map(|element| element.siacoin_output.address.to_string()) + .collect(); + + let to: Vec = tx + .transaction + .siacoin_outputs + .iter() + .map(|output| output.address.to_string()) + .collect(); + + let total_input: u128 = tx + .spent_siacoin_elements + .iter() + .map(|element| *element.siacoin_output.value) + .sum(); + + let total_output: u128 = tx.transaction.siacoin_outputs.iter().map(|output| *output.value).sum(); + + // This accounts for v1 coinbase transactions where this is expected to underflow + let fee = match total_input.checked_sub(total_output) { + Some(value) => value, + None => { + // this should be a rare case, but logging will help if we somehow hit it unexpectedly + debug!( + "SiaCoin::tx_details_from_event: fee underflow: total_input < total_output for event: {:?}", tx + ); + 0 + } + }; + + let my_address = self.my_address().mm_err(|e| e.to_string())?; + + let spent_by_me: u128 = tx + .spent_siacoin_elements + .iter() + .filter(|element| element.siacoin_output.address.to_string() == my_address) + .map(|element| *element.siacoin_output.value) + .sum(); + + let received_by_me: u128 = tx + .transaction + .siacoin_outputs + .iter() + .filter(|output| output.address.to_string() == my_address) + .map(|output| *output.value) + .sum(); + + let my_balance_change = hastings_to_siacoin(received_by_me.into()) - hastings_to_siacoin(spent_by_me.into()); + + Ok(TransactionDetails { + tx: TransactionData::Sia { + tx_json: SiaTransactionTypes::V1Transaction(tx.transaction.clone()), + tx_hash: txid, + }, + from, + to, + total_amount: hastings_to_siacoin(total_input.into()), + spent_by_me: hastings_to_siacoin(spent_by_me.into()), + received_by_me: hastings_to_siacoin(received_by_me.into()), + my_balance_change, + block_height: event.index.height, + timestamp: event.timestamp.timestamp() as u64, + fee_details: Some( + SiaFeeDetails { + coin: self.ticker().to_string(), + policy: SiaFeePolicy::Unknown, + total_amount: hastings_to_siacoin(fee.into()), + } + .into(), + ), + coin: self.ticker().to_string(), + internal_id, + kmd_rewards: None, + transaction_type: TransactionType::SiaV1Transaction, + memo: None, + }) + }, + EventDataWrapper::MinerPayout(event_payout) | EventDataWrapper::FoundationPayout(event_payout) => { + let txid = event_payout.siacoin_element.id.to_string(); + let internal_id = hex::decode(&txid).map_to_mm(|e| e.to_string())?.into(); + + let from: Vec = vec![]; + + let to: Vec = vec![event_payout.siacoin_element.siacoin_output.address.to_string()]; + + let total_output: u128 = event_payout.siacoin_element.siacoin_output.value.0; + + let my_address = self.my_address().mm_err(|e| e.to_string())?; + + let received_by_me: u128 = + if event_payout.siacoin_element.siacoin_output.address.to_string() == my_address { + total_output + } else { + 0 + }; + + let my_balance_change = hastings_to_siacoin(received_by_me.into()); + + Ok(TransactionDetails { + tx: TransactionData::Sia { + tx_json: SiaTransactionTypes::EventPayout(event_payout.clone()), + tx_hash: txid, + }, + from, + to, + total_amount: hastings_to_siacoin(total_output.into()), + spent_by_me: BigDecimal::from(0), + received_by_me: hastings_to_siacoin(received_by_me.into()), + my_balance_change, + block_height: event.index.height, + timestamp: event.timestamp.timestamp() as u64, + fee_details: None, + coin: self.ticker().to_string(), + internal_id, + kmd_rewards: None, + transaction_type: TransactionType::SiaMinerPayout, + memo: None, + }) + }, + EventDataWrapper::ClaimPayout(_) // TODO this can be moved to the above case with Miner and Foundation payouts + | EventDataWrapper::V2FileContractResolution(_) + | EventDataWrapper::EventV1ContractResolution(_) => MmError::err(ERRL!("Unsupported event type")), + } + } +} #[cfg(test)] mod tests { @@ -623,22 +2213,191 @@ mod tests { use mm2_number::BigDecimal; use std::str::FromStr; + fn valid_transaction() -> SiaTransaction { + let j = json!( + {"siacoinInputs":[{"parent":{"id":"0f088eddda5320f8453a55349063abe43ba5b282631d5d2b9e684548f083055a","stateElement":{"leafIndex":3,"merkleProof":["ff9ce7f558df52b35d40fda59a8ab6d5ffb3dfab029992d02d1e929e0a36b6eb","b37a5387883748f73c1475ca85c8f3200eef09126c44824d0f44574109dabedc","0f1d4ef5b1bf0e6eb45240e717a1d548326f8379878944c6536fc73989cf2e7a","f85d8f6578bc2db41e8a206a060a10386c18505b533f64a5ceff574d602b57bd","c21c0b980cd4184996558b49e8b901c70cadb17fd27c62f472a2707f0eb6b092","482e9402c0c5e43d599b9683ba25224f74499f89e9bc33249794ff5e9f55e337","a29aabab81d0cf20e1bd33bb3e1138f1628a1337eb0430e4de47cf695eb25897","aeb60668a7e0ee0232f81642626104a1d4eb9ce3d0d9cf81196de48112e3ea41"]},"siacoinOutput":{"value":"299999000000000000000000000000","address":"c34caa97740668de2bbdb7174572ed64c861342bf27e80313cbfa02e9251f52e30aad3892533"},"maturityHeight":11},"satisfiedPolicy":{"policy":{"type":"pk","policy":"ed25519:a729be53dae7b0ed812f2a123ce93556014bbad8516ba6b1b496a112b46bbd97"},"signatures":["160e79ac52e0eaab5e92bd1675604a94b56ec58fdd0be3f3a842a4ece07d794f7ee1e8cc8f29b596bf71b2dc594df53347b9a4bcbec46fe09244ce6d3f6a6708"]}}],"siacoinOutputs":[{"value":"50000000000000000000000","address":"71731d7efe821794742c72a8376f56355b3c8a1984b861ccd42eed77a779a26626ea26ced3e2"},{"value":"299998949990000000000000000000","address":"c34caa97740668de2bbdb7174572ed64c861342bf27e80313cbfa02e9251f52e30aad3892533"}],"minerFee":"10000000000000000000"} + ); + let tx = serde_json::from_value::(j).unwrap(); + SiaTransaction(tx) + } + #[test] - fn test_siacoin_from_hastings() { + fn test_siacoin_from_hastings_u128_max() { let hastings = u128::MAX; - let siacoin = siacoin_from_hastings(hastings); + let siacoin = hastings_to_siacoin(hastings.into()); assert_eq!( siacoin, BigDecimal::from_str("340282366920938.463463374607431768211455").unwrap() ); + } - let hastings = 0; - let siacoin = siacoin_from_hastings(hastings); - assert_eq!(siacoin, BigDecimal::from_str("0").unwrap()); - + #[test] + fn test_siacoin_from_hastings_total_supply() { // Total supply of Siacoin - let hastings = 57769875000000000000000000000000000; - let siacoin = siacoin_from_hastings(hastings); + let hastings = 57769875000000000000000000000000000u128; + let siacoin = hastings_to_siacoin(hastings.into()); assert_eq!(siacoin, BigDecimal::from_str("57769875000").unwrap()); } + + #[test] + fn test_siacoin_to_hastings_supply() { + // Total supply of Siacoin + let siacoin = BigDecimal::from_str("57769875000").unwrap(); + let hastings = siacoin_to_hastings(siacoin).unwrap(); + assert_eq!(hastings, Currency(57769875000000000000000000000000000)); + } + + #[test] + fn test_sia_transaction_serde_roundtrip() { + let tx = valid_transaction(); + + let vec = serde_json::ser::to_vec(&tx).unwrap(); + let tx2: SiaTransaction = serde_json::from_slice(&vec).unwrap(); + + assert_eq!(tx, tx2); + } + + /// Test the .expect()s used during lazy_static initialization of FEE_PUBLIC_KEY + #[test] + fn test_sia_fee_pubkey_init() { + let pubkey_bytes: Vec = hex::decode(DEX_FEE_PUBKEY_ED25519).unwrap(); + let pubkey = PublicKey::from_bytes(&FEE_PUBLIC_KEY_BYTES).unwrap(); + assert_eq!(pubkey_bytes, *FEE_PUBLIC_KEY_BYTES); + assert_eq!(pubkey, *FEE_PUBLIC_KEY); + } + + #[test] + fn test_siacoin_from_hastings_coin() { + let coin = hastings_to_siacoin(Currency::COIN); + assert_eq!(coin, BigDecimal::from(1)); + } + + #[test] + fn test_siacoin_from_hastings_zero() { + let coin = hastings_to_siacoin(Currency::ZERO); + assert_eq!(coin, BigDecimal::from(0)); + } + + #[test] + fn test_siacoin_to_hastings_coin() { + let coin = BigDecimal::from(1); + let hastings = siacoin_to_hastings(coin).unwrap(); + assert_eq!(hastings, Currency::COIN); + } + + #[test] + fn test_siacoin_to_hastings_zero() { + let coin = BigDecimal::from(0); + let hastings = siacoin_to_hastings(coin).unwrap(); + assert_eq!(hastings, Currency::ZERO); + } + + #[test] + fn test_siacoin_to_hastings_one() { + let coin = serde_json::from_str::("0.000000000000000000000001").unwrap(); + println!("coin {:?}", coin); + let hastings = siacoin_to_hastings(coin).unwrap(); + assert_eq!(hastings, Currency(1)); + } +} + +#[cfg(all(test, target_arch = "wasm32"))] +mod wasm_tests { + use super::*; + use common::log::info; + use common::log::wasm_log::register_wasm_log; + use wasm_bindgen::prelude::*; + use wasm_bindgen_test::*; + + use url::Url; + + wasm_bindgen_test_configure!(run_in_browser); + + async fn init_client() -> SiaClientType { + let conf = SiaClientConf { + server_url: Url::parse("https://api.siascan.com/wallet/api").unwrap(), + headers: HashMap::new(), + }; + SiaClientType::new(conf).await.unwrap() + } + + #[wasm_bindgen_test] + async fn test_endpoint_txpool_broadcast() { + register_wasm_log(); + + use sia_rust::transaction::V2Transaction; + + let client = init_client().await; + + let tx = serde_json::from_str::( + r#" + { + "siacoinInputs": [ + { + "parent": { + "id": "h:27248ab562cbbee260e07ccae87c74aae71c9358d7f91eee25837e2011ce36d3", + "leafIndex": 21867, + "merkleProof": [ + "h:ac2fdcbed40f103e54b0b1a37c20a865f6f1f765950bc6ac358ff3a0e769da50", + "h:b25570eb5c106619d4eef5ad62482023df7a1c7461e9559248cb82659ebab069", + "h:baa78ec23a169d4e9d7f801e5cf25926bf8c29e939e0e94ba065b43941eb0af8", + "h:239857343f2997462bed6c253806cf578d252dbbfd5b662c203e5f75d897886d", + "h:ad727ef2112dc738a72644703177f730c634a0a00e0b405bd240b0da6cdfbc1c", + "h:4cfe0579eabafa25e98d83c3b5d07ae3835ce3ea176072064ea2b3be689e99aa", + "h:736af73aa1338f3bc28d1d8d3cf4f4d0393f15c3b005670f762709b6231951fc" + ], + "siacoinOutput": { + "value": "772999980000000000000000000", + "address": "addr:1599ea80d9af168ce823e58448fad305eac2faf260f7f0b56481c5ef18f0961057bf17030fb3" + }, + "maturityHeight": 0 + }, + "satisfiedPolicy": { + "policy": { + "type": "pk", + "policy": "ed25519:968e286ef5df3954b7189c53a0b4b3d827664357ebc85d590299b199af46abad" + }, + "signatures": [ + "sig:7a2c332fef3958a0486ef5e55b70d2a68514ff46d9307a85c3c0e40b76a19eebf4371ab3dd38a668cefe94dbedff2c50cc67856fbf42dce2194b380e536c1500" + ] + } + } + ], + "siacoinOutputs": [ + { + "value": "2000000000000000000000000", + "address": "addr:1d9a926b1e14b54242375c7899a60de883c8cad0a45a49a7ca2fdb6eb52f0f01dfe678918204" + }, + { + "value": "770999970000000000000000000", + "address": "addr:1599ea80d9af168ce823e58448fad305eac2faf260f7f0b56481c5ef18f0961057bf17030fb3" + } + ], + "minerFee": "10000000000000000000" + } + "#).unwrap(); + + let request = TxpoolBroadcastRequest { + transactions: vec![], + v2transactions: vec![tx], + }; + let resp = client.dispatcher(request).await.unwrap(); + } + + #[wasm_bindgen_test] + async fn test_helper_address_balance() { + register_wasm_log(); + use sia_rust::http::endpoints::AddressBalanceRequest; + use sia_rust::types::Address; + + let client = init_client().await; + + client + .address_balance( + Address::from_str("addr:1599ea80d9af168ce823e58448fad305eac2faf260f7f0b56481c5ef18f0961057bf17030fb3") + .unwrap(), + ) + .await + .unwrap(); + } } diff --git a/mm2src/coins/siacoin/error.rs b/mm2src/coins/siacoin/error.rs new file mode 100644 index 0000000000..defb8f3d2d --- /dev/null +++ b/mm2src/coins/siacoin/error.rs @@ -0,0 +1,375 @@ +use crate::siacoin::client_error::{ + BroadcastTransactionError, ClientError, CurrentHeightError, FindWhereUtxoSpentError, GetMedianTimestampError, + GetUnconfirmedTransactionError, UtxoFromTxidError, +}; +use crate::siacoin::{ + Address, Currency, Event, EventDataWrapper, Hash256, Hash256Error, KeypairError, PreimageError, PublicKeyError, + SiaTransaction, SiacoinOutput, TransactionId, V2TransactionBuilderError, +}; +use crate::{DexFee, TransactionEnum}; +use common::executor::AbortedError; +use crypto::privkey::PrivKeyError; +use mm2_number::BigDecimal; +use thiserror::Error; +use uuid::Uuid; + +#[derive(Debug, Error)] +pub enum SiacoinToHastingsError { + #[error("Sia Failed to convert BigDecimal:{0} to BigInt")] + BigDecimalToBigInt(BigDecimal), + #[error("Sia Failed to convert BigDecimal:{0} to u128")] + BigIntToU128(BigDecimal), +} + +#[derive(Debug, Error)] +pub enum SendTakerFeeError { + #[error("SiaCoin::new_send_taker_fee: failed to parse uuid from bytes {0}")] + ParseUuid(#[from] uuid::Error), + #[error("SiaCoin::new_send_taker_fee: Unexpected Uuid version {0}")] + UuidVersion(usize), + #[error("SiaCoin::new_send_taker_fee: failed to convert trade_fee_amount to Currency {0}")] + SiacoinToHastings(#[from] SiacoinToHastingsError), + #[error("SiaCoin::new_send_taker_fee: unexpected DexFee variant: {0:?}")] + DexFeeVariant(DexFee), + #[error("SiaCoin::new_send_taker_fee: failed to fetch my_keypair {0}")] + MyKeypair(#[from] SiaCoinMyKeypairError), + #[error("SiaCoin::new_send_taker_fee: failed to fund transaction {0}")] + FundTx(#[from] V2TransactionBuilderError), + #[error("SiaCoin::new_send_taker_fee: failed to broadcast taker_fee transaction {0}")] + BroadcastTx(#[from] BroadcastTransactionError), +} + +#[derive(Debug, Error)] +pub enum SendMakerPaymentError { + #[error("SiaCoin::new_send_maker_payment: invalid taker pubkey, expected 33 bytes found: {0:?}")] + InvalidTakerPublicKeyLength(Vec), + #[error("SiaCoin::new_send_maker_payment: invalid taker pubkey {0}")] + InvalidTakerPublicKey(#[from] PublicKeyError), + #[error("SiaCoin::new_send_maker_payment: failed to fetch my_keypair {0}")] + MyKeypair(#[from] SiaCoinMyKeypairError), + #[error("SiaCoin::new_send_maker_payment: failed to convert trade amount to Currency {0}")] + SiacoinToHastings(#[from] SiacoinToHastingsError), + #[error("SiaCoin::new_send_maker_payment: failed to fund transaction {0}")] + FundTx(#[from] V2TransactionBuilderError), + #[error("SiaCoin::new_send_maker_payment: failed to parse secret_hash {0}")] + ParseSecretHash(#[from] Hash256Error), + #[error("SiaCoin::new_send_maker_payment: failed to broadcast maker_payment transaction {0}")] + BroadcastTx(#[from] BroadcastTransactionError), +} + +#[derive(Debug, Error)] +pub enum SendTakerPaymentError { + #[error("SiaCoin::new_send_taker_payment: invalid maker pubkey, expected 33 bytes found: {0:?}")] + InvalidMakerPublicKeyLength(Vec), + #[error("SiaCoin::new_send_taker_payment: invalid maker pubkey {0}")] + InvalidMakerPublicKey(#[from] PublicKeyError), + #[error("SiaCoin::new_send_taker_payment: failed to fetch my_keypair {0}")] + MyKeypair(#[from] SiaCoinMyKeypairError), + #[error("SiaCoin::new_send_taker_payment: failed to convert trade amount to Currency {0}")] + SiacoinToHastings(#[from] SiacoinToHastingsError), + #[error("SiaCoin::new_send_taker_payment: failed to fund transaction {0}")] + FundTx(#[from] V2TransactionBuilderError), + #[error("SiaCoin::new_send_taker_payment: invalid secret_hash length {0}")] + SecretHashLength(#[from] Hash256Error), + #[error("SiaCoin::new_send_taker_payment: failed to broadcast taker_payment transaction {0}")] + BroadcastTx(#[from] BroadcastTransactionError), +} + +/// Wrapper around SendRefundHltcError to allow indicating Maker or Taker context within the error +#[derive(Debug, Error)] +pub enum SendRefundHltcMakerOrTakerError { + #[error("SiaCoin::send_refund_hltc: maker: {0}")] + Maker(SendRefundHltcError), + #[error("SiaCoin::send_refund_hltc: taker: {0}")] + Taker(SendRefundHltcError), +} + +#[derive(Debug, Error)] +pub enum SendRefundHltcError { + #[error("SiaCoin::send_refund_hltc: failed to fetch my_keypair: {0}")] + MyKeypair(#[from] SiaCoinMyKeypairError), + #[error("SiaCoin::send_refund_hltc: failed to parse RefundPaymentArgs: {0}")] + ParseArgs(#[from] SiaRefundPaymentArgsError), + #[error("SiaCoin::send_refund_hltc: failed to fetch SiacoinElement from txid {0}")] + // TODO: This is boxed since it's very large compared to the other variants. + // This shows up in many different enums where this embedded field is used as a variant. + // We should consider boxing the `EventVariant` within this field instead (requires changes in sia-rust). + UtxoFromTxid(#[from] Box), + #[error("SiaCoin::send_refund_hltc: failed to satisfy HTLC SpendPolicy {0}")] + SatisfyHtlc(#[from] V2TransactionBuilderError), + #[error("SiaCoin::send_refund_hltc: failed to broadcast transaction {0}")] + BroadcastTx(#[from] BroadcastTransactionError), +} + +#[derive(Debug, Error)] +pub enum ValidateFeeError { + #[error("SiaCoin::new_validate_fee: failed to parse ValidateFeeArgs {0}")] + ParseArgs(#[from] SiaValidateFeeArgsError), + #[error("SiaCoin::new_validate_fee: failed to fetch mempool: {0}")] + FetchMempool(#[from] GetUnconfirmedTransactionError), + #[error("SiaCoin::new_validate_fee: fee_tx:{0} not found on chain or in mempool")] + TxNotFound(TransactionId), + #[error("SiaCoin::new_validate_fee: unexpected event variant: {0:?}")] + EventVariant(Event), + #[error("SiaCoin::new_validate_fee: tx confirmed before min_block_number:{min_block_number} txid:{txid}")] + MininumConfirmedHeight { txid: TransactionId, min_block_number: u64 }, + #[error("SiaCoin::new_validate_fee: failed to fetch current_height: {0}")] + FetchHeight(#[from] CurrentHeightError), + #[error("SiaCoin::new_validate_fee: tx in mempool before height:{min_block_number} txid:{txid}")] + MininumMempoolHeight { txid: TransactionId, min_block_number: u64 }, + #[error("SiaCoin::new_validate_fee: all inputs do not originate from taker address txid:{0}")] + InputsOrigin(TransactionId), + #[error("SiaCoin::new_validate_fee: fee_tx:{txid} has {outputs_length} outputs, expected 1 or 2")] + VoutLength { txid: TransactionId, outputs_length: usize }, + #[error("SiaCoin::new_validate_fee: fee_tx:{txid} pays wrong address:{address}")] + InvalidFeeAddress { txid: TransactionId, address: Address }, + #[error("SiaCoin::new_validate_fee: fee_tx:{txid} pays wrong amount. expected:{expected} actual:{actual}")] + InvalidFeeAmount { + txid: TransactionId, + expected: Currency, + actual: Currency, + }, + #[error("SiaCoin::new_validate_fee: failed to parse uuid from arbitrary_bytes {0}")] + ParseUuid(#[from] uuid::Error), + #[error("SiaCoin::new_validate_fee: fee_tx:{txid} wrong uuid. expected:{expected} actual:{actual}")] + InvalidUuid { + txid: TransactionId, + expected: Uuid, + actual: Uuid, + }, +} + +// TODO Alright - nearly identical to MakerSpendsTakerPaymentError +// refactor similar to SendRefundHltcMakerOrTakerError +#[derive(Debug, Error)] +pub enum TakerSpendsMakerPaymentError { + #[error("SiaCoin::new_send_taker_spends_maker_payment: failed to fetch my_keypair {0}")] + MyKeypair(#[from] SiaCoinMyKeypairError), + #[error("SiaCoin::new_send_taker_spends_maker_payment: invalid maker pubkey, expected 33 bytes found: {0:?}")] + InvalidMakerPublicKeyLength(Vec), + #[error("SiaCoin::new_send_taker_spends_maker_payment: invalid maker pubkey {0}")] + InvalidMakerPublicKey(#[from] PublicKeyError), + #[error("SiaCoin::new_send_taker_spends_maker_paymentt: failed to parse taker_payment_tx {0}")] + ParseTx(#[from] SiaTransactionError), + #[error("SiaCoin::new_send_taker_spends_maker_payment: failed to parse secret {0}")] + ParseSecret(#[from] PreimageError), + #[error("SiaCoin::new_send_taker_spends_maker_payment: failed to parse secret_hash {0}")] + ParseSecretHash(#[from] Hash256Error), + #[error("SiaCoin::new_send_taker_spends_maker_payment: failed to fetch SiacoinElement from txid {0}")] + UtxoFromTxid(#[from] Box), + #[error("SiaCoin::new_send_taker_spends_maker_payment: failed to satisfy HTLC SpendPolicy {0}")] + SatisfyHtlc(#[from] V2TransactionBuilderError), + #[error("SiaCoin::new_send_taker_spends_maker_payment: failed to broadcast spend_maker_payment transaction {0}")] + BroadcastTx(#[from] BroadcastTransactionError), +} + +#[derive(Debug, Error)] +pub enum MakerSpendsTakerPaymentError { + #[error("SiaCoin::new_send_maker_spends_taker_payment: failed to fetch my_keypair {0}")] + MyKeypair(#[from] SiaCoinMyKeypairError), + #[error("SiaCoin::new_send_maker_spends_taker_payment: invalid taker pubkey, expected 33 bytes found: {0:?}")] + InvalidTakerPublicKeyLength(Vec), + #[error("SiaCoin::new_send_maker_spends_taker_payment: invalid taker pubkey {0}")] + InvalidTakerPublicKey(#[from] PublicKeyError), + #[error("SiaCoin::new_send_maker_spends_taker_payment: failed to parse taker_payment_tx {0}")] + ParseTx(#[from] SiaTransactionError), + #[error("SiaCoin::new_send_maker_spends_taker_payment: failed to parse secret {0}")] + ParseSecret(#[from] PreimageError), + #[error("SiaCoin::new_send_maker_spends_taker_payment: failed to parse secret_hash {0}")] + ParseSecretHash(#[from] Hash256Error), + #[error("SiaCoin::new_send_maker_spends_taker_payment: failed to fetch SiacoinElement from txid {0}")] + UtxoFromTxid(#[from] Box), + #[error("SiaCoin::new_send_maker_spends_taker_payment: failed to satisfy HTLC SpendPolicy {0}")] + SatisfyHtlc(#[from] V2TransactionBuilderError), + #[error("SiaCoin::new_send_maker_spends_taker_payment: failed to broadcast spend_taker_payment transaction {0}")] + BroadcastTx(#[from] BroadcastTransactionError), +} + +#[derive(Debug, Error)] +pub enum SiaRefundPaymentArgsError { + #[error("SiaRefundPaymentArgs::TryFrom: failed to parse payment_tx {0}")] + ParseTx(#[from] SiaTransactionError), + #[error("SiaRefundPaymentArgs::TryFrom: invalid other_pubkey, expected 33 bytes found: {0:?}")] + InvalidOtherPublicKeyLength(Vec), + #[error("SiaRefundPaymentArgs::TryFrom: failed to parse other_pubkey {0}")] + ParseOtherPublicKey(#[from] PublicKeyError), + #[error("SiaRefundPaymentArgs::TryFrom: failed to parse secret_hash {0}")] + ParseSecretHash(#[from] Hash256Error), + // SwapTxTypeVariant uses String Debug trait representation to avoid explicit lifetime annotations + // otherwise this should be SwapTxTypeVariant(SwapTxTypeWithSecretHash) and displayed via {0:?} + #[error("SiaRefundPaymentArgs::TryFrom: unexpected SwapTxTypeWithSecretHash variant {0}")] + SwapTxTypeVariant(String), +} + +#[derive(Debug, Error)] +#[allow(clippy::large_enum_variant)] +pub enum SiaValidateFeeArgsError { + #[error("SiaValidateFeeArgs::TryFrom: failed to parse uuid from bytes {0}")] + ParseUuid(#[from] uuid::Error), + #[error("SiaValidateFeeArgs::TryFrom: Unexpected Uuid version {0}")] + UuidVersion(usize), + #[error("SiaValidateFeeArgs::TryFrom: invalid taker pubkey, expected 33 bytes found: {0:?}")] + InvalidTakerPublicKeyLength(Vec), + #[error("SiaValidateFeeArgs::TryFrom: invalid taker pubkey {0}")] + InvalidTakerPublicKey(#[from] PublicKeyError), + #[error("SiaValidateFeeArgs::TryFrom: failed to convert trade_fee_amount to Currency {0}")] + SiacoinToHastings(#[from] SiacoinToHastingsError), + #[error("SiaValidateFeeArgs::TryFrom: unexpected DexFee variant {0:?}")] + DexFeeVariant(DexFee), + #[error("SiaValidateFeeArgs::TryFrom: unexpected TransactionEnum variant {0:?}")] + TxEnumVariant(TransactionEnum), +} + +#[derive(Debug, Error)] +pub enum SiaTransactionError { + #[error("Vec::TryFrom: failed to convert to Vec")] + ToVec(serde_json::Error), + #[error("SiaTransaction::TryFrom>: failed to convert from Vec")] + FromVec(serde_json::Error), +} + +#[derive(Debug, Error)] +pub enum SiaCoinBuilderError { + #[error("SiaCoinBuilder::build: failed to create abortable system: {0}")] + AbortableSystem(AbortedError), + #[error("SiaCoinBuilder::build: failed to initialize client {0}")] + Client(#[from] ClientError), +} + +// This is required because AbortedError doesn't impl Error +impl From for SiaCoinBuilderError { + fn from(e: AbortedError) -> Self { + SiaCoinBuilderError::AbortableSystem(e) + } +} + +#[derive(Debug, Error)] +pub enum SiaCoinNewError { + #[error("SiaCoin::new: failed to parse SiaCoinConf from JSON: {0}")] + InvalidConf(#[from] serde_json::Error), + #[error("SiaCoin::new: invalid private key: {0}")] + InvalidPrivateKey(#[from] KeypairError), + #[error("SiaCoin::new: invalid private key policy, must use iguana seed")] + UnsupportedPrivKeyPolicy, + #[error("SiaCoin::new: failed to build SiaCoin: {0}")] + Builder(#[from] SiaCoinBuilderError), + #[error("SiaCoin::new: failed to derive address from master extended key: {0}")] + DeriveExtendedKey(#[from] PrivKeyError), +} + +#[derive(Debug, Error)] +pub enum SiaCoinMyKeypairError { + #[error("SiaCoin::my_keypair: invalid private key policy, must use iguana seed")] + PrivKeyPolicy, +} + +#[derive(Debug, Error)] +pub enum SiaCheckIfMyPaymentSentArgsError { + #[error("SiaCheckIfMyPaymentSentArgs::TryFrom: invalid other_pub, expected 33 bytes found: {0:?}")] + InvalidOtherPublicKeyLength(Vec), + #[error("SiaCheckIfMyPaymentSentArgs::TryFrom: failed to parse other_pub {0}")] + ParseOtherPublicKey(#[from] PublicKeyError), + #[error("SiaCheckIfMyPaymentSentArgs::TryFrom: failed to parse secret_hash {0}")] + ParseSecretHash(#[from] Hash256Error), + #[error( + "SiaCheckIfMyPaymentSentArgs::TryFrom: failed to convert amount to Currency {0}" + )] + SiacoinToHastings(#[from] SiacoinToHastingsError), +} + +#[derive(Debug, Error)] +#[allow(clippy::large_enum_variant)] +pub enum SiaCheckIfMyPaymentSentError { + #[error("SiaCoin::new_check_if_my_payment_sent: failed to parse CheckIfMyPaymentSentArgs: {0}")] + ParseArgs(#[from] SiaCheckIfMyPaymentSentArgsError), + #[error("SiaCoin::new_check_if_my_payment_sent: invalid private key policy, must use iguana seed")] + MyKeypair(#[from] SiaCoinMyKeypairError), + #[error("SiaCoin::new_check_if_my_payment_sent: unexpected event variant: {0:?}")] + EventVariant(EventDataWrapper), +} + +#[derive(Debug, Error)] +#[allow(clippy::large_enum_variant)] +pub enum SiaCoinSiaExtractSecretError { + #[error("SiaCoin::sia_extract_secret: failed to parse spend_tx {0}")] + ParseTx(#[from] SiaTransactionError), + #[error("SiaCoin::sia_extract_secret: failed to parse secret_hash {0}")] + ParseSecretHash(#[from] Hash256Error), + #[error( + "SiaCoin::sia_extract_secret: failed to extract secret of secret_hash:{expected_hash} from spend_tx: {tx}" + )] + FailedToExtract { expected_hash: Hash256, tx: SiaTransaction }, +} + +#[derive(Debug, Error)] +pub enum SiaCoinSiaCanRefundHtlcError { + #[error("SiaCoin::sia_can_refund_htlc: failed to fetch median_timestamp: {0}")] + FetchTimestamp(#[from] GetMedianTimestampError), +} + +#[derive(Debug, Error)] +pub enum SiaWaitForHTLCTxSpendArgsError { + #[error("SiaWaitForHTLCTxSpendArgs::TryFrom: Failed to parse transaction: {0}")] + ParseTx(#[from] SiaTransactionError), + #[error("SiaWaitForHTLCTxSpendArgs::TryFrom: Failed to parse secret hash: {0}")] + ParseSecretHash(#[from] Hash256Error), +} + +#[derive(Debug, Error)] +pub enum SiaWaitForHTLCTxSpendError { + #[error("SiaCoin::sia_wait_for_htlc_tx_spend: Failed to parse arguments: {0}")] + ParseArgs(#[from] SiaWaitForHTLCTxSpendArgsError), + #[error("SiaCoin::sia_wait_for_htlc_tx_spend: timed out waiting for spend of txid:{txid} vout 0")] + Timeout { txid: TransactionId }, + #[error("SiaCoin::sia_wait_for_htlc_tx_spend: find_where_utxo_spent failed: {0}")] + FindWhereUtxoSpent(#[from] Box), +} + +#[derive(Debug, Error)] +pub enum SiaValidatePaymentInputError { + #[error("SiaValidatePaymentInput::TryFrom: Failed to parse payment_tx: {0}")] + ParseTx(#[from] SiaTransactionError), + #[error("SiaValidateFeeArgs::TryFrom: invalid other_pub, expected 33 bytes found: {0:?}")] + InvalidOtherPublicKeyLength(Vec), + #[error("SiaValidateFeeArgs::TryFrom: Failed to parse other_pub: {0}")] + ParseOtherPublicKey(#[from] PublicKeyError), + #[error("SiaValidateFeeArgs::TryFrom: Failed to parse secret_hash: {0}")] + ParseSecretHash(#[from] Hash256Error), + #[error("SiaValidateFeeArgs::TryFrom: failed to convert amount to Currency: {0}")] + SiacoinToHastings(#[from] SiacoinToHastingsError), +} + +#[derive(Debug, Error)] +#[allow(clippy::large_enum_variant)] +pub enum SiaValidateHtlcPaymentError { + #[error("SiaCoin::validate_htlc_payment: failed to parse ValidatePaymentInput: {0}")] + ParseArgs(#[from] SiaValidatePaymentInputError), + #[error("SiaCoin::validate_htlc_payment: failed to fetch my_keypair {0}")] + MyKeypair(#[from] SiaCoinMyKeypairError), + #[error("SiaCoin::validate_htlc_payment: unexpected event variant, expected V2Transaction, found: {0:?}")] + EventVariant(Event), + #[error("SiaCoin::validate_htlc_payment: txid:{txid} has {actual} inputs, expected at least:{expected}")] + InvalidOutputLength { + expected: u32, + actual: u32, + txid: TransactionId, + }, + #[error("SiaCoin::validate_htlc_payment: txid:{txid} has unexpected output:{actual:?}, expected:{expected:?}")] + InvalidOutput { + expected: SiacoinOutput, + actual: SiacoinOutput, + txid: TransactionId, + }, +} + +#[derive(Debug, Error)] +pub enum SiaValidateMakerPaymentError { + #[error("SiaCoin::sia_validate_maker_payment: validation failed: {0}")] + ValidatePayment(#[from] SiaValidateHtlcPaymentError), +} + +#[derive(Debug, Error)] +pub enum SiaValidateTakerPaymentError { + #[error("SiaCoin::sia_validate_taker_payment: validation failed: {0}")] + ValidatePayment(#[from] SiaValidateHtlcPaymentError), +} diff --git a/mm2src/coins/siacoin/sia_hd_wallet.rs b/mm2src/coins/siacoin/sia_hd_wallet.rs index 4c6a288ef5..c8c09fd967 100644 --- a/mm2src/coins/siacoin/sia_hd_wallet.rs +++ b/mm2src/coins/siacoin/sia_hd_wallet.rs @@ -1,44 +1,127 @@ -use crate::hd_wallet::{HDAccount, HDAddress, HDWallet}; -use bip32::{ExtendedPublicKey, PrivateKeyBytes, PublicKey as bip32PublicKey, PublicKeyBytes, Result as bip32Result}; -use sia_rust::types::Address; -use sia_rust::PublicKey; +use crate::hd_wallet::{ + DisplayAddress, ExtendedPublicKeyOps, HDAccount, HDAccountMut, HDAccountOps, HDAccountsMap, HDAccountsMut, + HDAccountsMutex, HDAddress, HDWallet, HDWalletOps, +}; +use crate::siacoin::{Address, PublicKey}; -pub struct SiaPublicKey(pub PublicKey); +use async_trait::async_trait; +use crypto::{Bip32Error, Bip44Chain, ChildNumber, HDPathToCoin}; +use ed25519_dalek_bip32::ExtendedSigningKey; +use std::str::FromStr; +use std::sync::Arc; -pub type SiaHDAddress = HDAddress; -pub type SiaHDAccount = HDAccount; -pub type SiaHDWallet = HDWallet; -pub type Ed25519ExtendedPublicKey = ExtendedPublicKey; +// TODO Alright - I began to do the HD wallet implementation, but the decision was made to simply +// use `m/44'/1991'/0'/0'/0` and use this key as PrivKeyBuildPolicy::IguanaPrivKey. This means users +// will not need to do any migration of their seed phrases when HD wallet support is added. +// It was simpler to use PrivKeyBuildPolicy::IguanaPrivKey because all the logic already assumes +// a single address per seed phrease. -impl bip32PublicKey for SiaPublicKey { - fn from_bytes(_bytes: PublicKeyBytes) -> bip32Result { +impl DisplayAddress for Address { + fn display_address(&self) -> String { + self.to_string() + } +} +pub type SiaHDAddress = HDAddress; + +pub type SiaHDAccount = HDAccount; + +// See the crypto::GlobalHDAccountCtx dev comment regarding security considerations! +// this will be left unfinished in the RC, but this was intended to be the type of abstraction +// mentioned in the dev comment. The hope was to integrate cleanly into the existing HD wallet traits. +// If this is pattern implemented, this type must be treated securely. +#[allow(dead_code)] +pub struct SiaFauxExtendedPublicKey(Arc); + +impl FromStr for SiaFauxExtendedPublicKey { + type Err = String; + + fn from_str(_: &str) -> Result { todo!() - //Ok(secp256k1_ffi::PublicKey::from_slice(&bytes)?) } +} - fn to_bytes(&self) -> PublicKeyBytes { +impl ExtendedPublicKeyOps for SiaFauxExtendedPublicKey { + fn derive_child(&self, _: ChildNumber) -> Result { todo!() - // self.serialize() } - fn derive_child(&self, _other: PrivateKeyBytes) -> bip32Result { + fn to_string(&self, _: bip32::Prefix) -> String { + todo!() + } +} + +#[allow(dead_code)] +pub struct SiaHdWallet(HDWallet); + +#[async_trait] +impl HDWalletOps for SiaHdWallet { + /// Any type that represents a Hierarchical Deterministic (HD) wallet account. + type HDAccount = SiaHDAccount; + + /// Returns the coin type associated with this HD Wallet. + /// + /// This method should be implemented to fetch the coin type as specified in the wallet's BIP44 derivation path. + /// For example, in the derivation path `m/44'/0'/0'/0`, the coin type would be the third level `0'` + /// (representing Bitcoin). + fn coin_type(&self) -> u32 { todo!() - // use secp256k1_ffi::{Secp256k1, VerifyOnly}; - // let engine = Secp256k1::::verification_only(); + } - // let mut child_key = *self; - // child_key - // .add_exp_assign(&engine, &other) - // .map_err(|_| Error::Crypto)?; + /// Returns the derivation path associated with this HD Wallet. This is the path used to derive the accounts. + fn derivation_path(&self) -> &HDPathToCoin { + todo!() + } - // Ok(child_key) + /// Fetches the gap limit associated with this HD Wallet. + /// Gap limit is the maximum number of consecutive unused addresses in an account + /// that should be checked before considering the wallet as having no more funds. + fn gap_limit(&self) -> u32 { + todo!() } -} -// coin type 1991 -// path component 0x800007c7 + /// Returns the limit on the number of accounts that can be added to the wallet. + fn account_limit(&self) -> u32 { + todo!() + } -#[test] -fn test_something() { - println!("This is a test"); + /// Returns the default BIP44 chain for receiver addresses. + fn default_receiver_chain(&self) -> Bip44Chain { + todo!() + } + + /// Returns a mutex that can be used to access the accounts. + fn get_accounts_mutex(&self) -> &HDAccountsMutex { + todo!() + } + + /// Fetches an account based on its ID. This method will return `None` if the account is not activated. + async fn get_account(&self, _account_id: u32) -> Option { + todo!() + } + + /// Similar to `get_account`, but provides a mutable reference. + async fn get_account_mut(&self, _account_id: u32) -> Option> { + todo!() + } + + /// Fetches all accounts in the wallet. + async fn get_accounts(&self) -> HDAccountsMap { + todo!() + } + + /// Similar to `get_accounts`, but provides a mutable reference to the accounts. + async fn get_accounts_mut(&self) -> HDAccountsMut<'_, Self::HDAccount> { + todo!() + } + + /// Attempts to remove an account only if it's the last in the set. + /// This method will return the removed account if successful or `None` otherwise. + async fn remove_account_if_last(&self, _account_id: u32) -> Option { + todo!() + } + + /// Returns an address that's currently enabled for single-address operations, such as swaps. + async fn get_enabled_address(&self) -> Option<::HDAddress> { + todo!() + } } diff --git a/mm2src/coins/siacoin/sia_withdraw.rs b/mm2src/coins/siacoin/sia_withdraw.rs new file mode 100644 index 0000000000..e81a01ca69 --- /dev/null +++ b/mm2src/coins/siacoin/sia_withdraw.rs @@ -0,0 +1,178 @@ +use crate::siacoin::{ + hastings_to_siacoin, siacoin_to_hastings, Address, ApiClientHelpers, Currency, SiaCoin, SiaFeeDetails, + SiaFeePolicy, SiaKeypair as Keypair, SiaTransactionTypes, SiacoinElement, SiacoinOutput, SpendPolicy, + V2TransactionBuilder, +}; +use crate::{ + MarketCoinOps, PrivKeyPolicy, TransactionData, TransactionDetails, TransactionType, WithdrawError, WithdrawRequest, + WithdrawResult, +}; +use common::now_sec; +use mm2_err_handle::mm_error::MmError; +use std::str::FromStr; + +pub struct SiaWithdrawBuilder<'a> { + coin: &'a SiaCoin, + req: WithdrawRequest, + from_address: Address, + key_pair: &'a Keypair, +} + +// TODO Alright this was written prior to Currency arithmetic traits being added and various +// V2TransactionBuilder helpers being added; refactor to use those +impl<'a> SiaWithdrawBuilder<'a> { + #[allow(clippy::result_large_err)] + pub fn new(coin: &'a SiaCoin, req: WithdrawRequest) -> Result> { + let (key_pair, from_address) = match &*coin.priv_key_policy { + PrivKeyPolicy::Iguana(key_pair) => (key_pair, key_pair.public().address()), + _ => { + return Err(WithdrawError::UnsupportedError( + "Only Iguana keypair is supported for Sia coin for now!".to_string(), + ) + .into()); + }, + }; + + Ok(SiaWithdrawBuilder { + coin, + req, + from_address, + key_pair, + }) + } + + #[allow(clippy::result_large_err)] + fn select_outputs( + &self, + mut unspent_outputs: Vec, + total_amount: u128, + ) -> Result, MmError> { + // Sort outputs from largest to smallest + unspent_outputs.sort_by(|a, b| b.siacoin_output.value.0.cmp(&a.siacoin_output.value.0)); + + let mut selected = Vec::new(); + let mut selected_amount = 0; + + // Select outputs until the total amount is reached + for output in unspent_outputs { + selected_amount += *output.siacoin_output.value; + selected.push(output); + + if selected_amount >= total_amount { + break; + } + } + + if selected_amount < total_amount { + return Err(MmError::new(WithdrawError::NotSufficientBalance { + coin: self.coin.ticker().to_string(), + available: hastings_to_siacoin(selected_amount.into()), + required: hastings_to_siacoin(total_amount.into()), + })); + } + + Ok(selected) + } + + pub async fn build(self) -> WithdrawResult { + // Todo: fee estimation based on transaction size + const TX_FEE_HASTINGS: u128 = 10_000_000_000_000_000_000; + let tx_fee = Currency(TX_FEE_HASTINGS); + + let to = Address::from_str(&self.req.to).map_err(|e| WithdrawError::InvalidAddress(e.to_string()))?; + + // Get unspent outputs + let unspent_outputs = self + .coin + .client + .get_unspent_outputs(&self.from_address, None, None, true) + .await + .map_err(|e| WithdrawError::Transport(e.to_string()))?; + + let basis = unspent_outputs.basis; + let outputs = unspent_outputs.outputs; + + // Select outputs to use as inputs and calculate the total amount to send (including fee) and the change amount + let (selected_outputs, tx_amount_hastings, change_amount, input_sum) = if self.req.max { + // spend everything minus fee + let input_sum: Currency = outputs.iter().map(|o| o.siacoin_output.value).sum(); + if input_sum <= tx_fee { + return Err(MmError::new(WithdrawError::NotSufficientBalance { + coin: self.coin.ticker().to_string(), + available: hastings_to_siacoin(input_sum), + required: hastings_to_siacoin(tx_fee), + })); + } + let tx_amount_hastings = input_sum - tx_fee; + (outputs, tx_amount_hastings, Currency::ZERO, input_sum) + } else { + // Calculate the total amount to send (including fee) + let tx_amount_hastings = siacoin_to_hastings(self.req.amount.clone()) + .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + let total_amount = tx_amount_hastings + tx_fee; + let selected_outputs = self.select_outputs(outputs, total_amount.into())?; + let input_sum: Currency = selected_outputs.iter().map(|o| o.siacoin_output.value).sum(); + let change_amount = input_sum - total_amount; + (selected_outputs, tx_amount_hastings, change_amount, input_sum) + }; + + // Construct transaction + let mut tx_builder = V2TransactionBuilder::new() + .update_basis(basis) + // Add output for recipient + .add_siacoin_output(SiacoinOutput { + value: tx_amount_hastings, + address: to.clone(), + }) + // Add miner fee + .miner_fee(Currency::from(TX_FEE_HASTINGS)); + + // Add inputs + for output in selected_outputs { + tx_builder = tx_builder.add_siacoin_input(output, SpendPolicy::PublicKey(self.key_pair.public())); + } + + // Add change output if necessary + if change_amount > Currency::ZERO { + tx_builder = tx_builder.add_siacoin_output(SiacoinOutput { + value: change_amount, + address: self.from_address.clone(), + }); + } + + // Sign the transaction + let signed_tx = tx_builder.sign_simple(vec![self.key_pair]).build(); + + let spent_by_me = hastings_to_siacoin(input_sum); + let fee_amount = hastings_to_siacoin(tx_fee); + let received_by_me = hastings_to_siacoin(change_amount); + + Ok(TransactionDetails { + tx: TransactionData::Sia { + tx_json: SiaTransactionTypes::V2Transaction(signed_tx.clone()), + tx_hash: signed_tx.txid().to_string(), + }, + from: vec![self.from_address.to_string()], + to: vec![self.req.to.clone()], + total_amount: spent_by_me.clone() - fee_amount.clone(), + spent_by_me: spent_by_me.clone(), + received_by_me: received_by_me.clone(), + my_balance_change: received_by_me - spent_by_me, + fee_details: Some( + SiaFeeDetails { + coin: self.coin.ticker().to_string(), + policy: SiaFeePolicy::Fixed, + total_amount: fee_amount, + } + .into(), + ), + block_height: 0, + coin: self.coin.ticker().to_string(), + internal_id: vec![].into(), + timestamp: now_sec(), + kmd_rewards: None, + transaction_type: TransactionType::SiaV2Transaction, + memo: None, + }) + } +} diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 0a788a6a2f..1bbc1489d0 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -3580,7 +3580,7 @@ impl MmCoin for TendermintCoin { ticker: &str, ) -> Result, MmError> { match lp_coinfind(ctx, ticker).await { - Ok(Some(MmCoinEnum::Tendermint(coin))) => Ok(Some(coin)), + Ok(Some(MmCoinEnum::TendermintVariant(coin))) => Ok(Some(coin)), Ok(Some(other)) => MmError::err(OrderCreationPreCheckError::InternalError { reason: format!( "Expected a Tendermint coin for '{}', but found '{}'.", diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 3e4e2edaa6..a1a3a55cd0 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -2922,7 +2922,7 @@ pub async fn get_taker_watcher_reward coin, + MmCoinEnum::EthCoinVariant(coin) => coin, _ => { return Err(WatcherRewardError::InvalidCoinType( "At least one coin must be Ethereum to use watcher rewards".to_string(), diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index 0cb179b756..f212c1ce9f 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -313,9 +313,6 @@ impl ZCoin { &self.z_fields.consensus_params } - /// Asynchronously checks the synchronization status and returns `true` if - /// the Sapling state has finished synchronizing, meaning that the block number is available. - /// Otherwise, it returns `false`. #[cfg(any(test, feature = "run-docker-tests"))] #[inline] pub async fn is_sapling_state_synced(&self) -> bool { diff --git a/mm2src/coins/z_coin/storage.rs b/mm2src/coins/z_coin/storage.rs index 6b146ec8cb..91b6441de6 100644 --- a/mm2src/coins/z_coin/storage.rs +++ b/mm2src/coins/z_coin/storage.rs @@ -18,8 +18,6 @@ pub(crate) use z_params::ZcashParamsWasmImpl; use mm2_err_handle::mm_error::MmResult; #[cfg(target_arch = "wasm32")] use walletdb::wasm::storage::DataConnStmtCacheWasm; -#[cfg(debug_assertions)] -use zcash_client_backend::data_api::error::Error; use zcash_client_backend::data_api::PrunedBlock; use zcash_client_backend::proto::compact_formats::CompactBlock; use zcash_client_backend::wallet::{AccountId, WalletTx}; diff --git a/mm2src/coins_activation/Cargo.toml b/mm2src/coins_activation/Cargo.toml index dc9b2641f9..86f8ab7a13 100644 --- a/mm2src/coins_activation/Cargo.toml +++ b/mm2src/coins_activation/Cargo.toml @@ -7,7 +7,6 @@ edition = "2018" doctest = false [features] -enable-sia = [] default = [] for-tests = [] diff --git a/mm2src/coins_activation/src/bch_with_tokens_activation.rs b/mm2src/coins_activation/src/bch_with_tokens_activation.rs index d4d4e42332..95af24e839 100644 --- a/mm2src/coins_activation/src/bch_with_tokens_activation.rs +++ b/mm2src/coins_activation/src/bch_with_tokens_activation.rs @@ -296,7 +296,7 @@ impl PlatformCoinWithTokensActivationOps for BchCoin { Self: Sized, { match coin { - MmCoinEnum::Bch(coin) => Some(coin), + MmCoinEnum::BchVariant(coin) => Some(coin), _ => None, } } diff --git a/mm2src/coins_activation/src/context.rs b/mm2src/coins_activation/src/context.rs index ed9aedcbdb..85f971fcbd 100644 --- a/mm2src/coins_activation/src/context.rs +++ b/mm2src/coins_activation/src/context.rs @@ -2,7 +2,6 @@ use crate::eth_with_token_activation::EthTaskManagerShared; use crate::init_erc20_token_activation::Erc20TokenTaskManagerShared; #[cfg(not(target_arch = "wasm32"))] use crate::lightning_activation::LightningTaskManagerShared; -#[cfg(feature = "enable-sia")] use crate::sia_coin_activation::SiaCoinTaskManagerShared; use crate::solana_with_assets::SolanaCoinTaskManagerShared; use crate::tendermint_with_assets_activation::TendermintCoinTaskManagerShared; @@ -16,7 +15,6 @@ pub struct CoinsActivationContext { pub(crate) init_utxo_standard_task_manager: UtxoStandardTaskManagerShared, pub(crate) init_bch_task_manager: BchTaskManagerShared, pub(crate) init_qtum_task_manager: QtumTaskManagerShared, - #[cfg(feature = "enable-sia")] pub(crate) init_sia_task_manager: SiaCoinTaskManagerShared, pub(crate) init_z_coin_task_manager: ZcoinTaskManagerShared, pub(crate) init_eth_task_manager: EthTaskManagerShared, @@ -32,7 +30,6 @@ impl CoinsActivationContext { pub fn from_ctx(ctx: &MmArc) -> Result, String> { from_ctx(&ctx.coins_activation_ctx, move || { Ok(CoinsActivationContext { - #[cfg(feature = "enable-sia")] init_sia_task_manager: RpcTaskManager::new_shared(ctx.event_stream_manager.clone()), init_utxo_standard_task_manager: RpcTaskManager::new_shared(ctx.event_stream_manager.clone()), init_bch_task_manager: RpcTaskManager::new_shared(ctx.event_stream_manager.clone()), diff --git a/mm2src/coins_activation/src/erc20_token_activation.rs b/mm2src/coins_activation/src/erc20_token_activation.rs index f7a2dd5a3b..25a8df5cd5 100644 --- a/mm2src/coins_activation/src/erc20_token_activation.rs +++ b/mm2src/coins_activation/src/erc20_token_activation.rs @@ -62,7 +62,7 @@ impl TryPlatformCoinFromMmCoinEnum for EthCoin { Self: Sized, { match coin { - MmCoinEnum::EthCoin(coin) => Some(coin), + MmCoinEnum::EthCoinVariant(coin) => Some(coin), _ => None, } } diff --git a/mm2src/coins_activation/src/eth_with_token_activation.rs b/mm2src/coins_activation/src/eth_with_token_activation.rs index e0eb93f1bb..0f40c09f71 100644 --- a/mm2src/coins_activation/src/eth_with_token_activation.rs +++ b/mm2src/coins_activation/src/eth_with_token_activation.rs @@ -337,7 +337,7 @@ impl PlatformCoinWithTokensActivationOps for EthCoin { None => return Ok(None), }; let nft_global = self.initialize_global_nft(url, proxy_auth).await.map_mm_err()?; - Ok(Some(MmCoinEnum::EthCoin(nft_global))) + Ok(Some(MmCoinEnum::EthCoinVariant(nft_global))) } fn try_from_mm_coin(coin: MmCoinEnum) -> Option @@ -345,7 +345,7 @@ impl PlatformCoinWithTokensActivationOps for EthCoin { Self: Sized, { match coin { - MmCoinEnum::EthCoin(coin) => Some(coin), + MmCoinEnum::EthCoinVariant(coin) => Some(coin), _ => None, } } @@ -370,7 +370,7 @@ impl PlatformCoinWithTokensActivationOps for EthCoin { .await .map_err(EthActivationV2Error::InternalError)?; - let nfts_map = if let Some(MmCoinEnum::EthCoin(nft_global)) = nft_global { + let nfts_map = if let Some(MmCoinEnum::EthCoinVariant(nft_global)) = nft_global { nft_global.nfts_infos.lock().await.clone() } else { Default::default() diff --git a/mm2src/coins_activation/src/lib.rs b/mm2src/coins_activation/src/lib.rs index e3fba13c17..6aa6fea108 100644 --- a/mm2src/coins_activation/src/lib.rs +++ b/mm2src/coins_activation/src/lib.rs @@ -9,7 +9,6 @@ mod l2; mod lightning_activation; mod platform_coin_with_tokens; mod prelude; -#[cfg(feature = "enable-sia")] mod sia_coin_activation; mod slp_token_activation; mod solana_token_activation; diff --git a/mm2src/coins_activation/src/lightning_activation.rs b/mm2src/coins_activation/src/lightning_activation.rs index cb87d08018..97c59bc9ff 100644 --- a/mm2src/coins_activation/src/lightning_activation.rs +++ b/mm2src/coins_activation/src/lightning_activation.rs @@ -75,7 +75,7 @@ impl TryPlatformCoinFromMmCoinEnum for UtxoStandardCoin { Self: Sized, { match coin { - MmCoinEnum::UtxoCoin(coin) => Some(coin), + MmCoinEnum::UtxoCoinVariant(coin) => Some(coin), _ => None, } } diff --git a/mm2src/coins_activation/src/prelude.rs b/mm2src/coins_activation/src/prelude.rs index 91e9cef58c..4771ca2dd3 100644 --- a/mm2src/coins_activation/src/prelude.rs +++ b/mm2src/coins_activation/src/prelude.rs @@ -1,5 +1,4 @@ -#[cfg(feature = "enable-sia")] -use coins::siacoin::SiaCoinActivationParams; +use coins::siacoin::SiaCoinActivationRequest; use coins::utxo::bch::BchActivationRequest; use coins::utxo::UtxoActivationParams; use coins::z_coin::ZcoinActivationParams; @@ -31,8 +30,7 @@ impl TxHistory for BchActivationRequest { } } -#[cfg(feature = "enable-sia")] -impl TxHistory for SiaCoinActivationParams { +impl TxHistory for SiaCoinActivationRequest { fn tx_history(&self) -> bool { self.tx_history } diff --git a/mm2src/coins_activation/src/sia_coin_activation.rs b/mm2src/coins_activation/src/sia_coin_activation.rs index 88ea10ef93..1e39da3f9c 100644 --- a/mm2src/coins_activation/src/sia_coin_activation.rs +++ b/mm2src/coins_activation/src/sia_coin_activation.rs @@ -8,11 +8,11 @@ use async_trait::async_trait; use coins::coin_balance::{CoinBalanceReport, IguanaWalletBalance}; use coins::coin_errors::MyAddressError; use coins::my_tx_history_v2::TxHistoryStorage; -use coins::siacoin::{ - sia_coin_from_conf_and_params, SiaCoin, SiaCoinActivationParams, SiaCoinBuildError, SiaCoinProtocolInfo, -}; +use coins::siacoin::{SiaCoin, SiaCoinActivationRequest, SiaCoinNewError, SiaCoinProtocolInfo}; use coins::tx_history_storage::CreateTxHistoryStorageError; -use coins::{BalanceError, CoinBalance, CoinProtocol, MarketCoinOps, PrivKeyBuildPolicy, RegisterCoinError}; +use coins::{ + lp_spawn_tx_history, BalanceError, CoinBalance, CoinProtocol, MarketCoinOps, PrivKeyBuildPolicy, RegisterCoinError, +}; use crypto::hw_rpc_task::{HwRpcTaskAwaitingStatus, HwRpcTaskUserAction}; use crypto::CryptoCtxError; use derive_more::Display; @@ -100,7 +100,7 @@ pub enum SiaCoinInitError { } impl SiaCoinInitError { - pub fn from_build_err(build_err: SiaCoinBuildError, ticker: String) -> Self { + pub fn from_build_err(build_err: SiaCoinNewError, ticker: String) -> Self { SiaCoinInitError::CoinCreationError { ticker, error: build_err.to_string(), @@ -192,7 +192,7 @@ impl TryFromCoinProtocol for SiaCoinProtocolInfo { #[async_trait] impl InitStandaloneCoinActivationOps for SiaCoin { - type ActivationRequest = SiaCoinActivationParams; + type ActivationRequest = SiaCoinActivationRequest; type StandaloneProtocol = SiaCoinProtocolInfo; type ActivationResult = SiaCoinActivationResult; type ActivationError = SiaCoinInitError; @@ -208,13 +208,13 @@ impl InitStandaloneCoinActivationOps for SiaCoin { ctx: MmArc, ticker: String, coin_conf: Json, - activation_request: &SiaCoinActivationParams, + activation_request: &SiaCoinActivationRequest, _protocol_info: SiaCoinProtocolInfo, _task_handle: SiaCoinRpcTaskHandleShared, ) -> MmResult { let priv_key_policy = PrivKeyBuildPolicy::detect_priv_key_policy(&ctx).map_mm_err()?; - let coin = sia_coin_from_conf_and_params(&ctx, &ticker, &coin_conf, activation_request, priv_key_policy) + let coin = SiaCoin::new(&ctx, coin_conf, activation_request, priv_key_policy) .await .mm_err(|e| SiaCoinInitError::from_build_err(e, ticker))?; @@ -223,7 +223,7 @@ impl InitStandaloneCoinActivationOps for SiaCoin { async fn get_activation_result( &self, - _ctx: MmArc, + ctx: MmArc, task_handle: SiaCoinRpcTaskHandleShared, _activation_request: &Self::ActivationRequest, ) -> MmResult { @@ -240,6 +240,8 @@ impl InitStandaloneCoinActivationOps for SiaCoin { let balance = self.my_balance().compat().await.map_mm_err()?; let address = self.my_address().map_mm_err()?; + lp_spawn_tx_history(ctx, self.clone().into()).map_to_mm(SiaCoinInitError::Internal)?; + Ok(SiaCoinActivationResult { ticker: self.ticker().into(), current_block, @@ -255,5 +257,6 @@ impl InitStandaloneCoinActivationOps for SiaCoin { _streaming_manager: StreamingManager, _current_balances: HashMap, ) { + // TODO: Implement v2 transaction history fetching for SiaCoin } } diff --git a/mm2src/coins_activation/src/slp_token_activation.rs b/mm2src/coins_activation/src/slp_token_activation.rs index 4066c678de..477fa1d9c5 100644 --- a/mm2src/coins_activation/src/slp_token_activation.rs +++ b/mm2src/coins_activation/src/slp_token_activation.rs @@ -16,7 +16,7 @@ impl TryPlatformCoinFromMmCoinEnum for BchCoin { Self: Sized, { match coin { - MmCoinEnum::Bch(coin) => Some(coin), + MmCoinEnum::BchVariant(coin) => Some(coin), _ => None, } } diff --git a/mm2src/coins_activation/src/solana_with_assets.rs b/mm2src/coins_activation/src/solana_with_assets.rs index 226f9c60bc..2afed8e05c 100644 --- a/mm2src/coins_activation/src/solana_with_assets.rs +++ b/mm2src/coins_activation/src/solana_with_assets.rs @@ -75,7 +75,7 @@ impl TryPlatformCoinFromMmCoinEnum for SolanaCoin { Self: Sized, { match coin { - MmCoinEnum::Solana(coin) => Some(coin), + MmCoinEnum::SolanaCoinVariant(coin) => Some(coin), _ => None, } } @@ -151,7 +151,7 @@ impl PlatformCoinWithTokensActivationOps for SolanaCoin { Self: Sized, { match coin { - MmCoinEnum::Solana(coin) => Some(coin), + MmCoinEnum::SolanaCoinVariant(coin) => Some(coin), _ => None, } } diff --git a/mm2src/coins_activation/src/tendermint_token_activation.rs b/mm2src/coins_activation/src/tendermint_token_activation.rs index d6aab90824..0f59b08aea 100644 --- a/mm2src/coins_activation/src/tendermint_token_activation.rs +++ b/mm2src/coins_activation/src/tendermint_token_activation.rs @@ -43,7 +43,7 @@ impl TryPlatformCoinFromMmCoinEnum for TendermintCoin { Self: Sized, { match coin { - MmCoinEnum::Tendermint(coin) => Some(coin), + MmCoinEnum::TendermintVariant(coin) => Some(coin), _ => None, } } diff --git a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs index 5cb95ecaec..061229017f 100644 --- a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs +++ b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs @@ -373,7 +373,7 @@ impl PlatformCoinWithTokensActivationOps for TendermintCoin { Self: Sized, { match coin { - MmCoinEnum::Tendermint(coin) => Some(coin), + MmCoinEnum::TendermintVariant(coin) => Some(coin), _ => None, } } diff --git a/mm2src/common/common.rs b/mm2src/common/common.rs index 494167b1d1..a8a3456bc5 100644 --- a/mm2src/common/common.rs +++ b/mm2src/common/common.rs @@ -224,6 +224,8 @@ pub const DEX_FEE_ADDR_PUBKEY: &str = "03bc2c7ba671bae4a6fc835244c9762b41647b982 /// Public key to collect the burn part of dex fee, for chains where SECP256K1 is supported pub const DEX_BURN_ADDR_PUBKEY: &str = "0369aa10c061cd9e085f4adb7399375ba001b54136145cb748eb4c48657be13153"; +pub const DEX_FEE_PUBKEY_ED25519: &str = "77b0936728f63257b074c7b3fb2c4fad98df345f57de1ec418fc42619e4e29f8"; + pub const PROXY_REQUEST_EXPIRATION_SEC: i64 = 15; lazy_static! { diff --git a/mm2src/crypto/Cargo.toml b/mm2src/crypto/Cargo.toml index 2999e3053b..849cdfe845 100644 --- a/mm2src/crypto/Cargo.toml +++ b/mm2src/crypto/Cargo.toml @@ -20,6 +20,7 @@ cbc.workspace = true cipher.workspace = true common = { path = "../common" } derive_more.workspace = true +ed25519-dalek-bip32.workspace = true enum_derives = { path = "../derives/enum_derives" } enum-primitive-derive.workspace = true futures.workspace = true @@ -44,6 +45,7 @@ serde.workspace = true serde_derive.workspace = true serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } sha2.workspace = true +thiserror.workspace = true trezor = { path = "../trezor" } zeroize.workspace = true diff --git a/mm2src/crypto/src/global_hd_ctx.rs b/mm2src/crypto/src/global_hd_ctx.rs index 913ad97a3b..4f3bcd6ea0 100644 --- a/mm2src/crypto/src/global_hd_ctx.rs +++ b/mm2src/crypto/src/global_hd_ctx.rs @@ -1,14 +1,19 @@ #![allow(non_local_definitions)] -use crate::privkey::{bip39_seed_from_passphrase, key_pair_from_secret, PrivKeyError}; -use crate::{mm2_internal_der_path, Bip32Error, CryptoInitError, CryptoInitResult}; +use crate::privkey::{bip39_seed_from_mnemonic, key_pair_from_secret, PrivKeyError}; +use crate::{mm2_internal_der_path, Bip32Error, CryptoInitResult}; use bip32::{DerivationPath, ExtendedPrivateKey}; use common::drop_mutability; +use ed25519_dalek_bip32::ExtendedSigningKey; use keys::{KeyPair, Secret as Secp256k1Secret}; use mm2_err_handle::prelude::*; use std::ops::Deref; use std::sync::Arc; use zeroize::{Zeroize, ZeroizeOnDrop}; +// Ed25519DerivationPath represents the same exact thing as bip32::DerivationPath, but is a different type +// used within the ed25519_dalek_bip32 library. +// We should consider our own wrapper around both types to avoid confusion. +use ed25519_dalek_bip32::DerivationPath as Ed25519DerivationPath; pub(super) type Mm2InternalKeyPair = KeyPair; @@ -28,24 +33,37 @@ pub struct Bip39Seed(pub [u8; 64]); pub struct GlobalHDAccountCtx { bip39_seed: Bip39Seed, + /// The master extended private key `m`, as defined within the BIP32 standard. bip39_secp_priv_key: ExtendedPrivateKey, + /// The master extended private key `m`, as defined within the SLIP-10 standard. + /// + /// ## Security Considerations + /// - ed25519 key derivation via SLIP-10 does not support deriving children from a public key alone. + /// - Any type or abstraction that acts like an `xpub` must embed private key material, which can easily + /// lead to misuse if consumers assume it is safe to expose or serialize. + ed25519_master_priv_key: ExtendedSigningKey, } impl GlobalHDAccountCtx { - pub fn new(passphrase: &str) -> CryptoInitResult<(Mm2InternalKeyPair, GlobalHDAccountCtx)> { - let bip39_seed = bip39_seed_from_passphrase(passphrase).map_mm_err()?; + pub fn new(mnemonic_str: &str) -> CryptoInitResult<(Mm2InternalKeyPair, GlobalHDAccountCtx)> { + let bip39_seed = bip39_seed_from_mnemonic(mnemonic_str).map_mm_err()?; let bip39_secp_priv_key: ExtendedPrivateKey = ExtendedPrivateKey::new(bip39_seed.0) - .map_to_mm(|e| PrivKeyError::InvalidPrivKey(e.to_string())) + .map_to_mm(PrivKeyError::Secp256k1MasterKey) + .map_mm_err()?; + + let ed25519_master_priv_key = ExtendedSigningKey::from_seed(&bip39_seed.0) + .map_to_mm(PrivKeyError::Ed25519MasterKey) .map_mm_err()?; + // The derivation path for an "internal key". See CryptoCtx::mm2_internal_key_pair comment. let derivation_path = mm2_internal_der_path(); let mut internal_priv_key = bip39_secp_priv_key.clone(); for child in derivation_path { internal_priv_key = internal_priv_key .derive_child(child) - .map_to_mm(|e| CryptoInitError::InvalidPassphrase(PrivKeyError::InvalidPrivKey(e.to_string()))) - .map_mm_err()?; + .map_to_mm(PrivKeyError::Secp256k1InternalKey) + .map_mm_err()? } let mm2_internal_key_pair = key_pair_from_secret(internal_priv_key.private_key().as_ref()).map_mm_err()?; @@ -53,6 +71,7 @@ impl GlobalHDAccountCtx { let global_hd_ctx = GlobalHDAccountCtx { bip39_seed, bip39_secp_priv_key, + ed25519_master_priv_key, }; Ok((mm2_internal_key_pair, global_hd_ctx)) } @@ -88,6 +107,15 @@ impl GlobalHDAccountCtx { pub fn derive_secp256k1_secret(&self, derivation_path: &DerivationPath) -> MmResult { derive_secp256k1_secret(self.bip39_secp_priv_key.clone(), derivation_path) } + + pub fn derive_ed25519_signing_key( + &self, + derivation_path: &Ed25519DerivationPath, + ) -> MmResult { + self.ed25519_master_priv_key + .derive(derivation_path) + .map_to_mm(|e| PrivKeyError::Ed25519DeriveKey(e, derivation_path.clone())) + } } pub fn derive_secp256k1_secret( @@ -103,3 +131,182 @@ pub fn derive_secp256k1_secret( let secret = *priv_key.private_key().as_ref(); Ok(Secp256k1Secret::from(secret)) } + +// https://github.com/satoshilabs/slips/blob/master/slip-0010.md#test-vector-1-for-ed25519 +#[test] +fn test_slip_10_ed25519_vector_1() { + use std::str::FromStr; + + let ed25519_master_priv_key = + ExtendedSigningKey::from_seed(&hex::decode("000102030405060708090a0b0c0d0e0f").unwrap()).unwrap(); + + // master xpriv aka "m" + let known_chain_code = hex::decode("90046a93de5380a72b5e45010748567d5ea02bbf6522f979e05c0d8d8ca9fffb").unwrap(); + let known_priv_key = hex::decode("2b4be7f19ee27bbf30c667b642d5f4aa69fd169872f8fc3059c08ebae2eb19e7").unwrap(); + let known_pub_key = hex::decode("a4b2856bfec510abab89753fac1ac0e1112364e7d250545963f135f2a33188ed").unwrap(); + assert_eq!(known_chain_code, ed25519_master_priv_key.chain_code.to_vec()); + assert_eq!(known_priv_key, ed25519_master_priv_key.signing_key.to_bytes().to_vec()); + assert_eq!( + known_pub_key, + ed25519_master_priv_key.signing_key.verifying_key().to_bytes().to_vec() + ); + + let path = Ed25519DerivationPath::from_str("m/0'").unwrap(); + let child_priv_key = ed25519_master_priv_key.derive(&path).unwrap(); + let known_chain_code = hex::decode("8b59aa11380b624e81507a27fedda59fea6d0b779a778918a2fd3590e16e9c69").unwrap(); + let known_priv_key = hex::decode("68e0fe46dfb67e368c75379acec591dad19df3cde26e63b93a8e704f1dade7a3").unwrap(); + let known_pub_key = hex::decode("8c8a13df77a28f3445213a0f432fde644acaa215fc72dcdf300d5efaa85d350c").unwrap(); + assert_eq!(known_chain_code, child_priv_key.chain_code.to_vec()); + assert_eq!(known_priv_key, child_priv_key.signing_key.to_bytes().to_vec()); + assert_eq!( + known_pub_key, + child_priv_key.signing_key.verifying_key().to_bytes().to_vec() + ); + + let path = Ed25519DerivationPath::from_str("m/0'/1'").unwrap(); + let child_priv_key = ed25519_master_priv_key.derive(&path).unwrap(); + let known_chain_code = hex::decode("a320425f77d1b5c2505a6b1b27382b37368ee640e3557c315416801243552f14").unwrap(); + let known_priv_key = hex::decode("b1d0bad404bf35da785a64ca1ac54b2617211d2777696fbffaf208f746ae84f2").unwrap(); + let known_pub_key = hex::decode("1932a5270f335bed617d5b935c80aedb1a35bd9fc1e31acafd5372c30f5c1187").unwrap(); + assert_eq!(known_chain_code, child_priv_key.chain_code.to_vec()); + assert_eq!(known_priv_key, child_priv_key.signing_key.to_bytes().to_vec()); + assert_eq!( + known_pub_key, + child_priv_key.signing_key.verifying_key().to_bytes().to_vec() + ); + + let path = Ed25519DerivationPath::from_str("m/0'/1'/2'").unwrap(); + let child_priv_key = ed25519_master_priv_key.derive(&path).unwrap(); + let known_chain_code = hex::decode("2e69929e00b5ab250f49c3fb1c12f252de4fed2c1db88387094a0f8c4c9ccd6c").unwrap(); + let known_priv_key = hex::decode("92a5b23c0b8a99e37d07df3fb9966917f5d06e02ddbd909c7e184371463e9fc9").unwrap(); + let known_pub_key = hex::decode("ae98736566d30ed0e9d2f4486a64bc95740d89c7db33f52121f8ea8f76ff0fc1").unwrap(); + assert_eq!(known_chain_code, child_priv_key.chain_code.to_vec()); + assert_eq!(known_priv_key, child_priv_key.signing_key.to_bytes().to_vec()); + assert_eq!( + known_pub_key, + child_priv_key.signing_key.verifying_key().to_bytes().to_vec() + ); + + let path = Ed25519DerivationPath::from_str("m/0'/1'/2'/2'").unwrap(); + let child_priv_key = ed25519_master_priv_key.derive(&path).unwrap(); + let known_chain_code = hex::decode("8f6d87f93d750e0efccda017d662a1b31a266e4a6f5993b15f5c1f07f74dd5cc").unwrap(); + let known_priv_key = hex::decode("30d1dc7e5fc04c31219ab25a27ae00b50f6fd66622f6e9c913253d6511d1e662").unwrap(); + let known_pub_key = hex::decode("8abae2d66361c879b900d204ad2cc4984fa2aa344dd7ddc46007329ac76c429c").unwrap(); + assert_eq!(known_chain_code, child_priv_key.chain_code.to_vec()); + assert_eq!(known_priv_key, child_priv_key.signing_key.to_bytes().to_vec()); + assert_eq!( + known_pub_key, + child_priv_key.signing_key.verifying_key().to_bytes().to_vec() + ); + + let path = Ed25519DerivationPath::from_str("m/0'/1'/2'/2'/1000000000'").unwrap(); + let child_priv_key = ed25519_master_priv_key.derive(&path).unwrap(); + let known_chain_code = hex::decode("68789923a0cac2cd5a29172a475fe9e0fb14cd6adb5ad98a3fa70333e7afa230").unwrap(); + let known_priv_key = hex::decode("8f94d394a8e8fd6b1bc2f3f49f5c47e385281d5c17e65324b0f62483e37e8793").unwrap(); + let known_pub_key = hex::decode("3c24da049451555d51a7014a37337aa4e12d41e485abccfa46b47dfb2af54b7a").unwrap(); + assert_eq!(known_chain_code, child_priv_key.chain_code.to_vec()); + assert_eq!(known_priv_key, child_priv_key.signing_key.to_bytes().to_vec()); + assert_eq!( + known_pub_key, + child_priv_key.signing_key.verifying_key().to_bytes().to_vec() + ); +} + +// https://github.com/satoshilabs/slips/blob/master/slip-0010.md#test-vector-2-for-ed25519 +#[test] +fn test_slip_10_ed25519_vector_2() { + use std::convert::TryInto; + use std::str::FromStr; + + // Ed25519DerivationPath represents the same exact thing as bip32::DerivationPath, but is a different type + // used within the ed25519_dalek_bip32 library. + // We should consider our own wrapper around both types to avoid confusion. + use ed25519_dalek_bip32::DerivationPath as Ed25519DerivationPath; + + let seed_bytes : [u8;64] = hex::decode("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542").unwrap().try_into().unwrap(); + let seed = Bip39Seed(seed_bytes); + let ed25519_master_priv_key = ExtendedSigningKey::from_seed(&seed.0).unwrap(); + + // master xpriv aka "m" + let known_chain_code = hex::decode("ef70a74db9c3a5af931b5fe73ed8e1a53464133654fd55e7a66f8570b8e33c3b").unwrap(); + let known_priv_key = hex::decode("171cb88b1b3c1db25add599712e36245d75bc65a1a5c9e18d76f9f2b1eab4012").unwrap(); + let known_pub_key = hex::decode("8fe9693f8fa62a4305a140b9764c5ee01e455963744fe18204b4fb948249308a").unwrap(); + assert_eq!(known_chain_code, ed25519_master_priv_key.chain_code.to_vec()); + assert_eq!(known_priv_key, ed25519_master_priv_key.signing_key.to_bytes().to_vec()); + assert_eq!( + known_pub_key, + ed25519_master_priv_key.signing_key.verifying_key().to_bytes().to_vec() + ); + + let path = Ed25519DerivationPath::from_str("m/0'").unwrap(); + let child_priv_key = ed25519_master_priv_key.derive(&path).unwrap(); + let known_chain_code = hex::decode("0b78a3226f915c082bf118f83618a618ab6dec793752624cbeb622acb562862d").unwrap(); + let known_priv_key = hex::decode("1559eb2bbec5790b0c65d8693e4d0875b1747f4970ae8b650486ed7470845635").unwrap(); + let known_pub_key = hex::decode("86fab68dcb57aa196c77c5f264f215a112c22a912c10d123b0d03c3c28ef1037").unwrap(); + assert_eq!(known_chain_code, child_priv_key.chain_code.to_vec()); + assert_eq!(known_priv_key, child_priv_key.signing_key.to_bytes().to_vec()); + assert_eq!( + known_pub_key, + child_priv_key.signing_key.verifying_key().to_bytes().to_vec() + ); + + let path = Ed25519DerivationPath::from_str("m/0'/2147483647'").unwrap(); + let child_priv_key = ed25519_master_priv_key.derive(&path).unwrap(); + let known_chain_code = hex::decode("138f0b2551bcafeca6ff2aa88ba8ed0ed8de070841f0c4ef0165df8181eaad7f").unwrap(); + let known_priv_key = hex::decode("ea4f5bfe8694d8bb74b7b59404632fd5968b774ed545e810de9c32a4fb4192f4").unwrap(); + let known_pub_key = hex::decode("5ba3b9ac6e90e83effcd25ac4e58a1365a9e35a3d3ae5eb07b9e4d90bcf7506d").unwrap(); + assert_eq!(known_chain_code, child_priv_key.chain_code.to_vec()); + assert_eq!(known_priv_key, child_priv_key.signing_key.to_bytes().to_vec()); + assert_eq!( + known_pub_key, + child_priv_key.signing_key.verifying_key().to_bytes().to_vec() + ); + + let path = Ed25519DerivationPath::from_str("m/0'/2147483647'").unwrap(); + let child_priv_key = ed25519_master_priv_key.derive(&path).unwrap(); + let known_chain_code = hex::decode("138f0b2551bcafeca6ff2aa88ba8ed0ed8de070841f0c4ef0165df8181eaad7f").unwrap(); + let known_priv_key = hex::decode("ea4f5bfe8694d8bb74b7b59404632fd5968b774ed545e810de9c32a4fb4192f4").unwrap(); + let known_pub_key = hex::decode("5ba3b9ac6e90e83effcd25ac4e58a1365a9e35a3d3ae5eb07b9e4d90bcf7506d").unwrap(); + assert_eq!(known_chain_code, child_priv_key.chain_code.to_vec()); + assert_eq!(known_priv_key, child_priv_key.signing_key.to_bytes().to_vec()); + assert_eq!( + known_pub_key, + child_priv_key.signing_key.verifying_key().to_bytes().to_vec() + ); + + let path = Ed25519DerivationPath::from_str("m/0'/2147483647'/1'").unwrap(); + let child_priv_key = ed25519_master_priv_key.derive(&path).unwrap(); + let known_chain_code = hex::decode("73bd9fff1cfbde33a1b846c27085f711c0fe2d66fd32e139d3ebc28e5a4a6b90").unwrap(); + let known_priv_key = hex::decode("3757c7577170179c7868353ada796c839135b3d30554bbb74a4b1e4a5a58505c").unwrap(); + let known_pub_key = hex::decode("2e66aa57069c86cc18249aecf5cb5a9cebbfd6fadeab056254763874a9352b45").unwrap(); + assert_eq!(known_chain_code, child_priv_key.chain_code.to_vec()); + assert_eq!(known_priv_key, child_priv_key.signing_key.to_bytes().to_vec()); + assert_eq!( + known_pub_key, + child_priv_key.signing_key.verifying_key().to_bytes().to_vec() + ); + + let path = Ed25519DerivationPath::from_str("m/0'/2147483647'/1'/2147483646'").unwrap(); + let child_priv_key = ed25519_master_priv_key.derive(&path).unwrap(); + let known_chain_code = hex::decode("0902fe8a29f9140480a00ef244bd183e8a13288e4412d8389d140aac1794825a").unwrap(); + let known_priv_key = hex::decode("5837736c89570de861ebc173b1086da4f505d4adb387c6a1b1342d5e4ac9ec72").unwrap(); + let known_pub_key = hex::decode("e33c0f7d81d843c572275f287498e8d408654fdf0d1e065b84e2e6f157aab09b").unwrap(); + assert_eq!(known_chain_code, child_priv_key.chain_code.to_vec()); + assert_eq!(known_priv_key, child_priv_key.signing_key.to_bytes().to_vec()); + assert_eq!( + known_pub_key, + child_priv_key.signing_key.verifying_key().to_bytes().to_vec() + ); + + let path = Ed25519DerivationPath::from_str("m/0'/2147483647'/1'/2147483646'/2'").unwrap(); + let child_priv_key = ed25519_master_priv_key.derive(&path).unwrap(); + let known_chain_code = hex::decode("5d70af781f3a37b829f0d060924d5e960bdc02e85423494afc0b1a41bbe196d4").unwrap(); + let known_priv_key = hex::decode("551d333177df541ad876a60ea71f00447931c0a9da16f227c11ea080d7391b8d").unwrap(); + let known_pub_key = hex::decode("47150c75db263559a70d5778bf36abbab30fb061ad69f69ece61a72b0cfa4fc0").unwrap(); + assert_eq!(known_chain_code, child_priv_key.chain_code.to_vec()); + assert_eq!(known_priv_key, child_priv_key.signing_key.to_bytes().to_vec()); + assert_eq!( + known_pub_key, + child_priv_key.signing_key.verifying_key().to_bytes().to_vec() + ); +} diff --git a/mm2src/crypto/src/privkey.rs b/mm2src/crypto/src/privkey.rs index 4f90248e4b..4be0bfcb59 100644 --- a/mm2src/crypto/src/privkey.rs +++ b/mm2src/crypto/src/privkey.rs @@ -20,51 +20,56 @@ // use crate::global_hd_ctx::Bip39Seed; +use bip32::Error as Bip32Error; +use bip39::Error as Bip39Error; use bitcrypto::{sha256, ChecksumType}; -use derive_more::Display; +use ed25519_dalek_bip32::{DerivationPath as Ed25519DerivationPath, Error as Ed25519Bip32Error}; use keys::{Error as KeysError, KeyPair, Private, Secret as Secp256k1Secret}; use mm2_err_handle::prelude::*; use rustc_hex::FromHexError; use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use thiserror::Error; pub type PrivKeyResult = Result>; -#[derive(Debug, Display, Serialize)] +#[derive(Debug, Error)] pub enum PrivKeyError { - #[display(fmt = "Provided WIF passphrase has invalid checksum!")] - WifPassphraseInvalidChecksum, - #[display(fmt = "Error parsing passphrase: {_0}")] - ErrorParsingPassphrase(String), - #[display(fmt = "Invalid private key: {_0}")] - InvalidPrivKey(String), - #[display(fmt = "We only support compressed keys at the moment")] + #[error("bip39_seed_from_passphrase: Error parsing passphrase: {0}")] + Bip39Parsing(#[from] Bip39Error), + #[error("private_from_seed: Error parsing provided WIF: {0}")] + WifSecp256k1Parsing(KeysError), + #[error( + "private_from_seed: Error parsing raw secp256k1 private key, expected 0x prefixed 32 byte hex string: {0}" + )] + RawSecp256k1Parsing(#[from] FromHexError), + #[error("GlobalHDAccountCtx::new: Failed to calculate secp256k1 master xpriv from bip39 seed: {0}")] + Secp256k1MasterKey(Bip32Error), + #[error("GlobalHDAccountCtx::new: Failed to calculate ed25519 master xpriv from bip39 seed: {0}")] + Ed25519MasterKey(Ed25519Bip32Error), + #[error("GlobalHDAccountCtx::derive_ed25519_signing_key: Failed to derive key for path:{1} with error: {0}")] + Ed25519DeriveKey(Ed25519Bip32Error, Ed25519DerivationPath), + #[error("GlobalHDAccountCtx::new: Failed to derive internal secp256k1 private key: {0}")] + Secp256k1InternalKey(Bip32Error), + #[error("key_pair_from_secret: Failed to create KeyPair from byte array {0}")] + KeyPairFromSecret(KeysError), + #[error("key_pair_from_seed: Expected compressed public key, found uncompressed")] ExpectedCompressedKeys, + #[error("key_pair_from_seed: Failed to create KeyPair from Private {0}")] + PrivateIntoKeyPair(KeysError), } -impl From for PrivKeyError { - fn from(e: FromHexError) -> Self { - PrivKeyError::ErrorParsingPassphrase(e.to_string()) - } -} - -impl From for PrivKeyError { - fn from(e: KeysError) -> Self { - PrivKeyError::InvalidPrivKey(e.to_string()) - } -} - -impl std::error::Error for PrivKeyError {} - fn private_from_seed(seed: &str) -> PrivKeyResult { + // Attempt to parse the seed as a WIF match seed.parse() { Ok(private) => return Ok(private), Err(e) => { if let KeysError::InvalidChecksum = e { - return MmError::err(PrivKeyError::WifPassphraseInvalidChecksum); + return MmError::err(PrivKeyError::WifSecp256k1Parsing(e)); } }, // else ignore other errors, assume the passphrase is not WIF } + // If the seed starts with 0x, we treat it as hex string representing a secp256k1 private key match seed.strip_prefix("0x") { Some(stripped) => { let hash: Secp256k1Secret = stripped.parse()?; @@ -102,7 +107,7 @@ pub fn key_pair_from_seed(seed: &str) -> PrivKeyResult { if !private.compressed { return MmError::err(PrivKeyError::ExpectedCompressedKeys); } - let pair = KeyPair::from_private(private)?; + let pair = KeyPair::from_private(private).map_err(PrivKeyError::PrivateIntoKeyPair)?; // Just a sanity check. We rely on the public key being 33 bytes (aka compressed). assert_eq!(pair.public().len(), 33); Ok(pair) @@ -115,12 +120,11 @@ pub fn key_pair_from_secret(secret: &[u8; 32]) -> PrivKeyResult { compressed: true, checksum_type: ChecksumType::DSHA256, }; - Ok(KeyPair::from_private(private)?) + Ok(KeyPair::from_private(private).map_err(PrivKeyError::KeyPairFromSecret)?) } -pub fn bip39_seed_from_passphrase(passphrase: &str) -> PrivKeyResult { - let mnemonic = bip39::Mnemonic::parse_in_normalized(bip39::Language::English, passphrase) - .map_to_mm(|e| PrivKeyError::ErrorParsingPassphrase(e.to_string()))?; +pub fn bip39_seed_from_mnemonic(mnemonic_str: &str) -> PrivKeyResult { + let mnemonic = bip39::Mnemonic::parse_in_normalized(bip39::Language::English, mnemonic_str)?; let seed = mnemonic.to_seed_normalized(""); Ok(Bip39Seed(seed)) } diff --git a/mm2src/crypto/src/standard_hd_path.rs b/mm2src/crypto/src/standard_hd_path.rs index fc83578b45..6f63bb2f59 100644 --- a/mm2src/crypto/src/standard_hd_path.rs +++ b/mm2src/crypto/src/standard_hd_path.rs @@ -6,6 +6,18 @@ use hw_common::primitives::Bip32Error; use num_traits::FromPrimitive; use std::convert::TryFrom; +/* +Alright TODO - These type aliases can be confusing at first glance. They allow us to impose a specific +structure on a value of Bip32Child type as compile time checks. + +This is maybe clever, but this module needs developer documentation(or a refactor!) as it was a +serious pain point during the Sia implementation. My biggest complaint is that typical IDE workflows +such as "go to definition" or "find references" do not work well with these type aliases and their +generic impls. + +Consider wrapping the Bip32Child type in a newtype struct leaving the inner private to constrict +inner value via constructors such as ::new(), from_str() or from_bytes(). +*/ /// Standard HD Path for [BIP-44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki), /// [BIP-49](https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki), /// [BIP-84](https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki) diff --git a/mm2src/mm2_main/Cargo.toml b/mm2src/mm2_main/Cargo.toml index ddcc883003..b3b7698882 100644 --- a/mm2src/mm2_main/Cargo.toml +++ b/mm2src/mm2_main/Cargo.toml @@ -21,7 +21,6 @@ run-docker-tests = ["coins/run-docker-tests"] default = [] trezor-udp = ["crypto/trezor-udp"] # use for tests to connect to trezor emulator over udp run-device-tests = [] -enable-sia = ["coins/enable-sia", "coins_activation/enable-sia"] sepolia-maker-swap-v2-tests = [] sepolia-taker-swap-v2-tests = [] test-ext-api = ["trading_api/test-ext-api"] @@ -45,7 +44,7 @@ coins = { path = "../coins" } coins_activation = { path = "../coins_activation" } common = { path = "../common" } compatible-time.workspace = true -crc32fast.workspace = true +crc32fast.workspace = true crossbeam.workspace = true crypto = { path = "../crypto" } db_common = { path = "../db_common" } @@ -56,7 +55,7 @@ enum_derives = { path = "../derives/enum_derives" } enum-primitive-derive.workspace = true futures01.workspace = true futures = { workspace = true, features = ["compat", "async-await"] } -gstuff.workspace = true +gstuff.workspace = true hash256-std-hasher.workspace = true hash-db.workspace = true hex.workspace = true @@ -105,7 +104,7 @@ sp-trie.workspace = true tempfile.workspace = true trie-db.workspace = true trie-root.workspace = true -uuid.workspace = true +uuid.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] # TODO: Removing this causes `wasm-pack` to fail when starting a web session (even though we don't use this crate). @@ -136,6 +135,7 @@ winapi.workspace = true signal-hook-tokio = { version = "0.3", features = [ "futures-v0_3" ] } [dev-dependencies] +base64.workspace = true coins = { path = "../coins", features = ["for-tests"] } coins_activation = { path = "../coins_activation", features = ["for-tests"] } common = { path = "../common", features = ["for-tests"] } @@ -143,7 +143,6 @@ mm2_test_helpers = { path = "../mm2_test_helpers" } trading_api = { path = "../trading_api", features = ["for-tests"] } env_logger.workspace = true mocktopus.workspace = true -testcontainers.workspace = true web3 = { workspace = true, default-features = false, features = ["http-rustls-tls"] } ethabi.workspace = true rlp.workspace = true @@ -152,6 +151,11 @@ rustc-hex.workspace = true sia-rust.workspace = true url.workspace = true +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +# todo: try to use async features +testcontainers = { workspace = true, features = ["blocking"] } +reqwest = { workspace = true, features = ["json"] } + [build-dependencies] chrono.workspace = true gstuff.workspace = true diff --git a/mm2src/mm2_main/src/lp_network.rs b/mm2src/mm2_main/src/lp_network.rs index 7de3d33e38..d21d713fcc 100644 --- a/mm2src/mm2_main/src/lp_network.rs +++ b/mm2src/mm2_main/src/lp_network.rs @@ -42,6 +42,9 @@ use serde::de; use crate::{lp_healthcheck, lp_ordermatch, lp_stats, lp_swap}; +const LP_RPCPORT: u16 = 7783; +pub const MAX_NETID: u16 = (65535 - 40 - LP_RPCPORT) / 4; + pub type P2PRequestResult = Result>; pub type P2PProcessResult = Result>; @@ -455,10 +458,11 @@ pub enum NetIdError { } pub fn lp_ports(netid: u16) -> Result<(u16, u16, u16), MmError> { - const LP_RPCPORT: u16 = 7783; - let max_netid = (65535 - 40 - LP_RPCPORT) / 4; - if netid > max_netid { - return MmError::err(NetIdError::LargerThanMax { netid, max_netid }); + if netid > MAX_NETID { + return MmError::err(NetIdError::LargerThanMax { + netid, + max_netid: MAX_NETID, + }); } let other_ports = if netid != 0 { diff --git a/mm2src/mm2_main/src/lp_ordermatch.rs b/mm2src/mm2_main/src/lp_ordermatch.rs index f09d72d068..d6c2ab5a44 100644 --- a/mm2src/mm2_main/src/lp_ordermatch.rs +++ b/mm2src/mm2_main/src/lp_ordermatch.rs @@ -3286,16 +3286,16 @@ fn lp_connect_start_bob(ctx: MmArc, maker_match: MakerMatch, maker_order: MakerO // TODO try to handle it more gracefully during project redesign match (&maker_coin, &taker_coin) { - (MmCoinEnum::UtxoCoin(m), MmCoinEnum::UtxoCoin(t)) => { + (MmCoinEnum::UtxoCoinVariant(m), MmCoinEnum::UtxoCoinVariant(t)) => { start_maker_swap_state_machine(&ctx, &maker_order, &taker_p2p_pubkey, &secret, m, t, ¶ms).await; }, - (MmCoinEnum::EthCoin(m), MmCoinEnum::EthCoin(t)) => { + (MmCoinEnum::EthCoinVariant(m), MmCoinEnum::EthCoinVariant(t)) => { start_maker_swap_state_machine(&ctx, &maker_order, &taker_p2p_pubkey, &secret, m, t, ¶ms).await; }, - (MmCoinEnum::UtxoCoin(m), MmCoinEnum::EthCoin(t)) => { + (MmCoinEnum::UtxoCoinVariant(m), MmCoinEnum::EthCoinVariant(t)) => { start_maker_swap_state_machine(&ctx, &maker_order, &taker_p2p_pubkey, &secret, m, t, ¶ms).await; }, - (MmCoinEnum::EthCoin(m), MmCoinEnum::UtxoCoin(t)) => { + (MmCoinEnum::EthCoinVariant(m), MmCoinEnum::UtxoCoinVariant(t)) => { start_maker_swap_state_machine(&ctx, &maker_order, &taker_p2p_pubkey, &secret, m, t, ¶ms).await; }, _ => { @@ -3533,19 +3533,19 @@ fn lp_connected_alice(ctx: MmArc, taker_order: TakerOrder, taker_match: TakerMat // TODO try to handle it more gracefully during project redesign match (&maker_coin, &taker_coin) { - (MmCoinEnum::UtxoCoin(m), MmCoinEnum::UtxoCoin(t)) => { + (MmCoinEnum::UtxoCoinVariant(m), MmCoinEnum::UtxoCoinVariant(t)) => { start_taker_swap_state_machine(&ctx, &taker_order, &maker_p2p_pubkey, &taker_secret, m, t, ¶ms) .await; }, - (MmCoinEnum::EthCoin(m), MmCoinEnum::EthCoin(t)) => { + (MmCoinEnum::EthCoinVariant(m), MmCoinEnum::EthCoinVariant(t)) => { start_taker_swap_state_machine(&ctx, &taker_order, &maker_p2p_pubkey, &taker_secret, m, t, ¶ms) .await; }, - (MmCoinEnum::UtxoCoin(m), MmCoinEnum::EthCoin(t)) => { + (MmCoinEnum::UtxoCoinVariant(m), MmCoinEnum::EthCoinVariant(t)) => { start_taker_swap_state_machine(&ctx, &taker_order, &maker_p2p_pubkey, &taker_secret, m, t, ¶ms) .await; }, - (MmCoinEnum::EthCoin(m), MmCoinEnum::UtxoCoin(t)) => { + (MmCoinEnum::EthCoinVariant(m), MmCoinEnum::UtxoCoinVariant(t)) => { start_taker_swap_state_machine(&ctx, &taker_order, &maker_p2p_pubkey, &taker_secret, m, t, ¶ms) .await; }, @@ -4458,7 +4458,7 @@ pub async fn lp_auto_buy( // For non-HTLC Tendermint orders, include the channel information which will be used // later from the other pair. #[cfg(feature = "ibc-routing-for-swaps")] - if let MmCoinEnum::Tendermint(tendermint_coin) = &base_coin { + if let MmCoinEnum::TendermintVariant(tendermint_coin) = &base_coin { if !tendermint_coin.supports_htlc() { let channel_id = try_s!(tendermint_coin.get_healthy_ibc_channel_to_htlc_chain().await); order_builder.order_metadata.channel_id_if_ibc_routing = Some(channel_id); @@ -5221,7 +5221,7 @@ pub async fn create_maker_order(ctx: &MmArc, req: SetPriceReq) -> Result Ok(OrderbookAddress::Shielded), - // TODO implement for SIA "this is needed to show the address in the orderbook" - #[cfg(feature = "enable-sia")] - CoinProtocol::SIA => MmError::err(OrderbookAddrErr::CoinIsNotSupported(coin.to_owned())), + // TODO implement for SIA "this is needed to show the address in the orderbook", we leave is as shielded for now + CoinProtocol::SIA => Ok(OrderbookAddress::Shielded), CoinProtocol::SOLANA(_) => MmError::err(OrderbookAddrErr::CoinIsNotSupported(coin.to_owned())), CoinProtocol::SOLANATOKEN(_) => MmError::err(OrderbookAddrErr::CoinIsNotSupported(coin.to_owned())), } diff --git a/mm2src/mm2_main/src/lp_ordermatch/my_orders_storage.rs b/mm2src/mm2_main/src/lp_ordermatch/my_orders_storage.rs index 4ab107708e..0e71ee4c6a 100644 --- a/mm2src/mm2_main/src/lp_ordermatch/my_orders_storage.rs +++ b/mm2src/mm2_main/src/lp_ordermatch/my_orders_storage.rs @@ -791,6 +791,8 @@ mod tests { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: crate::lp_ordermatch::OrderMetadata::default(), swap_version: SwapVersion::default(), }, matches: HashMap::new(), @@ -802,8 +804,6 @@ mod tests { base_orderbook_ticker: None, rel_orderbook_ticker: None, p2p_privkey: None, - #[cfg(feature = "ibc-routing-for-swaps")] - order_metadata: crate::lp_ordermatch::OrderMetadata::default(), } } diff --git a/mm2src/mm2_main/src/lp_swap.rs b/mm2src/mm2_main/src/lp_swap.rs index 4749e54cc0..52fb27cd80 100644 --- a/mm2src/mm2_main/src/lp_swap.rs +++ b/mm2src/mm2_main/src/lp_swap.rs @@ -454,7 +454,7 @@ const PAYMENT_LOCKTIME: u64 = 3600 * 2 + 300 * 2; /// Default atomic swap payment locktime, in seconds. /// Maker sends payment with LOCKTIME * 2 /// Taker sends payment with LOCKTIME -pub(crate) static PAYMENT_LOCKTIME: AtomicU64 = AtomicU64::new(super::CUSTOM_PAYMENT_LOCKTIME_DEFAULT); +pub static PAYMENT_LOCKTIME: AtomicU64 = AtomicU64::new(super::CUSTOM_PAYMENT_LOCKTIME_DEFAULT); #[inline] /// Returns `PAYMENT_LOCKTIME` @@ -1624,11 +1624,16 @@ pub async fn active_swaps_rpc(ctx: MmArc, req: Json) -> Result> #[cfg(not(target_arch = "wasm32"))] pub fn detect_secret_hash_algo(maker_coin: &MmCoinEnum, taker_coin: &MmCoinEnum) -> SecretHashAlgo { match (maker_coin, taker_coin) { - (MmCoinEnum::Tendermint(_) | MmCoinEnum::TendermintToken(_) | MmCoinEnum::LightningCoin(_), _) => { - SecretHashAlgo::SHA256 - }, + ( + MmCoinEnum::TendermintVariant(_) + | MmCoinEnum::TendermintTokenVariant(_) + | MmCoinEnum::LightningCoinVariant(_), + _, + ) => SecretHashAlgo::SHA256, // If taker is lightning coin the SHA256 of the secret will be sent as part of the maker signed invoice - (_, MmCoinEnum::Tendermint(_) | MmCoinEnum::TendermintToken(_)) => SecretHashAlgo::SHA256, + (_, MmCoinEnum::TendermintVariant(_) | MmCoinEnum::TendermintTokenVariant(_)) => SecretHashAlgo::SHA256, + (_, MmCoinEnum::SiaCoinVariant(_)) => SecretHashAlgo::SHA256, + (MmCoinEnum::SiaCoinVariant(_), _) => SecretHashAlgo::SHA256, (_, _) => SecretHashAlgo::DHASH160, } } @@ -1637,8 +1642,10 @@ pub fn detect_secret_hash_algo(maker_coin: &MmCoinEnum, taker_coin: &MmCoinEnum) #[cfg(target_arch = "wasm32")] pub fn detect_secret_hash_algo(maker_coin: &MmCoinEnum, taker_coin: &MmCoinEnum) -> SecretHashAlgo { match (maker_coin, taker_coin) { - (MmCoinEnum::Tendermint(_) | MmCoinEnum::TendermintToken(_), _) => SecretHashAlgo::SHA256, - (_, MmCoinEnum::Tendermint(_) | MmCoinEnum::TendermintToken(_)) => SecretHashAlgo::SHA256, + (MmCoinEnum::TendermintVariant(_) | MmCoinEnum::TendermintTokenVariant(_), _) => SecretHashAlgo::SHA256, + (_, MmCoinEnum::TendermintVariant(_) | MmCoinEnum::TendermintTokenVariant(_)) => SecretHashAlgo::SHA256, + (_, MmCoinEnum::SiaCoinVariant(_)) => SecretHashAlgo::SHA256, + (MmCoinEnum::SiaCoinVariant(_), _) => SecretHashAlgo::SHA256, (_, _) => SecretHashAlgo::DHASH160, } } diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap.rs b/mm2src/mm2_main/src/lp_swap/maker_swap.rs index 1f2bac0e7e..93682a25ec 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap.rs @@ -2289,18 +2289,6 @@ pub async fn run_maker_swap(swap: RunMakerSwapInput, ctx: MmArc) { .dispatch_async(ctx.clone(), LpEvents::MakerSwapStatusChanged(event_to_send)) .await; drop(dispatcher); - // Send a notification to the swap status streamer about a new event. - ctx.event_stream_manager - .send_fn(&SwapStatusStreamer::derive_streamer_id(()), || { - SwapStatusEvent::MakerV1 { - uuid: running_swap.uuid, - event: to_save.clone(), - } - }) - .ok(); - save_my_maker_swap_event(&ctx, &running_swap, to_save) - .await - .expect("!save_my_maker_swap_event"); if event.should_ban_taker() { ban_pubkey_on_failed_swap( &ctx, @@ -2314,8 +2302,22 @@ pub async fn run_maker_swap(swap: RunMakerSwapInput, ctx: MmArc) { error!("[swap uuid={uuid_str}] {event:?}"); } - status.status(swap_tags!(), &event.status_str()); + let event_status_str = event.status_str(); running_swap.apply_event(event); + + // Send a notification to the swap status streamer about a new event. + ctx.event_stream_manager + .send_fn(&SwapStatusStreamer::derive_streamer_id(()), || { + SwapStatusEvent::MakerV1 { + uuid: running_swap.uuid, + event: to_save.clone(), + } + }) + .ok(); + save_my_maker_swap_event(&ctx, &running_swap, to_save) + .await + .expect("!save_my_maker_swap_event"); + status.status(swap_tags!(), &event_status_str); } match res.0 { Some(c) => { @@ -2599,8 +2601,8 @@ mod maker_swap_tests { }); TestCoin::search_for_swap_tx_spend_my .mock_safe(|_, _| MockResult::Return(Box::pin(futures::future::ready(Ok(None))))); - let maker_coin = MmCoinEnum::Test(TestCoin::default()); - let taker_coin = MmCoinEnum::Test(TestCoin::default()); + let maker_coin = MmCoinEnum::TestVariant(TestCoin::default()); + let taker_coin = MmCoinEnum::TestVariant(TestCoin::default()); let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); let actual = block_on(maker_swap.recover_funds()).unwrap(); let expected = RecoveredSwap { @@ -2636,8 +2638,8 @@ mod maker_swap_tests { TestCoin::search_for_swap_tx_spend_my .mock_safe(|_, _| MockResult::Return(Box::pin(futures::future::ready(Ok(None))))); - let maker_coin = MmCoinEnum::Test(TestCoin::default()); - let taker_coin = MmCoinEnum::Test(TestCoin::default()); + let maker_coin = MmCoinEnum::TestVariant(TestCoin::default()); + let taker_coin = MmCoinEnum::TestVariant(TestCoin::default()); let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); let actual = block_on(maker_swap.recover_funds()).unwrap(); let expected = RecoveredSwap { @@ -2665,8 +2667,8 @@ mod maker_swap_tests { eth_tx_for_test().into(), )))))) }); - let maker_coin = MmCoinEnum::Test(TestCoin::default()); - let taker_coin = MmCoinEnum::Test(TestCoin::default()); + let maker_coin = MmCoinEnum::TestVariant(TestCoin::default()); + let taker_coin = MmCoinEnum::TestVariant(TestCoin::default()); let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); assert!(block_on(maker_swap.recover_funds()).is_err()); } @@ -2697,8 +2699,8 @@ mod maker_swap_tests { eth_tx_for_test().into(), )))))) }); - let maker_coin = MmCoinEnum::Test(TestCoin::default()); - let taker_coin = MmCoinEnum::Test(TestCoin::default()); + let maker_coin = MmCoinEnum::TestVariant(TestCoin::default()); + let taker_coin = MmCoinEnum::TestVariant(TestCoin::default()); let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); let err = block_on(maker_swap.recover_funds()).expect_err("Expected an error"); assert!(err.contains("Taker payment was already refunded")); @@ -2727,8 +2729,8 @@ mod maker_swap_tests { .mock_safe(|_, _| MockResult::Return(Box::pin(futures::future::ok(CanRefundHtlc::HaveToWait(1000))))); TestCoin::search_for_swap_tx_spend_my .mock_safe(|_, _| MockResult::Return(Box::pin(futures::future::ready(Ok(None))))); - let maker_coin = MmCoinEnum::Test(TestCoin::default()); - let taker_coin = MmCoinEnum::Test(TestCoin::default()); + let maker_coin = MmCoinEnum::TestVariant(TestCoin::default()); + let taker_coin = MmCoinEnum::TestVariant(TestCoin::default()); let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); let error = block_on(maker_swap.recover_funds()).unwrap_err(); assert!(error.contains("Too early to refund")); @@ -2752,8 +2754,8 @@ mod maker_swap_tests { MY_PAYMENT_SENT_CALLED.store(true, Ordering::Relaxed); MockResult::Return(Box::pin(futures::future::ok(None))) }); - let maker_coin = MmCoinEnum::Test(TestCoin::default()); - let taker_coin = MmCoinEnum::Test(TestCoin::default()); + let maker_coin = MmCoinEnum::TestVariant(TestCoin::default()); + let taker_coin = MmCoinEnum::TestVariant(TestCoin::default()); let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); assert!(block_on(maker_swap.recover_funds()).is_err()); assert!(MY_PAYMENT_SENT_CALLED.load(Ordering::Relaxed)); @@ -2769,8 +2771,8 @@ mod maker_swap_tests { TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); - let maker_coin = MmCoinEnum::Test(TestCoin::default()); - let taker_coin = MmCoinEnum::Test(TestCoin::default()); + let maker_coin = MmCoinEnum::TestVariant(TestCoin::default()); + let taker_coin = MmCoinEnum::TestVariant(TestCoin::default()); let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); assert!(block_on(maker_swap.recover_funds()).is_err()); } @@ -2802,8 +2804,8 @@ mod maker_swap_tests { )))))) }); - let maker_coin = MmCoinEnum::Test(TestCoin::default()); - let taker_coin = MmCoinEnum::Test(TestCoin::default()); + let maker_coin = MmCoinEnum::TestVariant(TestCoin::default()); + let taker_coin = MmCoinEnum::TestVariant(TestCoin::default()); let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); let err = block_on(maker_swap.recover_funds()).expect_err("Expected an error"); assert!(err.contains("Taker payment was already spent")); @@ -2821,8 +2823,8 @@ mod maker_swap_tests { TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); - let maker_coin = MmCoinEnum::Test(TestCoin::default()); - let taker_coin = MmCoinEnum::Test(TestCoin::default()); + let maker_coin = MmCoinEnum::TestVariant(TestCoin::default()); + let taker_coin = MmCoinEnum::TestVariant(TestCoin::default()); let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); assert!(block_on(maker_swap.recover_funds()).is_err()); } @@ -2860,8 +2862,8 @@ mod maker_swap_tests { MockResult::Return(Box::pin(futures::future::ready(Ok(eth_tx_for_test().into())))) }); - let maker_coin = MmCoinEnum::Test(TestCoin::default()); - let taker_coin = MmCoinEnum::Test(TestCoin::default()); + let maker_coin = MmCoinEnum::TestVariant(TestCoin::default()); + let taker_coin = MmCoinEnum::TestVariant(TestCoin::default()); let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); let expected = Ok(RecoveredSwap { coin: "ticker".into(), @@ -2904,8 +2906,8 @@ mod maker_swap_tests { MockResult::Return(Box::pin(futures::future::ok(eth_tx_for_test().into()))) }); - let maker_coin = MmCoinEnum::Test(TestCoin::default()); - let taker_coin = MmCoinEnum::Test(TestCoin::default()); + let maker_coin = MmCoinEnum::TestVariant(TestCoin::default()); + let taker_coin = MmCoinEnum::TestVariant(TestCoin::default()); let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); let err = block_on(maker_swap.recover_funds()).unwrap_err(); assert!(err.contains("Taker payment spend transaction has been sent and confirmed")); @@ -2925,8 +2927,8 @@ mod maker_swap_tests { TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); - let maker_coin = MmCoinEnum::Test(TestCoin::default()); - let taker_coin = MmCoinEnum::Test(TestCoin::default()); + let maker_coin = MmCoinEnum::TestVariant(TestCoin::default()); + let taker_coin = MmCoinEnum::TestVariant(TestCoin::default()); let (_maker_swap, _) = MakerSwap::load_from_saved(ctx.clone(), maker_coin, taker_coin, maker_saved_swap).unwrap(); @@ -2948,8 +2950,8 @@ mod maker_swap_tests { SWAP_CONTRACT_ADDRESS_CALLED.fetch_add(1, Ordering::Relaxed); MockResult::Return(Some(BytesJson::default())) }); - let maker_coin = MmCoinEnum::Test(TestCoin::default()); - let taker_coin = MmCoinEnum::Test(TestCoin::default()); + let maker_coin = MmCoinEnum::TestVariant(TestCoin::default()); + let taker_coin = MmCoinEnum::TestVariant(TestCoin::default()); let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); assert_eq!(SWAP_CONTRACT_ADDRESS_CALLED.load(Ordering::Relaxed), 2); @@ -2977,8 +2979,8 @@ mod maker_swap_tests { SWAP_CONTRACT_ADDRESS_CALLED.fetch_add(1, Ordering::Relaxed); MockResult::Return(Some(BytesJson::default())) }); - let maker_coin = MmCoinEnum::Test(TestCoin::default()); - let taker_coin = MmCoinEnum::Test(TestCoin::default()); + let maker_coin = MmCoinEnum::TestVariant(TestCoin::default()); + let taker_coin = MmCoinEnum::TestVariant(TestCoin::default()); let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); assert_eq!(SWAP_CONTRACT_ADDRESS_CALLED.load(Ordering::Relaxed), 1); diff --git a/mm2src/mm2_main/src/lp_swap/recreate_swap_data.rs b/mm2src/mm2_main/src/lp_swap/recreate_swap_data.rs index f05da8d59c..7ae7deb3e1 100644 --- a/mm2src/mm2_main/src/lp_swap/recreate_swap_data.rs +++ b/mm2src/mm2_main/src/lp_swap/recreate_swap_data.rs @@ -585,7 +585,7 @@ mod tests { let ctx = MmCtxBuilder::default().into_mm_arc(); let coins_ctx = CoinsContext::from_ctx(&ctx).unwrap(); - block_on(coins_ctx.add_token(MmCoinEnum::Test(TestCoin::new("RICK")))).unwrap(); + block_on(coins_ctx.add_token(MmCoinEnum::TestVariant(TestCoin::new("RICK")))).unwrap(); let taker_actual_swap = block_on(recreate_taker_swap(ctx, maker_saved_swap)).expect("!recreate_maker_swap"); println!("{}", json::to_string(&taker_actual_swap).unwrap()); @@ -606,7 +606,7 @@ mod tests { let ctx = MmCtxBuilder::default().into_mm_arc(); let coins_ctx = CoinsContext::from_ctx(&ctx).unwrap(); - block_on(coins_ctx.add_token(MmCoinEnum::Test(TestCoin::new("RICK")))).unwrap(); + block_on(coins_ctx.add_token(MmCoinEnum::TestVariant(TestCoin::new("RICK")))).unwrap(); let taker_actual_swap = block_on(recreate_taker_swap(ctx, maker_saved_swap)).expect("!recreate_maker_swap"); println!("{}", json::to_string(&taker_actual_swap).unwrap()); diff --git a/mm2src/mm2_main/src/lp_swap/swap_v2_common.rs b/mm2src/mm2_main/src/lp_swap/swap_v2_common.rs index 16d3bfe7a1..73c6d9425a 100644 --- a/mm2src/mm2_main/src/lp_swap/swap_v2_common.rs +++ b/mm2src/mm2_main/src/lp_swap/swap_v2_common.rs @@ -507,16 +507,16 @@ pub(super) async fn swap_kickstart_handler_for_maker( ) { if let Some((maker_coin, taker_coin)) = swap_kickstart_coins(&ctx, &swap_repr, &uuid).await { match (maker_coin, taker_coin) { - (MmCoinEnum::UtxoCoin(m), MmCoinEnum::UtxoCoin(t)) => { + (MmCoinEnum::UtxoCoinVariant(m), MmCoinEnum::UtxoCoinVariant(t)) => { swap_kickstart_handler::, _, _>(swap_repr, storage, uuid, m, t).await }, - (MmCoinEnum::EthCoin(m), MmCoinEnum::EthCoin(t)) => { + (MmCoinEnum::EthCoinVariant(m), MmCoinEnum::EthCoinVariant(t)) => { swap_kickstart_handler::, _, _>(swap_repr, storage, uuid, m, t).await }, - (MmCoinEnum::UtxoCoin(m), MmCoinEnum::EthCoin(t)) => { + (MmCoinEnum::UtxoCoinVariant(m), MmCoinEnum::EthCoinVariant(t)) => { swap_kickstart_handler::, _, _>(swap_repr, storage, uuid, m, t).await }, - (MmCoinEnum::EthCoin(m), MmCoinEnum::UtxoCoin(t)) => { + (MmCoinEnum::EthCoinVariant(m), MmCoinEnum::UtxoCoinVariant(t)) => { swap_kickstart_handler::, _, _>(swap_repr, storage, uuid, m, t).await }, _ => { @@ -538,16 +538,16 @@ pub(super) async fn swap_kickstart_handler_for_taker( ) { if let Some((maker_coin, taker_coin)) = swap_kickstart_coins(&ctx, &swap_repr, &uuid).await { match (maker_coin, taker_coin) { - (MmCoinEnum::UtxoCoin(m), MmCoinEnum::UtxoCoin(t)) => { + (MmCoinEnum::UtxoCoinVariant(m), MmCoinEnum::UtxoCoinVariant(t)) => { swap_kickstart_handler::, _, _>(swap_repr, storage, uuid, m, t).await }, - (MmCoinEnum::EthCoin(m), MmCoinEnum::EthCoin(t)) => { + (MmCoinEnum::EthCoinVariant(m), MmCoinEnum::EthCoinVariant(t)) => { swap_kickstart_handler::, _, _>(swap_repr, storage, uuid, m, t).await }, - (MmCoinEnum::UtxoCoin(m), MmCoinEnum::EthCoin(t)) => { + (MmCoinEnum::UtxoCoinVariant(m), MmCoinEnum::EthCoinVariant(t)) => { swap_kickstart_handler::, _, _>(swap_repr, storage, uuid, m, t).await }, - (MmCoinEnum::EthCoin(m), MmCoinEnum::UtxoCoin(t)) => { + (MmCoinEnum::EthCoinVariant(m), MmCoinEnum::UtxoCoinVariant(t)) => { swap_kickstart_handler::, _, _>(swap_repr, storage, uuid, m, t).await }, _ => { diff --git a/mm2src/mm2_main/src/lp_swap/taker_swap.rs b/mm2src/mm2_main/src/lp_swap/taker_swap.rs index 9484f0c811..7a31ee7d0a 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap.rs @@ -480,18 +480,6 @@ pub async fn run_taker_swap(swap: RunTakerSwapInput, ctx: MmArc) { event: event.clone(), }; - // Send a notification to the swap status streamer about a new event. - ctx.event_stream_manager - .send_fn(&SwapStatusStreamer::derive_streamer_id(()), || { - SwapStatusEvent::TakerV1 { - uuid: running_swap.uuid, - event: to_save.clone(), - } - }) - .ok(); - save_my_taker_swap_event(&ctx, &running_swap, to_save) - .await - .expect("!save_my_taker_swap_event"); if event.should_ban_maker() { ban_pubkey_on_failed_swap( &ctx, @@ -505,8 +493,22 @@ pub async fn run_taker_swap(swap: RunTakerSwapInput, ctx: MmArc) { error!("[swap uuid={uuid_str}] {event:?}"); } - status.status(&[&"swap", &("uuid", uuid_str.as_str())], &event.status_str()); + let event_status_str = event.status_str(); running_swap.apply_event(event); + + // Send a notification to the swap status streamer about a new event. + ctx.event_stream_manager + .send_fn(&SwapStatusStreamer::derive_streamer_id(()), || { + SwapStatusEvent::TakerV1 { + uuid: running_swap.uuid, + event: to_save.clone(), + } + }) + .ok(); + save_my_taker_swap_event(&ctx, &running_swap, to_save) + .await + .expect("!save_my_taker_swap_event"); + status.status(&[&"swap", &("uuid", uuid_str.as_str())], &event_status_str); } match res.0 { Some(c) => { @@ -3014,8 +3016,8 @@ mod taker_swap_tests { }); TestCoin::search_for_swap_tx_spend_other .mock_safe(|_, _| MockResult::Return(Box::pin(futures::future::ready(Ok(None))))); - let maker_coin = MmCoinEnum::Test(TestCoin::default()); - let taker_coin = MmCoinEnum::Test(TestCoin::default()); + let maker_coin = MmCoinEnum::TestVariant(TestCoin::default()); + let taker_coin = MmCoinEnum::TestVariant(TestCoin::default()); let (taker_swap, _) = block_on(TakerSwap::load_from_saved( ctx, maker_coin, @@ -3062,8 +3064,8 @@ mod taker_swap_tests { TAKER_PAYMENT_REFUND_CALLED.store(true, Ordering::Relaxed); MockResult::Return(Box::pin(futures::future::ok(eth_tx_for_test().into()))) }); - let maker_coin = MmCoinEnum::Test(TestCoin::default()); - let taker_coin = MmCoinEnum::Test(TestCoin::default()); + let maker_coin = MmCoinEnum::TestVariant(TestCoin::default()); + let taker_coin = MmCoinEnum::TestVariant(TestCoin::default()); let (taker_swap, _) = block_on(TakerSwap::load_from_saved( ctx, maker_coin, @@ -3115,8 +3117,8 @@ mod taker_swap_tests { MAKER_PAYMENT_SPEND_CALLED.store(true, Ordering::Relaxed); MockResult::Return(Box::pin(futures::future::ready(Ok(eth_tx_for_test().into())))) }); - let maker_coin = MmCoinEnum::Test(TestCoin::default()); - let taker_coin = MmCoinEnum::Test(TestCoin::default()); + let maker_coin = MmCoinEnum::TestVariant(TestCoin::default()); + let taker_coin = MmCoinEnum::TestVariant(TestCoin::default()); let (taker_swap, _) = block_on(TakerSwap::load_from_saved( ctx, maker_coin, @@ -3159,8 +3161,8 @@ mod taker_swap_tests { REFUND_CALLED.store(true, Ordering::Relaxed); MockResult::Return(Box::pin(futures::future::ok(eth_tx_for_test().into()))) }); - let maker_coin = MmCoinEnum::Test(TestCoin::default()); - let taker_coin = MmCoinEnum::Test(TestCoin::default()); + let maker_coin = MmCoinEnum::TestVariant(TestCoin::default()); + let taker_coin = MmCoinEnum::TestVariant(TestCoin::default()); let (taker_swap, _) = block_on(TakerSwap::load_from_saved( ctx, maker_coin, @@ -3196,8 +3198,8 @@ mod taker_swap_tests { SEARCH_TX_SPEND_CALLED.store(true, Ordering::Relaxed); MockResult::Return(Box::pin(futures::future::ready(Ok(None)))) }); - let maker_coin = MmCoinEnum::Test(TestCoin::default()); - let taker_coin = MmCoinEnum::Test(TestCoin::default()); + let maker_coin = MmCoinEnum::TestVariant(TestCoin::default()); + let taker_coin = MmCoinEnum::TestVariant(TestCoin::default()); let (taker_swap, _) = block_on(TakerSwap::load_from_saved( ctx, maker_coin, @@ -3236,8 +3238,8 @@ mod taker_swap_tests { MAKER_PAYMENT_SPEND_CALLED.store(true, Ordering::Relaxed); MockResult::Return(Box::pin(futures::future::ready(Ok(eth_tx_for_test().into())))) }); - let maker_coin = MmCoinEnum::Test(TestCoin::default()); - let taker_coin = MmCoinEnum::Test(TestCoin::default()); + let maker_coin = MmCoinEnum::TestVariant(TestCoin::default()); + let taker_coin = MmCoinEnum::TestVariant(TestCoin::default()); let (taker_swap, _) = block_on(TakerSwap::load_from_saved( ctx, maker_coin, @@ -3266,8 +3268,8 @@ mod taker_swap_tests { TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); - let maker_coin = MmCoinEnum::Test(TestCoin::default()); - let taker_coin = MmCoinEnum::Test(TestCoin::default()); + let maker_coin = MmCoinEnum::TestVariant(TestCoin::default()); + let taker_coin = MmCoinEnum::TestVariant(TestCoin::default()); let (taker_swap, _) = block_on(TakerSwap::load_from_saved( ctx, maker_coin, @@ -3307,8 +3309,8 @@ mod taker_swap_tests { SWAP_CONTRACT_ADDRESS_CALLED.fetch_add(1, Ordering::Relaxed); MockResult::Return(Some(BytesJson::default())) }); - let maker_coin = MmCoinEnum::Test(TestCoin::default()); - let taker_coin = MmCoinEnum::Test(TestCoin::default()); + let maker_coin = MmCoinEnum::TestVariant(TestCoin::default()); + let taker_coin = MmCoinEnum::TestVariant(TestCoin::default()); let (taker_swap, _) = block_on(TakerSwap::load_from_saved( ctx, maker_coin, @@ -3342,8 +3344,8 @@ mod taker_swap_tests { SWAP_CONTRACT_ADDRESS_CALLED.fetch_add(1, Ordering::Relaxed); MockResult::Return(Some(BytesJson::default())) }); - let maker_coin = MmCoinEnum::Test(TestCoin::default()); - let taker_coin = MmCoinEnum::Test(TestCoin::default()); + let maker_coin = MmCoinEnum::TestVariant(TestCoin::default()); + let taker_coin = MmCoinEnum::TestVariant(TestCoin::default()); let (taker_swap, _) = block_on(TakerSwap::load_from_saved( ctx, maker_coin, @@ -3514,8 +3516,8 @@ mod taker_swap_tests { let ctx = mm_ctx_with_iguana(PASSPHRASE); - let maker_coin = MmCoinEnum::Test(TestCoin::new("RICK")); - let taker_coin = MmCoinEnum::Test(TestCoin::new("MORTY")); + let maker_coin = MmCoinEnum::TestVariant(TestCoin::new("RICK")); + let taker_coin = MmCoinEnum::TestVariant(TestCoin::new("MORTY")); TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(BigDecimal::from(0))); diff --git a/mm2src/mm2_main/src/ordermatch_tests.rs b/mm2src/mm2_main/src/ordermatch_tests.rs index 56915a4e25..7d53976c12 100644 --- a/mm2src/mm2_main/src/ordermatch_tests.rs +++ b/mm2src/mm2_main/src/ordermatch_tests.rs @@ -391,7 +391,7 @@ fn test_match_maker_order_and_taker_request() { // https://github.com/KomodoPlatform/atomicDEX-API/pull/739#discussion_r517275495 #[test] fn maker_order_match_with_request_zero_volumes() { - let coin = MmCoinEnum::Test(TestCoin::default()); + let coin = MmCoinEnum::TestVariant(TestCoin::default()); let maker_order = MakerOrderBuilder::new(&coin, &coin) .with_max_base_vol(1.into()) @@ -2407,7 +2407,7 @@ fn test_taker_request_can_match_with_maker_pubkey() { #[test] fn test_taker_request_can_match_with_uuid() { let uuid = new_uuid(); - let coin = MmCoinEnum::Test(TestCoin::default()); + let coin = MmCoinEnum::TestVariant(TestCoin::default()); // default has MatchBy::Any let mut order = TakerOrderBuilder::new(&coin, &coin).build_unchecked(); diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index c51e9ef27b..d2f46f6d88 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -55,7 +55,6 @@ use coins::rpc_command::{ init_withdraw::{cancel_withdraw, init_withdraw, withdraw_status, withdraw_user_action}, offline_keys::get_private_keys, }; -#[cfg(feature = "enable-sia")] use coins::siacoin::SiaCoin; use coins::tendermint::{TendermintCoin, TendermintToken}; use coins::utxo::bch::BchCoin; @@ -248,6 +247,7 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, enable_platform_coin_with_tokens::).await, "enable_erc20" => handle_mmrpc(ctx, request, enable_token::).await, "enable_nft" => handle_mmrpc(ctx, request, enable_token::).await, + "enable_sia" => handle_mmrpc(ctx, request, init_standalone_coin::).await, "enable_tendermint_with_assets" => { handle_mmrpc(ctx, request, enable_platform_coin_with_tokens::).await }, @@ -387,12 +387,10 @@ async fn rpc_task_dispatcher( "withdraw::init" => handle_mmrpc(ctx, request, init_withdraw).await, "withdraw::status" => handle_mmrpc(ctx, request, withdraw_status).await, "withdraw::user_action" => handle_mmrpc(ctx, request, withdraw_user_action).await, - //"enable_sia::cancel" => handle_mmrpc(ctx, request, cancel_init_standalone_coin::).await, - #[cfg(feature = "enable-sia")] + "enable_sia::cancel" => handle_mmrpc(ctx, request, cancel_init_standalone_coin::).await, "enable_sia::init" => handle_mmrpc(ctx, request, init_standalone_coin::).await, - #[cfg(feature = "enable-sia")] "enable_sia::status" => handle_mmrpc(ctx, request, init_standalone_coin_status::).await, - //"enable_sia::user_action" => handle_mmrpc(ctx, request, init_standalone_coin_user_action::).await, + "enable_sia::user_action" => handle_mmrpc(ctx, request, init_standalone_coin_user_action::).await, "enable_z_coin::init" => handle_mmrpc(ctx, request, init_standalone_coin::).await, "enable_z_coin::cancel" => handle_mmrpc(ctx, request, cancel_init_standalone_coin::).await, "enable_z_coin::status" => handle_mmrpc(ctx, request, init_standalone_coin_status::).await, diff --git a/mm2src/mm2_main/src/rpc/lp_commands/one_inch/rpcs.rs b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/rpcs.rs index 0b1738af1f..820ce2c580 100644 --- a/mm2src/mm2_main/src/rpc/lp_commands/one_inch/rpcs.rs +++ b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/rpcs.rs @@ -158,7 +158,7 @@ pub(crate) async fn get_coin_for_one_inch( ticker: &Ticker, ) -> MmResult<(EthCoin, Address), ApiIntegrationRpcError> { let coin = match lp_coinfind_or_err(ctx, ticker).await.map_mm_err()? { - MmCoinEnum::EthCoin(coin) => coin, + MmCoinEnum::EthCoinVariant(coin) => coin, _ => return Err(MmError::new(ApiIntegrationRpcError::CoinTypeError)), }; let contract = match coin.coin_type { diff --git a/mm2src/mm2_main/src/rpc/lp_commands/tokens.rs b/mm2src/mm2_main/src/rpc/lp_commands/tokens.rs index e31a20b371..f2664b8dbb 100644 --- a/mm2src/mm2_main/src/rpc/lp_commands/tokens.rs +++ b/mm2src/mm2_main/src/rpc/lp_commands/tokens.rs @@ -77,7 +77,7 @@ pub async fn get_token_info(ctx: MmArc, req: TokenInfoRequest) -> MmResult { + MmCoinEnum::EthCoinVariant(eth_coin) => { let contract_address_str = req.protocol .contract_address() @@ -166,7 +166,7 @@ pub async fn approve_token_rpc(ctx: MmArc, req: Erc20ApproveRequest) -> MmResult async fn find_erc20_eth_coin(ctx: &MmArc, coin: &str) -> Result> { match lp_coinfind_or_err(ctx, coin).await { - Ok(MmCoinEnum::EthCoin(eth_coin)) => Ok(eth_coin), + Ok(MmCoinEnum::EthCoinVariant(eth_coin)) => Ok(eth_coin), Ok(_) => Err(MmError::new(Erc20CallError::CoinNotSupported { coin: coin.to_string(), })), diff --git a/mm2src/mm2_main/src/rpc/streaming_activations/balance.rs b/mm2src/mm2_main/src/rpc/streaming_activations/balance.rs index a06b4ea1a1..d6a5ac8dc3 100644 --- a/mm2src/mm2_main/src/rpc/streaming_activations/balance.rs +++ b/mm2src/mm2_main/src/rpc/streaming_activations/balance.rs @@ -52,12 +52,12 @@ pub async fn enable_balance( .ok_or(BalanceStreamingRequestError::CoinNotFound)?; match coin { - MmCoinEnum::EthCoin(_) => (), - MmCoinEnum::ZCoin(_) - | MmCoinEnum::UtxoCoin(_) - | MmCoinEnum::Bch(_) - | MmCoinEnum::QtumCoin(_) - | MmCoinEnum::Tendermint(_) => { + MmCoinEnum::EthCoinVariant(_) => (), + MmCoinEnum::ZCoinVariant(_) + | MmCoinEnum::UtxoCoinVariant(_) + | MmCoinEnum::BchVariant(_) + | MmCoinEnum::QtumCoinVariant(_) + | MmCoinEnum::TendermintVariant(_) => { if req.config.is_some() { Err(BalanceStreamingRequestError::EnableError( "Invalid config provided. No config needed".to_string(), @@ -68,28 +68,28 @@ pub async fn enable_balance( } let enable_result = match coin { - MmCoinEnum::UtxoCoin(coin) => { + MmCoinEnum::UtxoCoinVariant(coin) => { let streamer = UtxoBalanceEventStreamer::new(coin.clone().into()); ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await }, - MmCoinEnum::Bch(coin) => { + MmCoinEnum::BchVariant(coin) => { let streamer = UtxoBalanceEventStreamer::new(coin.clone().into()); ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await }, - MmCoinEnum::QtumCoin(coin) => { + MmCoinEnum::QtumCoinVariant(coin) => { let streamer = UtxoBalanceEventStreamer::new(coin.clone().into()); ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await }, - MmCoinEnum::EthCoin(coin) => { + MmCoinEnum::EthCoinVariant(coin) => { let streamer = EthBalanceEventStreamer::try_new(req.config, coin.clone()) .map_to_mm(|e| BalanceStreamingRequestError::EnableError(format!("{e:?}")))?; ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await }, - MmCoinEnum::ZCoin(coin) => { + MmCoinEnum::ZCoinVariant(coin) => { let streamer = ZCoinBalanceEventStreamer::new(coin.clone()); ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await }, - MmCoinEnum::Tendermint(coin) => { + MmCoinEnum::TendermintVariant(coin) => { let streamer = TendermintBalanceEventStreamer::new(coin.clone()); ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await }, diff --git a/mm2src/mm2_main/src/rpc/streaming_activations/fee_estimation.rs b/mm2src/mm2_main/src/rpc/streaming_activations/fee_estimation.rs index 9ff19d8020..435b1d455f 100644 --- a/mm2src/mm2_main/src/rpc/streaming_activations/fee_estimation.rs +++ b/mm2src/mm2_main/src/rpc/streaming_activations/fee_estimation.rs @@ -46,7 +46,7 @@ pub async fn enable_fee_estimation( .ok_or(FeeStreamingRequestError::CoinNotFound)?; match coin { - MmCoinEnum::EthCoin(coin) => { + MmCoinEnum::EthCoinVariant(coin) => { let eth_fee_estimator_streamer = EthFeeEventStreamer::new(req.config, coin.clone()); ctx.event_stream_manager .add(client_id, eth_fee_estimator_streamer, coin.spawner()) diff --git a/mm2src/mm2_main/src/rpc/streaming_activations/tx_history.rs b/mm2src/mm2_main/src/rpc/streaming_activations/tx_history.rs index 10bb9d1786..055d5726a8 100644 --- a/mm2src/mm2_main/src/rpc/streaming_activations/tx_history.rs +++ b/mm2src/mm2_main/src/rpc/streaming_activations/tx_history.rs @@ -47,25 +47,25 @@ pub async fn enable_tx_history( .ok_or(TxHistoryStreamingRequestError::CoinNotFound)?; let enable_result = match coin { - MmCoinEnum::UtxoCoin(coin) => { + MmCoinEnum::UtxoCoinVariant(coin) => { let streamer = TxHistoryEventStreamer::new(req.coin); ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await }, - MmCoinEnum::Bch(coin) => { + MmCoinEnum::BchVariant(coin) => { let streamer = TxHistoryEventStreamer::new(req.coin); ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await }, - MmCoinEnum::QtumCoin(coin) => { + MmCoinEnum::QtumCoinVariant(coin) => { let streamer = TxHistoryEventStreamer::new(req.coin); ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await }, - MmCoinEnum::Tendermint(coin) => { + MmCoinEnum::TendermintVariant(coin) => { // The tx history streamer is very primitive reactive streamer that only emits new txs. // it's logic is exactly the same for utxo coins and tendermint coins as well. let streamer = TxHistoryEventStreamer::new(req.coin); ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await }, - MmCoinEnum::ZCoin(coin) => { + MmCoinEnum::ZCoinVariant(coin) => { let streamer = ZCoinTxHistoryEventStreamer::new(coin.clone()); ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await }, diff --git a/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs b/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs index 3e27bb0b72..514532f31f 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs @@ -1,12 +1,3 @@ -use coins::z_coin::ZCoin; -pub use common::{block_on, block_on_f01, now_ms, now_sec, wait_until_ms, wait_until_sec}; -pub use mm2_number::MmNumber; -use mm2_rpc::data::legacy::BalanceResponse; -pub use mm2_test_helpers::for_tests::{ - check_my_swap_status, check_recent_swaps, enable_eth_coin, enable_native, enable_native_bch, erc20_dev_conf, - eth_dev_conf, mm_dump, wait_check_stats_swap_status, MarketMakerIt, -}; - use super::eth_docker_tests::{erc20_contract_checksum, fill_eth, fill_eth_erc20_with_private_key, swap_contract}; use super::z_coin_docker_tests::z_coin_from_spending_key; use bitcrypto::{dhash160, ChecksumType}; @@ -24,7 +15,11 @@ use coins::utxo::{ coin_daemon_data_dir, sat_from_big_decimal, zcash_params_path, UtxoActivationParams, UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps, }; +use coins::z_coin::ZCoin; use coins::{ConfirmPaymentInput, MarketCoinOps, Transaction}; +use common::executor::Timer; +use common::Future01CompatExt; +pub use common::{block_on, block_on_f01, now_ms, now_sec, wait_until_ms, wait_until_sec}; use crypto::privkey::{key_pair_from_secret, key_pair_from_seed}; use crypto::Secp256k1Secret; use ethabi::Token; @@ -37,6 +32,12 @@ use keys::{ }; use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; use mm2_number::BigDecimal; +pub use mm2_number::MmNumber; +use mm2_rpc::data::legacy::BalanceResponse; +pub use mm2_test_helpers::for_tests::{ + check_my_swap_status, check_recent_swaps, enable_eth_coin, enable_native, enable_native_bch, erc20_dev_conf, + eth_dev_conf, mm_dump, wait_check_stats_swap_status, MarketMakerIt, +}; use mm2_test_helpers::get_passphrase; use mm2_test_helpers::structs::TransactionDetails; use primitives::hash::{H160, H256}; @@ -51,18 +52,20 @@ use std::process::{Command, Stdio}; use std::str::FromStr; pub use std::{env, thread}; use std::{path::PathBuf, sync::Mutex, time::Duration}; -use testcontainers::{clients::Cli, core::WaitFor, Container, GenericImage, RunnableImage}; +use testcontainers::core::Mount; +use testcontainers::runners::SyncRunner; +use testcontainers::{core::WaitFor, Container, GenericImage, RunnableImage}; +use tokio::sync::Mutex as AsyncMutex; #[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] use web3::types::Address as EthAddress; use web3::types::{BlockId, BlockNumber, TransactionRequest}; use web3::{transports::Http, Web3}; lazy_static! { - static ref MY_COIN_LOCK: Mutex<()> = Mutex::new(()); - static ref MY_COIN1_LOCK: Mutex<()> = Mutex::new(()); - static ref QTUM_LOCK: Mutex<()> = Mutex::new(()); - static ref FOR_SLP_LOCK: Mutex<()> = Mutex::new(()); - static ref ZOMBIE_LOCK: Mutex<()> = Mutex::new(()); + static ref MY_COIN_LOCK: AsyncMutex<()> = AsyncMutex::new(()); + static ref MY_COIN1_LOCK: AsyncMutex<()> = AsyncMutex::new(()); + static ref QTUM_LOCK: AsyncMutex<()> = AsyncMutex::new(()); + static ref FOR_SLP_LOCK: AsyncMutex<()> = AsyncMutex::new(()); pub static ref SLP_TOKEN_ID: Mutex = Mutex::new(H256::default()); // Private keys supplied with 1000 SLP tokens on tests initialization. // Due to the SLP protocol limitations only 19 outputs (18 + change) can be sent in one transaction, which is sufficient for now though. @@ -121,6 +124,8 @@ pub static mut SEPOLIA_ETOMIC_MAKER_NFT_SWAP_V2: H160Eth = H160Eth::zero(); pub static GETH_RPC_URL: &str = "http://127.0.0.1:8545"; #[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] pub static SEPOLIA_RPC_URL: &str = "https://ethereum-sepolia-rpc.publicnode.com"; +/// SIA daemon RPC connection parameters +pub static SIA_RPC_PARAMS: (&str, u16, &str) = ("127.0.0.1", 9980, "password"); // use thread local to affect only the current running test thread_local! { @@ -135,10 +140,8 @@ pub const GETH_DOCKER_IMAGE_WITH_TAG: &str = "docker.io/ethereum/client-go:stabl pub const ZOMBIE_ASSET_DOCKER_IMAGE: &str = "docker.io/borngraced/zombietestrunner"; pub const ZOMBIE_ASSET_DOCKER_IMAGE_WITH_TAG: &str = "docker.io/borngraced/zombietestrunner:multiarch"; -#[allow(dead_code)] -pub const SIA_DOCKER_IMAGE: &str = "docker.io/alrighttt/walletd-komodo"; -#[allow(dead_code)] -pub const SIA_DOCKER_IMAGE_WITH_TAG: &str = "docker.io/alrighttt/walletd-komodo:latest"; +pub const SIA_DOCKER_IMAGE: &str = "ghcr.io/siafoundation/walletd"; +pub const SIA_DOCKER_IMAGE_WITH_TAG: &str = "ghcr.io/siafoundation/walletd:latest"; pub const NUCLEUS_IMAGE: &str = "docker.io/komodoofficial/nucleusd"; pub const ATOM_IMAGE_WITH_TAG: &str = "docker.io/komodoofficial/gaiad:kdf-ci"; @@ -371,9 +374,9 @@ impl CoinDockerOps for BchDockerOps { } } -pub struct DockerNode<'a> { +pub struct DockerNode { #[allow(dead_code)] - pub container: Container<'a, GenericImage>, + pub container: Container, #[allow(dead_code)] pub ticker: String, #[allow(dead_code)] @@ -385,9 +388,12 @@ pub fn random_secp256k1_secret() -> Secp256k1Secret { Secp256k1Secret::from(*priv_key.as_ref()) } -pub fn utxo_asset_docker_node<'a>(docker: &'a Cli, ticker: &'static str, port: u16) -> DockerNode<'a> { +pub fn utxo_asset_docker_node(ticker: &'static str, port: u16) -> DockerNode { let image = GenericImage::new(UTXO_ASSET_DOCKER_IMAGE, "multiarch") - .with_volume(zcash_params_path().display().to_string(), "/root/.zcash-params") + .with_mount(Mount::bind_mount( + zcash_params_path().display().to_string(), + "/root/.zcash-params", + )) .with_env_var("CLIENTS", "2") .with_env_var("CHAIN", ticker) .with_env_var("TEST_ADDY", "R9imXLs1hEcU9KbFDQq2hJEEJ1P5UoekaF") @@ -401,7 +407,7 @@ pub fn utxo_asset_docker_node<'a>(docker: &'a Cli, ticker: &'static str, port: u .with_env_var("COIN_RPC_PORT", port.to_string()) .with_wait_for(WaitFor::message_on_stdout("config is ready")); let image = RunnableImage::from(image).with_mapped_port((port, port)); - let container = docker.run(image); + let container = image.start().expect("Failed to start UTXO asset docker node"); let mut conf_path = coin_daemon_data_dir(ticker, true); std::fs::create_dir_all(&conf_path).unwrap(); conf_path.push(format!("{ticker}.conf")); @@ -425,11 +431,11 @@ pub fn utxo_asset_docker_node<'a>(docker: &'a Cli, ticker: &'static str, port: u } } -pub fn geth_docker_node<'a>(docker: &'a Cli, ticker: &'static str, port: u16) -> DockerNode<'a> { +pub fn geth_docker_node(ticker: &'static str, port: u16) -> DockerNode { let image = GenericImage::new(GETH_DOCKER_IMAGE, "stable"); let args = vec!["--dev".into(), "--http".into(), "--http.addr=0.0.0.0".into()]; let image = RunnableImage::from((image, args)).with_mapped_port((port, port)); - let container = docker.run(image); + let container = image.start().expect("Failed to start Geth docker node"); DockerNode { container, ticker: ticker.into(), @@ -437,15 +443,38 @@ pub fn geth_docker_node<'a>(docker: &'a Cli, ticker: &'static str, port: u16) -> } } -#[allow(dead_code)] -pub fn sia_docker_node<'a>(docker: &'a Cli, ticker: &'static str, port: u16) -> DockerNode<'a> { - let image = - GenericImage::new(SIA_DOCKER_IMAGE, "latest").with_env_var("WALLETD_API_PASSWORD", "password".to_string()); - let args = vec![]; - let image = RunnableImage::from((image, args)) +pub fn sia_docker_node(ticker: &'static str, port: u16) -> DockerNode { + use crate::sia_tests::utils::{WALLETD_CONFIG, WALLETD_NETWORK_CONFIG}; + + let config_dir = std::env::temp_dir() + .join(format!( + "sia-docker-tests-temp-{}", + chrono::Local::now().format("%Y-%m-%d_%H-%M-%S-%3f") + )) + .join("walletd_config"); + std::fs::create_dir_all(&config_dir).unwrap(); + + // Write walletd.yml + std::fs::write(config_dir.join("walletd.yml"), WALLETD_CONFIG).expect("failed to write walletd.yml"); + + // Write ci_network.json + std::fs::write(config_dir.join("ci_network.json"), WALLETD_NETWORK_CONFIG) + .expect("failed to write ci_network.json"); + + let image = GenericImage::new(SIA_DOCKER_IMAGE, "latest") + .with_env_var("WALLETD_CONFIG_FILE", "/config/walletd.yml") + .with_wait_for(WaitFor::message_on_stdout("node started")) + .with_mount(Mount::bind_mount( + config_dir.to_str().expect("config path is invalid"), + "/config", + )); + + let args = vec!["-network=/config/ci_network.json".to_string(), "-debug".to_string()]; + let image = RunnableImage::from(image) .with_mapped_port((port, port)) - .with_container_name("sia-docker"); - let container = docker.run(image); + .with_args(args); + + let container = image.start().expect("Failed to start Sia docker node"); DockerNode { container, ticker: ticker.into(), @@ -453,14 +482,16 @@ pub fn sia_docker_node<'a>(docker: &'a Cli, ticker: &'static str, port: u16) -> } } -pub fn nucleus_node(docker: &'_ Cli, runtime_dir: PathBuf) -> DockerNode<'_> { +pub fn nucleus_node(runtime_dir: PathBuf) -> DockerNode { let nucleus_node_runtime_dir = runtime_dir.join("nucleus-testnet-data"); assert!(nucleus_node_runtime_dir.exists()); - let image = GenericImage::new(NUCLEUS_IMAGE, "latest") - .with_volume(nucleus_node_runtime_dir.to_str().unwrap(), "/root/.nucleus"); + let image = GenericImage::new(NUCLEUS_IMAGE, "latest").with_mount(Mount::bind_mount( + nucleus_node_runtime_dir.to_str().unwrap(), + "/root/.nucleus", + )); let image = RunnableImage::from((image, vec![])).with_network("host"); - let container = docker.run(image); + let container = image.start().expect("Failed to start Nucleus docker node"); DockerNode { container, @@ -469,14 +500,17 @@ pub fn nucleus_node(docker: &'_ Cli, runtime_dir: PathBuf) -> DockerNode<'_> { } } -pub fn atom_node(docker: &'_ Cli, runtime_dir: PathBuf) -> DockerNode<'_> { +pub fn atom_node(runtime_dir: PathBuf) -> DockerNode { let atom_node_runtime_dir = runtime_dir.join("atom-testnet-data"); assert!(atom_node_runtime_dir.exists()); let (image, tag) = ATOM_IMAGE_WITH_TAG.rsplit_once(':').unwrap(); - let image = GenericImage::new(image, tag).with_volume(atom_node_runtime_dir.to_str().unwrap(), "/root/.gaia"); + let image = GenericImage::new(image, tag).with_mount(Mount::bind_mount( + atom_node_runtime_dir.to_str().unwrap(), + "/root/.gaia", + )); let image = RunnableImage::from((image, vec![])).with_network("host"); - let container = docker.run(image); + let container = image.start().expect("Failed to start Atom docker node"); DockerNode { container, @@ -485,14 +519,17 @@ pub fn atom_node(docker: &'_ Cli, runtime_dir: PathBuf) -> DockerNode<'_> { } } -pub fn ibc_relayer_node(docker: &'_ Cli, runtime_dir: PathBuf) -> DockerNode<'_> { +pub fn ibc_relayer_node(runtime_dir: PathBuf) -> DockerNode { let relayer_node_runtime_dir = runtime_dir.join("ibc-relayer-data"); assert!(relayer_node_runtime_dir.exists()); let (image, tag) = IBC_RELAYER_IMAGE_WITH_TAG.rsplit_once(':').unwrap(); - let image = GenericImage::new(image, tag).with_volume(relayer_node_runtime_dir.to_str().unwrap(), "/root/.relayer"); + let image = GenericImage::new(image, tag).with_mount(Mount::bind_mount( + relayer_node_runtime_dir.to_str().unwrap(), + "/root/.relayer", + )); let image = RunnableImage::from((image, vec![])).with_network("host"); - let container = docker.run(image); + let container = image.start().expect("Failed to start IBC Relayer docker node"); DockerNode { container, @@ -501,14 +538,17 @@ pub fn ibc_relayer_node(docker: &'_ Cli, runtime_dir: PathBuf) -> DockerNode<'_> } } -pub fn zombie_asset_docker_node(docker: &Cli, port: u16) -> DockerNode<'_> { +pub fn zombie_asset_docker_node(port: u16) -> DockerNode { let image = GenericImage::new(ZOMBIE_ASSET_DOCKER_IMAGE, "multiarch") - .with_volume(zcash_params_path().display().to_string(), "/root/.zcash-params") + .with_mount(Mount::bind_mount( + zcash_params_path().display().to_string(), + "/root/.zcash-params", + )) .with_env_var("COIN_RPC_PORT", port.to_string()) .with_wait_for(WaitFor::message_on_stdout("config is ready")); let image = RunnableImage::from(image).with_mapped_port((port, port)); - let container = docker.run(image); + let container = image.start().expect("Failed to start Zombie asset docker node"); let config_ticker = "ZOMBIE"; let mut conf_path = coin_daemon_data_dir(config_ticker, true); @@ -547,7 +587,7 @@ pub fn get_slp_token_id() -> String { hex::encode(SLP_TOKEN_ID.lock().unwrap().as_slice()) } -pub fn import_address(coin: &T) +pub async fn import_address(coin: &T) where T: MarketCoinOps + AsRef, { @@ -558,12 +598,16 @@ where "FORSLP" => &*FOR_SLP_LOCK, ticker => panic!("Unknown ticker {}", ticker), }; - let _lock = mutex.lock().unwrap(); + let _lock = mutex.lock().await; match coin.as_ref().rpc_client { UtxoRpcClientEnum::Native(ref native) => { let my_address = coin.my_address().unwrap(); - block_on_f01(native.import_address(&my_address, &my_address, false)).unwrap() + native + .import_address(&my_address, &my_address, false) + .compat() + .await + .unwrap(); }, UtxoRpcClientEnum::Electrum(_) => panic!("Expected NativeClient"), } @@ -615,7 +659,7 @@ pub fn qrc20_coin_from_privkey(ticker: &str, priv_key: Secp256k1Secret) -> (MmAr )) .unwrap(); - import_address(&coin); + block_on(import_address(&coin)); (ctx, coin) } @@ -649,7 +693,7 @@ pub fn utxo_coin_from_privkey(ticker: &str, priv_key: Secp256k1Secret) -> (MmArc let req = json!({"method":"enable"}); let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); let coin = block_on(utxo_standard_coin_with_priv_key(&ctx, ticker, &conf, ¶ms, priv_key)).unwrap(); - import_address(&coin); + block_on(import_address(&coin)); (ctx, coin) } @@ -661,6 +705,18 @@ pub fn generate_utxo_coin_with_privkey(ticker: &str, balance: BigDecimal, priv_k fill_address(&coin, &my_address, balance, timeout); } +pub async fn fund_privkey_utxo(ticker: &str, balance: BigDecimal, priv_key: &Secp256k1Secret) { + let ctx = MmCtxBuilder::new().into_mm_arc(); + let conf = json!({"coin":ticker,"asset":ticker,"txversion":4,"overwintered":1,"txfee":1000,"network":"regtest"}); + let req = json!({"method":"enable"}); + let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); + let coin = utxo_standard_coin_with_priv_key(&ctx, ticker, &conf, ¶ms, *priv_key) + .await + .unwrap(); + let my_address = coin.my_address().expect("!my_address"); + fill_address_async(&coin, &my_address, balance, 30).await; +} + /// Generate random privkey, create a UTXO coin and fill it's address with the specified balance. pub fn generate_utxo_coin_with_random_privkey( ticker: &str, @@ -697,7 +753,7 @@ pub fn fill_qrc20_address(coin: &Qrc20Coin, amount: BigDecimal, timeout: u64) { // prevent concurrent fill since daemon RPC returns errors if send_to_address // is called concurrently (insufficient funds) and it also may return other errors // if previous transaction is not confirmed yet - let _lock = QTUM_LOCK.lock().unwrap(); + let _lock = block_on(QTUM_LOCK.lock()); let timeout = wait_until_sec(timeout); let client = match coin.as_ref().rpc_client { UtxoRpcClientEnum::Native(ref client) => client, @@ -818,6 +874,13 @@ pub fn generate_segwit_qtum_coin_with_random_privkey( } pub fn fill_address(coin: &T, address: &str, amount: BigDecimal, timeout: u64) +where + T: MarketCoinOps + AsRef, +{ + block_on(fill_address_async(coin, address, amount, timeout)); +} + +pub async fn fill_address_async(coin: &T, address: &str, amount: BigDecimal, timeout: u64) where T: MarketCoinOps + AsRef, { @@ -831,13 +894,13 @@ where "FORSLP" => &*FOR_SLP_LOCK, ticker => panic!("Unknown ticker {}", ticker), }; - let _lock = mutex.lock().unwrap(); + let _lock = mutex.lock().await; let timeout = wait_until_sec(timeout); if let UtxoRpcClientEnum::Native(client) = &coin.as_ref().rpc_client { - block_on_f01(client.import_address(address, address, false)).unwrap(); - let hash = block_on_f01(client.send_to_address(address, &amount)).unwrap(); - let tx_bytes = block_on_f01(client.get_transaction_bytes(&hash)).unwrap(); + client.import_address(address, address, false).compat().await.unwrap(); + let hash = client.send_to_address(address, &amount).compat().await.unwrap(); + let tx_bytes = client.get_transaction_bytes(&hash).compat().await.unwrap(); let confirm_payment_input = ConfirmPaymentInput { payment_tx: tx_bytes.clone().0, confirmations: 1, @@ -845,15 +908,22 @@ where wait_until: timeout, check_every: 1, }; - block_on_f01(coin.wait_for_confirmations(confirm_payment_input)).unwrap(); + coin.wait_for_confirmations(confirm_payment_input) + .compat() + .await + .unwrap(); log!("{:02x}", tx_bytes); loop { - let unspents = block_on_f01(client.list_unspent_impl(0, i32::MAX, vec![address.to_string()])).unwrap(); + let unspents = client + .list_unspent_impl(0, i32::MAX, vec![address.to_string()]) + .compat() + .await + .unwrap(); if !unspents.is_empty() { break; } assert!(now_sec() < timeout, "Test timed out"); - thread::sleep(Duration::from_secs(1)); + Timer::sleep(1.0).await; } }; } diff --git a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs index a51c09d2dd..8cc759ffb8 100644 --- a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs @@ -534,7 +534,7 @@ fn sepolia_coin_from_privkey(ctx: &MmArc, secret: &'static str, ticker: &str, co let mut coins = block_on(coins_ctx.lock_coins()); coins.insert( coin.ticker().into(), - MmCoinStruct::new(MmCoinEnum::EthCoin(coin.clone())), + MmCoinStruct::new(MmCoinEnum::EthCoinVariant(coin.clone())), ); coin } @@ -544,7 +544,7 @@ fn get_or_create_sepolia_coin(ctx: &MmArc, priv_key: &'static str, ticker: &str, match block_on(lp_coinfind(ctx, ticker)).unwrap() { None => sepolia_coin_from_privkey(ctx, priv_key, ticker, conf, erc20), Some(mm_coin) => match mm_coin { - MmCoinEnum::EthCoin(coin) => coin, + MmCoinEnum::EthCoinVariant(coin) => coin, _ => panic!("Unexpected coin type found. Expected MmCoinEnum::EthCoin"), }, } @@ -1108,7 +1108,7 @@ fn test_nonce_erc20_lock() { eth_coin_v2_activation_with_random_privkey(&ctx, ð_ticker, ð_conf, swap_addresses, false); block_on(lp_register_coin( &ctx, - MmCoinEnum::EthCoin(eth_coin.clone()), + MmCoinEnum::EthCoinVariant(eth_coin.clone()), RegisterCoinParams { ticker: eth_ticker.clone(), }, diff --git a/mm2src/mm2_main/tests/docker_tests/mod.rs b/mm2src/mm2_main/tests/docker_tests/mod.rs index b08a0bfda2..e9b07c3c84 100644 --- a/mm2src/mm2_main/tests/docker_tests/mod.rs +++ b/mm2src/mm2_main/tests/docker_tests/mod.rs @@ -5,7 +5,6 @@ mod docker_ordermatch_tests; mod docker_tests_inner; mod eth_docker_tests; pub mod qrc20_tests; -#[cfg(feature = "enable-sia")] mod sia_docker_tests; mod slp_tests; mod swap_proto_v2_tests; diff --git a/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs b/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs index 5ed24d98c6..b39aeb1f8d 100644 --- a/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs @@ -28,8 +28,8 @@ use std::process::Command; use std::str::FromStr; use std::sync::Mutex; use std::time::Duration; -use testcontainers::clients::Cli; use testcontainers::core::WaitFor; +use testcontainers::runners::SyncRunner; use testcontainers::{GenericImage, RunnableImage}; pub const QTUM_REGTEST_DOCKER_IMAGE: &str = "docker.io/sergeyboyko/qtumregtest"; @@ -90,7 +90,7 @@ impl QtumDockerOps { } } -pub fn qtum_docker_node(docker: &Cli, port: u16) -> DockerNode<'_> { +pub fn qtum_docker_node(port: u16) -> DockerNode { let image = GenericImage::new(QTUM_REGTEST_DOCKER_IMAGE, "latest") .with_env_var("CLIENTS", "2") .with_env_var("COIN_RPC_PORT", port.to_string()) @@ -98,7 +98,7 @@ pub fn qtum_docker_node(docker: &Cli, port: u16) -> DockerNode<'_> { .with_env_var("FILL_MEMPOOL", "true") .with_wait_for(WaitFor::message_on_stdout("config is ready")); let image = RunnableImage::from(image).with_mapped_port((port, port)); - let container = docker.run(image); + let container = image.start().expect("Failed to start Qtum regtest docker container"); let name = "qtum"; let mut conf_path = temp_dir().join("qtum-regtest"); diff --git a/mm2src/mm2_main/tests/docker_tests/sia_docker_tests.rs b/mm2src/mm2_main/tests/docker_tests/sia_docker_tests.rs index 3286dcdb51..2770e58a92 100644 --- a/mm2src/mm2_main/tests/docker_tests/sia_docker_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/sia_docker_tests.rs @@ -1,34 +1,23 @@ +//! TODO: These tests have nothing to do with SiaCoin and should rather be in `sia-rust` repo instead. + use common::block_on; -use sia_rust::http_client::{SiaApiClient, SiaApiClientError, SiaHttpConf}; -use sia_rust::http_endpoints::{ - AddressBalanceRequest, AddressUtxosRequest, ConsensusTipRequest, TxpoolBroadcastRequest, +use sia_rust::transport::client::{ + error::ClientError as SiaApiClientError, ApiClient, ApiClientHelpers, Client as SiaApiClient, Conf as SiaHttpConf, +}; +use sia_rust::transport::endpoints::{ + AddressBalanceRequest, ConsensusTipRequest, GetAddressUtxosRequest, TxpoolBroadcastRequest, }; -use sia_rust::spend_policy::SpendPolicy; -use sia_rust::transaction::{SiacoinOutput, V2TransactionBuilder}; -use sia_rust::types::{Address, Currency}; -use sia_rust::{Keypair, PublicKey, SecretKey}; -use std::process::Command; +use sia_rust::types::{Address, Currency, Keypair, SiacoinOutput, SpendPolicy}; +use sia_rust::utils::V2TransactionBuilder; use std::str::FromStr; use url::Url; -#[cfg(test)] -fn mine_blocks(n: u64, addr: &Address) { - Command::new("docker") - .arg("exec") - .arg("sia-docker") - .arg("walletd") - .arg("mine") - .arg(format!("-addr={addr}")) - .arg(format!("-n={n}")) - .status() - .expect("Failed to execute docker command"); -} - #[test] fn test_sia_new_client() { let conf = SiaHttpConf { - url: Url::parse("http://localhost:9980/").unwrap(), - password: "password".to_string(), + server_url: Url::parse("http://localhost:9980/").unwrap(), + password: Some("password".to_string()), + timeout: None, }; let _api_client = block_on(SiaApiClient::new(conf)).unwrap(); } @@ -36,18 +25,35 @@ fn test_sia_new_client() { #[test] fn test_sia_client_bad_auth() { let conf = SiaHttpConf { - url: Url::parse("http://localhost:9980/").unwrap(), - password: "foo".to_string(), + server_url: Url::parse("http://localhost:9980/").unwrap(), + password: Some("foo".to_string()), + timeout: None, }; let result = block_on(SiaApiClient::new(conf)); - assert!(matches!(result, Err(SiaApiClientError::UnexpectedHttpStatus(401)))); + let Err(error) = result else { + panic!("Expected error but got success"); + }; + + match error { + SiaApiClientError::PingServer(nested_error) => match *nested_error { + SiaApiClientError::DispatcherUnexpectedStatus { status, .. } => { + assert_eq!(status, http::StatusCode::UNAUTHORIZED); + }, + different_error => panic!( + "Unexpected DispatcherUnexpectedStatus error, got: {:?}", + different_error + ), + }, + different_error => panic!("Expected PingServer error, got: {:?}", different_error), + } } #[test] fn test_sia_client_consensus_tip() { let conf = SiaHttpConf { - url: Url::parse("http://localhost:9980/").unwrap(), - password: "password".to_string(), + server_url: Url::parse("http://localhost:9980/").unwrap(), + password: Some("password".to_string()), + timeout: None, }; let api_client = block_on(SiaApiClient::new(conf)).unwrap(); let _response = block_on(api_client.dispatcher(ConsensusTipRequest)).unwrap(); @@ -58,63 +64,69 @@ fn test_sia_client_consensus_tip() { #[test] fn test_sia_client_address_balance() { let conf = SiaHttpConf { - url: Url::parse("http://localhost:9980/").unwrap(), - password: "password".to_string(), + server_url: Url::parse("http://localhost:9980/").unwrap(), + password: Some("password".to_string()), + timeout: None, }; let api_client = block_on(SiaApiClient::new(conf)).unwrap(); let address = - Address::from_str("addr:591fcf237f8854b5653d1ac84ae4c107b37f148c3c7b413f292d48db0c25a8840be0653e411f").unwrap(); - mine_blocks(10, &address); + Address::from_str("591fcf237f8854b5653d1ac84ae4c107b37f148c3c7b413f292d48db0c25a8840be0653e411f").unwrap(); + block_on(api_client.mine_blocks(10, &address)).unwrap(); let request = AddressBalanceRequest { address }; let response = block_on(api_client.dispatcher(request)).unwrap(); - let expected = Currency::new(12919594847110692864, 54210108624275221); - assert_eq!(response.siacoins, expected); - assert_eq!(expected.to_u128(), 1000000000000000000000000000000000000); + // It's hard to predict how much was mined to this address while other tests are also mining in the same network. + // Looks like the halving happens so quickly and the sum of mined coins change between different test runs. + // Just make sure we at least mined something. + assert!(response.immature_siacoins + response.siacoins > Currency(0)); } #[test] fn test_sia_client_build_tx() { let conf = SiaHttpConf { - url: Url::parse("http://localhost:9980/").unwrap(), - password: "password".to_string(), + server_url: Url::parse("http://localhost:9980/").unwrap(), + password: Some("password".to_string()), + timeout: None, }; let api_client = block_on(SiaApiClient::new(conf)).unwrap(); - let sk: SecretKey = SecretKey::from_bytes( + let keypair = Keypair::from_private_bytes( &hex::decode("0100000000000000000000000000000000000000000000000000000000000000").unwrap(), ) .unwrap(); - let pk: PublicKey = (&sk).into(); - let keypair = Keypair { public: pk, secret: sk }; - let spend_policy = SpendPolicy::PublicKey(pk); + let spend_policy = SpendPolicy::PublicKey(keypair.public()); let address = spend_policy.address(); - mine_blocks(201, &address); + block_on(api_client.mine_blocks(201, &address)).unwrap(); - let utxos = block_on(api_client.dispatcher(AddressUtxosRequest { + let utxos = block_on(api_client.dispatcher(GetAddressUtxosRequest { address: address.clone(), + limit: None, + offset: None, + include_mempool: true, })) .unwrap(); - let spend_this = utxos[0].clone(); + let spend_this = utxos.outputs[0].clone(); let vin = spend_this.clone(); println!("utxo[0]: {spend_this:?}"); let vout = SiacoinOutput { value: spend_this.siacoin_output.value, address, }; - let tx = V2TransactionBuilder::new(0u64.into()) + let tx = V2TransactionBuilder::new() .add_siacoin_input(vin, spend_policy) .add_siacoin_output(vout) .sign_simple(vec![&keypair]) - .unwrap() + .update_basis(utxos.basis.clone()) .build(); let req = TxpoolBroadcastRequest { + basis: utxos.basis, transactions: vec![], v2transactions: vec![tx], }; + let _response = block_on(api_client.dispatcher(req)).unwrap(); } diff --git a/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs b/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs index 42282b1dab..67799a3f30 100644 --- a/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs @@ -1574,7 +1574,7 @@ fn test_watcher_validate_taker_payment_utxo() { secret_hash: secret_hash.to_vec(), wait_until: timeout, confirmations: 1, - maker_coin: MmCoinEnum::UtxoCoin(maker_coin.clone()), + maker_coin: MmCoinEnum::UtxoCoinVariant(maker_coin.clone()), })); assert!(validate_taker_payment_res.is_ok()); @@ -1587,7 +1587,7 @@ fn test_watcher_validate_taker_payment_utxo() { secret_hash: secret_hash.to_vec(), wait_until: timeout, confirmations: 1, - maker_coin: MmCoinEnum::UtxoCoin(maker_coin.clone()), + maker_coin: MmCoinEnum::UtxoCoinVariant(maker_coin.clone()), })) .unwrap_err() .into_inner(); @@ -1611,7 +1611,7 @@ fn test_watcher_validate_taker_payment_utxo() { secret_hash: wrong_secret_hash.to_vec(), wait_until: timeout, confirmations: 1, - maker_coin: MmCoinEnum::UtxoCoin(maker_coin.clone()), + maker_coin: MmCoinEnum::UtxoCoinVariant(maker_coin.clone()), })) .unwrap_err() .into_inner(); @@ -1659,7 +1659,7 @@ fn test_watcher_validate_taker_payment_utxo() { secret_hash: wrong_secret_hash.to_vec(), wait_until: timeout, confirmations: 1, - maker_coin: MmCoinEnum::UtxoCoin(maker_coin.clone()), + maker_coin: MmCoinEnum::UtxoCoinVariant(maker_coin.clone()), })) .unwrap_err() .into_inner(); @@ -1694,7 +1694,7 @@ fn test_watcher_validate_taker_payment_utxo() { secret_hash: secret_hash.to_vec(), wait_until: timeout, confirmations: 1, - maker_coin: MmCoinEnum::UtxoCoin(maker_coin.clone()), + maker_coin: MmCoinEnum::UtxoCoinVariant(maker_coin.clone()), })) .unwrap_err() .into_inner(); @@ -1773,7 +1773,7 @@ fn test_watcher_validate_taker_payment_eth() { secret_hash: secret_hash.to_vec(), wait_until: timeout, confirmations: 1, - maker_coin: MmCoinEnum::EthCoin(taker_coin.clone()), + maker_coin: MmCoinEnum::EthCoinVariant(taker_coin.clone()), }, )); assert!(validate_taker_payment_res.is_ok()); @@ -1788,7 +1788,7 @@ fn test_watcher_validate_taker_payment_eth() { secret_hash: secret_hash.to_vec(), wait_until: timeout, confirmations: 1, - maker_coin: MmCoinEnum::EthCoin(taker_coin.clone()), + maker_coin: MmCoinEnum::EthCoinVariant(taker_coin.clone()), }), ) .unwrap_err() @@ -1828,7 +1828,7 @@ fn test_watcher_validate_taker_payment_eth() { secret_hash: secret_hash.to_vec(), wait_until: timeout, confirmations: 1, - maker_coin: MmCoinEnum::EthCoin(taker_coin.clone()), + maker_coin: MmCoinEnum::EthCoinVariant(taker_coin.clone()), }), ) .unwrap_err() @@ -1856,7 +1856,7 @@ fn test_watcher_validate_taker_payment_eth() { secret_hash: wrong_secret_hash.to_vec(), wait_until: timeout, confirmations: 1, - maker_coin: MmCoinEnum::EthCoin(taker_coin.clone()), + maker_coin: MmCoinEnum::EthCoinVariant(taker_coin.clone()), }), ) .unwrap_err() @@ -1904,7 +1904,7 @@ fn test_watcher_validate_taker_payment_eth() { secret_hash: wrong_secret_hash.to_vec(), wait_until: timeout, confirmations: 1, - maker_coin: MmCoinEnum::EthCoin(taker_coin.clone()), + maker_coin: MmCoinEnum::EthCoinVariant(taker_coin.clone()), })) .unwrap_err() .into_inner(); @@ -1928,7 +1928,7 @@ fn test_watcher_validate_taker_payment_eth() { secret_hash: secret_hash.to_vec(), wait_until: timeout, confirmations: 1, - maker_coin: MmCoinEnum::EthCoin(taker_coin.clone()), + maker_coin: MmCoinEnum::EthCoinVariant(taker_coin.clone()), })) .unwrap_err() .into_inner(); @@ -2009,7 +2009,7 @@ fn test_watcher_validate_taker_payment_erc20() { secret_hash: secret_hash.to_vec(), wait_until: timeout, confirmations: 1, - maker_coin: MmCoinEnum::EthCoin(taker_coin.clone()), + maker_coin: MmCoinEnum::EthCoinVariant(taker_coin.clone()), })); assert!(validate_taker_payment_res.is_ok()); @@ -2022,7 +2022,7 @@ fn test_watcher_validate_taker_payment_erc20() { secret_hash: secret_hash.to_vec(), wait_until: timeout, confirmations: 1, - maker_coin: MmCoinEnum::EthCoin(taker_coin.clone()), + maker_coin: MmCoinEnum::EthCoinVariant(taker_coin.clone()), })) .unwrap_err() .into_inner(); @@ -2060,7 +2060,7 @@ fn test_watcher_validate_taker_payment_erc20() { secret_hash: secret_hash.to_vec(), wait_until: timeout, confirmations: 1, - maker_coin: MmCoinEnum::EthCoin(taker_coin.clone()), + maker_coin: MmCoinEnum::EthCoinVariant(taker_coin.clone()), })) .unwrap_err() .into_inner(); @@ -2086,7 +2086,7 @@ fn test_watcher_validate_taker_payment_erc20() { secret_hash: wrong_secret_hash.to_vec(), wait_until: timeout, confirmations: 1, - maker_coin: MmCoinEnum::EthCoin(taker_coin.clone()), + maker_coin: MmCoinEnum::EthCoinVariant(taker_coin.clone()), })) .unwrap_err() .into_inner(); @@ -2133,7 +2133,7 @@ fn test_watcher_validate_taker_payment_erc20() { secret_hash: wrong_secret_hash.to_vec(), wait_until: timeout, confirmations: 1, - maker_coin: MmCoinEnum::EthCoin(taker_coin.clone()), + maker_coin: MmCoinEnum::EthCoinVariant(taker_coin.clone()), })) .unwrap_err() .into_inner(); @@ -2157,7 +2157,7 @@ fn test_watcher_validate_taker_payment_erc20() { secret_hash: secret_hash.to_vec(), wait_until: timeout, confirmations: 1, - maker_coin: MmCoinEnum::EthCoin(taker_coin.clone()), + maker_coin: MmCoinEnum::EthCoinVariant(taker_coin.clone()), })) .unwrap_err() .into_inner(); @@ -3240,15 +3240,20 @@ fn test_watcher_reward() { let (_ctx, utxo_coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); let eth_coin = eth_coin_with_random_privkey(watchers_swap_contract()); - let watcher_reward = - block_on(eth_coin.get_taker_watcher_reward(&MmCoinEnum::EthCoin(eth_coin.clone()), None, None, None, timeout)) - .unwrap(); + let watcher_reward = block_on(eth_coin.get_taker_watcher_reward( + &MmCoinEnum::EthCoinVariant(eth_coin.clone()), + None, + None, + None, + timeout, + )) + .unwrap(); assert!(!watcher_reward.is_exact_amount); assert!(matches!(watcher_reward.reward_target, RewardTarget::Contract)); assert!(!watcher_reward.send_contract_reward_on_spend); let watcher_reward = block_on(eth_coin.get_taker_watcher_reward( - &MmCoinEnum::EthCoin(eth_coin.clone()), + &MmCoinEnum::EthCoinVariant(eth_coin.clone()), None, None, Some(BigDecimal::one()), @@ -3261,7 +3266,7 @@ fn test_watcher_reward() { assert_eq!(watcher_reward.amount, BigDecimal::one()); let watcher_reward = block_on(eth_coin.get_taker_watcher_reward( - &MmCoinEnum::UtxoCoin(utxo_coin.clone()), + &MmCoinEnum::UtxoCoinVariant(utxo_coin.clone()), None, None, None, @@ -3273,7 +3278,7 @@ fn test_watcher_reward() { assert!(!watcher_reward.send_contract_reward_on_spend); let watcher_reward = - block_on(eth_coin.get_maker_watcher_reward(&MmCoinEnum::EthCoin(eth_coin.clone()), None, timeout)) + block_on(eth_coin.get_maker_watcher_reward(&MmCoinEnum::EthCoinVariant(eth_coin.clone()), None, timeout)) .unwrap() .unwrap(); assert!(!watcher_reward.is_exact_amount); @@ -3281,7 +3286,7 @@ fn test_watcher_reward() { assert!(watcher_reward.send_contract_reward_on_spend); let watcher_reward = block_on(eth_coin.get_maker_watcher_reward( - &MmCoinEnum::EthCoin(eth_coin.clone()), + &MmCoinEnum::EthCoinVariant(eth_coin.clone()), Some(BigDecimal::one()), timeout, )) @@ -3293,7 +3298,7 @@ fn test_watcher_reward() { assert_eq!(watcher_reward.amount, BigDecimal::one()); let watcher_reward = - block_on(eth_coin.get_maker_watcher_reward(&MmCoinEnum::UtxoCoin(utxo_coin.clone()), None, timeout)) + block_on(eth_coin.get_maker_watcher_reward(&MmCoinEnum::UtxoCoinVariant(utxo_coin.clone()), None, timeout)) .unwrap() .unwrap(); assert!(!watcher_reward.is_exact_amount); @@ -3301,7 +3306,7 @@ fn test_watcher_reward() { assert!(!watcher_reward.send_contract_reward_on_spend); let watcher_reward = block_on(utxo_coin.get_taker_watcher_reward( - &MmCoinEnum::EthCoin(eth_coin), + &MmCoinEnum::EthCoinVariant(eth_coin), Some(BigDecimal::from_str("0.01").unwrap()), Some(BigDecimal::from_str("1").unwrap()), None, @@ -3313,6 +3318,7 @@ fn test_watcher_reward() { assert!(!watcher_reward.send_contract_reward_on_spend); let watcher_reward = - block_on(utxo_coin.get_maker_watcher_reward(&MmCoinEnum::UtxoCoin(utxo_coin.clone()), None, timeout)).unwrap(); + block_on(utxo_coin.get_maker_watcher_reward(&MmCoinEnum::UtxoCoinVariant(utxo_coin.clone()), None, timeout)) + .unwrap(); assert!(watcher_reward.is_none()); } diff --git a/mm2src/mm2_main/tests/docker_tests/swaps_file_lock_tests.rs b/mm2src/mm2_main/tests/docker_tests/swaps_file_lock_tests.rs index f7c20fc4c0..fdabe53544 100644 --- a/mm2src/mm2_main/tests/docker_tests/swaps_file_lock_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/swaps_file_lock_tests.rs @@ -19,7 +19,7 @@ const FINISHED_TAKER_SWAP: &str = r#"{"type":"Taker","uuid":"5acb0e63-8b26-469e- fn swap_file_lock_prevents_double_swap_start_on_kick_start(swap_json: &str) { let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); let addr_hash = addr_hash_for_privkey(bob_priv_key); - let db_folder = new_mm2_temp_folder_path(None).join("DB"); + let db_folder = new_mm2_temp_folder_path(None, None).join("DB"); let swaps_db_folder = db_folder.join(addr_hash).join("SWAPS").join("MY"); std::fs::create_dir_all(&swaps_db_folder).unwrap(); let swap_path = swaps_db_folder.join("5acb0e63-8b26-469e-81df-7dd9e4a9ad15.json"); @@ -190,7 +190,7 @@ fn swap_should_not_kick_start_if_finished_during_waiting_for_file_lock( ) { let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); let addr_hash = addr_hash_for_privkey(bob_priv_key); - let db_folder = new_mm2_temp_folder_path(None).join("DB"); + let db_folder = new_mm2_temp_folder_path(None, None).join("DB"); let swaps_db_folder = db_folder.join(addr_hash).join("SWAPS").join("MY"); std::fs::create_dir_all(&swaps_db_folder).unwrap(); let swap_path = swaps_db_folder.join("5acb0e63-8b26-469e-81df-7dd9e4a9ad15.json"); diff --git a/mm2src/mm2_main/tests/docker_tests_main.rs b/mm2src/mm2_main/tests/docker_tests_main.rs index 5499f1dfe2..c85ffe4c05 100644 --- a/mm2src/mm2_main/tests/docker_tests_main.rs +++ b/mm2src/mm2_main/tests/docker_tests_main.rs @@ -28,11 +28,12 @@ use std::path::PathBuf; use std::process::Command; use std::time::Duration; use test::{test_main, StaticBenchFn, StaticTestFn, TestDescAndFn}; -use testcontainers::clients::Cli; mod docker_tests; +mod sia_tests; use docker_tests::docker_tests_common::*; use docker_tests::qrc20_tests::{qtum_docker_node, QtumDockerOps, QTUM_REGTEST_DOCKER_IMAGE_WITH_TAG}; +use sia_tests::utils::wait_for_dsia_node_ready; #[allow(dead_code)] mod integration_tests_common; @@ -43,6 +44,7 @@ const ENV_VAR_NO_SLP_DOCKER: &str = "_KDF_NO_SLP_DOCKER"; const ENV_VAR_NO_ETH_DOCKER: &str = "_KDF_NO_ETH_DOCKER"; const ENV_VAR_NO_COSMOS_DOCKER: &str = "_KDF_NO_COSMOS_DOCKER"; const ENV_VAR_NO_ZOMBIE_DOCKER: &str = "_KDF_NO_ZOMBIE_DOCKER"; +const ENV_VAR_NO_SIA_DOCKER: &str = "_KDF_NO_SIA_DOCKER"; // AP: custom test runner is intended to initialize the required environment (e.g. coin daemons in the docker containers) // and then gracefully clear it by dropping the RAII docker container handlers @@ -54,7 +56,6 @@ const ENV_VAR_NO_ZOMBIE_DOCKER: &str = "_KDF_NO_ZOMBIE_DOCKER"; // Linux and MacOS - https://github.com/KomodoPlatform/komodo/blob/master/zcutil/fetch-params.sh pub fn docker_tests_runner(tests: &[&TestDescAndFn]) { // pretty_env_logger::try_init(); - let docker = Cli::default(); let mut containers = vec![]; // skip Docker containers initialization if we are intended to run test_mm_start only if env::var("_MM2_TEST_CONF").is_err() { @@ -66,6 +67,7 @@ pub fn docker_tests_runner(tests: &[&TestDescAndFn]) { let disable_eth: bool = env::var(ENV_VAR_NO_ETH_DOCKER).is_ok(); let disable_cosmos: bool = env::var(ENV_VAR_NO_COSMOS_DOCKER).is_ok(); let disable_zombie: bool = env::var(ENV_VAR_NO_ZOMBIE_DOCKER).is_ok(); + let disable_sia: bool = env::var(ENV_VAR_NO_SIA_DOCKER).is_ok(); if !disable_utxo || !disable_slp { images.push(UTXO_ASSET_DOCKER_IMAGE_WITH_TAG) @@ -85,6 +87,10 @@ pub fn docker_tests_runner(tests: &[&TestDescAndFn]) { images.push(ZOMBIE_ASSET_DOCKER_IMAGE_WITH_TAG); } + if !disable_sia { + images.push(SIA_DOCKER_IMAGE_WITH_TAG); + } + for image in images { pull_docker_image(image); remove_docker_containers(image); @@ -92,45 +98,52 @@ pub fn docker_tests_runner(tests: &[&TestDescAndFn]) { let (nucleus_node, atom_node, ibc_relayer_node) = if !disable_cosmos { let runtime_dir = prepare_runtime_dir().unwrap(); - let nucleus_node = nucleus_node(&docker, runtime_dir.clone()); - let atom_node = atom_node(&docker, runtime_dir.clone()); - let ibc_relayer_node = ibc_relayer_node(&docker, runtime_dir); + let nucleus_node = nucleus_node(runtime_dir.clone()); + let atom_node = atom_node(runtime_dir.clone()); + let ibc_relayer_node = ibc_relayer_node(runtime_dir); (Some(nucleus_node), Some(atom_node), Some(ibc_relayer_node)) } else { (None, None, None) }; let (utxo_node, utxo_node1) = if !disable_utxo { - let utxo_node = utxo_asset_docker_node(&docker, "MYCOIN", 8000); - let utxo_node1 = utxo_asset_docker_node(&docker, "MYCOIN1", 8001); + let utxo_node = utxo_asset_docker_node("MYCOIN", 8000); + let utxo_node1 = utxo_asset_docker_node("MYCOIN1", 8001); (Some(utxo_node), Some(utxo_node1)) } else { (None, None) }; let qtum_node = if !disable_qtum { - let qtum_node = qtum_docker_node(&docker, 9000); + let qtum_node = qtum_docker_node(9000); Some(qtum_node) } else { None }; let for_slp_node = if !disable_slp { - let for_slp_node = utxo_asset_docker_node(&docker, "FORSLP", 10000); + let for_slp_node = utxo_asset_docker_node("FORSLP", 10000); Some(for_slp_node) } else { None }; let geth_node = if !disable_eth { - let geth_node = geth_docker_node(&docker, "ETH", 8545); + let geth_node = geth_docker_node("ETH", 8545); Some(geth_node) } else { None }; let zombie_node = if !disable_zombie { - let zombie_node = zombie_asset_docker_node(&docker, 7090); + let zombie_node = zombie_asset_docker_node(7090); Some(zombie_node) } else { None }; + let sia_node = if !disable_sia { + let sia_node = sia_docker_node("SIA", 9980); + Some(sia_node) + } else { + None + }; + if let (Some(utxo_node), Some(utxo_node1)) = (utxo_node, utxo_node1) { let utxo_ops = UtxoAssetDockerOps::from_ticker("MYCOIN"); let utxo_ops1 = UtxoAssetDockerOps::from_ticker("MYCOIN1"); @@ -171,6 +184,10 @@ pub fn docker_tests_runner(tests: &[&TestDescAndFn]) { containers.push(atom_node); containers.push(ibc_relayer_node); } + if let Some(sia_node) = sia_node { + block_on(wait_for_dsia_node_ready()); + containers.push(sia_node); + } } // detect if docker is installed // skip the tests that use docker if not installed diff --git a/mm2src/mm2_main/tests/docker_tests_sia_unique.rs b/mm2src/mm2_main/tests/docker_tests_sia_unique.rs deleted file mode 100644 index c7327006bc..0000000000 --- a/mm2src/mm2_main/tests/docker_tests_sia_unique.rs +++ /dev/null @@ -1,106 +0,0 @@ -#![feature(custom_test_frameworks)] -#![feature(test)] -#![cfg(feature = "enable-sia")] -#![cfg(not(target_arch = "wasm32"))] -#![allow(unused_imports, dead_code)] -#![test_runner(docker_tests_runner)] - -#[cfg(test)] -#[macro_use] -extern crate common; -#[cfg(test)] -#[macro_use] -extern crate gstuff; -#[cfg(test)] -#[macro_use] -extern crate lazy_static; -#[cfg(test)] -#[macro_use] -extern crate serde_json; -#[cfg(test)] -extern crate ser_error_derive; -#[cfg(test)] -extern crate test; - -use std::env; -use std::io::{BufRead, BufReader}; -use std::path::PathBuf; -use std::process::Command; -use test::{test_main, StaticBenchFn, StaticTestFn, TestDescAndFn}; -use testcontainers::clients::Cli; - -mod docker_tests; -use docker_tests::docker_tests_common::*; - -#[allow(dead_code)] -mod integration_tests_common; - -/// Custom test runner intended to initialize the SIA coin daemon in a Docker container. -pub fn docker_tests_runner(tests: &[&TestDescAndFn]) { - let docker = Cli::default(); - let mut containers = vec![]; - - let skip_docker_tests_runner = std::env::var("SKIP_DOCKER_TESTS_RUNNER") - .map(|v| v == "1") - .unwrap_or(false); - - if !skip_docker_tests_runner { - const IMAGES: &[&str] = &[SIA_DOCKER_IMAGE_WITH_TAG]; - - for image in IMAGES { - pull_docker_image(image); - remove_docker_containers(image); - } - - let sia_node = sia_docker_node(&docker, "SIA", 9980); - println!("ran container?"); - containers.push(sia_node); - } - // detect if docker is installed - // skip the tests that use docker if not installed - let owned_tests: Vec<_> = tests - .iter() - .map(|t| match t.testfn { - StaticTestFn(f) => TestDescAndFn { - testfn: StaticTestFn(f), - desc: t.desc.clone(), - }, - StaticBenchFn(f) => TestDescAndFn { - testfn: StaticBenchFn(f), - desc: t.desc.clone(), - }, - _ => panic!("non-static tests passed to lp_coins test runner"), - }) - .collect(); - let args: Vec = env::args().collect(); - test_main(&args, owned_tests, None); -} - -fn pull_docker_image(name: &str) { - Command::new("docker") - .arg("pull") - .arg(name) - .status() - .expect("Failed to execute docker command"); -} - -fn remove_docker_containers(name: &str) { - let stdout = Command::new("docker") - .arg("ps") - .arg("-f") - .arg(format!("ancestor={name}")) - .arg("-q") - .output() - .expect("Failed to execute docker command"); - - let reader = BufReader::new(stdout.stdout.as_slice()); - let ids: Vec<_> = reader.lines().map(|line| line.unwrap()).collect(); - if !ids.is_empty() { - Command::new("docker") - .arg("rm") - .arg("-f") - .args(ids) - .status() - .expect("Failed to execute docker command"); - } -} diff --git a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs index 71c55fec0f..4f302bfa6b 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs @@ -4542,7 +4542,7 @@ fn test_mm2_db_migration() { let coins = json!([rick_conf(), morty_conf(), eth_dev_conf(),]); - let mm2_folder = new_mm2_temp_folder_path(None); + let mm2_folder = new_mm2_temp_folder_path(None, None); let swaps_dir = mm2_folder.join(format!( "{}/SWAPS/STATS/MAKER", hex::encode(rmd160_from_passphrase(&bob_passphrase)) @@ -7064,7 +7064,7 @@ mod trezor_tests { .unwrap(); let coin = block_on(lp_coinfind(&ctx, ticker_coin)).unwrap(); - let eth_coin = if let Some(MmCoinEnum::EthCoin(eth_coin)) = coin { + let eth_coin = if let Some(MmCoinEnum::EthCoinVariant(eth_coin)) = coin { eth_coin } else { panic!("eth coin not enabled"); diff --git a/mm2src/mm2_main/tests/sia_tests/docker_functional_tests.rs b/mm2src/mm2_main/tests/sia_tests/docker_functional_tests.rs new file mode 100644 index 0000000000..c9881ce1a3 --- /dev/null +++ b/mm2src/mm2_main/tests/sia_tests/docker_functional_tests.rs @@ -0,0 +1,731 @@ +use crate::docker_tests::docker_tests_common::{fund_privkey_utxo, random_secp256k1_secret}; + +use super::utils::*; + +use coins::siacoin::{ApiClientHelpers, SiaTransactionTypes}; +use mm2_number::BigDecimal; +use mm2_test_helpers::for_tests::{start_swaps, wait_for_swap_finished_or_err}; +use serde::Deserialize; +use serde_json::Value as Json; + +use std::str::FromStr; + +#[derive(Debug, Deserialize)] +struct SiaWithdrawResponse { + tx_json: SiaTransactionTypes, + from: Vec, + to: Vec, + total_amount: BigDecimal, + spent_by_me: BigDecimal, + received_by_me: BigDecimal, + my_balance_change: BigDecimal, + fee_details: Json, + coin: String, +} + +/// Tests sia client and it's connectivity to the sia walletd global container. +#[tokio::test] +async fn debug_init_sia_client() { + let client = init_sia_client().await.unwrap(); + + let address = + Address::from_str("439536d27e5cbf46b0ff873056fa8ef5424fd3f574e5ed694450c8dc4323fe6062d40a11fbc9").unwrap(); + + let response = client.address_balance(address.clone()).await.unwrap(); + log!("Address balance: {:?}", response); + assert_eq!(response.siacoins, Currency(0)); + + fund_address(&address, Currency(10)).await; + + let response = client.address_balance(address).await.unwrap(); + log!("Address balance: {:?}", response); + + assert_eq!(response.siacoins, Currency(10)); +} + +/// Initialize Alice and Bob, check that they connected via p2p network, enable DSIA for both parties +#[tokio::test] +async fn test_alice_and_bob_enable_dsia() { + let alice_priv = random_secp256k1_secret(); + let bob_priv = random_secp256k1_secret(); + + let mm_bob = init_bob(&bob_priv, None).await; + let mm_alice = init_alice(&alice_priv, &mm_bob.ip, None).await; + + wait_for_peers_connected(&mm_alice, &mm_bob, std::time::Duration::from_secs(30)) + .await + .unwrap(); + + let _bob_enable_sia_resp = enable_dsia(&mm_alice).await; + let _alice_enable_sia_resp = enable_dsia(&mm_bob).await; +} + +/// Test komodo client and it's connectivity to the komodod (mycoin) global container. +/// Validate Alice and Bob's addresses were imported via `importaddress` +#[tokio::test] +async fn test_utxo_container_and_client() { + let client = get_komodod_client( + "RNa3bJJC2L3UUCGQ9WY5fhCSzSd5ExiAWr", + "RLHqXM7q689D1PZvt9nH5nmouSPMG9sopG", + ) + .await; + + let alice_validate_address_resp = client + .rpc("validateaddress", json!(["RNa3bJJC2L3UUCGQ9WY5fhCSzSd5ExiAWr"])) + .await; + let bob_validate_address_resp = client + .rpc("validateaddress", json!(["RLHqXM7q689D1PZvt9nH5nmouSPMG9sopG"])) + .await; + + assert_eq!(alice_validate_address_resp["result"]["iswatchonly"], true); + assert_eq!(bob_validate_address_resp["result"]["iswatchonly"], true); +} + +/// Fund a DSIA account, call `withdraw` with `max: true`, and verify that: +/// * The TransactionDetails report spending the full balance. +/// * The fixed Sia fee is correctly reflected in `fee_details`. +/// * The transaction targets the expected address. +/// * After broadcasting, the Sia address has zero remaining balance (no unexpected change). +#[tokio::test] +async fn test_dsia_withdraw_max_spends_full_balance_minus_fee() { + // Use a fresh private key so the DSIA account starts empty. + let priv_key = random_secp256k1_secret(); + + // Match the fixed fee used in `SiaWithdrawBuilder::build`. + // If `TX_FEE_HASTINGS` in `sia_withdraw.rs` changes, this test should be updated. + const FIXED_WITHDRAW_FEE_HASTINGS: u128 = 10_000_000_000_000_000_000; // 1e19 Hastings + const FUNDING_MULTIPLIER: u128 = 5; + let funding_amount_hastings = FIXED_WITHDRAW_FEE_HASTINGS * FUNDING_MULTIPLIER; + + // Fund the DSIA account on the Sia testnet. + fund_privkey_sia(&priv_key, Currency(funding_amount_hastings)).await; + + // Compute the Sia address corresponding to this MarketMaker key. + let keypair = Keypair::from_private_bytes(priv_key.as_slice()).unwrap(); + let mm_sia_address = Address::from_public_key(&keypair.public()); + + let client = init_sia_client().await.unwrap(); + let balance_before = client.address_balance(mm_sia_address.clone()).await.unwrap(); + assert_eq!(balance_before.siacoins, Currency(funding_amount_hastings)); + + // Spin up a MarketMaker node using the same key and enable DSIA. + let mm = init_bob(&priv_key, None).await; + let _ = enable_dsia(&mm).await; + + // Withdraw everything to a distinct address (Charlie's). + let to_address = CHARLIE_SIA_ADDRESS.to_string(); + + let tx_details: SiaWithdrawResponse = mm + .rpc_typed(&json!({ + "method": "withdraw", + "coin": "DSIA", + "to": to_address, + "max": true, + })) + .await + .unwrap(); + + // Basic shape assertions. + assert_eq!(tx_details.coin, "DSIA"); + assert_eq!(tx_details.from.len(), 1); + assert_eq!(tx_details.to, vec![to_address.clone()]); + + // Sia has 24 decimal places; 1 siacoin = 10^24 Hastings. + // We'll convert our known Hastings amounts into BigDecimal siacoin amounts + // using this fixed scale. + let scale = BigDecimal::from_str("1000000000000000000000000").unwrap(); // 10^24 + + let expected_fee = BigDecimal::from_str(&FIXED_WITHDRAW_FEE_HASTINGS.to_string()).unwrap() / scale.clone(); + let expected_total = BigDecimal::from_str(&(funding_amount_hastings - FIXED_WITHDRAW_FEE_HASTINGS).to_string()) + .unwrap() + / scale.clone(); + let expected_spent = &expected_total + &expected_fee; + let zero = BigDecimal::from(0); + + // Amount semantics: + // * total_amount == value sent to the recipient (funds minus fee) + // * spent_by_me == total_amount + fee (full amount deducted from our wallet) + // * received_by_me == 0 (no change back to ourselves) + // * my_balance_change == -spent_by_me + assert_eq!(tx_details.total_amount, expected_total); + assert_eq!(tx_details.spent_by_me, expected_spent); + assert_eq!(tx_details.received_by_me, zero); + assert_eq!(&tx_details.my_balance_change + &tx_details.spent_by_me, zero); + + // Fee details should reflect the fixed Sia withdraw fee. + let fee_total_str = tx_details.fee_details["total_amount"] + .as_str() + .expect("fee_details.total_amount as string"); + let fee_total: BigDecimal = fee_total_str.parse().unwrap(); + assert_eq!(fee_total, expected_fee); + + // Broadcast the transaction on Sia and ensure no balance remains for the DSIA address. + let signed_tx = match tx_details.tx_json { + SiaTransactionTypes::V2Transaction(tx) => tx, + _ => panic!("Expected V2Transaction in tx_json"), + }; + + client.broadcast_transaction(&signed_tx).await.unwrap(); + client.mine_blocks(1, &CHARLIE_SIA_ADDRESS).await.unwrap(); + + let balance_after = client.address_balance(mm_sia_address).await.unwrap(); + assert_eq!(balance_after.siacoins, Currency(0)); +} + +/// Initialize Alice and Bob, initialize Sia testnet container, initialize UTXO testnet container, +/// Bob sells DSIA for Alice's MYCOIN +#[tokio::test] +async fn test_bob_sells_dsia_for_mycoin() { + let alice_priv = random_secp256k1_secret(); + let bob_priv = random_secp256k1_secret(); + + // Give bob some sia and alice some mycoin + fund_privkey_sia(&bob_priv, Currency(1e23 as u128)).await; + fund_privkey_utxo("MYCOIN", 5.into(), &alice_priv).await; + + // Initalize Alice and Bob KDF instances + let mut mm_bob = init_bob(&bob_priv, None).await; + let mut mm_alice = init_alice(&alice_priv, &mm_bob.ip, None).await; + + // Enable DSIA coin for Alice and Bob + let _ = enable_dsia(&mm_bob).await; + let _ = enable_dsia(&mm_alice).await; + + // Enable MYCOIN coin via Native node for Alice and Bob + let _ = enable_mycoin(&mm_alice).await; + let _ = enable_mycoin(&mm_bob).await; + + // Wait for Alice and Bob KDF instances to connect + wait_for_peers_connected(&mm_alice, &mm_bob, std::time::Duration::from_secs(30)) + .await + .unwrap(); + + // Start a swap where Bob sells DSIA for Alice's MYCOIN + let uuid = start_swaps(&mut mm_bob, &mut mm_alice, &[("DSIA", "MYCOIN")], 1., 1., 0.05) + .await + .first() + .cloned() + .unwrap(); + + // Wait for the swap to complete + wait_for_swap_finished_or_err(&mm_alice, &uuid, 360).await.unwrap(); + wait_for_swap_finished_or_err(&mm_bob, &uuid, 60).await.unwrap(); +} + +/// Initialize Alice and Bob, initialize Sia testnet container, initialize UTXO testnet container, +/// Bob sells MYCOIN for Alice's DSIA +#[tokio::test] +async fn test_bob_sells_mycoin_for_dsia() { + let alice_priv = random_secp256k1_secret(); + let bob_priv = random_secp256k1_secret(); + + // Give alice some sia and bob some mycoin + fund_privkey_sia(&alice_priv, Currency(1e23 as u128)).await; + fund_privkey_utxo("MYCOIN", 5.into(), &bob_priv).await; + + // Initalize Alice and Bob KDF instances + let mut mm_bob = init_bob(&bob_priv, None).await; + let mut mm_alice = init_alice(&alice_priv, &mm_bob.ip, None).await; + + // Enable DSIA coin for Alice and Bob + let _ = enable_dsia(&mm_bob).await; + let _ = enable_dsia(&mm_alice).await; + + // Enable MYCOIN coin via Native node for Alice and Bob + let _ = enable_mycoin(&mm_alice).await; + let _ = enable_mycoin(&mm_bob).await; + + // Wait for Alice and Bob KDF instances to connect + wait_for_peers_connected(&mm_alice, &mm_bob, std::time::Duration::from_secs(30)) + .await + .unwrap(); + + // Start a swap where Bob sells MYCOIN for Alice's DSIA + let uuid = start_swaps(&mut mm_bob, &mut mm_alice, &[("MYCOIN", "DSIA")], 1., 1., 0.05) + .await + .first() + .cloned() + .unwrap(); + + // Wait for the swap to complete + wait_for_swap_finished_or_err(&mm_alice, &uuid, 600).await.unwrap(); + wait_for_swap_finished_or_err(&mm_bob, &uuid, 60).await.unwrap(); +} + +/* +// WIP the following tests are "functional tests" and lie somewhere between a unit test and integration test +// All are disabled for now until this sia_tests module can be better organized. +// These were written as SiaCoin implementation was being developed and are not currently maintained + +use crate::lp_swap::SecretHashAlgo; +use crate::lp_wallet::initialize_wallet_passphrase; + +use coins::siacoin::{ApiClientHelpers, SiaCoin, SiaCoinActivationRequest}; +use coins::Transaction; + +use common::now_sec; + +use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; +use mm2_number::BigDecimal; +use coins::{PrivKeyBuildPolicy, RefundPaymentArgs, SendPaymentArgs, SpendPaymentArgs, SwapOps, + SwapTxTypeWithSecretHash, TransactionEnum}; +fn helper_activation_request(port: u16) -> SiaCoinActivationRequest { + let activation_request_json = json!( + { + "tx_history": true, + "client_conf": { + "server_url": format!("http://localhost:{}/", port), + "password": "password" + } + } + ); + serde_json::from_value::(activation_request_json).unwrap() +} + +/// Initialize a minimal MarketMaker intended for unit testing. +/// See `init_bob` or `init_alice` for creating "full" MarketMaker instances. +async fn init_ctx(passphrase: &str, netid: u16) -> MmArc { + let kdf_conf = json!({ + "gui": "sia-docker-tests", + "netid": netid, + "rpc_password": "rpc_password", + "passphrase": passphrase, + }); + + let ctx = MmCtxBuilder::new().with_conf(kdf_conf).into_mm_arc(); + + initialize_wallet_passphrase(&ctx).await.unwrap(); + ctx +} + +async fn init_siacoin(ctx: MmArc, ticker: &str, request: &SiaCoinActivationRequest) -> SiaCoin { + let coin_conf_str = json!( + { + "coin": ticker, + "required_confirmations": 1, + } + ); + + let priv_key_policy = PrivKeyBuildPolicy::detect_priv_key_policy(&ctx).unwrap(); + SiaCoin::new(&ctx, coin_conf_str, request, priv_key_policy) + .await + .unwrap() +} + +/** + * Initialize ctx and SiaCoin for both parties, maker and taker + * Initialize a new SiaCoin testnet and mine blocks to maker for funding + * Send a HTLC payment from maker + * Spend the HTLC payment from taker + * + * maker_* indicates data created by the maker + * taker_* indicates data created by the taker + * negotiated_* indicates data that is negotiated via p2p communication + */ +#[tokio::test] +async fn test_send_maker_payment_then_spend_maker_payment() { + let docker = Cli::default(); + + // Start the container + let (_container, host_port) = init_walletd_container(&docker); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + let maker_ctx = init_ctx("maker passphrase", 9995).await; + let maker_sia_coin = init_siacoin(maker_ctx, "TSIA", &helper_activation_request(host_port)).await; + let maker_public_key = maker_sia_coin.my_keypair().unwrap().public(); + let maker_address = maker_public_key.address(); + let maker_secret = vec![0u8; 32]; + let maker_secret_hash = SecretHashAlgo::SHA256.hash_secret(&maker_secret); + maker_sia_coin.client.mine_blocks(201, &maker_address).await.unwrap(); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + let taker_ctx = init_ctx("taker passphrase", 9995).await; + let taker_sia_coin = init_siacoin(taker_ctx, "TSIA", &helper_activation_request(host_port)).await; + let taker_public_key = taker_sia_coin.my_keypair().unwrap().public(); + + let negotiated_time_lock = now_sec(); + let negotiated_time_lock_duration = 10u64; + let negotiated_amount: BigDecimal = 1u64.into(); + + let maker_send_payment_args = SendPaymentArgs { + time_lock_duration: negotiated_time_lock_duration, + time_lock: negotiated_time_lock, + other_pubkey: taker_public_key.as_bytes(), + secret_hash: &maker_secret_hash, + amount: negotiated_amount, + swap_contract_address: &None, + swap_unique_data: &[], + payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, + }; + let maker_payment_tx = match maker_sia_coin + .send_maker_payment(maker_send_payment_args) + .await + .unwrap() + { + TransactionEnum::SiaTransaction(tx) => tx, + _ => panic!("Expected SiaTransaction"), + }; + maker_sia_coin.client.mine_blocks(1, &maker_address).await.unwrap(); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + let taker_spend_payment_args = SpendPaymentArgs { + other_payment_tx: &maker_payment_tx.tx_hex(), + time_lock: negotiated_time_lock, + other_pubkey: maker_public_key.as_bytes(), + secret: &maker_secret, + secret_hash: &maker_secret_hash, + swap_contract_address: &None, + swap_unique_data: &[], + watcher_reward: false, + }; + + let taker_spends_maker_payment_tx = match taker_sia_coin + .send_taker_spends_maker_payment(taker_spend_payment_args) + .await + .unwrap() + { + TransactionEnum::SiaTransaction(tx) => tx, + _ => panic!("Expected SiaTransaction"), + }; + maker_sia_coin.client.mine_blocks(1, &maker_address).await.unwrap(); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + let event = maker_sia_coin + .client + .get_event(&taker_spends_maker_payment_tx.txid()) + .await + .unwrap(); + assert_eq!(event.confirmations, 1u64); +} + +/** + * Initialize ctx and SiaCoin for both parties, maker and taker + * Initialize a new SiaCoin testnet and mine blocks to taker for funding + * Send a HTLC payment from taker + * Spend the HTLC payment from maker + */ +#[tokio::test] +async fn test_send_taker_payment_then_spend_taker_payment() { + let docker = Cli::default(); + + // Start the container + let (_container, host_port) = init_walletd_container(&docker); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + let taker_ctx = init_ctx("taker passphrase", 9995).await; + let taker_sia_coin = init_siacoin(taker_ctx, "TSIA", &helper_activation_request(host_port)).await; + let taker_public_key = taker_sia_coin.my_keypair().unwrap().public(); + let taker_address = taker_public_key.address(); + taker_sia_coin.client.mine_blocks(201, &taker_address).await.unwrap(); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + let maker_ctx = init_ctx("maker passphrase", 9995).await; + let maker_sia_coin = init_siacoin(maker_ctx, "TSIA", &helper_activation_request(host_port)).await; + let maker_public_key = maker_sia_coin.my_keypair().unwrap().public(); + let maker_secret = vec![0u8; 32]; + let maker_secret_hash = SecretHashAlgo::SHA256.hash_secret(&maker_secret); + + let negotiated_time_lock = now_sec(); + let negotiated_time_lock_duration = 10u64; + let negotiated_amount: BigDecimal = 1u64.into(); + + let taker_send_payment_args = SendPaymentArgs { + time_lock_duration: negotiated_time_lock_duration, + time_lock: negotiated_time_lock, + other_pubkey: maker_public_key.as_bytes(), + secret_hash: &maker_secret_hash, + amount: negotiated_amount, + swap_contract_address: &None, + swap_unique_data: &[], + payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, + }; + let taker_payment_tx = match taker_sia_coin + .send_taker_payment(taker_send_payment_args) + .await + .unwrap() + { + TransactionEnum::SiaTransaction(tx) => tx, + _ => panic!("Expected SiaTransaction"), + }; + taker_sia_coin.client.mine_blocks(1, &taker_address).await.unwrap(); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + let maker_spend_payment_args = SpendPaymentArgs { + other_payment_tx: &taker_payment_tx.tx_hex(), + time_lock: negotiated_time_lock, + other_pubkey: taker_public_key.as_bytes(), + secret: &maker_secret, + secret_hash: &maker_secret_hash, + swap_contract_address: &None, + swap_unique_data: &[], + watcher_reward: false, + }; + + let maker_spends_taker_payment_tx = match maker_sia_coin + .send_maker_spends_taker_payment(maker_spend_payment_args) + .await + .unwrap() + { + TransactionEnum::SiaTransaction(tx) => tx, + _ => panic!("Expected SiaTransaction"), + }; + taker_sia_coin.client.mine_blocks(1, &taker_address).await.unwrap(); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + taker_sia_coin + .client + .get_transaction(&maker_spends_taker_payment_tx.txid()) + .await + .unwrap(); +} + +#[tokio::test] +async fn test_send_maker_payment_then_refund_maker_payment() { + let docker = Cli::default(); + + // Start the container + let (_container, host_port) = init_walletd_container(&docker); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + let maker_ctx = init_ctx("maker passphrase", 9995).await; + let maker_sia_coin = init_siacoin(maker_ctx, "TSIA", &helper_activation_request(host_port)).await; + let maker_public_key = maker_sia_coin.my_keypair().unwrap().public(); + let maker_address = maker_public_key.address(); + let maker_secret = vec![0u8; 32]; + let maker_secret_hash = SecretHashAlgo::SHA256.hash_secret(&maker_secret); + maker_sia_coin.client.mine_blocks(201, &maker_address).await.unwrap(); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + let taker_ctx = init_ctx("taker passphrase", 9995).await; + let taker_sia_coin = init_siacoin(taker_ctx, "TSIA", &helper_activation_request(host_port)).await; + let taker_public_key = taker_sia_coin.my_keypair().unwrap().public(); + + // time lock is set in the past to allow immediate refund + let negotiated_time_lock = now_sec() - 1000; + let negotiated_time_lock_duration = 10u64; + let negotiated_amount: BigDecimal = 1u64.into(); + + let maker_send_payment_args = SendPaymentArgs { + time_lock_duration: negotiated_time_lock_duration, + time_lock: negotiated_time_lock, + other_pubkey: taker_public_key.as_bytes(), + secret_hash: &maker_secret_hash, + amount: negotiated_amount, + swap_contract_address: &None, + swap_unique_data: &[], + payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, + }; + let maker_payment_tx = match maker_sia_coin + .send_maker_payment(maker_send_payment_args) + .await + .unwrap() + { + TransactionEnum::SiaTransaction(tx) => tx, + _ => panic!("Expected SiaTransaction"), + }; + maker_sia_coin.client.mine_blocks(1, &maker_address).await.unwrap(); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + let secret_hash_type = SwapTxTypeWithSecretHash::TakerOrMakerPayment { + maker_secret_hash: &maker_secret_hash, + }; + let maker_refunds_payment_args = RefundPaymentArgs { + payment_tx: &maker_payment_tx.tx_hex(), + time_lock: negotiated_time_lock, + other_pubkey: taker_public_key.as_bytes(), + tx_type_with_secret_hash: secret_hash_type, + swap_contract_address: &None, + swap_unique_data: &[], + watcher_reward: false, + }; + + let maker_refunds_maker_payment_tx = match maker_sia_coin + .send_maker_refunds_payment(maker_refunds_payment_args) + .await + .unwrap() + { + TransactionEnum::SiaTransaction(tx) => tx, + _ => panic!("Expected SiaTransaction"), + }; + maker_sia_coin.client.mine_blocks(1, &maker_address).await.unwrap(); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + maker_sia_coin + .client + .get_transaction(&maker_refunds_maker_payment_tx.txid()) + .await + .unwrap(); +} + +#[tokio::test] +async fn test_send_taker_payment_then_refund_taker_payment() { + let docker = Cli::default(); + + // Start the container + let (_container, host_port) = init_walletd_container(&docker); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + let maker_ctx = init_ctx("maker passphrase", 9995).await; + let maker_sia_coin = init_siacoin(maker_ctx, "TSIA", &helper_activation_request(host_port)).await; + let maker_public_key = maker_sia_coin.my_keypair().unwrap().public(); + let maker_secret = vec![0u8; 32]; + let maker_secret_hash = SecretHashAlgo::SHA256.hash_secret(&maker_secret); + + let taker_ctx = init_ctx("taker passphrase", 9995).await; + let taker_sia_coin = init_siacoin(taker_ctx, "TSIA", &helper_activation_request(host_port)).await; + let taker_public_key = taker_sia_coin.my_keypair().unwrap().public(); + let taker_address = taker_public_key.address(); + taker_sia_coin.client.mine_blocks(201, &taker_address).await.unwrap(); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + // time lock is set in the past to allow immediate refund + let negotiated_time_lock = now_sec() - 1000; + let negotiated_time_lock_duration = 10u64; + let negotiated_amount: BigDecimal = 1u64.into(); + + let taker_send_payment_args = SendPaymentArgs { + time_lock_duration: negotiated_time_lock_duration, + time_lock: negotiated_time_lock, + other_pubkey: maker_public_key.as_bytes(), + secret_hash: &maker_secret_hash, + amount: negotiated_amount, + swap_contract_address: &None, + swap_unique_data: &[], + payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, + }; + let taker_payment_tx = match taker_sia_coin + .send_maker_payment(taker_send_payment_args) + .await + .unwrap() + { + TransactionEnum::SiaTransaction(tx) => tx, + _ => panic!("Expected SiaTransaction"), + }; + taker_sia_coin.client.mine_blocks(1, &taker_address).await.unwrap(); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + let secret_hash_type = SwapTxTypeWithSecretHash::TakerOrMakerPayment { + maker_secret_hash: &maker_secret_hash, + }; + let taker_refunds_payment_args = RefundPaymentArgs { + payment_tx: &taker_payment_tx.tx_hex(), + time_lock: negotiated_time_lock, + other_pubkey: maker_public_key.as_bytes(), + tx_type_with_secret_hash: secret_hash_type, + swap_contract_address: &None, + swap_unique_data: &[], + watcher_reward: false, + }; + + let taker_refunds_taker_payment_tx = match taker_sia_coin + .send_taker_refunds_payment(taker_refunds_payment_args) + .await + .unwrap() + { + TransactionEnum::SiaTransaction(tx) => tx, + _ => panic!("Expected SiaTransaction"), + }; + taker_sia_coin.client.mine_blocks(1, &taker_address).await.unwrap(); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + taker_sia_coin + .client + .get_transaction(&taker_refunds_taker_payment_tx.txid()) + .await + .unwrap(); +} + +#[tokio::test] +async fn test_spend_taker_payment_then_taker_extract_secret() { + let docker = Cli::default(); + + // Start the container + let (_container, host_port) = init_walletd_container(&docker); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + let taker_ctx = init_ctx("taker passphrase", 9995).await; + let taker_sia_coin = init_siacoin(taker_ctx, "TSIA", &helper_activation_request(host_port)).await; + let taker_public_key = taker_sia_coin.my_keypair().unwrap().public(); + let taker_address = taker_public_key.address(); + taker_sia_coin.client.mine_blocks(201, &taker_address).await.unwrap(); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + let maker_ctx = init_ctx("maker passphrase", 9995).await; + let maker_sia_coin = init_siacoin(maker_ctx, "TSIA", &helper_activation_request(host_port)).await; + let maker_public_key = maker_sia_coin.my_keypair().unwrap().public(); + let maker_secret = vec![0u8; 32]; + let maker_secret_hash = SecretHashAlgo::SHA256.hash_secret(&maker_secret); + + let negotiated_time_lock = now_sec(); + let negotiated_time_lock_duration = 10u64; + let negotiated_amount: BigDecimal = 1u64.into(); + + let taker_send_payment_args = SendPaymentArgs { + time_lock_duration: negotiated_time_lock_duration, + time_lock: negotiated_time_lock, + other_pubkey: maker_public_key.as_bytes(), + secret_hash: &maker_secret_hash, + amount: negotiated_amount, + swap_contract_address: &None, + swap_unique_data: &[], + payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, + }; + let taker_payment_tx = match taker_sia_coin + .send_taker_payment(taker_send_payment_args) + .await + .unwrap() + { + TransactionEnum::SiaTransaction(tx) => tx, + _ => panic!("Expected SiaTransaction"), + }; + taker_sia_coin.client.mine_blocks(1, &taker_address).await.unwrap(); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + let maker_spend_payment_args = SpendPaymentArgs { + other_payment_tx: &taker_payment_tx.tx_hex(), + time_lock: negotiated_time_lock, + other_pubkey: taker_public_key.as_bytes(), + secret: &maker_secret, + secret_hash: &maker_secret_hash, + swap_contract_address: &None, + swap_unique_data: &[], + watcher_reward: false, + }; + + let maker_spends_taker_payment_tx = match maker_sia_coin + .send_maker_spends_taker_payment(maker_spend_payment_args) + .await + .unwrap() + { + TransactionEnum::SiaTransaction(tx) => tx, + _ => panic!("Expected SiaTransaction"), + }; + taker_sia_coin.client.mine_blocks(1, &taker_address).await.unwrap(); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + taker_sia_coin + .client + .get_transaction(&maker_spends_taker_payment_tx.txid()) + .await + .unwrap(); + + let maker_spends_taker_payment_tx_hex = maker_spends_taker_payment_tx.tx_hex(); + + let taker_extracted_secret = taker_sia_coin + .extract_secret(&maker_secret_hash, maker_spends_taker_payment_tx_hex.as_slice(), false) + .await + .unwrap(); + + assert_eq!(taker_extracted_secret, maker_secret); +} +*/ diff --git a/mm2src/mm2_main/tests/sia_tests/mod.rs b/mm2src/mm2_main/tests/sia_tests/mod.rs new file mode 100644 index 0000000000..f5e712ddaa --- /dev/null +++ b/mm2src/mm2_main/tests/sia_tests/mod.rs @@ -0,0 +1,4 @@ +mod docker_functional_tests; +mod short_locktime_tests; + +pub(crate) mod utils; diff --git a/mm2src/mm2_main/tests/sia_tests/short_locktime_tests.rs b/mm2src/mm2_main/tests/sia_tests/short_locktime_tests.rs new file mode 100644 index 0000000000..7033535533 --- /dev/null +++ b/mm2src/mm2_main/tests/sia_tests/short_locktime_tests.rs @@ -0,0 +1,171 @@ +use crate::docker_tests::docker_tests_common::{fund_privkey_utxo, random_secp256k1_secret}; + +use super::utils::*; + +use mm2_test_helpers::for_tests::{start_swaps, wait_until_event}; + +/* +KDF currently sits in an odd state between a binary and a library. These tests fall between a +"unit test" and a "integration test" due to this. + +These Sia "functional tests" are running multiple KDF instances(multiple MmCtx using lp_init) within +the same process. This was not supported until now, and we encounter some issues with it. + +The "payment_locktime" conf field used to set the HTLC locktime. + +This "short_locktime_tests" module is an extension of "docker_functional_tests" and is simply a hack +to allow grouping the relevant tests together via `cargo test` commands. The tests in this module will +use a custom locktime of 60 seconds. + +The "docker_functional_tests" will hold any tests that +can use the default of 900 seconds (CUSTOM_PAYMENT_LOCKTIME_DEFAULT). +*/ +/// Initialize Alice and Bob, initialize Sia testnet container, initialize UTXO testnet container, +/// Bob sells DSIA for Alice's MYCOIN +/// Alice pays fee, Bob locks payment, Alice disappears prior to locking her payment +#[tokio::test] +async fn test_bob_sells_dsia_for_mycoin_alice_fails_to_lock() { + let alice_priv = random_secp256k1_secret(); + let bob_priv = random_secp256k1_secret(); + + // Give bob some sia and alice some mycoin + fund_privkey_sia(&bob_priv, Currency(1e23 as u128)).await; + fund_privkey_utxo("MYCOIN", 5.into(), &alice_priv).await; + + // Initalize Alice and Bob KDF instances + let mut mm_bob = init_bob(&bob_priv, Some(60)).await; + let mut mm_alice = init_alice(&alice_priv, &mm_bob.ip, Some(60)).await; + + // Enable DSIA coin for Alice and Bob + let _ = enable_dsia(&mm_bob).await; + let _ = enable_dsia(&mm_alice).await; + + // Enable MYCOIN coin via Native node for Alice and Bob + let _ = enable_mycoin(&mm_alice).await; + let _ = enable_mycoin(&mm_bob).await; + + // Wait for Alice and Bob KDF instances to connect + wait_for_peers_connected(&mm_alice, &mm_bob, std::time::Duration::from_secs(30)) + .await + .unwrap(); + + // Start a swap where Bob sells DSIA for Alice's MYCOIN + let uuid = start_swaps(&mut mm_bob, &mut mm_alice, &[("DSIA", "MYCOIN")], 1., 1., 0.05) + .await + .first() + .cloned() + .unwrap(); + + // Stop Alice before she locks her payment + wait_until_event(&mm_alice, &uuid, "TakerFeeSent", 600).await; + mm_alice.stop().await.unwrap(); + + // Wait for the swap to complete + wait_until_event(&mm_bob, &uuid, "MakerPaymentRefundFinished", 600).await; +} + +/// Initialize Alice and Bob, initialize Sia testnet container, initialize UTXO testnet container, +/// Bob sells DSIA for Alice's MYCOIN +/// Alice pays fee, Bob locks payment, Alice locks payment, Bob disappears prior to spending Alice's +/// payment, Alice refunds her payment, Bob refunds his payment +#[tokio::test] +async fn bob_sells_dsia_for_mycoin_bob_fails_to_spend() { + let alice_priv = random_secp256k1_secret(); + let bob_priv = random_secp256k1_secret(); + + // Give bob some sia and alice some mycoin + fund_privkey_sia(&bob_priv, Currency(1e23 as u128)).await; + fund_privkey_utxo("MYCOIN", 5.into(), &alice_priv).await; + + // Initalize Alice and Bob KDF instances + let mut mm_bob = init_bob(&bob_priv, Some(60)).await; + let mut mm_alice = init_alice(&alice_priv, &mm_bob.ip, Some(60)).await; + + // Enable DSIA coin for Alice and Bob + let _ = enable_dsia(&mm_bob).await; + let _ = enable_dsia(&mm_alice).await; + + // Enable MYCOIN coin via Native node for Alice and Bob + let _ = enable_mycoin(&mm_alice).await; + let _ = enable_mycoin(&mm_bob).await; + + // Wait for Alice and Bob KDF instances to connect + wait_for_peers_connected(&mm_alice, &mm_bob, std::time::Duration::from_secs(30)) + .await + .unwrap(); + + // Start a swap where Bob sells DSIA for Alice's MYCOIN + let uuid = start_swaps(&mut mm_bob, &mut mm_alice, &[("DSIA", "MYCOIN")], 1., 1., 0.05) + .await + .first() + .cloned() + .unwrap(); + + // Stop Bob before he spends Alice's payment + wait_until_event(&mm_bob, &uuid, "MakerPaymentSent", 600).await; + mm_bob.stop().await.unwrap(); + + // Wait for Alice to refund alice_payment + wait_until_event(&mm_alice, &uuid, "TakerPaymentRefundFinished", 600).await; + + // Restart Bob and activate coins + let mm_bob = re_init_bob(&mm_bob, &bob_priv, Some(60)).await; + let _ = enable_dsia(&mm_bob).await; + let _ = enable_mycoin(&mm_bob).await; + + // Wait for Bob to refund bob_payment + wait_until_event(&mm_bob, &uuid, "MakerPaymentRefundFinished", 600).await; +} + +/// Initialize Alice and Bob, initialize Sia testnet container, initialize UTXO testnet container, +/// Bob sells MYCOIN for Alice's DSIA +/// Alice pays fee, Bob locks payment, Alice locks payment, Bob disappears prior to spending Alice's +/// payment, Alice refunds her payment, Bob refunds his payment +#[tokio::test] +async fn bob_sells_mycoin_for_dsia_bob_fails_to_spend() { + let alice_priv = random_secp256k1_secret(); + let bob_priv = random_secp256k1_secret(); + + // Give alice some sia and bob some mycoin + fund_privkey_sia(&alice_priv, Currency(1e23 as u128)).await; + fund_privkey_utxo("MYCOIN", 5.into(), &bob_priv).await; + + // Initalize Alice and Bob KDF instances + let mut mm_bob = init_bob(&bob_priv, Some(60)).await; + let mut mm_alice = init_alice(&alice_priv, &mm_bob.ip, Some(60)).await; + + // Enable DSIA coin for Alice and Bob + let _ = enable_dsia(&mm_bob).await; + let _ = enable_dsia(&mm_alice).await; + + // Enable MYCOIN coin via Native node for Alice and Bob + let _ = enable_mycoin(&mm_alice).await; + let _ = enable_mycoin(&mm_bob).await; + + // Wait for Alice and Bob KDF instances to connect + wait_for_peers_connected(&mm_alice, &mm_bob, std::time::Duration::from_secs(30)) + .await + .unwrap(); + + // Start a swap where Bob sells DSIA for Alice's MYCOIN + let uuid = start_swaps(&mut mm_bob, &mut mm_alice, &[("MYCOIN", "DSIA")], 1., 1., 0.05) + .await + .first() + .cloned() + .unwrap(); + + // Stop Bob before he spends Alice's payment + wait_until_event(&mm_bob, &uuid, "MakerPaymentSent", 600).await; + mm_bob.stop().await.unwrap(); + + // Wait for Alice to refund alice_payment + wait_until_event(&mm_alice, &uuid, "TakerPaymentRefundFinished", 600).await; + + // Restart Bob and activate coins + let mm_bob = re_init_bob(&mm_bob, &bob_priv, Some(60)).await; + let _ = enable_dsia(&mm_bob).await; + let _ = enable_mycoin(&mm_bob).await; + + // Wait for Bob to refund bob_payment + wait_until_event(&mm_bob, &uuid, "MakerPaymentRefundFinished", 600).await; +} diff --git a/mm2src/mm2_main/tests/sia_tests/utils.rs b/mm2src/mm2_main/tests/sia_tests/utils.rs new file mode 100644 index 0000000000..f609fbd70c --- /dev/null +++ b/mm2src/mm2_main/tests/sia_tests/utils.rs @@ -0,0 +1,388 @@ +pub use coins::siacoin::sia_rust::types::{Address, Currency, Keypair}; +pub use coins::siacoin::sia_rust::utils::V2TransactionBuilder; + +use coins::siacoin::{ApiClientHelpers, SiaApiClient, SiaClient, SiaClientConf}; +use keys::hash::H256; + +use crate::docker_tests::docker_tests_common::SIA_RPC_PARAMS; +use common::custom_futures::timeout::FutureTimerExt; +use common::executor::Timer; +use mm2_rpc::data::legacy::CoinInitResponse; +use mm2_test_helpers::for_tests::{MarketMakerIt, Mm2TestConf}; + +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; +use serde_json::Value as Json; +use std::collections::HashMap; +use std::net::IpAddr; +use std::time::Duration; +use tokio::sync::Mutex; +use url::Url; // for read_line() + +mod komodod_client; +pub use komodod_client::*; + +pub const WALLETD_CONFIG: &str = r#" +http: + address: :9980 + password: password + publicEndpoints: false +index: + mode: full +log: + stdout: + enabled: true + level: debug + format: human +"#; + +pub const WALLETD_NETWORK_CONFIG: &str = r#"{ + "network": { + "name": "komodo-ci", + "initialCoinbase": "300000000000000000000000000000", + "minimumCoinbase": "30000000000000000000000000000", + "initialTarget": "0100000000000000000000000000000000000000000000000000000000000000", + "blockInterval": 60000000000, + "maturityDelay": 10, + "hardforkDevAddr": { + "height": 1, + "oldAddress": "000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69", + "newAddress": "000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69" + }, + "hardforkTax": { + "height": 2 + }, + "hardforkStorageProof": { + "height": 5 + }, + "hardforkOak": { + "height": 10, + "fixHeight": 12, + "genesisTimestamp": "2023-01-13T00:53:20-08:00" + }, + "hardforkASIC": { + "height": 20, + "oakTime": 600000000000, + "oakTarget": "0100000000000000000000000000000000000000000000000000000000000000", + "nonceFactor": 1009 + }, + "hardforkFoundation": { + "height": 30, + "primaryAddress": "053b2def3cbdd078c19d62ce2b4f0b1a3c5e0ffbeeff01280efb1f8969b2f5bb4fdc680f0807", + "failsafeAddress": "000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69" + }, + "hardforkV2": { + "allowHeight": 0, + "requireHeight": 7777777, + "finalCutHeight": 8888888 + } + }, + "genesis": { + "parentID": "0000000000000000000000000000000000000000000000000000000000000000", + "nonce": 0, + "timestamp": "2023-01-13T00:53:20-08:00", + "minerPayouts": null, + "transactions": [ + { + "id": "268ef8627241b3eb505cea69b21379c4b91c21dfc4b3f3f58c66316249058cfd", + "siacoinOutputs": [ + { + "value": "1000000000000000000000000000000000000", + "address": "a0cfbc1089d129f52d00bc0b0fac190d4d87976a1d7f34da7ca0c295c99a628de344d19ad469" + } + ], + "siafundOutputs": [ + { + "value": 10000, + "address": "053b2def3cbdd078c19d62ce2b4f0b1a3c5e0ffbeeff01280efb1f8969b2f5bb4fdc680f0807" + } + ] + } + ] + } +}"#; + +lazy_static! { + pub static ref COINS: Json = json!( + [ + // Dockerized Sia coin + { + "coin": "DSIA", + "mm2": 1, + "required_confirmations": 1, + "protocol": { + "type": "SIA" + } + }, + // Dockerized UTXO coin + { + "coin":"MYCOIN", + "asset":"MYCOIN", + "mm2": 1, + "txversion":4, + "overwintered":1, + "protocol":{ + "type":"UTXO" + } + } + ] + ); + + /// A Sia Address that is not Alice's or Bob's. Global walletd container will mine to this address. + /// iguana seed "neutral neutral neutral neutral neutral neutral neutral neutral neutral neutral neutral noise" + pub static ref CHARLIE_SIA_KEYPAIR: Keypair = Keypair::from_private_bytes(&[ + 0x38, 0x9d, 0xd4, 0xd0, 0x09, 0xe6, 0xb1, 0x1d, + 0xb0, 0xf1, 0x55, 0x16, 0xbc, 0x29, 0x0e, 0x7b, + 0xa0, 0xcc, 0x58, 0x09, 0x30, 0xac, 0xe2, 0x00, + 0x5d, 0x39, 0xd0, 0xe4, 0x97, 0xb4, 0xa6, 0x67 + ]).unwrap(); + + /// Sia Address of CHARLIE_SIA_KEYPAIR + pub static ref CHARLIE_SIA_ADDRESS: Address = CHARLIE_SIA_KEYPAIR.public().address(); +} + +/// Send coins from Charlie to the given address. +/// Assumes Charlie has enough coins to send. +pub async fn fund_address(address: &Address, amount: Currency) { + lazy_static! { + static ref SIA_FUNDING_LOCK: Mutex<()> = Mutex::new(()); + } + // Lock the funding operation so to not let multiple tests fund address from the same utxos and double spend them. + let _lock = SIA_FUNDING_LOCK.lock().await; + + let client = init_sia_client().await.unwrap(); + + let tx = V2TransactionBuilder::new() + .miner_fee(Currency::DEFAULT_FEE) + .add_siacoin_output((address.clone(), amount).into()) + .fund_tx_single_source(&client, &CHARLIE_SIA_KEYPAIR.public()) + .await + .expect("fund_address helper failed at fund_tx_single_source") + .add_change_output(&CHARLIE_SIA_KEYPAIR.public().address()) + .sign_simple(vec![&CHARLIE_SIA_KEYPAIR]) + .build(); + + // Broadcast the transaction + client.broadcast_transaction(&tx).await.unwrap(); + // Mine some blocks to confirm the transaction + client.mine_blocks(10, &CHARLIE_SIA_ADDRESS).await.unwrap(); +} + +pub async fn fund_privkey_sia(priv_key: &H256, amount: Currency) { + let keypair = Keypair::from_private_bytes(priv_key.as_slice()).unwrap(); + let address = Address::from_public_key(&keypair.public()); + fund_address(&address, amount).await; +} + +/// Response from `get_directly_connected_peers` RPC endpoint. +/// eg, {"": ["", ""], "": [""]}} +/// TODO: Should technically be HashMap> but not needed for current use cases. +#[derive(Debug, Serialize, Deserialize)] +#[serde(transparent, rename = "result")] +pub struct GetDirectlyConnectedPeersResponse(pub HashMap>); + +pub async fn enable_dsia(mm: &MarketMakerIt) -> CoinInitResponse { + let (ip, port, password) = SIA_RPC_PARAMS; + let url = format!("http://{ip}:{port}/"); + mm.rpc_typed::(&json!({ + "method": "enable", + "coin": "DSIA", + "tx_history": true, + "client_conf": { + "server_url": url, + "password": password, + } + })) + .await + .unwrap() +} + +pub async fn enable_mycoin(mm: &MarketMakerIt) -> CoinInitResponse { + mm.rpc_typed::(&json!({ + "method": "enable", + "coin": "MYCOIN", + "tx_history": true + })) + .await + .unwrap() +} + +/** +Initialize a MarketMaker instance with a configuration suitable for the taker aka Alice. + +Intended to be used in conjunction with `init_bob` to create a taker/maker setup. + +This node will not act as a seed node and will not listen on the p2p port. +**/ +pub async fn init_alice(priv_key: &H256, seednode_ip: &IpAddr, custom_locktime: Option) -> MarketMakerIt { + let seed = format!("0x{}", hex::encode(priv_key)); + let mut conf = Mm2TestConf::light_node(&seed, &COINS, &[&seednode_ip.to_string()]); + if let Some(lt) = custom_locktime { + conf.conf["payment_locktime"] = lt.into(); + } + let mm = MarketMakerIt::start_async(conf.conf, conf.rpc_password, None) + .await + .unwrap(); + + //let (_dump_log, _dump_dashboard) = mm.mm_dump(); + log!("alice's log path: {}", mm.log_path.display()); + + mm +} + +/** +Initialize a MarketMaker instance with a configuration suitable for the maker aka Bob. + +Intended to be used in conjunction with `init_alice` to create a taker/maker setup. + +This node will act as a seed node and will listen on the p2p port. +**/ +pub async fn init_bob(priv_key: &H256, custom_locktime: Option) -> MarketMakerIt { + let seed = format!("0x{}", hex::encode(priv_key)); + let mut conf = Mm2TestConf::seednode(&seed, &COINS); + if let Some(lt) = custom_locktime { + conf.conf["payment_locktime"] = lt.into(); + } + let mm = MarketMakerIt::start_async(conf.conf, conf.rpc_password, None) + .await + .unwrap(); + + //let (_dump_log, _dump_dashboard) = mm.mm_dump(); + log!("bob's log path: {}", mm.log_path.display()); + + mm +} + +/// Re-initialize Bob's MarketMaker instance with the same configuration but a new instance. +/// This is useful to simulate Bob going offline and then coming back online. +pub async fn re_init_bob(mm: &MarketMakerIt, priv_key: &H256, custom_locktime: Option) -> MarketMakerIt { + let seed = format!("0x{}", hex::encode(priv_key)); + let mut conf = Mm2TestConf::seednode(&seed, &COINS); + conf.conf["dbdir"] = mm.folder.join("DB").to_str().unwrap().into(); + conf.conf["log"] = mm.folder.join("mm2_dup.log").to_str().unwrap().into(); + if let Some(lt) = custom_locktime { + conf.conf["payment_locktime"] = lt.into(); + } + MarketMakerIt::start_async(conf.conf, conf.rpc_password, None) + .await + .unwrap() +} + +/// Initialize a Sia standalone SiaClient. +/// This is useful to interact with a Sia testnet container for commands that are not from Alice or +/// Bob. Eg, mining blocks to progress the chain. +pub async fn init_sia_client() -> Result { + let (ip, port, password) = SIA_RPC_PARAMS; + let conf = SiaClientConf { + server_url: Url::parse(&format!("http://{}:{}/", ip, port)).unwrap(), + password: Some(password.to_string()), + timeout: Some(10), + }; + SiaClient::new(conf).await.map_err(|e| e.to_string()) +} + +/// Wait for the global Dsia node to be ready by polling the current_height endpoint. +/// Panics if the node is not ready after several attempts. +/// Spawns a mining thread that will mine blocks every 10 seconds to advance the chain. +pub async fn wait_for_dsia_node_ready() { + let mut attempts = 0; + loop { + if attempts >= 5 { + panic!("Failed to connect to Dsia node after several attempts."); + } + + match init_sia_client().await { + Ok(client) => match client.current_height().timeout(Duration::from_secs(6)).await { + Ok(Ok(block_number)) => { + log!("Dsia node is ready, latest block number: {:?}", block_number); + break; + }, + Ok(Err(e)) => { + log!("Failed to connect to Dsia node: {:?}, retrying...", e); + }, + Err(_) => { + log!("Connection to Dsia node timed out, retrying..."); + }, + }, + Err(e) => { + log!("Failed to create Sia client: {:?}, retrying...", e); + }, + }; + + attempts += 1; + Timer::sleep(1.).await; + } + + let client = init_sia_client().await.unwrap(); + // Mine 155 blocks to begin because coinbase maturity is 150 + client.mine_blocks(155, &CHARLIE_SIA_ADDRESS).await.unwrap(); + + // Spawn a loop that will keep mining blocks every 10 seconds to advance the chain + // and get the swap tests running. + tokio::spawn(async move { + loop { + tokio::time::sleep(Duration::from_secs(10)).await; + client.mine_blocks(1, &CHARLIE_SIA_ADDRESS).await.unwrap(); + common::log::debug!("Mined 1 block on global DSIA container"); + } + }); +} + +/// Connects to the the already initilized komodod container (running MYCOIN) and funds `funded_address` with some coins. +/// Also imports both `funded_address` and `unfunded_address` addresses into the node. +pub async fn get_komodod_client(funded_address: &str, unfunded_address: &str) -> KomododClient { + let conf = KomododClientConf { + // This is where MYCOIN node runs. + // TODO: make a global constant for this. + ip: IpAddr::from([127, 0, 0, 1]), + port: 8000, + rpcuser: "test".to_string(), + rpcpassword: "test".to_string(), + timeout: None, + }; + let client = KomododClient::new(conf).await; + + // Send 1,000,000 coins to funded_address + let _ = client.rpc("sendtoaddress", json!([funded_address, 1000000])).await; + + // Import both addresses to our node. + let _ = client.rpc("importaddress", json!([funded_address])).await; + let _ = client.rpc("importaddress", json!([unfunded_address])).await; + + client +} + +// Wait until Alice connects to Bob as a peer or timeout +pub async fn wait_for_peers_connected( + alice: &MarketMakerIt, + bob: &MarketMakerIt, + timeout_duration: Duration, +) -> Result<(), ()> { + let start_time = tokio::time::Instant::now(); + + // fetch Bob's PeerId + let bob_peer_id = bob + .rpc_typed::(&json!({"method": "get_my_peer_id"})) + .await + .unwrap(); + + loop { + // fetch Alice's connected peers + let alice_peers = alice + .rpc_typed::(&json!({"method": "get_directly_connected_peers"})) + .await + .unwrap(); + + // Check if Bob's PeerId is in Alice's connected peers + if alice_peers.0.contains_key(&bob_peer_id) { + return Ok(()); + } + + // Check if we've reached the timeout + if start_time.elapsed() >= timeout_duration { + return Err(()); // Timed out + } + + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + } +} diff --git a/mm2src/mm2_main/tests/sia_tests/utils/komodod_client.rs b/mm2src/mm2_main/tests/sia_tests/utils/komodod_client.rs new file mode 100644 index 0000000000..319c25bf1b --- /dev/null +++ b/mm2src/mm2_main/tests/sia_tests/utils/komodod_client.rs @@ -0,0 +1,97 @@ +//! Extremely bare-bones `komodod` client to facilitate Sia functional and integration tests. +//! +//! This client is **not intended for production use**. +//! It offers **no error handling**, and `.unwrap()` is used liberally, +//! as it is expected to **panic on any error**. + +use base64::engine::general_purpose::STANDARD as BASE64; +use base64::Engine; +use http::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE}; +use reqwest::Client as ReqwestClient; +use std::net::IpAddr; +use std::time::Duration; +use url::Url; + +pub struct KomododClientConf { + pub ip: IpAddr, + pub port: u16, + pub rpcuser: String, + pub rpcpassword: String, + pub timeout: Option, +} + +pub struct KomododClient { + pub client: ReqwestClient, + pub url: Url, +} + +impl KomododClient { + /// Create a new KomododClient. Note, does not actually ping the server. + pub async fn new(conf: KomododClientConf) -> Self { + // Construct default headers for all requests + let mut headers = HeaderMap::new(); + + // Set Authorization header with given rpcuser and rpcpassword + let auth_value = format!( + "Basic {}", + BASE64.encode(format!("{}:{}", conf.rpcuser, conf.rpcpassword)) + ); + headers.insert(AUTHORIZATION, HeaderValue::from_str(&auth_value).unwrap()); + + // Set Content-Type header to application/json + headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + + // set a timeout for http requests + let timeout = conf.timeout.unwrap_or(15); + + let url: Url = format!("http://{}:{}/", conf.ip, conf.port).parse().unwrap(); + + let client = ReqwestClient::builder() + .default_headers(headers) + .timeout(Duration::from_secs(timeout)) + .build() + .unwrap(); + + let client = KomododClient { client, url }; + + let _getinfo = client.rpc("getinfo", json!([])).await; + client + } + + pub async fn rpc(&self, method: &str, params: serde_json::Value) -> serde_json::Value { + let payload = json!({ + "jsonrpc": "1.0", + "id": "sia_tests_utils", + "method": method, + "params": params + }); + common::log::debug!("Sending komodod RPC request: {}", payload); + + let mut attempts = 0; + let max_retries = 3; + loop { + match self.client.post(self.url.clone()).json(&payload).send().await { + Ok(response) => { + let json_response: serde_json::Value = response.json().await.unwrap(); + common::log::debug!("Received komodod RPC response: {}", json_response); + return json_response; + }, + Err(err) => { + attempts += 1; + if attempts >= max_retries { + common::log::debug!("RPC request failed after {} attempts: {}", max_retries, err); + panic!("RPC request failed: {}", err); + } else { + common::log::debug!( + "RPC request attempt {}/{} failed: {}. Retrying...", + attempts, + max_retries, + err + ); + tokio::time::sleep(Duration::from_secs(1)).await; // Add a short delay before retrying + } + }, + } + } + } +} diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 0b69ad9b55..ca48513a21 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -18,11 +18,12 @@ use mm2_metrics::{MetricType, MetricsJson}; use mm2_number::BigDecimal; use mm2_rpc::data::legacy::{BalanceResponse, ElectrumProtocol}; use rand::Rng; -use serde::Serialize; +use serde::{Deserialize, Deserializer, Serialize}; use serde_json::{self as json, json, Value as Json}; use std::collections::HashMap; use std::convert::TryFrom; use std::env; +use std::fmt::Debug; use std::net::IpAddr; use std::num::NonZeroUsize; use std::process::Child; @@ -41,7 +42,6 @@ cfg_native! { use futures::task::SpawnExt; use http::Request; use regex::Regex; - use serde::Deserialize; use std::fs; use std::io::Write; use std::net::Ipv4Addr; @@ -263,6 +263,53 @@ pub const ETH_SEPOLIA_TOKEN_CONTRACT: &str = "0x09d0d71FBC00D7CCF9CFf132f5E6825C pub const BCHD_TESTNET_URLS: &[&str] = &["https://bchd-testnet.greyh.at:18335"]; +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum TypedRpcResponse { + Result { result: T }, + Error { error: String }, +} + +/// Custom wrapper type for deserializing API responses into `Result` +/// used by MarketMakerIt::rpc_typed +#[derive(Debug, Serialize)] +#[serde(transparent)] // Keep serialization the same as `Result` +pub struct RpcResult(pub Result); + +/// Custom deserialization logic for `RpcResult` +impl<'de, T> Deserialize<'de> for RpcResult +where + T: Deserialize<'de> + Debug, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + // Define an untagged enum that matches the API response + #[derive(Deserialize, Debug)] + #[serde(untagged)] + enum InternalRpcResponse { + // eg {"result": "foobar"} or {"result": {"foo": "bar"}} + Result { result: T }, + // eg {"result": "success", "foo": "bar"} + ResultFlattened(T), + Error { error: String }, + } + + // Deserialize into the internal representation first + let response = InternalRpcResponse::::deserialize(deserializer)?; + + // Convert into Result + let result = match response { + InternalRpcResponse::Result { result } => Ok(result), + InternalRpcResponse::ResultFlattened(result) => Ok(result), + InternalRpcResponse::Error { error } => Err(error), + }; + + Ok(RpcResult(result)) + } +} + pub struct Mm2TestConf { pub conf: Json, pub rpc_password: String, @@ -1313,6 +1360,8 @@ pub struct MarketMakerIt { pub folder: PathBuf, /// Unique (to run multiple instances) IP, like "127.0.0.$x". pub ip: IpAddr, + /// Port to bind RPC interface to on the given IP, defaults to 7783 if None. + pub rpc_port: Option, /// The file we redirected the standard output and error streams to. pub log_path: PathBuf, /// The PID of the MarketMaker process. @@ -1372,7 +1421,11 @@ impl MarketMakerIt { ) -> Result { conf["allow_weak_password"] = true.into(); let ip = try_s!(Self::myipaddr_from_conf(&mut conf)); - let folder = new_mm2_temp_folder_path(Some(ip)); + let rpc_port = match conf["rpcport"].as_u64() { + Some(port) => Some(port as u16), + None => None, + }; + let folder = new_mm2_temp_folder_path(Some(ip), rpc_port); let db_dir = match conf["dbdir"].as_str() { Some(path) => path.into(), None => { @@ -1426,6 +1479,7 @@ impl MarketMakerIt { let mut mm = MarketMakerIt { folder, ip, + rpc_port, log_path, pc, userpass, @@ -1608,11 +1662,43 @@ impl MarketMakerIt { } } + /// Modifies the provided payload to include the stored `userpass` and calls the `rpc` method. + #[cfg(not(target_arch = "wasm32"))] + pub async fn rpc_with_stored_auth(&self, payload: &Json) -> Result<(StatusCode, String, HeaderMap), String> { + // Clone the payload to avoid requiring a mutable reference + let mut modified_payload = payload.clone(); + + // Ensure the payload is an object to insert the `userpass` + if let Some(payload_obj) = modified_payload.as_object_mut() { + // Insert the `userpass` into the payload + payload_obj.insert("userpass".to_string(), json!(self.userpass)); + } else { + return Err(format!("Expected payload to be a JSON object, but got: {}", payload)); + } + + // Call the existing `rpc` method with the modified payload + self.rpc(&modified_payload).await + } + + /// Calls the rpc_with_stored_auth method and deserializes the result into a typed value. + /// eg, mm.rpc_typed::(&json!({ "method": "my_method" })).await + #[cfg(not(target_arch = "wasm32"))] + pub async fn rpc_typed serde::Deserialize<'a> + Debug>(&self, payload: &Json) -> Result { + let (status, body, _headers) = self.rpc_with_stored_auth(payload).await?; + if status != StatusCode::OK { + return ERR!("RPC failed with status {}: {}", status, body); + } + let result: RpcResult = + serde_json::from_str(&body).map_err(|e| format!("Failed to parse JSON: {} body:{}", e, body))?; + result.0 + } + /// Invokes the locally running MM and returns its reply. #[cfg(not(target_arch = "wasm32"))] pub async fn rpc(&self, payload: &Json) -> Result<(StatusCode, String, HeaderMap), String> { - let uri = format!("http://{}:7783", self.ip); - log!("sending rpc request {} to {}", json::to_string(payload).unwrap(), uri); + let port = self.rpc_port.unwrap_or(7783); + let uri = format!("http://{}:{}", self.ip, port); + common::log::debug!("sending rpc request {} to {}", json::to_string(payload).unwrap(), uri); let payload = try_s!(json::to_vec(payload)); let request = try_s!(Request::builder().method("POST").uri(uri).body(payload)); @@ -2366,15 +2452,16 @@ pub async fn init_lightning_status(mm: &MarketMakerIt, task_id: u64) -> Json { /// Use a separate (unique) temporary folder for each MM. /// We could also remove the old folders after some time in order not to spam the temporary folder. /// Though we don't always want to remove them right away, allowing developers to check the files). -/// Appends IpAddr if it is pre-known +/// Appends IpAddr if it is pre-known. Appends port number if IpAddr and port are provided. #[cfg(not(target_arch = "wasm32"))] -pub fn new_mm2_temp_folder_path(ip: Option) -> PathBuf { +pub fn new_mm2_temp_folder_path(ip: Option, port: Option) -> PathBuf { let now = common::now_ms(); #[allow(deprecated)] let now = Local.timestamp((now / 1000) as i64, (now % 1000) as u32 * 1_000_000); - let folder = match ip { - Some(ip) => format!("mm2_{}_{}", now.format("%Y-%m-%d_%H-%M-%S-%3f"), ip), - None => format!("mm2_{}", now.format("%Y-%m-%d_%H-%M-%S-%3f")), + let folder = match (ip, port) { + (Some(ip), Some(port)) => format!("mm2_{}_{}_{}", now.format("%Y-%m-%d_%H-%M-%S-%3f"), ip, port), + (Some(ip), None) => format!("mm2_{}_{}", now.format("%Y-%m-%d_%H-%M-%S-%3f"), ip), + (None, _) => format!("mm2_{}", now.format("%Y-%m-%d_%H-%M-%S-%3f")), }; common::temp_dir().join(folder) } @@ -2459,6 +2546,65 @@ pub async fn wait_for_swap_finished(mm: &MarketMakerIt, uuid: &str, wait_sec: i6 } } +pub async fn wait_for_swap_finished_or_err(mm: &MarketMakerIt, uuid: &str, wait_sec: i64) -> Result<(), String> { + let wait_until = get_utc_timestamp() + wait_sec; + loop { + let swap_status = my_swap_status(mm, uuid).await.unwrap(); + if swap_status["result"]["is_finished"].as_bool().unwrap() { + match swap_status["result"]["is_success"].as_bool() { + Some(true) => return Ok(()), + _ => { + return Err(format!( + "Swap {} failed with status: {}", + uuid, + serde_json::to_string(&swap_status).unwrap() + )); + }, + } + } + + if get_utc_timestamp() > wait_until { + return Err(format!( + "Timed out waiting for swap {} to finish; latest status: {}", + uuid, + serde_json::to_string(&swap_status).unwrap() + )); + } + + Timer::sleep(0.5).await; + } +} + +/// Wait until the `event_str` appears in the swap's events or throw an error after `seconds` seconds. +pub async fn wait_until_event(mm: &MarketMakerIt, swap: &str, event_str: &str, seconds: i64) { + let started_at = get_utc_timestamp(); + let until = started_at + seconds; + loop { + let swap_status = my_swap_status(mm, swap).await.unwrap(); + + if get_utc_timestamp() > until { + panic!( + "Timed out waiting for event {} with status: {}", + event_str, + serde_json::to_string(&swap_status).unwrap() + ); + } + + let events = swap_status["result"]["events"].as_array().unwrap(); + + let event_strs = events + .iter() + .map(|event| event["event"]["type"].as_str().unwrap()) + .collect::>(); + + if event_strs.contains(&event_str) { + break; + } + Timer::sleep(1.).await; + } +} + +// TakerFeeSent pub async fn wait_for_swap_contract_negotiation(mm: &MarketMakerIt, swap: &str, expected_contract: Json, until: i64) { let events = loop { if get_utc_timestamp() > until { @@ -2565,7 +2711,13 @@ pub async fn wait_check_stats_swap_status(mm: &MarketMakerIt, uuid: &str, timeou })) .await .unwrap(); - assert!(response.0.is_success(), "!status of {}: {}", uuid, response.1); + if !response.0.is_success() { + Timer::sleep(1.).await; + if get_utc_timestamp() > wait_until { + panic!("Timed out waiting for swap stats status uuid={}, latest status={}", uuid, response.1); + } + continue; + } let status_response: Json = json::from_str(&response.1).unwrap(); // Perform the checks only if the maker and taker stats are available. diff --git a/mm2src/mm2_test_helpers/src/structs.rs b/mm2src/mm2_test_helpers/src/structs.rs index 819b819486..d1f601eb04 100644 --- a/mm2src/mm2_test_helpers/src/structs.rs +++ b/mm2src/mm2_test_helpers/src/structs.rs @@ -15,6 +15,10 @@ use std::fmt; use std::num::NonZeroUsize; use uuid::Uuid; +// TODO Alright: many of the type names within this file contain a misnomer +// `*Result` is used for many types that are not a "Result<>" +// Should be renamed `*Response` or similar + #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] pub struct RpcSuccessResponse {