diff --git a/Cargo.toml b/Cargo.toml index db62394467..e90b2770ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,9 +9,10 @@ version = "0.1.0" edition = "2018" default-run = "mm2" -# Deprecated [features] +# Deprecated native = [] +zhtlc = ["coins/zhtlc"] [[bin]] name = "mm2" diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index 0a99b5bed9..5c14d3e24a 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -3,6 +3,9 @@ name = "coins" version = "0.1.0" edition = "2018" +[features] +zhtlc = ["zcash_client_backend", "zcash_primitives", "zcash_proofs"] + [lib] name = "coins" path = "lp_coins.rs" @@ -77,7 +80,6 @@ rustls = { version = "0.18", features = ["dangerous_configuration"] } tokio = { version = "0.2" } tokio-rustls = { version = "0.14.1" } webpki-roots = { version = "0.19.0" } -zcash_client_backend = { git = "https://github.com/KomodoPlatform/librustzcash.git" } -zcash_primitives = { features = ["transparent-inputs"], git = "https://github.com/KomodoPlatform/librustzcash.git" } -zcash_proofs = { features = ["bundled-prover"], git = "https://github.com/KomodoPlatform/librustzcash.git" } - +zcash_client_backend = { git = "https://github.com/KomodoPlatform/librustzcash.git", optional = true } +zcash_primitives = { features = ["transparent-inputs"], git = "https://github.com/KomodoPlatform/librustzcash.git", optional = true } +zcash_proofs = { features = ["bundled-prover"], git = "https://github.com/KomodoPlatform/librustzcash.git", optional = true } diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 18f0fc8ce4..dbfb47e7de 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -96,9 +96,10 @@ use qrc20::{qrc20_coin_from_conf_and_request, Qrc20Coin, Qrc20FeeDetails}; pub mod test_coin; pub use test_coin::TestCoin; -pub mod tx_history_db; - -#[cfg(not(target_arch = "wasm32"))] pub mod z_coin; +#[cfg(all(not(target_arch = "wasm32"), feature = "zhtlc"))] +pub mod z_coin; +#[cfg(all(not(target_arch = "wasm32"), feature = "zhtlc"))] +use z_coin::{z_coin_from_conf_and_request, ZCoin}; pub type BalanceResult = Result>; pub type BalanceFut = Box> + Send>; @@ -841,6 +842,8 @@ pub enum MmCoinEnum { QtumCoin(QtumCoin), Qrc20Coin(Qrc20Coin), EthCoin(EthCoin), + #[cfg(all(not(target_arch = "wasm32"), feature = "zhtlc"))] + ZCoin(ZCoin), Test(TestCoin), } @@ -864,6 +867,11 @@ impl From for MmCoinEnum { fn from(c: Qrc20Coin) -> MmCoinEnum { MmCoinEnum::Qrc20Coin(c) } } +#[cfg(all(not(target_arch = "wasm32"), feature = "zhtlc"))] +impl From for MmCoinEnum { + fn from(c: ZCoin) -> MmCoinEnum { MmCoinEnum::ZCoin(c) } +} + // NB: When stable and groked by IDEs, `enum_dispatch` can be used instead of `Deref` to speed things up. impl Deref for MmCoinEnum { type Target = dyn MmCoin; @@ -873,6 +881,8 @@ impl Deref for MmCoinEnum { MmCoinEnum::QtumCoin(ref c) => c, MmCoinEnum::Qrc20Coin(ref c) => c, MmCoinEnum::EthCoin(ref c) => c, + #[cfg(all(not(target_arch = "wasm32"), feature = "zhtlc"))] + MmCoinEnum::ZCoin(ref c) => c, MmCoinEnum::Test(ref c) => c, } } @@ -906,9 +916,17 @@ impl CoinsContext { pub enum CoinProtocol { UTXO, QTUM, - QRC20 { platform: String, contract_address: String }, + QRC20 { + platform: String, + contract_address: String, + }, ETH, - ERC20 { platform: String, contract_address: String }, + ERC20 { + platform: String, + contract_address: String, + }, + #[cfg(all(not(target_arch = "wasm32"), feature = "zhtlc"))] + ZHTLC, } pub type RpcTransportEventHandlerShared = Arc; @@ -1114,6 +1132,8 @@ pub async fn lp_coininit(ctx: &MmArc, ticker: &str, req: &Json) -> Result try_s!(z_coin_from_conf_and_request(ctx, ticker, &coins_en, req, secret).await).into(), }; let block_count = try_s!(coin.current_block().compat().await); @@ -1576,5 +1596,7 @@ pub fn address_by_coin_conf_and_pubkey_str(coin: &str, conf: &Json, pubkey: &str CoinProtocol::UTXO | CoinProtocol::QTUM | CoinProtocol::QRC20 { .. } => { utxo::address_by_conf_and_pubkey_str(coin, conf, pubkey) }, + #[cfg(all(not(target_arch = "wasm32"), feature = "zhtlc"))] + CoinProtocol::ZHTLC => utxo::address_by_conf_and_pubkey_str(coin, conf, pubkey), } } diff --git a/mm2src/coins/qrc20/history.rs b/mm2src/coins/qrc20/history.rs index 48beba1062..a29b68c17a 100644 --- a/mm2src/coins/qrc20/history.rs +++ b/mm2src/coins/qrc20/history.rs @@ -332,19 +332,28 @@ impl Qrc20Coin { let history_res = block_on(TransferHistoryBuilder::new(self.clone()).build_tx_idents()); let history = match history_res { Ok(h) => h, - Err(e) => match &e.error { - JsonRpcErrorType::Transport(e) | JsonRpcErrorType::Parse(_, e) => { + Err(e) => match e.into_inner() { + UtxoRpcError::Transport(json_rpc_e) | UtxoRpcError::ResponseParseError(json_rpc_e) => { + match json_rpc_e.error { + JsonRpcErrorType::Response(_addr, err) => { + return if HISTORY_TOO_LARGE_ERROR.eq(&err) { + RequestTxHistoryResult::HistoryTooLarge + } else { + RequestTxHistoryResult::Retry { + error: ERRL!("Error {:?} on blockchain_contract_event_get_history", err), + } + } + }, + JsonRpcErrorType::Transport(err) | JsonRpcErrorType::Parse(_, err) => { + return RequestTxHistoryResult::Retry { + error: ERRL!("Error {} on blockchain_contract_event_get_history", err), + }; + }, + } + }, + UtxoRpcError::InvalidResponse(e) | UtxoRpcError::Internal(e) => { return RequestTxHistoryResult::Retry { error: ERRL!("Error {} on blockchain_contract_event_get_history", e), - }; - }, - JsonRpcErrorType::Response(_addr, err) => { - return if HISTORY_TOO_LARGE_ERROR.eq(err) { - RequestTxHistoryResult::HistoryTooLarge - } else { - RequestTxHistoryResult::Retry { - error: ERRL!("Error {:?} on blockchain_contract_event_get_history", e), - } } }, }, @@ -574,32 +583,38 @@ impl TransferHistoryBuilder { self } - pub async fn build(self) -> Result, JsonRpcError> { + pub async fn build(self) -> Result, MmError> { self.coin.utxo.rpc_client.build(self.params).await } - pub async fn build_tx_idents(self) -> Result, JsonRpcError> { + pub async fn build_tx_idents(self) -> Result, MmError> { self.coin.utxo.rpc_client.build_tx_idents(self.params).await } } #[async_trait] trait BuildTransferHistory { - async fn build(&self, params: TransferHistoryParams) -> Result, JsonRpcError>; + async fn build(&self, params: TransferHistoryParams) -> Result, MmError>; - async fn build_tx_idents(&self, params: TransferHistoryParams) -> Result, JsonRpcError>; + async fn build_tx_idents( + &self, + params: TransferHistoryParams, + ) -> Result, MmError>; } #[async_trait] impl BuildTransferHistory for UtxoRpcClientEnum { - async fn build(&self, params: TransferHistoryParams) -> Result, JsonRpcError> { + async fn build(&self, params: TransferHistoryParams) -> Result, MmError> { match self { UtxoRpcClientEnum::Native(native) => native.build(params).await, UtxoRpcClientEnum::Electrum(electrum) => electrum.build(params).await, } } - async fn build_tx_idents(&self, params: TransferHistoryParams) -> Result, JsonRpcError> { + async fn build_tx_idents( + &self, + params: TransferHistoryParams, + ) -> Result, MmError> { match self { UtxoRpcClientEnum::Native(native) => native.build_tx_idents(params).await, UtxoRpcClientEnum::Electrum(electrum) => electrum.build_tx_idents(params).await, @@ -609,7 +624,7 @@ impl BuildTransferHistory for UtxoRpcClientEnum { #[async_trait] impl BuildTransferHistory for ElectrumClient { - async fn build(&self, params: TransferHistoryParams) -> Result, JsonRpcError> { + async fn build(&self, params: TransferHistoryParams) -> Result, MmError> { let tx_idents = self.build_tx_idents(params).await?; let mut receipts = Vec::new(); @@ -623,7 +638,10 @@ impl BuildTransferHistory for ElectrumClient { Ok(receipts) } - async fn build_tx_idents(&self, params: TransferHistoryParams) -> Result, JsonRpcError> { + async fn build_tx_idents( + &self, + params: TransferHistoryParams, + ) -> Result, MmError> { let address = contract_addr_into_rpc_format(¶ms.address); let token_address = contract_addr_into_rpc_format(¶ms.token_address); let history = self @@ -642,7 +660,7 @@ impl BuildTransferHistory for ElectrumClient { #[async_trait] impl BuildTransferHistory for NativeClient { - async fn build(&self, params: TransferHistoryParams) -> Result, JsonRpcError> { + async fn build(&self, params: TransferHistoryParams) -> Result, MmError> { const SEARCH_LOGS_STEP: u64 = 100; let token_address = contract_addr_into_rpc_format(¶ms.token_address); @@ -680,7 +698,10 @@ impl BuildTransferHistory for NativeClient { Ok(result) } - async fn build_tx_idents(&self, params: TransferHistoryParams) -> Result, JsonRpcError> { + async fn build_tx_idents( + &self, + params: TransferHistoryParams, + ) -> Result, MmError> { let receipts = self.build(params).await?; Ok(receipts .into_iter() diff --git a/mm2src/coins/qrc20/rpc_clients.rs b/mm2src/coins/qrc20/rpc_clients.rs index b8c704167d..61a2da8c45 100644 --- a/mm2src/coins/qrc20/rpc_clients.rs +++ b/mm2src/coins/qrc20/rpc_clients.rs @@ -296,7 +296,7 @@ pub trait Qrc20NativeOps { to_block: Option, addresses: Vec, topics: Vec, - ) -> RpcRes>; + ) -> UtxoRpcFut>; } impl Qrc20NativeOps for NativeClient { @@ -314,7 +314,7 @@ impl Qrc20NativeOps for NativeClient { to_block: Option, addresses: Vec, topics: Vec, - ) -> RpcRes> { + ) -> UtxoRpcFut> { let to_block = to_block.map(|x| x as i64).unwrap_or(-1); let addr_block = json!({ "addresses": addresses }); let topics: Vec = topics @@ -327,7 +327,10 @@ impl Qrc20NativeOps for NativeClient { let topic_block = json!({ "topics": topics, }); - rpc_func!(self, "searchlogs", from_block, to_block, addr_block, topic_block) + Box::new( + rpc_func!(self, "searchlogs", from_block, to_block, addr_block, topic_block) + .map_to_mm_fut(UtxoRpcError::from), + ) } } diff --git a/mm2src/coins/utxo/rpc_clients.rs b/mm2src/coins/utxo/rpc_clients.rs index 8e49ffc464..63cb9a6e0b 100644 --- a/mm2src/coins/utxo/rpc_clients.rs +++ b/mm2src/coins/utxo/rpc_clients.rs @@ -215,13 +215,13 @@ pub trait UtxoRpcClientOps: fmt::Debug + Send + Sync + 'static { fn send_transaction(&self, tx: &UtxoTx) -> Box + Send + 'static>; - fn send_raw_transaction(&self, tx: BytesJson) -> RpcRes; + fn send_raw_transaction(&self, tx: BytesJson) -> UtxoRpcFut; fn get_transaction_bytes(&self, txid: H256Json) -> UtxoRpcFut; fn get_verbose_transaction(&self, txid: H256Json) -> RpcRes; - fn get_block_count(&self) -> RpcRes; + fn get_block_count(&self) -> UtxoRpcFut; fn display_balance(&self, address: Address, decimals: u8) -> RpcRes; @@ -569,7 +569,9 @@ impl UtxoRpcClientOps for NativeClient { } /// https://developer.bitcoin.org/reference/rpc/sendrawtransaction - fn send_raw_transaction(&self, tx: BytesJson) -> RpcRes { rpc_func!(self, "sendrawtransaction", tx) } + fn send_raw_transaction(&self, tx: BytesJson) -> UtxoRpcFut { + Box::new(rpc_func!(self, "sendrawtransaction", tx).map_to_mm_fut(UtxoRpcError::from)) + } fn get_transaction_bytes(&self, txid: H256Json) -> UtxoRpcFut { Box::new(self.get_raw_transaction_bytes(txid).map_to_mm_fut(UtxoRpcError::from)) @@ -579,7 +581,9 @@ impl UtxoRpcClientOps for NativeClient { self.get_raw_transaction_verbose(txid) } - fn get_block_count(&self) -> RpcRes { self.0.get_block_count() } + fn get_block_count(&self) -> UtxoRpcFut { + Box::new(self.0.get_block_count().map_to_mm_fut(UtxoRpcError::from)) + } fn display_balance(&self, address: Address, _decimals: u8) -> RpcRes { Box::new( @@ -1470,7 +1474,12 @@ impl UtxoRpcClientOps for ElectrumClient { Box::new(self.blockchain_transaction_broadcast(bytes).map_err(|e| ERRL!("{}", e))) } - fn send_raw_transaction(&self, tx: BytesJson) -> RpcRes { self.blockchain_transaction_broadcast(tx) } + fn send_raw_transaction(&self, tx: BytesJson) -> UtxoRpcFut { + Box::new( + self.blockchain_transaction_broadcast(tx) + .map_to_mm_fut(UtxoRpcError::from), + ) + } /// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-transaction-get /// returns transaction bytes by default @@ -1486,7 +1495,13 @@ impl UtxoRpcClientOps for ElectrumClient { rpc_func!(self, "blockchain.transaction.get", txid, verbose) } - fn get_block_count(&self) -> RpcRes { Box::new(self.blockchain_headers_subscribe().map(|r| r.block_height())) } + fn get_block_count(&self) -> UtxoRpcFut { + Box::new( + self.blockchain_headers_subscribe() + .map(|r| r.block_height()) + .map_to_mm_fut(UtxoRpcError::from), + ) + } fn display_balance(&self, address: Address, decimals: u8) -> RpcRes { let hash = electrum_script_hash(&Builder::build_p2pkh(&address.hash)); diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index 8541e1a7e3..93562cf500 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -1,46 +1,66 @@ -use crate::utxo::rpc_clients::UtxoRpcClientEnum; -use crate::utxo::utxo_common::payment_script; -use crate::utxo::{utxo_common::UtxoArcBuilder, UtxoArc, UtxoCoinBuilder}; -use crate::{BalanceFut, CoinBalance, FoundSwapTxSpend, MarketCoinOps, NegotiateSwapContractAddrErr, SwapOps, - TransactionEnum, TransactionFut}; +use crate::utxo::rpc_clients::{UnspentInfo, UtxoRpcClientEnum, UtxoRpcResult}; +use crate::utxo::utxo_common::{payment_script, UtxoArcBuilder}; +use crate::utxo::{utxo_common, ActualTxFee, AdditionalTxData, Address, FeePolicy, GenerateTxResult, + RecentlySpentOutPoints, UtxoArc, UtxoCoinBuilder, UtxoCoinFields, UtxoCommonOps, + VerboseTransactionFrom}; +use crate::{BalanceFut, CoinBalance, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MarketCoinOps, MmCoin, + NegotiateSwapContractAddrErr, SwapOps, TradeFee, TradePreimageFut, TradePreimageResult, + TradePreimageValue, TransactionEnum, TransactionFut, ValidateAddressResult, WithdrawFut, WithdrawRequest}; +use async_trait::async_trait; use bitcrypto::dhash160; use chain::constants::SEQUENCE_FINAL; -use chain::Transaction as UtxoTx; +use chain::{Transaction as UtxoTx, TransactionOutput}; +use common::jsonrpc_client::JsonRpcError; use common::mm_ctx::MmArc; use common::mm_error::prelude::*; use common::mm_number::{BigDecimal, MmNumber}; -use futures::compat::Future01CompatExt; +use common::now_ms; +use derive_more::Display; +use futures::lock::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; use futures::{FutureExt, TryFutureExt}; use futures01::Future; use keys::Public; -use rpc::v1::types::Bytes as BytesJson; -use script::{Builder as ScriptBuilder, Opcode}; -use secp256k1_bindings::SecretKey; +use primitives::bytes::Bytes; +use rpc::v1::types::{Bytes as BytesJson, Transaction as RpcTransaction, H256 as H256Json}; +use script::{Builder as ScriptBuilder, Opcode, Script, TransactionInputSigner}; use serde_json::Value as Json; use serialization::deserialize; -use zcash_client_backend::encoding::encode_payment_address; -use zcash_primitives::{consensus, - constants::mainnet as z_mainnet_constants, - legacy::Script as ZCashScript, - sapling::PaymentAddress, - transaction::builder::Builder as ZTxBuilder, - transaction::components::{Amount, OutPoint as ZCashOutpoint, TxOut}, - zip32::ExtendedSpendingKey}; +use std::sync::Arc; +use zcash_client_backend::encoding::{encode_extended_spending_key, encode_payment_address}; +use zcash_primitives::{constants::mainnet as z_mainnet_constants, sapling::PaymentAddress, zip32::ExtendedSpendingKey}; use zcash_proofs::prover::LocalTxProver; mod z_htlc; -use z_htlc::z_send_htlc; +use z_htlc::{z_p2sh_spend, z_send_dex_fee, z_send_htlc}; mod z_rpc; use z_rpc::ZRpcOps; #[cfg(test)] mod z_coin_tests; +pub struct ZCoinFields { + z_spending_key: ExtendedSpendingKey, + z_addr: PaymentAddress, + z_addr_encoded: String, + z_tx_prover: LocalTxProver, + /// Mutex preventing concurrent transaction generation/same input usage + z_tx_mutex: AsyncMutex<()>, +} + +impl std::fmt::Debug for ZCoinFields { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "ZCoinFields {{ z_addr: {:?}, z_addr_encoded: {} }}", + self.z_addr, self.z_addr_encoded + ) + } +} + #[derive(Clone, Debug)] pub struct ZCoin { utxo_arc: UtxoArc, - z_spending_key: ExtendedSpendingKey, - z_addr: PaymentAddress, + z_fields: Arc, } impl ZCoin { @@ -49,7 +69,11 @@ impl ZCoin { pub fn rpc_client(&self) -> &UtxoRpcClientEnum { &self.utxo_arc.rpc_client } } -#[derive(Debug)] +impl AsRef for ZCoin { + fn as_ref(&self) -> &UtxoCoinFields { &self.utxo_arc } +} + +#[derive(Debug, Display)] pub enum ZCoinBuildError { BuilderError(String), GetAddressError, @@ -61,6 +85,17 @@ pub async fn z_coin_from_conf_and_request( conf: &Json, req: &Json, secp_priv_key: &[u8], +) -> Result> { + let z_key = ExtendedSpendingKey::master(secp_priv_key); + z_coin_from_conf_and_request_with_z_key(ctx, ticker, conf, req, secp_priv_key, z_key).await +} + +async fn z_coin_from_conf_and_request_with_z_key( + ctx: &MmArc, + ticker: &str, + conf: &Json, + req: &Json, + secp_priv_key: &[u8], z_spending_key: ExtendedSpendingKey, ) -> Result> { let builder = UtxoArcBuilder::new(ctx, ticker, conf, req, secp_priv_key); @@ -72,26 +107,34 @@ pub async fn z_coin_from_conf_and_request( let (_, z_addr) = z_spending_key .default_address() .map_err(|_| MmError::new(ZCoinBuildError::GetAddressError))?; - Ok(ZCoin { - utxo_arc, + + let z_tx_prover = LocalTxProver::bundled(); + let z_addr_encoded = encode_payment_address(z_mainnet_constants::HRP_SAPLING_PAYMENT_ADDRESS, &z_addr); + let z_fields = ZCoinFields { z_spending_key, z_addr, + z_addr_encoded, + z_tx_prover, + z_tx_mutex: AsyncMutex::new(()), + }; + Ok(ZCoin { + utxo_arc, + z_fields: Arc::new(z_fields), }) } impl MarketCoinOps for ZCoin { - fn ticker(&self) -> &str { todo!() } + fn ticker(&self) -> &str { &self.utxo_arc.conf.ticker } - fn my_address(&self) -> Result { todo!() } + fn my_address(&self) -> Result { Ok(self.z_fields.z_addr_encoded.clone()) } fn my_balance(&self) -> BalanceFut { - let my_address = encode_payment_address(z_mainnet_constants::HRP_SAPLING_PAYMENT_ADDRESS, &self.z_addr); let min_conf = 0; let fut = self .utxo_arc .rpc_client .as_ref() - .z_get_balance(&my_address, min_conf) + .z_get_balance(&self.z_fields.z_addr_encoded, min_conf) // at the moment Z coins do not have an unspendable balance .map(|spendable| CoinBalance { spendable: spendable.to_decimal(), @@ -101,46 +144,73 @@ impl MarketCoinOps for ZCoin { Box::new(fut) } - fn base_coin_balance(&self) -> BalanceFut { todo!() } + fn base_coin_balance(&self) -> BalanceFut { utxo_common::base_coin_balance(self) } - fn send_raw_tx(&self, _tx: &str) -> Box + Send> { todo!() } + fn send_raw_tx(&self, tx: &str) -> Box + Send> { + utxo_common::send_raw_tx(self.as_ref(), tx) + } fn wait_for_confirmations( &self, - _tx: &[u8], - _confirmations: u64, - _requires_nota: bool, - _wait_until: u64, - _check_every: u64, + tx: &[u8], + confirmations: u64, + requires_nota: bool, + wait_until: u64, + check_every: u64, ) -> Box + Send> { - todo!() + utxo_common::wait_for_confirmations(self.as_ref(), tx, confirmations, requires_nota, wait_until, check_every) } fn wait_for_tx_spend( &self, - _transaction: &[u8], - _wait_until: u64, - _from_block: u64, + transaction: &[u8], + wait_until: u64, + from_block: u64, _swap_contract_address: &Option, ) -> TransactionFut { - todo!() + utxo_common::wait_for_tx_spend(self.as_ref(), transaction, wait_until, from_block) } - fn tx_enum_from_bytes(&self, _bytes: &[u8]) -> Result { todo!() } + fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result { + utxo_common::tx_enum_from_bytes(self.as_ref(), bytes) + } - fn current_block(&self) -> Box + Send> { todo!() } + fn current_block(&self) -> Box + Send> { + utxo_common::current_block(&self.utxo_arc) + } fn address_from_pubkey_str(&self, _pubkey: &str) -> Result { todo!() } - fn display_priv_key(&self) -> String { todo!() } + fn display_priv_key(&self) -> String { + encode_extended_spending_key( + z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, + &self.z_fields.z_spending_key, + ) + } - fn min_tx_amount(&self) -> BigDecimal { todo!() } + fn min_tx_amount(&self) -> BigDecimal { utxo_common::min_tx_amount(self.as_ref()) } - fn min_trading_vol(&self) -> MmNumber { todo!() } + fn min_trading_vol(&self) -> MmNumber { utxo_common::min_trading_vol(self.as_ref()) } } impl SwapOps for ZCoin { - fn send_taker_fee(&self, _fee_addr: &[u8], _amount: BigDecimal) -> TransactionFut { todo!() } + fn send_taker_fee(&self, _fee_addr: &[u8], amount: BigDecimal) -> TransactionFut { + // TODO replace dummy locktime and watcher pub + let selfi = self.clone(); + let fut = async move { + let (tx, _) = try_s!( + z_send_dex_fee( + &selfi, + (now_ms() / 1000) as u32, + selfi.utxo_arc.key_pair.public(), + amount + ) + .await + ); + Ok(tx.into()) + }; + Box::new(fut.boxed().compat()) + } fn send_maker_payment( &self, @@ -151,7 +221,7 @@ impl SwapOps for ZCoin { _swap_contract_address: &Option, ) -> TransactionFut { let selfi = self.clone(); - let taker_pub = taker_pub.to_vec(); + let taker_pub = try_fus!(Public::from_slice(taker_pub)); let secret_hash = secret_hash.to_vec(); let fut = async move { let utxo_tx = try_s!(z_send_htlc(&selfi, time_lock, &taker_pub, &secret_hash, amount).await); @@ -169,7 +239,7 @@ impl SwapOps for ZCoin { _swap_contract_address: &Option, ) -> TransactionFut { let selfi = self.clone(); - let maker_pub = maker_pub.to_vec(); + let maker_pub = try_fus!(Public::from_slice(maker_pub)); let secret_hash = secret_hash.to_vec(); let fut = async move { let utxo_tx = try_s!(z_send_htlc(&selfi, time_lock, &maker_pub, &secret_hash, amount).await); @@ -180,13 +250,30 @@ impl SwapOps for ZCoin { fn send_maker_spends_taker_payment( &self, - _taker_payment_tx: &[u8], - _time_lock: u32, - _taker_pub: &[u8], - _secret: &[u8], + taker_payment_tx: &[u8], + time_lock: u32, + taker_pub: &[u8], + secret: &[u8], _swap_contract_address: &Option, ) -> TransactionFut { - todo!() + let tx: UtxoTx = try_fus!(deserialize(taker_payment_tx).map_err(|e| ERRL!("{:?}", e))); + let redeem_script = payment_script( + time_lock, + &*dhash160(secret), + &Public::from_slice(taker_pub).unwrap(), + self.utxo_arc.key_pair.public(), + ); + let script_data = ScriptBuilder::default() + .push_data(secret) + .push_opcode(Opcode::OP_0) + .into_script(); + let selfi = self.clone(); + let fut = async move { + let tx_fut = z_p2sh_spend(&selfi, tx, time_lock, SEQUENCE_FINAL, redeem_script, script_data); + let tx = try_s!(tx_fut.await); + Ok(tx.into()) + }; + Box::new(fut.boxed().compat()) } fn send_taker_spends_maker_payment( @@ -197,76 +284,49 @@ impl SwapOps for ZCoin { secret: &[u8], _swap_contract_address: &Option, ) -> TransactionFut { - let tx: UtxoTx = deserialize(maker_payment_tx).unwrap(); - let secret_hash = dhash160(secret); + let tx: UtxoTx = try_fus!(deserialize(maker_payment_tx).map_err(|e| ERRL!("{:?}", e))); let redeem_script = payment_script( time_lock, - &*secret_hash, + &*dhash160(secret), &Public::from_slice(maker_pub).unwrap(), self.utxo_arc.key_pair.public(), - ) - .to_vec(); - + ); let script_data = ScriptBuilder::default() .push_data(secret) .push_opcode(Opcode::OP_0) - .into_bytes(); + .into_script(); let selfi = self.clone(); let fut = async move { - let current_block = try_s!(selfi.utxo_arc.rpc_client.get_block_count().compat().await) as u32; - let mut tx_builder = ZTxBuilder::new(consensus::MAIN_NETWORK, current_block.into()); - - let secp_secret = SecretKey::from_slice(&*selfi.utxo_arc.key_pair.private().secret).unwrap(); - - let outpoint = ZCashOutpoint::new(tx.hash().into(), 0); - let tx_out = TxOut { - value: Amount::from_u64(tx.outputs[0].value).unwrap(), - script_pubkey: ZCashScript(redeem_script.clone()), - }; - tx_builder - .add_transparent_input( - secp_secret, - outpoint, - SEQUENCE_FINAL, - ZCashScript(script_data.take()), - tx_out, - ) - .unwrap(); - tx_builder - .add_sapling_output( - None, - selfi.z_addr, - Amount::from_u64(tx.outputs[0].value - 1000).unwrap(), - None, - ) - .unwrap(); - let prover = LocalTxProver::bundled(); - let (zcash_tx, _) = tx_builder.build(consensus::BranchId::Sapling, &prover).unwrap(); - let mut tx_buffer = Vec::with_capacity(1024); - zcash_tx.write(&mut tx_buffer).unwrap(); - let spend_tx: UtxoTx = deserialize(tx_buffer.as_slice()).expect("librustzcash should produce a valid tx"); - try_s!( - selfi - .utxo_arc - .rpc_client - .send_raw_transaction(tx_buffer.into()) - .compat() - .await - ); - Ok(spend_tx.into()) + let tx_fut = z_p2sh_spend(&selfi, tx, time_lock, SEQUENCE_FINAL, redeem_script, script_data); + let tx = try_s!(tx_fut.await); + Ok(tx.into()) }; Box::new(fut.boxed().compat()) } fn send_taker_refunds_payment( &self, - _taker_payment_tx: &[u8], - _time_lock: u32, - _maker_pub: &[u8], - _secret_hash: &[u8], + taker_payment_tx: &[u8], + time_lock: u32, + maker_pub: &[u8], + secret_hash: &[u8], _swap_contract_address: &Option, ) -> TransactionFut { - todo!() + let tx: UtxoTx = try_fus!(deserialize(taker_payment_tx).map_err(|e| ERRL!("{:?}", e))); + let redeem_script = payment_script( + time_lock, + secret_hash, + self.utxo_arc.key_pair.public(), + &Public::from_slice(maker_pub).unwrap(), + ); + let script_data = ScriptBuilder::default().push_opcode(Opcode::OP_1).into_script(); + let selfi = self.clone(); + let fut = async move { + let tx_fut = z_p2sh_spend(&selfi, tx, time_lock, SEQUENCE_FINAL - 1, redeem_script, script_data); + let tx = try_s!(tx_fut.await); + Ok(tx.into()) + }; + Box::new(fut.boxed().compat()) } fn send_maker_refunds_payment( @@ -277,59 +337,19 @@ impl SwapOps for ZCoin { secret_hash: &[u8], _swap_contract_address: &Option, ) -> TransactionFut { - let tx: UtxoTx = deserialize(maker_payment_tx).unwrap(); + let tx: UtxoTx = try_fus!(deserialize(maker_payment_tx).map_err(|e| ERRL!("{:?}", e))); let redeem_script = payment_script( time_lock, secret_hash, self.utxo_arc.key_pair.public(), &Public::from_slice(taker_pub).unwrap(), - ) - .to_vec(); + ); + let script_data = ScriptBuilder::default().push_opcode(Opcode::OP_1).into_script(); let selfi = self.clone(); let fut = async move { - let current_block = try_s!(selfi.utxo_arc.rpc_client.get_block_count().compat().await) as u32; - let mut tx_builder = ZTxBuilder::new(consensus::MAIN_NETWORK, current_block.into()); - tx_builder.set_lock_time(time_lock); - - let secp_secret = SecretKey::from_slice(&*selfi.utxo_arc.key_pair.private().secret).unwrap(); - - let outpoint = ZCashOutpoint::new(tx.hash().into(), 0); - let tx_out = TxOut { - value: Amount::from_u64(tx.outputs[0].value).unwrap(), - script_pubkey: ZCashScript(redeem_script.clone()), - }; - let script_data = ScriptBuilder::default().push_opcode(Opcode::OP_1).into_bytes(); - tx_builder - .add_transparent_input( - secp_secret, - outpoint, - SEQUENCE_FINAL - 1, - ZCashScript(script_data.take()), - tx_out, - ) - .unwrap(); - tx_builder - .add_sapling_output( - None, - selfi.z_addr, - Amount::from_u64(tx.outputs[0].value - 1000).unwrap(), - None, - ) - .unwrap(); - let prover = LocalTxProver::bundled(); - let (zcash_tx, _) = tx_builder.build(consensus::BranchId::Sapling, &prover).unwrap(); - let mut tx_buffer = Vec::with_capacity(1024); - zcash_tx.write(&mut tx_buffer).unwrap(); - let refund_tx: UtxoTx = deserialize(tx_buffer.as_slice()).expect("librustzcash should produce a valid tx"); - try_s!( - selfi - .utxo_arc - .rpc_client - .send_raw_transaction(tx_buffer.into()) - .compat() - .await - ); - Ok(refund_tx.into()) + let tx_fut = z_p2sh_spend(&selfi, tx, time_lock, SEQUENCE_FINAL - 1, redeem_script, script_data); + let tx = try_s!(tx_fut.await); + Ok(tx.into()) }; Box::new(fut.boxed().compat()) } @@ -342,69 +362,86 @@ impl SwapOps for ZCoin { _amount: &BigDecimal, _min_block_number: u64, ) -> Box + Send> { - todo!() + // TODO avoid dummy implementation + Box::new(futures01::future::ok(())) } fn validate_maker_payment( &self, - _payment_tx: &[u8], - _time_lock: u32, - _maker_pub: &[u8], - _priv_bn_hash: &[u8], - _amount: BigDecimal, + payment_tx: &[u8], + time_lock: u32, + maker_pub: &[u8], + priv_bn_hash: &[u8], + amount: BigDecimal, _swap_contract_address: &Option, ) -> Box + Send> { - todo!() + utxo_common::validate_maker_payment(self, payment_tx, time_lock, maker_pub, priv_bn_hash, amount) } fn validate_taker_payment( &self, - _payment_tx: &[u8], - _time_lock: u32, - _taker_pub: &[u8], - _priv_bn_hash: &[u8], - _amount: BigDecimal, + payment_tx: &[u8], + time_lock: u32, + taker_pub: &[u8], + priv_bn_hash: &[u8], + amount: BigDecimal, _swap_contract_address: &Option, ) -> Box + Send> { - todo!() + utxo_common::validate_taker_payment(self, payment_tx, time_lock, taker_pub, priv_bn_hash, amount) } fn check_if_my_payment_sent( &self, - _time_lock: u32, - _other_pub: &[u8], - _secret_hash: &[u8], + time_lock: u32, + other_pub: &[u8], + secret_hash: &[u8], _search_from_block: u64, _swap_contract_address: &Option, ) -> Box, Error = String> + Send> { - todo!() + utxo_common::check_if_my_payment_sent(self.clone(), time_lock, other_pub, secret_hash) } fn search_for_swap_tx_spend_my( &self, - _time_lock: u32, - _other_pub: &[u8], - _secret_hash: &[u8], - _tx: &[u8], - _search_from_block: u64, + time_lock: u32, + other_pub: &[u8], + secret_hash: &[u8], + tx: &[u8], + search_from_block: u64, _swap_contract_address: &Option, ) -> Result, String> { - todo!() + utxo_common::search_for_swap_tx_spend_my( + self.as_ref(), + time_lock, + other_pub, + secret_hash, + tx, + search_from_block, + ) } fn search_for_swap_tx_spend_other( &self, - _time_lock: u32, - _other_pub: &[u8], - _secret_hash: &[u8], - _tx: &[u8], - _search_from_block: u64, + time_lock: u32, + other_pub: &[u8], + secret_hash: &[u8], + tx: &[u8], + search_from_block: u64, _swap_contract_address: &Option, ) -> Result, String> { - todo!() + utxo_common::search_for_swap_tx_spend_other( + self.as_ref(), + time_lock, + other_pub, + secret_hash, + tx, + search_from_block, + ) } - fn extract_secret(&self, _secret_hash: &[u8], _spend_tx: &[u8]) -> Result, String> { todo!() } + fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { + utxo_common::extract_secret(secret_hash, spend_tx) + } fn negotiate_swap_contract_addr( &self, @@ -414,6 +451,172 @@ impl SwapOps for ZCoin { } } +impl MmCoin for ZCoin { + fn is_asset_chain(&self) -> bool { self.utxo_arc.conf.asset_chain } + + fn withdraw(&self, _req: WithdrawRequest) -> WithdrawFut { todo!() } + + fn decimals(&self) -> u8 { self.utxo_arc.decimals } + + fn convert_to_address(&self, _from: &str, _to_address_format: Json) -> Result { todo!() } + + fn validate_address(&self, _address: &str) -> ValidateAddressResult { todo!() } + + fn process_history_loop(&self, _ctx: MmArc) { todo!() } + + fn history_sync_status(&self) -> HistorySyncState { todo!() } + + fn get_trade_fee(&self) -> Box + Send> { + utxo_common::get_trade_fee(self.clone()) + } + + fn get_sender_trade_fee(&self, value: TradePreimageValue, stage: FeeApproxStage) -> TradePreimageFut { + utxo_common::get_sender_trade_fee(self.clone(), value, stage) + } + + fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { + utxo_common::get_receiver_trade_fee(self.clone()) + } + + fn get_fee_to_send_taker_fee( + &self, + dex_fee_amount: BigDecimal, + stage: FeeApproxStage, + ) -> TradePreimageFut { + utxo_common::get_fee_to_send_taker_fee(self.clone(), dex_fee_amount, stage) + } + + fn required_confirmations(&self) -> u64 { utxo_common::required_confirmations(&self.utxo_arc) } + + fn requires_notarization(&self) -> bool { utxo_common::requires_notarization(&self.utxo_arc) } + + fn set_required_confirmations(&self, confirmations: u64) { + utxo_common::set_required_confirmations(&self.utxo_arc, confirmations) + } + + fn set_requires_notarization(&self, requires_nota: bool) { + utxo_common::set_requires_notarization(&self.utxo_arc, requires_nota) + } + + fn swap_contract_address(&self) -> Option { utxo_common::swap_contract_address() } + + fn mature_confirmations(&self) -> Option { Some(self.utxo_arc.conf.mature_confirmations) } +} + +#[async_trait] +impl UtxoCommonOps for ZCoin { + async fn get_tx_fee(&self) -> Result { utxo_common::get_tx_fee(&self.utxo_arc).await } + + async fn get_htlc_spend_fee(&self) -> UtxoRpcResult { utxo_common::get_htlc_spend_fee(self).await } + + fn addresses_from_script(&self, script: &Script) -> Result, String> { + utxo_common::addresses_from_script(&self.utxo_arc.conf, script) + } + + fn denominate_satoshis(&self, satoshi: i64) -> f64 { utxo_common::denominate_satoshis(&self.utxo_arc, satoshi) } + + fn my_public_key(&self) -> &Public { self.utxo_arc.key_pair.public() } + + fn display_address(&self, address: &Address) -> Result { + utxo_common::display_address(&self.utxo_arc.conf, address) + } + + fn address_from_str(&self, address: &str) -> Result { + utxo_common::address_from_str(&self.utxo_arc.conf, address) + } + + async fn get_current_mtp(&self) -> UtxoRpcResult { utxo_common::get_current_mtp(&self.utxo_arc).await } + + fn is_unspent_mature(&self, output: &RpcTransaction) -> bool { + utxo_common::is_unspent_mature(self.utxo_arc.conf.mature_confirmations, output) + } + + async fn generate_transaction( + &self, + utxos: Vec, + outputs: Vec, + fee_policy: FeePolicy, + fee: Option, + gas_fee: Option, + ) -> GenerateTxResult { + utxo_common::generate_transaction(self, utxos, outputs, fee_policy, fee, gas_fee).await + } + + async fn calc_interest_if_required( + &self, + unsigned: TransactionInputSigner, + data: AdditionalTxData, + my_script_pub: Bytes, + ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { + utxo_common::calc_interest_if_required(self, unsigned, data, my_script_pub).await + } + + fn p2sh_spending_tx( + &self, + prev_transaction: UtxoTx, + redeem_script: Bytes, + outputs: Vec, + script_data: Script, + sequence: u32, + lock_time: u32, + ) -> Result { + utxo_common::p2sh_spending_tx( + self, + prev_transaction, + redeem_script, + outputs, + script_data, + sequence, + lock_time, + ) + } + + async fn ordered_mature_unspents<'a>( + &'a self, + address: &Address, + ) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)> { + utxo_common::ordered_mature_unspents(self, address).await + } + + fn get_verbose_transaction_from_cache_or_rpc( + &self, + txid: H256Json, + ) -> Box + Send> { + let selfi = self.clone(); + let fut = async move { utxo_common::get_verbose_transaction_from_cache_or_rpc(&selfi.utxo_arc, txid).await }; + Box::new(fut.boxed().compat()) + } + + async fn cache_transaction_if_possible(&self, tx: &RpcTransaction) -> Result<(), String> { + utxo_common::cache_transaction_if_possible(&self.utxo_arc, tx).await + } + + async fn list_unspent_ordered<'a>( + &'a self, + address: &Address, + ) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)> { + utxo_common::list_unspent_ordered(self, address).await + } + + async fn preimage_trade_fee_required_to_send_outputs( + &self, + outputs: Vec, + fee_policy: FeePolicy, + gas_fee: Option, + stage: &FeeApproxStage, + ) -> TradePreimageResult { + utxo_common::preimage_trade_fee_required_to_send_outputs(self, outputs, fee_policy, gas_fee, stage).await + } + + fn increase_dynamic_fee_by_stage(&self, dynamic_fee: u64, stage: &FeeApproxStage) -> u64 { + utxo_common::increase_dynamic_fee_by_stage(self, dynamic_fee, stage) + } + + fn p2sh_tx_locktime(&self, htlc_locktime: u32) -> u32 { + utxo_common::p2sh_tx_locktime(&self.utxo_arc.conf.ticker, htlc_locktime) + } +} + #[test] fn derive_z_key_from_mm_seed() { use common::privkey::key_pair_from_seed; @@ -431,4 +634,17 @@ fn derive_z_key_from_mm_seed() { encoded_addr, "zs182ht30wnnnr8jjhj2j9v5dkx3qsknnr5r00jfwk2nczdtqy7w0v836kyy840kv2r8xle5gcl549" ); + + let seed = "also shoot benefit prefer juice shell elder veteran woman mimic image kidney"; + let secp_keypair = key_pair_from_seed(seed).unwrap(); + let z_spending_key = ExtendedSpendingKey::master(&*secp_keypair.private().secret); + let encoded = encode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, &z_spending_key); + assert_eq!(encoded, "secret-extended-key-main1qqqqqqqqqqqqqq8jnhc9stsqwts6pu5ayzgy4szplvy03u227e50n3u8e6dwn5l0q5s3s8xfc03r5wmyh5s5dq536ufwn2k89ngdhnxy64sd989elwas6kr7ygztsdkw6k6xqyvhtu6e0dhm4mav8rus0fy8g0hgy9vt97cfjmus0m2m87p4qz5a00um7gwjwk494gul0uvt3gqyjujcclsqry72z57kr265jsajactgfn9m3vclqvx8fsdnwp4jwj57ffw560vvwks9g9hpu"); + + let (_, address) = z_spending_key.default_address().unwrap(); + let encoded_addr = encode_payment_address(z_mainnet_constants::HRP_SAPLING_PAYMENT_ADDRESS, &address); + assert_eq!( + encoded_addr, + "zs1funuwrjr2stlr6fnhkdh7fyz3p7n0p8rxase9jnezdhc286v5mhs6q3myw0phzvad5mvqgfxpam" + ); } diff --git a/mm2src/coins/z_coin/z_coin_tests.rs b/mm2src/coins/z_coin/z_coin_tests.rs index fc785e17c6..0c2abbce36 100644 --- a/mm2src/coins/z_coin/z_coin_tests.rs +++ b/mm2src/coins/z_coin/z_coin_tests.rs @@ -1,11 +1,11 @@ use super::*; +use crate::z_coin::z_htlc::z_send_dex_fee; use common::block_on; use common::mm_ctx::MmCtxBuilder; use common::now_ms; use zcash_client_backend::encoding::decode_extended_spending_key; #[test] -#[ignore] fn zombie_coin_send_and_refund_maker_payment() { let conf = json!({ "coin": "ZOMBIE", @@ -24,12 +24,12 @@ fn zombie_coin_send_and_refund_maker_payment() { let priv_key = [1; 32]; let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); - let coin = block_on(z_coin_from_conf_and_request( + let coin = block_on(z_coin_from_conf_and_request_with_z_key( &ctx, "ZOMBIE", &conf, &req, &priv_key, z_key, )) .unwrap(); - let lock_time = (now_ms() / 1000) as u32 - 1000; + let lock_time = (now_ms() / 1000) as u32 - 3600; let taker_pub = coin.utxo_arc.key_pair.public(); let secret_hash = [0; 20]; let tx = coin @@ -46,7 +46,6 @@ fn zombie_coin_send_and_refund_maker_payment() { } #[test] -#[ignore] fn zombie_coin_send_and_spend_maker_payment() { let conf = json!({ "coin": "ZOMBIE", @@ -65,7 +64,7 @@ fn zombie_coin_send_and_spend_maker_payment() { let priv_key = [1; 32]; let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); - let coin = block_on(z_coin_from_conf_and_request( + let coin = block_on(z_coin_from_conf_and_request_with_z_key( &ctx, "ZOMBIE", &conf, &req, &priv_key, z_key, )) .unwrap(); @@ -87,3 +86,87 @@ fn zombie_coin_send_and_spend_maker_payment() { .unwrap(); println!("spend tx {}", hex::encode(&spend_tx.tx_hash().0)); } + +#[test] +fn zombie_coin_send_and_refund_dex_fee() { + let conf = json!({ + "coin": "ZOMBIE", + "asset": "ZOMBIE", + "fname": "ZOMBIE (TESTCOIN)", + "txversion": 4, + "overwintered": 1, + "mm2": 1, + }); + let req = json!({ + "method": "enable", + "coin": "ZOMBIE" + }); + + let ctx = MmCtxBuilder::default().into_mm_arc(); + let priv_key = [1; 32]; + let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); + + let coin = block_on(z_coin_from_conf_and_request_with_z_key( + &ctx, "ZOMBIE", &conf, &req, &priv_key, z_key, + )) + .unwrap(); + + let lock_time = (now_ms() / 1000) as u32 - 1000; + let watcher_pub = coin.utxo_arc.key_pair.public(); + let (tx, redeem_script) = block_on(z_send_dex_fee(&coin, lock_time, watcher_pub, "0.01".parse().unwrap())).unwrap(); + println!("dex fee tx {}", hex::encode(&*tx.hash().reversed())); + + let script_data = ScriptBuilder::default().push_opcode(Opcode::OP_1).into_script(); + let refund = block_on(z_p2sh_spend( + &coin, + tx, + lock_time, + SEQUENCE_FINAL - 1, + redeem_script, + script_data, + )) + .unwrap(); + println!("dex fee refund tx {}", hex::encode(&*refund.hash().reversed())); +} + +#[test] +fn zombie_coin_send_and_spend_dex_fee() { + let conf = json!({ + "coin": "ZOMBIE", + "asset": "ZOMBIE", + "fname": "ZOMBIE (TESTCOIN)", + "txversion": 4, + "overwintered": 1, + "mm2": 1, + }); + let req = json!({ + "method": "enable", + "coin": "ZOMBIE" + }); + + let ctx = MmCtxBuilder::default().into_mm_arc(); + let priv_key = [1; 32]; + let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); + + let coin = block_on(z_coin_from_conf_and_request_with_z_key( + &ctx, "ZOMBIE", &conf, &req, &priv_key, z_key, + )) + .unwrap(); + + let lock_time = (now_ms() / 1000) as u32 - 1000; + let watcher_pub = coin.utxo_arc.key_pair.public(); + let (tx, redeem_script) = block_on(z_send_dex_fee(&coin, lock_time, watcher_pub, "0.01".parse().unwrap())).unwrap(); + println!("dex fee tx {}", hex::encode(&*tx.hash().reversed())); + + let script_data = ScriptBuilder::default().push_opcode(Opcode::OP_0).into_script(); + let spend = block_on(z_p2sh_spend( + &coin, + tx, + lock_time, + SEQUENCE_FINAL, + redeem_script, + script_data, + )) + .unwrap(); + println!("dex fee spend tx {}", hex::encode(&*spend.hash().reversed())); +} diff --git a/mm2src/coins/z_coin/z_htlc.rs b/mm2src/coins/z_coin/z_htlc.rs index 09cc7c8150..0cc344910b 100644 --- a/mm2src/coins/z_coin/z_htlc.rs +++ b/mm2src/coins/z_coin/z_htlc.rs @@ -1,46 +1,55 @@ +// historical milestone, first performed RICK/ZOMBIE swap +// dex fee - https://zombie.explorer.lordofthechains.com/tx/40bec29f268c349722a3228743e5c5b461cf16d124cddcfd2fc624fe895a0bdd +// maker payment - https://rick.explorer.dexstats.info/tx/9d36e95e5147450399895f0f248ac2e2de13382401c2986e134cc3d62bda738e +// taker payment - https://zombie.explorer.lordofthechains.com/tx/b248992e064fab579774c0479b04043091cf62f3975cb1664ea7d4f857ebe6f8 +// taker payment spend - https://zombie.explorer.lordofthechains.com/tx/af6bb0f99f9a5a070a0c1f53d69e4189b0e9b68f9d66e69f201a6b6d9f93897e +// maker payment spend - https://rick.explorer.dexstats.info/tx/6a2dcc866ad75cebecb780a02320073a88bcf5e57ddccbe2657494e7747d591e + use super::z_rpc::{ZOperationStatus, ZOperationTxid, ZSendManyItem}; use super::ZCoin; -use crate::utxo::rpc_clients::UtxoRpcError; -use crate::utxo::utxo_common::payment_script; +use crate::utxo::rpc_clients::{UtxoRpcClientEnum, UtxoRpcError}; +use crate::utxo::sat_from_big_decimal; +use crate::utxo::utxo_common::{big_decimal_from_sat_unsigned, dex_fee_script, payment_script}; use bigdecimal::BigDecimal; use bitcrypto::dhash160; use chain::Transaction as UtxoTx; use common::executor::Timer; use common::mm_error::prelude::*; +use common::now_ms; use derive_more::Display; use futures::compat::Future01CompatExt; -use keys::{Address, Error as KeysError, Public}; +use keys::{Address, Public}; +use script::Script; +use secp256k1_bindings::SecretKey; use serialization::deserialize; -use zcash_client_backend::encoding::encode_payment_address; -use zcash_primitives::constants::mainnet as z_mainnet_constants; +use zcash_primitives::consensus; +use zcash_primitives::legacy::Script as ZCashScript; +use zcash_primitives::transaction::builder::{Builder as ZTxBuilder, Error as ZTxBuilderError}; +use zcash_primitives::transaction::components::{Amount, OutPoint as ZCashOutpoint, TxOut}; #[derive(Debug, Display)] #[allow(clippy::large_enum_variant)] pub enum ZSendHtlcError { - ParseOtherPubFailed(KeysError), #[display(fmt = "z operation failed with statuses {:?}", _0)] ZOperationFailed(Vec>), ZOperationStatusesEmpty, RpcError(UtxoRpcError), } -impl From for ZSendHtlcError { - fn from(keys: KeysError) -> ZSendHtlcError { ZSendHtlcError::ParseOtherPubFailed(keys) } -} - impl From for ZSendHtlcError { fn from(rpc: UtxoRpcError) -> ZSendHtlcError { ZSendHtlcError::RpcError(rpc) } } +/// Sends HTLC output from the coin's z_addr pub async fn z_send_htlc( coin: &ZCoin, time_lock: u32, - other_pub: &[u8], + other_pub: &Public, secret_hash: &[u8], amount: BigDecimal, ) -> Result> { - let taker_pub = Public::from_slice(other_pub).map_to_mm(ZSendHtlcError::from)?; - let payment_script = payment_script(time_lock, secret_hash, coin.utxo_arc.key_pair.public(), &taker_pub); + let _lock = coin.z_fields.z_tx_mutex.lock().await; + let payment_script = payment_script(time_lock, secret_hash, coin.utxo_arc.key_pair.public(), &other_pub); let hash = dhash160(&payment_script); let htlc_address = Address { prefix: coin.utxo_arc.conf.p2sh_addr_prefix, @@ -49,20 +58,30 @@ pub async fn z_send_htlc( checksum_type: coin.utxo_arc.conf.checksum_type, }; - let from_addr = encode_payment_address(z_mainnet_constants::HRP_SAPLING_PAYMENT_ADDRESS, &coin.z_addr); + let amount_sat = sat_from_big_decimal(&amount, coin.utxo_arc.decimals).expect("temporary code"); + let amount = big_decimal_from_sat_unsigned(amount_sat, coin.utxo_arc.decimals); + let address = htlc_address.to_string(); + if let UtxoRpcClientEnum::Native(native) = coin.rpc_client() { + native.import_address(&address, &address, false).compat().await.unwrap(); + } + let send_item = ZSendManyItem { amount, op_return: Some(payment_script.to_vec().into()), address: htlc_address.to_string(), }; - let op_id = coin.z_rpc().z_send_many(&from_addr, vec![send_item]).compat().await?; + let op_id = coin + .z_rpc() + .z_send_many(&coin.z_fields.z_addr_encoded, vec![send_item]) + .compat() + .await?; loop { let operation_statuses = coin.z_rpc().z_get_send_many_status(&[&op_id]).compat().await?; match operation_statuses.first() { - Some(ZOperationStatus::Executing { .. }) => { + Some(ZOperationStatus::Executing { .. }) | Some(ZOperationStatus::Queued { .. }) => { Timer::sleep(1.).await; continue; }, @@ -82,3 +101,141 @@ pub async fn z_send_htlc( } } } + +/// Sends HTLC output from the coin's z_addr +pub async fn z_send_dex_fee( + coin: &ZCoin, + time_lock: u32, + watcher_pub: &Public, + amount: BigDecimal, +) -> Result<(UtxoTx, Script), MmError> { + let _lock = coin.z_fields.z_tx_mutex.lock().await; + let payment_script = dex_fee_script([0; 16], time_lock, watcher_pub, coin.utxo_arc.key_pair.public()); + let hash = dhash160(&payment_script); + let htlc_address = Address { + prefix: coin.utxo_arc.conf.p2sh_addr_prefix, + t_addr_prefix: coin.utxo_arc.conf.p2sh_t_addr_prefix, + hash, + checksum_type: coin.utxo_arc.conf.checksum_type, + }; + + let amount_sat = sat_from_big_decimal(&amount, coin.utxo_arc.decimals).expect("temporary code"); + let amount = big_decimal_from_sat_unsigned(amount_sat, coin.utxo_arc.decimals); + + let address = htlc_address.to_string(); + if let UtxoRpcClientEnum::Native(native) = coin.rpc_client() { + native.import_address(&address, &address, false).compat().await.unwrap(); + } + + let send_item = ZSendManyItem { + amount, + op_return: Some(payment_script.to_vec().into()), + address, + }; + + let op_id = coin + .z_rpc() + .z_send_many(&coin.z_fields.z_addr_encoded, vec![send_item]) + .compat() + .await?; + + loop { + let operation_statuses = coin.z_rpc().z_get_send_many_status(&[&op_id]).compat().await?; + + match operation_statuses.first() { + Some(ZOperationStatus::Executing { .. }) | Some(ZOperationStatus::Queued { .. }) => { + Timer::sleep(1.).await; + continue; + }, + Some(ZOperationStatus::Failed { .. }) => { + break Err(MmError::new(ZSendHtlcError::ZOperationFailed(operation_statuses))); + }, + Some(ZOperationStatus::Success { result, .. }) => { + let tx_bytes = coin + .rpc_client() + .get_transaction_bytes(result.txid.clone()) + .compat() + .await?; + let tx: UtxoTx = deserialize(tx_bytes.0.as_slice()).expect("rpc returns valid tx bytes"); + + coin.rpc_client() + .wait_for_confirmations(&tx, 1, false, now_ms() / 1000 + 120, 1) + .compat() + .await + .unwrap(); + break Ok((tx, payment_script)); + }, + None => break Err(MmError::new(ZSendHtlcError::ZOperationStatusesEmpty)), + } + } +} + +#[derive(Debug, Display)] +#[allow(clippy::large_enum_variant)] +pub enum ZP2SHSpendError { + ZTxBuilderError(ZTxBuilderError), + Rpc(UtxoRpcError), +} + +impl From for ZP2SHSpendError { + fn from(tx_builder: ZTxBuilderError) -> ZP2SHSpendError { ZP2SHSpendError::ZTxBuilderError(tx_builder) } +} + +impl From for ZP2SHSpendError { + fn from(rpc: UtxoRpcError) -> ZP2SHSpendError { ZP2SHSpendError::Rpc(rpc) } +} + +/// Spends P2SH output 0 to the coin's z_addr +pub async fn z_p2sh_spend( + coin: &ZCoin, + p2sh_tx: UtxoTx, + tx_locktime: u32, + input_sequence: u32, + redeem_script: Script, + script_data: Script, +) -> Result> { + let current_block = coin.utxo_arc.rpc_client.get_block_count().compat().await? as u32; + let mut tx_builder = ZTxBuilder::new(consensus::MAIN_NETWORK, current_block.into()); + tx_builder.set_lock_time(tx_locktime); + + let secp_secret = + SecretKey::from_slice(&*coin.utxo_arc.key_pair.private().secret).expect("Keypair contains a valid secret key"); + + let outpoint = ZCashOutpoint::new(p2sh_tx.hash().into(), 0); + let tx_out = TxOut { + value: Amount::from_u64(p2sh_tx.outputs[0].value).expect("p2sh_tx transaction always contains valid amount"), + script_pubkey: ZCashScript(redeem_script.to_vec()), + }; + tx_builder + .add_transparent_input( + secp_secret, + outpoint, + input_sequence, + ZCashScript(script_data.to_vec()), + tx_out, + ) + .map_to_mm(ZP2SHSpendError::from)?; + tx_builder + .add_sapling_output( + None, + coin.z_fields.z_addr.clone(), + Amount::from_u64(p2sh_tx.outputs[0].value - 1000).unwrap(), + None, + ) + .map_to_mm(ZP2SHSpendError::from)?; + + let (zcash_tx, _) = tx_builder + .build(consensus::BranchId::Sapling, &coin.z_fields.z_tx_prover) + .map_to_mm(ZP2SHSpendError::from)?; + + let mut tx_buffer = Vec::with_capacity(1024); + zcash_tx.write(&mut tx_buffer).unwrap(); + let refund_tx: UtxoTx = deserialize(tx_buffer.as_slice()).expect("librustzcash should produce a valid tx"); + + coin.rpc_client() + .send_raw_transaction(tx_buffer.into()) + .compat() + .await?; + + Ok(refund_tx) +} diff --git a/mm2src/coins/z_coin/z_rpc.rs b/mm2src/coins/z_coin/z_rpc.rs index 6b1cfd3572..88130cd3d4 100644 --- a/mm2src/coins/z_coin/z_rpc.rs +++ b/mm2src/coins/z_coin/z_rpc.rs @@ -29,11 +29,9 @@ pub struct ZOperationHex { #[serde(tag = "status")] #[serde(rename_all = "lowercase")] pub enum ZOperationStatus { - Success { + Queued { id: String, creation_time: u64, - result: T, - execution_secs: f64, method: String, params: Json, }, @@ -43,6 +41,14 @@ pub enum ZOperationStatus { method: String, params: Json, }, + Success { + id: String, + creation_time: u64, + result: T, + execution_secs: f64, + method: String, + params: Json, + }, Failed { id: String, creation_time: u64, diff --git a/mm2src/mm2_tests.rs b/mm2src/mm2_tests.rs index fe1c4c13e3..88a858f295 100644 --- a/mm2src/mm2_tests.rs +++ b/mm2src/mm2_tests.rs @@ -67,6 +67,8 @@ async fn enable_coins_eth_electrum( ); replies.insert("ETH", enable_native(mm, "ETH", eth_urls).await); replies.insert("JST", enable_native(mm, "JST", eth_urls).await); + #[cfg(feature = "zhtlc")] + replies.insert("ZOMBIE", enable_native(mm, "ZOMBIE", eth_urls).await); replies } @@ -973,7 +975,7 @@ fn test_rpc_password_from_json_no_userpass() { /// Trading test using coins with remote RPC (Electrum, ETH nodes), it needs only ENV variables to be set, coins daemons are not required. /// Trades few pairs concurrently to speed up the process and also act like "load" test -async fn trade_base_rel_electrum(pairs: Vec<(&'static str, &'static str)>) { +async fn trade_base_rel_electrum(pairs: &[(&'static str, &'static str)]) { let bob_passphrase = get_passphrase(&".env.seed", "BOB_PASSPHRASE").unwrap(); let alice_passphrase = get_passphrase(&".env.client", "ALICE_PASSPHRASE").unwrap(); @@ -981,6 +983,7 @@ async fn trade_base_rel_electrum(pairs: Vec<(&'static str, &'static str)>) { {"coin":"RICK","asset":"RICK","required_confirmations":0,"txversion":4,"overwintered":1,"protocol":{"type":"UTXO"}}, {"coin":"MORTY","asset":"MORTY","required_confirmations":0,"txversion":4,"overwintered":1,"protocol":{"type":"UTXO"}}, {"coin":"ETH","name":"ethereum","protocol":{"type":"ETH"}}, + {"coin":"ZOMBIE","asset":"ZOMBIE","fname":"ZOMBIE (TESTCOIN)","txversion":4,"overwintered":1,"mm2":1,"protocol":{"type":"ZHTLC"}}, {"coin":"JST","name":"jst","protocol":{"type":"ERC20","protocol_data":{"platform":"ETH","contract_address":"0x2b294F029Fde858b2c62184e8390591755521d8E"}}} ]); @@ -1139,12 +1142,12 @@ async fn trade_base_rel_electrum(pairs: Vec<(&'static str, &'static str)>) { for uuid in uuids.iter() { mm_bob - .wait_for_log(600., |log| log.contains(&format!("[swap uuid={}] Finished", uuid))) + .wait_for_log(300., |log| log.contains(&format!("[swap uuid={}] Finished", uuid))) .await .unwrap(); mm_alice - .wait_for_log(600., |log| log.contains(&format!("[swap uuid={}] Finished", uuid))) + .wait_for_log(300., |log| log.contains(&format!("[swap uuid={}] Finished", uuid))) .await .unwrap(); @@ -1217,7 +1220,14 @@ async fn trade_base_rel_electrum(pairs: Vec<(&'static str, &'static str)>) { #[test] #[cfg(not(target_arch = "wasm32"))] -fn trade_test_electrum_and_eth_coins() { block_on(trade_base_rel_electrum(vec![("ETH", "JST")])); } +fn trade_test_electrum_and_eth_coins() { + let pairs: &[_] = if cfg!(feature = "zhtlc") { + &[("ETH", "JST"), ("RICK", "ZOMBIE")] + } else { + &[("ETH", "JST")] + }; + block_on(trade_base_rel_electrum(pairs)); +} #[cfg(target_arch = "wasm32")] #[no_mangle] @@ -1225,8 +1235,8 @@ pub extern "C" fn trade_test_electrum_and_eth_coins(cb_id: i32) { use std::ptr::null; common::executor::spawn(async move { - let pairs = vec![("ETH", "JST")]; - trade_base_rel_electrum(pairs).await; + let pairs = [("ETH", "JST")]; + trade_base_rel_electrum(&pairs).await; call_back(cb_id, null(), 0) }) }