diff --git a/.cargo/config.toml b/.cargo/config.toml index 5704896780..909a31799e 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -24,4 +24,4 @@ rustflags = [ "-Zshare-generics=y" ] [target.wasm32-unknown-unknown] runner = 'wasm-bindgen-test-runner' -rustflags = [ "--cfg=web_sys_unstable_apis" ] +rustflags = [ "--cfg=web_sys_unstable_apis" ] \ No newline at end of file diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index a4fce83cbe..26001494aa 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -53,7 +53,7 @@ jobs: run: | rm -f ./MM_VERSION echo $COMMIT_HASH > ./MM_VERSION - cargo build --release + cargo build --features "enable-sia" --release - name: Compress mm2 build output env: @@ -64,6 +64,7 @@ jobs: zip $NAME target/release/mm2 -j mkdir $BRANCH_NAME mv $NAME ./$BRANCH_NAME/ + shasum -a 256 ./$BRANCH_NAME/$NAME | tee ./$BRANCH_NAME/$NAME.sha256 - name: Compress kdf build output env: @@ -73,6 +74,7 @@ jobs: NAME="kdf_$COMMIT_HASH-linux-x86-64.zip" zip $NAME target/release/kdf -j mv $NAME ./$BRANCH_NAME/ + shasum -a 256 ./$BRANCH_NAME/$NAME | tee ./$BRANCH_NAME/$NAME.sha256 - name: Upload build artifact env: @@ -126,7 +128,7 @@ jobs: run: | rm -f ./MM_VERSION echo $COMMIT_HASH > ./MM_VERSION - cargo build --release --target x86_64-apple-darwin + cargo build --features "enable-sia" --release --target x86_64-apple-darwin - name: Compress mm2 build output env: @@ -137,6 +139,7 @@ jobs: zip $NAME target/x86_64-apple-darwin/release/mm2 -j mkdir $BRANCH_NAME mv $NAME ./$BRANCH_NAME/ + shasum -a 256 ./$BRANCH_NAME/$NAME | tee ./$BRANCH_NAME/$NAME.sha256 - name: Compress kdf build output env: @@ -146,6 +149,7 @@ jobs: NAME="kdf_$COMMIT_HASH-mac-x86-64.zip" zip $NAME target/x86_64-apple-darwin/release/kdf -j mv $NAME ./$BRANCH_NAME/ + shasum -a 256 ./$BRANCH_NAME/$NAME | tee ./$BRANCH_NAME/$NAME.sha256 - name: Upload build artifact env: @@ -187,7 +191,7 @@ jobs: run: | rm -f ./MM_VERSION echo $COMMIT_HASH > ./MM_VERSION - cargo build --release --target aarch64-apple-darwin + cargo build --features "enable-sia" --release --target aarch64-apple-darwin - name: Compress mm2 build output env: @@ -198,6 +202,7 @@ jobs: zip $NAME target/aarch64-apple-darwin/release/mm2 -j mkdir $BRANCH_NAME mv $NAME ./$BRANCH_NAME/ + shasum -a 256 ./$BRANCH_NAME/$NAME | tee ./$BRANCH_NAME/$NAME.sha256 - name: Compress kdf build output env: @@ -207,6 +212,7 @@ jobs: NAME="kdf_$COMMIT_HASH-mac-arm64.zip" zip $NAME target/aarch64-apple-darwin/release/kdf -j mv $NAME ./$BRANCH_NAME/ + shasum -a 256 ./$BRANCH_NAME/$NAME | tee ./$BRANCH_NAME/$NAME.sha256 - name: Upload build artifact env: @@ -249,26 +255,50 @@ jobs: remove-item "./MM_VERSION" } echo $Env:COMMIT_HASH > ./MM_VERSION - cargo build --release + cargo build --features "enable-sia" --release - name: Compress mm2 build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} run: | - $NAME="mm2_$Env:COMMIT_HASH-win-x86-64.zip" + $NAME = "mm2_$Env:COMMIT_HASH-win-x86-64.zip" + + # Compress the mm2.exe binary and required DLLs using 7z 7z a $NAME .\target\release\mm2.exe .\target\release\*.dll - mkdir $Env:BRANCH_NAME - mv $NAME ./$Env:BRANCH_NAME/ + + # Generate the SHA256 hash for the zip file + Get-FileHash $NAME -Algorithm SHA256 | Format-Table Hash | Out-File "$NAME.sha256" -Encoding ascii + + # Display the SHA256 hash + Get-Content "$NAME.sha256" + + # Create branch directory if it doesn't exist + New-Item -Path $Env:BRANCH_NAME -ItemType Directory -Force + + # Move the zip file and the SHA256 file to the branch directory + Move-Item $NAME ./$Env:BRANCH_NAME/ + Move-Item "$NAME.sha256" ./$Env:BRANCH_NAME/ - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} run: | - $NAME="kdf_$Env:COMMIT_HASH-win-x86-64.zip" + $NAME = "kdf_$Env:COMMIT_HASH-win-x86-64.zip" + # Compress the kdf.exe binary and required DLLs using 7z 7z a $NAME .\target\release\kdf.exe .\target\release\*.dll - mv $NAME ./$Env:BRANCH_NAME/ + + # Generate the SHA256 hash for the zip file + Get-FileHash $NAME -Algorithm SHA256 | Format-Table Hash | Out-File "$NAME.sha256" -Encoding ascii + + # Display the SHA256 hash + Get-Content "$NAME.sha256" + + # Move the zip file and the SHA256 file to the branch directory + New-Item -Path $Env:BRANCH_NAME -ItemType Directory -Force + Move-Item $NAME ./$Env:BRANCH_NAME/ + Move-Item "$NAME.sha256" ./$Env:BRANCH_NAME/ - name: Upload build artifact env: @@ -310,7 +340,7 @@ jobs: run: | rm -f ./MM_VERSION echo $COMMIT_HASH > ./MM_VERSION - cargo rustc --target x86_64-apple-darwin --lib --release --package mm2_bin_lib --crate-type=staticlib + cargo rustc --features "enable-sia" --target x86_64-apple-darwin --lib --release --package mm2_bin_lib --crate-type=staticlib - name: Compress mm2 build output env: @@ -322,6 +352,7 @@ jobs: zip $NAME target/x86_64-apple-darwin/release/libmm2.a -j mkdir $BRANCH_NAME mv $NAME ./$BRANCH_NAME/ + shasum -a 256 ./$BRANCH_NAME/$NAME | tee ./$BRANCH_NAME/$NAME.sha256 - name: Compress kdf build output env: @@ -332,6 +363,7 @@ jobs: mv target/x86_64-apple-darwin/release/libkdflib.a target/x86_64-apple-darwin/release/libkdf.a zip $NAME target/x86_64-apple-darwin/release/libkdf.a -j mv $NAME ./$BRANCH_NAME/ + shasum -a 256 ./$BRANCH_NAME/$NAME | tee ./$BRANCH_NAME/$NAME.sha256 - name: Upload build artifact env: @@ -385,8 +417,8 @@ jobs: run: | rm -f ./MM_VERSION echo $COMMIT_HASH > ./MM_VERSION - wasm-pack build --release mm2src/mm2_bin_lib --target web --out-dir ../../target/target-wasm-release - + wasm-pack build --release mm2src/mm2_bin_lib --target web --out-dir ../../target/target-wasm-release --features enable-sia + - name: Compress build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} @@ -396,6 +428,7 @@ jobs: (cd ./target/target-wasm-release && zip -r - .) > $NAME mkdir $BRANCH_NAME mv $NAME ./$BRANCH_NAME/ + shasum -a 256 ./$BRANCH_NAME/$NAME | tee ./$BRANCH_NAME/$NAME.sha256 - name: Upload build artifact env: @@ -437,7 +470,7 @@ jobs: run: | rm -f ./MM_VERSION echo $COMMIT_HASH > ./MM_VERSION - cargo rustc --target aarch64-apple-ios --lib --release --package mm2_bin_lib --crate-type=staticlib + cargo rustc --features "enable-sia" --target aarch64-apple-ios --lib --release --package mm2_bin_lib --crate-type=staticlib - name: Compress mm2 build output env: @@ -449,6 +482,8 @@ jobs: zip $NAME target/aarch64-apple-ios/release/libmm2.a -j mkdir $BRANCH_NAME mv $NAME ./$BRANCH_NAME/ + shasum -a 256 ./$BRANCH_NAME/$NAME | tee ./$BRANCH_NAME/$NAME.sha256 + - name: Compress kdf build output env: @@ -459,6 +494,7 @@ jobs: mv target/aarch64-apple-ios/release/libkdflib.a target/aarch64-apple-ios/release/libkdf.a zip $NAME target/aarch64-apple-ios/release/libkdf.a -j mv $NAME ./$BRANCH_NAME/ + shasum -a 256 ./$BRANCH_NAME/$NAME | tee ./$BRANCH_NAME/$NAME.sha256 - name: Upload build artifact env: @@ -514,7 +550,7 @@ jobs: echo $COMMIT_HASH > ./MM_VERSION export PATH=$PATH:/android-ndk/bin - CC_aarch64_linux_android=aarch64-linux-android21-clang CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=aarch64-linux-android21-clang cargo rustc --target=aarch64-linux-android --lib --release --crate-type=staticlib --package mm2_bin_lib + CC_aarch64_linux_android=aarch64-linux-android21-clang CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=aarch64-linux-android21-clang cargo rustc --features "enable-sia" --target=aarch64-linux-android --lib --release --crate-type=staticlib --package mm2_bin_lib - name: Compress mm2 build output env: @@ -526,6 +562,7 @@ jobs: zip $NAME target/aarch64-linux-android/release/libmm2.a -j mkdir $BRANCH_NAME mv $NAME ./$BRANCH_NAME/ + shasum -a 256 ./$BRANCH_NAME/$NAME | tee ./$BRANCH_NAME/$NAME.sha256 - name: Compress kdf build output env: @@ -536,6 +573,7 @@ jobs: mv target/aarch64-linux-android/release/libkdflib.a target/aarch64-linux-android/release/libkdf.a zip $NAME target/aarch64-linux-android/release/libkdf.a -j mv $NAME ./$BRANCH_NAME/ + shasum -a 256 ./$BRANCH_NAME/$NAME | tee ./$BRANCH_NAME/$NAME.sha256 - name: Upload build artifact env: @@ -591,7 +629,7 @@ jobs: echo $COMMIT_HASH > ./MM_VERSION export PATH=$PATH:/android-ndk/bin - CC_armv7_linux_androideabi=armv7a-linux-androideabi21-clang CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER=armv7a-linux-androideabi21-clang cargo rustc --target=armv7-linux-androideabi --lib --release --crate-type=staticlib --package mm2_bin_lib + CC_armv7_linux_androideabi=armv7a-linux-androideabi21-clang CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER=armv7a-linux-androideabi21-clang cargo rustc --features "enable-sia" --target=armv7-linux-androideabi --lib --release --crate-type=staticlib --package mm2_bin_lib - name: Compress mm2 build output env: @@ -603,6 +641,7 @@ jobs: zip $NAME target/armv7-linux-androideabi/release/libmm2.a -j mkdir $BRANCH_NAME mv $NAME ./$BRANCH_NAME/ + shasum -a 256 ./$BRANCH_NAME/$NAME | tee ./$BRANCH_NAME/$NAME.sha256 - name: Compress kdf build output env: @@ -613,6 +652,7 @@ jobs: mv target/armv7-linux-androideabi/release/libkdflib.a target/armv7-linux-androideabi/release/libkdf.a zip $NAME target/armv7-linux-androideabi/release/libkdf.a -j mv $NAME ./$BRANCH_NAME/ + shasum -a 256 ./$BRANCH_NAME/$NAME | tee ./$BRANCH_NAME/$NAME.sha256 - name: Upload build artifact env: diff --git a/.github/workflows/fmt-and-lint.yml b/.github/workflows/fmt-and-lint.yml index f5ea217eee..4b41e7a5a5 100644 --- a/.github/workflows/fmt-and-lint.yml +++ b/.github/workflows/fmt-and-lint.yml @@ -58,4 +58,4 @@ jobs: uses: ./.github/actions/cargo-cache - name: clippy lint - run: cargo clippy --target wasm32-unknown-unknown -- --D warnings + run: cargo clippy --target wasm32-unknown-unknown --all-features -- --D warnings diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index a74a589d10..97ce333bd0 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -53,7 +53,7 @@ jobs: run: | rm -f ./MM_VERSION echo $COMMIT_HASH > ./MM_VERSION - cargo build --release + cargo build --features "enable-sia" --release - name: Compress mm2 build output run: | @@ -117,7 +117,7 @@ jobs: run: | rm -f ./MM_VERSION echo $COMMIT_HASH > ./MM_VERSION - cargo build --release --target x86_64-apple-darwin + cargo build --features "enable-sia" --release --target x86_64-apple-darwin - name: Compress mm2 build output run: | @@ -172,7 +172,7 @@ jobs: run: | rm -f ./MM_VERSION echo $COMMIT_HASH > ./MM_VERSION - cargo build --release --target aarch64-apple-darwin + cargo build --features "enable-sia" --release --target aarch64-apple-darwin - name: Compress mm2 build output run: | @@ -228,7 +228,7 @@ jobs: remove-item "./MM_VERSION" } echo $Env:COMMIT_HASH > ./MM_VERSION - cargo build --release + cargo build --features "enable-sia" --release - name: Compress mm2 build output run: | @@ -282,7 +282,7 @@ jobs: run: | rm -f ./MM_VERSION echo $COMMIT_HASH > ./MM_VERSION - cargo rustc --target x86_64-apple-darwin --lib --release --package mm2_bin_lib --crate-type=staticlib + cargo rustc --features "enable-sia" --target x86_64-apple-darwin --lib --release --package mm2_bin_lib --crate-type=staticlib - name: Compress mm2 build output run: | @@ -400,7 +400,7 @@ jobs: run: | rm -f ./MM_VERSION echo $COMMIT_HASH > ./MM_VERSION - cargo rustc --target aarch64-apple-ios --lib --release --package mm2_bin_lib --crate-type=staticlib + cargo rustc --features "enable-sia" --target aarch64-apple-ios --lib --release --package mm2_bin_lib --crate-type=staticlib - name: Compress mm2 build output run: | @@ -471,7 +471,7 @@ jobs: echo $COMMIT_HASH > ./MM_VERSION export PATH=$PATH:/android-ndk/bin - CC_aarch64_linux_android=aarch64-linux-android21-clang CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=aarch64-linux-android21-clang cargo rustc --target=aarch64-linux-android --lib --release --crate-type=staticlib --package mm2_bin_lib + CC_aarch64_linux_android=aarch64-linux-android21-clang CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=aarch64-linux-android21-clang cargo rustc --features "enable-sia" --target=aarch64-linux-android --lib --release --crate-type=staticlib --package mm2_bin_lib - name: Compress mm2 build output run: | @@ -542,7 +542,7 @@ jobs: echo $COMMIT_HASH > ./MM_VERSION export PATH=$PATH:/android-ndk/bin - CC_armv7_linux_androideabi=armv7a-linux-androideabi21-clang CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER=armv7a-linux-androideabi21-clang cargo rustc --target=armv7-linux-androideabi --lib --release --crate-type=staticlib --package mm2_bin_lib + CC_armv7_linux_androideabi=armv7a-linux-androideabi21-clang CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER=armv7a-linux-androideabi21-clang cargo rustc --features "enable-sia" --target=armv7-linux-androideabi --lib --release --crate-type=staticlib --package mm2_bin_lib - name: Compress mm2 build output run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 12a60bbc3c..8a29823c0f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,7 +38,7 @@ jobs: - name: Test run: | - cargo test --bins --lib --no-fail-fast + cargo test --bins --lib --no-fail-fast --features enable-sia mac-x86-64-unit: timeout-minutes: 90 @@ -66,7 +66,7 @@ jobs: - name: Test run: | - cargo test --bins --lib --no-fail-fast + cargo test --bins --lib --no-fail-fast --features enable-sia win-x86-64-unit: timeout-minutes: 90 @@ -94,7 +94,7 @@ jobs: - name: Test run: | - cargo test --bins --lib --no-fail-fast + cargo test --bins --lib --no-fail-fast --features enable-sia linux-x86-64-kdf-integration: timeout-minutes: 90 @@ -123,7 +123,7 @@ jobs: - name: Test run: | wget -O - https://raw.githubusercontent.com/KomodoPlatform/komodo/0adeeabdd484ef40539d1275c6a765f5c530ea79/zcutil/fetch-params-alt.sh | bash - cargo test --test 'mm2_tests_main' --no-fail-fast + cargo test --test 'mm2_tests_main' --no-fail-fast --features enable-sia mac-x86-64-kdf-integration: timeout-minutes: 90 @@ -155,7 +155,7 @@ jobs: - name: Test run: | wget -O - https://raw.githubusercontent.com/KomodoPlatform/komodo/0adeeabdd484ef40539d1275c6a765f5c530ea79/zcutil/fetch-params-alt.sh | bash - cargo test --test 'mm2_tests_main' --no-fail-fast + cargo test --test 'mm2_tests_main' --no-fail-fast --features enable-sia win-x86-64-kdf-integration: timeout-minutes: 90 @@ -191,7 +191,7 @@ jobs: - name: Test run: | Invoke-WebRequest -Uri https://raw.githubusercontent.com/KomodoPlatform/komodo/0adeeabdd484ef40539d1275c6a765f5c530ea79/zcutil/fetch-params-alt.bat -OutFile \cmd.bat && \cmd.bat - cargo test --test 'mm2_tests_main' --no-fail-fast + cargo test --test 'mm2_tests_main' --no-fail-fast --features enable-sia docker-tests: timeout-minutes: 90 @@ -265,4 +265,4 @@ jobs: uses: ./.github/actions/cargo-cache - name: Test - run: WASM_BINDGEN_TEST_TIMEOUT=480 GECKODRIVER=/bin/geckodriver wasm-pack test --firefox --headless mm2src/mm2_main + run: WASM_BINDGEN_TEST_TIMEOUT=480 GECKODRIVER=/bin/geckodriver wasm-pack test --firefox --headless mm2src/mm2_main --features enable-sia diff --git a/Cargo.lock b/Cargo.lock index 99227202a5..fa53c682d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -896,7 +896,6 @@ dependencies = [ "proxy_signature", "rand 0.7.3", "regex", - "reqwest", "rlp", "rmp-serde", "rpc", @@ -919,6 +918,7 @@ dependencies = [ "sia-rust", "spv_validation", "tendermint-rpc", + "thiserror", "time 0.3.20", "tokio", "tokio-rustls 0.24.1", @@ -4099,7 +4099,6 @@ dependencies = [ "serde_json", "serialization", "serialization_derive", - "sia-rust", "sp-runtime-interface", "sp-trie", "spv_validation", @@ -6273,21 +6272,33 @@ dependencies = [ [[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.git?rev=ce321bb#ce321bb1b365aed6f8e3bdbf52af25d9d1c9dfbe" dependencies = [ + "async-trait", "base64 0.21.7", "blake2b_simd", "chrono", + "curve25519-dalek 3.2.0", "derive_more", "ed25519-dalek", + "futures 0.3.28", + "getrandom 0.2.9", "hex", + "http 0.2.12", + "js-sys", + "log", "nom", + "percent-encoding", "reqwest", "rustc-hex", "serde", + "serde-wasm-bindgen", "serde_json", - "serde_with", + "thiserror", "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index 8fa02034c1..75e47e7483 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -6,8 +6,6 @@ edition = "2018" [features] zhtlc-native-tests = [] enable-sia = [ - "dep:reqwest", - "dep:blake2b_simd", "dep:sia-rust" ] default = [] @@ -26,7 +24,6 @@ base58 = "0.2.0" bip32 = { version = "0.2.2", default-features = false, features = ["alloc", "secp256k1-ffi"] } bitcoin_hashes = "0.11" bitcrypto = { path = "../mm2_bitcoin/crypto" } -blake2b_simd = { version = "0.5.10", optional = true } byteorder = "1.3" bytes = "0.4" cfg-if = "1.0" @@ -80,11 +77,11 @@ protobuf = "2.20" proxy_signature = { path = "../proxy_signature" } rand = { version = "0.7", features = ["std", "small_rng"] } regex = "1" -reqwest = { version = "0.11.9", default-features = false, features = ["json"], optional = true } rlp = { version = "0.5" } rmp-serde = "0.14.3" rpc = { path = "../mm2_bitcoin/rpc" } rpc_task = { path = "../rpc_task" } +sia-rust = { git = "https://github.com/KomodoPlatform/sia-rust.git", rev = "a136d4f", optional = true } script = { path = "../mm2_bitcoin/script" } secp256k1 = { version = "0.20" } ser_error = { path = "../derives/ser_error" } @@ -95,13 +92,13 @@ serde_json = { version = "1", features = ["preserve_order", "raw_value"] } serde_with = "1.14.0" 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 = "0.10" sha3 = "0.9" utxo_signer = { path = "utxo_signer" } # using the same version as cosmrs tendermint-rpc = { version = "0.34", default-features = false } +thiserror = "1.0.40" tokio-tungstenite-wasm = { git = "https://github.com/KomodoPlatform/tokio-tungstenite-wasm", rev = "d20abdb", features = ["rustls-tls-native-roots"]} url = { version = "2.2.2", features = ["serde"] } uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 068c750d37..ebba3c2686 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -64,7 +64,6 @@ use crypto::{Bip44Chain, CryptoCtx, CryptoCtxError, GlobalHDAccountArc, KeyPairP use derive_more::Display; use enum_derives::EnumFromStringify; use ethabi::{Contract, Function, Token}; -use ethcore_transaction::tx_builders::TxBuilderError; use ethcore_transaction::{Action, TransactionWrapper, TransactionWrapperBuilder as UnSignedEthTxBuilder, UnverifiedEip1559Transaction, UnverifiedEip2930Transaction, UnverifiedLegacyTransaction, UnverifiedTransactionWrapper}; @@ -134,9 +133,9 @@ cfg_native! { mod eth_balance_events; mod eth_rpc; -#[cfg(test)] mod eth_tests; +#[cfg(all(test, not(target_arch = "wasm32")))] mod eth_tests; // FIXME Alright - no idea why I had to change this to fix compilation #[cfg(target_arch = "wasm32")] mod eth_wasm_tests; -#[cfg(any(test, target_arch = "wasm32"))] mod for_tests; +#[cfg(test)] mod for_tests; pub(crate) mod nft_swap_v2; mod web3_transport; use web3_transport::{http_transport::HttpTransportNode, Web3Transport}; @@ -536,14 +535,6 @@ impl From for BalanceError { } } -impl From for TransactionErr { - fn from(e: TxBuilderError) -> Self { TransactionErr::Plain(e.to_string()) } -} - -impl From for TransactionErr { - fn from(e: ethcore_transaction::Error) -> Self { TransactionErr::Plain(e.to_string()) } -} - #[derive(Debug, Deserialize, Serialize)] struct SavedTraces { /// ETH traces for my_address diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index fb2b2c07ec..d182b539cd 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -255,7 +255,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; +#[cfg(feature = "enable-sia")] +use siacoin::{SiaCoin, SiaFeeDetails, SiaTransaction, SiaTransactionTypes}; pub mod utxo; use utxo::bch::{bch_coin_with_policy, BchActivationRequest, BchCoin}; @@ -557,9 +558,9 @@ pub enum UnexpectedDerivationMethod { ExpectedHDWallet, #[display(fmt = "Trezor derivation method is not supported yet!")] Trezor, - #[display(fmt = "Unsupported error: {}", _0)] + #[display(fmt = "UnexpectedDerivationMethod Unsupported error: {}", _0)] UnsupportedError(String), - #[display(fmt = "Internal error: {}", _0)] + #[display(fmt = "UnexpectedDerivationMethod Internal error: {}", _0)] InternalError(String), } @@ -588,6 +589,8 @@ pub enum TransactionEnum { CosmosTransaction(CosmosTransaction), #[cfg(not(target_arch = "wasm32"))] LightningPayment(LightningPayment), + #[cfg(feature = "enable-sia")] + SiaTransaction(SiaTransaction), } ifrom!(TransactionEnum, UtxoTx); @@ -595,6 +598,8 @@ ifrom!(TransactionEnum, SignedEthTx); ifrom!(TransactionEnum, ZTransaction); #[cfg(not(target_arch = "wasm32"))] ifrom!(TransactionEnum, LightningPayment); +#[cfg(feature = "enable-sia")] +ifrom!(TransactionEnum, SiaTransaction); impl TransactionEnum { #[cfg(not(target_arch = "wasm32"))] @@ -615,6 +620,8 @@ impl Deref for TransactionEnum { TransactionEnum::CosmosTransaction(ref t) => t, #[cfg(not(target_arch = "wasm32"))] TransactionEnum::LightningPayment(ref p) => p, + #[cfg(feature = "enable-sia")] + TransactionEnum::SiaTransaction(ref t) => t, } } } @@ -629,17 +636,20 @@ pub enum TxMarshalingErr { Internal(String), } -#[derive(Clone, Debug, EnumFromStringify)] +#[derive(Clone, Debug)] #[allow(clippy::large_enum_variant)] pub enum TransactionErr { /// Keeps transactions while throwing errors. TxRecoverable(TransactionEnum, String), /// Simply for plain error messages. - #[from_stringify("keys::Error")] Plain(String), ProtocolNotSupported(String), } +impl From for TransactionErr { + fn from(e: T) -> Self { TransactionErr::Plain(e.to_string()) } +} + impl TransactionErr { /// Returns transaction if the error includes it. #[inline] @@ -784,6 +794,9 @@ pub struct WatcherSearchForSwapTxSpendInput<'a> { pub watcher_reward: bool, } +// TODO Alright Do we really want to manually manage lifetimes here? +// Would be nice to understand the motivation for this choice. +// Was this pattern simply copied naitvely or is this a significant impact on memory usage? #[derive(Clone, Debug)] pub struct SendMakerPaymentSpendPreimageInput<'a> { pub preimage: &'a [u8], @@ -987,6 +1000,7 @@ 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 fee_addr: &'a [u8], pub dex_fee: &'a DexFee, @@ -1201,57 +1215,80 @@ pub trait MakerSwapTakerCoin { async fn on_maker_payment_refund_success(&self, taker_payment: &[u8]) -> RefundResult<()>; } +// 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; + fn send_maker_payment_spend_preimage(&self, _input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { + unimplemented!(); + } - fn send_taker_payment_refund_preimage(&self, watcher_refunds_payment_args: RefundPaymentArgs) -> TransactionFut; + fn send_taker_payment_refund_preimage(&self, _watcher_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { + unimplemented!(); + } fn create_taker_payment_refund_preimage( &self, - taker_payment_tx: &[u8], - time_lock: u64, - maker_pub: &[u8], - secret_hash: &[u8], - swap_contract_address: &Option, - swap_unique_data: &[u8], - ) -> TransactionFut; + _taker_payment_tx: &[u8], + _time_lock: u64, + _maker_pub: &[u8], + _secret_hash: &[u8], + _swap_contract_address: &Option, + _swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!(); + } fn create_maker_payment_spend_preimage( &self, - maker_payment_tx: &[u8], - time_lock: u64, - maker_pub: &[u8], - secret_hash: &[u8], - swap_unique_data: &[u8], - ) -> TransactionFut; + _maker_payment_tx: &[u8], + _time_lock: u64, + _maker_pub: &[u8], + _secret_hash: &[u8], + _swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!(); + } - fn watcher_validate_taker_fee(&self, input: WatcherValidateTakerFeeInput) -> ValidatePaymentFut<()>; + fn watcher_validate_taker_fee(&self, _input: WatcherValidateTakerFeeInput) -> ValidatePaymentFut<()> { + unimplemented!(); + } - fn watcher_validate_taker_payment(&self, input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()>; + fn watcher_validate_taker_payment(&self, _input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()> { + unimplemented!(); + } - fn taker_validates_payment_spend_or_refund(&self, _input: ValidateWatcherSpendInput) -> ValidatePaymentFut<()>; + fn taker_validates_payment_spend_or_refund(&self, _input: ValidateWatcherSpendInput) -> ValidatePaymentFut<()> { + unimplemented!(); + } async fn watcher_search_for_swap_tx_spend( &self, - input: WatcherSearchForSwapTxSpendInput<'_>, - ) -> Result, String>; + _input: WatcherSearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + unimplemented!(); + } async fn get_taker_watcher_reward( &self, - other_coin: &MmCoinEnum, - coin_amount: Option, - other_coin_amount: Option, - reward_amount: Option, - wait_until: u64, - ) -> Result>; + _other_coin: &MmCoinEnum, + _coin_amount: Option, + _other_coin_amount: Option, + _reward_amount: Option, + _wait_until: u64, + ) -> Result> { + unimplemented!(); + } async fn get_maker_watcher_reward( &self, - other_coin: &MmCoinEnum, - reward_amount: Option, - wait_until: u64, - ) -> Result, MmError>; + _other_coin: &MmCoinEnum, + _reward_amount: Option, + _wait_until: u64, + ) -> Result, MmError> { + unimplemented!(); + } } /// Helper struct wrapping arguments for [TakerCoinSwapOpsV2::send_taker_funding] @@ -1995,10 +2032,19 @@ pub trait MarketCoinOps { async fn get_public_key(&self) -> Result>; + // TODO Alright: should be separated into a "OptionalDispatcherOps" trait. + // This trait can handle all methods that are only used by dispatcher methods. + // this is literally only used by sign_message impls and doesn't need to be a method fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]>; + // 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 "sign_message" rpc method fn sign_message(&self, _message: &str) -> SignatureResult; + // 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 "verify_message" rpc method fn verify_message(&self, _signature: &str, _message: &str, _address: &str) -> VerificationResult; fn get_non_zero_balance(&self) -> NonZeroBalanceFut { @@ -2029,7 +2075,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>; @@ -2217,6 +2268,8 @@ pub enum TxFeeDetails { Qrc20(Qrc20FeeDetails), Slp(SlpFeeDetails), Tendermint(TendermintFeeDetails), + #[cfg(feature = "enable-sia")] + Sia(SiaFeeDetails), } /// Deserialize the TxFeeDetails as an untagged enum. @@ -2232,6 +2285,8 @@ impl<'de> Deserialize<'de> for TxFeeDetails { Eth(EthTxFeeDetails), Qrc20(Qrc20FeeDetails), Tendermint(TendermintFeeDetails), + #[cfg(feature = "enable-sia")] + Sia(SiaFeeDetails), } match Deserialize::deserialize(deserializer)? { @@ -2239,6 +2294,8 @@ impl<'de> Deserialize<'de> for TxFeeDetails { TxFeeDetailsUnTagged::Eth(f) => Ok(TxFeeDetails::Eth(f)), TxFeeDetailsUnTagged::Qrc20(f) => Ok(TxFeeDetails::Qrc20(f)), TxFeeDetailsUnTagged::Tendermint(f) => Ok(TxFeeDetails::Tendermint(f)), + #[cfg(feature = "enable-sia")] + TxFeeDetailsUnTagged::Sia(f) => Ok(TxFeeDetails::Sia(f)), } } } @@ -2255,6 +2312,11 @@ impl From for TxFeeDetails { fn from(qrc20_details: Qrc20FeeDetails) -> Self { TxFeeDetails::Qrc20(qrc20_details) } } +#[cfg(feature = "enable-sia")] +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) } } @@ -2288,6 +2350,12 @@ pub enum TransactionType { }, NftTransfer, TendermintIBCTransfer, + #[cfg(feature = "enable-sia")] + SiaV1Transaction, + #[cfg(feature = "enable-sia")] + SiaV2Transaction, + #[cfg(feature = "enable-sia")] + SiaMinerPayout, } /// Transaction details @@ -2332,7 +2400,7 @@ pub struct TransactionDetails { #[serde(untagged)] 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, @@ -2340,6 +2408,15 @@ 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 + #[cfg(feature = "enable-sia")] + 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 { @@ -2351,6 +2428,8 @@ impl TransactionData { match self { TransactionData::Signed { tx_hex, .. } => Some(tx_hex), TransactionData::Unsigned(_) => None, + #[cfg(feature = "enable-sia")] + TransactionData::Sia { .. } => None, } } @@ -2358,6 +2437,8 @@ impl TransactionData { match self { TransactionData::Signed { tx_hash, .. } => Some(tx_hash), TransactionData::Unsigned(_) => None, + #[cfg(feature = "enable-sia")] + TransactionData::Sia { tx_hash, .. } => Some(tx_hash), } } } @@ -3271,7 +3352,7 @@ pub trait MmCoin: // 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 { @@ -3288,16 +3369,26 @@ pub trait MmCoin: 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 @@ -3605,6 +3696,7 @@ impl MmCoinStruct { } /// 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 { /// Standard dex fee which will be sent to the dex fee address @@ -4565,7 +4657,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; @@ -4770,8 +4863,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))) } diff --git a/mm2src/coins/my_tx_history_v2.rs b/mm2src/coins/my_tx_history_v2.rs index f7348a5e78..d43059e2ec 100644 --- a/mm2src/coins/my_tx_history_v2.rs +++ b/mm2src/coins/my_tx_history_v2.rs @@ -239,6 +239,10 @@ impl<'a, Addr: Clone + DisplayAddress + Eq + std::hash::Hash, Tx: Transaction> T | TransactionType::StandardTransfer | TransactionType::NftTransfer | TransactionType::TendermintIBCTransfer => tx_hash.clone(), + #[cfg(feature = "enable-sia")] + TransactionType::SiaV1Transaction | TransactionType::SiaV2Transaction | TransactionType::SiaMinerPayout => { + tx_hash.clone() + }, }; TransactionDetails { diff --git a/mm2src/coins/siacoin.rs b/mm2src/coins/siacoin.rs index 1bd8ef6c2d..5090f68e2d 100644 --- a/mm2src/coins/siacoin.rs +++ b/mm2src/coins/siacoin.rs @@ -1,249 +1,534 @@ -use super::{BalanceError, CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, RawTransactionFut, - RawTransactionRequest, SwapOps, TradeFee, TransactionEnum, TransactionFut}; -use crate::{coin_errors::MyAddressError, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinFutSpawner, - ConfirmPaymentInput, DexFee, FeeApproxStage, FoundSwapTxSpend, MakerSwapTakerCoin, MmCoinEnum, +use super::{BalanceError, CoinBalance, CoinsContext, HistorySyncState, MarketCoinOps, MmCoin, RawTransactionFut, + RawTransactionRequest, SwapOps, SwapTxTypeWithSecretHash, TradeFee, TransactionData, TransactionDetails, + TransactionEnum, TransactionErr, TransactionFut, TransactionType}; +use crate::siacoin::sia_withdraw::SiaWithdrawBuilder; +use crate::{coin_errors::MyAddressError, now_sec, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinFutSpawner, + ConfirmPaymentInput, DexFee, FeeApproxStage, FoundSwapTxSpend, MakerSwapTakerCoin, NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, - PrivKeyBuildPolicy, PrivKeyPolicy, RawTransactionResult, RefundPaymentArgs, RefundResult, - SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignRawTransactionRequest, - SignatureResult, SpendPaymentArgs, TakerSwapMakerCoin, TradePreimageFut, TradePreimageResult, - TradePreimageValue, TransactionResult, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, - ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, - ValidatePaymentFut, ValidatePaymentInput, ValidatePaymentResult, ValidateWatcherSpendInput, - VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, - WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut, - WithdrawRequest}; + PrivKeyBuildPolicy, PrivKeyPolicy, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, + SendPaymentArgs, SignatureResult, SpendPaymentArgs, TakerSwapMakerCoin, TradePreimageFut, + TradePreimageResult, TradePreimageValue, Transaction, TransactionResult, TxMarshalingErr, + UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, + ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentInput, ValidatePaymentResult, + VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WithdrawFut, WithdrawRequest}; use async_trait::async_trait; -use common::executor::AbortedError; -pub use ed25519_dalek::{Keypair, PublicKey, SecretKey, Signature}; +use common::executor::abortable_queue::AbortableQueue; +use common::executor::{AbortableSystem, AbortedError, Timer}; +use common::log::info; +use common::DEX_FEE_PUBKEY_ED25510; +use derive_more::{From, Into}; +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; +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, ApiClientError as SiaApiClientError, + ApiClientHelpers, HelperError as SiaClientHelperError}; +pub use sia_rust::transport::endpoints::{AddressesEventsRequest, GetAddressUtxosRequest, GetEventRequest, + TxpoolBroadcastRequest}; +pub use sia_rust::types::{Address, Currency, Event, EventDataWrapper, EventPayout, EventType, Hash256, + Keypair as SiaKeypair, ParseHashError, Preimage, PreimageError, PrivateKeyError, PublicKey, + PublicKeyError, SiacoinElement, SiacoinOutput, SpendPolicy, TransactionId, V1Transaction, + V2Transaction, V2TransactionBuilder, V2TransactionBuilderError}; +use std::collections::hash_map::Entry; +use std::collections::{HashMap, HashSet}; +use std::convert::TryFrom; +use std::str::FromStr; +use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering}; +use std::sync::{Arc, Mutex}; +use thiserror::Error; +use uuid::Uuid; + +// TODO this is not well documented, and we should work toward removing this entire module. +// It serves no purpose if we follow thiserror patterns and uniformly use the Error trait. +use mm2_err_handle::prelude::*; + +lazy_static! { + pub static ref FEE_PUBLIC_KEY_BYTES: Vec = + hex::decode(DEX_FEE_PUBKEY_ED25510).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); +} + +// TODO consider if this is the best way to handle wasm vs native +#[cfg(not(target_arch = "wasm32"))] +use sia_rust::transport::client::native::Conf as SiaClientConf; +#[cfg(not(target_arch = "wasm32"))] +use sia_rust::transport::client::native::NativeClient as SiaClientType; + +#[cfg(target_arch = "wasm32")] +use sia_rust::transport::client::wasm::Client as SiaClientType; +#[cfg(target_arch = "wasm32")] +use sia_rust::transport::client::wasm::Conf as SiaClientConf; -use sia_rust::http_client::{SiaApiClient, SiaApiClientError, SiaHttpConf}; -use sia_rust::spend_policy::SpendPolicy; +pub mod error; +pub use error::SiaCoinError; +use error::*; pub mod sia_hd_wallet; +mod sia_withdraw; +// TODO see https://github.com/KomodoPlatform/komodo-defi-framework/pull/2086#discussion_r1521668313 +// for additional fields needed #[derive(Clone)] -pub struct SiaCoin(SiaArc); -#[derive(Clone)] -pub struct SiaArc(Arc); - -#[derive(Debug, Display)] -pub enum SiaConfError { - #[display(fmt = "'foo' field is not found in config")] - Foo, - Bar(String), +pub struct SiaCoinGeneric { + /// SIA coin config + pub conf: SiaCoinConf, + pub priv_key_policy: Arc>, + /// Client used to interact with the blockchain, most likely a 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>; +pub type SiaCoin = SiaCoinGeneric; + +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 { +#[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, -} - -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 SiaCoin { + pub async fn from_conf_and_request( + ctx: &MmArc, + json_conf: Json, + request: &SiaCoinActivationRequest, + priv_key_policy: PrivKeyBuildPolicy, + ) -> Result> { + let priv_key = match priv_key_policy { + PrivKeyBuildPolicy::IguanaPrivKey(priv_key) => priv_key, + _ => return Err(SiaCoinError::UnsupportedPrivKeyPolicy.into()), + }; + let key_pair = SiaKeypair::from_private_bytes(priv_key.as_slice()).map_err(SiaCoinError::InvalidPrivateKey)?; + let conf: SiaCoinConf = serde_json::from_value(json_conf).map_err(SiaCoinError::InvalidConf)?; + SiaCoinBuilder::new(ctx, conf, key_pair, request) + .build() + .await + .map_err(|e| SiaCoinError::Builder(e).into()) } } -// 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 -} - 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<'a> SiaCoinBuilder<'a> { - #[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()?; - 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)) + // 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( + SiaClientType::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 Deref for SiaArc { - type Target = SiaCoinFields; - fn deref(&self) -> &SiaCoinFields { &self.0 } +/// 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(coin: SiaCoinFields) -> SiaArc { SiaArc::new(coin) } +/// Convert "coin" representation to hastings amount +/// BigDecimal(1) == 1 SC == 10^24 hastings +// TODO 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 SiaArc { - fn from(arc: Arc) -> SiaArc { SiaArc(arc) } +// TODO Alright - refactor and move to siacoin::error +#[derive(Debug, Error)] +pub enum FrameworkError { + #[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 SiaCoin { - fn from(coin: SiaArc) -> SiaCoin { SiaCoin(coin) } +impl From for FrameworkError { + fn from(e: TransactionErr) -> Self { FrameworkError::MmTransactionErr(e) } } -impl SiaArc { - pub fn new(fields: SiaCoinFields) -> SiaArc { SiaArc(Arc::new(fields)) } - - pub fn with_arc(inner: Arc) -> SiaArc { SiaArc(inner) } +impl From for FrameworkError { + fn from(e: MyAddressError) -> Self { FrameworkError::MyAddressError(e) } } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct SiaCoinProtocolInfo; +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum SiaFeePolicy { + Fixed, + HastingsPerByte(Currency), + Unknown, +} + +#[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) -> CoinFutSpawner { unimplemented!() } + fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.abortable_system) } fn get_raw_transaction(&self, _req: RawTransactionRequest) -> RawTransactionFut { unimplemented!() } fn get_tx_hex_by_hash(&self, _tx_hash: Vec) -> RawTransactionFut { unimplemented!() } - 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!() } + fn decimals(&self) -> u8 { 24 } fn convert_to_address(&self, _from: &str, _to_address_format: Json) -> Result { unimplemented!() } - 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()), + }, + } + } + + // 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(())); + } - fn process_history_loop(&self, _ctx: MmArc) -> Box + Send> { unimplemented!() } + let mut my_balance: Option = None; + let coin = self.clone(); - fn history_sync_status(&self) -> HistorySyncState { unimplemented!() } + 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 { self.history_sync_state.lock().unwrap().clone() } /// Get fee to be paid per 1 swap transaction fn get_trade_fee(&self) -> Box + Send> { unimplemented!() } + // Todo: Implement this method when working on swaps async fn get_sender_trade_fee( &self, _value: TradePreimageValue, _stage: FeeApproxStage, _include_refund_fee: bool, ) -> TradePreimageResult { - unimplemented!() + Ok(TradeFee { + coin: self.conf.ticker.clone(), + amount: Default::default(), + paid_from_trading_vol: false, + }) } - fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { unimplemented!() } + /// Get the transaction fee required to spend the HTLC output + // TODO Dummy value for now + fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { + let ticker = self.conf.ticker.clone(); + let fut = async move { + Ok(TradeFee { + coin: ticker, + amount: Default::default(), + paid_from_trading_vol: false, + }) + }; + Box::new(fut.boxed().compat()) + } async fn get_fee_to_send_taker_fee( &self, @@ -253,11 +538,14 @@ impl MmCoin for SiaCoin { unimplemented!() } - fn required_confirmations(&self) -> u64 { unimplemented!() } + fn required_confirmations(&self) -> u64 { 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!() } @@ -287,11 +575,11 @@ impl MmCoin for SiaCoin { // 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 } + fn ticker(&self) -> &str { &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( @@ -313,11 +601,26 @@ impl MarketCoinOps for SiaCoin { .into()); }, }; - let address = SpendPolicy::PublicKey(key_pair.public).address(); + let address = key_pair.public().address(); Ok(address.to_string()) } - async fn get_public_key(&self) -> Result> { unimplemented!() } + async fn get_public_key(&self) -> Result> { + 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); + }, + }; + Ok(public_key.to_string()) + } fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]> { unimplemented!() } @@ -330,8 +633,8 @@ impl MarketCoinOps for SiaCoin { 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, @@ -339,35 +642,80 @@ 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 base_coin_balance(&self) -> BalanceFut { unimplemented!() } + fn base_coin_balance(&self) -> BalanceFut { Box::new(self.my_balance().map(|res| res.spendable)) } - fn platform_ticker(&self) -> &str { "FOO" } // TODO Alright + fn platform_ticker(&self) -> &str { "TSIA" } /// 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(); + let request = TxpoolBroadcastRequest { + transactions: vec![], + v2transactions: vec![transaction], + }; + + client.dispatcher(request).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!() } - #[inline(always)] - async fn sign_raw_tx(&self, _args: &SignRawTransactionRequest) -> RawTransactionResult { unimplemented!() } + 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() }; - fn wait_for_confirmations(&self, _input: ConfirmPaymentInput) -> Box + Send> { - unimplemented!() + 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 { + if event.index.height > 0 { + return Ok(()); // Transaction is confirmed at least once + } + }, + Err(e) => info!("Waiting for confirmation of Sia txid {}: {}", txid, e), + } + // TODO Alright above is a placeholder to allow swaps to progress after 1 confirmation. + // Sia team will add a "confirmations" field in GetEventResponse for us to use here. + + Timer::sleep(input.check_every as f64).await; + } + }; + + Box::new(fut.boxed().compat()) } fn wait_for_htlc_tx_spend(&self, _args: WaitForHTLCTxSpendArgs<'_>) -> TransactionFut { unimplemented!() } @@ -379,9 +727,9 @@ impl MarketCoinOps for SiaCoin { } 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 @@ -390,78 +738,677 @@ impl MarketCoinOps for SiaCoin { fn display_priv_key(&self) -> Result { unimplemented!() } - fn min_tx_amount(&self) -> BigDecimal { unimplemented!() } + // Todo: revise this when working on swaps + fn min_tx_amount(&self) -> BigDecimal { hastings_to_siacoin(1u64.into()) } - fn min_trading_vol(&self) -> MmNumber { unimplemented!() } + // TODO Alright: research a sensible value for this. It represents the minimum amount of coins that can be traded + fn min_trading_vol(&self) -> MmNumber { hastings_to_siacoin(1u64.into()).into() } - fn is_trezor(&self) -> bool { self.0.priv_key_policy.is_trezor() } + fn is_trezor(&self) -> bool { self.priv_key_policy.is_trezor() } } -#[async_trait] -impl SwapOps for SiaCoin { - async fn send_taker_fee( +// contains various helpers to account for subpar error handling trait method signatures +impl SiaCoin { + pub fn my_keypair(&self) -> Result<&SiaKeypair, SiaCoinError> { + match &*self.priv_key_policy { + PrivKeyPolicy::Iguana(keypair) => Ok(keypair), + _ => Err(SiaCoinError::MyKeypairPrivKeyPolicy), + } + } +} + +// 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, _fee_addr: &[u8], - _dex_fee: DexFee, - _uuid: &[u8], + dex_fee: DexFee, + uuid: &[u8], _expire_at: u64, - ) -> TransactionResult { - unimplemented!() + ) -> Result { + // Check the Uuid provided is valid v4 as we will encode it into the transaction + let uuid_type_check = Uuid::from_slice(uuid).map_err(SendTakerFeeError::ParseUuid)?; + + 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)).map_err(SendTakerFeeError::SiacoinToHastings)? + }, + wrong_variant => return Err(SendTakerFeeError::DexFeeVariant(wrong_variant)), + }; + + let my_keypair = self.my_keypair().map_err(SendTakerFeeError::MyKeypair)?; + + // Create a new transaction builder + let mut tx_builder = V2TransactionBuilder::new(); + + // FIXME Alright: Calculate the miner fee amount + tx_builder.miner_fee(Currency::DEFAULT_FEE); + + // Add the trade fee output + tx_builder.add_siacoin_output((FEE_ADDR.clone(), trade_fee_amount).into()); + + // Fund the transaction + self.client + .fund_tx_single_source(&mut tx_builder, &my_keypair.public()) + .await + .map_err(SendTakerFeeError::FundTx)?; + + // Embed swap uuid to provide better validation from maker + tx_builder.arbitrary_data(uuid.to_vec()); + + // Sign inputs and finalize the transaction + let tx = tx_builder.sign_simple(vec![my_keypair]).build(); + + // Broadcast the transaction + self.client + .broadcast_transaction(&tx) + .await + .map_err(|e| SendTakerFeeError::BroadcastTx(e))?; + + Ok(TransactionEnum::SiaTransaction(tx.into())) } - async fn send_maker_payment(&self, _maker_payment_args: SendPaymentArgs<'_>) -> TransactionResult { - unimplemented!() + async fn new_send_maker_payment( + &self, + args: SendPaymentArgs<'_>, + ) -> Result { + let my_keypair = self.my_keypair().map_err(SendMakerPaymentError::MyKeypair)?; + + let maker_public_key = my_keypair.public(); + let taker_public_key = + PublicKey::from_bytes(args.other_pubkey).map_err(SendMakerPaymentError::InvalidTakerPublicKey)?; + + let secret_hash = Hash256::try_from(args.secret_hash).map_err(SendMakerPaymentError::ParseSecretHash)?; + + // 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).map_err(SendMakerPaymentError::SiacoinToHastings)?; + + // Create a new transaction builder + let mut tx_builder = V2TransactionBuilder::new(); + + // FIXME Alright: Calculate the miner fee amount + tx_builder.miner_fee(Currency::DEFAULT_FEE); + + // Add the HTLC output + tx_builder.add_siacoin_output((htlc_spend_policy.address(), trade_amount).into()); + + // Fund the transaction + self.client + .fund_tx_single_source(&mut tx_builder, &my_keypair.public()) + .await + .map_err(|e| SendMakerPaymentError::FundTx(e))?; + + // Sign inputs and finalize the transaction + let tx = tx_builder.sign_simple(vec![my_keypair]).build(); + + // Broadcast the transaction + self.client + .broadcast_transaction(&tx) + .await + .map_err(|e| SendMakerPaymentError::BroadcastTx(e))?; + + Ok(TransactionEnum::SiaTransaction(tx.into())) } - async fn send_taker_payment(&self, _taker_payment_args: SendPaymentArgs<'_>) -> TransactionResult { - unimplemented!() + async fn new_send_taker_payment( + &self, + args: SendPaymentArgs<'_>, + ) -> Result { + let my_keypair = self.my_keypair().map_err(SendTakerPaymentError::MyKeypair)?; + + let taker_public_key = my_keypair.public(); + let maker_public_key = + PublicKey::from_bytes(args.other_pubkey).map_err(SendTakerPaymentError::InvalidMakerPublicKey)?; + + let secret_hash = Hash256::try_from(args.secret_hash).map_err(SendTakerPaymentError::SecretHashLength)?; + + // 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).map_err(SendTakerPaymentError::SiacoinToHastings)?; + + // Create a new transaction builder + let mut tx_builder = V2TransactionBuilder::new(); + + tx_builder + .miner_fee(Currency::DEFAULT_FEE) // Set the miner fee amount + .add_siacoin_output((htlc_spend_policy.address(), trade_amount).into()); // Add the HTLC output + + // Fund the transaction + self.client + .fund_tx_single_source(&mut tx_builder, &my_keypair.public()) + .await + .map_err(|e| SendTakerPaymentError::FundTx(e))?; + + // Sign inputs and finalize the transaction + let tx = tx_builder.sign_simple(vec![my_keypair]).build(); + + // Broadcast the transaction + self.client + .broadcast_transaction(&tx) + .await + .map_err(|e| SendTakerPaymentError::BroadcastTx(e))?; + + Ok(TransactionEnum::SiaTransaction(tx.into())) } - async fn send_maker_spends_taker_payment( + // 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, - _maker_spends_payment_args: SpendPaymentArgs<'_>, - ) -> TransactionResult { - unimplemented!() + args: SpendPaymentArgs<'_>, + ) -> Result { + let my_keypair = self.my_keypair().map_err(MakerSpendsTakerPaymentError::MyKeypair)?; + + let maker_public_key = my_keypair.public(); + let taker_public_key = + PublicKey::from_bytes(args.other_pubkey).map_err(MakerSpendsTakerPaymentError::InvalidTakerPublicKey)?; + + let taker_payment_tx = + SiaTransaction::try_from(args.other_payment_tx.to_vec()).map_err(MakerSpendsTakerPaymentError::ParseTx)?; + let taker_payment_txid = taker_payment_tx.txid(); + + let secret = Preimage::try_from(args.secret).map_err(MakerSpendsTakerPaymentError::ParseSecret)?; + let secret_hash = Hash256::try_from(args.secret_hash).map_err(MakerSpendsTakerPaymentError::ParseSecretHash)?; + // 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(|e| MakerSpendsTakerPaymentError::UtxoFromTxid(e))?; + + // 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.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, input_spend_policy) + // Satisfy the HTLC by providing a signature and the secret + .satisfy_atomic_swap_success(my_keypair, secret, 0u32) + .map_err(MakerSpendsTakerPaymentError::SatisfyHtlc)? + .build(); + + // Broadcast the transaction + self.client + .broadcast_transaction(&tx) + .await + .map_err(|e| MakerSpendsTakerPaymentError::BroadcastTx(e))?; + + Ok(TransactionEnum::SiaTransaction(tx.into())) } - async fn send_taker_spends_maker_payment( + async fn new_send_taker_spends_maker_payment( &self, - _taker_spends_payment_args: SpendPaymentArgs<'_>, - ) -> TransactionResult { - unimplemented!() + args: SpendPaymentArgs<'_>, + ) -> Result { + let my_keypair = self.my_keypair().map_err(TakerSpendsMakerPaymentError::MyKeypair)?; + + let taker_public_key = my_keypair.public(); + let maker_public_key = + PublicKey::from_bytes(args.other_pubkey).map_err(TakerSpendsMakerPaymentError::InvalidMakerPublicKey)?; + + let maker_payment_tx = + SiaTransaction::try_from(args.other_payment_tx.to_vec()).map_err(TakerSpendsMakerPaymentError::ParseTx)?; + let maker_payment_txid = maker_payment_tx.txid(); + + let secret = Preimage::try_from(args.secret).map_err(TakerSpendsMakerPaymentError::ParseSecret)?; + let secret_hash = Hash256::try_from(args.secret_hash).map_err(TakerSpendsMakerPaymentError::ParseSecretHash)?; + // 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(|e| TakerSpendsMakerPaymentError::UtxoFromTxid(e))?; + + let miner_fee = Currency::DEFAULT_FEE; + let htlc_utxo_amount = htlc_utxo.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(htlc_utxo, input_spend_policy) + // Satisfy the HTLC by providing a signature and the secret + .satisfy_atomic_swap_success(my_keypair, secret, 0u32) + .map_err(TakerSpendsMakerPaymentError::SatisfyHtlc)? + .build(); + + // Broadcast the transaction + self.client + .broadcast_transaction(&tx) + .await + .map_err(|e| TakerSpendsMakerPaymentError::BroadcastTx(e))?; + + Ok(TransactionEnum::SiaTransaction(tx.into())) } - async fn send_taker_refunds_payment( + async fn new_validate_fee(&self, args: ValidateFeeArgs<'_>) -> Result<(), ValidateFeeError> { + let args = SiaValidateFeeArgs::try_from(args).map_err(ValidateFeeError::ParseArgs)?; + + let fee_tx = args.fee_tx.0.clone(); + let fee_txid = fee_tx.txid(); + + let event = self + .client + .get_event(&fee_txid) + .await + .map_err(ValidateFeeError::FetchEvent)?; + + // Begin validation logic + + // check that 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::MininumHeight { + event, + min_block_number: args.min_block_number, + }); + } + + // 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 exactly 1 output + match fee_tx.siacoin_outputs.len() { + 1 => (), + 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).map_err(ValidateFeeError::ParseUuid)?; + 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_hltc(&self, args: RefundPaymentArgs<'_>) -> Result { + let my_keypair = self.my_keypair().map_err(SendRefundHltcError::MyKeypair)?; + let refund_public_key = my_keypair.public(); + + // parse KDF provided data to Sia specific types + let sia_args = SiaRefundPaymentArgs::try_from(args).map_err(SendRefundHltcError::ParseArgs)?; + + // 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(|e| SendRefundHltcError::UtxoFromTxid(e))?; + + let miner_fee = Currency::DEFAULT_FEE; + let htlc_utxo_amount = htlc_utxo.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(htlc_utxo, input_spend_policy) + // Satisfy the HTLC by providing a signature and the secret + .satisfy_atomic_swap_refund(my_keypair, 0u32) + .map_err(SendRefundHltcError::SatisfyHtlc)? + .build(); + + // Broadcast the transaction + self.client + .broadcast_transaction(&tx) + .await + .map_err(|e| SendRefundHltcError::BroadcastTx(e))?; + + Ok(TransactionEnum::SiaTransaction(tx.into())) + } + + async fn new_check_if_my_payment_sent( &self, - _taker_refunds_payment_args: RefundPaymentArgs<'_>, + args: CheckIfMyPaymentSentArgs<'_>, + ) -> Result, SiaCheckIfMyPaymentSentError> { + let sia_args = SiaCheckIfMyPaymentSentArgs::try_from(args).map_err(SiaCheckIfMyPaymentSentError::ParseArgs)?; + + let my_keypair = self.my_keypair().map_err(SiaCheckIfMyPaymentSentError::MyKeypair)?; + let refund_public_key = my_keypair.public(); + + // Generate HTLC SpendPolicy + 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(); + + let events_result = self.client.get_address_events(htlc_address).await; + + let events = match events_result { + Ok(events) => events, + Err(_) => return Ok(None), + }; + + let event = match events.len() { + 0 => return Ok(None), + 1 => events[0].clone(), + _ => return Err(SiaCheckIfMyPaymentSentError::MultipleEvents(events)), + }; + + 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())) + } +} + +/// 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()).map_err(SiaRefundPaymentArgsError::ParseTx)?; + + let time_lock = args.time_lock; + + let success_public_key = + PublicKey::from_bytes(args.other_pubkey).map_err(SiaRefundPaymentArgsError::ParseOtherPublicKey)?; + + 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).map_err(SiaRefundPaymentArgsError::ParseSecretHash)?; + + // 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())), + }; + + let expected_sender_public_key = + PublicKey::from_bytes(args.expected_sender).map_err(SiaValidateFeeArgsError::InvalidTakerPublicKey)?; + + // 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())) + .map_err(SiaValidateFeeArgsError::SiacoinToHastings)?, + wrong_variant => return Err(SiaValidateFeeArgsError::DexFeeVariant(wrong_variant.clone())), + }; + + // Check the Uuid provided is valid v4 + let uuid = Uuid::from_slice(args.uuid).map_err(SiaValidateFeeArgsError::ParseUuid)?; + + 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::ValidatePaymentInput +/// Does not include swap_contract_address, try_spv_proof_until, unique_swap_data, watcher_reward +/// as they are not relevant to Sia +// #[derive(Clone, Debug)] +// struct SiaValidatePaymentInput { +// payment_tx: SiaTransaction, +// time_lock_duration: u64, +// time_lock: u64, +// other_pub: PublicKey, +// secret_hash: Hash256, +// amount: Currency, +// confirmations: u64, +// } + +/// 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, + search_from_block: u64, + amount: Currency, +} + +impl TryFrom> for SiaCheckIfMyPaymentSentArgs { + type Error = SiaCheckIfMyPaymentSentArgsError; + + fn try_from(args: CheckIfMyPaymentSentArgs<'_>) -> Result { + let time_lock = args.time_lock; + let success_public_key = + PublicKey::from_bytes(args.other_pub).map_err(SiaCheckIfMyPaymentSentArgsError::ParseOtherPublicKey)?; + let secret_hash = + Hash256::try_from(args.secret_hash).map_err(SiaCheckIfMyPaymentSentArgsError::ParseSecretHash)?; + let search_from_block = args.search_from_block; + let amount = + siacoin_to_hastings(args.amount.clone()).map_err(SiaCheckIfMyPaymentSentArgsError::SiacoinToHastings)?; + + Ok(SiaCheckIfMyPaymentSentArgs { + time_lock, + success_public_key, + secret_hash, + search_from_block, + amount, + }) + } +} + +#[async_trait] +impl SwapOps for SiaCoin { + /* 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, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8], expire_at: u64) -> TransactionResult { + self.new_send_taker_fee(fee_addr, dex_fee, uuid, expire_at) + .await + .map_err(|e| e.to_string().into()) + } + + 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 { + 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<'_>, ) -> TransactionResult { - unimplemented!() + self.new_send_maker_spends_taker_payment(maker_spends_payment_args) + .await + .map_err(|e| e.to_string().into()) } - async fn send_maker_refunds_payment( + async fn send_taker_spends_maker_payment( &self, - _maker_refunds_payment_args: RefundPaymentArgs<'_>, + 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 validate_fee(&self, _validate_fee_args: ValidateFeeArgs<'_>) -> ValidatePaymentResult<()> { - unimplemented!() + async fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { + self.send_refund_hltc(taker_refunds_payment_args) + .await + .map_err(|e| SendRefundHltcMakerOrTakerError::Taker(e).to_string().into()) } - async fn validate_maker_payment(&self, _input: ValidatePaymentInput) -> ValidatePaymentResult<()> { - unimplemented!() + async fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { + self.send_refund_hltc(maker_refunds_payment_args) + .await + .map_err(|e| SendRefundHltcMakerOrTakerError::Maker(e).to_string().into()) } - async fn validate_taker_payment(&self, _input: ValidatePaymentInput) -> 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()))) } + // FIXME Alright + async fn validate_maker_payment(&self, _input: ValidatePaymentInput) -> ValidatePaymentResult<()> { Ok(()) } + + // FIXME Alright + async fn validate_taker_payment(&self, _input: ValidatePaymentInput) -> ValidatePaymentResult<()> { Ok(()) } + + // 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( @@ -504,26 +1451,29 @@ impl SwapOps for SiaCoin { fn derive_htlc_key_pair(&self, _swap_unique_data: &[u8]) -> KeyPair { unimplemented!() } + // FIXME Alright - return the iguana ed25519 public key fn derive_htlc_pubkey(&self, _swap_unique_data: &[u8]) -> Vec { unimplemented!() } async fn can_refund_htlc(&self, _locktime: u64) -> Result { unimplemented!() } + // FIXME Alright - validate the other side's "htlc_pubkey" - other party generates this with SwapOps::derive_htlc_pubkey fn validate_other_pubkey(&self, _raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { unimplemented!() } + // lightning specific async fn maker_payment_instructions( &self, _args: PaymentInstructionArgs<'_>, ) -> Result>, MmError> { unimplemented!() } - + // lightning specific async fn taker_payment_instructions( &self, _args: PaymentInstructionArgs<'_>, ) -> Result>, MmError> { unimplemented!() } - + // lightning specific fn validate_maker_payment_instructions( &self, _instructions: &[u8], @@ -531,7 +1481,7 @@ impl SwapOps for SiaCoin { ) -> Result> { unimplemented!() } - + // lightning specific fn validate_taker_payment_instructions( &self, _instructions: &[u8], @@ -541,6 +1491,7 @@ impl SwapOps for SiaCoin { } } +// lightning specific #[async_trait] impl TakerSwapMakerCoin for SiaCoin { async fn on_taker_payment_refund_start(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } @@ -548,6 +1499,7 @@ impl TakerSwapMakerCoin for SiaCoin { async fn on_taker_payment_refund_success(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } } +// lightning specific #[async_trait] impl MakerSwapTakerCoin for SiaCoin { async fn on_maker_payment_refund_start(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } @@ -555,76 +1507,271 @@ impl MakerSwapTakerCoin for SiaCoin { async fn on_maker_payment_refund_success(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } } -#[async_trait] -impl WatcherOps for SiaCoin { - fn send_maker_payment_spend_preimage(&self, _input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { - unimplemented!(); - } +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, From, Into)] +#[serde(transparent)] +pub struct SiaTransaction(pub V2Transaction); - fn send_taker_payment_refund_preimage(&self, _watcher_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { - unimplemented!(); - } +impl SiaTransaction { + pub fn txid(&self) -> Hash256 { self.0.txid() } +} - fn create_taker_payment_refund_preimage( - &self, - _taker_payment_tx: &[u8], - _time_lock: u64, - _maker_pub: &[u8], - _secret_hash: &[u8], - _swap_contract_address: &Option, - _swap_unique_data: &[u8], - ) -> TransactionFut { - unimplemented!(); - } +impl TryFrom for Vec { + type Error = SiaTransactionError; - fn create_maker_payment_spend_preimage( - &self, - _maker_payment_tx: &[u8], - _time_lock: u64, - _maker_pub: &[u8], - _secret_hash: &[u8], - _swap_unique_data: &[u8], - ) -> TransactionFut { - unimplemented!(); + fn try_from(tx: SiaTransaction) -> Result { + serde_json::ser::to_vec(&tx).map_err(SiaTransactionError::ToVec) } +} - fn watcher_validate_taker_fee(&self, _input: WatcherValidateTakerFeeInput) -> ValidatePaymentFut<()> { - unimplemented!(); - } +impl TryFrom> for SiaTransaction { + type Error = SiaTransactionError; - fn watcher_validate_taker_payment(&self, _input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()> { - unimplemented!(); + fn try_from(tx: Vec) -> Result { + serde_json::de::from_slice(&tx).map_err(SiaTransactionError::FromVec) } +} - fn taker_validates_payment_spend_or_refund(&self, _input: ValidateWatcherSpendInput) -> ValidatePaymentFut<()> { - unimplemented!() - } +impl Transaction for SiaTransaction { + // serde should always be succesful but write an empty vec just in case. + // FIXME Alright this trait should be refactored to return a Result for this method + fn tx_hex(&self) -> Vec { serde_json::ser::to_vec(self).unwrap_or_default() } - async fn watcher_search_for_swap_tx_spend( - &self, - _input: WatcherSearchForSwapTxSpendInput<'_>, - ) -> Result, String> { - unimplemented!(); + 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 { + async fn get_unspent_outputs(&self, address: Address) -> Result, MmError> { + let request = GetAddressUtxosRequest { + address, + limit: None, + offset: None, + }; + let res = self.client.dispatcher(request).await?; + Ok(res) } - async fn get_taker_watcher_reward( - &self, - _other_coin: &MmCoinEnum, - _coin_amount: Option, - _other_coin_amount: Option, - _reward_amount: Option, - _wait_until: u64, - ) -> Result> { - unimplemented!() + 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) } - async fn get_maker_watcher_reward( - &self, - _other_coin: &MmCoinEnum, - _reward_amount: Option, - _wait_until: u64, - ) -> Result, MmError> { - unimplemented!() + // 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 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: vec![].into(), + kmd_rewards: None, + transaction_type: TransactionType::SiaV2Transaction, + memo: None, + }) + }, + EventDataWrapper::V1Transaction(tx) => { + let txid = tx.transaction.txid().to_string(); + + 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(); + + let fee = total_input - total_output; + + 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: vec![].into(), + kmd_rewards: None, + transaction_type: TransactionType::SiaV1Transaction, + memo: None, + }) + }, + EventDataWrapper::MinerPayout(event_payout) | EventDataWrapper::FoundationPayout(event_payout) => { + let txid = event_payout.siacoin_element.state_element.id.to_string(); + + 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: vec![].into(), + 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")), + } } } @@ -634,22 +1781,241 @@ mod tests { use mm2_number::BigDecimal; use std::str::FromStr; + fn valid_transaction() -> SiaTransaction { + let j = json!( + { + "siacoinInputs": [ + { + "parent": { + "id": "h:f59e395dc5cbe3217ee80eff60585ffc9802e7ca580d55297782d4a9b4e08589", + "leafIndex": 3, + "merkleProof": [ + "h:ab0e1726444c50e2c0f7325eb65e5bd262a97aad2647d2816c39d97958d9588a", + "h:467e2be4d8482eca1f99440b6efd531ab556d10a8371a98a05b00cb284620cf0", + "h:64d5766fce1ff78a13a4a4744795ad49a8f8d187c01f9f46544810049643a74a", + "h:31d5151875152bc25d1df18ca6bbda1bef5b351e8d53c277791ecf416fcbb8a8", + "h:12a92a1ba87c7b38f3c4e264c399abfa28fb46274cfa429605a6409bd6d0a779", + "h:eda1d58a9282dbf6c3f1beb4d6c7bdc036d14a1cfee8ab1e94fabefa9bd63865", + "h:e03dee6e27220386c906f19fec711647353a5f6d76633a191cbc2f6dce239e89", + "h:e70fcf0129c500f7afb49f4f2bb82950462e952b7cdebb2ad0aa1561dc6ea8eb" + ], + "siacoinOutput": { + "value": "300000000000000000000000000000", + "address": "addr:f7843ac265b037658b304468013da4fd0f304a1b73df0dc68c4273c867bfa38d01a7661a187f" + }, + "maturityHeight": 145 + }, + "satisfiedPolicy": { + "policy": { + "type": "uc", + "policy": { + "timelock": 0, + "publicKeys": [ + "ed25519:cecc1507dc1ddd7295951c290888f095adb9044d1b73d696e6df065d683bd4fc" + ], + "signaturesRequired": 1 + } + }, + "signatures": [ + "sig:f0a29ba576eb0dbc3438877ac1d3a6da4f3c4cbafd9030709c8a83c2fffa64f4dd080d37444261f023af3bd7a10a9597c33616267d5371bf2c0ade5e25e61903" + ] + } + } + ], + "siacoinOutputs": [ + { + "value": "1000000000000000000000000000", + "address": "addr:000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69" + }, + { + "value": "299000000000000000000000000000", + "address": "addr:f7843ac265b037658b304468013da4fd0f304a1b73df0dc68c4273c867bfa38d01a7661a187f" + } + ], + "minerFee": "0" + } + ); + 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_ED25510).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://sia-walletd.komodo.earth/").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..6a02b7d096 --- /dev/null +++ b/mm2src/coins/siacoin/error.rs @@ -0,0 +1,251 @@ +use crate::siacoin::{Address, Currency, Event, EventDataWrapper, ParseHashError, PreimageError, PrivateKeyError, + PublicKeyError, SiaApiClientError, SiaClientHelperError, TransactionId, V2TransactionBuilderError}; +use crate::{DexFee, TransactionEnum}; +use common::executor::AbortedError; +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] SiaCoinError), + #[error("SiaCoin::new_send_taker_fee: failed to fund transaction {0}")] + FundTx(SiaClientHelperError), + #[error("SiaCoin::new_send_taker_fee: failed to broadcast taker_fee transaction {0}")] + BroadcastTx(SiaClientHelperError), +} + +#[derive(Debug, Error)] +pub enum SendMakerPaymentError { + #[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] SiaCoinError), + #[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(SiaClientHelperError), + #[error("SiaCoin::new_send_maker_payment: failed to parse secret_hash {0}")] + ParseSecretHash(#[from] ParseHashError), + #[error("SiaCoin::new_send_maker_payment: failed to broadcast maker_payment transaction {0}")] + BroadcastTx(SiaClientHelperError), +} + +#[derive(Debug, Error)] +pub enum SendTakerPaymentError { + #[error("SiaCoin::new_send_taker_payment: invalid taker pubkey {0}")] + InvalidMakerPublicKey(#[from] PublicKeyError), + #[error("SiaCoin::new_send_taker_payment: failed to fetch my_keypair {0}")] + MyKeypair(#[from] SiaCoinError), + #[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(SiaClientHelperError), + #[error("SiaCoin::new_send_taker_payment: invalid secret_hash length {0}")] + SecretHashLength(#[from] ParseHashError), + #[error("SiaCoin::new_send_taker_payment: failed to broadcast taker_payment transaction {0}")] + BroadcastTx(SiaClientHelperError), +} + +/// 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] SiaCoinError), + #[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}")] + UtxoFromTxid(SiaClientHelperError), + #[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(SiaClientHelperError), +} + +#[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 fee_tx event {0}")] + FetchEvent(#[from] SiaClientHelperError), + #[error("SiaCoin::new_validate_fee: tx confirmed before min_block_number:{min_block_number} event:{event:?}")] + MininumHeight { event: Event, 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")] + 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] SiaCoinError), + #[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] ParseHashError), + #[error("SiaCoin::new_send_taker_spends_maker_payment: failed to fetch SiacoinElement from txid {0}")] + UtxoFromTxid(SiaClientHelperError), + #[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(SiaClientHelperError), +} + +#[derive(Debug, Error)] +pub enum MakerSpendsTakerPaymentError { + #[error("SiaCoin::new_send_maker_spends_taker_payment: failed to fetch my_keypair {0}")] + MyKeypair(#[from] SiaCoinError), + #[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] ParseHashError), + #[error("SiaCoin::new_send_maker_spends_taker_payment: failed to fetch SiacoinElement from txid {0}")] + UtxoFromTxid(SiaClientHelperError), + #[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(SiaClientHelperError), +} + +#[derive(Debug, Error)] +pub enum SiaRefundPaymentArgsError { + #[error("SiaRefundPaymentArgs: failed to parse other_pubkey {0}")] + ParseOtherPublicKey(#[from] PublicKeyError), + #[error("SiaRefundPaymentArgs: failed to parse payment_tx {0}")] + ParseTx(#[from] SiaTransactionError), + #[error("SiaRefundPaymentArgs: failed to parse secret_hash {0}")] + ParseSecretHash(#[from] ParseHashError), + // SwapTxTypeVariant uses String Debug trait representation to avoid explicit lifetime annotations + // otherwise this should be SwapTxTypeVariant(SwapTxTypeWithSecretHash) and displayed via {0:?} + #[error("SiaRefundPaymentArgs: unexpected SwapTxTypeWithSecretHash variant {0}")] + SwapTxTypeVariant(String), +} + +#[derive(Debug, Error)] +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 {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] SiaApiClientError), +} + +// 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 SiaCoinError { + #[error("SiaCoin::from_conf_and_request: failed to parse SiaCoinConf from JSON: {0}")] + InvalidConf(#[from] serde_json::Error), + #[error("SiaCoin::from_conf_and_request: invalid private key: {0}")] + InvalidPrivateKey(#[from] PrivateKeyError), + #[error("SiaCoin::from_conf_and_request: invalid private key policy, must use iguana seed")] + UnsupportedPrivKeyPolicy, + #[error("SiaCoin::from_conf_and_request: failed to build SiaCoin: {0}")] + Builder(#[from] SiaCoinBuilderError), + #[error("SiaCoin::my_keypair: invalid private key policy, must use iguana seed")] + MyKeypairPrivKeyPolicy, +} + +#[derive(Debug, Error)] +pub enum SiaCheckIfMyPaymentSentArgsError { + #[error("SiaCheckIfMyPaymentSentArgs::TryFrom: failed to parse other_pub {0}")] + ParseOtherPublicKey(#[from] PublicKeyError), + #[error("SiaCheckIfMyPaymentSentArgs::TryFrom: failed to parse secret_hash {0}")] + ParseSecretHash(#[from] ParseHashError), + #[error( + "SiaCheckIfMyPaymentSentArgs::TryFrom: failed to convert amount to Currency {0}" + )] + SiacoinToHastings(#[from] SiacoinToHastingsError), +} + +#[derive(Debug, Error)] +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] SiaCoinError), + #[error("SiaCoin::new_check_if_my_payment_sent: failed to fetch address events: {0}")] + FetchEvents(#[from] SiaClientHelperError), + #[error("SiaCoin::new_check_if_my_payment_sent: expected to find single event found: {0:?}")] + MultipleEvents(Vec), + #[error("SiaCoin::new_check_if_my_payment_sent: unexpected event variant: {0:?}")] + EventVariant(EventDataWrapper), +} diff --git a/mm2src/coins/siacoin/sia_hd_wallet.rs b/mm2src/coins/siacoin/sia_hd_wallet.rs index 4c6a288ef5..0cc086d59b 100644 --- a/mm2src/coins/siacoin/sia_hd_wallet.rs +++ b/mm2src/coins/siacoin/sia_hd_wallet.rs @@ -1,8 +1,8 @@ use crate::hd_wallet::{HDAccount, HDAddress, HDWallet}; +use crate::siacoin::{Address, PublicKey}; use bip32::{ExtendedPublicKey, PrivateKeyBytes, PublicKey as bip32PublicKey, PublicKeyBytes, Result as bip32Result}; -use sia_rust::types::Address; -use sia_rust::PublicKey; +// TODO remove this wrapper? pub struct SiaPublicKey(pub PublicKey); pub type SiaHDAddress = HDAddress; diff --git a/mm2src/coins/siacoin/sia_withdraw.rs b/mm2src/coins/siacoin/sia_withdraw.rs new file mode 100644 index 0000000000..16e910a744 --- /dev/null +++ b/mm2src/coins/siacoin/sia_withdraw.rs @@ -0,0 +1,158 @@ +use crate::siacoin::{hastings_to_siacoin, siacoin_to_hastings, Address, 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()))?; + + // 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; + + // Get unspent outputs + let unspent_outputs = self + .coin + .get_unspent_outputs(self.from_address.clone()) + .await + .map_err(|e| WithdrawError::Transport(e.to_string()))?; + + // Select outputs to use as inputs + let selected_outputs = self.select_outputs(unspent_outputs, total_amount.into())?; + + // Calculate change amount + let input_sum: Currency = selected_outputs.iter().map(|o| o.siacoin_output.value).sum(); + let change_amount = input_sum - total_amount; + + // Construct transaction + let mut tx_builder = V2TransactionBuilder::new(); + + // Add inputs + for output in selected_outputs { + tx_builder.add_siacoin_input(output, SpendPolicy::PublicKey(self.key_pair.public())); + } + + // Add output for recipient + tx_builder.add_siacoin_output(SiacoinOutput { + value: tx_amount_hastings, + address: to.clone(), + }); + + // Add change output if necessary + if change_amount > Currency::ZERO { + tx_builder.add_siacoin_output(SiacoinOutput { + value: change_amount, + address: self.from_address.clone(), + }); + } + + // Add miner fee + tx_builder.miner_fee(Currency::from(TX_FEE_HASTINGS)); + + // 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 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(), + 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: hastings_to_siacoin(tx_fee), + } + .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/z_coin.rs b/mm2src/coins/z_coin.rs index 390619ce8b..9e3795368e 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -1,7 +1,7 @@ pub mod storage; mod z_balance_streaming; mod z_coin_errors; -#[cfg(all(test, feature = "zhtlc-native-tests"))] +#[cfg(all(test, feature = "zhtlc-native-tests", not(target_arch = "wasm32")))] mod z_coin_native_tests; mod z_htlc; mod z_rpc; diff --git a/mm2src/coins_activation/src/prelude.rs b/mm2src/coins_activation/src/prelude.rs index d000170fa3..c64e54774f 100644 --- a/mm2src/coins_activation/src/prelude.rs +++ b/mm2src/coins_activation/src/prelude.rs @@ -1,5 +1,5 @@ #[cfg(feature = "enable-sia")] -use coins::siacoin::SiaCoinActivationParams; +use coins::siacoin::SiaCoinActivationRequest; use coins::utxo::UtxoActivationParams; use coins::z_coin::ZcoinActivationParams; use coins::{coin_conf, CoinBalance, CoinProtocol, DerivationMethodResponse, MmCoinEnum}; @@ -23,7 +23,7 @@ impl TxHistory for UtxoActivationParams { } #[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 11c72955ab..bcc529f3b4 100644 --- a/mm2src/coins_activation/src/sia_coin_activation.rs +++ b/mm2src/coins_activation/src/sia_coin_activation.rs @@ -7,10 +7,10 @@ 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, SiaCoinError, 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; @@ -92,7 +92,7 @@ pub enum SiaCoinInitError { } impl SiaCoinInitError { - pub fn from_build_err(build_err: SiaCoinBuildError, ticker: String) -> Self { + pub fn from_build_err(build_err: SiaCoinError, ticker: String) -> Self { SiaCoinInitError::CoinCreationError { ticker, error: build_err.to_string(), @@ -180,7 +180,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; @@ -196,13 +196,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)?; - let coin = sia_coin_from_conf_and_params(&ctx, &ticker, &coin_conf, activation_request, priv_key_policy) + let coin = SiaCoin::from_conf_and_request(&ctx, coin_conf, activation_request, priv_key_policy) .await .mm_err(|e| SiaCoinInitError::from_build_err(e, ticker))?; @@ -211,7 +211,7 @@ impl InitStandaloneCoinActivationOps for SiaCoin { async fn get_activation_result( &self, - _ctx: MmArc, + ctx: MmArc, task_handle: SiaCoinRpcTaskHandleShared, _activation_request: &Self::ActivationRequest, ) -> MmResult { @@ -225,6 +225,8 @@ impl InitStandaloneCoinActivationOps for SiaCoin { let balance = self.my_balance().compat().await?; let address = self.my_address()?; + lp_spawn_tx_history(ctx, self.clone().into()).map_to_mm(SiaCoinInitError::Internal)?; + Ok(SiaCoinActivationResult { ticker: self.ticker().into(), current_block, @@ -239,5 +241,6 @@ impl InitStandaloneCoinActivationOps for SiaCoin { _storage: impl TxHistoryStorage, _current_balances: HashMap, ) { + // TODO Alright unclear what this is } } diff --git a/mm2src/common/common.rs b/mm2src/common/common.rs index f665a9a8e0..75b0072274 100644 --- a/mm2src/common/common.rs +++ b/mm2src/common/common.rs @@ -204,6 +204,10 @@ pub const SATOSHIS: u64 = 100_000_000; pub const DEX_FEE_ADDR_PUBKEY: &str = "03bc2c7ba671bae4a6fc835244c9762b41647b9827d4780a89a949b984a8ddcc06"; +// TODO: needs a real key +// pubkey of iguana passphrase "horribly insecure passphrase" for now +pub const DEX_FEE_PUBKEY_ED25510: &str = "8483e8da48fbac06b292fcd077a71078d094789fccfab581debd4dd13410ea08"; + pub const PROXY_REQUEST_EXPIRATION_SEC: i64 = 15; lazy_static! { diff --git a/mm2src/mm2_bin_lib/Cargo.toml b/mm2src/mm2_bin_lib/Cargo.toml index 7415f21b4f..d230ee2760 100644 --- a/mm2src/mm2_bin_lib/Cargo.toml +++ b/mm2src/mm2_bin_lib/Cargo.toml @@ -15,6 +15,8 @@ custom-swap-locktime = ["mm2_main/custom-swap-locktime"] # only for testing purp native = ["mm2_main/native"] # Deprecated track-ctx-pointer = ["mm2_main/track-ctx-pointer"] zhtlc-native-tests = ["mm2_main/zhtlc-native-tests"] +enable-sia = ["mm2_main/enable-sia"] + [[bin]] name = "mm2" diff --git a/mm2src/mm2_main/Cargo.toml b/mm2src/mm2_main/Cargo.toml index 259d3fc934..ad0a0fb271 100644 --- a/mm2src/mm2_main/Cargo.toml +++ b/mm2src/mm2_main/Cargo.toml @@ -128,7 +128,6 @@ ethabi = { version = "17.0.0" } rlp = { version = "0.5" } ethcore-transaction = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git", rev = "mm2-v2.1.1" } rustc-hex = "2" -sia-rust = { git = "https://github.com/KomodoPlatform/sia-rust", rev = "9f188b80b3213bcb604e7619275251ce08fae808" } url = { version = "2.2.2", features = ["serde"] } [build-dependencies] diff --git a/mm2src/mm2_main/src/lp_swap.rs b/mm2src/mm2_main/src/lp_swap.rs index 295e847132..f3772e96dc 100644 --- a/mm2src/mm2_main/src/lp_swap.rs +++ b/mm2src/mm2_main/src/lp_swap.rs @@ -1649,7 +1649,7 @@ impl TryFrom for SecretHashAlgo { } impl SecretHashAlgo { - fn hash_secret(&self, secret: &[u8]) -> Vec { + pub fn hash_secret(&self, secret: &[u8]) -> Vec { match self { SecretHashAlgo::DHASH160 => dhash160(secret).take().into(), SecretHashAlgo::SHA256 => sha256(secret).take().into(), diff --git a/mm2src/mm2_main/src/lp_wallet.rs b/mm2src/mm2_main/src/lp_wallet.rs index 559821a26a..50521a45b5 100644 --- a/mm2src/mm2_main/src/lp_wallet.rs +++ b/mm2src/mm2_main/src/lp_wallet.rs @@ -302,7 +302,7 @@ fn initialize_crypto_context(ctx: &MmArc, passphrase: &str) -> WalletInitResult< /// # Errors /// Returns `MmInitError` if deserialization fails or if there are issues in passphrase handling. /// -pub(crate) async fn initialize_wallet_passphrase(ctx: &MmArc) -> WalletInitResult<()> { +pub async fn initialize_wallet_passphrase(ctx: &MmArc) -> WalletInitResult<()> { let (wallet_name, passphrase) = deserialize_wallet_config(ctx)?; ctx.wallet_name .pin(wallet_name.clone()) diff --git a/mm2src/mm2_main/src/mm2.rs b/mm2src/mm2_main/src/mm2.rs index dd7c7bed27..62a83d0278 100644 --- a/mm2src/mm2_main/src/mm2.rs +++ b/mm2src/mm2_main/src/mm2.rs @@ -83,6 +83,8 @@ pub mod lp_wallet; pub mod rpc; #[cfg(all(target_arch = "wasm32", test))] mod wasm_tests; +#[cfg(all(feature = "enable-sia", test))] mod sia_tests; + pub const PASSWORD_MAXIMUM_CONSECUTIVE_CHARACTERS: usize = 3; #[cfg(feature = "custom-swap-locktime")] diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index ba99379892..0da31fe8d7 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -166,6 +166,8 @@ 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, + #[cfg(feature = "enable-sia")] + "enable_sia" => handle_mmrpc(ctx, request, init_standalone_coin::).await, "enable_tendermint_with_assets" => { handle_mmrpc(ctx, request, enable_platform_coin_with_tokens::).await }, diff --git a/mm2src/mm2_main/src/sia_tests/docker_functional_tests.rs b/mm2src/mm2_main/src/sia_tests/docker_functional_tests.rs new file mode 100644 index 0000000000..5173611d20 --- /dev/null +++ b/mm2src/mm2_main/src/sia_tests/docker_functional_tests.rs @@ -0,0 +1,419 @@ +use coins::siacoin::sia_rust::transport::client::native::NativeClient; +use coins::siacoin::sia_rust::transport::client::{ApiClient as SiaApiClient, ApiClientError}; +use coins::siacoin::sia_rust::transport::endpoints::DebugMineRequest; +use coins::siacoin::sia_rust::types::Address; +use coins::siacoin::{ApiClientHelpers, SiaCoin, SiaCoinActivationRequest}; +use coins::Transaction; +use coins::{PrivKeyBuildPolicy, RefundPaymentArgs, SendPaymentArgs, SpendPaymentArgs, SwapOps, + SwapTxTypeWithSecretHash, TransactionEnum}; +use common::now_sec; +use mm2_number::BigDecimal; +use testcontainers::clients::Cli; + +use crate::lp_swap::SecretHashAlgo; +use crate::lp_wallet::initialize_wallet_passphrase; +use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; + +use tokio; + +use testcontainers::{Container, GenericImage, RunnableImage}; + +async fn mine_blocks(client: &NativeClient, n: i64, addr: &Address) -> Result<(), ApiClientError> { + client + .dispatcher(DebugMineRequest { + address: addr.clone(), + blocks: n, + }) + .await?; + Ok(()) +} + +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 walletd docker container with walletd API bound to a random host port +/// returns the container and the host port it is bound to +fn init_walletd_container(docker: &Cli) -> (Container, u16) { + // Define the Docker image with a tag + let image = GenericImage::new("docker.io/alrighttt/walletd-komodo", "latest").with_exposed_port(9980); + + // Wrap the image in `RunnableImage` to allow custom port mapping to an available host port + // 0 indicates that the host port will be automatically assigned to an available port + let runnable_image = RunnableImage::from(image).with_mapped_port((0, 9980)); + + // Start the container. It will run until `Container` falls out of scope + let container = docker.run(runnable_image); + + // Retrieve the host port that is mapped to the container's 9980 port + let host_port = container.get_host_port_ipv4(9980); + + (container, host_port) +} + +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::from_conf_and_request(&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); + mine_blocks(&maker_sia_coin.client, 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"), + }; + mine_blocks(&maker_sia_coin.client, 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"), + }; + mine_blocks(&maker_sia_coin.client, 1, &maker_address).await.unwrap(); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + maker_sia_coin + .client + .get_transaction(&taker_spends_maker_payment_tx.txid()) + .await + .unwrap(); +} + +/** + * 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(); + mine_blocks(&taker_sia_coin.client, 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"), + }; + mine_blocks(&taker_sia_coin.client, 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"), + }; + mine_blocks(&taker_sia_coin.client, 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); + mine_blocks(&maker_sia_coin.client, 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"), + }; + mine_blocks(&maker_sia_coin.client, 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"), + }; + mine_blocks(&maker_sia_coin.client, 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(); + mine_blocks(&taker_sia_coin.client, 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"), + }; + mine_blocks(&taker_sia_coin.client, 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"), + }; + mine_blocks(&taker_sia_coin.client, 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(); +} diff --git a/mm2src/mm2_main/src/sia_tests/mod.rs b/mm2src/mm2_main/src/sia_tests/mod.rs new file mode 100644 index 0000000000..44819ed8d5 --- /dev/null +++ b/mm2src/mm2_main/src/sia_tests/mod.rs @@ -0,0 +1,2 @@ +#[cfg(all(test, not(target_arch = "wasm32")))] +mod docker_functional_tests; 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 b8546aa218..e9cdd17df7 100644 --- a/mm2src/mm2_main/tests/docker_tests/sia_docker_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/sia_docker_tests.rs @@ -1,118 +1,227 @@ +use coins::siacoin::sia_rust::transport::client::native::{Conf, NativeClient}; +use coins::siacoin::sia_rust::transport::client::{ApiClient, ApiClientError, ApiClientHelpers}; +use coins::siacoin::sia_rust::transport::endpoints::{AddressBalanceRequest, ConsensusTipRequest, DebugMineRequest, + GetAddressUtxosRequest, TxpoolBroadcastRequest}; +use coins::siacoin::sia_rust::types::{Address, Currency, Keypair, SiacoinOutput, SiacoinOutputId, SpendPolicy, + V2TransactionBuilder}; +use coins::siacoin::{SiaCoin, SiaCoinActivationRequest, SiaCoinConf}; +use coins::{MarketCoinOps, PrivKeyBuildPolicy}; use common::block_on; -use sia_rust::http_client::{SiaApiClient, SiaApiClientError, SiaHttpConf}; -use sia_rust::http_endpoints::{AddressBalanceRequest, AddressUtxosRequest, ConsensusTipRequest, 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 mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; +use mm2_main::lp_wallet::initialize_wallet_passphrase; use std::process::Command; 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"); +/* +These tests are intended to ran manually for now. +Otherwise, they can interfere with each other since there is only one docker container initialized for all of them. +TODO: refactor; see block comment in ../docker_tests_sia_unique.rs for more information. +*/ + +fn mine_blocks(client: &NativeClient, n: i64, addr: &Address) -> Result<(), ApiClientError> { + block_on(client.dispatcher(DebugMineRequest { + address: addr.clone(), + blocks: n, + }))?; + Ok(()) +} + +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::from_conf_and_request(&ctx, coin_conf_str, request, priv_key_policy) + .await + .unwrap() +} + +fn default_activation_request() -> SiaCoinActivationRequest { + let activation_request_json = json!( + { + "tx_history": true, + "client_conf": { + "server_url": "http://localhost:9980/", + "password": "password" + } + } + ); + serde_json::from_value::(activation_request_json).unwrap() +} + +#[test] +fn test_sia_init_siacoin() { + let ctx = block_on(init_ctx("horribly insecure passphrase", 9995)); + let coin = block_on(init_siacoin(ctx, "TSIA", &default_activation_request())); + assert_eq!(block_on(coin.client.current_height()).unwrap(), 0); } #[test] fn test_sia_new_client() { - let conf = SiaHttpConf { - url: Url::parse("http://localhost:9980/").unwrap(), - password: "password".to_string(), + let conf = Conf { + server_url: Url::parse("http://localhost:9980/").unwrap(), + password: Some("password".to_string()), + timeout: Some(10), }; - let _api_client = block_on(SiaApiClient::new(conf)).unwrap(); + let _api_client = block_on(NativeClient::new(conf)).unwrap(); } #[test] -fn test_sia_client_bad_auth() { - let conf = SiaHttpConf { - url: Url::parse("http://localhost:9980/").unwrap(), - password: "foo".to_string(), +fn test_sia_endpoint_consensus_tip() { + let conf = Conf { + server_url: Url::parse("http://localhost:9980/").unwrap(), + password: Some("password".to_string()), + timeout: Some(10), }; - let result = block_on(SiaApiClient::new(conf)); - assert!(matches!(result, Err(SiaApiClientError::UnexpectedHttpStatus(401)))); + let api_client = block_on(NativeClient::new(conf)).unwrap(); + let _response = block_on(api_client.dispatcher(ConsensusTipRequest)).unwrap(); } #[test] -fn test_sia_client_consensus_tip() { - let conf = SiaHttpConf { - url: Url::parse("http://localhost:9980/").unwrap(), - password: "password".to_string(), +fn test_sia_endpoint_debug_mine() { + let conf = Conf { + server_url: Url::parse("http://localhost:9980/").unwrap(), + password: Some("password".to_string()), + timeout: Some(10), }; - let api_client = block_on(SiaApiClient::new(conf)).unwrap(); - let _response = block_on(api_client.dispatcher(ConsensusTipRequest)).unwrap(); + let api_client = block_on(NativeClient::new(conf)).unwrap(); + + let address = + Address::from_str("addr:591fcf237f8854b5653d1ac84ae4c107b37f148c3c7b413f292d48db0c25a8840be0653e411f").unwrap(); + block_on(api_client.dispatcher(DebugMineRequest { + address: address.clone(), + blocks: 100, + })) + .unwrap(); + + let height = block_on(api_client.current_height()).unwrap(); + assert_eq!(height, 100); + + // test the helper function as well + mine_blocks(&api_client, 100, &address).unwrap(); + let response = block_on(api_client.dispatcher(ConsensusTipRequest)).unwrap(); + assert_eq!(response.height, 200); } -// This test likely needs to be removed because mine_blocks has possibility of interfering with other async tests -// related to block height #[test] -fn test_sia_client_address_balance() { - let conf = SiaHttpConf { - url: Url::parse("http://localhost:9980/").unwrap(), - password: "password".to_string(), +fn test_sia_endpoint_address_balance() { + let conf = Conf { + server_url: Url::parse("http://localhost:9980/").unwrap(), + password: Some("password".to_string()), + timeout: Some(10), }; - let api_client = block_on(SiaApiClient::new(conf)).unwrap(); + let api_client = block_on(NativeClient::new(conf)).unwrap(); let address = Address::from_str("addr:591fcf237f8854b5653d1ac84ae4c107b37f148c3c7b413f292d48db0c25a8840be0653e411f").unwrap(); - mine_blocks(10, &address); + mine_blocks(&api_client, 10, &address).unwrap(); let request = AddressBalanceRequest { address }; let response = block_on(api_client.dispatcher(request)).unwrap(); - let expected = Currency::new(12919594847110692864, 54210108624275221); + let expected = Currency(1u128); assert_eq!(response.siacoins, expected); - assert_eq!(expected.to_u128(), 1000000000000000000000000000000000000); + assert_eq!(*expected, 1000000000000000000000000000000000000); } #[test] -fn test_sia_client_build_tx() { - let conf = SiaHttpConf { - url: Url::parse("http://localhost:9980/").unwrap(), - password: "password".to_string(), +fn test_sia_build_tx() { + let conf = Conf { + server_url: Url::parse("http://localhost:9980/").unwrap(), + password: Some("password".to_string()), + timeout: Some(10), }; - let api_client = block_on(SiaApiClient::new(conf)).unwrap(); - let sk: SecretKey = SecretKey::from_bytes( + let api_client = block_on(NativeClient::new(conf)).unwrap(); + 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 address = spend_policy.address(); + let address = Address::from_public_key(&keypair.public()); - mine_blocks(201, &address); + mine_blocks(&api_client, 201, &address).unwrap(); - let utxos = block_on(api_client.dispatcher(AddressUtxosRequest { - address: address.clone(), - })) - .unwrap(); - let spend_this = utxos[0].clone(); - let vin = spend_this.clone(); - println!("utxo[0]: {:?}", spend_this); - let vout = SiacoinOutput { - value: spend_this.siacoin_output.value, - address, + // Create a new transaction builder + let mut tx_builder = V2TransactionBuilder::new(); + + // FIXME Alright: Calculate the miner fee amount + tx_builder.miner_fee(2000000u128.into()); + + // send 1 SC to self + tx_builder.add_siacoin_output((address, Currency::COIN).into()); + + // Fund the transaction + block_on(api_client.fund_tx_single_source(&mut tx_builder, &keypair.public())).unwrap(); + + // Sign inputs and finalize the transaction + let tx = tx_builder.sign_simple(vec![&keypair]).build(); + let req = TxpoolBroadcastRequest { + transactions: vec![], + v2transactions: vec![tx], }; - let tx = V2TransactionBuilder::new(0u64.into()) - .add_siacoin_input(vin, spend_policy) - .add_siacoin_output(vout) - .sign_simple(vec![&keypair]) - .unwrap() - .build(); + let _response = block_on(api_client.dispatcher(req)).unwrap(); +} +#[test] +fn test_sia_fetch_utxos() { + let conf = Conf { + server_url: Url::parse("http://localhost:9980/").unwrap(), + password: Some("password".to_string()), + timeout: Some(10), + }; + let api_client = block_on(NativeClient::new(conf)).unwrap(); + let keypair = Keypair::from_private_bytes( + &hex::decode("0100000000000000000000000000000000000000000000000000000000000000").unwrap(), + ) + .unwrap(); + + let address = Address::from_public_key(&keypair.public()); + + mine_blocks(&api_client, 201, &address).unwrap(); + + // Create a new transaction builder + let mut tx_builder = V2TransactionBuilder::new(); + + // FIXME Alright: Calculate the miner fee amount + tx_builder.miner_fee(2000000u128.into()); + + // send 1 SC to self + tx_builder.add_siacoin_output((address.clone(), Currency::COIN).into()); + + // Fund the transaction + block_on(api_client.fund_tx_single_source(&mut tx_builder, &keypair.public())).unwrap(); + + // Sign inputs and finalize the transaction + let tx = tx_builder.sign_simple(vec![&keypair]).build(); + let txid = tx.txid(); let req = TxpoolBroadcastRequest { transactions: vec![], v2transactions: vec![tx], }; let _response = block_on(api_client.dispatcher(req)).unwrap(); + //mine_blocks(&api_client, 2, &address).unwrap(); + + println!("txid: {}", txid); + println!("address: {}", address); + println!("SiacoinOutputId: {}", SiacoinOutputId::new(txid, 0)); + panic!(); } diff --git a/mm2src/mm2_main/tests/docker_tests_sia_unique.rs b/mm2src/mm2_main/tests/docker_tests_sia_unique.rs index 521da60e01..262478c0c3 100644 --- a/mm2src/mm2_main/tests/docker_tests_sia_unique.rs +++ b/mm2src/mm2_main/tests/docker_tests_sia_unique.rs @@ -1,11 +1,31 @@ -#![allow(unused_imports, dead_code)] +/* +Sia docker tests runner. This module is used to run the tests in the sia_docker_tests module. + +An environment variable, SKIP_DOCKER_TESTS_RUNNER, can be set to skip the docker container initialization. This will run the tests with the assumption +that there is a walletd instance at 127.0.0.1:9980. This was added to help with debugging the tests in a local environment. +It can be useful otherwise to inspect the state of the walletd instance after the tests have run. + +This module used the existing docker test suite at the time of Sia integration. It is not a good example of how to write tests in the mm2 codebase. + +Usage: + Run all sia docker tests: + cargo test --test docker_tests_sia_unique --all-features -- --nocapture + + Run a specific test: + cargo test --test docker_tests_sia_unique --all-features test_sia_endpoint_debug_mine -- --nocapture + + Run all sia docker tests without running the docker container: + SKIP_DOCKER_TESTS_RUNNER=1 cargo test --test docker_tests_sia_unique --all-features -- --nocapture + + Run a specific test without running the docker container: + SKIP_DOCKER_TESTS_RUNNER=1 cargo test --test docker_tests_sia_unique --all-features test_sia_endpoint_debug_mine -- --nocapture + +note: `--nocapture` is shown in the example usage, but it is not neccesary. +*/ #![cfg(feature = "enable-sia")] -#![feature(async_closure)] #![feature(custom_test_frameworks)] #![feature(test)] #![test_runner(docker_tests_runner)] -#![feature(drain_filter)] -#![feature(hash_raw_entry)] #![cfg(not(target_arch = "wasm32"))] #[cfg(test)] @@ -20,17 +40,18 @@ 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; +// TODO This docker_tests module is a mess. +// Separate common pieces into a docker_tests_common module that doesn't import an insane amount of unrelated code. +// the use of this tests_runner feature seems unnecessary. Why can't each module initialize its own docker containers? +#[allow(unused_imports, dead_code)] mod docker_tests; use docker_tests::docker_tests_common::*; #[allow(dead_code)] mod integration_tests_common; @@ -53,7 +74,6 @@ pub fn docker_tests_runner(tests: &[&TestDescAndFn]) { } let sia_node = sia_docker_node(&docker, "SIA", 9980); - println!("ran container?"); containers.push(sia_node); } // detect if docker is installed