diff --git a/Cargo.lock b/Cargo.lock index eb77406386..b14a301008 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -393,7 +393,7 @@ dependencies = [ [[package]] name = "bitcrypto" version = "0.1.0" -source = "git+https://github.com/artemii235/parity-bitcoin.git#1296f50c1ad5a51e6a44c8fdf1526ec6e244607f" +source = "git+https://github.com/artemii235/parity-bitcoin.git#0c095d051c8b1aa3d2e7cb07d491f7546311b110" dependencies = [ "groestl", "primitives", @@ -441,18 +441,6 @@ dependencies = [ "constant_time_eq", ] -[[package]] -name = "blake2b_simd" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce2571a6cd634670daa2977cc894c1cc2ba57c563c498e5a82c35446f34d056e" -dependencies = [ - "arrayref", - "arrayvec 0.4.12", - "byteorder 1.3.4", - "constant_time_eq", -] - [[package]] name = "blake2b_simd" version = "0.5.10" @@ -681,7 +669,7 @@ dependencies = [ [[package]] name = "chain" version = "0.1.0" -source = "git+https://github.com/artemii235/parity-bitcoin.git#1296f50c1ad5a51e6a44c8fdf1526ec6e244607f" +source = "git+https://github.com/artemii235/parity-bitcoin.git#0c095d051c8b1aa3d2e7cb07d491f7546311b110" dependencies = [ "bitcrypto", "primitives", @@ -2348,7 +2336,7 @@ dependencies = [ [[package]] name = "keys" version = "0.1.0" -source = "git+https://github.com/artemii235/parity-bitcoin.git#1296f50c1ad5a51e6a44c8fdf1526ec6e244607f" +source = "git+https://github.com/artemii235/parity-bitcoin.git#0c095d051c8b1aa3d2e7cb07d491f7546311b110" dependencies = [ "base58", "bitcrypto", @@ -3048,6 +3036,7 @@ dependencies = [ "dirs 1.0.5", "either", "enum-primitive-derive", + "ethereum-types 0.4.2", "fomat-macros 0.2.1", "futures 0.1.29", "futures 0.3.5", @@ -3156,7 +3145,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f75db05d738947aa5389863aadafbcf2e509d7ba099dc2ddcdf4fc66bf7a9e03" dependencies = [ - "blake2b_simd 0.5.10", + "blake2b_simd", "blake2s_simd", "digest 0.8.1", "sha-1", @@ -3641,7 +3630,7 @@ dependencies = [ [[package]] name = "primitives" version = "0.1.0" -source = "git+https://github.com/artemii235/parity-bitcoin.git#1296f50c1ad5a51e6a44c8fdf1526ec6e244607f" +source = "git+https://github.com/artemii235/parity-bitcoin.git#0c095d051c8b1aa3d2e7cb07d491f7546311b110" dependencies = [ "bigint", "byteorder 1.3.4", @@ -4170,7 +4159,7 @@ dependencies = [ [[package]] name = "rpc" version = "0.1.0" -source = "git+https://github.com/artemii235/parity-bitcoin.git#1296f50c1ad5a51e6a44c8fdf1526ec6e244607f" +source = "git+https://github.com/artemii235/parity-bitcoin.git#0c095d051c8b1aa3d2e7cb07d491f7546311b110" dependencies = [ "chain", "keys", @@ -4191,7 +4180,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bc8af4bda8e1ff4932523b94d3dd20ee30a87232323eda55903ffd71d2fb017" dependencies = [ "base64 0.11.0", - "blake2b_simd 0.5.10", + "blake2b_simd", "constant_time_eq", "crossbeam-utils 0.7.2", ] @@ -4351,10 +4340,10 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "script" version = "0.1.0" -source = "git+https://github.com/artemii235/parity-bitcoin.git#1296f50c1ad5a51e6a44c8fdf1526ec6e244607f" +source = "git+https://github.com/artemii235/parity-bitcoin.git#0c095d051c8b1aa3d2e7cb07d491f7546311b110" dependencies = [ "bitcrypto", - "blake2b_simd 0.4.1", + "blake2b_simd", "chain", "keys", "log 0.4.11", @@ -4556,7 +4545,7 @@ dependencies = [ [[package]] name = "serialization" version = "0.1.0" -source = "git+https://github.com/artemii235/parity-bitcoin.git#1296f50c1ad5a51e6a44c8fdf1526ec6e244607f" +source = "git+https://github.com/artemii235/parity-bitcoin.git#0c095d051c8b1aa3d2e7cb07d491f7546311b110" dependencies = [ "byteorder 1.3.4", "primitives", @@ -4565,7 +4554,7 @@ dependencies = [ [[package]] name = "serialization_derive" version = "0.1.0" -source = "git+https://github.com/artemii235/parity-bitcoin.git#1296f50c1ad5a51e6a44c8fdf1526ec6e244607f" +source = "git+https://github.com/artemii235/parity-bitcoin.git#0c095d051c8b1aa3d2e7cb07d491f7546311b110" dependencies = [ "quote 0.3.15", "syn 0.11.11", diff --git a/Cargo.toml b/Cargo.toml index 8479efdd8c..183bf44e3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,7 @@ crc32fast = { version = "1.2", features = ["std", "nightly"] } crossbeam = "0.7" dirs = { version = "1", optional = true } either = "1.6" +ethereum-types = { version = "0.4", default-features = false, features = ["std", "serialize"] } enum-primitive-derive = "0.1" fomat-macros = "0.2" futures01 = { version = "0.1", package = "futures" } diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 45b0abf2eb..f0c1e2b898 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -274,32 +274,44 @@ impl EthCoinImpl { } } - /// Gets `ReceiverSpent` events from etomic swap smart contract (`self.swap_contract_address` ) since `from_block` - fn spend_events(&self, from_block: u64) -> Box, Error = String> + Send> { + /// Gets `ReceiverSpent` events from etomic swap smart contract since `from_block` + fn spend_events( + &self, + swap_contract_address: Address, + from_block: u64, + ) -> Box, Error = String> + Send> { let contract_event = try_fus!(SWAP_CONTRACT.event("ReceiverSpent")); let filter = FilterBuilder::default() .topics(Some(vec![contract_event.signature()]), None, None, None) .from_block(BlockNumber::Number(from_block)) - .address(vec![self.swap_contract_address]) + .address(vec![swap_contract_address]) .build(); Box::new(self.web3.eth().logs(filter).map_err(|e| ERRL!("{}", e))) } - /// Gets `SenderRefunded` events from etomic swap smart contract (`self.swap_contract_address` ) since `from_block` - fn refund_events(&self, from_block: u64) -> Box, Error = String>> { + /// Gets `SenderRefunded` events from etomic swap smart contract since `from_block` + fn refund_events( + &self, + swap_contract_address: Address, + from_block: u64, + ) -> Box, Error = String>> { let contract_event = try_fus!(SWAP_CONTRACT.event("SenderRefunded")); - log!([contract_event.signature()]); let filter = FilterBuilder::default() .topics(Some(vec![contract_event.signature()]), None, None, None) .from_block(BlockNumber::Number(from_block)) - .address(vec![self.swap_contract_address]) + .address(vec![swap_contract_address]) .build(); Box::new(self.web3.eth().logs(filter).map_err(|e| ERRL!("{}", e))) } - fn search_for_swap_tx_spend(&self, tx: &[u8], search_from_block: u64) -> Result, String> { + fn search_for_swap_tx_spend( + &self, + tx: &[u8], + swap_contract_address: Address, + search_from_block: u64, + ) -> Result, String> { let unverified: UnverifiedTransaction = try_s!(rlp::decode(tx)); let tx = try_s!(SignedEthTx::new(unverified)); @@ -315,7 +327,7 @@ impl EthCoinImpl { _ => panic!(), }; - let spend_events = try_s!(self.spend_events(search_from_block).wait()); + let spend_events = try_s!(self.spend_events(swap_contract_address, search_from_block).wait()); let found = spend_events.iter().find(|event| &event.data.0[..32] == id.as_slice()); if let Some(event) = found { @@ -334,7 +346,7 @@ impl EthCoinImpl { } } - let refund_events = try_s!(self.refund_events(search_from_block).wait()); + let refund_events = try_s!(self.refund_events(swap_contract_address, search_from_block).wait()); let found = refund_events.iter().find(|event| &event.data.0[..32] == id.as_slice()); if let Some(event) = found { @@ -492,8 +504,10 @@ impl SwapOps for EthCoin { taker_pub: &[u8], secret_hash: &[u8], amount: BigDecimal, + swap_contract_address: &Option, ) -> TransactionFut { let taker_addr = try_fus!(addr_from_raw_pubkey(taker_pub)); + let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); Box::new( self.send_hash_time_locked_payment( @@ -502,6 +516,7 @@ impl SwapOps for EthCoin { time_lock, secret_hash, taker_addr, + swap_contract_address, ) .map(TransactionEnum::from), ) @@ -513,8 +528,10 @@ impl SwapOps for EthCoin { maker_pub: &[u8], secret_hash: &[u8], amount: BigDecimal, + swap_contract_address: &Option, ) -> TransactionFut { let maker_addr = try_fus!(addr_from_raw_pubkey(maker_pub)); + let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); Box::new( self.send_hash_time_locked_payment( @@ -523,6 +540,7 @@ impl SwapOps for EthCoin { time_lock, secret_hash, maker_addr, + swap_contract_address, ) .map(TransactionEnum::from), ) @@ -534,12 +552,14 @@ impl SwapOps for EthCoin { _time_lock: u32, _taker_pub: &[u8], secret: &[u8], + swap_contract_address: &Option, ) -> TransactionFut { let tx: UnverifiedTransaction = try_fus!(rlp::decode(taker_payment_tx)); let signed = try_fus!(SignedEthTx::new(tx)); + let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); Box::new( - self.spend_hash_time_locked_payment(signed, secret) + self.spend_hash_time_locked_payment(signed, swap_contract_address, secret) .map(TransactionEnum::from), ) } @@ -550,11 +570,13 @@ impl SwapOps for EthCoin { _time_lock: u32, _maker_pub: &[u8], secret: &[u8], + swap_contract_address: &Option, ) -> TransactionFut { let tx: UnverifiedTransaction = try_fus!(rlp::decode(maker_payment_tx)); let signed = try_fus!(SignedEthTx::new(tx)); + let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); Box::new( - self.spend_hash_time_locked_payment(signed, secret) + self.spend_hash_time_locked_payment(signed, swap_contract_address, secret) .map(TransactionEnum::from), ) } @@ -565,11 +587,16 @@ impl SwapOps for EthCoin { _time_lock: u32, _maker_pub: &[u8], _secret_hash: &[u8], + swap_contract_address: &Option, ) -> TransactionFut { let tx: UnverifiedTransaction = try_fus!(rlp::decode(taker_payment_tx)); let signed = try_fus!(SignedEthTx::new(tx)); + let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); - Box::new(self.refund_hash_time_locked_payment(signed).map(TransactionEnum::from)) + Box::new( + self.refund_hash_time_locked_payment(swap_contract_address, signed) + .map(TransactionEnum::from), + ) } fn send_maker_refunds_payment( @@ -578,11 +605,16 @@ impl SwapOps for EthCoin { _time_lock: u32, _taker_pub: &[u8], _secret_hash: &[u8], + swap_contract_address: &Option, ) -> TransactionFut { let tx: UnverifiedTransaction = try_fus!(rlp::decode(maker_payment_tx)); let signed = try_fus!(SignedEthTx::new(tx)); + let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); - Box::new(self.refund_hash_time_locked_payment(signed).map(TransactionEnum::from)) + Box::new( + self.refund_hash_time_locked_payment(swap_contract_address, signed) + .map(TransactionEnum::from), + ) } fn validate_fee( @@ -675,8 +707,17 @@ impl SwapOps for EthCoin { maker_pub: &[u8], secret_hash: &[u8], amount: BigDecimal, + swap_contract_address: &Option, ) -> Box + Send> { - self.validate_payment(payment_tx, time_lock, maker_pub, secret_hash, amount) + let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + self.validate_payment( + payment_tx, + time_lock, + maker_pub, + secret_hash, + amount, + swap_contract_address, + ) } fn validate_taker_payment( @@ -686,8 +727,17 @@ impl SwapOps for EthCoin { taker_pub: &[u8], secret_hash: &[u8], amount: BigDecimal, + swap_contract_address: &Option, ) -> Box + Send> { - self.validate_payment(payment_tx, time_lock, taker_pub, secret_hash, amount) + let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + self.validate_payment( + payment_tx, + time_lock, + taker_pub, + secret_hash, + amount, + swap_contract_address, + ) } fn check_if_my_payment_sent( @@ -696,15 +746,27 @@ impl SwapOps for EthCoin { _other_pub: &[u8], secret_hash: &[u8], from_block: u64, + swap_contract_address: &Option, ) -> Box, Error = String> + Send> { let id = self.etomic_swap_id(time_lock, secret_hash); + let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); let selfi = self.clone(); let fut = async move { - let status = try_s!(selfi.payment_status(Token::FixedBytes(id.clone())).compat().await); + let status = try_s!( + selfi + .payment_status(swap_contract_address, Token::FixedBytes(id.clone())) + .compat() + .await + ); if status == PAYMENT_STATE_UNINITIALIZED.into() { return Ok(None); }; - let events = try_s!(selfi.payment_sent_events(from_block).compat().await); + let events = try_s!( + selfi + .payment_sent_events(swap_contract_address, from_block) + .compat() + .await + ); let found = events.iter().find(|event| &event.data.0[..32] == id.as_slice()); @@ -736,8 +798,10 @@ impl SwapOps for EthCoin { _secret_hash: &[u8], tx: &[u8], search_from_block: u64, + swap_contract_address: &Option, ) -> Result, String> { - self.search_for_swap_tx_spend(tx, search_from_block) + let swap_contract_address = try_s!(swap_contract_address.try_to_address()); + self.search_for_swap_tx_spend(tx, swap_contract_address, search_from_block) } fn search_for_swap_tx_spend_other( @@ -747,8 +811,10 @@ impl SwapOps for EthCoin { _secret_hash: &[u8], tx: &[u8], search_from_block: u64, + swap_contract_address: &Option, ) -> Result, String> { - self.search_for_swap_tx_spend(tx, search_from_block) + let swap_contract_address = try_s!(swap_contract_address.try_to_address()); + self.search_for_swap_tx_spend(tx, swap_contract_address, search_from_block) } fn extract_secret(&self, _secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { @@ -871,9 +937,16 @@ impl MarketCoinOps for EthCoin { Box::new(fut.boxed().compat()) } - fn wait_for_tx_spend(&self, tx_bytes: &[u8], wait_until: u64, from_block: u64) -> TransactionFut { + fn wait_for_tx_spend( + &self, + tx_bytes: &[u8], + wait_until: u64, + from_block: u64, + swap_contract_address: &Option, + ) -> TransactionFut { let unverified: UnverifiedTransaction = try_fus!(rlp::decode(tx_bytes)); let tx = try_fus!(SignedEthTx::new(unverified)); + let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); let func_name = match self.coin_type { EthCoinType::Eth => "ethPayment", @@ -890,7 +963,7 @@ impl MarketCoinOps for EthCoin { let fut = async move { loop { - let events = match selfi.spend_events(from_block).compat().await { + let events = match selfi.spend_events(swap_contract_address, from_block).compat().await { Ok(ev) => ev, Err(e) => { log!("Error " (e) " getting spend events"); @@ -1091,6 +1164,7 @@ impl EthCoin { time_lock: u32, secret_hash: &[u8], receiver_addr: Address, + swap_contract_address: Address, ) -> EthTxFut { match self.coin_type { EthCoinType::Eth => { @@ -1101,15 +1175,10 @@ impl EthCoin { Token::FixedBytes(secret_hash.to_vec()), Token::Uint(U256::from(time_lock)) ])); - self.sign_and_send_transaction( - value, - Action::Call(self.swap_contract_address), - data, - U256::from(150_000), - ) + self.sign_and_send_transaction(value, Action::Call(swap_contract_address), data, U256::from(150_000)) }, EthCoinType::Erc20(token_addr) => { - let allowance_fut = self.allowance(self.swap_contract_address); + let allowance_fut = self.allowance(swap_contract_address); let function = try_fus!(SWAP_CONTRACT.function("erc20Payment")); let data = try_fus!(function.encode_input(&[ @@ -1126,20 +1195,19 @@ impl EthCoin { if allowed < value { let balance_f = arc.my_balance(); Box::new(balance_f.and_then(move |balance| { - arc.approve(arc.swap_contract_address, balance) - .and_then(move |_approved| { - arc.sign_and_send_transaction( - 0.into(), - Action::Call(arc.swap_contract_address), - data, - U256::from(150_000), - ) - }) + arc.approve(swap_contract_address, balance).and_then(move |_approved| { + arc.sign_and_send_transaction( + 0.into(), + Action::Call(swap_contract_address), + data, + U256::from(150_000), + ) + }) })) } else { Box::new(arc.sign_and_send_transaction( 0.into(), - Action::Call(arc.swap_contract_address), + Action::Call(swap_contract_address), data, U256::from(150_000), )) @@ -1149,7 +1217,12 @@ impl EthCoin { } } - fn spend_hash_time_locked_payment(&self, payment: SignedEthTx, secret: &[u8]) -> EthTxFut { + fn spend_hash_time_locked_payment( + &self, + payment: SignedEthTx, + swap_contract_address: Address, + secret: &[u8], + ) -> EthTxFut { let spend_func = try_fus!(SWAP_CONTRACT.function("receiverSpend")); let clone = self.clone(); let secret_vec = secret.to_vec(); @@ -1159,7 +1232,7 @@ impl EthCoin { let payment_func = try_fus!(SWAP_CONTRACT.function("ethPayment")); let decoded = try_fus!(payment_func.decode_input(&payment.data)); - let state_f = self.payment_status(decoded[0].clone()); + let state_f = self.payment_status(swap_contract_address, decoded[0].clone()); Box::new(state_f.and_then(move |state| -> EthTxFut { if state != PAYMENT_STATE_SENT.into() { return Box::new(futures01::future::err(ERRL!( @@ -1180,7 +1253,7 @@ impl EthCoin { clone.sign_and_send_transaction( 0.into(), - Action::Call(clone.swap_contract_address), + Action::Call(swap_contract_address), data, U256::from(150_000), ) @@ -1189,7 +1262,7 @@ impl EthCoin { EthCoinType::Erc20(token_addr) => { let payment_func = try_fus!(SWAP_CONTRACT.function("erc20Payment")); let decoded = try_fus!(payment_func.decode_input(&payment.data)); - let state_f = self.payment_status(decoded[0].clone()); + let state_f = self.payment_status(swap_contract_address, decoded[0].clone()); Box::new(state_f.and_then(move |state| -> EthTxFut { if state != PAYMENT_STATE_SENT.into() { @@ -1209,7 +1282,7 @@ impl EthCoin { clone.sign_and_send_transaction( 0.into(), - Action::Call(clone.swap_contract_address), + Action::Call(swap_contract_address), data, U256::from(150_000), ) @@ -1218,7 +1291,7 @@ impl EthCoin { } } - fn refund_hash_time_locked_payment(&self, payment: SignedEthTx) -> EthTxFut { + fn refund_hash_time_locked_payment(&self, swap_contract_address: Address, payment: SignedEthTx) -> EthTxFut { let refund_func = try_fus!(SWAP_CONTRACT.function("senderRefund")); let clone = self.clone(); @@ -1227,7 +1300,7 @@ impl EthCoin { let payment_func = try_fus!(SWAP_CONTRACT.function("ethPayment")); let decoded = try_fus!(payment_func.decode_input(&payment.data)); - let state_f = self.payment_status(decoded[0].clone()); + let state_f = self.payment_status(swap_contract_address, decoded[0].clone()); Box::new(state_f.and_then(move |state| -> EthTxFut { if state != PAYMENT_STATE_SENT.into() { return Box::new(futures01::future::err(ERRL!( @@ -1248,7 +1321,7 @@ impl EthCoin { clone.sign_and_send_transaction( 0.into(), - Action::Call(clone.swap_contract_address), + Action::Call(swap_contract_address), data, U256::from(150_000), ) @@ -1257,7 +1330,7 @@ impl EthCoin { EthCoinType::Erc20(token_addr) => { let payment_func = try_fus!(SWAP_CONTRACT.function("erc20Payment")); let decoded = try_fus!(payment_func.decode_input(&payment.data)); - let state_f = self.payment_status(decoded[0].clone()); + let state_f = self.payment_status(swap_contract_address, decoded[0].clone()); Box::new(state_f.and_then(move |state| -> EthTxFut { if state != PAYMENT_STATE_SENT.into() { return Box::new(futures01::future::err(ERRL!( @@ -1277,7 +1350,7 @@ impl EthCoin { clone.sign_and_send_transaction( 0.into(), - Action::Call(clone.swap_contract_address), + Action::Call(swap_contract_address), data, U256::from(150_000), ) @@ -1376,14 +1449,18 @@ impl EthCoin { } } - /// Gets `PaymentSent` events from etomic swap smart contract (`self.swap_contract_address` ) since `from_block` - fn payment_sent_events(&self, from_block: u64) -> Box, Error = String> + Send> { + /// Gets `PaymentSent` events from etomic swap smart contract since `from_block` + fn payment_sent_events( + &self, + swap_contract_address: Address, + from_block: u64, + ) -> Box, Error = String> + Send> { let contract_event = try_fus!(SWAP_CONTRACT.event("PaymentSent")); let filter = FilterBuilder::default() .topics(Some(vec![contract_event.signature()]), None, None, None) .from_block(BlockNumber::Number(from_block)) .to_block(BlockNumber::Pending) - .address(vec![self.swap_contract_address]) + .address(vec![swap_contract_address]) .build(); Box::new(self.web3.eth().logs(filter).map_err(|e| ERRL!("{}", e))) @@ -1396,6 +1473,7 @@ impl EthCoin { sender_pub: &[u8], secret_hash: &[u8], amount: BigDecimal, + expected_swap_contract_address: Address, ) -> Box + Send> { let unsigned: UnverifiedTransaction = try_fus!(rlp::decode(payment_tx)); let tx = try_fus!(SignedEthTx::new(unsigned)); @@ -1405,7 +1483,12 @@ impl EthCoin { let secret_hash = secret_hash.to_vec(); let fut = async move { let swap_id = selfi.etomic_swap_id(time_lock, &secret_hash); - let status = try_s!(selfi.payment_status(Token::FixedBytes(swap_id.clone())).compat().await); + let status = try_s!( + selfi + .payment_status(expected_swap_contract_address, Token::FixedBytes(swap_id.clone())) + .compat() + .await + ); if status != PAYMENT_STATE_SENT.into() { return ERR!("Payment state is not PAYMENT_STATE_SENT, got {}", status); } @@ -1433,11 +1516,11 @@ impl EthCoin { match selfi.coin_type { EthCoinType::Eth => { - if tx_from_rpc.to != Some(selfi.swap_contract_address) { + if tx_from_rpc.to != Some(expected_swap_contract_address) { return ERR!( "Payment tx {:?} was sent to wrong address, expected {:?}", tx_from_rpc, - selfi.swap_contract_address + expected_swap_contract_address ); } @@ -1480,11 +1563,11 @@ impl EthCoin { } }, EthCoinType::Erc20(token_addr) => { - if tx_from_rpc.to != Some(selfi.swap_contract_address) { + if tx_from_rpc.to != Some(expected_swap_contract_address) { return ERR!( "Payment tx {:?} was sent to wrong address, expected {:?}", tx_from_rpc, - selfi.swap_contract_address + expected_swap_contract_address ); } @@ -1541,13 +1624,17 @@ impl EthCoin { Box::new(fut.boxed().compat()) } - fn payment_status(&self, token: Token) -> Box + Send + 'static> { + fn payment_status( + &self, + swap_contract_address: H160, + token: Token, + ) -> Box + Send + 'static> { let function = try_fus!(SWAP_CONTRACT.function("payments")); let data = try_fus!(function.encode_input(&[token])); Box::new( - self.call_request(self.swap_contract_address, None, Some(data.into())) + self.call_request(swap_contract_address, None, Some(data.into())) .and_then(move |bytes| { let decoded_tokens = try_s!(function.decode_output(&bytes.0)); match decoded_tokens[2] { @@ -2350,6 +2437,27 @@ impl MmCoin for EthCoin { // Eth has not unspendable outputs Box::new(futures01::future::ok(0.into())) } + + fn swap_contract_address(&self) -> Option { + Some(BytesJson::from(self.swap_contract_address.0.as_ref())) + } +} + +pub trait TryToAddress { + fn try_to_address(&self) -> Result; +} + +impl TryToAddress for BytesJson { + fn try_to_address(&self) -> Result { Ok(Address::from(self.0.as_slice())) } +} + +impl TryToAddress for Option { + fn try_to_address(&self) -> Result { + match self { + Some(ref inner) => inner.try_to_address(), + None => ERR!("Cannot convert None to address"), + } + } } fn addr_from_raw_pubkey(pubkey: &[u8]) -> Result { diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index a94957166c..ab1a8bfc13 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -203,6 +203,7 @@ fn send_and_refund_erc20_payment() { )), &[1; 20], "0.001".parse().unwrap(), + &coin.swap_contract_address(), ) .wait() .unwrap(); @@ -219,6 +220,7 @@ fn send_and_refund_erc20_payment() { "03bc2c7ba671bae4a6fc835244c9762b41647b9827d4780a89a949b984a8ddcc06" )), &[1; 20], + &coin.swap_contract_address(), ) .wait() .unwrap(); @@ -263,6 +265,7 @@ fn send_and_refund_eth_payment() { )), &[1; 20], "0.001".parse().unwrap(), + &coin.swap_contract_address(), ) .wait() .unwrap(); @@ -279,6 +282,7 @@ fn send_and_refund_eth_payment() { "03bc2c7ba671bae4a6fc835244c9762b41647b9827d4780a89a949b984a8ddcc06" )), &[1; 20], + &coin.swap_contract_address(), ) .wait() .unwrap(); @@ -347,7 +351,7 @@ fn test_nonce_several_urls() { #[test] fn test_wait_for_payment_spend_timeout() { - EthCoinImpl::spend_events.mock_safe(|_, _| MockResult::Return(Box::new(futures01::future::ok(vec![])))); + EthCoinImpl::spend_events.mock_safe(|_, _, _| MockResult::Return(Box::new(futures01::future::ok(vec![])))); let key_pair = KeyPair::from_secret_slice( &hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(), @@ -393,7 +397,7 @@ fn test_wait_for_payment_spend_timeout() { ]; assert!(coin - .wait_for_tx_spend(&tx_bytes, wait_until, from_block) + .wait_for_tx_spend(&tx_bytes, wait_until, from_block, &coin.swap_contract_address()) .wait() .is_err()); } @@ -411,6 +415,7 @@ fn test_search_for_swap_tx_spend_was_spent() { let web3 = Web3::new(transport); let ctx = MmCtxBuilder::new().into_mm_arc(); + let swap_contract_address = Address::from("0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94"); let coin = EthCoin(Arc::new(EthCoinImpl { coin_type: EthCoinType::Eth, decimals: 18, @@ -418,7 +423,7 @@ fn test_search_for_swap_tx_spend_was_spent() { history_sync_state: Mutex::new(HistorySyncState::NotEnabled), my_address: key_pair.address(), key_pair, - swap_contract_address: Address::from("0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94"), + swap_contract_address, ticker: "ETH".into(), web3_instances: vec![Web3Instance { web3: web3.clone(), @@ -457,7 +462,11 @@ fn test_search_for_swap_tx_spend_was_spent() { ]; let spend_tx = FoundSwapTxSpend::Spent(unwrap!(signed_eth_tx_from_bytes(&spend_tx)).into()); - let found_tx = unwrap!(unwrap!(coin.search_for_swap_tx_spend(&payment_tx, 6051857,))); + let found_tx = unwrap!(unwrap!(coin.search_for_swap_tx_spend( + &payment_tx, + swap_contract_address, + 6051857, + ))); assert_eq!(spend_tx, found_tx); } @@ -474,6 +483,7 @@ fn test_search_for_swap_tx_spend_was_refunded() { let web3 = Web3::new(transport); let ctx = MmCtxBuilder::new().into_mm_arc(); + let swap_contract_address = Address::from("0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94"); let coin = EthCoin(Arc::new(EthCoinImpl { coin_type: EthCoinType::Erc20(Address::from("0xc0eb7aed740e1796992a08962c15661bdeb58003")), decimals: 18, @@ -481,7 +491,7 @@ fn test_search_for_swap_tx_spend_was_refunded() { history_sync_state: Mutex::new(HistorySyncState::NotEnabled), my_address: key_pair.address(), key_pair, - swap_contract_address: Address::from("0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94"), + swap_contract_address, ticker: "ETH".into(), web3_instances: vec![Web3Instance { web3: web3.clone(), @@ -521,7 +531,11 @@ fn test_search_for_swap_tx_spend_was_refunded() { ]; let refund_tx = FoundSwapTxSpend::Refunded(unwrap!(signed_eth_tx_from_bytes(&refund_tx)).into()); - let found_tx = unwrap!(unwrap!(coin.search_for_swap_tx_spend(&payment_tx, 5886908,))); + let found_tx = unwrap!(unwrap!(coin.search_for_swap_tx_spend( + &payment_tx, + swap_contract_address, + 5886908, + ))); assert_eq!(refund_tx, found_tx); } diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 7be8f02e06..574df401c9 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -71,10 +71,11 @@ pub mod coins_tests; pub mod eth; use self::eth::{eth_coin_from_conf_and_request, EthCoin, EthTxFeeDetails, SignedEthTx}; pub mod utxo; +use self::utxo::qtum::{self, qtum_coin_from_conf_and_request, QtumCoin}; use self::utxo::utxo_standard::{utxo_standard_coin_from_conf_and_request, UtxoStandardCoin}; use self::utxo::{UtxoFeeDetails, UtxoTx}; pub mod qrc20; -use qrc20::{qrc20_addr_from_str, qrc20_coin_from_conf_and_request, Qrc20Coin, Qrc20FeeDetails}; +use qrc20::{qrc20_coin_from_conf_and_request, Qrc20Coin, Qrc20FeeDetails}; #[doc(hidden)] #[allow(unused_variables)] pub mod test_coin; @@ -124,6 +125,7 @@ pub trait SwapOps { taker_pub: &[u8], secret_hash: &[u8], amount: BigDecimal, + swap_contract_address: &Option, ) -> TransactionFut; fn send_taker_payment( @@ -132,6 +134,7 @@ pub trait SwapOps { maker_pub: &[u8], secret_hash: &[u8], amount: BigDecimal, + swap_contract_address: &Option, ) -> TransactionFut; fn send_maker_spends_taker_payment( @@ -140,6 +143,7 @@ pub trait SwapOps { time_lock: u32, taker_pub: &[u8], secret: &[u8], + swap_contract_address: &Option, ) -> TransactionFut; fn send_taker_spends_maker_payment( @@ -148,6 +152,7 @@ pub trait SwapOps { time_lock: u32, maker_pub: &[u8], secret: &[u8], + swap_contract_address: &Option, ) -> TransactionFut; fn send_taker_refunds_payment( @@ -156,6 +161,7 @@ pub trait SwapOps { time_lock: u32, maker_pub: &[u8], secret_hash: &[u8], + swap_contract_address: &Option, ) -> TransactionFut; fn send_maker_refunds_payment( @@ -164,6 +170,7 @@ pub trait SwapOps { time_lock: u32, taker_pub: &[u8], secret_hash: &[u8], + swap_contract_address: &Option, ) -> TransactionFut; fn validate_fee( @@ -180,6 +187,7 @@ pub trait SwapOps { maker_pub: &[u8], priv_bn_hash: &[u8], amount: BigDecimal, + swap_contract_address: &Option, ) -> Box + Send>; fn validate_taker_payment( @@ -189,6 +197,7 @@ pub trait SwapOps { taker_pub: &[u8], priv_bn_hash: &[u8], amount: BigDecimal, + swap_contract_address: &Option, ) -> Box + Send>; fn check_if_my_payment_sent( @@ -197,6 +206,7 @@ pub trait SwapOps { other_pub: &[u8], secret_hash: &[u8], search_from_block: u64, + swap_contract_address: &Option, ) -> Box, Error = String> + Send>; fn search_for_swap_tx_spend_my( @@ -206,6 +216,7 @@ pub trait SwapOps { secret_hash: &[u8], tx: &[u8], search_from_block: u64, + swap_contract_address: &Option, ) -> Result, String>; fn search_for_swap_tx_spend_other( @@ -215,6 +226,7 @@ pub trait SwapOps { secret_hash: &[u8], tx: &[u8], search_from_block: u64, + swap_contract_address: &Option, ) -> Result, String>; fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String>; @@ -244,7 +256,13 @@ pub trait MarketCoinOps { check_every: u64, ) -> Box + Send>; - fn wait_for_tx_spend(&self, transaction: &[u8], wait_until: u64, from_block: u64) -> TransactionFut; + fn wait_for_tx_spend( + &self, + transaction: &[u8], + wait_until: u64, + from_block: u64, + swap_contract_address: &Option, + ) -> TransactionFut; fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result; @@ -486,11 +504,15 @@ pub trait MmCoin: SwapOps + MarketCoinOps + fmt::Debug + Send + Sync + 'static { /// Get unspendable balance (sum of non-mature output values). fn my_unspendable_balance(&self) -> Box + Send>; + + /// Get swap contract address if the coin uses it in Atomic Swaps. + fn swap_contract_address(&self) -> Option; } #[derive(Clone, Debug)] pub enum MmCoinEnum { UtxoCoin(UtxoStandardCoin), + QtumCoin(QtumCoin), Qrc20Coin(Qrc20Coin), EthCoin(EthCoin), Test(TestCoin), @@ -508,6 +530,10 @@ impl From for MmCoinEnum { fn from(c: TestCoin) -> MmCoinEnum { MmCoinEnum::Test(c) } } +impl From for MmCoinEnum { + fn from(coin: QtumCoin) -> Self { MmCoinEnum::QtumCoin(coin) } +} + impl From for MmCoinEnum { fn from(c: Qrc20Coin) -> MmCoinEnum { MmCoinEnum::Qrc20Coin(c) } } @@ -518,6 +544,7 @@ impl Deref for MmCoinEnum { fn deref(&self) -> &dyn MmCoin { match self { MmCoinEnum::UtxoCoin(ref c) => c, + MmCoinEnum::QtumCoin(ref c) => c, MmCoinEnum::Qrc20Coin(ref c) => c, MmCoinEnum::EthCoin(ref c) => c, MmCoinEnum::Test(ref c) => c, @@ -552,6 +579,7 @@ impl CoinsContext { #[serde(tag = "type", content = "protocol_data")] pub enum CoinProtocol { UTXO, + QTUM, QRC20 { platform: String, contract_address: String }, ETH, ERC20 { platform: String, contract_address: String }, @@ -734,6 +762,7 @@ pub async fn lp_coininit(ctx: &MmArc, ticker: &str, req: &Json) -> Result { try_s!(utxo_standard_coin_from_conf_and_request(ctx, ticker, coins_en, req, secret).await).into() }, + CoinProtocol::QTUM => try_s!(qtum_coin_from_conf_and_request(ctx, ticker, coins_en, req, secret).await).into(), CoinProtocol::ETH | CoinProtocol::ERC20 { .. } => { try_s!(eth_coin_from_conf_and_request(ctx, ticker, coins_en, req, secret, protocol).await).into() }, @@ -741,7 +770,7 @@ pub async fn lp_coininit(ctx: &MmArc, ticker: &str, req: &Json) -> Result { - let contract_address = try_s!(qrc20_addr_from_str(&contract_address)); + let contract_address = try_s!(qtum::contract_addr_from_str(&contract_address)); try_s!( qrc20_coin_from_conf_and_request(ctx, ticker, &platform, coins_en, req, secret, contract_address).await ) diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index fe47897016..ead6dc72aa 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -1,10 +1,12 @@ -use crate::eth::{self, u256_to_big_decimal, wei_from_big_decimal}; -use crate::qrc20::rpc_electrum::{ContractCallResult, LogEntry, Qrc20RpcOps, TxHistoryItem, TxReceipt}; -use crate::utxo::rpc_clients::{ElectrumClient, UnspentInfo, UtxoRpcClientEnum, UtxoRpcClientOps}; +use crate::eth::{self, u256_to_big_decimal, wei_from_big_decimal, TryToAddress}; +use crate::qrc20::rpc_clients::{LogEntry, Qrc20ElectrumOps, Qrc20NativeOps, Qrc20RpcOps, TopicFilter, TxReceipt, + ViewContractCallType}; +use crate::utxo::qtum::QtumBasedCoin; +use crate::utxo::rpc_clients::{ElectrumClient, NativeClient, UnspentInfo, UtxoRpcClientEnum, UtxoRpcClientOps}; use crate::utxo::utxo_common::{self, big_decimal_from_sat}; -use crate::utxo::{qtum, sign_tx, utxo_fields_from_conf_and_request, ActualTxFee, AdditionalTxData, FeePolicy, - GenerateTransactionError, RecentlySpentOutPoints, UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps, - UtxoTx, VerboseTransactionFrom, UTXO_LOCK}; +use crate::utxo::{coin_daemon_data_dir, qtum, sign_tx, ActualTxFee, AdditionalTxData, FeePolicy, + GenerateTransactionError, RecentlySpentOutPoints, UtxoAddressFormat, UtxoCoinBuilder, + UtxoCoinFields, UtxoCommonOps, UtxoTx, VerboseTransactionFrom, UTXO_LOCK}; use crate::{FoundSwapTxSpend, HistorySyncState, MarketCoinOps, MmCoin, SwapOps, TradeFee, TransactionDetails, TransactionEnum, TransactionFut, ValidateAddressResult, WithdrawFee, WithdrawRequest}; use async_trait::async_trait; @@ -14,6 +16,7 @@ use chain::TransactionOutput; use common::block_on; use common::executor::Timer; use common::jsonrpc_client::{JsonRpcClient, JsonRpcError, JsonRpcRequest, RpcRes}; +use common::log::{error, warn}; use common::mm_ctx::MmArc; use ethabi::{Function, Token}; use ethereum_types::{H160, U256}; @@ -32,14 +35,15 @@ use serde_json::{self as json, Value as Json}; use serialization::deserialize; use serialization::serialize; use std::ops::{Deref, Neg}; +use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; mod history; #[cfg(test)] mod qrc20_tests; -pub mod rpc_electrum; +pub mod rpc_clients; mod script_pubkey; -mod swap_ops; +mod swap; /// Qtum amount is always 0 for the QRC20 UTXO outputs, /// because we should pay only a fee in Qtum to send the QRC20 transaction. @@ -50,10 +54,125 @@ const QRC20_SWAP_GAS_REQUIRED: u64 = QRC20_GAS_LIMIT_DEFAULT * 3; const QRC20_DUST: u64 = 0; // Keccak-256 hash of `Transfer` event const QRC20_TRANSFER_TOPIC: &str = "ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"; -#[allow(dead_code)] -const QRC20_APPROVE_TOPIC: &str = "8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925"; -#[allow(dead_code)] const QRC20_PAYMENT_SENT_TOPIC: &str = "ccc9c05183599bd3135da606eaaf535daffe256e9de33c048014cffcccd4ad57"; +const QRC20_RECEIVER_SPENT_TOPIC: &str = "36c177bcb01c6d568244f05261e2946c8c977fa50822f3fa098c470770ee1f3e"; +const QRC20_SENDER_REFUNDED_TOPIC: &str = "1797d500133f8e427eb9da9523aa4a25cb40f50ebc7dbda3c7c81778973f35ba"; + +struct Qrc20CoinBuilder<'a> { + ctx: &'a MmArc, + ticker: &'a str, + conf: &'a Json, + req: &'a Json, + priv_key: &'a [u8], + platform: String, + contract_address: H160, +} + +impl<'a> Qrc20CoinBuilder<'a> { + pub fn new( + ctx: &'a MmArc, + ticker: &'a str, + conf: &'a Json, + req: &'a Json, + priv_key: &'a [u8], + platform: String, + contract_address: H160, + ) -> Qrc20CoinBuilder<'a> { + Qrc20CoinBuilder { + ctx, + ticker, + conf, + req, + priv_key, + platform, + contract_address, + } + } +} + +impl Qrc20CoinBuilder<'_> { + fn swap_contract_address(&self) -> Result { + match self.req()["swap_contract_address"].as_str() { + Some(address) => qtum::contract_addr_from_str(address).map_err(|e| ERRL!("{}", e)), + None => return ERR!("\"swap_contract_address\" field is expected"), + } + } +} + +#[async_trait] +impl UtxoCoinBuilder for Qrc20CoinBuilder<'_> { + type ResultCoin = Qrc20Coin; + + async fn build(self) -> Result { + let swap_contract_address = try_s!(self.swap_contract_address()); + let utxo = try_s!(self.build_utxo_fields().await); + let inner = Qrc20CoinFields { + utxo, + platform: self.platform, + contract_address: self.contract_address, + swap_contract_address, + }; + Ok(Qrc20Coin(Arc::new(inner))) + } + + fn ctx(&self) -> &MmArc { self.ctx } + + fn conf(&self) -> &Json { self.conf } + + fn req(&self) -> &Json { self.req } + + fn ticker(&self) -> &str { self.ticker } + + fn priv_key(&self) -> &[u8] { self.priv_key } + + fn address_format(&self) -> Result { Ok(UtxoAddressFormat::Standard) } + + async fn decimals(&self, rpc_client: &UtxoRpcClientEnum) -> Result { + if let Some(d) = self.conf()["decimals"].as_u64() { + return Ok(d as u8); + } + + rpc_client + .token_decimals(&self.contract_address) + .compat() + .await + .map_err(|e| ERRL!("{}", e)) + } + + fn dust_amount(&self) -> u64 { QRC20_DUST } + + #[cfg(feature = "native")] + fn confpath(&self) -> Result { + // Documented at https://github.com/jl777/coins#bitcoin-protocol-specific-json + // "USERHOME/" prefix should be replaced with the user's home folder. + let declared_confpath = match self.conf()["confpath"].as_str() { + Some(path) if !path.is_empty() => path.trim(), + _ => { + let is_asset_chain = false; + let platform = self.platform.to_lowercase(); + let data_dir = coin_daemon_data_dir(&platform, is_asset_chain); + + let confname = format!("{}.conf", platform); + return Ok(data_dir.join(&confname[..])); + }, + }; + + let (confpath, rel_to_home) = match declared_confpath.strip_prefix("~/") { + Some(stripped) => (stripped, true), + None => match declared_confpath.strip_prefix("USERHOME/") { + Some(stripped) => (stripped, true), + None => (declared_confpath, false), + }, + }; + + if rel_to_home { + let home = try_s!(dirs::home_dir().ok_or("Can not detect the user home directory")); + Ok(home.join(confpath)) + } else { + Ok(confpath.into()) + } + } +} pub async fn qrc20_coin_from_conf_and_request( ctx: &MmArc, @@ -64,40 +183,8 @@ pub async fn qrc20_coin_from_conf_and_request( priv_key: &[u8], contract_address: H160, ) -> Result { - if let Some("enable") = req["method"].as_str() { - return ERR!("Native mode not supported yet for QRC20"); - } - let swap_contract_address = match req["swap_contract_address"].as_str() { - Some(address) => try_s!(qrc20_addr_from_str(address)), - None => return ERR!("\"swap_contract_address\" field is expected"), - }; - - let mut utxo = try_s!(utxo_fields_from_conf_and_request(ctx, ticker, conf, req, priv_key, QRC20_DUST).await); - match &utxo.address_format { - UtxoAddressFormat::Standard => (), - _ => return ERR!("Expect standard UTXO address format"), - } - - // if `decimals` is not set in config then we have to request it from contract - if conf["decimals"].as_u64().is_none() { - let contract_addr = qrc20_addr_into_rpc_format(&contract_address); - let token_info = match utxo.rpc_client { - UtxoRpcClientEnum::Electrum(ref electrum) => { - try_s!(electrum.blockchain_token_get_info(&contract_addr).compat().await) - }, - UtxoRpcClientEnum::Native(_) => return ERR!("Electrum client expected"), - }; - utxo.decimals = token_info.decimals; - } - - let platform = platform.to_owned(); - let inner = Qrc20CoinFields { - utxo, - platform, - contract_address, - swap_contract_address, - }; - Ok(Qrc20Coin(Arc::new(inner))) + let builder = Qrc20CoinBuilder::new(ctx, ticker, conf, req, priv_key, platform.to_owned(), contract_address); + builder.build().await } #[derive(Debug)] @@ -120,33 +207,7 @@ impl AsRef for Qrc20Coin { fn as_ref(&self) -> &UtxoCoinFields { &self.utxo } } -enum RpcContractCallType { - /// Erc20 function. - BalanceOf, - /// Erc20 function. - Allowance, - /// EtomicSwap function. - Payments, -} - -impl RpcContractCallType { - fn as_function_name(&self) -> &'static str { - match self { - RpcContractCallType::BalanceOf => "balanceOf", - RpcContractCallType::Allowance => "allowance", - RpcContractCallType::Payments => "payments", - } - } - - fn as_function(&self) -> &'static Function { - match self { - RpcContractCallType::BalanceOf | RpcContractCallType::Allowance => { - unwrap!(eth::ERC20_CONTRACT.function(self.as_function_name())) - }, - RpcContractCallType::Payments => unwrap!(eth::SWAP_CONTRACT.function(self.as_function_name())), - } - } -} +impl qtum::QtumBasedCoin for Qrc20Coin {} #[derive(Clone, Debug, PartialEq)] pub struct ContractCallOutput { @@ -165,46 +226,35 @@ impl From for TransactionOutput { } } -#[derive(Debug, Serialize, Deserialize)] -#[serde(tag = "format")] -pub enum Qrc20AddressFormat { - /// Standard Qtum/UTXO address format. - #[serde(rename = "wallet")] - Wallet, - /// Contract address format. The same as used in ETH/ERC20. - /// Note starts with "0x" prefix. - #[serde(rename = "contract")] - Contract, -} - +/// Functions of ERC20/EtomicSwap smart contracts that may change the blockchain state. #[derive(Debug, Eq, PartialEq)] -pub enum ContractCallType { +pub enum MutContractCallType { Transfer, Erc20Payment, ReceiverSpend, SenderRefund, } -impl ContractCallType { +impl MutContractCallType { fn as_function_name(&self) -> &'static str { match self { - ContractCallType::Transfer => "transfer", - ContractCallType::Erc20Payment => "erc20Payment", - ContractCallType::ReceiverSpend => "receiverSpend", - ContractCallType::SenderRefund => "senderRefund", + MutContractCallType::Transfer => "transfer", + MutContractCallType::Erc20Payment => "erc20Payment", + MutContractCallType::ReceiverSpend => "receiverSpend", + MutContractCallType::SenderRefund => "senderRefund", } } fn as_function(&self) -> &'static Function { match self { - ContractCallType::Transfer => unwrap!(eth::ERC20_CONTRACT.function(self.as_function_name())), - ContractCallType::Erc20Payment | ContractCallType::ReceiverSpend | ContractCallType::SenderRefund => { - unwrap!(eth::SWAP_CONTRACT.function(self.as_function_name())) - }, + MutContractCallType::Transfer => unwrap!(eth::ERC20_CONTRACT.function(self.as_function_name())), + MutContractCallType::Erc20Payment + | MutContractCallType::ReceiverSpend + | MutContractCallType::SenderRefund => unwrap!(eth::SWAP_CONTRACT.function(self.as_function_name())), } } - pub fn from_script_pubkey(script: &[u8]) -> Result, String> { + pub fn from_script_pubkey(script: &[u8]) -> Result, String> { lazy_static! { static ref TRANSFER_SHORT_SIGN: [u8; 4] = eth::ERC20_CONTRACT.function("transfer").unwrap().short_signature(); @@ -221,16 +271,16 @@ impl ContractCallType { } if script.starts_with(TRANSFER_SHORT_SIGN.as_ref()) { - return Ok(Some(ContractCallType::Transfer)); + return Ok(Some(MutContractCallType::Transfer)); } if script.starts_with(ERC20_PAYMENT_SHORT_SIGN.as_ref()) { - return Ok(Some(ContractCallType::Erc20Payment)); + return Ok(Some(MutContractCallType::Erc20Payment)); } if script.starts_with(RECEIVER_SPEND_SHORT_SIGN.as_ref()) { - return Ok(Some(ContractCallType::ReceiverSpend)); + return Ok(Some(MutContractCallType::ReceiverSpend)); } if script.starts_with(SENDER_REFUND_SHORT_SIGN.as_ref()) { - return Ok(Some(ContractCallType::SenderRefund)); + return Ok(Some(MutContractCallType::SenderRefund)); } Ok(None) } @@ -246,78 +296,6 @@ struct GenerateQrc20TxResult { } impl Qrc20Coin { - async fn rpc_contract_call( - &self, - func: RpcContractCallType, - contract_addr: &H160, - tokens: &[Token], - ) -> Result, String> { - let function = func.as_function(); - let params = try_s!(function.encode_input(tokens)); - let contract_addr = qrc20_addr_into_rpc_format(contract_addr); - - let electrum = match self.utxo.rpc_client { - UtxoRpcClientEnum::Electrum(ref electrum) => electrum, - _ => return ERR!("Electrum client expected"), - }; - - let result: ContractCallResult = try_s!( - electrum - .blockchain_contract_call(&contract_addr, params.into()) - .compat() - .await - ); - Ok(try_s!(function.decode_output(&result.execution_result.output))) - } - - pub fn utxo_address_from_qrc20(&self, address: H160) -> UtxoAddress { - let utxo = self.as_ref(); - UtxoAddress { - prefix: utxo.pub_addr_prefix, - t_addr_prefix: utxo.pub_t_addr_prefix, - hash: address.0.into(), - checksum_type: utxo.checksum_type, - } - } - - pub fn utxo_address_from_raw_pubkey(&self, pubkey: &[u8]) -> Result { - Ok(try_s!(utxo_common::address_from_raw_pubkey( - pubkey, - self.utxo.pub_addr_prefix, - self.utxo.pub_t_addr_prefix, - self.utxo.checksum_type - ))) - } - - pub fn qrc20_address_from_raw_pubkey(&self, pubkey: &[u8]) -> Result { - let qtum_address = try_s!(self.utxo_address_from_raw_pubkey(pubkey)); - Ok(qrc20_addr_from_utxo_addr(qtum_address)) - } - - /// Try to parse address from either wallet (UTXO) format or contract (QRC20) format. - pub fn utxo_address_from_any_format(&self, from: &str) -> Result { - let utxo_err = match UtxoAddress::from_str(from) { - Ok(addr) => { - let is_p2pkh = - addr.prefix == self.utxo.pub_addr_prefix && addr.t_addr_prefix == self.utxo.pub_t_addr_prefix; - if is_p2pkh { - return Ok(addr); - } - "Address has invalid prefixes".to_string() - }, - Err(e) => e.to_string(), - }; - let qrc20_err = match qrc20_addr_from_str(from) { - Ok(qrc20_addr) => return Ok(self.utxo_address_from_qrc20(qrc20_addr)), - Err(e) => e, - }; - ERR!( - "error on parse wallet address: {:?}, error on parse contract address: {:?}", - utxo_err, - qrc20_err, - ) - } - /// `gas_fee` should be calculated by: gas_limit * gas_price * (count of contract calls), /// or should be sum of gas fee of all contract calls. pub async fn get_qrc20_tx_fee(&self, gas_fee: u64) -> Result { @@ -436,9 +414,7 @@ impl UtxoCommonOps for Qrc20Coin { async fn get_current_mtp(&self) -> Result { utxo_common::get_current_mtp(&self.utxo).await } - fn is_unspent_mature(&self, output: &RpcTransaction) -> bool { - qtum::is_qtum_unspent_mature(self.utxo.mature_confirmations, output) - } + fn is_unspent_mature(&self, output: &RpcTransaction) -> bool { self.is_qtum_unspent_mature(output) } /// Generate UTXO transaction with specified unspent inputs and specified outputs. async fn generate_transaction( @@ -513,7 +489,7 @@ impl UtxoCommonOps for Qrc20Coin { impl SwapOps for Qrc20Coin { fn send_taker_fee(&self, fee_addr: &[u8], amount: BigDecimal) -> TransactionFut { - let to_address = try_fus!(self.qrc20_address_from_raw_pubkey(fee_addr)); + let to_address = try_fus!(self.contract_address_from_raw_pubkey(fee_addr)); let amount = try_fus!(wei_from_big_decimal(&amount, self.utxo.decimals)); let transfer_output = try_fus!(self.transfer_output(to_address, amount, QRC20_GAS_LIMIT_DEFAULT, QRC20_GAS_PRICE_DEFAULT)); @@ -531,16 +507,18 @@ impl SwapOps for Qrc20Coin { taker_pub: &[u8], secret_hash: &[u8], amount: BigDecimal, + swap_contract_address: &Option, ) -> TransactionFut { - let taker_addr = try_fus!(self.qrc20_address_from_raw_pubkey(taker_pub)); + let taker_addr = try_fus!(self.contract_address_from_raw_pubkey(taker_pub)); let id = qrc20_swap_id(time_lock, secret_hash); let value = try_fus!(wei_from_big_decimal(&amount, self.utxo.decimals)); let secret_hash = Vec::from(secret_hash); + let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); let selfi = self.clone(); let fut = async move { selfi - .send_hash_time_locked_payment(id, value, time_lock, secret_hash, taker_addr) + .send_hash_time_locked_payment(id, value, time_lock, secret_hash, taker_addr, swap_contract_address) .await }; Box::new(fut.boxed().compat()) @@ -552,16 +530,18 @@ impl SwapOps for Qrc20Coin { maker_pub: &[u8], secret_hash: &[u8], amount: BigDecimal, + swap_contract_address: &Option, ) -> TransactionFut { - let maker_addr = try_fus!(self.qrc20_address_from_raw_pubkey(maker_pub)); + let maker_addr = try_fus!(self.contract_address_from_raw_pubkey(maker_pub)); let id = qrc20_swap_id(time_lock, secret_hash); let value = try_fus!(wei_from_big_decimal(&amount, self.utxo.decimals)); let secret_hash = Vec::from(secret_hash); + let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); let selfi = self.clone(); let fut = async move { selfi - .send_hash_time_locked_payment(id, value, time_lock, secret_hash, maker_addr) + .send_hash_time_locked_payment(id, value, time_lock, secret_hash, maker_addr, swap_contract_address) .await }; Box::new(fut.boxed().compat()) @@ -573,12 +553,18 @@ impl SwapOps for Qrc20Coin { _time_lock: u32, _taker_pub: &[u8], secret: &[u8], + swap_contract_address: &Option, ) -> TransactionFut { let payment_tx: UtxoTx = try_fus!(deserialize(taker_payment_tx).map_err(|e| ERRL!("{:?}", e))); + let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); let secret = secret.to_vec(); let selfi = self.clone(); - let fut = async move { selfi.spend_hash_time_locked_payment(payment_tx, secret).await }; + let fut = async move { + selfi + .spend_hash_time_locked_payment(payment_tx, swap_contract_address, secret) + .await + }; Box::new(fut.boxed().compat()) } @@ -588,12 +574,18 @@ impl SwapOps for Qrc20Coin { _time_lock: u32, _maker_pub: &[u8], secret: &[u8], + swap_contract_address: &Option, ) -> TransactionFut { let payment_tx: UtxoTx = try_fus!(deserialize(maker_payment_tx).map_err(|e| ERRL!("{:?}", e))); let secret = secret.to_vec(); + let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); let selfi = self.clone(); - let fut = async move { selfi.spend_hash_time_locked_payment(payment_tx, secret).await }; + let fut = async move { + selfi + .spend_hash_time_locked_payment(payment_tx, swap_contract_address, secret) + .await + }; Box::new(fut.boxed().compat()) } @@ -603,10 +595,17 @@ impl SwapOps for Qrc20Coin { _time_lock: u32, _maker_pub: &[u8], _secret_hash: &[u8], + swap_contract_address: &Option, ) -> TransactionFut { let payment_tx: UtxoTx = try_fus!(deserialize(taker_payment_tx).map_err(|e| ERRL!("{:?}", e))); + let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + let selfi = self.clone(); - let fut = async move { selfi.refund_hash_time_locked_payment(payment_tx).await }; + let fut = async move { + selfi + .refund_hash_time_locked_payment(swap_contract_address, payment_tx) + .await + }; Box::new(fut.boxed().compat()) } @@ -616,11 +615,17 @@ impl SwapOps for Qrc20Coin { _time_lock: u32, _taker_pub: &[u8], _secret_hash: &[u8], + swap_contract_address: &Option, ) -> TransactionFut { let payment_tx: UtxoTx = try_fus!(deserialize(maker_payment_tx).map_err(|e| ERRL!("{:?}", e))); + let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); let selfi = self.clone(); - let fut = async move { selfi.refund_hash_time_locked_payment(payment_tx).await }; + let fut = async move { + selfi + .refund_hash_time_locked_payment(swap_contract_address, payment_tx) + .await + }; Box::new(fut.boxed().compat()) } @@ -634,7 +639,7 @@ impl SwapOps for Qrc20Coin { TransactionEnum::UtxoTx(tx) => tx.hash().reversed().into(), _ => panic!("Unexpected TransactionEnum"), }; - let fee_addr = try_fus!(self.qrc20_address_from_raw_pubkey(fee_addr)); + let fee_addr = try_fus!(self.contract_address_from_raw_pubkey(fee_addr)); let expected_value = try_fus!(wei_from_big_decimal(amount, self.utxo.decimals)); let selfi = self.clone(); @@ -649,15 +654,24 @@ impl SwapOps for Qrc20Coin { maker_pub: &[u8], secret_hash: &[u8], amount: BigDecimal, + swap_contract_address: &Option, ) -> Box + Send> { let payment_tx: UtxoTx = try_fus!(deserialize(payment_tx).map_err(|e| ERRL!("{:?}", e))); - let sender = try_fus!(self.qrc20_address_from_raw_pubkey(maker_pub)); + let sender = try_fus!(self.contract_address_from_raw_pubkey(maker_pub)); let secret_hash = secret_hash.to_vec(); + let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); let selfi = self.clone(); let fut = async move { selfi - .validate_payment(payment_tx, time_lock, sender, secret_hash, amount) + .validate_payment( + payment_tx, + time_lock, + sender, + secret_hash, + amount, + swap_contract_address, + ) .await }; Box::new(fut.boxed().compat()) @@ -670,15 +684,24 @@ impl SwapOps for Qrc20Coin { taker_pub: &[u8], secret_hash: &[u8], amount: BigDecimal, + swap_contract_address: &Option, ) -> Box + Send> { + let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); let payment_tx: UtxoTx = try_fus!(deserialize(payment_tx).map_err(|e| ERRL!("{:?}", e))); - let sender = try_fus!(self.qrc20_address_from_raw_pubkey(taker_pub)); + let sender = try_fus!(self.contract_address_from_raw_pubkey(taker_pub)); let secret_hash = secret_hash.to_vec(); let selfi = self.clone(); let fut = async move { selfi - .validate_payment(payment_tx, time_lock, sender, secret_hash, amount) + .validate_payment( + payment_tx, + time_lock, + sender, + secret_hash, + amount, + swap_contract_address, + ) .await }; Box::new(fut.boxed().compat()) @@ -690,11 +713,17 @@ impl SwapOps for Qrc20Coin { _other_pub: &[u8], secret_hash: &[u8], search_from_block: u64, + swap_contract_address: &Option, ) -> Box, Error = String> + Send> { let swap_id = qrc20_swap_id(time_lock, secret_hash); + let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); let selfi = self.clone(); - let fut = async move { selfi.check_if_my_payment_sent_impl(swap_id, search_from_block).await }; + let fut = async move { + selfi + .check_if_my_payment_sent_impl(swap_contract_address, swap_id, search_from_block) + .await + }; Box::new(fut.boxed().compat()) } @@ -705,6 +734,7 @@ impl SwapOps for Qrc20Coin { secret_hash: &[u8], tx: &[u8], search_from_block: u64, + _swap_contract_address: &Option, ) -> Result, String> { let tx: UtxoTx = try_s!(deserialize(tx).map_err(|e| ERRL!("{:?}", e))); @@ -720,6 +750,7 @@ impl SwapOps for Qrc20Coin { secret_hash: &[u8], tx: &[u8], search_from_block: u64, + _swap_contract_address: &Option, ) -> Result, String> { let tx: UtxoTx = try_s!(deserialize(tx).map_err(|e| ERRL!("{:?}", e))); @@ -739,25 +770,22 @@ impl MarketCoinOps for Qrc20Coin { fn my_address(&self) -> Result { utxo_common::my_address(self) } fn my_balance(&self) -> Box + Send> { - let my_address = qrc20_addr_from_utxo_addr(self.utxo.my_address.clone()); + let my_address = self.my_addr_as_contract_addr(); + let params = &[Token::Address(my_address)]; let contract_address = self.contract_address; - let selfi = self.clone(); - let fut = async move { - let params = &[Token::Address(my_address)]; - let tokens = try_s!( - selfi - .rpc_contract_call(RpcContractCallType::BalanceOf, &contract_address, params) - .await - ); - - match tokens.first() { - Some(Token::Uint(bal)) => u256_to_big_decimal(*bal, selfi.utxo.decimals), + let decimals = self.utxo.decimals; + + let fut = self + .utxo + .rpc_client + .rpc_contract_call(ViewContractCallType::BalanceOf, &contract_address, params) + .map_err(|e| ERRL!("{}", e)) + .and_then(move |tokens| match tokens.first() { + Some(Token::Uint(bal)) => u256_to_big_decimal(*bal, decimals), Some(_) => ERR!(r#"Expected Uint as "balanceOf" result but got {:?}"#, tokens), None => ERR!(r#"Expected Uint as "balanceOf" result but got nothing"#), - } - }; - - Box::new(fut.boxed().compat()) + }); + Box::new(fut) } fn base_coin_balance(&self) -> Box + Send> { @@ -787,7 +815,13 @@ impl MarketCoinOps for Qrc20Coin { Box::new(fut.boxed().compat()) } - fn wait_for_tx_spend(&self, transaction: &[u8], wait_until: u64, from_block: u64) -> TransactionFut { + fn wait_for_tx_spend( + &self, + transaction: &[u8], + wait_until: u64, + from_block: u64, + _swap_contract_address: &Option, + ) -> TransactionFut { let tx: UtxoTx = try_fus!(deserialize(transaction).map_err(|e| ERRL!("{:?}", e))); let selfi = self.clone(); @@ -823,7 +857,6 @@ impl MmCoin for Qrc20Coin { let gas_fee = QRC20_GAS_LIMIT_DEFAULT * QRC20_GAS_PRICE_DEFAULT; let min_amount: U256 = try_s!(selfi.get_qrc20_tx_fee(gas_fee).await).into(); - log!("qtum_balance " [qtum_balance_sat] " min_amount " (min_amount)); if qtum_balance_sat < min_amount { // u256_to_big_decimal() is expected to return no error let min_amount = try_s!(u256_to_big_decimal(min_amount, selfi.utxo.decimals)); @@ -847,13 +880,7 @@ impl MmCoin for Qrc20Coin { fn decimals(&self) -> u8 { utxo_common::decimals(&self.utxo) } fn convert_to_address(&self, from: &str, to_address_format: Json) -> Result { - let to_address_format: Qrc20AddressFormat = - json::from_value(to_address_format).map_err(|e| ERRL!("Error on parse QRC20 address format {:?}", e))?; - let from_address = try_s!(self.utxo_address_from_any_format(from)); - match to_address_format { - Qrc20AddressFormat::Wallet => Ok(from_address.to_string()), - Qrc20AddressFormat::Contract => Ok(display_contract_address(from_address)), - } + qtum::QtumBasedCoin::convert_to_address(self, from, to_address_format) } fn validate_address(&self, address: &str) -> ValidateAddressResult { utxo_common::validate_address(self, address) } @@ -895,6 +922,10 @@ impl MmCoin for Qrc20Coin { // QRC20 cannot have unspendable balance Box::new(futures01::future::ok(0.into())) } + + fn swap_contract_address(&self) -> Option { + Some(BytesJson::from(self.swap_contract_address.0.as_ref())) + } } pub fn qrc20_swap_id(time_lock: u32, secret_hash: &[u8]) -> Vec { @@ -904,16 +935,7 @@ pub fn qrc20_swap_id(time_lock: u32, secret_hash: &[u8]) -> Vec { sha256(&input).to_vec() } -/// Parse QRC20 address (H160) from string. -/// QRC20 addresses have another checksum verification algorithm, because of this do not use [`eth::valid_addr_from_str`]. -pub fn qrc20_addr_from_str(addr: &str) -> Result { eth::addr_from_str(addr) } - -pub fn qrc20_addr_from_utxo_addr(address: UtxoAddress) -> H160 { address.hash.take().into() } - -#[allow(dead_code)] -fn utxo_addr_into_rpc_format(address: UtxoAddress) -> H160Json { address.hash.take().into() } - -fn qrc20_addr_into_rpc_format(address: &H160) -> H160Json { address.to_vec().as_slice().into() } +fn contract_addr_into_rpc_format(address: &H160) -> H160Json { H160Json::from(address.0) } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct Qrc20FeeDetails { @@ -973,7 +995,7 @@ async fn qrc20_withdraw(coin: Qrc20Coin, req: WithdrawRequest) -> Result Result { Ok(hash.0.into()) } +fn address_to_log_topic(address: &H160) -> String { + let zeros = std::str::from_utf8(&[b'0'; 24]).expect("Expected a valid str from slice of '0' chars"); + let mut topic = format!("{:02x}", address); + topic.insert_str(0, zeros); + topic +} + pub struct TransferEventDetails { contract_address: H160, amount: U256, @@ -1043,10 +1072,10 @@ pub struct TransferEventDetails { fn transfer_event_from_log(log: &LogEntry) -> Result { let contract_address = if log.address.starts_with("0x") { - try_s!(qrc20_addr_from_str(&log.address)) + try_s!(qtum::contract_addr_from_str(&log.address)) } else { let address = format!("0x{}", log.address); - try_s!(qrc20_addr_from_str(&address)) + try_s!(qtum::contract_addr_from_str(&address)) }; if log.topics.len() != 3 { @@ -1067,8 +1096,3 @@ fn transfer_event_from_log(log: &LogEntry) -> Result String { - let address = qrc20_addr_from_utxo_addr(address); - format!("{:#02x}", address) -} diff --git a/mm2src/coins/qrc20/history.rs b/mm2src/coins/qrc20/history.rs index 6f65c16b8e..692aac9a88 100644 --- a/mm2src/coins/qrc20/history.rs +++ b/mm2src/coins/qrc20/history.rs @@ -11,10 +11,7 @@ use itertools::Itertools; use script_pubkey::{extract_contract_call_from_script, extract_gas_from_script, ExtractGasEnum}; use std::cmp::Ordering; use std::collections::HashMap; -use std::future::Future; use std::io::Cursor; -use std::pin::Pin; -use std::task::{Context, Poll}; use std::thread; use std::time::Duration; use utxo_common::{HISTORY_TOO_LARGE_ERROR, HISTORY_TOO_LARGE_ERR_CODE}; @@ -184,11 +181,7 @@ impl Qrc20Coin { } pub async fn transfer_details_by_hash(&self, tx_hash: H256Json) -> Result { - let electrum = match self.utxo.rpc_client { - UtxoRpcClientEnum::Electrum(ref rpc) => rpc, - UtxoRpcClientEnum::Native(_) => return ERR!("Electrum client expected"), - }; - let receipts = try_s!(electrum.blochchain_transaction_get_receipt(&tx_hash).compat().await); + let receipts = try_s!(self.utxo.rpc_client.get_transaction_receipts(&tx_hash).compat().await); // request Qtum transaction details to get a tx_hex, timestamp, block_height and calculate a miner_fee let qtum_details = try_s!(utxo_common::tx_details_by_hash(self, &tx_hash.0).await); // Deserialize the UtxoTx to get a script pubkey @@ -271,8 +264,8 @@ impl Qrc20Coin { continue; } let amount = try_s!(u256_to_big_decimal(event.amount, self.decimals())); - let from = self.utxo_address_from_qrc20(event.sender); - let to = self.utxo_address_from_qrc20(event.receiver); + let from = self.utxo_addr_from_contract_addr(event.sender); + let to = self.utxo_addr_from_contract_addr(event.receiver); (amount, from, to) }; @@ -298,14 +291,14 @@ impl Qrc20Coin { let my_balance_change = &received_by_me - &spent_by_me; let internal_id = TxInternalId::new(tx_hash.clone(), receipt.output_index, log_index as u64); - let from = if is_sender_contract(&script_pubkey) { - display_contract_address(from) + let from = if is_transferred_from_contract(&script_pubkey) { + qtum::display_as_contract_address(from) } else { try_s!(self.display_address(&from)) }; - let to = if is_receiver_contract(&script_pubkey) { - display_contract_address(to) + let to = if is_transferred_to_contract(&script_pubkey) { + qtum::display_as_contract_address(to) } else { try_s!(self.display_address(&to)) }; @@ -332,12 +325,8 @@ impl Qrc20Coin { fn request_tx_history(&self, metrics: MetricsArc) -> RequestTxHistoryResult { mm_counter!(metrics, "tx.history.request.count", 1, "coin" => self.utxo.ticker.clone(), "client" => "electrum", "method" => "blockchain.contract.event.get_history"); - let history_res = block_on( - HistoryBuilder::new(self.clone()) - .order(HistoryOrder::NewestToOldest) - .build_with_rpc_error(), - ); - let history_cont = match history_res { + 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) => { @@ -359,17 +348,10 @@ impl Qrc20Coin { mm_counter!(metrics, "tx.history.response.count", 1, "coin" => self.utxo.ticker.clone(), "client" => "electrum", "method" => "blockchain.contract.event.get_history"); - mm_counter!(metrics, "tx.history.response.total_length", history_cont.len() as u64, + mm_counter!(metrics, "tx.history.response.total_length", history.len() as u64, "coin" => self.utxo.ticker.clone(), "client" => "electrum", "method" => "blockchain.contract.event.get_history"); - let tx_ids = history_cont - .into_iter() - // electrum can returns multiple `TxHistoryItem` with the same `TxHistoryItem::tx_hash` - // but with the different `TxHistoryItem::log_index` - .unique_by(|item| item.tx_hash.clone()) - .map(|item| (item.tx_hash, item.height as u64)) - .collect(); - RequestTxHistoryResult::Ok(tx_ids) + RequestTxHistoryResult::Ok(history) } fn check_if_history_update_is_needed( @@ -548,217 +530,207 @@ impl Qrc20Coin { } } -pub struct HistoryBuilder { +pub struct TransferHistoryBuilder { coin: Qrc20Coin, + params: TransferHistoryParams, +} + +struct TransferHistoryParams { from_block: u64, - topic: String, address: H160, token_address: H160, - order: Option, -} - -pub struct HistoryCont { - history: Vec, -} - -/// Future for the [`HistoryBuilder::build_utxo_lazy()`]. -/// Loads `UtxoTx` from `tx_hash`. -pub struct UtxoFromHashFuture { - future: Option> + Unpin + 'static>>, -} - -pub enum HistoryOrder { - NewestToOldest, - OldestToNewest, } -impl HistoryBuilder { - pub fn new(coin: Qrc20Coin) -> HistoryBuilder { - let address = qrc20_addr_from_utxo_addr(coin.utxo.my_address.clone()); +impl TransferHistoryBuilder { + pub fn new(coin: Qrc20Coin) -> TransferHistoryBuilder { + let address = qtum::contract_addr_from_utxo_addr(coin.utxo.my_address.clone()); let token_address = coin.contract_address; - HistoryBuilder { - coin, + let params = TransferHistoryParams { from_block: 0, - topic: QRC20_TRANSFER_TOPIC.to_string(), address, token_address, - order: None, - } + }; + TransferHistoryBuilder { coin, params } } #[allow(clippy::wrong_self_convention)] - pub fn from_block(mut self, from_block: u64) -> HistoryBuilder { - self.from_block = from_block; - self - } - - #[allow(dead_code)] - pub fn topic(mut self, topic: &str) -> HistoryBuilder { - self.topic = topic.to_string(); + pub fn from_block(mut self, from_block: u64) -> TransferHistoryBuilder { + self.params.from_block = from_block; self } - pub fn address(mut self, address: H160) -> HistoryBuilder { - self.address = address; + pub fn address(mut self, address: H160) -> TransferHistoryBuilder { + self.params.address = address; self } #[allow(dead_code)] - pub fn token_address(mut self, token_address: H160) -> HistoryBuilder { - self.token_address = token_address; + pub fn token_address(mut self, token_address: H160) -> TransferHistoryBuilder { + self.params.token_address = token_address; self } - pub fn order(mut self, order: HistoryOrder) -> HistoryBuilder { - self.order = Some(order); - self + pub async fn build(self) -> Result, JsonRpcError> { + self.coin.utxo.rpc_client.build(self.params).await } - pub async fn build(self) -> Result, String> { - self.build_with_rpc_error().await.map_err(|e| ERRL!("{}", e)) + pub async fn build_tx_idents(self) -> Result, JsonRpcError> { + self.coin.utxo.rpc_client.build_tx_idents(self.params).await } +} - pub async fn build_with_rpc_error(self) -> Result, JsonRpcError> { - let electrum = match self.coin.utxo.rpc_client { - UtxoRpcClientEnum::Electrum(ref rpc_cln) => rpc_cln, - UtxoRpcClientEnum::Native(_) => panic!("Native mode doesn't support"), - }; +#[async_trait] +trait BuildTransferHistory { + async fn build(&self, params: TransferHistoryParams) -> Result, JsonRpcError>; - let address = qrc20_addr_into_rpc_format(&self.address); - let token_address = qrc20_addr_into_rpc_format(&self.token_address); - let mut history = electrum - .blockchain_contract_event_get_history(&address, &token_address, &self.topic) - .compat() - .await?; + async fn build_tx_idents(&self, params: TransferHistoryParams) -> Result, JsonRpcError>; +} - if self.from_block != 0 { - history = history - .into_iter() - .filter(|item| self.from_block <= item.height) - .collect(); +#[async_trait] +impl BuildTransferHistory for UtxoRpcClientEnum { + async fn build(&self, params: TransferHistoryParams) -> Result, JsonRpcError> { + match self { + UtxoRpcClientEnum::Native(native) => native.build(params).await, + UtxoRpcClientEnum::Electrum(electrum) => electrum.build(params).await, } + } - match self.order { - Some(HistoryOrder::NewestToOldest) => { - history.sort_unstable_by(|a, b| sort_newest_to_oldest(a.height, b.height)) - }, - Some(HistoryOrder::OldestToNewest) => { - history.sort_unstable_by(|a, b| sort_oldest_to_newest(a.height, b.height)) - }, - None => (), + async fn build_tx_idents(&self, params: TransferHistoryParams) -> Result, JsonRpcError> { + match self { + UtxoRpcClientEnum::Native(native) => native.build_tx_idents(params).await, + UtxoRpcClientEnum::Electrum(electrum) => electrum.build_tx_idents(params).await, + } + } +} + +#[async_trait] +impl BuildTransferHistory for ElectrumClient { + async fn build(&self, params: TransferHistoryParams) -> Result, JsonRpcError> { + let tx_idents = self.build_tx_idents(params).await?; + + let mut receipts = Vec::new(); + for (tx_hash, _height) in tx_idents { + let mut tx_receipts = self.blochchain_transaction_get_receipt(&tx_hash).compat().await?; + // remove receipts of contract calls didn't emit at least one `Transfer` event + tx_receipts.retain(|receipt| receipt.log.iter().any(is_transfer_event_log)); + receipts.extend(tx_receipts.into_iter()); } - Ok(HistoryCont { history }) + Ok(receipts) } - /// Request history by `tx_hash` and wrap it into list of UtxoLazyFuture. - /// This method is used when there is no reason to load all `UtxoTx`, - /// only the necessary ones. - /// - /// In particular this is used to load `UtxoTx` until the wanted tx is found. - /// See [`FindLazy::find_lazy()`] and [`FindMapLazy::find_map_lazy()`]. - pub async fn build_utxo_lazy(self) -> Result, String> { - let coin = self.coin.clone(); - let cont = try_s!(self.build().await); + async fn build_tx_idents(&self, params: TransferHistoryParams) -> Result, JsonRpcError> { + let address = contract_addr_into_rpc_format(¶ms.address); + let token_address = contract_addr_into_rpc_format(¶ms.token_address); + let history = self + .blockchain_contract_event_get_history(&address, &token_address, QRC20_TRANSFER_TOPIC) + .compat() + .await?; - let history = cont + Ok(history .into_iter() - .unique_by(|tx| tx.tx_hash.clone()) - .map(|item| UtxoFromHashFuture::new(coin.clone(), item.tx_hash)) - .collect(); - - Ok(HistoryCont { history }) + .filter(|item| params.from_block <= item.height) + .map(|tx| (tx.tx_hash, tx.height)) + .unique() + .collect()) } } -impl HistoryCont { - #[allow(dead_code)] - pub fn into_vec(self) -> Vec { self.history } +#[async_trait] +impl BuildTransferHistory for NativeClient { + async fn build(&self, params: TransferHistoryParams) -> Result, JsonRpcError> { + const SEARCH_LOGS_STEP: u64 = 100; + + let token_address = contract_addr_into_rpc_format(¶ms.token_address); + let address_topic = address_to_log_topic(¶ms.address); + + // «Skip the log if none of the topics are matched» + // https://github.com/qtumproject/qtum-enterprise/blob/qtumx_beta_0.16.0/src/rpc/blockchain.cpp#L1590 + // + // It means disjunction of topics (binary `OR`). + // So we cannot specify `Transfer` event signature in the first topic, + // but we can specify either `sender` or `receiver` in `Transfer` event. + let topics = vec![ + TopicFilter::Skip, // event signature + TopicFilter::Match(address_topic.clone()), // `sender` address in `Transfer` event + TopicFilter::Match(address_topic.clone()), // `receiver` address in `Transfer` event + ]; - pub fn into_iter(self) -> impl Iterator { self.history.into_iter() } + let block_count = self.get_block_count().compat().await?; - pub fn len(&self) -> usize { self.history.len() } -} + let mut result = Vec::new(); + let mut from_block = params.from_block; + while from_block <= block_count { + let to_block = from_block + SEARCH_LOGS_STEP - 1; + let mut receipts = self + .search_logs(from_block, Some(to_block), vec![token_address.clone()], topics.clone()) + .compat() + .await?; -impl UtxoFromHashFuture { - /// Create the future. - fn new(coin: Qrc20Coin, tx_hash: H256Json) -> UtxoFromHashFuture { - let fut = async move { - let electrum = match coin.utxo.rpc_client { - UtxoRpcClientEnum::Electrum(ref rpc_cln) => rpc_cln, - UtxoRpcClientEnum::Native(_) => panic!("Native mode doesn't support"), - }; + // remove receipts of transaction that didn't emit at least one `Transfer` event + receipts.retain(|receipt| receipt.log.iter().any(is_transfer_event_log)); - let verbose_tx = try_s!(electrum.get_verbose_transaction(tx_hash).compat().await); - let utxo_tx: UtxoTx = deserialize(verbose_tx.hex.as_slice()).map_err(|e| ERRL!("{:?}", e))?; - Ok(utxo_tx) - }; - UtxoFromHashFuture { - future: Some(Box::new(fut.boxed())), + result.extend(receipts.into_iter()); + from_block += SEARCH_LOGS_STEP; } + Ok(result) } -} -unsafe impl Send for UtxoFromHashFuture {} - -impl Future for UtxoFromHashFuture { - type Output = Result; - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let mut fut = self.future.take().expect("cannot poll UtxoLazyFuture twice"); - if let Poll::Ready(result) = fut.poll_unpin(cx) { - return Poll::Ready(result); - } - self.future = Some(fut); - Poll::Pending + async fn build_tx_idents(&self, params: TransferHistoryParams) -> Result, JsonRpcError> { + let receipts = self.build(params).await?; + Ok(receipts + .into_iter() + .map(|receipt| (receipt.transaction_hash, receipt.block_number)) + .unique() + .collect()) } } -fn is_sender_contract(script_pubkey: &Script) -> bool { +fn is_transferred_from_contract(script_pubkey: &Script) -> bool { let contract_call_bytes = match extract_contract_call_from_script(&script_pubkey) { Ok(bytes) => bytes, Err(e) => { - log!((e)); + error!("{}", e); return false; }, }; - let call_type = match ContractCallType::from_script_pubkey(&contract_call_bytes) { + let call_type = match MutContractCallType::from_script_pubkey(&contract_call_bytes) { Ok(Some(t)) => t, Ok(None) => return false, Err(e) => { - log!((e)); + error!("{}", e); return false; }, }; match call_type { - ContractCallType::Transfer => false, - ContractCallType::Erc20Payment => false, - ContractCallType::ReceiverSpend => true, - ContractCallType::SenderRefund => true, + MutContractCallType::Transfer => false, + MutContractCallType::Erc20Payment => false, + MutContractCallType::ReceiverSpend => true, + MutContractCallType::SenderRefund => true, } } -fn is_receiver_contract(script_pubkey: &Script) -> bool { +fn is_transferred_to_contract(script_pubkey: &Script) -> bool { let contract_call_bytes = match extract_contract_call_from_script(&script_pubkey) { Ok(bytes) => bytes, Err(e) => { - log!((e)); + error!("{}", e); return false; }, }; - let call_type = match ContractCallType::from_script_pubkey(&contract_call_bytes) { + let call_type = match MutContractCallType::from_script_pubkey(&contract_call_bytes) { Ok(Some(t)) => t, Ok(None) => return false, Err(e) => { - log!((e)); + error!("{}", e); return false; }, }; match call_type { - ContractCallType::Transfer => false, - ContractCallType::Erc20Payment => true, - ContractCallType::ReceiverSpend => false, - ContractCallType::SenderRefund => false, + MutContractCallType::Transfer => false, + MutContractCallType::Erc20Payment => true, + MutContractCallType::ReceiverSpend => false, + MutContractCallType::SenderRefund => false, } } @@ -773,8 +745,11 @@ fn sort_newest_to_oldest(x_height: u64, y_height: u64) -> Ordering { } } -fn sort_oldest_to_newest(x_height: u64, y_height: u64) -> Ordering { - sort_newest_to_oldest(x_height, y_height).reverse() +fn is_transfer_event_log(log: &LogEntry) -> bool { + match log.topics.first() { + Some(first_topic) => first_topic == QRC20_TRANSFER_TOPIC, + None => false, + } } #[cfg(test)] diff --git a/mm2src/coins/qrc20/qrc20_tests.rs b/mm2src/coins/qrc20/qrc20_tests.rs index 026d5d2dd1..ab93a6a428 100644 --- a/mm2src/coins/qrc20/qrc20_tests.rs +++ b/mm2src/coins/qrc20/qrc20_tests.rs @@ -2,11 +2,9 @@ use super::*; use crate::TxFeeDetails; use bigdecimal::Zero; use chain::OutPoint; -use common::executor::spawn; use common::mm_ctx::MmCtxBuilder; use itertools::Itertools; use mocktopus::mocking::{MockResult, Mockable}; -use std::sync::{Arc, Mutex}; pub fn qrc20_coin_for_test(priv_key: &[u8]) -> (MmArc, Qrc20Coin) { let conf = json!({ @@ -101,6 +99,7 @@ fn test_can_i_spend_other_payment() { MockResult::Return(Box::new(futures01::future::ok(balance))) }); + // qfkXE2cNFEwPFQqvBcqs8m9KrkNa9KV4xi let priv_key = [ 192, 240, 176, 226, 14, 170, 226, 96, 107, 47, 166, 243, 154, 48, 28, 243, 18, 144, 240, 1, 79, 103, 178, 42, 32, 161, 106, 119, 241, 227, 42, 102, @@ -134,43 +133,6 @@ fn test_can_i_spend_other_payment_err() { assert!(error.contains("Base coin balance 0.04000999 is too low to cover gas fee, required 0.04001")); } -#[test] -#[ignore] -fn test_send_maker_payment() { - // priv_key of qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG - let priv_key = [ - 3, 98, 177, 3, 108, 39, 234, 144, 131, 178, 103, 103, 127, 80, 230, 166, 53, 68, 147, 215, 42, 216, 144, 72, - 172, 110, 180, 13, 123, 179, 10, 49, - ]; - let (_ctx, coin) = qrc20_coin_for_test(&priv_key); - - let timelock = (now_ms() / 1000) as u32 - 200; - let taker_pub = hex::decode("022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1a").unwrap(); - let secret_hash = &[1; 20]; - let amount = BigDecimal::from_str("0.2").unwrap(); - let payment = coin - .send_maker_payment(timelock, &taker_pub, secret_hash, amount) - .wait() - .unwrap(); - let tx = match payment { - TransactionEnum::UtxoTx(tx) => tx, - _ => panic!("Expected UtxoTx"), - }; - - let tx_hash: H256Json = tx.hash().reversed().into(); - log!([tx_hash]); - let tx_hex = serialize(&tx); - log!("tx_hex: "[tx_hex]); - - let confirmations = 1; - let requires_nota = false; - let wait_until = (now_ms() / 1000) + 240; // timeout if test takes more than 240 seconds to run - let check_every = 1; - unwrap!(coin - .wait_for_confirmations(&tx_hex, confirmations, requires_nota, wait_until, check_every) - .wait()); -} - #[test] fn test_validate_maker_payment() { // this priv_key corresponds to "taker_passphrase" passphrase @@ -191,12 +153,26 @@ fn test_validate_maker_payment() { let amount = BigDecimal::from_str("0.2").unwrap(); unwrap!(coin - .validate_maker_payment(&payment_tx, time_lock, &maker_pub, secret_hash, amount.clone()) + .validate_maker_payment( + &payment_tx, + time_lock, + &maker_pub, + secret_hash, + amount.clone(), + &coin.swap_contract_address() + ) .wait()); let maker_pub_dif = hex::decode("022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1a").unwrap(); let error = unwrap!(coin - .validate_maker_payment(&payment_tx, time_lock, &maker_pub_dif, secret_hash, amount.clone()) + .validate_maker_payment( + &payment_tx, + time_lock, + &maker_pub_dif, + secret_hash, + amount.clone(), + &coin.swap_contract_address() + ) .wait() .err()); log!("error: "[error]); @@ -206,7 +182,14 @@ fn test_validate_maker_payment() { let amount_dif = BigDecimal::from_str("0.3").unwrap(); let error = unwrap!(coin - .validate_maker_payment(&payment_tx, time_lock, &maker_pub, secret_hash, amount_dif) + .validate_maker_payment( + &payment_tx, + time_lock, + &maker_pub, + secret_hash, + amount_dif, + &coin.swap_contract_address() + ) .wait() .err()); log!("error: "[error]); @@ -214,7 +197,14 @@ fn test_validate_maker_payment() { let secret_hash_dif = &[2; 20]; let error = unwrap!(coin - .validate_maker_payment(&payment_tx, time_lock, &maker_pub, secret_hash_dif, amount.clone()) + .validate_maker_payment( + &payment_tx, + time_lock, + &maker_pub, + secret_hash_dif, + amount.clone(), + &coin.swap_contract_address() + ) .wait() .err()); log!("error: "[error]); @@ -222,7 +212,14 @@ fn test_validate_maker_payment() { let time_lock_dif = 123; let error = unwrap!(coin - .validate_maker_payment(&payment_tx, time_lock_dif, &maker_pub, secret_hash, amount) + .validate_maker_payment( + &payment_tx, + time_lock_dif, + &maker_pub, + secret_hash, + amount, + &coin.swap_contract_address() + ) .wait() .err()); log!("error: "[error]); @@ -273,302 +270,6 @@ fn test_wait_for_confirmations_excepted() { assert!(error.contains("Contract call failed with an error: Revert")); } -#[test] -#[ignore] -fn test_taker_spends_maker_payment() { - // priv_key of qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG - let priv_key = [ - 3, 98, 177, 3, 108, 39, 234, 144, 131, 178, 103, 103, 127, 80, 230, 166, 53, 68, 147, 215, 42, 216, 144, 72, - 172, 110, 180, 13, 123, 179, 10, 49, - ]; - let (_ctx, maker_coin) = qrc20_coin_for_test(&priv_key); - - // priv_key of qUX9FGHubczidVjWPCUWuwCUJWpkAtGCgf - let priv_key = [ - 24, 181, 194, 193, 18, 152, 142, 168, 71, 73, 70, 244, 9, 101, 92, 168, 243, 61, 132, 48, 25, 39, 103, 92, 29, - 17, 11, 29, 113, 235, 48, 70, - ]; - let (_ctx, taker_coin) = qrc20_coin_for_test(&priv_key); - - let bob_balance = taker_coin.my_balance().wait().unwrap(); - - let timelock = (now_ms() / 1000) as u32 - 200; - // pubkey of "taker_passphrase" passphrase and qUX9FGHubczidVjWPCUWuwCUJWpkAtGCgf address - let taker_pub = hex::decode("022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1a").unwrap(); - // pubkey of "cMhHM3PMpMrChygR4bLF7QsTdenhWpFrrmf2UezBG3eeFsz41rtL" passphrase - let maker_pub = hex::decode("03693bff1b39e8b5a306810023c29b95397eb395530b106b1820ea235fd81d9ce9").unwrap(); - let secret = &[1; 32]; - let secret_hash = &*dhash160(secret); - let amount = BigDecimal::from_str("0.2").unwrap(); - let payment = maker_coin - .send_maker_payment(timelock, &taker_pub, secret_hash, amount.clone()) - .wait() - .unwrap(); - let tx = match payment { - TransactionEnum::UtxoTx(tx) => tx, - _ => panic!("Expected UtxoTx"), - }; - - let payment_tx_hash: H256Json = tx.hash().reversed().into(); - log!("Maker payment: "[payment_tx_hash]); - let tx_hex = serialize(&tx); - - let confirmations = 1; - let requires_nota = false; - let wait_until = (now_ms() / 1000) + 320; // timeout if test takes more than 320 seconds to run - let check_every = 1; - unwrap!(taker_coin - .wait_for_confirmations(&tx_hex, confirmations, requires_nota, wait_until, check_every) - .wait()); - - unwrap!(taker_coin - .validate_maker_payment(&tx_hex, timelock, &maker_pub, secret_hash, amount.clone()) - .wait()); - - let spend = unwrap!(taker_coin - .send_taker_spends_maker_payment(&tx_hex, timelock, &maker_pub, secret) - .wait()); - let spend_tx = match spend { - TransactionEnum::UtxoTx(tx) => tx, - _ => panic!("Expected UtxoTx"), - }; - - let spend_tx_hash: H256Json = spend_tx.hash().reversed().into(); - log!("Taker spends tx: "[spend_tx_hash]); - let spend_tx_hex = serialize(&spend_tx); - let wait_until = (now_ms() / 1000) + 240; // timeout if test takes more than 240 seconds to run - unwrap!(taker_coin - .wait_for_confirmations(&spend_tx_hex, confirmations, requires_nota, wait_until, check_every) - .wait()); - - let bob_new_balance = taker_coin.my_balance().wait().unwrap(); - assert_eq!(bob_balance + amount, bob_new_balance); -} - -#[test] -#[ignore] -fn test_maker_spends_taker_payment() { - // priv_key of qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG - let priv_key = [ - 3, 98, 177, 3, 108, 39, 234, 144, 131, 178, 103, 103, 127, 80, 230, 166, 53, 68, 147, 215, 42, 216, 144, 72, - 172, 110, 180, 13, 123, 179, 10, 49, - ]; - let (_ctx, maker_coin) = qrc20_coin_for_test(&priv_key); - - // priv_key of qUX9FGHubczidVjWPCUWuwCUJWpkAtGCgf - let priv_key = [ - 24, 181, 194, 193, 18, 152, 142, 168, 71, 73, 70, 244, 9, 101, 92, 168, 243, 61, 132, 48, 25, 39, 103, 92, 29, - 17, 11, 29, 113, 235, 48, 70, - ]; - let (_ctx, taker_coin) = qrc20_coin_for_test(&priv_key); - - let maker_balance = maker_coin.my_balance().wait().unwrap(); - - let timelock = (now_ms() / 1000) as u32 - 200; - // pubkey of "taker_passphrase" passphrase and qUX9FGHubczidVjWPCUWuwCUJWpkAtGCgf address - let taker_pub = hex::decode("022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1a").unwrap(); - // pubkey of "cMhHM3PMpMrChygR4bLF7QsTdenhWpFrrmf2UezBG3eeFsz41rtL" passphrase - let maker_pub = hex::decode("03693bff1b39e8b5a306810023c29b95397eb395530b106b1820ea235fd81d9ce9").unwrap(); - let secret = &[1; 32]; - let secret_hash = &*dhash160(secret); - let amount = BigDecimal::from_str("0.2").unwrap(); - let payment = taker_coin - .send_taker_payment(timelock, &maker_pub, secret_hash, amount.clone()) - .wait() - .unwrap(); - let tx = match payment { - TransactionEnum::UtxoTx(tx) => tx, - _ => panic!("Expected UtxoTx"), - }; - - let payment_tx_hash: H256Json = tx.hash().reversed().into(); - log!("Maker payment: "[payment_tx_hash]); - let tx_hex = serialize(&tx); - - let confirmations = 1; - let requires_nota = false; - let wait_until = (now_ms() / 1000) + 320; // timeout if test takes more than 320 seconds to run - let check_every = 1; - unwrap!(maker_coin - .wait_for_confirmations(&tx_hex, confirmations, requires_nota, wait_until, check_every) - .wait()); - - unwrap!(maker_coin - .validate_taker_payment(&tx_hex, timelock, &taker_pub, secret_hash, amount.clone()) - .wait()); - - let spend = unwrap!(maker_coin - .send_maker_spends_taker_payment(&tx_hex, timelock, &taker_pub, secret) - .wait()); - let spend_tx = match spend { - TransactionEnum::UtxoTx(tx) => tx, - _ => panic!("Expected UtxoTx"), - }; - - let spend_tx_hash: H256Json = spend_tx.hash().reversed().into(); - log!("Taker spends tx: "[spend_tx_hash]); - let spend_tx_hex = serialize(&spend_tx); - let wait_until = (now_ms() / 1000) + 240; // timeout if test takes more than 240 seconds to run - unwrap!(maker_coin - .wait_for_confirmations(&spend_tx_hex, confirmations, requires_nota, wait_until, check_every) - .wait()); - - let maker_new_balance = maker_coin.my_balance().wait().unwrap(); - assert_eq!(maker_balance + amount, maker_new_balance); -} - -#[test] -#[ignore] -fn test_maker_refunds_payment() { - // priv_key of qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG - let priv_key = [ - 3, 98, 177, 3, 108, 39, 234, 144, 131, 178, 103, 103, 127, 80, 230, 166, 53, 68, 147, 215, 42, 216, 144, 72, - 172, 110, 180, 13, 123, 179, 10, 49, - ]; - let (_ctx, coin) = qrc20_coin_for_test(&priv_key); - - let expected_balance = unwrap!(coin.my_balance().wait()); - - let timelock = (now_ms() / 1000) as u32 - 200; - // pubkey of "taker_passphrase" passphrase and qUX9FGHubczidVjWPCUWuwCUJWpkAtGCgf address - let taker_pub = hex::decode("022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1a").unwrap(); - let secret_hash = &[1; 20]; - let amount = BigDecimal::from_str("0.2").unwrap(); - let payment = coin - .send_maker_payment(timelock, &taker_pub, secret_hash, amount.clone()) - .wait() - .unwrap(); - let tx = match payment { - TransactionEnum::UtxoTx(tx) => tx, - _ => panic!("Expected UtxoTx"), - }; - - let payment_tx_hash: H256Json = tx.hash().reversed().into(); - log!("Maker payment: "[payment_tx_hash]); - let tx_hex = serialize(&tx); - - let confirmations = 1; - let requires_nota = false; - let wait_until = (now_ms() / 1000) + 320; // timeout if test takes more than 320 seconds to run - let check_every = 1; - unwrap!(coin - .wait_for_confirmations(&tx_hex, confirmations, requires_nota, wait_until, check_every) - .wait()); - - let balance_after_payment = unwrap!(coin.my_balance().wait()); - assert_eq!(expected_balance.clone() - amount, balance_after_payment); - - let refund = unwrap!(coin - .send_maker_refunds_payment(&tx_hex, timelock, &taker_pub, secret_hash) - .wait()); - let refund_tx = match refund { - TransactionEnum::UtxoTx(tx) => tx, - _ => panic!("Expected UtxoTx"), - }; - - let refund_tx_hash: H256Json = refund_tx.hash().reversed().into(); - log!("Taker spends tx: "[refund_tx_hash]); - let refund_tx_hex = serialize(&refund_tx); - let wait_until = (now_ms() / 1000) + 240; // timeout if test takes more than 240 seconds to run - unwrap!(coin - .wait_for_confirmations(&refund_tx_hex, confirmations, requires_nota, wait_until, check_every) - .wait()); - - let balance_after_refund = unwrap!(coin.my_balance().wait()); - assert_eq!(expected_balance, balance_after_refund); -} - -#[test] -#[ignore] -fn test_taker_refunds_payment() { - // priv_key of qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG - let priv_key = [ - 3, 98, 177, 3, 108, 39, 234, 144, 131, 178, 103, 103, 127, 80, 230, 166, 53, 68, 147, 215, 42, 216, 144, 72, - 172, 110, 180, 13, 123, 179, 10, 49, - ]; - let (_ctx, coin) = qrc20_coin_for_test(&priv_key); - - let expected_balance = unwrap!(coin.my_balance().wait()); - - let timelock = (now_ms() / 1000) as u32 - 200; - // pubkey of "taker_passphrase" passphrase and qUX9FGHubczidVjWPCUWuwCUJWpkAtGCgf address - let maker_pub = hex::decode("022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1a").unwrap(); - let secret_hash = &[1; 20]; - let amount = BigDecimal::from_str("0.2").unwrap(); - let payment = coin - .send_taker_payment(timelock, &maker_pub, secret_hash, amount.clone()) - .wait() - .unwrap(); - let tx = match payment { - TransactionEnum::UtxoTx(tx) => tx, - _ => panic!("Expected UtxoTx"), - }; - - let payment_tx_hash: H256Json = tx.hash().reversed().into(); - log!("Maker payment: "[payment_tx_hash]); - let tx_hex = serialize(&tx); - - let confirmations = 1; - let requires_nota = false; - let wait_until = (now_ms() / 1000) + 320; // timeout if test takes more than 320 seconds to run - let check_every = 1; - unwrap!(coin - .wait_for_confirmations(&tx_hex, confirmations, requires_nota, wait_until, check_every) - .wait()); - - let balance_after_payment = unwrap!(coin.my_balance().wait()); - assert_eq!(expected_balance.clone() - amount, balance_after_payment); - - let refund = unwrap!(coin - .send_taker_refunds_payment(&tx_hex, timelock, &maker_pub, secret_hash) - .wait()); - let refund_tx = match refund { - TransactionEnum::UtxoTx(tx) => tx, - _ => panic!("Expected UtxoTx"), - }; - - let refund_tx_hash: H256Json = refund_tx.hash().reversed().into(); - log!("Taker spends tx: "[refund_tx_hash]); - let refund_tx_hex = serialize(&refund_tx); - let wait_until = (now_ms() / 1000) + 240; // timeout if test takes more than 240 seconds to run - unwrap!(coin - .wait_for_confirmations(&refund_tx_hex, confirmations, requires_nota, wait_until, check_every) - .wait()); - - let balance_after_refund = unwrap!(coin.my_balance().wait()); - assert_eq!(expected_balance, balance_after_refund); -} - -#[test] -fn test_check_if_my_payment_sent() { - // priv_key of qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG - let priv_key = [ - 3, 98, 177, 3, 108, 39, 234, 144, 131, 178, 103, 103, 127, 80, 230, 166, 53, 68, 147, 215, 42, 216, 144, 72, - 172, 110, 180, 13, 123, 179, 10, 49, - ]; - let (_ctx, coin) = qrc20_coin_for_test(&priv_key); - - let time_lock = 1601367157; - // pubkey of "cMhHM3PMpMrChygR4bLF7QsTdenhWpFrrmf2UezBG3eeFsz41rtL" passphrase - let maker_pub = hex::decode("03693bff1b39e8b5a306810023c29b95397eb395530b106b1820ea235fd81d9ce9").unwrap(); - let secret_hash = &[1; 20]; - // search from b22ee034e860d89af6e76e54bb7f8efb69d833a8670e61c60e5dfdfaa27db371 transaction - let search_from_block = 686125; - - // tx_hash: 016a59dd2b181b3906b0f0333d5c7561dacb332dc99ac39679a591e523f2c49a - let expected_tx = TransactionEnum::UtxoTx("010000000194448324c14fc6b78c7a52c59debe3240fc392019dbd6f1457422e3308ce1e75010000006b483045022100800a4956a30a36708536d98e8ea55a3d0983b963af6c924f60241616e2ff056d0220239e622f8ec8f1a0f5ef0fc93ff094a8e6b5aab964a62bed680b17bf6a848aac012103693bff1b39e8b5a306810023c29b95397eb395530b106b1820ea235fd81d9ce9ffffffff020000000000000000e35403a0860101284cc49b415b2a0c692f2ec8ebab181a79e31b7baab30fef0902e57f901c47a342643eeafa6b510000000000000000000000000000000000000000000000000000000001312d00000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc44000000000000000000000000783cf0be521101942da509846ea476e683aad8320101010101010101010101010101010101010101000000000000000000000000000000000000000000000000000000000000000000000000000000005f72ec7514ba8b71f3544b93e2f681f996da519a98ace0107ac201319302000000001976a9149e032d4b0090a11dc40fe6c47601499a35d55fbb88ac40ed725f".into()); - let tx = unwrap!(coin - .check_if_my_payment_sent(time_lock, &maker_pub, secret_hash, search_from_block) - .wait()); - assert_eq!(tx, Some(expected_tx)); - - let time_lock_dif = 1601367156; - let tx = unwrap!(coin - .check_if_my_payment_sent(time_lock_dif, &maker_pub, secret_hash, search_from_block) - .wait()); - assert_eq!(tx, None); -} - #[test] fn test_send_taker_fee() { // priv_key of qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG @@ -638,146 +339,6 @@ fn test_validate_fee() { assert!(err.contains("Expected 'transfer' contract call")); } -#[test] -#[ignore] -fn test_search_for_swap_tx_spend() { - // priv_key of qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG - let priv_key = [ - 3, 98, 177, 3, 108, 39, 234, 144, 131, 178, 103, 103, 127, 80, 230, 166, 53, 68, 147, 215, 42, 216, 144, 72, - 172, 110, 180, 13, 123, 179, 10, 49, - ]; - let (_ctx, coin) = qrc20_coin_for_test(&priv_key); - - let other_pub = &[0]; //ignored - let search_from_block = 693000; - - // taker spent maker payment - d3f5dab4d54c14b3d7ed8c7f5c8cc7f47ccf45ce589fdc7cd5140a3c1c3df6e1 - let expected = Ok(Some(FoundSwapTxSpend::Spent(TransactionEnum::UtxoTx("01000000033f56ecafafc8602fde083ba868d1192d6649b8433e42e1a2d79ba007ea4f7abb010000006b48304502210093404e90e40d22730013035d31c404c875646dcf2fad9aa298348558b6d65ba60220297d045eac5617c1a3eddb71d4bca9772841afa3c4c9d6c68d8d2d42ee6de3950121022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1affffffff9cac7fe90d597922a1d92e05306c2215628e7ea6d5b855bfb4289c2944f4c73a030000006b483045022100b987da58c2c0c40ce5b6ef2a59e8124ed4ef7a8b3e60c7fb631139280019bc93022069649bcde6fe4dd5df9462a1fcae40598488d6af8c324cd083f5c08afd9568be0121022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1affffffff70b9870f2b0c65d220a839acecebf80f5b44c3ca4c982fa2fdc5552c037f5610010000006a473044022071b34dd3ebb72d29ca24f3fa0fc96571c815668d3b185dd45cc46a7222b6843f02206c39c030e618d411d4124f7b3e7ca1dd5436775bd8083a85712d123d933a51300121022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1affffffff020000000000000000c35403a0860101284ca402ed292b806a1835a1b514ad643f2acdb5c8db6b6a9714accff3275ea0d79a3f23be8fd00000000000000000000000000000000000000000000000000000000001312d000101010101010101010101010101010101010101010101010101010101010101000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc440000000000000000000000009e032d4b0090a11dc40fe6c47601499a35d55fbb14ba8b71f3544b93e2f681f996da519a98ace0107ac2c02288d4010000001976a914783cf0be521101942da509846ea476e683aad83288ac0f047f5f".into())))); - // maker sent payment - c8112c75be039100c30d71293571f081e189540818ef8e2903ff75d2d556b446 - let tx_hex = hex::decode("0100000001e6b256dd9d390be2ccd8eddaf67a40d1994a983845fb223c102ce8e58eca2b48010000006b4830450221008e8e793ad00ed1d45f4546b9e7b9dc8305d61c384e126c24e7945bd0056df099022077f033cf16535f0d3627548196cd3868d904ca6ccac9d80d56f1f70df6589915012103693bff1b39e8b5a306810023c29b95397eb395530b106b1820ea235fd81d9ce9ffffffff020000000000000000e35403a0860101284cc49b415b2a806a1835a1b514ad643f2acdb5c8db6b6a9714accff3275ea0d79a3f23be8fd00000000000000000000000000000000000000000000000000000000001312d00000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc44000000000000000000000000783cf0be521101942da509846ea476e683aad8324b6b2e5444c2639cc0fb7bcea5afba3f3cdce239000000000000000000000000000000000000000000000000000000000000000000000000000000005f7f02c014ba8b71f3544b93e2f681f996da519a98ace0107ac27046a001000000001976a9149e032d4b0090a11dc40fe6c47601499a35d55fbb88ac8b037f5f").unwrap(); - let timelock = 1602159296; - let secret = &[1; 32]; - let secret_hash = &*dhash160(secret); - let actual = coin.search_for_swap_tx_spend_my(timelock, other_pub, secret_hash, &tx_hex, search_from_block); - assert_eq!(actual, expected); - - // maker refunded payment his - df41079d58a13320590476e648d37007459366b0fbfce8d0b72fae502e39cc01 - let expected = Ok(Some(FoundSwapTxSpend::Refunded(TransactionEnum::UtxoTx("010000000191999480813e0284212d08a16b32146e7d32315feaf6489cd3aa696b54e5ce71010000006a4730440220282a32f05a4802caee065ee8d2b08a9b366c26b16d9afb068b3259aa54107b0e0220039c7697620e91096d566ddb6056ad347c395584114f790a2a727db86789c576012103693bff1b39e8b5a306810023c29b95397eb395530b106b1820ea235fd81d9ce9ffffffff020000000000000000c35403a0860101284ca446fc0294796332096ae329d7aa84c52f036bbeb9dd4b872c8d2021ccb8775e23f56a422e0000000000000000000000000000000000000000000000000000000001312d000101010101010101010101010101010101010101000000000000000000000000000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc44000000000000000000000000783cf0be521101942da509846ea476e683aad83214ba8b71f3544b93e2f681f996da519a98ace0107ac2d012ac00000000001976a9149e032d4b0090a11dc40fe6c47601499a35d55fbb88ac97067f5f".into())))); - // maker sent payment - 71cee5546b69aad39c48f6ea5f31327d6e14326ba1082d2184023e8180949991 - let tx_hex = hex::decode("0100000001422dd62a9405fbda1f0e01ed45917cd908a68258a5f5530a1f53c4cd173bc82b010000006a47304402201c2c3b789a651143a657217b5b459027b68a78545a5036e03f90bacbc4cfd8b1022055200a3da6b208dc8763471a87d869d6b045f1dd38f855b0fda0b526f23f88ea012103693bff1b39e8b5a306810023c29b95397eb395530b106b1820ea235fd81d9ce9ffffffff020000000000000000e35403a0860101284cc49b415b2a796332096ae329d7aa84c52f036bbeb9dd4b872c8d2021ccb8775e23f56a422e0000000000000000000000000000000000000000000000000000000001312d00000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc44000000000000000000000000783cf0be521101942da509846ea476e683aad8320101010101010101010101010101010101010101000000000000000000000000000000000000000000000000000000000000000000000000000000005f7f059f14ba8b71f3544b93e2f681f996da519a98ace0107ac2b81fe900000000001976a9149e032d4b0090a11dc40fe6c47601499a35d55fbb88ac6a067f5f").unwrap(); - let timelock = 1602160031; - let secret_hash = &[1; 20]; - let actual = coin.search_for_swap_tx_spend_my(timelock, other_pub, secret_hash, &tx_hex, search_from_block); - assert_eq!(actual, expected); - - // maker payment hasn't been spent or refunded yet - let expected = Ok(None); - // maker sent payment 9fae1771bb542f9860d845091109a6a951f95fc277faebe3ec6ab3e8df9e58b6 - let tx_hex = hex::decode("010000000101cc392e50ae2fb7d0e8fcfbb06693450770d348e67604592033a1589d0741df010000006b483045022100935cf73d2b01a694f4383eb844d5e93e041496b13e6bdf1f7a8f3bb8dd83b50002204952184584460cc1ab979895ec4850ea9e26a7308d231376fc21c133c7eeaf08012103693bff1b39e8b5a306810023c29b95397eb395530b106b1820ea235fd81d9ce9ffffffff020000000000000000e35403a0860101284cc49b415b2a4357ff815e6657ea5b4cf992475e29940b3a4cda9b589d5e5061bb06c1f5bf5a0000000000000000000000000000000000000000000000000000000001312d00000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc44000000000000000000000000783cf0be521101942da509846ea476e683aad8320101010101010101010101010101010101010101000000000000000000000000000000000000000000000000000000000000000000000000000000005f7f066014ba8b71f3544b93e2f681f996da519a98ace0107ac2e8056f00000000001976a9149e032d4b0090a11dc40fe6c47601499a35d55fbb88ac2b077f5f").unwrap(); - let timelock = 1602160224; - let secret_hash = &[1; 20]; - let actual = coin.search_for_swap_tx_spend_my(timelock, other_pub, secret_hash, &tx_hex, search_from_block); - assert_eq!(actual, expected); -} - -#[test] -#[ignore] -fn test_wait_for_tx_spend() { - // priv_key of qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG - let priv_key = [ - 3, 98, 177, 3, 108, 39, 234, 144, 131, 178, 103, 103, 127, 80, 230, 166, 53, 68, 147, 215, 42, 216, 144, 72, - 172, 110, 180, 13, 123, 179, 10, 49, - ]; - let (_ctx, maker_coin) = qrc20_coin_for_test(&priv_key); - - // priv_key of qUX9FGHubczidVjWPCUWuwCUJWpkAtGCgf - let priv_key = [ - 24, 181, 194, 193, 18, 152, 142, 168, 71, 73, 70, 244, 9, 101, 92, 168, 243, 61, 132, 48, 25, 39, 103, 92, 29, - 17, 11, 29, 113, 235, 48, 70, - ]; - let (_ctx, taker_coin) = qrc20_coin_for_test(&priv_key); - - let from_block = maker_coin.current_block().wait().unwrap(); - - let timelock = (now_ms() / 1000) as u32 - 200; - // pubkey of "taker_passphrase" passphrase and qUX9FGHubczidVjWPCUWuwCUJWpkAtGCgf address - let taker_pub = hex::decode("022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1a").unwrap(); - // pubkey of "cMhHM3PMpMrChygR4bLF7QsTdenhWpFrrmf2UezBG3eeFsz41rtL" passphrase - let maker_pub = hex::decode("03693bff1b39e8b5a306810023c29b95397eb395530b106b1820ea235fd81d9ce9").unwrap(); - let secret = &[1; 32]; - let secret_hash = &*dhash160(secret); - let amount = BigDecimal::from_str("0.2").unwrap(); - let payment = maker_coin - .send_maker_payment(timelock, &taker_pub, secret_hash, amount.clone()) - .wait() - .unwrap(); - let tx = match payment { - TransactionEnum::UtxoTx(tx) => tx, - _ => panic!("Expected UtxoTx"), - }; - - let payment_tx_hash: H256Json = tx.hash().reversed().into(); - log!("Maker payment: "[payment_tx_hash]); - let tx_hex = serialize(&tx); - - let confirmations = 1; - let requires_nota = false; - let wait_until = (now_ms() / 1000) + 320; // timeout if test takes more than 320 seconds to run - let check_every = 1; - unwrap!(taker_coin - .wait_for_confirmations(&tx_hex, confirmations, requires_nota, wait_until, check_every) - .wait()); - - unwrap!(taker_coin - .validate_maker_payment(&tx_hex, timelock, &maker_pub, secret_hash, amount.clone()) - .wait()); - - // first try to check if the wait_for_tx_spend() returns an error correctly - let wait_until = (now_ms() / 1000) + 11; - let err = maker_coin - .wait_for_tx_spend(&tx_hex, wait_until, from_block) - .wait() - .expect_err("Expected 'Waited too long' error"); - log!("error: "[err]); - assert!(err.contains("Waited too long")); - - // also spends the maker payment and try to check if the wait_for_tx_spend() returns the correct tx - let spend_tx: Arc>> = Arc::new(Mutex::new(None)); - - let tx_hex_c = tx_hex.clone(); - let spend_tx_c = spend_tx.clone(); - let fut = async move { - Timer::sleep(11.).await; - - let spend = unwrap!( - taker_coin - .send_taker_spends_maker_payment(&tx_hex_c, timelock, &maker_pub, secret) - .compat() - .await - ); - let mut lock = spend_tx_c.lock().unwrap(); - match spend { - TransactionEnum::UtxoTx(tx) => *lock = Some(tx), - _ => panic!("Expected UtxoTx"), - } - }; - - spawn(fut); - - let wait_until = (now_ms() / 1000) + 320; - let found = unwrap!(maker_coin.wait_for_tx_spend(&tx_hex, wait_until, from_block).wait()); - - let spend_tx = match spend_tx.lock().unwrap().as_ref() { - Some(tx) => tx.clone(), - None => panic!(), - }; - - match found { - TransactionEnum::UtxoTx(tx) => assert_eq!(tx, spend_tx), - _ => panic!("Unexpected Transaction type"), - } -} - #[test] fn test_wait_for_tx_spend_malicious() { // priv_key of qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG @@ -797,7 +358,9 @@ fn test_wait_for_tx_spend_malicious() { let payment_tx = hex::decode("01000000016601daa208531d20532c460d0c86b74a275f4a126bbffcf4eafdf33835af2859010000006a47304402205825657548bc1b5acf3f4bb2f89635a02b04f3228cd08126e63c5834888e7ac402207ca05fa0a629a31908a97a508e15076e925f8e621b155312b7526a6666b06a76012103693bff1b39e8b5a306810023c29b95397eb395530b106b1820ea235fd81d9ce9ffffffff020000000000000000e35403a0860101284cc49b415b2a8620ad3b72361a5aeba5dffd333fb64750089d935a1ec974d6a91ef4f24ff6ba0000000000000000000000000000000000000000000000000000000001312d00000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc44000000000000000000000000783cf0be521101942da509846ea476e683aad8324b6b2e5444c2639cc0fb7bcea5afba3f3cdce239000000000000000000000000000000000000000000000000000000000000000000000000000000005f855c7614ba8b71f3544b93e2f681f996da519a98ace0107ac2203de400000000001976a9149e032d4b0090a11dc40fe6c47601499a35d55fbb88ac415d855f").unwrap(); let wait_until = (now_ms() / 1000) + 1; let from_block = 696245; - let found = unwrap!(coin.wait_for_tx_spend(&payment_tx, wait_until, from_block).wait()); + let found = unwrap!(coin + .wait_for_tx_spend(&payment_tx, wait_until, from_block, &coin.swap_contract_address()) + .wait()); let spend_tx = match found { TransactionEnum::UtxoTx(tx) => tx, @@ -869,7 +432,7 @@ fn test_generate_token_transfer_script_pubkey() { }; let to_addr: UtxoAddress = "qHmJ3KA6ZAjR9wGjpFASn4gtUSeFAqdZgs".into(); - let to_addr = qrc20_addr_from_utxo_addr(to_addr); + let to_addr = qtum::contract_addr_from_utxo_addr(to_addr); let amount: U256 = 1000000000.into(); let actual = coin .transfer_output(to_addr.clone(), amount, gas_limit, gas_price) @@ -1060,7 +623,7 @@ fn test_get_trade_fee() { } #[test] -fn test_qrc20_coin_from_conf_without_decimals() { +fn test_coin_from_conf_without_decimals() { // priv_key of qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG let priv_key = [ 3, 98, 177, 3, 108, 39, 234, 144, 131, 178, 103, 103, 127, 80, 230, 166, 53, 68, 147, 215, 42, 216, 144, 72, @@ -1157,7 +720,14 @@ fn test_validate_maker_payment_malicious() { let secret_hash = &*dhash160(secret); let amount = BigDecimal::from_str("1").unwrap(); let error = coin - .validate_maker_payment(&payment_tx, time_lock, &maker_pub, secret_hash, amount) + .validate_maker_payment( + &payment_tx, + time_lock, + &maker_pub, + secret_hash, + amount, + &coin.swap_contract_address(), + ) .wait() .err() .expect("'erc20Payment' was called from another swap contract, expected an error"); diff --git a/mm2src/coins/qrc20/rpc_clients.rs b/mm2src/coins/qrc20/rpc_clients.rs new file mode 100644 index 0000000000..d9554a2400 --- /dev/null +++ b/mm2src/coins/qrc20/rpc_clients.rs @@ -0,0 +1,414 @@ +use super::*; +use rpc::v1::types::H256; + +pub mod for_tests { + use super::*; + + #[derive(Debug, Deserialize)] + pub struct ContractCreateResult { + /// The transaction id. + pub txid: H256Json, + /// QTUM address of the sender. + pub sender: String, + /// ripemd-160 hash of the sender. + pub hash160: H160Json, + /// Expected contract address. + pub address: H160Json, + } + + #[derive(Debug, Deserialize)] + pub struct SendToContractResult { + /// The transaction id. + pub txid: H256Json, + /// QTUM address of the sender. + pub sender: String, + /// ripemd-160 hash of the sender. + pub hash160: H160Json, + } + + /// QRC20 Native RPC operations that may change the wallet state. + pub trait Qrc20NativeWalletOps { + /// Create contract with bytecode and specified sender. + /// https://docs.qtum.site/en/Qtum-RPC-API/#createcontract + fn create_contract( + &self, + bytecode: &BytesJson, + gas_limit: u64, + gas_price: BigDecimal, + sender: &str, + ) -> RpcRes; + + /// Send data to a contract. + /// https://docs.qtum.site/en/Qtum-RPC-API/#sendtocontract + fn send_to_contract( + &self, + contract_addr: H160Json, + bytecode: &BytesJson, + qtum_amount: u64, + gas_limit: u64, + gas_price: BigDecimal, + from_addr: &str, + ) -> RpcRes; + + /// Send `transfer` contract call to the `token_addr`. + /// This method uses [`Qrc20NativeWallerOps::send_to_contract`] to send the encoded contract call params. + /// Note qtum_amount = 0, gas_limit = QRC20_GAS_LIMIT_DEFAULT, gas_price = QRC20_GAS_PRICE_DEFAULT will be used. + fn transfer_tokens( + &self, + token_addr: &H160, + from_addr: &str, + to_addr: H160, + amount: U256, + decimals: u8, + ) -> Box + Send> { + let token_addr = contract_addr_into_rpc_format(token_addr); + let qtum_amount = 0; + let gas_price = big_decimal_from_sat(QRC20_GAS_PRICE_DEFAULT as i64, decimals); + + let function = try_fus!(eth::ERC20_CONTRACT.function("transfer")); + let params = try_fus!(function.encode_input(&[Token::Address(to_addr), Token::Uint(amount)])); + Box::new( + self.send_to_contract( + token_addr, + ¶ms.into(), + qtum_amount, + QRC20_GAS_LIMIT_DEFAULT, + gas_price, + from_addr, + ) + .map_err(|e| ERRL!("{}", e)), + ) + } + } + + impl Qrc20NativeWalletOps for NativeClient { + fn create_contract( + &self, + bytecode: &BytesJson, + gas_limit: u64, + gas_price: BigDecimal, + sender: &str, + ) -> RpcRes { + rpc_func!(self, "createcontract", bytecode, gas_limit, gas_price, sender) + } + + fn send_to_contract( + &self, + contract_addr: H160Json, + bytecode: &BytesJson, + qtum_amount: u64, + gas_limit: u64, + gas_price: BigDecimal, + sender: &str, + ) -> RpcRes { + rpc_func!( + self, + "sendtocontract", + contract_addr, + bytecode, + qtum_amount, + gas_limit, + gas_price, + sender + ) + } + } +} + +#[derive(Debug, Deserialize, PartialEq)] +pub struct TokenInfo { + pub name: String, + pub decimals: u8, + pub total_supply: f64, + pub symbol: String, +} + +#[derive(Debug, Deserialize)] +pub struct ExecutionResult { + pub output: BytesJson, +} + +#[derive(Debug, Deserialize)] +pub struct ContractCallResult { + address: H160Json, + #[serde(rename = "executionResult")] + pub execution_result: ExecutionResult, +} + +#[derive(Debug, Deserialize)] +pub struct TxHistoryItem { + pub tx_hash: H256Json, + pub height: u64, + pub log_index: u64, +} + +/// Functions of ERC20/EtomicSwap smart contracts that don't change the blockchain state. +pub enum ViewContractCallType { + /// Erc20 function. + BalanceOf, + /// Erc20 function. + Allowance, + /// Erc20 function. + Decimals, + /// EtomicSwap function. + Payments, +} + +impl ViewContractCallType { + fn as_function_name(&self) -> &'static str { + match self { + ViewContractCallType::BalanceOf => "balanceOf", + ViewContractCallType::Allowance => "allowance", + ViewContractCallType::Decimals => "decimals", + ViewContractCallType::Payments => "payments", + } + } + + fn as_function(&self) -> &'static Function { + match self { + ViewContractCallType::BalanceOf | ViewContractCallType::Allowance | ViewContractCallType::Decimals => { + unwrap!(eth::ERC20_CONTRACT.function(self.as_function_name())) + }, + ViewContractCallType::Payments => unwrap!(eth::SWAP_CONTRACT.function(self.as_function_name())), + } + } +} + +/// The structure is the same as Qtum Core RPC gettransactionreceipt returned data. +/// https://docs.qtum.site/en/Qtum-RPC-API/#gettransactionreceipt +#[derive(Debug, Deserialize, PartialEq, Serialize)] +pub struct TxReceipt { + /// Hash of the block this transaction was included within. + #[serde(rename = "blockHash")] + pub block_hash: H256Json, + /// Number of the block this transaction was included within. + #[serde(rename = "blockNumber")] + pub block_number: u64, + /// Transaction hash. + #[serde(rename = "transactionHash")] + pub transaction_hash: H256Json, + /// Index within the block. + #[serde(rename = "transactionIndex")] + pub transaction_index: u64, + /// Index within the outputs. + #[serde(rename = "outputIndex")] + pub output_index: u64, + /// 20 bytesthe sender address of this tx. + pub from: String, + /// 20 bytesthe receiver address of this tx. if this address is created by a contract, return null. + #[serde(skip_serializing_if = "Option::is_none")] + pub to: Option, + /// The total amount of gas used after execution of the current transaction. + #[serde(rename = "cumulativeGasUsed")] + pub cumulative_gas_used: u64, + /// The gas cost alone to execute the current transaction. + #[serde(rename = "gasUsed")] + pub gas_used: u64, + /// Contract address created, or `None` if not a deployment. + #[serde(rename = "contractAddress")] + pub contract_address: Option, + /// Logs generated within this transaction. + pub log: Vec, + /// Whether corresponding contract call (specified in UTXO outputs[output_index]) was failed. + /// If None or Some("None") - completed, else failed. + pub excepted: Option, + #[serde(rename = "exceptedMessage")] + pub excepted_message: Option, +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +pub struct LogEntry { + /// Contract address. + pub address: String, + /// Vector of 0x-prefixed hex strings with length of 64. + pub topics: Vec, + /// In other words the data means a transaction value. + pub data: String, +} + +impl LogEntry { + pub fn parse_address(&self) -> Result { + if self.address.starts_with("0x") { + qtum::contract_addr_from_str(&self.address) + } else { + let address = format!("0x{}", self.address); + qtum::contract_addr_from_str(&address) + } + } +} + +#[derive(Clone, Debug)] +pub enum TopicFilter { + Match(String), + Skip, +} + +impl From<&str> for TopicFilter { + fn from(topic: &str) -> Self { TopicFilter::Match(topic.to_string()) } +} + +/// Qrc20 specific RPC ops +pub trait Qrc20ElectrumOps { + /// This can be used to get the basic information(name, decimals, total_supply, symbol) of a QRC20 token. + /// https://github.com/qtumproject/qtum-electrumx-server/blob/master/docs/qrc20-integration.md#blockchaintokenget_infotoken_address + fn blockchain_token_get_info(&self, token_addr: &H160Json) -> RpcRes; + + fn blockchain_contract_call(&self, contract_addr: &H160Json, data: BytesJson) -> RpcRes; + + /// This can be used to retrieve QRC20 token transfer history, params are the same as blockchain.contract.event.subscribe, + /// and it returns a list of map{tx_hash, height, log_index}, where log_index is the position for this event log in its transaction. + /// https://github.com/qtumproject/qtum-electrumx-server/blob/master/docs/qrc20-integration.md#blockchaincontracteventget_historyhash160-contract_addr-topic + fn blockchain_contract_event_get_history( + &self, + address: &H160Json, + contract_addr: &H160Json, + topic: &str, + ) -> RpcRes>; + + /// This can be used to get eventlogs in the transaction, the returned data is the same as Qtum Core RPC gettransactionreceipt. + /// from the eventlogs, we can get QRC20 Token transafer informations(from, to, amount). + /// https://github.com/qtumproject/qtum-electrumx-server/blob/master/docs/qrc20-integration.md#blochchaintransactionget_receipttxid + fn blochchain_transaction_get_receipt(&self, hash: &H256Json) -> RpcRes>; +} + +pub trait Qrc20NativeOps { + /// https://docs.qtum.site/en/Qtum-RPC-API/#callcontract + fn call_contract(&self, contract_addr: &H160Json, data: BytesJson) -> RpcRes; + + /// Similar to [`Qrc20ElectrumOps::blochchain_transaction_get_receipt`] + /// https://docs.qtum.site/en/Qtum-RPC-API/#gettransactionreceipt + fn get_transaction_receipt(&self, hash: &H256Json) -> RpcRes>; + + /// This can be used to retrieve QRC20 transaction history. + /// https://docs.qtum.site/en/Qtum-RPC-API/#searchlogs + fn search_logs( + &self, + from_block: u64, + to_block: Option, + addresses: Vec, + topics: Vec, + ) -> RpcRes>; +} + +impl Qrc20NativeOps for NativeClient { + fn call_contract(&self, contract_addr: &H160Json, data: BytesJson) -> RpcRes { + rpc_func!(self, "callcontract", contract_addr, data) + } + + fn get_transaction_receipt(&self, hash: &H256Json) -> RpcRes> { + rpc_func!(self, "gettransactionreceipt", hash) + } + + fn search_logs( + &self, + from_block: u64, + to_block: Option, + addresses: Vec, + topics: Vec, + ) -> RpcRes> { + let to_block = to_block.map(|x| x as i64).unwrap_or(-1); + let addr_block = json!({ "addresses": addresses }); + let topics: Vec = topics + .into_iter() + .map(|t| match t { + TopicFilter::Match(s) => Json::String(s), + TopicFilter::Skip => Json::Null, + }) + .collect(); + let topic_block = json!({ + "topics": topics, + }); + rpc_func!(self, "searchlogs", from_block, to_block, addr_block, topic_block) + } +} + +impl Qrc20ElectrumOps for ElectrumClient { + fn blockchain_token_get_info(&self, token_addr: &H160Json) -> RpcRes { + rpc_func!(self, "blockchain.token.get_info", token_addr) + } + + fn blockchain_contract_call(&self, contract_addr: &H160Json, data: BytesJson) -> RpcRes { + let sender = ""; + rpc_func!(self, "blockchain.contract.call", contract_addr, data, sender) + } + + fn blockchain_contract_event_get_history( + &self, + address: &H160Json, + contract_addr: &H160Json, + topic: &str, + ) -> RpcRes> { + rpc_func!( + self, + "blockchain.contract.event.get_history", + address, + contract_addr, + topic + ) + } + + fn blochchain_transaction_get_receipt(&self, hash: &H256Json) -> RpcRes> { + rpc_func!(self, "blochchain.transaction.get_receipt", hash) + } +} + +pub trait Qrc20RpcOps { + fn get_transaction_receipts(&self, tx_hash: &H256Json) -> RpcRes>; + + fn rpc_contract_call( + &self, + func: ViewContractCallType, + contract_addr: &H160, + tokens: &[Token], + ) -> Box, Error = String> + Send>; + + fn token_decimals(&self, token_address: &H160) -> Box + Send>; +} + +impl Qrc20RpcOps for UtxoRpcClientEnum { + fn get_transaction_receipts(&self, tx_hash: &H256) -> RpcRes> { + match self { + UtxoRpcClientEnum::Electrum(electrum) => electrum.blochchain_transaction_get_receipt(tx_hash), + UtxoRpcClientEnum::Native(native) => native.get_transaction_receipt(tx_hash), + } + } + + fn rpc_contract_call( + &self, + func: ViewContractCallType, + contract_addr: &H160, + tokens: &[Token], + ) -> Box, Error = String> + Send> { + let function = func.as_function().clone(); + let params = try_fus!(function.encode_input(tokens)); + let contract_addr = contract_addr_into_rpc_format(contract_addr); + + let fut = match self { + UtxoRpcClientEnum::Native(native) => native.call_contract(&contract_addr, params.into()), + UtxoRpcClientEnum::Electrum(electrum) => electrum.blockchain_contract_call(&contract_addr, params.into()), + }; + let fut = fut + .map_err(|e| ERRL!("{}", e)) + .and_then(move |result| Ok(try_s!(function.decode_output(&result.execution_result.output)))); + Box::new(fut) + } + + fn token_decimals(&self, token_address: &H160) -> Box + Send> { + let fut = self + .rpc_contract_call(ViewContractCallType::Decimals, token_address, &[]) + .map_err(|e| ERRL!("{}", e)) + .and_then(|tokens| { + let decimals = match tokens.first() { + Some(Token::Uint(decimals)) => decimals.as_u64(), + Some(_) => return ERR!(r#"Expected Uint as "decimals" result but got {:?}"#, tokens), + None => return ERR!(r#"Expected Uint as "decimals" result but got nothing"#), + }; + if decimals <= (std::u8::MAX as u64) { + Ok(decimals as u8) + } else { + ERR!("decimals {} is not u8", decimals) + } + }); + Box::new(fut) + } +} diff --git a/mm2src/coins/qrc20/rpc_electrum.rs b/mm2src/coins/qrc20/rpc_electrum.rs deleted file mode 100644 index e83270e602..0000000000 --- a/mm2src/coins/qrc20/rpc_electrum.rs +++ /dev/null @@ -1,142 +0,0 @@ -use super::*; - -#[derive(Debug, Deserialize, PartialEq)] -pub struct TokenInfo { - pub name: String, - pub decimals: u8, - pub total_supply: f64, - pub symbol: String, -} - -#[derive(Debug, Deserialize)] -pub struct ExecutionResult { - pub output: BytesJson, -} - -#[derive(Debug, Deserialize)] -pub struct ContractCallResult { - address: H160Json, - #[serde(rename = "executionResult")] - pub execution_result: ExecutionResult, -} - -#[derive(Debug, Deserialize)] -pub struct TxHistoryItem { - pub tx_hash: H256Json, - pub height: u64, - pub log_index: u64, -} - -/// The structure is the same as Qtum Core RPC gettransactionreceipt returned data. -/// https://docs.qtum.site/en/Qtum-RPC-API/#gettransactionreceipt -#[derive(Debug, Deserialize)] -pub struct TxReceipt { - /// Hash of the block this transaction was included within. - #[serde(rename = "blockHash")] - pub block_hash: H256Json, - /// Number of the block this transaction was included within. - #[serde(rename = "blockNumber")] - pub block_number: u64, - /// Transaction hash. - #[serde(rename = "transactionHash")] - pub transaction_hash: H256Json, - /// Index within the block. - #[serde(rename = "transactionIndex")] - pub transaction_index: u64, - /// Index within the outputs. - #[serde(rename = "outputIndex")] - pub output_index: u64, - /// 20 bytesthe sender address of this tx. - pub from: String, - /// 20 bytesthe receiver address of this tx. if this address is created by a contract, return null. - #[serde(skip_serializing_if = "Option::is_none")] - pub to: Option, - /// The total amount of gas used after execution of the current transaction. - #[serde(rename = "cumulativeGasUsed")] - pub cumulative_gas_used: u64, - /// The gas cost alone to execute the current transaction. - #[serde(rename = "gasUsed")] - pub gas_used: u64, - /// Contract address created, or `None` if not a deployment. - #[serde(rename = "contractAddress")] - pub contract_address: Option, - /// Logs generated within this transaction. - pub log: Vec, - /// Whether corresponding contract call (specified in UTXO outputs[output_index]) was failed. - /// If None or Some("None") - completed, else failed. - pub excepted: Option, - #[serde(rename = "exceptedMessage")] - pub excepted_message: Option, -} - -#[derive(Debug, Deserialize)] -pub struct LogEntry { - /// Contract address. - pub address: String, - /// Vector of 0x-prefixed hex strings with length of 64. - pub topics: Vec, - /// In other words the data means a transaction value. - pub data: String, -} - -impl LogEntry { - pub fn parse_address(&self) -> Result { - if self.address.starts_with("0x") { - qrc20_addr_from_str(&self.address) - } else { - let address = format!("0x{}", self.address); - qrc20_addr_from_str(&address) - } - } -} - -/// Qrc20 specific RPC ops -pub trait Qrc20RpcOps { - /// This can be used to get the basic information(name, decimals, total_supply, symbol) of a QRC20 token. - /// https://github.com/qtumproject/qtum-electrumx-server/blob/master/docs/qrc20-integration.md#blockchaintokenget_infotoken_address - fn blockchain_token_get_info(&self, token_addr: &H160Json) -> RpcRes; - - fn blockchain_contract_call(&self, contract_addr: &H160Json, data: BytesJson) -> RpcRes; - - /// this can be used to retrieve QRC20 token transfer history, params are the same as blockchain.contract.event.subscribe, - /// and it returns a list of map{tx_hash, height, log_index}, where log_index is the position for this event log in its transaction. - /// https://github.com/qtumproject/qtum-electrumx-server/blob/master/docs/qrc20-integration.md#blockchaincontracteventget_historyhash160-contract_addr-topic - fn blockchain_contract_event_get_history( - &self, - address: &H160Json, - contract_addr: &H160Json, - topic: &str, - ) -> RpcRes>; - - fn blochchain_transaction_get_receipt(&self, hash: &H256Json) -> RpcRes>; -} - -impl Qrc20RpcOps for ElectrumClient { - fn blockchain_token_get_info(&self, token_addr: &H160Json) -> RpcRes { - rpc_func!(self, "blockchain.token.get_info", token_addr) - } - - fn blockchain_contract_call(&self, contract_addr: &H160Json, data: BytesJson) -> RpcRes { - let sender = ""; - rpc_func!(self, "blockchain.contract.call", contract_addr, data, sender) - } - - fn blockchain_contract_event_get_history( - &self, - address: &H160Json, - contract_addr: &H160Json, - topic: &str, - ) -> RpcRes> { - rpc_func!( - self, - "blockchain.contract.event.get_history", - address, - contract_addr, - topic - ) - } - - fn blochchain_transaction_get_receipt(&self, hash: &H256Json) -> RpcRes> { - rpc_func!(self, "blochchain.transaction.get_receipt", hash) - } -} diff --git a/mm2src/coins/qrc20/script_pubkey.rs b/mm2src/coins/qrc20/script_pubkey.rs index 9bd113836a..2391c40b91 100644 --- a/mm2src/coins/qrc20/script_pubkey.rs +++ b/mm2src/coins/qrc20/script_pubkey.rs @@ -245,7 +245,7 @@ mod tests { let script: Script = "5403a02526012844a9059cbb0000000000000000000000000240b898276ad2cc0d2fe6f527e8e31104e7fde3000000000000000000000000000000000000000000000000000000003b9aca0014d362e096e873eb7907e205fadc6175c6fec7bc44c2".into(); let to_addr: UtxoAddress = "qHmJ3KA6ZAjR9wGjpFASn4gtUSeFAqdZgs".into(); - let to_addr = qrc20_addr_from_utxo_addr(to_addr); + let to_addr = qtum::contract_addr_from_utxo_addr(to_addr); let amount: U256 = 1000000000.into(); let function = eth::ERC20_CONTRACT.function("transfer").unwrap(); let expected = function @@ -287,7 +287,7 @@ mod tests { #[test] fn test_extract_contract_addr_from_script() { let script: Script = "5403a02526012844a9059cbb0000000000000000000000000240b898276ad2cc0d2fe6f527e8e31104e7fde3000000000000000000000000000000000000000000000000000000003b9aca0014d362e096e873eb7907e205fadc6175c6fec7bc44c2".into(); - let expected = qrc20_addr_from_str("0xd362e096e873eb7907e205fadc6175c6fec7bc44").unwrap(); + let expected = qtum::contract_addr_from_str("0xd362e096e873eb7907e205fadc6175c6fec7bc44").unwrap(); let actual = unwrap!(extract_contract_addr_from_script(&script)); assert_eq!(actual, expected); diff --git a/mm2src/coins/qrc20/swap_ops.rs b/mm2src/coins/qrc20/swap.rs similarity index 78% rename from mm2src/coins/qrc20/swap_ops.rs rename to mm2src/coins/qrc20/swap.rs index 4b57fea028..7521b3e91f 100644 --- a/mm2src/coins/qrc20/swap_ops.rs +++ b/mm2src/coins/qrc20/swap.rs @@ -1,6 +1,5 @@ +use super::history::TransferHistoryBuilder; use super::*; -use common::lazy::FindMapLazy; -use history::{HistoryBuilder, HistoryOrder}; use script_pubkey::{extract_contract_addr_from_script, extract_contract_call_from_script, is_contract_call}; /// `erc20Payment` call details consist of values obtained from [`TransactionOutput::script_pubkey`] and [`TxReceipt::logs`]. @@ -37,8 +36,9 @@ impl Qrc20Coin { time_lock: u32, secret_hash: Vec, receiver_addr: H160, + swap_contract_address: H160, ) -> Result { - let allowance = try_s!(self.allowance(self.swap_contract_address).await); + let allowance = try_s!(self.allowance(swap_contract_address).await); let mut outputs = Vec::default(); // check if we should reset the allowance to 0 and raise this to the max available value (our balance) @@ -47,10 +47,10 @@ impl Qrc20Coin { let balance = try_s!(wei_from_big_decimal(&balance, self.utxo.decimals)); if allowance > U256::zero() { // first reset the allowance to the 0 - outputs.push(try_s!(self.approve_output(self.swap_contract_address, 0.into()))); + outputs.push(try_s!(self.approve_output(swap_contract_address, 0.into()))); } // set the allowance from 0 to `balance` after the previous output will be executed - outputs.push(try_s!(self.approve_output(self.swap_contract_address, balance))); + outputs.push(try_s!(self.approve_output(swap_contract_address, balance))); } // when this output is executed, the allowance will be sufficient already @@ -59,7 +59,8 @@ impl Qrc20Coin { value, time_lock, &secret_hash, - receiver_addr + receiver_addr, + &swap_contract_address ))); self.send_contract_calls(outputs).await @@ -68,14 +69,11 @@ impl Qrc20Coin { pub async fn spend_hash_time_locked_payment( &self, payment_tx: UtxoTx, + swap_contract_address: H160, secret: Vec, ) -> Result { let Erc20PaymentDetails { - swap_id, - value, - swap_contract_address, - sender, - .. + swap_id, value, sender, .. } = try_s!(self.erc20_payment_details_from_tx(&payment_tx).await); let status = try_s!(self.payment_status(&swap_contract_address, swap_id.clone()).await); @@ -87,11 +85,14 @@ impl Qrc20Coin { self.send_contract_calls(vec![spend_output]).await } - pub async fn refund_hash_time_locked_payment(&self, payment_tx: UtxoTx) -> Result { + pub async fn refund_hash_time_locked_payment( + &self, + swap_contract_address: H160, + payment_tx: UtxoTx, + ) -> Result { let Erc20PaymentDetails { swap_id, value, - swap_contract_address, receiver, secret_hash, .. @@ -114,10 +115,11 @@ impl Qrc20Coin { sender: H160, secret_hash: Vec, amount: BigDecimal, + expected_swap_contract_address: H160, ) -> Result<(), String> { let expected_swap_id = qrc20_swap_id(time_lock, &secret_hash); let status = try_s!( - self.payment_status(&self.swap_contract_address, expected_swap_id.clone()) + self.payment_status(&expected_swap_contract_address, expected_swap_id.clone()) .await ); if status != eth::PAYMENT_STATE_SENT.into() { @@ -126,7 +128,7 @@ impl Qrc20Coin { let expected_call_bytes = { let expected_value = try_s!(wei_from_big_decimal(&amount, self.utxo.decimals)); - let expected_receiver = qrc20_addr_from_utxo_addr(self.utxo.my_address.clone()); + let expected_receiver = qtum::contract_addr_from_utxo_addr(self.utxo.my_address.clone()); try_s!(self.erc20_payment_call_bytes( expected_swap_id, expected_value, @@ -148,10 +150,10 @@ impl Qrc20Coin { return ERR!("Payment tx was sent from wrong address, expected {:?}", sender); } - if self.swap_contract_address != erc20_payment.swap_contract_address { + if expected_swap_contract_address != erc20_payment.swap_contract_address { return ERR!( "Payment tx was sent to wrong address, expected {:?}", - self.swap_contract_address + expected_swap_contract_address ); } @@ -164,10 +166,7 @@ impl Qrc20Coin { fee_addr: H160, expected_value: U256, ) -> Result<(), String> { - let verbose_tx = match self.utxo.rpc_client { - UtxoRpcClientEnum::Electrum(ref rpc) => try_s!(rpc.get_verbose_transaction(fee_tx_hash).compat().await), - UtxoRpcClientEnum::Native(_) => return ERR!("Electrum client expected"), - }; + let verbose_tx = try_s!(self.utxo.rpc_client.get_verbose_transaction(fee_tx_hash).compat().await); let qtum_tx: UtxoTx = try_s!(deserialize(verbose_tx.hex.as_slice()).map_err(|e| ERRL!("{:?}", e))); // The transaction could not being mined, just check the transfer tokens. @@ -213,16 +212,8 @@ impl Qrc20Coin { tx: UtxoTx, search_from_block: u64, ) -> Result, String> { - let electrum = match self.utxo.rpc_client { - UtxoRpcClientEnum::Electrum(ref rpc_cln) => rpc_cln, - - UtxoRpcClientEnum::Native(_) => { - return ERR!("Native mode not supported"); - }, - }; - let tx_hash = tx.hash().reversed().into(); - let verbose_tx = try_s!(electrum.get_verbose_transaction(tx_hash).compat().await); + let verbose_tx = try_s!(self.utxo.rpc_client.get_verbose_transaction(tx_hash).compat().await); if verbose_tx.confirmations < 1 { return ERR!("'erc20Payment' was not confirmed yet. Please wait for at least one confirmation"); } @@ -234,48 +225,20 @@ impl Qrc20Coin { } // First try to find a 'receiverSpend' contract call. - // This means that we should request a transaction history for the possible spender of our payment - [`Erc20PaymentDetails::receiver`]. - let history = try_s!( - HistoryBuilder::new(self.clone()) - .from_block(search_from_block) - .address(receiver) - // current function could be called much later than end of the swap - .order(HistoryOrder::OldestToNewest) - .build_utxo_lazy() - .await - ); - let found = history + let spend_txs = try_s!(self.receiver_spend_transactions(receiver, search_from_block).await); + let found = spend_txs .into_iter() - .find_map_lazy(|tx| { - let tx = tx.ok()?; - find_receiver_spend_with_swap_id_and_secret_hash(&tx, &expected_swap_id, &secret_hash) - // return Some(tx) if the `receiverSpend` was found - .map(|_| tx) - }) - .await; + .find(|tx| find_receiver_spend_with_swap_id_and_secret_hash(tx, &expected_swap_id, &secret_hash).is_some()); if let Some(spent_tx) = found { return Ok(Some(FoundSwapTxSpend::Spent(TransactionEnum::UtxoTx(spent_tx)))); } // Else try to find a 'senderRefund' contract call. - // This means that we should request our transaction history because we could refund the payment already. - let history = try_s!( - HistoryBuilder::new(self.clone()) - .from_block(search_from_block) - // current function could be called much later than end of the swap - .order(HistoryOrder::OldestToNewest) - .build_utxo_lazy() - .await - ); - let found = history - .into_iter() - .find_map_lazy(|tx| { - let tx = tx.ok()?; - find_swap_contract_call_with_swap_id(ContractCallType::SenderRefund, &tx, &expected_swap_id) - // return Some(tx) if the `senderRefund` was found - .map(|_| tx) - }) - .await; + let sender = qtum::contract_addr_from_utxo_addr(self.utxo.my_address.clone()); + let refund_txs = try_s!(self.sender_refund_transactions(sender, search_from_block).await); + let found = refund_txs.into_iter().find(|tx| { + find_swap_contract_call_with_swap_id(MutContractCallType::SenderRefund, &tx, &expected_swap_id).is_some() + }); if let Some(refunded_tx) = found { return Ok(Some(FoundSwapTxSpend::Refunded(TransactionEnum::UtxoTx(refunded_tx)))); } @@ -285,30 +248,21 @@ impl Qrc20Coin { pub async fn check_if_my_payment_sent_impl( &self, + swap_contract_address: H160, swap_id: Vec, search_from_block: u64, ) -> Result, String> { - let status = try_s!(self.payment_status(&self.swap_contract_address, swap_id.clone()).await); + let status = try_s!(self.payment_status(&swap_contract_address, swap_id.clone()).await); if status == eth::PAYMENT_STATE_UNINITIALIZED.into() { return Ok(None); }; - let history = try_s!( - HistoryBuilder::new(self.clone()) - .from_block(search_from_block) - .order(HistoryOrder::OldestToNewest) - .build_utxo_lazy() - .await - ); - let found = history + let sender = qtum::contract_addr_from_utxo_addr(self.utxo.my_address.clone()); + let erc20_payment_txs = try_s!(self.erc20_payment_transactions(sender, search_from_block).await); + let found = erc20_payment_txs .into_iter() - .find_map_lazy(|tx| { - let tx = tx.ok()?; - find_swap_contract_call_with_swap_id(ContractCallType::Erc20Payment, &tx, &swap_id) - // return Some(UtxoTx(tx)) if the `erc20Payment` was found - .map(|_| TransactionEnum::UtxoTx(tx)) - }) - .await; + .find(|tx| find_swap_contract_call_with_swap_id(MutContractCallType::Erc20Payment, &tx, &swap_id).is_some()) + .map(TransactionEnum::UtxoTx); Ok(found) } @@ -321,7 +275,7 @@ impl Qrc20Coin { match receiver_spend_call_details_from_script_pubkey(&script_pubkey) { Ok(details) => details, Err(e) => { - log!((e)); + error!("{}", e); // try to obtain the details from the next output continue; }, @@ -329,7 +283,10 @@ impl Qrc20Coin { let actual_secret_hash = &*dhash160(&secret); if actual_secret_hash != secret_hash { - log!("Warning: invalid 'dhash160(secret)' "[actual_secret_hash]", expected "[secret_hash]); + warn!( + "invalid 'dhash160(secret)' {:?}, expected {:?}", + actual_secret_hash, secret_hash, + ); continue; } @@ -354,24 +311,11 @@ impl Qrc20Coin { loop { // Try to find a 'receiverSpend' contract call. - // This means that we should request a transaction history for the possible spender of our payment - [`Erc20PaymentDetails::receiver`]. - let history = try_s!( - HistoryBuilder::new(self.clone()) - .from_block(from_block) - .address(receiver) - .order(HistoryOrder::NewestToOldest) - .build_utxo_lazy() - .await - ); - let found = history + let spend_txs = try_s!(self.receiver_spend_transactions(receiver, from_block).await); + let found = spend_txs .into_iter() - .find_map_lazy(|tx| { - let tx = tx.ok()?; - find_receiver_spend_with_swap_id_and_secret_hash(&tx, &swap_id, &secret_hash) - // return Some(UtxoTx(tx)) if the `receiverSpend` was found - .map(|_| TransactionEnum::UtxoTx(tx)) - }) - .await; + .find(|tx| find_receiver_spend_with_swap_id_and_secret_hash(&tx, &swap_id, &secret_hash).is_some()) + .map(TransactionEnum::UtxoTx); if let Some(spent_tx) = found { return Ok(spent_tx); @@ -400,12 +344,7 @@ impl Qrc20Coin { .await ); let tx_hash = qtum_tx.hash().reversed().into(); - let receipts = match self.utxo.rpc_client { - UtxoRpcClientEnum::Electrum(ref electrum) => { - try_s!(electrum.blochchain_transaction_get_receipt(&tx_hash).compat().await) - }, - UtxoRpcClientEnum::Native(_) => return ERR!("Electrum client expected"), - }; + let receipts = try_s!(self.utxo.rpc_client.get_transaction_receipts(&tx_hash).compat().await); for receipt in receipts { let output = try_s!(qtum_tx @@ -419,12 +358,11 @@ impl Qrc20Coin { let contract_call_bytes = try_s!(extract_contract_call_from_script(&script_pubkey)); - let call_type = try_s!(ContractCallType::from_script_pubkey(&contract_call_bytes)); - log!([call_type]); + let call_type = try_s!(MutContractCallType::from_script_pubkey(&contract_call_bytes)); match call_type { - Some(ContractCallType::Erc20Payment) - | Some(ContractCallType::ReceiverSpend) - | Some(ContractCallType::SenderRefund) => (), + Some(MutContractCallType::Erc20Payment) + | Some(MutContractCallType::ReceiverSpend) + | Some(MutContractCallType::SenderRefund) => (), _ => continue, // skip not etomic swap contract calls } @@ -436,11 +374,14 @@ impl Qrc20Coin { async fn allowance(&self, spender: H160) -> Result { let tokens = try_s!( - self.rpc_contract_call(RpcContractCallType::Allowance, &self.contract_address, &[ - Token::Address(qrc20_addr_from_utxo_addr(self.utxo.my_address.clone())), - Token::Address(spender), - ]) - .await + self.utxo + .rpc_client + .rpc_contract_call(ViewContractCallType::Allowance, &self.contract_address, &[ + Token::Address(qtum::contract_addr_from_utxo_addr(self.utxo.my_address.clone())), + Token::Address(spender), + ]) + .compat() + .await ); match tokens.first() { @@ -454,10 +395,13 @@ impl Qrc20Coin { /// Do not use self swap_contract_address, because it could be updated during restart. async fn payment_status(&self, swap_contract_address: &H160, swap_id: Vec) -> Result { let decoded = try_s!( - self.rpc_contract_call(RpcContractCallType::Payments, swap_contract_address, &[ - Token::FixedBytes(swap_id) - ]) - .await + self.utxo + .rpc_client + .rpc_contract_call(ViewContractCallType::Payments, swap_contract_address, &[ + Token::FixedBytes(swap_id) + ]) + .compat() + .await ); if decoded.len() < 3 { return ERR!( @@ -503,6 +447,7 @@ impl Qrc20Coin { time_lock: u32, secret_hash: &[u8], receiver_addr: H160, + swap_contract_address: &H160, ) -> Result { let params = try_s!(self.erc20_payment_call_bytes(id, value, time_lock, secret_hash, receiver_addr)); @@ -512,7 +457,7 @@ impl Qrc20Coin { ¶ms, // params of the function gas_limit, gas_price, - &self.swap_contract_address, // address of the contract which function will be called + swap_contract_address, // address of the contract which function will be called )) .to_bytes(); @@ -621,12 +566,7 @@ impl Qrc20Coin { /// Note returns an error if the contract call was excepted. async fn erc20_payment_details_from_tx(&self, qtum_tx: &UtxoTx) -> Result { let tx_hash: H256Json = qtum_tx.hash().reversed().into(); - let receipts = match self.utxo.rpc_client { - UtxoRpcClientEnum::Electrum(ref rpc) => { - try_s!(rpc.blochchain_transaction_get_receipt(&tx_hash).compat().await) - }, - UtxoRpcClientEnum::Native(_) => return ERR!("Electrum client expected"), - }; + let receipts = try_s!(self.utxo.rpc_client.get_transaction_receipts(&tx_hash).compat().await); for receipt in receipts { let output = try_s!(qtum_tx @@ -640,9 +580,9 @@ impl Qrc20Coin { let contract_call_bytes = try_s!(extract_contract_call_from_script(&script_pubkey)); - let call_type = try_s!(ContractCallType::from_script_pubkey(&contract_call_bytes)); + let call_type = try_s!(MutContractCallType::from_script_pubkey(&contract_call_bytes)); match call_type { - Some(ContractCallType::Erc20Payment) => (), + Some(MutContractCallType::Erc20Payment) => (), _ => continue, // skip non-erc20Payment contract calls } @@ -744,6 +684,61 @@ impl Qrc20Coin { } ERR!("Couldn't find erc20Payment contract call in {:?} tx", tx_hash) } + + /// Gets transactions emitted `ReceiverSpent` events from etomic swap smart contract since `from_block` + async fn receiver_spend_transactions(&self, receiver: H160, from_block: u64) -> Result, String> { + self.transactions_emitted_swap_event(QRC20_RECEIVER_SPENT_TOPIC, receiver, from_block) + .await + } + + /// Gets transactions emitted `SenderRefunded` events from etomic swap smart contract since `from_block` + async fn sender_refund_transactions(&self, sender: H160, from_block: u64) -> Result, String> { + self.transactions_emitted_swap_event(QRC20_SENDER_REFUNDED_TOPIC, sender, from_block) + .await + } + + /// Gets transactions emitted `PaymentSent` events from etomic swap smart contract since `from_block` + async fn erc20_payment_transactions(&self, sender: H160, from_block: u64) -> Result, String> { + self.transactions_emitted_swap_event(QRC20_PAYMENT_SENT_TOPIC, sender, from_block) + .await + } + + /// Gets transactions emitted the specified events from etomic swap smart contract since `from_block`. + /// `event_topic` is an event first and once topic in logs. + /// `caller_address` is who called etomic swap smart contract functions that emitted the specified event. + async fn transactions_emitted_swap_event( + &self, + event_topic: &str, + caller_address: H160, + from_block: u64, + ) -> Result, String> { + let receipts = try_s!( + TransferHistoryBuilder::new(self.clone()) + .from_block(from_block) + .address(caller_address) + .build() + .await + ); + + let mut txs = Vec::with_capacity(receipts.len()); + for receipt in receipts { + let swap_event_emitted = receipt.log.iter().any(|log| is_swap_event_log(event_topic, log)); + if !swap_event_emitted { + continue; + } + + let verbose_tx = try_s!( + self.utxo + .rpc_client + .get_verbose_transaction(receipt.transaction_hash) + .compat() + .await + ); + let tx = try_s!(deserialize(verbose_tx.hex.as_slice()).map_err(|e| ERRL!("{:?}", e))); + txs.push(tx); + } + Ok(txs) + } } /// Get `Transfer` events details from [`TxReceipt::logs`]. @@ -776,9 +771,9 @@ fn transfer_call_details_from_script_pubkey(script_pubkey: &Script) -> Result<(H } let contract_call_bytes = try_s!(extract_contract_call_from_script(&script_pubkey)); - let call_type = try_s!(ContractCallType::from_script_pubkey(&contract_call_bytes)); + let call_type = try_s!(MutContractCallType::from_script_pubkey(&contract_call_bytes)); match call_type { - Some(ContractCallType::Transfer) => (), + Some(MutContractCallType::Transfer) => (), _ => return ERR!("Expected 'transfer' contract call"), } @@ -808,9 +803,9 @@ pub fn receiver_spend_call_details_from_script_pubkey(script_pubkey: &Script) -> } let contract_call_bytes = try_s!(extract_contract_call_from_script(script_pubkey)); - let call_type = try_s!(ContractCallType::from_script_pubkey(&contract_call_bytes)); + let call_type = try_s!(MutContractCallType::from_script_pubkey(&contract_call_bytes)); match call_type { - Some(ContractCallType::ReceiverSpend) => (), + Some(MutContractCallType::ReceiverSpend) => (), _ => return ERR!("Expected 'receiverSpend' contract call"), } @@ -879,7 +874,10 @@ fn find_receiver_spend_with_swap_id_and_secret_hash( let secret_hash = &*dhash160(&secret); if secret_hash != expected_secret_hash { - log!("Warning: invalid 'dhash160(secret)' "[secret_hash]", expected "[expected_secret_hash]); + warn!( + "invalid 'dhash160(secret)' {:?}, expected {:?}", + secret_hash, expected_secret_hash + ); continue; } @@ -890,7 +888,7 @@ fn find_receiver_spend_with_swap_id_and_secret_hash( } fn find_swap_contract_call_with_swap_id( - expected_call_type: ContractCallType, + expected_call_type: MutContractCallType, tx: &UtxoTx, expected_swap_id: &[u8], ) -> Option { @@ -905,16 +903,16 @@ fn find_swap_contract_call_with_swap_id( let contract_call_bytes = match extract_contract_call_from_script(&script_pubkey) { Ok(bytes) => bytes, Err(e) => { - log!([e]); + error!("{}", e); continue; }, }; - let call_type = match ContractCallType::from_script_pubkey(&contract_call_bytes) { + let call_type = match MutContractCallType::from_script_pubkey(&contract_call_bytes) { Ok(Some(t)) => t, Ok(None) => continue, // unknown contract call type Err(e) => { - log!([e]); + error!("{}", e); continue; }, }; @@ -927,7 +925,7 @@ fn find_swap_contract_call_with_swap_id( let decoded = match function.decode_input(&contract_call_bytes) { Ok(d) => d, Err(e) => { - log!([e]); + error!("{}", e); continue; }, }; @@ -936,11 +934,11 @@ fn find_swap_contract_call_with_swap_id( let swap_id = match decoded.into_iter().next() { Some(Token::FixedBytes(id)) => id, Some(token) => { - log!("Warning: tx "[tx_hash]" 'swap_id' arg is invalid, found "[token]); + warn!("tx {:?} 'swap_id' arg is invalid, found {:?}", tx_hash, token); continue; }, None => { - log!("Warning: couldn't find 'swap_id' in "[tx_hash]); + warn!("Warning: couldn't find 'swap_id' in {:?}", tx_hash); continue; }, }; @@ -957,11 +955,20 @@ fn check_if_contract_call_completed(receipt: &TxReceipt) -> Result<(), String> { match receipt.excepted { Some(ref ex) if ex != "None" && ex != "none" => { let msg = match receipt.excepted_message { - Some(ref m) => format!(": {}", m), - None => String::default(), + Some(ref m) if !m.is_empty() => format!(": {}", m), + _ => String::default(), }; ERR!("Contract call failed with an error: {}{}", ex, msg) }, _ => Ok(()), } } + +fn is_swap_event_log(event_topic: &str, log: &LogEntry) -> bool { + let mut topics = log.topics.iter(); + match topics.next() { + // every swap event should have only one topic in log + Some(first_event) => first_event == event_topic && topics.next().is_none(), + _ => false, + } +} diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index 1166e4e46d..255e0d9a4c 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -5,6 +5,7 @@ use bigdecimal::BigDecimal; use common::mm_ctx::MmArc; use futures01::Future; use mocktopus::macros::*; +use rpc::v1::types::Bytes as BytesJson; use serde_json::Value as Json; /// Dummy coin struct used in tests which functions are unimplemented but then mocked @@ -37,7 +38,13 @@ impl MarketCoinOps for TestCoin { unimplemented!() } - fn wait_for_tx_spend(&self, transaction: &[u8], wait_until: u64, from_block: u64) -> TransactionFut { + fn wait_for_tx_spend( + &self, + transaction: &[u8], + wait_until: u64, + from_block: u64, + swap_contract_address: &Option, + ) -> TransactionFut { unimplemented!() } @@ -61,6 +68,7 @@ impl SwapOps for TestCoin { taker_pub: &[u8], secret_hash: &[u8], amount: BigDecimal, + swap_contract_address: &Option, ) -> TransactionFut { unimplemented!() } @@ -71,6 +79,7 @@ impl SwapOps for TestCoin { maker_pub: &[u8], secret_hash: &[u8], amount: BigDecimal, + swap_contract_address: &Option, ) -> TransactionFut { unimplemented!() } @@ -81,6 +90,7 @@ impl SwapOps for TestCoin { time_lock: u32, taker_pub: &[u8], secret: &[u8], + swap_contract_address: &Option, ) -> TransactionFut { unimplemented!() } @@ -91,6 +101,7 @@ impl SwapOps for TestCoin { time_lock: u32, maker_pub: &[u8], secret: &[u8], + swap_contract_address: &Option, ) -> TransactionFut { unimplemented!() } @@ -101,6 +112,7 @@ impl SwapOps for TestCoin { time_lock: u32, maker_pub: &[u8], secret_hash: &[u8], + swap_contract_address: &Option, ) -> TransactionFut { unimplemented!() } @@ -111,6 +123,7 @@ impl SwapOps for TestCoin { time_lock: u32, taker_pub: &[u8], secret_hash: &[u8], + swap_contract_address: &Option, ) -> TransactionFut { unimplemented!() } @@ -131,6 +144,7 @@ impl SwapOps for TestCoin { maker_pub: &[u8], priv_bn_hash: &[u8], amount: BigDecimal, + swap_contract_address: &Option, ) -> Box + Send> { unimplemented!() } @@ -142,6 +156,7 @@ impl SwapOps for TestCoin { taker_pub: &[u8], priv_bn_hash: &[u8], amount: BigDecimal, + swap_contract_address: &Option, ) -> Box + Send> { unimplemented!() } @@ -152,6 +167,7 @@ impl SwapOps for TestCoin { other_pub: &[u8], secret_hash: &[u8], search_from_block: u64, + swap_contract_address: &Option, ) -> Box, Error = String> + Send> { unimplemented!() } @@ -163,6 +179,7 @@ impl SwapOps for TestCoin { secret_hash: &[u8], tx: &[u8], search_from_block: u64, + swap_contract_address: &Option, ) -> Result, String> { unimplemented!() } @@ -174,6 +191,7 @@ impl SwapOps for TestCoin { secret_hash: &[u8], tx: &[u8], search_from_block: u64, + swap_contract_address: &Option, ) -> Result, String> { unimplemented!() } @@ -216,4 +234,6 @@ impl MmCoin for TestCoin { fn set_requires_notarization(&self, _requires_nota: bool) { unimplemented!() } fn my_unspendable_balance(&self) -> Box + Send> { unimplemented!() } + + fn swap_contract_address(&self) -> Option { unimplemented!() } } diff --git a/mm2src/coins/utxo.rs b/mm2src/coins/utxo.rs index b54afb5fcf..4858aa6e5b 100644 --- a/mm2src/coins/utxo.rs +++ b/mm2src/coins/utxo.rs @@ -82,6 +82,7 @@ const MAX_DER_SIGNATURE_LEN: usize = 72; const COMPRESSED_PUBKEY_LEN: usize = 33; const P2PKH_OUTPUT_LEN: u64 = 34; const MATURE_CONFIRMATIONS_DEFAULT: u32 = 100; +const UTXO_DUST_AMOUNT: u64 = 1000; /// Block count for KMD median time past calculation /// /// # Safety @@ -278,6 +279,16 @@ impl RecentlySpentOutPoints { } } +#[derive(Debug, Deserialize)] +pub enum BlockchainNetwork { + #[serde(rename = "mainnet")] + Mainnet, + #[serde(rename = "testnet")] + Testnet, + #[serde(rename = "regtest")] + Regtest, +} + #[derive(Debug)] pub struct UtxoCoinFields { pub ticker: String, @@ -468,7 +479,21 @@ impl From> for UtxoArc { impl UtxoArc { /// Returns weak reference to the inner UtxoCoinFields - fn downgrade(&self) -> Weak { Arc::downgrade(&self.0) } + fn downgrade(&self) -> UtxoWeak { + let weak = Arc::downgrade(&self.0); + UtxoWeak(weak) + } +} + +#[derive(Clone, Debug)] +pub struct UtxoWeak(Weak); + +impl From> for UtxoWeak { + fn from(weak: Weak) -> Self { UtxoWeak(weak) } +} + +impl UtxoWeak { + fn upgrade(&self) -> Option { self.0.upgrade().map(UtxoArc::from) } } // We can use a shared UTXO lock for all UTXO coins at 1 time. @@ -563,7 +588,6 @@ pub fn zcash_params_path() -> PathBuf { } } -#[cfg(feature = "native")] #[cfg(feature = "native")] pub fn coin_daemon_data_dir(name: &str, is_asset_chain: bool) -> PathBuf { // komodo/util.cpp/GetDefaultDataDir @@ -603,55 +627,25 @@ pub fn coin_daemon_data_dir(name: &str, is_asset_chain: bool) -> PathBuf { #[cfg(not(feature = "native"))] pub fn coin_daemon_data_dir(_name: &str, _is_asset_chain: bool) -> PathBuf { unimplemented!() } -#[cfg(feature = "native")] -/// Returns a path to the native coin wallet configuration. -/// (This path is used in to read the wallet credentials). -/// cf. https://github.com/artemii235/SuperNET/issues/346 -fn confpath(coins_en: &Json) -> Result { - // Documented at https://github.com/jl777/coins#bitcoin-protocol-specific-json - // "USERHOME/" prefix should be replaced with the user's home folder. - let confpathËą = coins_en["confpath"].as_str().unwrap_or("").trim(); - if confpathËą.is_empty() { - let (name, is_asset_chain) = { - match coins_en["asset"].as_str() { - Some(a) => (a, true), - None => ( - try_s!(coins_en["name"].as_str().ok_or("'name' field is not found in config")), - false, - ), - } - }; - - let data_dir = coin_daemon_data_dir(name, is_asset_chain); - - let confname = format!("{}.conf", name); - - return Ok(data_dir.join(&confname[..])); - } - let (confpathËą, rel_to_home) = match confpathËą.strip_prefix("~/") { - Some(stripped) => (stripped, true), - None => match confpathËą.strip_prefix("USERHOME/") { - Some(stripped) => (stripped, true), - None => (confpathËą, false), - }, - }; - - if rel_to_home { - let home = try_s!(home_dir().ok_or("Can not detect the user home directory")); - Ok(home.join(confpathËą)) - } else { - Ok(confpathËą.into()) - } -} - -#[cfg(not(feature = "native"))] -fn confpath(_coins_en: &Json) -> Result { unimplemented!() } - /// Attempts to parse native daemon conf file and return rpcport, rpcuser and rpcpassword #[cfg(feature = "native")] -fn read_native_mode_conf(filename: &dyn AsRef) -> Result<(Option, String, String), String> { +fn read_native_mode_conf( + filename: &dyn AsRef, + network: &BlockchainNetwork, +) -> Result<(Option, String, String), String> { use ini::Ini; + fn read_property<'a>(conf: &'a ini::Ini, network: &BlockchainNetwork, property: &str) -> Option<&'a String> { + let subsection = match network { + BlockchainNetwork::Mainnet => None, + BlockchainNetwork::Testnet => conf.section(Some("test")), + BlockchainNetwork::Regtest => conf.section(Some("regtest")), + }; + subsection + .and_then(|props| props.get(property)) + .or_else(|| conf.general_section().get(property)) + } + let conf: Ini = match Ini::load_from_file(&filename) { Ok(ini) => ini, Err(err) => { @@ -662,16 +656,15 @@ fn read_native_mode_conf(filename: &dyn AsRef) -> Result<(Option, Str ) }, }; - let section = conf.general_section(); - let rpc_port = match section.get("rpcport") { + let rpc_port = match read_property(&conf, network, "rpcport") { Some(port) => port.parse::().ok(), None => None, }; - let rpc_user = try_s!(section.get("rpcuser").ok_or(ERRL!( + let rpc_user = try_s!(read_property(&conf, network, "rpcuser").ok_or(ERRL!( "Conf file {} doesn't have the rpcuser key", filename.as_ref().display() ))); - let rpc_password = try_s!(section.get("rpcpassword").ok_or(ERRL!( + let rpc_password = try_s!(read_property(&conf, network, "rpcpassword").ok_or(ERRL!( "Conf file {} doesn't have the rpcpassword key", filename.as_ref().display() ))); @@ -679,7 +672,10 @@ fn read_native_mode_conf(filename: &dyn AsRef) -> Result<(Option, Str } #[cfg(not(feature = "native"))] -fn read_native_mode_conf(_filename: &dyn AsRef) -> Result<(Option, String, String), String> { +fn read_native_mode_conf( + _filename: &dyn AsRef, + network: &BlockchainNetwork, +) -> Result<(Option, String, String), String> { unimplemented!() } @@ -706,257 +702,419 @@ impl RpcTransportEventHandler for ElectrumProtoVerifier { } } -pub async fn utxo_arc_from_conf_and_request( - ctx: &MmArc, - ticker: &str, - conf: &Json, - req: &Json, - priv_key: &[u8], - dust_amount: u64, -) -> Result { - utxo_fields_from_conf_and_request(ctx, ticker, conf, req, priv_key, dust_amount) - .await - .map(|coin| UtxoArc(Arc::new(coin))) -} +#[async_trait] +pub trait UtxoCoinBuilder { + type ResultCoin; -pub async fn utxo_fields_from_conf_and_request( - ctx: &MmArc, - ticker: &str, - conf: &Json, - req: &Json, - priv_key: &[u8], - dust_amount: u64, -) -> Result { - let checksum_type = if ticker == "GRS" { - ChecksumType::DGROESTL512 - } else if ticker == "SMART" { - ChecksumType::KECCAK256 - } else { - ChecksumType::DSHA256 - }; + async fn build(self) -> Result; - let pub_addr_prefix = conf["pubtype"].as_u64().unwrap_or(if ticker == "BTC" { 0 } else { 60 }) as u8; - let wif_prefix = conf["wiftype"] - .as_u64() - .unwrap_or(if ticker == "BTC" { 128 } else { 188 }) as u8; + fn ctx(&self) -> &MmArc; - let private = Private { - prefix: wif_prefix, - secret: H256::from(priv_key), - compressed: true, - checksum_type, - }; + fn conf(&self) -> &Json; - let key_pair = try_s!(KeyPair::from_private(private)); - let my_address = Address { - prefix: pub_addr_prefix, - t_addr_prefix: conf["taddr"].as_u64().unwrap_or(0) as u8, - hash: key_pair.public().address_hash(), - checksum_type, - }; + fn req(&self) -> &Json; - let address_format = if conf["address_format"].is_null() { - UtxoAddressFormat::Standard - } else { - try_s!(json::from_value(conf["address_format"].clone())) - }; + fn ticker(&self) -> &str; - let decimals = conf["decimals"].as_u64().unwrap_or(8) as u8; - - let rpc_client = match req["method"].as_str() { - Some("enable") => { - if cfg!(feature = "native") { - let native_conf_path = try_s!(confpath(conf)); - let (rpc_port, rpc_user, rpc_password) = try_s!(read_native_mode_conf(&native_conf_path)); - let auth_str = fomat!((rpc_user)":"(rpc_password)); - let rpc_port = match rpc_port { - Some(p) => p, - None => try_s!(conf["rpcport"].as_u64().ok_or(ERRL!( - "Rpc port is not set neither in `coins` file nor in native daemon config" - ))) as u16, - }; - let event_handlers = - vec![ - CoinTransportMetrics::new(ctx.metrics.weak(), ticker.to_owned(), RpcClientType::Native) - .into_shared(), - ]; - let client = Arc::new(NativeClientImpl { - coin_ticker: ticker.to_string(), - uri: fomat!("http://127.0.0.1:"(rpc_port)), - auth: format!("Basic {}", base64_encode(&auth_str, URL_SAFE)), - event_handlers, - request_id: 0u64.into(), - list_unspent_in_progress: false.into(), - list_unspent_subs: AsyncMutex::new(Vec::new()), - coin_decimals: decimals, - }); - - UtxoRpcClientEnum::Native(NativeClient(client)) - } else { - return ERR!("Native UTXO mode is not available in non-native build"); - } - }, - Some("electrum") => { - let (on_connect_tx, on_connect_rx) = mpsc::unbounded(); - let event_handlers = vec![ - CoinTransportMetrics::new(ctx.metrics.weak(), ticker.to_owned(), RpcClientType::Electrum).into_shared(), - ElectrumProtoVerifier { on_connect_tx }.into_shared(), - ]; + fn priv_key(&self) -> &[u8]; + + async fn build_utxo_fields(&self) -> Result { + let checksum_type = self.checksum_type(); + let pub_addr_prefix = self.pub_addr_prefix(); + let p2sh_addr_prefix = self.p2sh_address_prefix(); + let pub_t_addr_prefix = self.pub_t_address_prefix(); + let p2sh_t_addr_prefix = self.p2sh_t_address_prefix(); + + let wif_prefix = self.wif_prefix(); + let private = Private { + prefix: wif_prefix, + secret: H256::from(self.priv_key()), + compressed: true, + checksum_type, + }; + let key_pair = try_s!(KeyPair::from_private(private)); + let my_address = Address { + prefix: pub_addr_prefix, + t_addr_prefix: pub_t_addr_prefix, + hash: key_pair.public().address_hash(), + checksum_type, + }; + let my_script_pubkey = Builder::build_p2pkh(&my_address.hash).to_bytes(); + let address_format = try_s!(self.address_format()); + let rpc_client = try_s!(self.rpc_client().await); + let decimals = try_s!(self.decimals(&rpc_client).await); + + let asset_chain = self.asset_chain(); + let tx_version = self.tx_version(); + let overwintered = self.overwintered(); + let tx_fee = try_s!(self.tx_fee(&rpc_client).await); + let version_group_id = try_s!(self.version_group_id(tx_version, overwintered)); + let consensus_branch_id = try_s!(self.consensus_branch_id(tx_version)); + let signature_version = self.signature_version(); + let fork_id = self.fork_id(); + + // should be sufficient to detect zcash by overwintered flag + let zcash = overwintered; + let initial_history_state = self.initial_history_state(); + + let required_confirmations = self.required_confirmations(); + let requires_notarization = self.requires_notarization(); + + let mature_confirmations = self.mature_confirmations(); + let tx_cache_directory = Some(self.ctx().dbdir().join("TX_CACHE")); + + let is_pos = self.is_pos(); + let segwit = self.segwit(); + let force_min_relay_fee = self.conf()["force_min_relay_fee"].as_bool().unwrap_or(false); + let mtp_block_count = self.mtp_block_count(); + let estimate_fee_mode = self.estimate_fee_mode(); + let dust_amount = self.dust_amount(); + + let _my_script_pubkey = Builder::build_p2pkh(&my_address.hash).to_bytes(); + let coin = UtxoCoinFields { + ticker: self.ticker().to_owned(), + decimals, + rpc_client, + key_pair, + is_pos, + requires_notarization, + overwintered, + pub_addr_prefix, + p2sh_addr_prefix, + pub_t_addr_prefix, + p2sh_t_addr_prefix, + segwit, + wif_prefix, + tx_version, + my_address, + address_format, + asset_chain, + tx_fee, + version_group_id, + consensus_branch_id, + zcash, + checksum_type, + signature_version, + fork_id, + history_sync_state: Mutex::new(initial_history_state), + required_confirmations: required_confirmations.into(), + force_min_relay_fee, + mtp_block_count, + estimate_fee_mode, + dust_amount, + mature_confirmations, + tx_cache_directory, + recently_spent_outpoints: AsyncMutex::new(RecentlySpentOutPoints::new(my_script_pubkey)), + }; + Ok(coin) + } + + fn checksum_type(&self) -> ChecksumType { + match self.ticker() { + "GRS" => ChecksumType::DGROESTL512, + "SMART" => ChecksumType::KECCAK256, + _ => ChecksumType::DSHA256, + } + } + + fn pub_addr_prefix(&self) -> u8 { + let pubtype = self.conf()["pubtype"] + .as_u64() + .unwrap_or(if self.ticker() == "BTC" { 0 } else { 60 }); + pubtype as u8 + } + + fn p2sh_address_prefix(&self) -> u8 { + self.conf()["p2shtype"] + .as_u64() + .unwrap_or(if self.ticker() == "BTC" { 5 } else { 85 }) as u8 + } + + fn pub_t_address_prefix(&self) -> u8 { self.conf()["taddr"].as_u64().unwrap_or(0) as u8 } + + fn p2sh_t_address_prefix(&self) -> u8 { self.conf()["taddr"].as_u64().unwrap_or(0) as u8 } - let mut servers: Vec = try_s!(json::from_value(req["servers"].clone())); - let mut rng = small_rng(); - servers.as_mut_slice().shuffle(&mut rng); - let client = ElectrumClientImpl::new(ticker.to_string(), event_handlers); - for server in servers.iter() { - match client.add_server(server).await { - Ok(_) => (), - Err(e) => log!("Error " (e) " connecting to " [server] ". Address won't be used"), + fn wif_prefix(&self) -> u8 { + let wiftype = self.conf()["wiftype"] + .as_u64() + .unwrap_or(if self.ticker() == "BTC" { 128 } else { 188 }); + wiftype as u8 + } + + fn address_format(&self) -> Result { + let conf = self.conf(); + if conf["address_format"].is_null() { + Ok(UtxoAddressFormat::Standard) + } else { + json::from_value(self.conf()["address_format"].clone()).map_err(|e| ERRL!("{}", e)) + } + } + + async fn decimals(&self, _rpc_client: &UtxoRpcClientEnum) -> Result { + Ok(self.conf()["decimals"].as_u64().unwrap_or(8) as u8) + } + + fn asset_chain(&self) -> bool { self.conf()["asset"].as_str().is_some() } + + fn tx_version(&self) -> i32 { self.conf()["txversion"].as_i64().unwrap_or(1) as i32 } + + fn overwintered(&self) -> bool { self.conf()["overwintered"].as_u64().unwrap_or(0) == 1 } + + async fn tx_fee(&self, rpc_client: &UtxoRpcClientEnum) -> Result { + let tx_fee = match self.conf()["txfee"].as_u64() { + None => TxFee::Fixed(1000), + Some(0) => { + let fee_method = match &rpc_client { + UtxoRpcClientEnum::Electrum(_) => EstimateFeeMethod::Standard, + UtxoRpcClientEnum::Native(client) => try_s!(client.detect_fee_method().compat().await), }; - } + TxFee::Dynamic(fee_method) + }, + Some(fee) => TxFee::Fixed(fee), + }; + Ok(tx_fee) + } - let mut attempts = 0i32; - while !client.is_connected().await { - if attempts >= 10 { - return ERR!("Failed to connect to at least 1 of {:?} in 5 seconds.", servers); + fn version_group_id(&self, tx_version: i32, overwintered: bool) -> Result { + let version_group_id = match self.conf()["version_group_id"].as_str() { + Some(mut s) => { + if s.starts_with("0x") { + s = &s[2..]; } + let bytes = try_s!(hex::decode(s)); + u32::from_be_bytes(try_s!(bytes.as_slice().try_into())) + }, + None => { + if tx_version == 3 && overwintered { + 0x03c4_8270 + } else if tx_version == 4 && overwintered { + 0x892f_2085 + } else { + 0 + } + }, + }; + Ok(version_group_id) + } - Timer::sleep(0.5).await; - attempts += 1; - } + fn consensus_branch_id(&self, tx_version: i32) -> Result { + let consensus_branch_id = match self.conf()["consensus_branch_id"].as_str() { + Some(mut s) => { + if s.starts_with("0x") { + s = &s[2..]; + } + let bytes = try_s!(hex::decode(s)); + u32::from_be_bytes(try_s!(bytes.as_slice().try_into())) + }, + None => match tx_version { + 3 => 0x5ba8_1b19, + 4 => 0x76b8_09bb, + _ => 0, + }, + }; + Ok(consensus_branch_id) + } + + fn signature_version(&self) -> SignatureVersion { + if self.ticker() == "BCH" { + SignatureVersion::ForkId + } else { + SignatureVersion::Base + } + } + + fn fork_id(&self) -> u32 { + if self.ticker() == "BCH" { + 0x40 + } else { + 0 + } + } - let client = Arc::new(client); + fn required_confirmations(&self) -> u64 { + // param from request should override the config + self.req()["required_confirmations"] + .as_u64() + .unwrap_or_else(|| self.conf()["required_confirmations"].as_u64().unwrap_or(1)) + } - let weak_client = Arc::downgrade(&client); - let client_name = format!("{} GUI/MM2 {}", ctx.gui().unwrap_or("UNKNOWN"), MM_VERSION); - spawn_electrum_version_loop(weak_client, on_connect_rx, client_name); + fn requires_notarization(&self) -> AtomicBool { + self.req()["requires_notarization"] + .as_bool() + .unwrap_or_else(|| self.conf()["requires_notarization"].as_bool().unwrap_or(false)) + .into() + } - try_s!(wait_for_protocol_version_checked(&client).await); + fn mature_confirmations(&self) -> u32 { + self.conf()["mature_confirmations"] + .as_u64() + .map(|x| x as u32) + .unwrap_or(MATURE_CONFIRMATIONS_DEFAULT) + } - let weak_client = Arc::downgrade(&client); - spawn_electrum_ping_loop(weak_client, servers); + fn is_pos(&self) -> bool { self.conf()["isPoS"].as_u64() == Some(1) } - UtxoRpcClientEnum::Electrum(ElectrumClient(client)) - }, - _ => return ERR!("utxo_arc_from_conf_and_request should be called only by enable or electrum requests"), - }; - let asset_chain = conf["asset"].as_str().is_some(); - let tx_version = conf["txversion"].as_i64().unwrap_or(1) as i32; - let overwintered = conf["overwintered"].as_u64().unwrap_or(0) == 1; - - let tx_fee = match conf["txfee"].as_u64() { - None => TxFee::Fixed(1000), - Some(0) => { - let fee_method = match &rpc_client { - UtxoRpcClientEnum::Electrum(_) => EstimateFeeMethod::Standard, - UtxoRpcClientEnum::Native(client) => try_s!(client.detect_fee_method().compat().await), + fn segwit(&self) -> bool { self.conf()["segwit"].as_bool().unwrap_or(false) } + + fn mtp_block_count(&self) -> NonZeroU64 { + json::from_value(self.conf()["mtp_block_count"].clone()).unwrap_or(KMD_MTP_BLOCK_COUNT) + } + + fn estimate_fee_mode(&self) -> Option { + json::from_value(self.conf()["estimate_fee_mode"].clone()).unwrap_or(None) + } + + fn dust_amount(&self) -> u64 { UTXO_DUST_AMOUNT } + + fn network(&self) -> Result { + let conf = self.conf(); + if !conf["network"].is_null() { + return json::from_value(conf["network"].clone()).map_err(|e| ERRL!("{}", e)); + } + Ok(BlockchainNetwork::Mainnet) + } + + fn initial_history_state(&self) -> HistorySyncState { + if self.req()["tx_history"].as_bool().unwrap_or(false) { + HistorySyncState::NotStarted + } else { + HistorySyncState::NotEnabled + } + } + + async fn rpc_client(&self) -> Result { + match self.req()["method"].as_str() { + Some("enable") => { + if cfg!(feature = "native") { + let native = try_s!(self.native_client()); + Ok(UtxoRpcClientEnum::Native(native)) + } else { + return ERR!("Native UTXO mode is not available in non-native build"); + } + }, + Some("electrum") => { + let electrum = try_s!(self.electrum_client().await); + Ok(UtxoRpcClientEnum::Electrum(electrum)) + }, + _ => ERR!("Expected enable or electrum request"), + } + } + + async fn electrum_client(&self) -> Result { + let (on_connect_tx, on_connect_rx) = mpsc::unbounded(); + let ticker = self.ticker().to_owned(); + let ctx = self.ctx(); + let event_handlers = vec![ + CoinTransportMetrics::new(ctx.metrics.weak(), ticker.clone(), RpcClientType::Electrum).into_shared(), + ElectrumProtoVerifier { on_connect_tx }.into_shared(), + ]; + + let mut servers: Vec = try_s!(json::from_value(self.req()["servers"].clone())); + let mut rng = small_rng(); + servers.as_mut_slice().shuffle(&mut rng); + let client = ElectrumClientImpl::new(ticker, event_handlers); + for server in servers.iter() { + match client.add_server(server).await { + Ok(_) => (), + Err(e) => log!("Error " (e) " connecting to " [server] ". Address won't be used"), }; - TxFee::Dynamic(fee_method) - }, - Some(fee) => TxFee::Fixed(fee), - }; - let version_group_id = match conf["version_group_id"].as_str() { - Some(mut s) => { - if s.starts_with("0x") { - s = &s[2..]; - } - let bytes = try_s!(hex::decode(s)); - u32::from_be_bytes(try_s!(bytes.as_slice().try_into())) - }, - None => { - if tx_version == 3 && overwintered { - 0x03c4_8270 - } else if tx_version == 4 && overwintered { - 0x892f_2085 - } else { - 0 - } - }, - }; + } - let consensus_branch_id = match conf["consensus_branch_id"].as_str() { - Some(mut s) => { - if s.starts_with("0x") { - s = &s[2..]; + let mut attempts = 0i32; + while !client.is_connected().await { + if attempts >= 10 { + return ERR!("Failed to connect to at least 1 of {:?} in 5 seconds.", servers); } - let bytes = try_s!(hex::decode(s)); - u32::from_be_bytes(try_s!(bytes.as_slice().try_into())) - }, - None => match tx_version { - 3 => 0x5ba8_1b19, - 4 => 0x76b8_09bb, - _ => 0, - }, - }; - let (signature_version, fork_id) = if ticker == "BCH" { - (SignatureVersion::ForkId, 0x40) - } else { - (SignatureVersion::Base, 0) - }; - // should be sufficient to detect zcash by overwintered flag - let zcash = overwintered; + Timer::sleep(0.5).await; + attempts += 1; + } - let initial_history_state = if req["tx_history"].as_bool().unwrap_or(false) { - HistorySyncState::NotStarted - } else { - HistorySyncState::NotEnabled - }; + let client = Arc::new(client); - // param from request should override the config - let required_confirmations = req["required_confirmations"] - .as_u64() - .unwrap_or_else(|| conf["required_confirmations"].as_u64().unwrap_or(1)); - let requires_notarization = req["requires_notarization"] - .as_bool() - .unwrap_or_else(|| conf["requires_notarization"].as_bool().unwrap_or(false)) - .into(); - - let mature_confirmations = conf["mature_confirmations"] - .as_u64() - .map(|x| x as u32) - .unwrap_or(MATURE_CONFIRMATIONS_DEFAULT); - let tx_cache_directory = Some(ctx.dbdir().join("TX_CACHE")); - - let my_script_pubkey = Builder::build_p2pkh(&my_address.hash).to_bytes(); - - let coin = UtxoCoinFields { - ticker: ticker.into(), - decimals, - rpc_client, - key_pair, - is_pos: conf["isPoS"].as_u64() == Some(1), - requires_notarization, - overwintered, - pub_addr_prefix, - p2sh_addr_prefix: conf["p2shtype"] - .as_u64() - .unwrap_or(if ticker == "BTC" { 5 } else { 85 }) as u8, - pub_t_addr_prefix: conf["taddr"].as_u64().unwrap_or(0) as u8, - p2sh_t_addr_prefix: conf["taddr"].as_u64().unwrap_or(0) as u8, - segwit: conf["segwit"].as_bool().unwrap_or(false), - wif_prefix, - tx_version, - my_address, - address_format, - asset_chain, - tx_fee, - version_group_id, - consensus_branch_id, - zcash, - checksum_type, - signature_version, - fork_id, - history_sync_state: Mutex::new(initial_history_state), - required_confirmations: required_confirmations.into(), - force_min_relay_fee: conf["force_min_relay_fee"].as_bool().unwrap_or(false), - mtp_block_count: json::from_value(conf["mtp_block_count"].clone()).unwrap_or(KMD_MTP_BLOCK_COUNT), - estimate_fee_mode: json::from_value(conf["estimate_fee_mode"].clone()).unwrap_or(None), - dust_amount, - mature_confirmations, - tx_cache_directory, - recently_spent_outpoints: AsyncMutex::new(RecentlySpentOutPoints::new(my_script_pubkey)), - }; - Ok(coin) + let weak_client = Arc::downgrade(&client); + let client_name = format!("{} GUI/MM2 {}", ctx.gui().unwrap_or("UNKNOWN"), MM_VERSION); + spawn_electrum_version_loop(weak_client, on_connect_rx, client_name); + + try_s!(wait_for_protocol_version_checked(&client).await); + + let weak_client = Arc::downgrade(&client); + spawn_electrum_ping_loop(weak_client, servers); + + Ok(ElectrumClient(client)) + } + + #[cfg(feature = "native")] + fn native_client(&self) -> Result { + let native_conf_path = try_s!(self.confpath()); + let network = try_s!(self.network()); + let (rpc_port, rpc_user, rpc_password) = try_s!(read_native_mode_conf(&native_conf_path, &network)); + let auth_str = fomat!((rpc_user)":"(rpc_password)); + let rpc_port = match rpc_port { + Some(p) => p, + None => try_s!(self.conf()["rpcport"].as_u64().ok_or(ERRL!( + "Rpc port is not set neither in `coins` file nor in native daemon config" + ))) as u16, + }; + + let ctx = self.ctx(); + let coin_ticker = self.ticker().to_owned(); + let event_handlers = + vec![ + CoinTransportMetrics::new(ctx.metrics.weak(), coin_ticker.clone(), RpcClientType::Native).into_shared(), + ]; + let client = Arc::new(NativeClientImpl { + coin_ticker, + uri: fomat!("http://127.0.0.1:"(rpc_port)), + auth: format!("Basic {}", base64_encode(&auth_str, URL_SAFE)), + event_handlers, + request_id: 0u64.into(), + list_unspent_in_progress: false.into(), + list_unspent_subs: AsyncMutex::new(Vec::new()), + }); + + Ok(NativeClient(client)) + } + + #[cfg(feature = "native")] + fn confpath(&self) -> Result { + let conf = self.conf(); + // Documented at https://github.com/jl777/coins#bitcoin-protocol-specific-json + // "USERHOME/" prefix should be replaced with the user's home folder. + let declared_confpath = match self.conf()["confpath"].as_str() { + Some(path) if !path.is_empty() => path.trim(), + _ => { + let (name, is_asset_chain) = { + match conf["asset"].as_str() { + Some(a) => (a, true), + None => ( + try_s!(conf["name"].as_str().ok_or("'name' field is not found in config")), + false, + ), + } + }; + let data_dir = coin_daemon_data_dir(name, is_asset_chain); + let confname = format!("{}.conf", name); + + return Ok(data_dir.join(&confname[..])); + }, + }; + + let (confpath, rel_to_home) = match declared_confpath.strip_prefix("~/") { + Some(stripped) => (stripped, true), + None => match declared_confpath.strip_prefix("USERHOME/") { + Some(stripped) => (stripped, true), + None => (declared_confpath, false), + }, + }; + + if rel_to_home { + let home = try_s!(home_dir().ok_or("Can not detect the user home directory")); + Ok(home.join(confpath)) + } else { + Ok(confpath.into()) + } + } } /// Ping the electrum servers every 30 seconds to prevent them from disconnecting us. @@ -1192,8 +1350,9 @@ where return ERR!("rewards info can be obtained for KMD only"); } - let rpc_client = &coin.as_ref().rpc_client; - let mut unspents = try_s!(rpc_client.list_unspent(&coin.as_ref().my_address).compat().await); + let utxo = coin.as_ref(); + let rpc_client = &utxo.rpc_client; + let mut unspents = try_s!(rpc_client.list_unspent(&utxo.my_address, utxo.decimals).compat().await); // list_unspent_ordered() returns ordered from lowest to highest by value unspent outputs. // reverse it to reorder from highest to lowest outputs. unspents.reverse(); diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index 3bbb018893..01894b4125 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -1,10 +1,98 @@ use super::*; -use crate::{SwapOps, ValidateAddressResult}; +use crate::{eth, SwapOps, ValidateAddressResult}; use common::mm_metrics::MetricsArc; +use ethereum_types::H160; use futures::{FutureExt, TryFutureExt}; pub const QTUM_STANDARD_DUST: u64 = 1000; +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "format")] +pub enum QtumAddressFormat { + /// Standard Qtum/UTXO address format. + #[serde(rename = "wallet")] + Wallet, + /// Contract address format. The same as used in ETH/ERC20. + /// Note starts with "0x" prefix. + #[serde(rename = "contract")] + Contract, +} + +pub trait QtumBasedCoin: AsRef { + fn convert_to_address(&self, from: &str, to_address_format: Json) -> Result { + let to_address_format: QtumAddressFormat = + json::from_value(to_address_format).map_err(|e| ERRL!("Error on parse Qtum address format {:?}", e))?; + let from_address = try_s!(self.utxo_address_from_any_format(from)); + match to_address_format { + QtumAddressFormat::Wallet => Ok(from_address.to_string()), + QtumAddressFormat::Contract => Ok(display_as_contract_address(from_address)), + } + } + + /// Try to parse address from either wallet (UTXO) format or contract format. + fn utxo_address_from_any_format(&self, from: &str) -> Result { + let utxo_err = match Address::from_str(from) { + Ok(addr) => { + let is_p2pkh = addr.prefix == self.as_ref().pub_addr_prefix + && addr.t_addr_prefix == self.as_ref().pub_t_addr_prefix; + if is_p2pkh { + return Ok(addr); + } + "Address has invalid prefixes".to_string() + }, + Err(e) => e.to_string(), + }; + let contract_err = match contract_addr_from_str(from) { + Ok(contract_addr) => return Ok(self.utxo_addr_from_contract_addr(contract_addr)), + Err(e) => e, + }; + ERR!( + "error on parse wallet address: {:?}, error on parse contract address: {:?}", + utxo_err, + contract_err, + ) + } + + fn utxo_addr_from_contract_addr(&self, address: H160) -> Address { + let utxo = self.as_ref(); + Address { + prefix: utxo.pub_addr_prefix, + t_addr_prefix: utxo.pub_t_addr_prefix, + hash: address.0.into(), + checksum_type: utxo.checksum_type, + } + } + + fn my_addr_as_contract_addr(&self) -> H160 { contract_addr_from_utxo_addr(self.as_ref().my_address.clone()) } + + fn utxo_address_from_contract_addr(&self, address: H160) -> Address { + let utxo = self.as_ref(); + Address { + prefix: utxo.pub_addr_prefix, + t_addr_prefix: utxo.pub_t_addr_prefix, + hash: address.0.into(), + checksum_type: utxo.checksum_type, + } + } + + fn contract_address_from_raw_pubkey(&self, pubkey: &[u8]) -> Result { + let utxo = self.as_ref(); + let qtum_address = try_s!(utxo_common::address_from_raw_pubkey( + pubkey, + utxo.pub_addr_prefix, + utxo.pub_t_addr_prefix, + utxo.checksum_type + )); + Ok(qtum::contract_addr_from_utxo_addr(qtum_address)) + } + + fn is_qtum_unspent_mature(&self, output: &RpcTransaction) -> bool { + let is_qrc20_coinbase = output.vout.iter().any(|x| x.is_empty()); + let is_coinbase = output.is_coinbase() || is_qrc20_coinbase; + !is_coinbase || output.confirmations >= self.as_ref().mature_confirmations + } +} + #[derive(Clone, Debug)] pub struct QtumCoin { utxo_arc: UtxoArc, @@ -29,10 +117,12 @@ pub async fn qtum_coin_from_conf_and_request( req: &Json, priv_key: &[u8], ) -> Result { - let inner = try_s!(utxo_arc_from_conf_and_request(ctx, ticker, conf, req, priv_key, QTUM_STANDARD_DUST).await); - Ok(inner.into()) + let coin: QtumCoin = try_s!(utxo_common::utxo_arc_from_conf_and_request(ctx, ticker, conf, req, priv_key).await); + Ok(coin) } +impl QtumBasedCoin for QtumCoin {} + #[cfg_attr(test, mockable)] #[async_trait] impl UtxoCommonOps for QtumCoin { @@ -58,9 +148,7 @@ impl UtxoCommonOps for QtumCoin { async fn get_current_mtp(&self) -> Result { utxo_common::get_current_mtp(&self.utxo_arc).await } - fn is_unspent_mature(&self, output: &RpcTransaction) -> bool { - is_qtum_unspent_mature(self.utxo_arc.mature_confirmations, output) - } + fn is_unspent_mature(&self, output: &RpcTransaction) -> bool { self.is_qtum_unspent_mature(output) } async fn generate_transaction( &self, @@ -154,6 +242,7 @@ impl SwapOps for QtumCoin { taker_pub: &[u8], secret_hash: &[u8], amount: BigDecimal, + _swap_contract_address: &Option, ) -> TransactionFut { utxo_common::send_maker_payment(self.clone(), time_lock, taker_pub, secret_hash, amount) } @@ -164,6 +253,7 @@ impl SwapOps for QtumCoin { maker_pub: &[u8], secret_hash: &[u8], amount: BigDecimal, + _swap_contract_address: &Option, ) -> TransactionFut { utxo_common::send_taker_payment(self.clone(), time_lock, maker_pub, secret_hash, amount) } @@ -174,6 +264,7 @@ impl SwapOps for QtumCoin { time_lock: u32, taker_pub: &[u8], secret: &[u8], + _swap_contract_address: &Option, ) -> TransactionFut { utxo_common::send_maker_spends_taker_payment(self.clone(), taker_payment_tx, time_lock, taker_pub, secret) } @@ -184,6 +275,7 @@ impl SwapOps for QtumCoin { time_lock: u32, maker_pub: &[u8], secret: &[u8], + _swap_contract_address: &Option, ) -> TransactionFut { utxo_common::send_taker_spends_maker_payment(self.clone(), maker_payment_tx, time_lock, maker_pub, secret) } @@ -194,6 +286,7 @@ impl SwapOps for QtumCoin { time_lock: u32, maker_pub: &[u8], secret_hash: &[u8], + _swap_contract_address: &Option, ) -> TransactionFut { utxo_common::send_taker_refunds_payment(self.clone(), taker_payment_tx, time_lock, maker_pub, secret_hash) } @@ -204,6 +297,7 @@ impl SwapOps for QtumCoin { time_lock: u32, taker_pub: &[u8], secret_hash: &[u8], + _swap_contract_address: &Option, ) -> TransactionFut { utxo_common::send_maker_refunds_payment(self.clone(), maker_payment_tx, time_lock, taker_pub, secret_hash) } @@ -224,6 +318,7 @@ impl SwapOps for QtumCoin { maker_pub: &[u8], priv_bn_hash: &[u8], amount: BigDecimal, + _swap_contract_address: &Option, ) -> Box + Send> { utxo_common::validate_maker_payment(self, payment_tx, time_lock, maker_pub, priv_bn_hash, amount) } @@ -235,6 +330,7 @@ impl SwapOps for QtumCoin { taker_pub: &[u8], priv_bn_hash: &[u8], amount: BigDecimal, + _swap_contract_address: &Option, ) -> Box + Send> { utxo_common::validate_taker_payment(self, payment_tx, time_lock, taker_pub, priv_bn_hash, amount) } @@ -245,6 +341,7 @@ impl SwapOps for QtumCoin { other_pub: &[u8], secret_hash: &[u8], _search_from_block: u64, + _swap_contract_address: &Option, ) -> Box, Error = String> + Send> { utxo_common::check_if_my_payment_sent(self.clone(), time_lock, other_pub, secret_hash) } @@ -256,6 +353,7 @@ impl SwapOps for QtumCoin { secret_hash: &[u8], tx: &[u8], search_from_block: u64, + _swap_contract_address: &Option, ) -> Result, String> { utxo_common::search_for_swap_tx_spend_my( &self.utxo_arc, @@ -274,6 +372,7 @@ impl SwapOps for QtumCoin { secret_hash: &[u8], tx: &[u8], search_from_block: u64, + _swap_contract_address: &Option, ) -> Result, String> { utxo_common::search_for_swap_tx_spend_other( &self.utxo_arc, @@ -325,7 +424,13 @@ impl MarketCoinOps for QtumCoin { ) } - fn wait_for_tx_spend(&self, transaction: &[u8], wait_until: u64, from_block: u64) -> TransactionFut { + fn wait_for_tx_spend( + &self, + transaction: &[u8], + wait_until: u64, + from_block: u64, + _swap_contract_address: &Option, + ) -> TransactionFut { utxo_common::wait_for_tx_spend(&self.utxo_arc, transaction, wait_until, from_block) } @@ -361,7 +466,7 @@ impl MmCoin for QtumCoin { /// Check if the `to_address_format` is standard and if the `from` address is standard UTXO address. fn convert_to_address(&self, from: &str, to_address_format: Json) -> Result { - convert_qtum_address(&self.utxo_arc.ticker, from, to_address_format) + QtumBasedCoin::convert_to_address(self, from, to_address_format) } fn validate_address(&self, address: &str) -> ValidateAddressResult { utxo_common::validate_address(self, address) } @@ -389,85 +494,17 @@ impl MmCoin for QtumCoin { fn my_unspendable_balance(&self) -> Box + Send> { Box::new(utxo_common::my_unspendable_balance(self.clone()).boxed().compat()) } -} - -pub fn is_qtum_unspent_mature(mature_confirmations: u32, output: &RpcTransaction) -> bool { - let is_qrc20_coinbase = output.vout.iter().any(|x| x.is_empty()); - let is_coinbase = output.is_coinbase() || is_qrc20_coinbase; - !is_coinbase || output.confirmations >= mature_confirmations -} - -pub fn convert_qtum_address(coin: &str, from: &str, to_address_format: Json) -> Result { - let to_address_format: UtxoAddressFormat = - json::from_value(to_address_format).map_err(|e| ERRL!("Error on parse UTXO address format {:?}", e))?; - match to_address_format { - UtxoAddressFormat::Standard => (), - _ => return ERR!("{} supports standard UTXO address format only", coin), - } - let from_address = try_s!(Address::from_str(from)); - Ok(from_address.to_string()) + fn swap_contract_address(&self) -> Option { utxo_common::swap_contract_address() } } -#[cfg(test)] -mod tests { - use super::*; - use rpc::v1::types::{ScriptType, SignedTransactionOutput, TransactionOutputScript}; - - #[test] - fn test_is_unspent_mature() { - let empty_output = SignedTransactionOutput { - value: 0., - n: 0, - script: TransactionOutputScript { - asm: "".into(), - hex: "".into(), - req_sigs: 0, - script_type: ScriptType::NonStandard, - addresses: vec![], - }, - }; - let real_output = SignedTransactionOutput { - value: 117.02430015, - n: 1, - script: TransactionOutputScript { - asm: "03e71b9c152bb233ddfe58f20056715c51b054a1823e0aba108e6f1cea0ceb89c8 OP_CHECKSIG".into(), - hex: "2103e71b9c152bb233ddfe58f20056715c51b054a1823e0aba108e6f1cea0ceb89c8ac".into(), - req_sigs: 0, - script_type: ScriptType::PubKey, - addresses: vec![], - }, - }; +/// Parse contract address (H160) from string. +/// Qtum Contract addresses have another checksum verification algorithm, because of this do not use [`eth::valid_addr_from_str`]. +pub fn contract_addr_from_str(addr: &str) -> Result { eth::addr_from_str(addr) } - let mut tx = RpcTransaction { - hex: Default::default(), - txid: "47d983175720ba2a67f36d0e1115a129351a2f340bdde6ecb6d6029e138fe920".into(), - hash: None, - size: Default::default(), - vsize: Default::default(), - version: 2, - locktime: 0, - vin: vec![], - vout: vec![empty_output, real_output], - blockhash: "c23882939ff695be36546ea998eb585e962b043396e4d91959477b9796ceb9e1".into(), - confirmations: 421, - rawconfirmations: None, - time: 1590671504, - blocktime: 1590671504, - height: None, - }; - - // output is coinbase and has confirmations < QTUM_MATURE_CONFIRMATIONS - assert_eq!(is_qtum_unspent_mature(500, &tx), false); +pub fn contract_addr_from_utxo_addr(address: Address) -> H160 { address.hash.take().into() } - tx.confirmations = 501; - // output is coinbase but has confirmations > QTUM_MATURE_CONFIRMATIONS - assert!(is_qtum_unspent_mature(500, &tx)); - - tx.confirmations = 421; - // remove empty output - tx.vout.remove(0); - // output is not coinbase - assert!(is_qtum_unspent_mature(500, &tx)); - } +pub fn display_as_contract_address(address: Address) -> String { + let address = qtum::contract_addr_from_utxo_addr(address); + format!("{:#02x}", address) } diff --git a/mm2src/coins/utxo/rpc_clients.rs b/mm2src/coins/utxo/rpc_clients.rs index 40513cd696..887041c102 100644 --- a/mm2src/coins/utxo/rpc_clients.rs +++ b/mm2src/coins/utxo/rpc_clients.rs @@ -57,6 +57,13 @@ use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader use tokio_rustls::{client::TlsStream, TlsConnector}; #[cfg(feature = "native")] use webpki_roots::TLS_SERVER_ROOTS; +pub type AddressesByLabelResult = HashMap; + +#[derive(Debug, Deserialize)] +pub struct AddressPurpose { + purpose: String, +} + /// Skips the server certificate verification on TLS connection pub struct NoCertificateVerification {} @@ -172,7 +179,7 @@ pub type UtxoRpcRes = Box + Send + 'stat /// Common operations that both types of UTXO clients have but implement them differently pub trait UtxoRpcClientOps: fmt::Debug + Send + Sync + 'static { - fn list_unspent(&self, address: &Address) -> UtxoRpcRes>; + fn list_unspent(&self, address: &Address, decimals: u8) -> UtxoRpcRes>; fn send_transaction(&self, tx: &UtxoTx) -> UtxoRpcRes; @@ -367,8 +374,6 @@ pub struct NativeClientImpl { pub request_id: AtomicU64, pub list_unspent_in_progress: AtomicBool, pub list_unspent_subs: AsyncMutex>>>, - /// coin decimals used to convert the decimal amount returned from daemon to correct satoshis amount - pub coin_decimals: u8, } #[cfg(test)] @@ -382,7 +387,6 @@ impl Default for NativeClientImpl { request_id: Default::default(), list_unspent_in_progress: Default::default(), list_unspent_subs: Default::default(), - coin_decimals: 8, } } } @@ -454,8 +458,7 @@ impl JsonRpcClient for NativeClientImpl { #[cfg_attr(test, mockable)] impl UtxoRpcClientOps for NativeClient { - fn list_unspent(&self, address: &Address) -> UtxoRpcRes> { - let decimals = self.coin_decimals; + fn list_unspent(&self, address: &Address, decimals: u8) -> UtxoRpcRes> { let fut = self .list_unspent_impl(0, std::i32::MAX, vec![address.to_string()]) .map_err(|e| ERRL!("{}", e)) @@ -767,6 +770,12 @@ impl NativeClientImpl { rpc_func!(self, "sendtoaddress", addr, amount) } + /// Returns the list of addresses assigned the specified label. + /// https://developer.bitcoin.org/reference/rpc/getaddressesbylabel.html + pub fn get_addresses_by_label(&self, label: &str) -> RpcRes { + rpc_func!(self, "getaddressesbylabel", label) + } + /// https://developer.bitcoin.org/reference/rpc/getnetworkinfo.html pub fn get_network_info(&self) -> RpcRes { rpc_func!(self, "getnetworkinfo") } @@ -1457,7 +1466,7 @@ impl ElectrumClient { #[cfg_attr(test, mockable)] impl UtxoRpcClientOps for ElectrumClient { - fn list_unspent(&self, address: &Address) -> UtxoRpcRes> { + fn list_unspent(&self, address: &Address, _decimals: u8) -> UtxoRpcRes> { let script = Builder::build_p2pkh(&address.hash); let script_hash = electrum_script_hash(&script); Box::new( diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 40e5176e11..3c9035da22 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -5,12 +5,12 @@ use chain::constants::SEQUENCE_FINAL; use chain::{OutPoint, TransactionInput, TransactionOutput}; use common::executor::Timer; use common::jsonrpc_client::{JsonRpcError, JsonRpcErrorType}; +use common::log::{error, info}; use common::mm_ctx::MmArc; use common::mm_metrics::MetricsArc; use futures::compat::Future01CompatExt; use futures::future::{FutureExt, TryFutureExt}; use futures01::future::Either; -#[cfg(feature = "native")] use futures01::Future; use gstuff::now_ms; use keys::bytes::Bytes; use keys::{Address, KeyPair, Public, Type}; @@ -51,6 +51,93 @@ lazy_static! { pub const HISTORY_TOO_LARGE_ERR_CODE: i64 = -1; +pub struct UtxoArcBuilder<'a> { + ctx: &'a MmArc, + ticker: &'a str, + conf: &'a Json, + req: &'a Json, + priv_key: &'a [u8], +} + +impl<'a> UtxoArcBuilder<'a> { + pub fn new( + ctx: &'a MmArc, + ticker: &'a str, + conf: &'a Json, + req: &'a Json, + priv_key: &'a [u8], + ) -> UtxoArcBuilder<'a> { + UtxoArcBuilder { + ctx, + ticker, + conf, + req, + priv_key, + } + } +} + +#[async_trait] +impl UtxoCoinBuilder for UtxoArcBuilder<'_> { + type ResultCoin = UtxoArc; + + async fn build(self) -> Result { + let utxo = try_s!(self.build_utxo_fields().await); + Ok(UtxoArc(Arc::new(utxo))) + } + + fn ctx(&self) -> &MmArc { self.ctx } + + fn conf(&self) -> &Json { self.conf } + + fn req(&self) -> &Json { self.req } + + fn ticker(&self) -> &str { self.ticker } + + fn priv_key(&self) -> &[u8] { self.priv_key } +} + +pub async fn utxo_arc_from_conf_and_request( + ctx: &MmArc, + ticker: &str, + conf: &Json, + req: &Json, + priv_key: &[u8], +) -> Result +where + T: From + AsRef + UtxoCommonOps + Send + Sync + 'static, +{ + let builder = UtxoArcBuilder::new(ctx, ticker, conf, req, priv_key); + let utxo_arc = try_s!(builder.build().await); + + let merge_params: Option = try_s!(json::from_value(req["utxo_merge_params"].clone())); + if let Some(merge_params) = merge_params { + let weak = utxo_arc.downgrade(); + let merge_loop = merge_utxo_loop::( + weak, + merge_params.merge_at, + merge_params.check_every, + merge_params.max_merge_at_once, + ); + info!("Starting UTXO merge loop for coin {}", ticker); + spawn(merge_loop); + } + Ok(T::from(utxo_arc)) +} + +fn ten_f64() -> f64 { 10. } + +fn one_hundred() -> usize { 100 } + +#[derive(Debug, Deserialize)] +struct UtxoMergeParams { + merge_at: usize, + #[serde(default = "ten_f64")] + check_every: f64, + #[serde(default = "one_hundred")] + max_merge_at_once: usize, +} + pub async fn get_tx_fee(coin: &UtxoCoinFields) -> Result { match &coin.tx_fee { TxFee::Fixed(fee) => Ok(ActualTxFee::Fixed(*fee)), @@ -1865,6 +1952,9 @@ where } } +/// Swap contract address is not used by standard UTXO coins. +pub fn swap_contract_address() -> Option { None } + /// Convert satoshis to BigDecimal amount of coin units pub fn big_decimal_from_sat(satoshis: i64, decimals: u8) -> BigDecimal { BigDecimal::from(satoshis) / BigDecimal::from(10u64.pow(decimals as u32)) @@ -2092,10 +2182,11 @@ pub async fn list_unspent_ordered<'a, T>( where T: AsRef, { + let decimals = coin.as_ref().decimals; let mut unspents = try_s!( coin.as_ref() .rpc_client - .list_unspent(address) + .list_unspent(address, decimals) .map_err(|e| ERRL!("{}", e)) .compat() .await @@ -2117,3 +2208,48 @@ where unspents.dedup_by(|one, another| one.outpoint == another.outpoint); Ok((unspents, recently_spent)) } + +async fn merge_utxo_loop(weak: UtxoWeak, merge_at: usize, check_every: f64, max_merge_at_once: usize) +where + T: From + AsRef + UtxoCommonOps, +{ + loop { + Timer::sleep(check_every).await; + + let coin = match weak.upgrade() { + Some(arc) => T::from(arc), + None => break, + }; + + let ticker = &coin.as_ref().ticker; + let (unspents, recently_spent) = match coin.list_unspent_ordered(&coin.as_ref().my_address).await { + Ok((unspents, recently_spent)) => (unspents, recently_spent), + Err(e) => { + error!("Error {} on list_unspent_ordered of coin {}", e, ticker); + continue; + }, + }; + if unspents.len() >= merge_at { + let unspents: Vec<_> = unspents.into_iter().take(max_merge_at_once).collect(); + info!("Trying to merge {} UTXOs of coin {}", unspents.len(), ticker); + let value = unspents.iter().fold(0, |sum, unspent| sum + unspent.value); + let script_pubkey = Builder::build_p2pkh(&coin.as_ref().my_address.hash).to_bytes(); + let output = TransactionOutput { value, script_pubkey }; + let merge_tx_fut = generate_and_send_tx( + &coin, + unspents, + vec![output], + FeePolicy::DeductFromOutput(0), + recently_spent, + ); + match merge_tx_fut.await { + Ok(tx) => info!( + "UTXO merge successful for coin {}, tx_hash {:?}", + ticker, + tx.hash().reversed() + ), + Err(e) => error!("Error {} on UTXO merge attempt for coin {}", e, ticker), + } + } + } +} diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index b953590a27..d0e25f5ef1 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -1,11 +1,8 @@ use super::*; use crate::{SwapOps, ValidateAddressResult}; -use common::log::{error, info}; use common::mm_metrics::MetricsArc; use futures::{FutureExt, TryFutureExt}; -pub const UTXO_STANDARD_DUST: u64 = 1000; - #[derive(Clone, Debug)] pub struct UtxoStandardCoin { utxo_arc: UtxoArc, @@ -19,27 +16,10 @@ impl From for UtxoStandardCoin { fn from(coin: UtxoArc) -> UtxoStandardCoin { UtxoStandardCoin { utxo_arc: coin } } } -impl From> for UtxoStandardCoin { - fn from(arc: Arc) -> UtxoStandardCoin { UtxoStandardCoin { utxo_arc: arc.into() } } -} - impl From for UtxoArc { fn from(coin: UtxoStandardCoin) -> Self { coin.utxo_arc } } -fn ten_f64() -> f64 { 10. } - -fn one_hundred() -> usize { 100 } - -#[derive(Debug, Deserialize)] -struct UtxoMergeParams { - merge_at: usize, - #[serde(default = "ten_f64")] - check_every: f64, - #[serde(default = "one_hundred")] - max_merge_at_once: usize, -} - pub async fn utxo_standard_coin_from_conf_and_request( ctx: &MmArc, ticker: &str, @@ -47,20 +27,9 @@ pub async fn utxo_standard_coin_from_conf_and_request( req: &Json, priv_key: &[u8], ) -> Result { - let inner = try_s!(utxo_arc_from_conf_and_request(ctx, ticker, conf, req, priv_key, UTXO_STANDARD_DUST).await); - let merge_params: Option = try_s!(json::from_value(req["utxo_merge_params"].clone())); - if let Some(merge_params) = merge_params { - let weak = inner.downgrade(); - let merge_loop = merge_utxo_loop( - weak, - merge_params.merge_at, - merge_params.check_every, - merge_params.max_merge_at_once, - ); - info!("Starting UTXO merge loop for coin {}", ticker); - spawn(merge_loop); - } - Ok(inner.into()) + let coin: UtxoStandardCoin = + try_s!(utxo_common::utxo_arc_from_conf_and_request(ctx, ticker, conf, req, priv_key).await); + Ok(coin) } // if mockable is placed before async_trait there is `munmap_chunk(): invalid pointer` error on async fn mocking attempt @@ -185,6 +154,7 @@ impl SwapOps for UtxoStandardCoin { taker_pub: &[u8], secret_hash: &[u8], amount: BigDecimal, + _swap_contract_address: &Option, ) -> TransactionFut { utxo_common::send_maker_payment(self.clone(), time_lock, taker_pub, secret_hash, amount) } @@ -195,6 +165,7 @@ impl SwapOps for UtxoStandardCoin { maker_pub: &[u8], secret_hash: &[u8], amount: BigDecimal, + _swap_contract_address: &Option, ) -> TransactionFut { utxo_common::send_taker_payment(self.clone(), time_lock, maker_pub, secret_hash, amount) } @@ -205,6 +176,7 @@ impl SwapOps for UtxoStandardCoin { time_lock: u32, taker_pub: &[u8], secret: &[u8], + _swap_contract_address: &Option, ) -> TransactionFut { utxo_common::send_maker_spends_taker_payment(self.clone(), taker_payment_tx, time_lock, taker_pub, secret) } @@ -215,6 +187,7 @@ impl SwapOps for UtxoStandardCoin { time_lock: u32, maker_pub: &[u8], secret: &[u8], + _swap_contract_address: &Option, ) -> TransactionFut { utxo_common::send_taker_spends_maker_payment(self.clone(), maker_payment_tx, time_lock, maker_pub, secret) } @@ -225,6 +198,7 @@ impl SwapOps for UtxoStandardCoin { time_lock: u32, maker_pub: &[u8], secret_hash: &[u8], + _swap_contract_address: &Option, ) -> TransactionFut { utxo_common::send_taker_refunds_payment(self.clone(), taker_payment_tx, time_lock, maker_pub, secret_hash) } @@ -235,6 +209,7 @@ impl SwapOps for UtxoStandardCoin { time_lock: u32, taker_pub: &[u8], secret_hash: &[u8], + _swap_contract_address: &Option, ) -> TransactionFut { utxo_common::send_maker_refunds_payment(self.clone(), maker_payment_tx, time_lock, taker_pub, secret_hash) } @@ -255,6 +230,7 @@ impl SwapOps for UtxoStandardCoin { maker_pub: &[u8], priv_bn_hash: &[u8], amount: BigDecimal, + _swap_contract_address: &Option, ) -> Box + Send> { utxo_common::validate_maker_payment(self, payment_tx, time_lock, maker_pub, priv_bn_hash, amount) } @@ -266,6 +242,7 @@ impl SwapOps for UtxoStandardCoin { taker_pub: &[u8], priv_bn_hash: &[u8], amount: BigDecimal, + _swap_contract_address: &Option, ) -> Box + Send> { utxo_common::validate_taker_payment(self, payment_tx, time_lock, taker_pub, priv_bn_hash, amount) } @@ -276,6 +253,7 @@ impl SwapOps for UtxoStandardCoin { other_pub: &[u8], secret_hash: &[u8], _search_from_block: u64, + _swap_contract_address: &Option, ) -> Box, Error = String> + Send> { utxo_common::check_if_my_payment_sent(self.clone(), time_lock, other_pub, secret_hash) } @@ -287,6 +265,7 @@ impl SwapOps for UtxoStandardCoin { secret_hash: &[u8], tx: &[u8], search_from_block: u64, + _swap_contract_address: &Option, ) -> Result, String> { utxo_common::search_for_swap_tx_spend_my( &self.utxo_arc, @@ -305,6 +284,7 @@ impl SwapOps for UtxoStandardCoin { secret_hash: &[u8], tx: &[u8], search_from_block: u64, + _swap_contract_address: &Option, ) -> Result, String> { utxo_common::search_for_swap_tx_spend_other( &self.utxo_arc, @@ -356,7 +336,13 @@ impl MarketCoinOps for UtxoStandardCoin { ) } - fn wait_for_tx_spend(&self, transaction: &[u8], wait_until: u64, from_block: u64) -> TransactionFut { + fn wait_for_tx_spend( + &self, + transaction: &[u8], + wait_until: u64, + from_block: u64, + _swap_contract_address: &Option, + ) -> TransactionFut { utxo_common::wait_for_tx_spend(&self.utxo_arc, transaction, wait_until, from_block) } @@ -419,47 +405,6 @@ impl MmCoin for UtxoStandardCoin { fn my_unspendable_balance(&self) -> Box + Send> { Box::new(utxo_common::my_unspendable_balance(self.clone()).boxed().compat()) } -} -async fn merge_utxo_loop(weak: Weak, merge_at: usize, check_every: f64, max_merge_at_once: usize) { - loop { - Timer::sleep(check_every).await; - - let arc = match weak.upgrade() { - Some(a) => a, - None => break, - }; - let coin: UtxoStandardCoin = UtxoArc(arc).into(); - - let ticker = &coin.as_ref().ticker; - let (unspents, recently_spent) = match coin.list_unspent_ordered(&coin.as_ref().my_address).await { - Ok((unspents, recently_spent)) => (unspents, recently_spent), - Err(e) => { - error!("Error {} on list_unspent_ordered of coin {}", e, ticker); - continue; - }, - }; - if unspents.len() >= merge_at { - let unspents: Vec<_> = unspents.into_iter().take(max_merge_at_once).collect(); - info!("Trying to merge {} UTXOs of coin {}", unspents.len(), ticker); - let value = unspents.iter().fold(0, |sum, unspent| sum + unspent.value); - let script_pubkey = Builder::build_p2pkh(&coin.as_ref().my_address.hash).to_bytes(); - let output = TransactionOutput { value, script_pubkey }; - let merge_tx_fut = generate_and_send_tx( - &coin, - unspents, - vec![output], - FeePolicy::DeductFromOutput(0), - recently_spent, - ); - match merge_tx_fut.await { - Ok(tx) => info!( - "UTXO merge successful for coin {}, tx_hash {:?}", - ticker, - tx.hash().reversed() - ), - Err(e) => error!("Error {} on UTXO merge attempt for coin {}", e, ticker), - } - } - } + fn swap_contract_address(&self) -> Option { utxo_common::swap_contract_address() } } diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index 4acebb49f8..9a4ddefe88 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -1,7 +1,7 @@ use super::rpc_clients::{ElectrumProtocol, ListSinceBlockRes, NetworkInfo}; use super::*; use crate::utxo::rpc_clients::{GetAddressInfoRes, UtxoRpcClientOps, ValidateAddressRes}; -use crate::utxo::utxo_standard::{utxo_standard_coin_from_conf_and_request, UtxoStandardCoin, UTXO_STANDARD_DUST}; +use crate::utxo::utxo_standard::{utxo_standard_coin_from_conf_and_request, UtxoStandardCoin}; use crate::{SwapOps, WithdrawFee}; use bigdecimal::BigDecimal; use chain::OutPoint; @@ -101,7 +101,7 @@ fn utxo_coin_fields_for_test(rpc_client: UtxoRpcClientEnum, force_seed: Option<& force_min_relay_fee: false, mtp_block_count: NonZeroU64::new(11).unwrap(), estimate_fee_mode: None, - dust_amount: UTXO_STANDARD_DUST, + dust_amount: UTXO_DUST_AMOUNT, mature_confirmations: MATURE_CONFIRMATIONS_DEFAULT, tx_cache_directory: None, recently_spent_outpoints: AsyncMutex::new(RecentlySpentOutPoints::new(my_script_pubkey)), @@ -358,7 +358,7 @@ fn test_wait_for_payment_spend_timeout_native() { let from_block = 1000; assert!(coin - .wait_for_tx_spend(&transaction, wait_until, from_block) + .wait_for_tx_spend(&transaction, wait_until, from_block, &None) .wait() .is_err()); assert!(unsafe { OUTPUT_SPEND_CALLED }); @@ -380,7 +380,7 @@ fn test_wait_for_payment_spend_timeout_electrum() { let from_block = 1000; assert!(coin - .wait_for_tx_spend(&transaction, wait_until, from_block) + .wait_for_tx_spend(&transaction, wait_until, from_block, &None) .wait() .is_err()); assert!(unsafe { OUTPUT_SPEND_CALLED }); @@ -407,7 +407,8 @@ fn test_search_for_swap_tx_spend_electrum_was_spent() { &*coin.my_public_key(), &*dhash160(&secret), &payment_tx_bytes, - 0 + 0, + &None, ))); assert_eq!(FoundSwapTxSpend::Spent(spend_tx), found); } @@ -433,7 +434,8 @@ fn test_search_for_swap_tx_spend_electrum_was_refunded() { coin.as_ref().key_pair.public(), &secret, &payment_tx_bytes, - 0 + 0, + &None, ))); assert_eq!(FoundSwapTxSpend::Refunded(refund_tx), found); } @@ -1636,7 +1638,7 @@ fn test_native_client_unspents_filtered_using_tx_cache_single_tx_in_cache() { tx.outputs.clone(), ); NativeClient::list_unspent - .mock_safe(move |_, _| MockResult::Return(Box::new(futures01::future::ok(spent_by_tx.clone())))); + .mock_safe(move |_, _, _| MockResult::Return(Box::new(futures01::future::ok(spent_by_tx.clone())))); let address: Address = "RGfFZaaNV68uVe1uMf6Y37Y8E1i2SyYZBN".into(); let (unspents_ordered, _) = block_on(coin.list_unspent_ordered(&address)).unwrap(); @@ -1730,7 +1732,7 @@ fn test_native_client_unspents_filtered_using_tx_cache_single_several_chained_tx unspents_to_return.extend(spent_by_tx_2); NativeClient::list_unspent - .mock_safe(move |_, _| MockResult::Return(Box::new(futures01::future::ok(unspents_to_return.clone())))); + .mock_safe(move |_, _, _| MockResult::Return(Box::new(futures01::future::ok(unspents_to_return.clone())))); let (unspents_ordered, _) = block_on(coin.list_unspent_ordered(&address)).unwrap(); @@ -1994,3 +1996,69 @@ fn test_find_output_spend_skips_conflicting_transactions() { assert_eq!(actual, Ok(None)); assert_eq!(unsafe { GET_RAW_TRANSACTION_BYTES_CALLED }, 1); } + +#[test] +fn test_qtum_is_unspent_mature() { + use crate::utxo::qtum::{QtumBasedCoin, QtumCoin}; + use rpc::v1::types::{ScriptType, SignedTransactionOutput, TransactionOutputScript}; + + let mut coin_fields = utxo_coin_fields_for_test(UtxoRpcClientEnum::Native(native_client_for_test()), None); + // Qtum's mature confirmations is 500 blocks + coin_fields.mature_confirmations = 500; + let arc: UtxoArc = coin_fields.into(); + let coin = QtumCoin::from(arc); + + let empty_output = SignedTransactionOutput { + value: 0., + n: 0, + script: TransactionOutputScript { + asm: "".into(), + hex: "".into(), + req_sigs: 0, + script_type: ScriptType::NonStandard, + addresses: vec![], + }, + }; + let real_output = SignedTransactionOutput { + value: 117.02430015, + n: 1, + script: TransactionOutputScript { + asm: "03e71b9c152bb233ddfe58f20056715c51b054a1823e0aba108e6f1cea0ceb89c8 OP_CHECKSIG".into(), + hex: "2103e71b9c152bb233ddfe58f20056715c51b054a1823e0aba108e6f1cea0ceb89c8ac".into(), + req_sigs: 0, + script_type: ScriptType::PubKey, + addresses: vec![], + }, + }; + + let mut tx = RpcTransaction { + hex: Default::default(), + txid: "47d983175720ba2a67f36d0e1115a129351a2f340bdde6ecb6d6029e138fe920".into(), + hash: None, + size: Default::default(), + vsize: Default::default(), + version: 2, + locktime: 0, + vin: vec![], + vout: vec![empty_output, real_output], + blockhash: "c23882939ff695be36546ea998eb585e962b043396e4d91959477b9796ceb9e1".into(), + confirmations: 421, + rawconfirmations: None, + time: 1590671504, + blocktime: 1590671504, + height: None, + }; + + // output is coinbase and has confirmations < QTUM_MATURE_CONFIRMATIONS + assert!(!coin.is_qtum_unspent_mature(&tx)); + + tx.confirmations = 501; + // output is coinbase but has confirmations > QTUM_MATURE_CONFIRMATIONS + assert!(coin.is_qtum_unspent_mature(&tx)); + + tx.confirmations = 421; + // remove empty output + tx.vout.remove(0); + // output is not coinbase + assert!(coin.is_qtum_unspent_mature(&tx)); +} diff --git a/mm2src/common/for_tests.rs b/mm2src/common/for_tests.rs index 4fd62edef0..a950a2c501 100644 --- a/mm2src/common/for_tests.rs +++ b/mm2src/common/for_tests.rs @@ -3,6 +3,7 @@ #![cfg_attr(not(feature = "native"), allow(unused_variables))] use crate::block_on; +use bigdecimal::BigDecimal; use bytes::Bytes; use chrono::{Local, TimeZone}; use futures::channel::oneshot::channel; @@ -34,6 +35,65 @@ use crate::mm_metrics::{MetricType, MetricsJson}; #[cfg(feature = "native")] use crate::wio::{slurp_req, POOL}; use crate::{now_float, slurp}; +pub const MAKER_SUCCESS_EVENTS: [&str; 11] = [ + "Started", + "Negotiated", + "TakerFeeValidated", + "MakerPaymentSent", + "TakerPaymentReceived", + "TakerPaymentWaitConfirmStarted", + "TakerPaymentValidatedAndConfirmed", + "TakerPaymentSpent", + "TakerPaymentSpendConfirmStarted", + "TakerPaymentSpendConfirmed", + "Finished", +]; + +pub const MAKER_ERROR_EVENTS: [&str; 13] = [ + "StartFailed", + "NegotiateFailed", + "TakerFeeValidateFailed", + "MakerPaymentTransactionFailed", + "MakerPaymentDataSendFailed", + "MakerPaymentWaitConfirmFailed", + "TakerPaymentValidateFailed", + "TakerPaymentWaitConfirmFailed", + "TakerPaymentSpendFailed", + "TakerPaymentSpendConfirmFailed", + "MakerPaymentWaitRefundStarted", + "MakerPaymentRefunded", + "MakerPaymentRefundFailed", +]; + +pub const TAKER_SUCCESS_EVENTS: [&str; 10] = [ + "Started", + "Negotiated", + "TakerFeeSent", + "MakerPaymentReceived", + "MakerPaymentWaitConfirmStarted", + "MakerPaymentValidatedAndConfirmed", + "TakerPaymentSent", + "TakerPaymentSpent", + "MakerPaymentSpent", + "Finished", +]; + +pub const TAKER_ERROR_EVENTS: [&str; 13] = [ + "StartFailed", + "NegotiateFailed", + "TakerFeeSendFailed", + "MakerPaymentValidateFailed", + "MakerPaymentWaitConfirmFailed", + "TakerPaymentTransactionFailed", + "TakerPaymentWaitConfirmFailed", + "TakerPaymentDataSendFailed", + "TakerPaymentWaitForSpendFailed", + "MakerPaymentSpendFailed", + "TakerPaymentWaitRefundStarted", + "TakerPaymentRefunded", + "TakerPaymentRefundFailed", +]; + /// Automatically kill a wrapped process. pub struct RaiiKill { pub handle: Child, @@ -700,3 +760,88 @@ pub fn find_metrics_in_json( true }) } + +/// Helper function requesting my swap status and checking it's events +pub async fn check_my_swap_status( + mm: &MarketMakerIt, + uuid: &str, + expected_success_events: &[&str], + expected_error_events: &[&str], + maker_amount: BigDecimal, + taker_amount: BigDecimal, +) { + let response = unwrap!( + mm.rpc(json! ({ + "userpass": mm.userpass, + "method": "my_swap_status", + "params": { + "uuid": uuid, + } + })) + .await + ); + assert!(response.0.is_success(), "!status of {}: {}", uuid, response.1); + let status_response: Json = unwrap!(json::from_str(&response.1)); + let success_events: Vec = unwrap!(json::from_value(status_response["result"]["success_events"].clone())); + assert_eq!(expected_success_events, success_events.as_slice()); + let error_events: Vec = unwrap!(json::from_value(status_response["result"]["error_events"].clone())); + assert_eq!(expected_error_events, error_events.as_slice()); + + let events_array = unwrap!(status_response["result"]["events"].as_array()); + let actual_maker_amount = unwrap!(json::from_value( + events_array[0]["event"]["data"]["maker_amount"].clone() + )); + assert_eq!(maker_amount, actual_maker_amount); + let actual_taker_amount = unwrap!(json::from_value( + events_array[0]["event"]["data"]["taker_amount"].clone() + )); + assert_eq!(taker_amount, actual_taker_amount); + let actual_events = events_array.iter().map(|item| unwrap!(item["event"]["type"].as_str())); + let actual_events: Vec<&str> = actual_events.collect(); + assert_eq!(expected_success_events, actual_events.as_slice()); +} + +pub async fn check_stats_swap_status( + mm: &MarketMakerIt, + uuid: &str, + maker_expected_events: &[&str], + taker_expected_events: &[&str], +) { + let response = unwrap!( + mm.rpc(json! ({ + "method": "stats_swap_status", + "params": { + "uuid": uuid, + } + })) + .await + ); + assert!(response.0.is_success(), "!status of {}: {}", uuid, response.1); + let status_response: Json = unwrap!(json::from_str(&response.1)); + let maker_events_array = unwrap!(status_response["result"]["maker"]["events"].as_array()); + let taker_events_array = unwrap!(status_response["result"]["taker"]["events"].as_array()); + let maker_actual_events = maker_events_array + .iter() + .map(|item| unwrap!(item["event"]["type"].as_str())); + let maker_actual_events: Vec<&str> = maker_actual_events.collect(); + let taker_actual_events = taker_events_array + .iter() + .map(|item| unwrap!(item["event"]["type"].as_str())); + let taker_actual_events: Vec<&str> = taker_actual_events.collect(); + assert_eq!(maker_expected_events, maker_actual_events.as_slice()); + assert_eq!(taker_expected_events, taker_actual_events.as_slice()); +} + +pub async fn check_recent_swaps(mm: &MarketMakerIt, expected_len: usize) { + let response = unwrap!( + mm.rpc(json! ({ + "method": "my_recent_swaps", + "userpass": mm.userpass, + })) + .await + ); + assert!(response.0.is_success(), "!status of my_recent_swaps {}", response.1); + let swaps_response: Json = unwrap!(json::from_str(&response.1)); + let swaps: &Vec = unwrap!(swaps_response["result"]["swaps"].as_array()); + assert_eq!(expected_len, swaps.len()); +} diff --git a/mm2src/docker_tests.rs b/mm2src/docker_tests.rs index 84d6802d6d..8932d0e7d5 100644 --- a/mm2src/docker_tests.rs +++ b/mm2src/docker_tests.rs @@ -50,20 +50,26 @@ mod swaps_confs_settings_sync_tests; #[path = "docker_tests/swaps_file_lock_tests.rs"] mod swaps_file_lock_tests; +#[cfg(rustfmt)] +#[path = "docker_tests/qrc20_tests.rs"] +mod qrc20_tests; + #[cfg(all(test, feature = "native"))] mod docker_tests { #[rustfmt::skip] mod swaps_confs_settings_sync_tests; #[rustfmt::skip] mod swaps_file_lock_tests; + #[rustfmt::skip] + mod qrc20_tests; use bigdecimal::BigDecimal; use bitcrypto::ChecksumType; use chain::OutPoint; use coins::utxo::rpc_clients::{UnspentInfo, UtxoRpcClientEnum, UtxoRpcClientOps}; use coins::utxo::utxo_standard::{utxo_standard_coin_from_conf_and_request, UtxoStandardCoin}; - use coins::utxo::{coin_daemon_data_dir, dhash160, zcash_params_path, UtxoCommonOps}; - use coins::{FoundSwapTxSpend, MarketCoinOps, SwapOps, TransactionEnum}; + use coins::utxo::{coin_daemon_data_dir, dhash160, zcash_params_path, UtxoCoinFields, UtxoCommonOps}; + use coins::{FoundSwapTxSpend, MarketCoinOps, MmCoin, SwapOps, TransactionEnum}; use common::block_on; use common::for_tests::enable_electrum; use common::{file_lock::FileLock, @@ -73,6 +79,7 @@ mod docker_tests { use gstuff::now_ms; use keys::{KeyPair, Private}; use primitives::hash::H160; + use qrc20_tests::{qtum_docker_node, QtumDockerOps, QTUM_REGTEST_DOCKER_IMAGE}; use secp256k1::{PublicKey, SecretKey}; use serde_json::{self as json, Value as Json}; use std::collections::HashMap; @@ -93,6 +100,8 @@ mod docker_tests { dhash160(&public.serialize_compressed()) } + const UTXO_ASSET_DOCKER_IMAGE: &str = "artempikulin/testblockchain"; + // AP: custom test runner is intended to initialize the required environment (e.g. coin daemons in the docker containers) // and then gracefully clear it by dropping the RAII docker container handlers // I've tried to use static for such singleton initialization but it turned out that despite @@ -107,37 +116,27 @@ mod docker_tests { let mut containers = vec![]; // skip Docker containers initialization if we are intended to run test_mm_start only if std::env::var("_MM2_TEST_CONF").is_err() { - Command::new("docker") - .arg("pull") - .arg("artempikulin/testblockchain") - .status() - .expect("Failed to execute docker command"); + pull_docker_image(UTXO_ASSET_DOCKER_IMAGE); + pull_docker_image(QTUM_REGTEST_DOCKER_IMAGE); + remove_docker_containers(UTXO_ASSET_DOCKER_IMAGE); + remove_docker_containers(QTUM_REGTEST_DOCKER_IMAGE); - let stdout = Command::new("docker") - .arg("ps") - .arg("-f") - .arg("ancestor=artempikulin/testblockchain") - .arg("-q") - .output() - .expect("Failed to execute docker command"); + let utxo_node = utxo_asset_docker_node(&docker, "MYCOIN", 7000); + let utxo_node1 = utxo_asset_docker_node(&docker, "MYCOIN1", 8000); + let qtum_node = qtum_docker_node(&docker, 9000); - let reader = BufReader::new(stdout.stdout.as_slice()); - let ids: Vec<_> = reader.lines().map(|line| line.unwrap()).collect(); - if !ids.is_empty() { - Command::new("docker") - .arg("rm") - .arg("-f") - .args(ids) - .status() - .expect("Failed to execute docker command"); - } + let utxo_ops = UtxoAssetDockerOps::from_ticker("MYCOIN"); + let utxo_ops1 = UtxoAssetDockerOps::from_ticker("MYCOIN1"); + let qtum_ops = QtumDockerOps::new(); + + utxo_ops.wait_ready(); + utxo_ops1.wait_ready(); + qtum_ops.wait_ready(); + qtum_ops.initialize_contracts(); - let utxo_node = utxo_docker_node(&docker, "MYCOIN", 7000); - let utxo_node1 = utxo_docker_node(&docker, "MYCOIN1", 8000); - utxo_node.wait_ready(); - utxo_node1.wait_ready(); containers.push(utxo_node); containers.push(utxo_node1); + containers.push(qtum_node); } // detect if docker is installed // skip the tests that use docker if not installed @@ -159,32 +158,42 @@ mod docker_tests { let _exit_code = test_main(&args, owned_tests, None); } - struct UtxoDockerNode<'a> { - #[allow(dead_code)] - container: Container<'a, Cli, GenericImage>, - ticker: String, - #[allow(dead_code)] - port: u16, + fn pull_docker_image(name: &str) { + Command::new("docker") + .arg("pull") + .arg(name) + .status() + .expect("Failed to execute docker command"); } - impl<'a> UtxoDockerNode<'a> { - pub fn wait_ready(&self) { - let ctx = MmCtxBuilder::new().into_mm_arc(); - let conf = json!({"asset":self.ticker, "txfee": 1000}); - let req = json!({"method":"enable"}); - let priv_key = unwrap!(hex::decode( - "809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f" - )); - let coin = unwrap!(block_on(utxo_standard_coin_from_conf_and_request( - &ctx, - &self.ticker, - &conf, - &req, - &priv_key - ))); + fn remove_docker_containers(name: &str) { + let stdout = Command::new("docker") + .arg("ps") + .arg("-f") + .arg(format!("ancestor={}", name)) + .arg("-q") + .output() + .expect("Failed to execute docker command"); + + let reader = BufReader::new(stdout.stdout.as_slice()); + let ids: Vec<_> = reader.lines().map(|line| line.unwrap()).collect(); + if !ids.is_empty() { + Command::new("docker") + .arg("rm") + .arg("-f") + .args(ids) + .status() + .expect("Failed to execute docker command"); + } + } + + trait CoinDockerOps { + fn rpc_client(&self) -> &UtxoRpcClientEnum; + + fn wait_ready(&self) { let timeout = now_ms() + 30000; loop { - match coin.as_ref().rpc_client.get_block_count().wait() { + match self.rpc_client().get_block_count().wait() { Ok(n) => { if n > 1 { break; @@ -198,14 +207,48 @@ mod docker_tests { } } - fn utxo_docker_node<'a>(docker: &'a Cli, ticker: &'static str, port: u16) -> UtxoDockerNode<'a> { + struct UtxoAssetDockerOps { + #[allow(dead_code)] + ctx: MmArc, + coin: UtxoStandardCoin, + } + + impl CoinDockerOps for UtxoAssetDockerOps { + fn rpc_client(&self) -> &UtxoRpcClientEnum { &self.coin.as_ref().rpc_client } + } + + impl UtxoAssetDockerOps { + fn from_ticker(ticker: &str) -> UtxoAssetDockerOps { + let conf = json!({"asset": ticker, "txfee": 1000, "network": "regtest"}); + let req = json!({"method":"enable"}); + let priv_key = unwrap!(hex::decode( + "809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f" + )); + let ctx = MmCtxBuilder::new().into_mm_arc(); + let coin = unwrap!(block_on(utxo_standard_coin_from_conf_and_request( + &ctx, ticker, &conf, &req, &priv_key, + ))); + UtxoAssetDockerOps { ctx, coin } + } + } + + pub struct UtxoDockerNode<'a> { + #[allow(dead_code)] + container: Container<'a, Cli, GenericImage>, + #[allow(dead_code)] + ticker: String, + #[allow(dead_code)] + port: u16, + } + + fn utxo_asset_docker_node<'a>(docker: &'a Cli, ticker: &'static str, port: u16) -> UtxoDockerNode<'a> { let args = vec![ "-v".into(), format!("{}:/data/.zcash-params", zcash_params_path().display()), "-p".into(), format!("127.0.0.1:{}:{}", port, port).into(), ]; - let image = GenericImage::new("artempikulin/testblockchain") + let image = GenericImage::new(UTXO_ASSET_DOCKER_IMAGE) .with_args(args) .with_env_var("CLIENTS", "2") .with_env_var("CHAIN", ticker) @@ -247,21 +290,45 @@ mod docker_tests { static ref COINS_LOCK: Mutex<()> = Mutex::new(()); } - // generate random privkey, create a coin and fill it's address with 1000 coins - fn generate_coin_with_random_privkey(ticker: &str, balance: BigDecimal) -> (MmArc, UtxoStandardCoin, [u8; 32]) { + /// Build asset `UtxoStandardCoin` from ticker and privkey without filling the balance. + fn utxo_coin_from_privkey(ticker: &str, priv_key: &[u8]) -> (MmArc, UtxoStandardCoin) { let ctx = MmCtxBuilder::new().into_mm_arc(); - let timeout = (now_ms() / 1000) + 120; // timeout if test takes more than 120 seconds to run - let conf = json!({"asset":ticker,"txversion":4,"overwintered":1,"txfee":1000}); + let conf = json!({"asset":ticker,"txversion":4,"overwintered":1,"txfee":1000,"network":"regtest"}); let req = json!({"method":"enable"}); - let priv_key = SecretKey::random(&mut rand4::thread_rng()).serialize(); let coin = unwrap!(block_on(utxo_standard_coin_from_conf_and_request( - &ctx, ticker, &conf, &req, &priv_key + &ctx, ticker, &conf, &req, priv_key ))); - fill_address(&coin, &coin.my_address().unwrap(), balance, timeout); + import_address(&coin); + (ctx, coin) + } + + /// Generate random privkey, create a coin and fill it's address with the specified balance. + fn generate_coin_with_random_privkey(ticker: &str, balance: BigDecimal) -> (MmArc, UtxoStandardCoin, [u8; 32]) { + let priv_key = SecretKey::random(&mut rand4::thread_rng()).serialize(); + let (ctx, coin) = utxo_coin_from_privkey(ticker, &priv_key); + let timeout = (now_ms() / 1000) + 120; // timeout if test takes more than 120 seconds to run + let my_address = coin.my_address().expect("!my_address"); + fill_address(&coin, &my_address, balance, timeout); (ctx, coin, priv_key) } - fn fill_address(coin: &UtxoStandardCoin, address: &str, amount: BigDecimal, timeout: u64) { + fn import_address(coin: &T) + where + T: MarketCoinOps + AsRef, + { + match coin.as_ref().rpc_client { + UtxoRpcClientEnum::Native(ref native) => { + let my_address = coin.my_address().unwrap(); + unwrap!(native.import_address(&my_address, &my_address, false).wait()) + }, + UtxoRpcClientEnum::Electrum(_) => panic!("Expected NativeClient"), + } + } + + fn fill_address(coin: &T, address: &str, amount: BigDecimal, timeout: u64) + where + T: MarketCoinOps + AsRef, + { // prevent concurrent fill since daemon RPC returns errors if send_to_address // is called concurrently (insufficient funds) and it also may return other errors // if previous transaction is not confirmed yet @@ -294,14 +361,14 @@ mod docker_tests { let time_lock = (now_ms() / 1000) as u32 - 3600; let tx = coin - .send_taker_payment(time_lock, &*coin.my_public_key(), &[0; 20], 1.into()) + .send_taker_payment(time_lock, &*coin.my_public_key(), &[0; 20], 1.into(), &None) .wait() .unwrap(); unwrap!(coin.wait_for_confirmations(&tx.tx_hex(), 1, false, timeout, 1).wait()); let refund_tx = coin - .send_taker_refunds_payment(&tx.tx_hex(), time_lock, &*coin.my_public_key(), &[0; 20]) + .send_taker_refunds_payment(&tx.tx_hex(), time_lock, &*coin.my_public_key(), &[0; 20], &None) .wait() .unwrap(); @@ -315,6 +382,7 @@ mod docker_tests { &[0; 20], &tx.tx_hex(), 0, + &None ))); assert_eq!(FoundSwapTxSpend::Refunded(refund_tx), found); } @@ -326,14 +394,14 @@ mod docker_tests { let time_lock = (now_ms() / 1000) as u32 - 3600; let tx = coin - .send_maker_payment(time_lock, &*coin.my_public_key(), &[0; 20], 1.into()) + .send_maker_payment(time_lock, &*coin.my_public_key(), &[0; 20], 1.into(), &None) .wait() .unwrap(); unwrap!(coin.wait_for_confirmations(&tx.tx_hex(), 1, false, timeout, 1).wait()); let refund_tx = coin - .send_maker_refunds_payment(&tx.tx_hex(), time_lock, &*coin.my_public_key(), &[0; 20]) + .send_maker_refunds_payment(&tx.tx_hex(), time_lock, &*coin.my_public_key(), &[0; 20], &None) .wait() .unwrap(); @@ -347,6 +415,7 @@ mod docker_tests { &[0; 20], &tx.tx_hex(), 0, + &None ))); assert_eq!(FoundSwapTxSpend::Refunded(refund_tx), found); } @@ -359,14 +428,14 @@ mod docker_tests { let time_lock = (now_ms() / 1000) as u32 - 3600; let tx = coin - .send_taker_payment(time_lock, &*coin.my_public_key(), &*dhash160(&secret), 1.into()) + .send_taker_payment(time_lock, &*coin.my_public_key(), &*dhash160(&secret), 1.into(), &None) .wait() .unwrap(); unwrap!(coin.wait_for_confirmations(&tx.tx_hex(), 1, false, timeout, 1).wait()); let spend_tx = coin - .send_maker_spends_taker_payment(&tx.tx_hex(), time_lock, &*coin.my_public_key(), &secret) + .send_maker_spends_taker_payment(&tx.tx_hex(), time_lock, &*coin.my_public_key(), &secret, &None) .wait() .unwrap(); @@ -380,6 +449,7 @@ mod docker_tests { &*dhash160(&secret), &tx.tx_hex(), 0, + &None ))); assert_eq!(FoundSwapTxSpend::Spent(spend_tx), found); } @@ -392,14 +462,14 @@ mod docker_tests { let time_lock = (now_ms() / 1000) as u32 - 3600; let tx = coin - .send_maker_payment(time_lock, &*coin.my_public_key(), &*dhash160(&secret), 1.into()) + .send_maker_payment(time_lock, &*coin.my_public_key(), &*dhash160(&secret), 1.into(), &None) .wait() .unwrap(); unwrap!(coin.wait_for_confirmations(&tx.tx_hex(), 1, false, timeout, 1).wait()); let spend_tx = coin - .send_taker_spends_maker_payment(&tx.tx_hex(), time_lock, &*coin.my_public_key(), &secret) + .send_taker_spends_maker_payment(&tx.tx_hex(), time_lock, &*coin.my_public_key(), &secret, &None) .wait() .unwrap(); @@ -413,6 +483,7 @@ mod docker_tests { &*dhash160(&secret), &tx.tx_hex(), 0, + &None ))); assert_eq!(FoundSwapTxSpend::Spent(spend_tx), found); } @@ -429,7 +500,13 @@ mod docker_tests { let mut sent_tx = vec![]; for i in 0..100 { let tx = coin - .send_maker_payment(time_lock + i, &*coin.my_public_key(), &*dhash160(&secret), 1.into()) + .send_maker_payment( + time_lock + i, + &*coin.my_public_key(), + &*dhash160(&secret), + 1.into(), + &coin.swap_contract_address(), + ) .wait() .unwrap(); if let TransactionEnum::UtxoTx(tx) = tx { diff --git a/mm2src/docker_tests/qrc20_tests.rs b/mm2src/docker_tests/qrc20_tests.rs new file mode 100644 index 0000000000..32c7e2819f --- /dev/null +++ b/mm2src/docker_tests/qrc20_tests.rs @@ -0,0 +1,1029 @@ +use super::*; +use bigdecimal::BigDecimal; +use coins::qrc20::rpc_clients::for_tests::Qrc20NativeWalletOps; +use coins::qrc20::{qrc20_coin_from_conf_and_request, Qrc20Coin}; +use coins::utxo::qtum::QtumBasedCoin; +use coins::utxo::qtum::{qtum_coin_from_conf_and_request, QtumCoin}; +use coins::utxo::sat_from_big_decimal; +use coins::{MarketCoinOps, MmCoin, TransactionEnum}; +use common::for_tests::{check_my_swap_status, check_recent_swaps, check_stats_swap_status, MAKER_ERROR_EVENTS, + MAKER_SUCCESS_EVENTS, TAKER_ERROR_EVENTS, TAKER_SUCCESS_EVENTS}; +use common::mm_ctx::MmArc; +use common::temp_dir; +use ethereum_types::H160; +use http::StatusCode; +use std::path::PathBuf; +use std::str::FromStr; + +pub const QTUM_REGTEST_DOCKER_IMAGE: &str = "sergeyboyko/qtumregtest"; + +const QRC20_TOKEN_BYTES: &str = "6080604052600860ff16600a0a633b9aca000260005534801561002157600080fd5b50600054600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550610c69806100776000396000f3006080604052600436106100a4576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806306fdde03146100a9578063095ea7b31461013957806318160ddd1461019e57806323b872dd146101c9578063313ce5671461024e5780635a3b7e421461027f57806370a082311461030f57806395d89b4114610366578063a9059cbb146103f6578063dd62ed3e1461045b575b600080fd5b3480156100b557600080fd5b506100be6104d2565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156100fe5780820151818401526020810190506100e3565b50505050905090810190601f16801561012b5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561014557600080fd5b50610184600480360381019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291908035906020019092919050505061050b565b604051808215151515815260200191505060405180910390f35b3480156101aa57600080fd5b506101b36106bb565b6040518082815260200191505060405180910390f35b3480156101d557600080fd5b50610234600480360381019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803590602001909291905050506106c1565b604051808215151515815260200191505060405180910390f35b34801561025a57600080fd5b506102636109a1565b604051808260ff1660ff16815260200191505060405180910390f35b34801561028b57600080fd5b506102946109a6565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156102d45780820151818401526020810190506102b9565b50505050905090810190601f1680156103015780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561031b57600080fd5b50610350600480360381019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291905050506109df565b6040518082815260200191505060405180910390f35b34801561037257600080fd5b5061037b6109f7565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156103bb5780820151818401526020810190506103a0565b50505050905090810190601f1680156103e85780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561040257600080fd5b50610441600480360381019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610a30565b604051808215151515815260200191505060405180910390f35b34801561046757600080fd5b506104bc600480360381019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610be1565b6040518082815260200191505060405180910390f35b6040805190810160405280600881526020017f515243205445535400000000000000000000000000000000000000000000000081525081565b60008260008173ffffffffffffffffffffffffffffffffffffffff161415151561053457600080fd5b60008314806105bf57506000600260003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054145b15156105ca57600080fd5b82600260003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508373ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925856040518082815260200191505060405180910390a3600191505092915050565b60005481565b60008360008173ffffffffffffffffffffffffffffffffffffffff16141515156106ea57600080fd5b8360008173ffffffffffffffffffffffffffffffffffffffff161415151561071157600080fd5b610797600260008873ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205485610c06565b600260008873ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550610860600160008873ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205485610c06565b600160008873ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055506108ec600160008773ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205485610c1f565b600160008773ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508473ffffffffffffffffffffffffffffffffffffffff168673ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef866040518082815260200191505060405180910390a36001925050509392505050565b600881565b6040805190810160405280600981526020017f546f6b656e20302e31000000000000000000000000000000000000000000000081525081565b60016020528060005260406000206000915090505481565b6040805190810160405280600381526020017f515443000000000000000000000000000000000000000000000000000000000081525081565b60008260008173ffffffffffffffffffffffffffffffffffffffff1614151515610a5957600080fd5b610aa2600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205484610c06565b600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550610b2e600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205484610c1f565b600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508373ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef856040518082815260200191505060405180910390a3600191505092915050565b6002602052816000526040600020602052806000526040600020600091509150505481565b6000818310151515610c1457fe5b818303905092915050565b6000808284019050838110151515610c3357fe5b80915050929150505600a165627a7a723058207f2e5248b61b80365ea08a0f6d11ac0b47374c4dfd538de76bc2f19591bbbba40029"; +const QRC20_SWAP_CONTRACT_BYTES: &str = "608060405234801561001057600080fd5b50611437806100206000396000f3fe60806040526004361061004a5760003560e01c806302ed292b1461004f5780630716326d146100de578063152cf3af1461017b57806346fc0294146101f65780639b415b2a14610294575b600080fd5b34801561005b57600080fd5b506100dc600480360360a081101561007257600080fd5b81019080803590602001909291908035906020019092919080359060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610339565b005b3480156100ea57600080fd5b506101176004803603602081101561010157600080fd5b8101908080359060200190929190505050610867565b60405180846bffffffffffffffffffffffff19166bffffffffffffffffffffffff191681526020018367ffffffffffffffff1667ffffffffffffffff16815260200182600381111561016557fe5b60ff168152602001935050505060405180910390f35b6101f46004803603608081101561019157600080fd5b8101908080359060200190929190803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080356bffffffffffffffffffffffff19169060200190929190803567ffffffffffffffff1690602001909291905050506108bf565b005b34801561020257600080fd5b50610292600480360360a081101561021957600080fd5b81019080803590602001909291908035906020019092919080356bffffffffffffffffffffffff19169060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610bd9565b005b610337600480360360c08110156102aa57600080fd5b810190808035906020019092919080359060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080356bffffffffffffffffffffffff19169060200190929190803567ffffffffffffffff169060200190929190505050610fe2565b005b6001600381111561034657fe5b600080878152602001908152602001600020600001601c9054906101000a900460ff16600381111561037457fe5b1461037e57600080fd5b6000600333836003600288604051602001808281526020019150506040516020818303038152906040526040518082805190602001908083835b602083106103db57805182526020820191506020810190506020830392506103b8565b6001836020036101000a038019825116818451168082178552505050505050905001915050602060405180830381855afa15801561041d573d6000803e3d6000fd5b5050506040513d602081101561043257600080fd5b8101908080519060200190929190505050604051602001808281526020019150506040516020818303038152906040526040518082805190602001908083835b602083106104955780518252602082019150602081019050602083039250610472565b6001836020036101000a038019825116818451168082178552505050505050905001915050602060405180830381855afa1580156104d7573d6000803e3d6000fd5b5050506040515160601b8689604051602001808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b81526014018573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b8152601401846bffffffffffffffffffffffff19166bffffffffffffffffffffffff191681526014018373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b8152601401828152602001955050505050506040516020818303038152906040526040518082805190602001908083835b602083106105fc57805182526020820191506020810190506020830392506105d9565b6001836020036101000a038019825116818451168082178552505050505050905001915050602060405180830381855afa15801561063e573d6000803e3d6000fd5b5050506040515160601b905060008087815260200190815260200160002060000160009054906101000a900460601b6bffffffffffffffffffffffff1916816bffffffffffffffffffffffff19161461069657600080fd5b6002600080888152602001908152602001600020600001601c6101000a81548160ff021916908360038111156106c857fe5b0217905550600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16141561074e573373ffffffffffffffffffffffffffffffffffffffff166108fc869081150290604051600060405180830381858888f19350505050158015610748573d6000803e3d6000fd5b50610820565b60008390508073ffffffffffffffffffffffffffffffffffffffff1663a9059cbb33886040518363ffffffff1660e01b8152600401808373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200182815260200192505050602060405180830381600087803b1580156107da57600080fd5b505af11580156107ee573d6000803e3d6000fd5b505050506040513d602081101561080457600080fd5b810190808051906020019092919050505061081e57600080fd5b505b7f36c177bcb01c6d568244f05261e2946c8c977fa50822f3fa098c470770ee1f3e8685604051808381526020018281526020019250505060405180910390a1505050505050565b60006020528060005260406000206000915090508060000160009054906101000a900460601b908060000160149054906101000a900467ffffffffffffffff169080600001601c9054906101000a900460ff16905083565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16141580156108fc5750600034115b801561094057506000600381111561091057fe5b600080868152602001908152602001600020600001601c9054906101000a900460ff16600381111561093e57fe5b145b61094957600080fd5b60006003843385600034604051602001808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b81526014018573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b8152601401846bffffffffffffffffffffffff19166bffffffffffffffffffffffff191681526014018373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b8152601401828152602001955050505050506040516020818303038152906040526040518082805190602001908083835b60208310610a6c5780518252602082019150602081019050602083039250610a49565b6001836020036101000a038019825116818451168082178552505050505050905001915050602060405180830381855afa158015610aae573d6000803e3d6000fd5b5050506040515160601b90506040518060600160405280826bffffffffffffffffffffffff191681526020018367ffffffffffffffff16815260200160016003811115610af757fe5b81525060008087815260200190815260200160002060008201518160000160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908360601c021790555060208201518160000160146101000a81548167ffffffffffffffff021916908367ffffffffffffffff160217905550604082015181600001601c6101000a81548160ff02191690836003811115610b9357fe5b02179055509050507fccc9c05183599bd3135da606eaaf535daffe256e9de33c048014cffcccd4ad57856040518082815260200191505060405180910390a15050505050565b60016003811115610be657fe5b600080878152602001908152602001600020600001601c9054906101000a900460ff166003811115610c1457fe5b14610c1e57600080fd5b600060038233868689604051602001808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b81526014018573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b8152601401846bffffffffffffffffffffffff19166bffffffffffffffffffffffff191681526014018373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b8152601401828152602001955050505050506040516020818303038152906040526040518082805190602001908083835b60208310610d405780518252602082019150602081019050602083039250610d1d565b6001836020036101000a038019825116818451168082178552505050505050905001915050602060405180830381855afa158015610d82573d6000803e3d6000fd5b5050506040515160601b905060008087815260200190815260200160002060000160009054906101000a900460601b6bffffffffffffffffffffffff1916816bffffffffffffffffffffffff1916148015610e10575060008087815260200190815260200160002060000160149054906101000a900467ffffffffffffffff1667ffffffffffffffff164210155b610e1957600080fd5b6003600080888152602001908152602001600020600001601c6101000a81548160ff02191690836003811115610e4b57fe5b0217905550600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff161415610ed1573373ffffffffffffffffffffffffffffffffffffffff166108fc869081150290604051600060405180830381858888f19350505050158015610ecb573d6000803e3d6000fd5b50610fa3565b60008390508073ffffffffffffffffffffffffffffffffffffffff1663a9059cbb33886040518363ffffffff1660e01b8152600401808373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200182815260200192505050602060405180830381600087803b158015610f5d57600080fd5b505af1158015610f71573d6000803e3d6000fd5b505050506040513d6020811015610f8757600080fd5b8101908080519060200190929190505050610fa157600080fd5b505b7f1797d500133f8e427eb9da9523aa4a25cb40f50ebc7dbda3c7c81778973f35ba866040518082815260200191505060405180910390a1505050505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff161415801561101f5750600085115b801561106357506000600381111561103357fe5b600080888152602001908152602001600020600001601c9054906101000a900460ff16600381111561106157fe5b145b61106c57600080fd5b60006003843385888a604051602001808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b81526014018573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b8152601401846bffffffffffffffffffffffff19166bffffffffffffffffffffffff191681526014018373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b8152601401828152602001955050505050506040516020818303038152906040526040518082805190602001908083835b6020831061118e578051825260208201915060208101905060208303925061116b565b6001836020036101000a038019825116818451168082178552505050505050905001915050602060405180830381855afa1580156111d0573d6000803e3d6000fd5b5050506040515160601b90506040518060600160405280826bffffffffffffffffffffffff191681526020018367ffffffffffffffff1681526020016001600381111561121957fe5b81525060008089815260200190815260200160002060008201518160000160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908360601c021790555060208201518160000160146101000a81548167ffffffffffffffff021916908367ffffffffffffffff160217905550604082015181600001601c6101000a81548160ff021916908360038111156112b557fe5b021790555090505060008590508073ffffffffffffffffffffffffffffffffffffffff166323b872dd33308a6040518463ffffffff1660e01b8152600401808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018281526020019350505050602060405180830381600087803b15801561137d57600080fd5b505af1158015611391573d6000803e3d6000fd5b505050506040513d60208110156113a757600080fd5b81019080805190602001909291905050506113c157600080fd5b7fccc9c05183599bd3135da606eaaf535daffe256e9de33c048014cffcccd4ad57886040518082815260200191505060405180910390a1505050505050505056fea265627a7a723158208c83db436905afce0b7be1012be64818c49323c12d451fe2ab6bce76ff6421c964736f6c63430005110032"; +const QTUM_ADDRESS_LABEL: &str = "MM2_ADDRESS_LABEL"; + +static mut QICK_TOKEN_ADDRESS: Option = None; +static mut QORTY_TOKEN_ADDRESS: Option = None; +static mut QRC20_SWAP_CONTRACT_ADDRESS: Option = None; +static mut QTUM_CONF_PATH: Option = None; + +pub struct QtumDockerOps { + #[allow(dead_code)] + ctx: MmArc, + coin: QtumCoin, +} + +impl CoinDockerOps for QtumDockerOps { + fn rpc_client(&self) -> &UtxoRpcClientEnum { &self.coin.as_ref().rpc_client } +} + +impl QtumDockerOps { + pub fn new() -> QtumDockerOps { + let ctx = MmCtxBuilder::new().into_mm_arc(); + let confpath = unsafe { QTUM_CONF_PATH.as_ref().expect("Qtum config is not set yet") }; + let conf = json!({"decimals":8,"network":"regtest","confpath":confpath}); + let req = json!({ + "method": "enable", + }); + let priv_key = unwrap!(hex::decode( + "809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f" + )); + let coin = unwrap!(block_on(qtum_coin_from_conf_and_request( + &ctx, "QTUM", &conf, &req, &priv_key + ))); + QtumDockerOps { ctx, coin } + } + + pub fn initialize_contracts(&self) { + let sender = get_address_by_label(&self.coin, QTUM_ADDRESS_LABEL); + unsafe { + QICK_TOKEN_ADDRESS = Some(self.create_contract(&sender, QRC20_TOKEN_BYTES)); + QORTY_TOKEN_ADDRESS = Some(self.create_contract(&sender, QRC20_TOKEN_BYTES)); + QRC20_SWAP_CONTRACT_ADDRESS = Some(self.create_contract(&sender, QRC20_SWAP_CONTRACT_BYTES)); + } + } + + fn create_contract(&self, sender: &str, hexbytes: &str) -> H160 { + let bytecode = hex::decode(hexbytes).expect("Hex encoded bytes expected"); + let gas_limit = 2_500_000u64; + let gas_price = BigDecimal::from_str("0.0000004").unwrap(); + + match self.coin.as_ref().rpc_client { + UtxoRpcClientEnum::Native(ref native) => { + let result = native + .create_contract(&bytecode.into(), gas_limit, gas_price, sender) + .wait() + .expect("!createcontract"); + result.address.0.into() + }, + UtxoRpcClientEnum::Electrum(_) => panic!("Native client expected"), + } + } +} + +pub fn qtum_docker_node<'a>(docker: &'a Cli, port: u16) -> UtxoDockerNode<'a> { + let args = vec!["-p".into(), format!("127.0.0.1:{}:{}", port, port).into()]; + let image = GenericImage::new(QTUM_REGTEST_DOCKER_IMAGE) + .with_args(args) + .with_env_var("CLIENTS", "2") + .with_env_var("COIN_RPC_PORT", port.to_string()) + .with_env_var("ADDRESS_LABEL", QTUM_ADDRESS_LABEL) + .with_wait_for(WaitFor::message_on_stdout("config is ready")); + let container = docker.run(image); + + let name = "qtum"; + let mut conf_path = temp_dir().join("qtum-regtest"); + unwrap!(std::fs::create_dir_all(&conf_path)); + conf_path.push(format!("{}.conf", name)); + Command::new("docker") + .arg("cp") + .arg(format!("{}:/data/node_0/{}.conf", container.id(), name)) + .arg(&conf_path) + .status() + .expect("Failed to execute docker command"); + let timeout = now_ms() + 3000; + loop { + if conf_path.exists() { + break; + }; + assert!(now_ms() < timeout, "Test timed out"); + } + + unsafe { QTUM_CONF_PATH = Some(conf_path) }; + UtxoDockerNode { + container, + ticker: name.to_owned(), + port, + } +} + +/// Build `Qrc20Coin` from ticker and privkey without filling the balance. +fn qrc20_coin_from_privkey(ticker: &str, priv_key: &[u8]) -> (MmArc, Qrc20Coin) { + let (contract_address, swap_contract_address) = unsafe { + let contract_address = match ticker { + "QICK" => QICK_TOKEN_ADDRESS + .expect("QICK_TOKEN_ADDRESS must be set already") + .clone(), + "QORTY" => QORTY_TOKEN_ADDRESS + .expect("QORTY_TOKEN_ADDRESS must be set already") + .clone(), + _ => panic!("Expected QICK or QORTY ticker"), + }; + ( + contract_address, + QRC20_SWAP_CONTRACT_ADDRESS + .expect("QRC20_SWAP_CONTRACT_ADDRESS must be set already") + .clone(), + ) + }; + let platform = "QTUM"; + let ctx = MmCtxBuilder::new().into_mm_arc(); + let confpath = unsafe { QTUM_CONF_PATH.as_ref().expect("Qtum config is not set yet") }; + let conf = json!({ + "coin":ticker, + "decimals": 8, + "required_confirmations":0, + "pubtype":120, + "p2shtype":50, + "wiftype":128, + "segwit":true, + "mm2":1, + "mature_confirmations":500, + "network":"regtest", + "confpath": confpath, + }); + let req = json!({ + "method": "enable", + "swap_contract_address": format!("{:#02x}", swap_contract_address), + }); + let coin = unwrap!(block_on(qrc20_coin_from_conf_and_request( + &ctx, + ticker, + platform, + &conf, + &req, + &priv_key, + contract_address, + ))); + + import_address(&coin); + (ctx, coin) +} + +/// Generate random privkey, create a QRC20 coin and fill it's address with the specified balance. +fn generate_qrc20_coin_with_random_privkey( + ticker: &str, + qtum_balance: BigDecimal, + qrc20_balance: BigDecimal, +) -> (MmArc, Qrc20Coin, [u8; 32]) { + let priv_key = SecretKey::random(&mut rand4::thread_rng()).serialize(); + let (ctx, coin) = qrc20_coin_from_privkey(ticker, &priv_key); + + let timeout = (now_ms() / 1000) + 120; // timeout if test takes more than 40 seconds to run + let my_address = coin.my_address().expect("!my_address"); + fill_address(&coin, &my_address, qtum_balance, timeout); + fill_qrc20_address(&coin, qrc20_balance, timeout); + (ctx, coin, priv_key) +} + +/// Get only one address assigned the specified label. +fn get_address_by_label(coin: T, label: &str) -> String +where + T: AsRef, +{ + let native = match coin.as_ref().rpc_client { + UtxoRpcClientEnum::Native(ref native) => native, + UtxoRpcClientEnum::Electrum(_) => panic!("NativeClient expected"), + }; + let mut addresses = native + .get_addresses_by_label(label) + .wait() + .expect("!getaddressesbylabel") + .into_iter(); + match addresses.next() { + Some((addr, _purpose)) if addresses.next().is_none() => addr, + Some(_) => panic!("Expected only one address by {:?}", label), + None => panic!("Expected one address by {:?}", label), + } +} + +fn fill_qrc20_address(coin: &Qrc20Coin, amount: BigDecimal, timeout: u64) { + // prevent concurrent fill since daemon RPC returns errors if send_to_address + // is called concurrently (insufficient funds) and it also may return other errors + // if previous transaction is not confirmed yet + let _lock = unwrap!(COINS_LOCK.lock()); + let client = match coin.as_ref().rpc_client { + UtxoRpcClientEnum::Native(ref client) => client, + UtxoRpcClientEnum::Electrum(_) => panic!("Expected NativeClient"), + }; + + let from_addr = get_address_by_label(coin, QTUM_ADDRESS_LABEL); + let to_addr = coin.my_addr_as_contract_addr(); + let satoshis = sat_from_big_decimal(&amount, coin.as_ref().decimals).expect("!sat_from_big_decimal"); + + let hash = client + .transfer_tokens( + &coin.contract_address, + &from_addr, + to_addr, + satoshis.into(), + coin.as_ref().decimals, + ) + .wait() + .expect("!transfer_tokens") + .txid; + + let tx_bytes = client.get_transaction_bytes(hash).wait().unwrap(); + log!({ "{:02x}", tx_bytes }); + unwrap!(coin.wait_for_confirmations(&tx_bytes, 1, false, timeout, 1).wait()); +} + +pub async fn enable_qrc20_native(mm: &MarketMakerIt, coin: &str) -> Json { + let swap_contract_address = unsafe { + QRC20_SWAP_CONTRACT_ADDRESS + .expect("QRC20_SWAP_CONTRACT_ADDRESS must be set already") + .clone() + }; + + let native = unwrap!( + mm.rpc(json! ({ + "userpass": mm.userpass, + "method": "enable", + "coin": coin, + "swap_contract_address": format!("{:#02x}", swap_contract_address), + "mm2": 1, + })) + .await + ); + assert_eq!(native.0, StatusCode::OK, "'enable' failed: {}", native.1); + unwrap!(json::from_str(&native.1)) +} + +fn qrc20_coin_conf_item(ticker: &str) -> Json { + let contract_address = unsafe { + match ticker { + "QICK" => QICK_TOKEN_ADDRESS + .expect("QICK_TOKEN_ADDRESS must be set already") + .clone(), + "QORTY" => QORTY_TOKEN_ADDRESS + .expect("QORTY_TOKEN_ADDRESS must be set already") + .clone(), + _ => panic!("Expected either QICK or QORTY ticker, found {}", ticker), + } + }; + let contract_address = format!("{:#02x}", contract_address); + + let confpath = unsafe { QTUM_CONF_PATH.as_ref().expect("Qtum config is not set yet") }; + json!({ + "coin":ticker, + "required_confirmations":1, + "pubtype":120, + "p2shtype":50, + "wiftype":128, + "segwit":true, + "mature_confirmations":500, + "confpath":confpath, + "network":"regtest", + "protocol":{"type":"QRC20","protocol_data":{"platform":"QTUM","contract_address":contract_address}}}) +} + +fn trade_base_rel((base, rel): (&str, &str)) { + /// Generate a wallet with the random private key and fill the wallet with Qtum (required by gas_fee) and specified in `ticker` coin. + fn generate_and_fill_priv_key(ticker: &str) -> [u8; 32] { + let priv_key = SecretKey::random(&mut rand4::thread_rng()).serialize(); + let timeout = (now_ms() / 1000) + 80; // timeout if test takes more than 40 seconds to run + + match ticker { + "QICK" | "QORTY" => { + let (_ctx, coin) = qrc20_coin_from_privkey(ticker, &priv_key); + let my_address = coin.my_address().expect("!my_address"); + fill_address(&coin, &my_address, 10.into(), timeout); + fill_qrc20_address(&coin, 10.into(), timeout); + }, + "MYCOIN" | "MYCOIN1" => { + let (_ctx, coin) = utxo_coin_from_privkey(ticker, &priv_key); + let my_address = coin.my_address().expect("!my_address"); + fill_address(&coin, &my_address, 10.into(), timeout); + // also fill the Qtum + let (_ctx, coin) = qrc20_coin_from_privkey("QICK", &priv_key); + let my_address = coin.my_address().expect("!my_address"); + fill_address(&coin, &my_address, 10.into(), timeout); + }, + _ => panic!("Expected either QICK or QORTY or MYCOIN or MYCOIN1, found {}", ticker), + } + + priv_key + } + + let bob_priv_key = generate_and_fill_priv_key(base); + let alice_priv_key = generate_and_fill_priv_key(rel); + + let coins = json! ([ + qrc20_coin_conf_item("QICK"), + qrc20_coin_conf_item("QORTY"), + {"coin":"MYCOIN","asset":"MYCOIN","required_confirmations":0,"txversion":4,"overwintered":1,"txfee":1000,"protocol":{"type":"UTXO"}}, + {"coin":"MYCOIN1","asset":"MYCOIN1","required_confirmations":0,"txversion":4,"overwintered":1,"txfee":1000,"protocol":{"type":"UTXO"}}, + ]); + let mut mm_bob = unwrap!(MarketMakerIt::start( + json! ({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(bob_priv_key)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + }), + "pass".to_string(), + None, + )); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + unwrap!(block_on( + mm_bob.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats ")) + )); + + let mut mm_alice = unwrap!(MarketMakerIt::start( + json! ({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(alice_priv_key)), + "coins": coins, + "rpc_password": "pass", + "seednodes": vec![format!("{}", mm_bob.ip)], + }), + "pass".to_string(), + None, + )); + let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); + unwrap!(block_on( + mm_alice.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats ")) + )); + + log!([block_on(enable_qrc20_native(&mm_bob, "QICK"))]); + log!([block_on(enable_qrc20_native(&mm_bob, "QORTY"))]); + log!([block_on(enable_native(&mm_bob, "MYCOIN", vec![]))]); + log!([block_on(enable_native(&mm_bob, "MYCOIN1", vec![]))]); + + log!([block_on(enable_qrc20_native(&mm_alice, "QICK"))]); + log!([block_on(enable_qrc20_native(&mm_alice, "QORTY"))]); + log!([block_on(enable_native(&mm_alice, "MYCOIN", vec![]))]); + log!([block_on(enable_native(&mm_alice, "MYCOIN1", vec![]))]); + let rc = unwrap!(block_on(mm_bob.rpc(json! ({ + "userpass": mm_bob.userpass, + "method": "setprice", + "base": base, + "rel": rel, + "price": 1, + "volume": "3", + })))); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + + thread::sleep(Duration::from_secs(12)); + + log!("Issue alice " (base) "/" (rel) " buy request"); + let rc = unwrap!(block_on(mm_alice.rpc(json! ({ + "userpass": mm_alice.userpass, + "method": "buy", + "base": base, + "rel": rel, + "price": 1, + "volume": "2", + })))); + assert!(rc.0.is_success(), "!buy: {}", rc.1); + let buy_json: Json = unwrap!(serde_json::from_str(&rc.1)); + let uuid = buy_json["result"]["uuid"].as_str().unwrap().to_owned(); + + // ensure the swaps are started + unwrap!(block_on(mm_bob.wait_for_log(22., |log| { + log.contains(&format!("Entering the maker_swap_loop {}/{}", base, rel)) + }))); + unwrap!(block_on(mm_alice.wait_for_log(22., |log| { + log.contains(&format!("Entering the taker_swap_loop {}/{}", base, rel)) + }))); + + // ensure the swaps are finished + unwrap!(block_on(mm_bob.wait_for_log(600., |log| { + log.contains(&format!("[swap uuid={}] Finished", uuid)) + }))); + unwrap!(block_on(mm_alice.wait_for_log(600., |log| { + log.contains(&format!("[swap uuid={}] Finished", uuid)) + }))); + + log!("Checking alice/taker status.."); + block_on(check_my_swap_status( + &mm_alice, + &uuid, + &TAKER_SUCCESS_EVENTS, + &TAKER_ERROR_EVENTS, + "2".parse().unwrap(), + "2".parse().unwrap(), + )); + + log!("Checking bob/maker status.."); + block_on(check_my_swap_status( + &mm_bob, + &uuid, + &MAKER_SUCCESS_EVENTS, + &MAKER_ERROR_EVENTS, + "2".parse().unwrap(), + "2".parse().unwrap(), + )); + + log!("Waiting 3 seconds for nodes to broadcast their swaps data.."); + thread::sleep(Duration::from_secs(3)); + + log!("Checking alice status.."); + block_on(check_stats_swap_status( + &mm_alice, + &uuid, + &MAKER_SUCCESS_EVENTS, + &TAKER_SUCCESS_EVENTS, + )); + + log!("Checking bob status.."); + block_on(check_stats_swap_status( + &mm_bob, + &uuid, + &MAKER_SUCCESS_EVENTS, + &TAKER_SUCCESS_EVENTS, + )); + + log!("Checking alice recent swaps.."); + block_on(check_recent_swaps(&mm_alice, 1)); + log!("Checking bob recent swaps.."); + block_on(check_recent_swaps(&mm_bob, 1)); + + unwrap!(block_on(mm_bob.stop())); + unwrap!(block_on(mm_alice.stop())); +} + +#[test] +fn test_taker_spends_maker_payment() { + let (_ctx, maker_coin, _priv_key) = generate_qrc20_coin_with_random_privkey("QICK", 20.into(), 10.into()); + let (_ctx, taker_coin, _priv_key) = generate_qrc20_coin_with_random_privkey("QICK", 20.into(), 1.into()); + let maker_old_balance = maker_coin.my_balance().wait().expect("Error on get maker balance"); + let taker_old_balance = taker_coin.my_balance().wait().expect("Error on get taker balance"); + assert_eq!(maker_old_balance, BigDecimal::from(10)); + assert_eq!(taker_old_balance, BigDecimal::from(1)); + + let timelock = (now_ms() / 1000) as u32 - 200; + let maker_pub = &*maker_coin.my_public_key(); + let taker_pub = &*taker_coin.my_public_key(); + let secret = &[1; 32]; + let secret_hash = &*dhash160(secret); + let amount = BigDecimal::from(0.2); + + let payment = maker_coin + .send_maker_payment( + timelock, + taker_pub, + secret_hash, + amount.clone(), + &maker_coin.swap_contract_address(), + ) + .wait() + .unwrap(); + let payment_tx_hash = payment.tx_hash(); + let payment_tx_hex = payment.tx_hex(); + log!("Maker payment: "[payment_tx_hash]); + + let confirmations = 1; + let requires_nota = false; + let wait_until = (now_ms() / 1000) + 40; // timeout if test takes more than 40 seconds to run + let check_every = 1; + unwrap!(taker_coin + .wait_for_confirmations(&payment_tx_hex, confirmations, requires_nota, wait_until, check_every) + .wait()); + + unwrap!(taker_coin + .validate_maker_payment( + &payment_tx_hex, + timelock, + maker_pub, + secret_hash, + amount.clone(), + &taker_coin.swap_contract_address(), + ) + .wait()); + + let spend = unwrap!(taker_coin + .send_taker_spends_maker_payment( + &payment_tx_hex, + timelock, + maker_pub, + secret, + &taker_coin.swap_contract_address(), + ) + .wait()); + let spend_tx_hash = spend.tx_hash(); + let spend_tx_hex = spend.tx_hex(); + log!("Taker spends tx: "[spend_tx_hash]); + + let wait_until = (now_ms() / 1000) + 40; // timeout if test takes more than 40 seconds to run + unwrap!(taker_coin + .wait_for_confirmations(&spend_tx_hex, confirmations, requires_nota, wait_until, check_every) + .wait()); + + let maker_balance = maker_coin.my_balance().wait().expect("Error on get maker balance"); + let taker_balance = taker_coin.my_balance().wait().expect("Error on get taker balance"); + assert_eq!(maker_old_balance - amount.clone(), maker_balance); + assert_eq!(taker_old_balance + amount, taker_balance); +} + +#[test] +fn test_maker_spends_taker_payment() { + let (_ctx, maker_coin, _priv_key) = generate_qrc20_coin_with_random_privkey("QICK", 20.into(), 10.into()); + let (_ctx, taker_coin, _priv_key) = generate_qrc20_coin_with_random_privkey("QICK", 20.into(), 10.into()); + let maker_old_balance = maker_coin.my_balance().wait().expect("Error on get maker balance"); + let taker_old_balance = taker_coin.my_balance().wait().expect("Error on get taker balance"); + assert_eq!(maker_old_balance, BigDecimal::from(10)); + assert_eq!(taker_old_balance, BigDecimal::from(10)); + + let timelock = (now_ms() / 1000) as u32 - 200; + let maker_pub = &*maker_coin.my_public_key(); + let taker_pub = &*taker_coin.my_public_key(); + let secret = &[1; 32]; + let secret_hash = &*dhash160(secret); + let amount = BigDecimal::from(0.2); + + let payment = taker_coin + .send_taker_payment( + timelock, + maker_pub, + secret_hash, + amount.clone(), + &taker_coin.swap_contract_address(), + ) + .wait() + .unwrap(); + let payment_tx_hash = payment.tx_hash(); + let payment_tx_hex = payment.tx_hex(); + log!("Taker payment: "[payment_tx_hash]); + + let confirmations = 1; + let requires_nota = false; + let wait_until = (now_ms() / 1000) + 40; // timeout if test takes more than 40 seconds to run + let check_every = 1; + unwrap!(maker_coin + .wait_for_confirmations(&payment_tx_hex, confirmations, requires_nota, wait_until, check_every) + .wait()); + + unwrap!(maker_coin + .validate_taker_payment( + &payment_tx_hex, + timelock, + taker_pub, + secret_hash, + amount.clone(), + &maker_coin.swap_contract_address(), + ) + .wait()); + + let spend = unwrap!(maker_coin + .send_maker_spends_taker_payment( + &payment_tx_hex, + timelock, + taker_pub, + secret, + &maker_coin.swap_contract_address(), + ) + .wait()); + let spend_tx_hash = spend.tx_hash(); + let spend_tx_hex = spend.tx_hex(); + log!("Maker spends tx: "[spend_tx_hash]); + + let wait_until = (now_ms() / 1000) + 40; // timeout if test takes more than 40 seconds to run + unwrap!(maker_coin + .wait_for_confirmations(&spend_tx_hex, confirmations, requires_nota, wait_until, check_every) + .wait()); + + let maker_balance = maker_coin.my_balance().wait().expect("Error on get maker balance"); + let taker_balance = taker_coin.my_balance().wait().expect("Error on get taker balance"); + assert_eq!(maker_old_balance + amount.clone(), maker_balance); + assert_eq!(taker_old_balance - amount, taker_balance); +} + +#[test] +fn test_maker_refunds_payment() { + let (_ctx, coin, _priv_key) = generate_qrc20_coin_with_random_privkey("QICK", 20.into(), 10.into()); + let expected_balance = unwrap!(coin.my_balance().wait()); + assert_eq!(expected_balance, BigDecimal::from(10)); + + let timelock = (now_ms() / 1000) as u32 - 200; + let taker_pub = hex::decode("022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1a").unwrap(); + let secret_hash = &[1; 20]; + let amount = BigDecimal::from_str("0.2").unwrap(); + + let payment = coin + .send_maker_payment( + timelock, + &taker_pub, + secret_hash, + amount.clone(), + &coin.swap_contract_address(), + ) + .wait() + .unwrap(); + let payment_tx_hash = payment.tx_hash(); + let payment_tx_hex = payment.tx_hex(); + log!("Maker payment: "[payment_tx_hash]); + + let confirmations = 1; + let requires_nota = false; + let wait_until = (now_ms() / 1000) + 40; // timeout if test takes more than 40 seconds to run + let check_every = 1; + unwrap!(coin + .wait_for_confirmations(&payment_tx_hex, confirmations, requires_nota, wait_until, check_every) + .wait()); + + let balance_after_payment = unwrap!(coin.my_balance().wait()); + assert_eq!(expected_balance.clone() - amount, balance_after_payment); + + let refund = unwrap!(coin + .send_maker_refunds_payment( + &payment_tx_hex, + timelock, + &taker_pub, + secret_hash, + &coin.swap_contract_address(), + ) + .wait()); + let refund_tx_hash = refund.tx_hash(); + let refund_tx_hex = refund.tx_hex(); + log!("Maker refunds payment: "[refund_tx_hash]); + + let wait_until = (now_ms() / 1000) + 40; // timeout if test takes more than 40 seconds to run + unwrap!(coin + .wait_for_confirmations(&refund_tx_hex, confirmations, requires_nota, wait_until, check_every) + .wait()); + + let balance_after_refund = unwrap!(coin.my_balance().wait()); + assert_eq!(expected_balance, balance_after_refund); +} + +#[test] +fn test_taker_refunds_payment() { + let (_ctx, coin, _priv_key) = generate_qrc20_coin_with_random_privkey("QICK", 20.into(), 10.into()); + let expected_balance = unwrap!(coin.my_balance().wait()); + assert_eq!(expected_balance, BigDecimal::from(10)); + + let timelock = (now_ms() / 1000) as u32 - 200; + let maker_pub = hex::decode("022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1a").unwrap(); + let secret_hash = &[1; 20]; + let amount = BigDecimal::from_str("0.2").unwrap(); + + let payment = coin + .send_taker_payment( + timelock, + &maker_pub, + secret_hash, + amount.clone(), + &coin.swap_contract_address(), + ) + .wait() + .unwrap(); + let payment_tx_hash = payment.tx_hash(); + let payment_tx_hex = payment.tx_hex(); + log!("Taker payment: "[payment_tx_hash]); + + let confirmations = 1; + let requires_nota = false; + let wait_until = (now_ms() / 1000) + 40; // timeout if test takes more than 40 seconds to run + let check_every = 1; + unwrap!(coin + .wait_for_confirmations(&payment_tx_hex, confirmations, requires_nota, wait_until, check_every) + .wait()); + + let balance_after_payment = unwrap!(coin.my_balance().wait()); + assert_eq!(expected_balance.clone() - amount, balance_after_payment); + + let refund = unwrap!(coin + .send_taker_refunds_payment( + &payment_tx_hex, + timelock, + &maker_pub, + secret_hash, + &coin.swap_contract_address(), + ) + .wait()); + let refund_tx_hash = refund.tx_hash(); + let refund_tx_hex = refund.tx_hex(); + log!("Taker refunds payment: "[refund_tx_hash]); + + let wait_until = (now_ms() / 1000) + 40; // timeout if test takes more than 40 seconds to run + unwrap!(coin + .wait_for_confirmations(&refund_tx_hex, confirmations, requires_nota, wait_until, check_every) + .wait()); + + let balance_after_refund = unwrap!(coin.my_balance().wait()); + assert_eq!(expected_balance, balance_after_refund); +} + +#[test] +fn test_check_if_my_payment_sent() { + let (_ctx, coin, _priv_key) = generate_qrc20_coin_with_random_privkey("QICK", 20.into(), 10.into()); + let timelock = (now_ms() / 1000) as u32 - 200; + let taker_pub = hex::decode("022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1a").unwrap(); + let secret_hash = &[1; 20]; + let amount = BigDecimal::from_str("0.2").unwrap(); + + let payment = coin + .send_maker_payment( + timelock, + &taker_pub, + secret_hash, + amount.clone(), + &coin.swap_contract_address(), + ) + .wait() + .unwrap(); + let payment_tx_hash = payment.tx_hash(); + let payment_tx_hex = payment.tx_hex(); + log!("Maker payment: "[payment_tx_hash]); + + let confirmations = 2; + let requires_nota = false; + let wait_until = (now_ms() / 1000) + 40; // timeout if test takes more than 40 seconds to run + let check_every = 1; + unwrap!(coin + .wait_for_confirmations(&payment_tx_hex, confirmations, requires_nota, wait_until, check_every) + .wait()); + + let search_from_block = coin.current_block().wait().expect("!current_block") - 10; + let found = unwrap!(coin + .check_if_my_payment_sent( + timelock, + &taker_pub, + secret_hash, + search_from_block, + &coin.swap_contract_address(), + ) + .wait()); + assert_eq!(found, Some(payment)); +} + +#[test] +fn test_search_for_swap_tx_spend_taker_spent() { + let (_ctx, maker_coin, _priv_key) = generate_qrc20_coin_with_random_privkey("QICK", 20.into(), 10.into()); + let (_ctx, taker_coin, _priv_key) = generate_qrc20_coin_with_random_privkey("QICK", 20.into(), 1.into()); + let search_from_block = maker_coin.current_block().wait().expect("!current_block"); + + let timelock = (now_ms() / 1000) as u32 - 200; + let maker_pub = &*maker_coin.my_public_key(); + let taker_pub = &*taker_coin.my_public_key(); + let secret = &[1; 32]; + let secret_hash = &*dhash160(secret); + let amount = BigDecimal::from(0.2); + + let payment = maker_coin + .send_maker_payment( + timelock, + taker_pub, + secret_hash, + amount.clone(), + &maker_coin.swap_contract_address(), + ) + .wait() + .unwrap(); + let payment_tx_hash = payment.tx_hash(); + let payment_tx_hex = payment.tx_hex(); + log!("Maker payment: "[payment_tx_hash]); + + let confirmations = 1; + let requires_nota = false; + let wait_until = (now_ms() / 1000) + 40; // timeout if test takes more than 40 seconds to run + let check_every = 1; + unwrap!(taker_coin + .wait_for_confirmations(&payment_tx_hex, confirmations, requires_nota, wait_until, check_every) + .wait()); + + let spend = unwrap!(taker_coin + .send_taker_spends_maker_payment( + &payment_tx_hex, + timelock, + maker_pub, + secret, + &taker_coin.swap_contract_address(), + ) + .wait()); + let spend_tx_hash = spend.tx_hash(); + let spend_tx_hex = spend.tx_hex(); + log!("Taker spends tx: "[spend_tx_hash]); + + let wait_until = (now_ms() / 1000) + 40; // timeout if test takes more than 40 seconds to run + unwrap!(taker_coin + .wait_for_confirmations(&spend_tx_hex, confirmations, requires_nota, wait_until, check_every) + .wait()); + + let actual = maker_coin.search_for_swap_tx_spend_my( + timelock, + taker_pub, + secret_hash, + &payment_tx_hex, + search_from_block, + &maker_coin.swap_contract_address(), + ); + let expected = Ok(Some(FoundSwapTxSpend::Spent(spend))); + assert_eq!(actual, expected); +} + +#[test] +fn test_search_for_swap_tx_spend_maker_refunded() { + let (_ctx, maker_coin, _priv_key) = generate_qrc20_coin_with_random_privkey("QICK", 20.into(), 10.into()); + let search_from_block = maker_coin.current_block().wait().expect("!current_block"); + + let timelock = (now_ms() / 1000) as u32 - 200; + let taker_pub = hex::decode("022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1a").unwrap(); + let secret = &[1; 32]; + let secret_hash = &*dhash160(secret); + let amount = BigDecimal::from(0.2); + + let payment = maker_coin + .send_maker_payment( + timelock, + &taker_pub, + secret_hash, + amount.clone(), + &maker_coin.swap_contract_address(), + ) + .wait() + .unwrap(); + let payment_tx_hash = payment.tx_hash(); + let payment_tx_hex = payment.tx_hex(); + log!("Maker payment: "[payment_tx_hash]); + + let confirmations = 1; + let requires_nota = false; + let wait_until = (now_ms() / 1000) + 40; // timeout if test takes more than 40 seconds to run + let check_every = 1; + unwrap!(maker_coin + .wait_for_confirmations(&payment_tx_hex, confirmations, requires_nota, wait_until, check_every) + .wait()); + + let refund = unwrap!(maker_coin + .send_maker_refunds_payment( + &payment_tx_hex, + timelock, + &taker_pub, + secret_hash, + &maker_coin.swap_contract_address(), + ) + .wait()); + let refund_tx_hash = refund.tx_hash(); + let refund_tx_hex = refund.tx_hex(); + log!("Maker refunds tx: "[refund_tx_hash]); + + let wait_until = (now_ms() / 1000) + 40; // timeout if test takes more than 40 seconds to run + unwrap!(maker_coin + .wait_for_confirmations(&refund_tx_hex, confirmations, requires_nota, wait_until, check_every) + .wait()); + + let actual = maker_coin.search_for_swap_tx_spend_my( + timelock, + &taker_pub, + secret_hash, + &payment_tx_hex, + search_from_block, + &maker_coin.swap_contract_address(), + ); + let expected = Ok(Some(FoundSwapTxSpend::Refunded(refund))); + assert_eq!(actual, expected); +} + +#[test] +fn test_search_for_swap_tx_spend_not_spent() { + let (_ctx, maker_coin, _priv_key) = generate_qrc20_coin_with_random_privkey("QICK", 20.into(), 10.into()); + let search_from_block = maker_coin.current_block().wait().expect("!current_block"); + + let timelock = (now_ms() / 1000) as u32 - 200; + let taker_pub = hex::decode("022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1a").unwrap(); + let secret = &[1; 32]; + let secret_hash = &*dhash160(secret); + let amount = BigDecimal::from(0.2); + + let payment = maker_coin + .send_maker_payment( + timelock, + &taker_pub, + secret_hash, + amount.clone(), + &maker_coin.swap_contract_address(), + ) + .wait() + .unwrap(); + let payment_tx_hash = payment.tx_hash(); + let payment_tx_hex = payment.tx_hex(); + log!("Maker payment: "[payment_tx_hash]); + + let confirmations = 1; + let requires_nota = false; + let wait_until = (now_ms() / 1000) + 40; // timeout if test takes more than 40 seconds to run + let check_every = 1; + unwrap!(maker_coin + .wait_for_confirmations(&payment_tx_hex, confirmations, requires_nota, wait_until, check_every) + .wait()); + + let actual = maker_coin.search_for_swap_tx_spend_my( + timelock, + &taker_pub, + secret_hash, + &payment_tx_hex, + search_from_block, + &maker_coin.swap_contract_address(), + ); + // maker payment hasn't been spent or refunded yet + assert_eq!(actual, Ok(None)); +} + +#[test] +fn test_wait_for_tx_spend() { + let (_ctx, maker_coin, _priv_key) = generate_qrc20_coin_with_random_privkey("QICK", 20.into(), 10.into()); + let (_ctx, taker_coin, _priv_key) = generate_qrc20_coin_with_random_privkey("QICK", 20.into(), 1.into()); + let from_block = maker_coin.current_block().wait().expect("!current_block"); + + let timelock = (now_ms() / 1000) as u32 - 200; + let maker_pub = &*maker_coin.my_public_key(); + let taker_pub = &*taker_coin.my_public_key(); + let secret = &[1; 32]; + let secret_hash = &*dhash160(secret); + let amount = BigDecimal::from(0.2); + + let payment = maker_coin + .send_maker_payment( + timelock, + taker_pub, + secret_hash, + amount.clone(), + &maker_coin.swap_contract_address(), + ) + .wait() + .unwrap(); + let payment_tx_hash = payment.tx_hash(); + let payment_tx_hex = payment.tx_hex(); + log!("Maker payment: "[payment_tx_hash]); + + let confirmations = 1; + let requires_nota = false; + let wait_until = (now_ms() / 1000) + 40; // timeout if test takes more than 40 seconds to run + let check_every = 1; + unwrap!(taker_coin + .wait_for_confirmations(&payment_tx_hex, confirmations, requires_nota, wait_until, check_every) + .wait()); + + // first try to check if the wait_for_tx_spend() returns an error correctly + let wait_until = (now_ms() / 1000) + 5; + let err = maker_coin + .wait_for_tx_spend( + &payment_tx_hex, + wait_until, + from_block, + &maker_coin.swap_contract_address(), + ) + .wait() + .expect_err("Expected 'Waited too long' error"); + log!("error: "[err]); + assert!(err.contains("Waited too long")); + + // also spends the maker payment and try to check if the wait_for_tx_spend() returns the correct tx + static mut SPEND_TX: Option = None; + + let maker_pub_c = maker_pub.to_vec(); + let payment_hex = payment_tx_hex.clone(); + thread::spawn(move || { + thread::sleep(Duration::from_secs(5)); + + let spend = unwrap!(taker_coin + .send_taker_spends_maker_payment( + &payment_hex, + timelock, + &maker_pub_c, + secret, + &taker_coin.swap_contract_address(), + ) + .wait()); + unsafe { SPEND_TX = Some(spend) } + }); + + let wait_until = (now_ms() / 1000) + 120; + let found = unwrap!(maker_coin + .wait_for_tx_spend( + &payment_tx_hex, + wait_until, + from_block, + &maker_coin.swap_contract_address(), + ) + .wait()); + + unsafe { assert_eq!(Some(found), SPEND_TX) } +} + +#[test] +fn test_trade_qrc20() { trade_base_rel(("QICK", "QORTY")); } + +#[test] +#[ignore] +fn test_trade_qrc20_utxo() { trade_base_rel(("QICK", "MYCOIN")); } + +#[test] +#[ignore] +fn test_trade_utxo_qrc20() { trade_base_rel(("MYCOIN", "QICK")); } diff --git a/mm2src/lp_swap/maker_swap.rs b/mm2src/lp_swap/maker_swap.rs index f2276769b2..6642422453 100644 --- a/mm2src/lp_swap/maker_swap.rs +++ b/mm2src/lp_swap/maker_swap.rs @@ -18,7 +18,7 @@ use futures01::Future; use parking_lot::Mutex as PaMutex; use primitives::hash::H264; use rand::Rng; -use rpc::v1::types::{H160 as H160Json, H256 as H256Json, H264 as H264Json}; +use rpc::v1::types::{Bytes as BytesJson, H160 as H160Json, H256 as H256Json, H264 as H264Json}; use serde_json::{self as json}; use std::path::PathBuf; use std::sync::{atomic::Ordering, Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; @@ -116,6 +116,10 @@ pub struct MakerSwapData { started_at: u64, maker_coin_start_block: u64, taker_coin_start_block: u64, + #[serde(skip_serializing_if = "Option::is_none")] + maker_coin_swap_contract_address: Option, + #[serde(skip_serializing_if = "Option::is_none")] + taker_coin_swap_contract_address: Option, } pub struct MakerSwapMut { @@ -298,6 +302,9 @@ impl MakerSwap { }, }; + let maker_coin_swap_contract_address = self.maker_coin.swap_contract_address(); + let taker_coin_swap_contract_address = self.taker_coin.swap_contract_address(); + let data = MakerSwapData { taker_coin: self.taker_coin.ticker().to_owned(), maker_coin: self.maker_coin.ticker().to_owned(), @@ -317,6 +324,8 @@ impl MakerSwap { uuid: self.uuid, maker_coin_start_block, taker_coin_start_block, + maker_coin_swap_contract_address, + taker_coin_swap_contract_address, }; Ok((Some(MakerSwapCommand::Negotiate), vec![MakerSwapEvent::Started(data)])) @@ -474,6 +483,7 @@ impl MakerSwap { &*self.r().other_persistent_pub, &*dhash160(&self.r().data.secret.0), self.r().data.maker_coin_start_block, + &self.r().data.maker_coin_swap_contract_address, ) .compat(); let transaction = match transaction_f.await { @@ -491,6 +501,7 @@ impl MakerSwap { &*self.r().other_persistent_pub, &*dhash160(&self.r().data.secret.0), self.maker_amount.clone(), + &self.r().data.maker_coin_swap_contract_address, ); match payment_fut.compat().await { @@ -632,6 +643,7 @@ impl MakerSwap { &*self.r().other_persistent_pub, &*dhash160(&self.r().data.secret.0), self.taker_amount.clone(), + &self.r().data.taker_coin_swap_contract_address, ) .compat(); @@ -668,6 +680,7 @@ impl MakerSwap { self.taker_payment_lock.load(Ordering::Relaxed) as u32, &*self.r().other_persistent_pub, &self.r().data.secret.0, + &self.r().data.taker_coin_swap_contract_address, ); let transaction = match spend_fut.compat().await { @@ -736,6 +749,7 @@ impl MakerSwap { self.r().data.maker_payment_lock as u32, &*self.r().other_persistent_pub, &*dhash160(&self.r().data.secret.0), + &self.r().data.maker_coin_swap_contract_address, ); let transaction = match spend_fut.compat().await { @@ -779,47 +793,55 @@ impl MakerSwap { ctx: MmArc, maker_coin: MmCoinEnum, taker_coin: MmCoinEnum, - saved: MakerSavedSwap, + mut saved: MakerSavedSwap, ) -> Result<(Self, Option), String> { if saved.events.is_empty() { return ERR!("Can't restore swap from empty events set"); }; - match &saved.events[0].event { - MakerSwapEvent::Started(data) => { - let mut taker = bits256::from([0; 32]); - taker.bytes = data.taker.0; - let my_persistent_pub = H264::from(&**ctx.secp256k1_key_pair().public()); - let conf_settings = SwapConfirmationsSettings { - maker_coin_confs: data.maker_payment_confirmations, - maker_coin_nota: data - .maker_payment_requires_nota - .unwrap_or_else(|| maker_coin.requires_notarization()), - taker_coin_confs: data.taker_payment_confirmations, - taker_coin_nota: data - .taker_payment_requires_nota - .unwrap_or_else(|| taker_coin.requires_notarization()), - }; - let swap = MakerSwap::new( - ctx, - taker, - data.maker_amount.clone(), - data.taker_amount.clone(), - my_persistent_pub, - saved.uuid, - conf_settings, - maker_coin, - taker_coin, - data.lock_duration, - ); - let command = saved.events.last().unwrap().get_command(); - for saved_event in saved.events { - try_s!(swap.apply_event(saved_event.event)); - } - Ok((swap, command)) - }, - _ => ERR!("First swap event must be Started"), + let data = match saved.events[0].event { + MakerSwapEvent::Started(ref mut data) => data, + _ => return ERR!("First swap event must be Started"), + }; + + // refresh swap contract addresses if the swap file is out-dated (doesn't contain the fields yet) + if data.maker_coin_swap_contract_address.is_none() { + data.maker_coin_swap_contract_address = maker_coin.swap_contract_address(); + } + if data.taker_coin_swap_contract_address.is_none() { + data.taker_coin_swap_contract_address = taker_coin.swap_contract_address(); + } + + let mut taker = bits256::from([0; 32]); + taker.bytes = data.taker.0; + let my_persistent_pub = H264::from(&**ctx.secp256k1_key_pair().public()); + let conf_settings = SwapConfirmationsSettings { + maker_coin_confs: data.maker_payment_confirmations, + maker_coin_nota: data + .maker_payment_requires_nota + .unwrap_or_else(|| maker_coin.requires_notarization()), + taker_coin_confs: data.taker_payment_confirmations, + taker_coin_nota: data + .taker_payment_requires_nota + .unwrap_or_else(|| taker_coin.requires_notarization()), + }; + let swap = MakerSwap::new( + ctx, + taker, + data.maker_amount.clone(), + data.taker_amount.clone(), + my_persistent_pub, + saved.uuid, + conf_settings, + maker_coin, + taker_coin, + data.lock_duration, + ); + let command = saved.events.last().unwrap().get_command(); + for saved_event in saved.events { + try_s!(swap.apply_event(saved_event.event)); } + Ok((swap, command)) } pub fn recover_funds(&self) -> Result { @@ -840,6 +862,7 @@ impl MakerSwap { secret_hash, taker_payment_hex, selfi.r().data.taker_coin_start_block, + &selfi.r().data.taker_coin_swap_contract_address, ) { Ok(Some(FoundSwapTxSpend::Spent(tx))) => { return ERR!( @@ -861,7 +884,13 @@ impl MakerSwap { selfi .taker_coin - .send_maker_spends_taker_payment(taker_payment_hex, timelock, other_pub, &selfi.r().data.secret.0) + .send_maker_spends_taker_payment( + taker_payment_hex, + timelock, + other_pub, + &selfi.r().data.secret.0, + &selfi.r().data.taker_coin_swap_contract_address, + ) .wait() .map_err(|e| ERRL!("{}", e)) } @@ -891,6 +920,7 @@ impl MakerSwap { &*self.r().other_persistent_pub, &secret_hash.0, self.r().data.maker_coin_start_block, + &self.r().data.maker_coin_swap_contract_address, ) .wait()); match maybe_maker_payment { @@ -906,6 +936,7 @@ impl MakerSwap { &secret_hash.0, &maker_payment, self.r().data.maker_coin_start_block, + &self.r().data.maker_coin_swap_contract_address, ) { Ok(Some(FoundSwapTxSpend::Spent(_))) => { log!("Warning: MakerPayment spent, but TakerPayment is not yet. Trying to spend TakerPayment"); @@ -940,6 +971,7 @@ impl MakerSwap { self.r().data.maker_payment_lock as u32, &*self.r().other_persistent_pub, &secret_hash.0, + &self.r().data.maker_coin_swap_contract_address, ) .wait()); @@ -1391,8 +1423,8 @@ pub fn calc_max_maker_vol(ctx: &MmArc, balance: &BigDecimal, trade_fee: &TradeFe #[cfg(test)] mod maker_swap_tests { use super::*; - use coins::eth::{signed_eth_tx_from_bytes, SignedEthTx}; - use coins::{MarketCoinOps, SwapOps, TestCoin}; + use coins::eth::{addr_from_str, signed_eth_tx_from_bytes, SignedEthTx}; + use coins::{MarketCoinOps, MmCoin, SwapOps, TestCoin}; use common::mm_ctx::MmCtxBuilder; use common::privkey::key_pair_from_seed; use mocktopus::mocking::*; @@ -1427,18 +1459,19 @@ mod maker_swap_tests { let ctx = MmCtxBuilder::default().with_secp256k1_key_pair(key_pair).into_mm_arc(); TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); + TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); static mut MY_PAYMENT_SENT_CALLED: bool = false; - TestCoin::check_if_my_payment_sent.mock_safe(|_, _, _, _, _| { + TestCoin::check_if_my_payment_sent.mock_safe(|_, _, _, _, _, _| { unsafe { MY_PAYMENT_SENT_CALLED = true }; MockResult::Return(Box::new(futures01::future::ok(Some(eth_tx_for_test().into())))) }); static mut MAKER_REFUND_CALLED: bool = false; - TestCoin::send_maker_refunds_payment.mock_safe(|_, _, _, _, _| { + TestCoin::send_maker_refunds_payment.mock_safe(|_, _, _, _, _, _| { unsafe { MAKER_REFUND_CALLED = true }; MockResult::Return(Box::new(futures01::future::ok(eth_tx_for_test().into()))) }); - TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _, _, _, _, _| MockResult::Return(Ok(None))); + TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _, _, _, _, _, _| MockResult::Return(Ok(None))); let maker_coin = MmCoinEnum::Test(TestCoin {}); let taker_coin = MmCoinEnum::Test(TestCoin {}); let (maker_swap, _) = unwrap!(MakerSwap::load_from_saved( @@ -1469,14 +1502,15 @@ mod maker_swap_tests { let ctx = MmCtxBuilder::default().with_secp256k1_key_pair(key_pair).into_mm_arc(); TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); + TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); static mut MAKER_REFUND_CALLED: bool = false; - TestCoin::send_maker_refunds_payment.mock_safe(|_, _, _, _, _| { + TestCoin::send_maker_refunds_payment.mock_safe(|_, _, _, _, _, _| { unsafe { MAKER_REFUND_CALLED = true }; MockResult::Return(Box::new(futures01::future::ok(eth_tx_for_test().into()))) }); - TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _, _, _, _, _| MockResult::Return(Ok(None))); + TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _, _, _, _, _, _| MockResult::Return(Ok(None))); let maker_coin = MmCoinEnum::Test(TestCoin {}); let taker_coin = MmCoinEnum::Test(TestCoin {}); let (maker_swap, _) = unwrap!(MakerSwap::load_from_saved( @@ -1506,7 +1540,9 @@ mod maker_swap_tests { let ctx = MmCtxBuilder::default().with_secp256k1_key_pair(key_pair).into_mm_arc(); TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); - TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _, _, _, _, _| { + TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); + + TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _, _, _, _, _, _| { MockResult::Return(Ok(Some(FoundSwapTxSpend::Refunded(eth_tx_for_test().into())))) }); let maker_coin = MmCoinEnum::Test(TestCoin {}); @@ -1531,15 +1567,16 @@ mod maker_swap_tests { let ctx = MmCtxBuilder::default().with_secp256k1_key_pair(key_pair).into_mm_arc(); TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); + TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); static mut SEARCH_FOR_SWAP_TX_SPEND_MY_CALLED: bool = true; - TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _, _, _, _, _| { + TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _, _, _, _, _, _| { unsafe { SEARCH_FOR_SWAP_TX_SPEND_MY_CALLED = true } MockResult::Return(Ok(Some(FoundSwapTxSpend::Spent(eth_tx_for_test().into())))) }); static mut SEARCH_FOR_SWAP_TX_SPEND_OTHER_CALLED: bool = true; - TestCoin::search_for_swap_tx_spend_other.mock_safe(|_, _, _, _, _, _| { + TestCoin::search_for_swap_tx_spend_other.mock_safe(|_, _, _, _, _, _, _| { unsafe { SEARCH_FOR_SWAP_TX_SPEND_OTHER_CALLED = true } MockResult::Return(Ok(Some(FoundSwapTxSpend::Refunded(eth_tx_for_test().into())))) }); @@ -1570,12 +1607,14 @@ mod maker_swap_tests { let ctx = MmCtxBuilder::default().with_secp256k1_key_pair(key_pair).into_mm_arc(); TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); + TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); + static mut MY_PAYMENT_SENT_CALLED: bool = false; - TestCoin::check_if_my_payment_sent.mock_safe(|_, _, _, _, _| { + TestCoin::check_if_my_payment_sent.mock_safe(|_, _, _, _, _, _| { unsafe { MY_PAYMENT_SENT_CALLED = true }; MockResult::Return(Box::new(futures01::future::ok(Some(eth_tx_for_test().into())))) }); - TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _, _, _, _, _| MockResult::Return(Ok(None))); + TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _, _, _, _, _, _| MockResult::Return(Ok(None))); let maker_coin = MmCoinEnum::Test(TestCoin {}); let taker_coin = MmCoinEnum::Test(TestCoin {}); let (maker_swap, _) = unwrap!(MakerSwap::load_from_saved( @@ -1601,8 +1640,10 @@ mod maker_swap_tests { let ctx = MmCtxBuilder::default().with_secp256k1_key_pair(key_pair).into_mm_arc(); TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); + TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); + static mut MY_PAYMENT_SENT_CALLED: bool = false; - TestCoin::check_if_my_payment_sent.mock_safe(|_, _, _, _, _| { + TestCoin::check_if_my_payment_sent.mock_safe(|_, _, _, _, _, _| { unsafe { MY_PAYMENT_SENT_CALLED = true }; MockResult::Return(Box::new(futures01::future::ok(None))) }); @@ -1629,6 +1670,7 @@ mod maker_swap_tests { let ctx = MmCtxBuilder::default().with_secp256k1_key_pair(key_pair).into_mm_arc(); TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); + TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); let maker_coin = MmCoinEnum::Test(TestCoin {}); let taker_coin = MmCoinEnum::Test(TestCoin {}); let (maker_swap, _) = unwrap!(MakerSwap::load_from_saved( @@ -1651,15 +1693,16 @@ mod maker_swap_tests { let ctx = MmCtxBuilder::default().with_secp256k1_key_pair(key_pair).into_mm_arc(); TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); + TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); static mut SEARCH_FOR_SWAP_TX_SPEND_MY_CALLED: bool = true; - TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _, _, _, _, _| { + TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _, _, _, _, _, _| { unsafe { SEARCH_FOR_SWAP_TX_SPEND_MY_CALLED = true } MockResult::Return(Ok(Some(FoundSwapTxSpend::Spent(eth_tx_for_test().into())))) }); static mut SEARCH_FOR_SWAP_TX_SPEND_OTHER_CALLED: bool = true; - TestCoin::search_for_swap_tx_spend_other.mock_safe(|_, _, _, _, _, _| { + TestCoin::search_for_swap_tx_spend_other.mock_safe(|_, _, _, _, _, _, _| { unsafe { SEARCH_FOR_SWAP_TX_SPEND_OTHER_CALLED = true } MockResult::Return(Ok(Some(FoundSwapTxSpend::Spent(eth_tx_for_test().into())))) }); @@ -1690,6 +1733,7 @@ mod maker_swap_tests { let ctx = MmCtxBuilder::default().with_secp256k1_key_pair(key_pair).into_mm_arc(); TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); + TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); let maker_coin = MmCoinEnum::Test(TestCoin {}); let taker_coin = MmCoinEnum::Test(TestCoin {}); let (maker_swap, _) = unwrap!(MakerSwap::load_from_saved( @@ -1714,21 +1758,22 @@ mod maker_swap_tests { let ctx = MmCtxBuilder::default().with_secp256k1_key_pair(key_pair).into_mm_arc(); TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); + TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); static mut SEARCH_FOR_SWAP_TX_SPEND_MY_CALLED: bool = false; - TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _, _, _, _, _| { + TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _, _, _, _, _, _| { unsafe { SEARCH_FOR_SWAP_TX_SPEND_MY_CALLED = true } MockResult::Return(Ok(Some(FoundSwapTxSpend::Spent(eth_tx_for_test().into())))) }); static mut SEARCH_FOR_SWAP_TX_SPEND_OTHER_CALLED: bool = false; - TestCoin::search_for_swap_tx_spend_other.mock_safe(|_, _, _, _, _, _| { + TestCoin::search_for_swap_tx_spend_other.mock_safe(|_, _, _, _, _, _, _| { unsafe { SEARCH_FOR_SWAP_TX_SPEND_OTHER_CALLED = true } MockResult::Return(Ok(None)) }); static mut SEND_MAKER_SPENDS_TAKER_PAYMENT_CALLED: bool = false; - TestCoin::send_maker_spends_taker_payment.mock_safe(|_, _, _, _, _| { + TestCoin::send_maker_spends_taker_payment.mock_safe(|_, _, _, _, _, _| { unsafe { SEND_MAKER_SPENDS_TAKER_PAYMENT_CALLED = true } MockResult::Return(Box::new(futures01::future::ok(eth_tx_for_test().into()))) }); @@ -1764,6 +1809,7 @@ mod maker_swap_tests { let ctx = MmCtxBuilder::default().with_secp256k1_key_pair(key_pair).into_mm_arc(); TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); + TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); let maker_coin = MmCoinEnum::Test(TestCoin {}); let taker_coin = MmCoinEnum::Test(TestCoin {}); let (_maker_swap, _) = unwrap!(MakerSwap::load_from_saved( @@ -1779,6 +1825,77 @@ mod maker_swap_tests { assert_eq!(get_locked_amount(&ctx, "ticker", &trade_fee), BigDecimal::from(0)); } + #[test] + fn test_recheck_swap_contract_address_if_none() { + // swap file contains neither maker_coin_swap_contract_address nor taker_coin_swap_contract_address + let maker_saved_json = r#"{"error_events":["StartFailed","NegotiateFailed","TakerFeeValidateFailed","MakerPaymentTransactionFailed","MakerPaymentDataSendFailed","TakerPaymentValidateFailed","TakerPaymentSpendFailed","TakerPaymentSpendConfirmFailed","MakerPaymentRefunded","MakerPaymentRefundFailed"],"events":[{"event":{"data":{"lock_duration":7800,"maker_amount":"3.54932734","maker_coin":"KMD","maker_coin_start_block":1452970,"maker_payment_confirmations":1,"maker_payment_lock":1563759539,"my_persistent_pub":"031bb83b58ec130e28e0a6d5d2acf2eb01b0d3f1670e021d47d31db8a858219da8","secret":"0000000000000000000000000000000000000000000000000000000000000000","started_at":1563743939,"taker":"101ace6b08605b9424b0582b5cce044b70a3c8d8d10cb2965e039b0967ae92b9","taker_amount":"0.02004833998671660000000000","taker_coin":"ETH","taker_coin_start_block":8196380,"taker_payment_confirmations":1,"uuid":"3447b727-fe93-4357-8e5a-8cf2699b7e86"},"type":"Started"},"timestamp":1563743939211},{"event":{"data":{"taker_payment_locktime":1563751737,"taker_pubkey":"03101ace6b08605b9424b0582b5cce044b70a3c8d8d10cb2965e039b0967ae92b9"},"type":"Negotiated"},"timestamp":1563743979835},{"event":{"data":{"tx_hash":"a59203eb2328827de00bed699a29389792906e4f39fdea145eb40dc6b3821bd6","tx_hex":"f8690284ee6b280082520894d8997941dd1346e9231118d5685d866294f59e5b865af3107a4000801ca0743d2b7c9fad65805d882179062012261be328d7628ae12ee08eff8d7657d993a07eecbd051f49d35279416778faa4664962726d516ce65e18755c9b9406a9c2fd"},"type":"TakerFeeValidated"},"timestamp":1563744052878}],"success_events":["Started","Negotiated","TakerFeeValidated","MakerPaymentSent","TakerPaymentReceived","TakerPaymentWaitConfirmStarted","TakerPaymentValidatedAndConfirmed","TakerPaymentSpent","TakerPaymentSpendConfirmStarted","TakerPaymentSpendConfirmed","Finished"],"uuid":"3447b727-fe93-4357-8e5a-8cf2699b7e86"}"#; + let maker_saved_swap: MakerSavedSwap = unwrap!(json::from_str(maker_saved_json)); + let key_pair = unwrap!(key_pair_from_seed( + "spice describe gravity federal blast come thank unfair canal monkey style afraid" + )); + let ctx = MmCtxBuilder::default().with_secp256k1_key_pair(key_pair).into_mm_arc(); + + TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); + static mut SWAP_CONTRACT_ADDRESS_CALLED: usize = 0; + TestCoin::swap_contract_address.mock_safe(|_| { + unsafe { SWAP_CONTRACT_ADDRESS_CALLED += 1 }; + MockResult::Return(Some(BytesJson::default())) + }); + let maker_coin = MmCoinEnum::Test(TestCoin {}); + let taker_coin = MmCoinEnum::Test(TestCoin {}); + let (maker_swap, _) = unwrap!(MakerSwap::load_from_saved( + ctx.clone(), + maker_coin, + taker_coin, + maker_saved_swap + )); + + assert_eq!(unsafe { SWAP_CONTRACT_ADDRESS_CALLED }, 2); + assert_eq!( + maker_swap.r().data.maker_coin_swap_contract_address, + Some(BytesJson::default()) + ); + assert_eq!( + maker_swap.r().data.taker_coin_swap_contract_address, + Some(BytesJson::default()) + ); + } + + #[test] + fn test_recheck_only_one_swap_contract_address() { + // swap file contains only maker_coin_swap_contract_address + let maker_saved_json = r#"{"type":"Maker","uuid":"c52659d7-4e13-41f5-9c1a-30cc2f646033","events":[{"timestamp":1608541830095,"event":{"type":"Started","data":{"taker_coin":"JST","maker_coin":"ETH","taker":"031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3","secret":"dc45c1d22028970d8d30d1ddacbfc50eb92403b0d6076c94f2216c4c44512b41","secret_hash":"943e11f7c74e2d6493ef8ad01a06ef2ce9bd1fb3","my_persistent_pub":"03c6a78589e18b482aea046975e6d0acbdea7bf7dbf04d9d5bd67fda917815e3ed","lock_duration":7800,"maker_amount":"0.1","taker_amount":"0.1","maker_payment_confirmations":1,"maker_payment_requires_nota":false,"taker_payment_confirmations":1,"taker_payment_requires_nota":false,"maker_payment_lock":1608557429,"uuid":"c52659d7-4e13-41f5-9c1a-30cc2f646033","started_at":1608541829,"maker_coin_start_block":14353,"taker_coin_start_block":14353,"maker_coin_swap_contract_address":"a09ad3cd7e96586ebd05a2607ee56b56fb2db8fd"}}},{"timestamp":1608541830399,"event":{"type":"Negotiated","data":{"taker_payment_locktime":1608549629,"taker_pubkey":"02031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3"}}},{"timestamp":1608541831810,"event":{"type":"TakerFeeValidated","data":{"tx_hex":"f8a7821fb58083033450942b294f029fde858b2c62184e8390591755521d8e80b844a9059cbb000000000000000000000000d8997941dd1346e9231118d5685d866294f59e5b0000000000000000000000000000000000000000000000000000750d557426e01ba06ddad2dfe6933b8d70d5739beb3005c8f367bc72eac4e5609b81c2f8e5843cd9a07fa695cc42f8c6b6a7b10f6ae9e4dca3e750e37f64a85b54dec736236790f05e","tx_hash":"b13c3428f70b46d8c1d7f5863af020a27c380a8ede0927554beabf234998bcc8"}}},{"timestamp":1608541832884,"event":{"type":"MakerPaymentSent","data":{"tx_hex":"f8ef82021980830249f094a09ad3cd7e96586ebd05a2607ee56b56fb2db8fd88016345785d8a0000b884152cf3af7c7ce37fac65bd995eae3d58ccdc367d79f3a10e6ca55f609e6dcefac960982b000000000000000000000000bab36286672fbdc7b250804bf6d14be0df69fa29943e11f7c74e2d6493ef8ad01a06ef2ce9bd1fb3000000000000000000000000000000000000000000000000000000000000000000000000000000005fe0a3751ca03ab6306b8b8875c7d2cbaa71a3991eb8e7ae44e192dc9974cecc1f9dcfe5e4d6a04ec2808db06fe7b246134997fcce81ca201ced1257f1f8e93cacadd6554ca653","tx_hash":"ceba36dff0b2c7aec69cb2d5be7055858e09889959ba63f7957b45a15dceade4"}}},{"timestamp":1608541835207,"event":{"type":"TakerPaymentReceived","data":{"tx_hex":"f90127821fb680830249f094a09ad3cd7e96586ebd05a2607ee56b56fb2db8fd80b8c49b415b2a64bdf61f195a1767f547bb0886ed697f3c1a063ce928ff9a47222c0b5d099200000000000000000000000000000000000000000000000000016345785d8a00000000000000000000000000002b294f029fde858b2c62184e8390591755521d8e0000000000000000000000004b2d0d6c2c785217457b69b922a2a9cea98f71e9943e11f7c74e2d6493ef8ad01a06ef2ce9bd1fb3000000000000000000000000000000000000000000000000000000000000000000000000000000005fe084fd1ba0a5b6ef54217c5a03a588d01410ef1187ce6107bdb075306ced06a06e25a50984a03f541f1f392079ae2590d0f48f2065f8721a8b46c44a060ae53f00bfb5160118","tx_hash":"1247a1be3da89f3612ca33d83d493808388775e2897036f640c0efe69c3b162f"}}},{"timestamp":1608541835208,"event":{"type":"TakerPaymentWaitConfirmStarted"}},{"timestamp":1608541836196,"event":{"type":"TakerPaymentValidatedAndConfirmed"}},{"timestamp":1608541837173,"event":{"type":"TakerPaymentSpent","data":{"tx_hex":"f9010782021a80830249f094a09ad3cd7e96586ebd05a2607ee56b56fb2db8fd80b8a402ed292b64bdf61f195a1767f547bb0886ed697f3c1a063ce928ff9a47222c0b5d099200000000000000000000000000000000000000000000000000016345785d8a0000dc45c1d22028970d8d30d1ddacbfc50eb92403b0d6076c94f2216c4c44512b410000000000000000000000002b294f029fde858b2c62184e8390591755521d8e000000000000000000000000bab36286672fbdc7b250804bf6d14be0df69fa291ba053af89feb4ab066b26e76de9788c85ec1bf14ae6dcbdd7ff53e561e48e1b822ca043796d45bd4233500a120a1571b3fee95a34e8cc6b616c69552da4352c0d8e39","tx_hash":"d9a839c6eead3fbf538eca0a4ec39e28647104920a5c8b9c107524287dd90165"}}},{"timestamp":1608541837175,"event":{"type":"TakerPaymentSpendConfirmStarted"}},{"timestamp":1608541837612,"event":{"type":"TakerPaymentSpendConfirmed"}},{"timestamp":1608541837614,"event":{"type":"Finished"}}],"maker_amount":"0.1","maker_coin":"ETH","taker_amount":"0.1","taker_coin":"JST","gui":"nogui","mm_version":"1a6082121","success_events":["Started","Negotiated","TakerFeeValidated","MakerPaymentSent","TakerPaymentReceived","TakerPaymentWaitConfirmStarted","TakerPaymentValidatedAndConfirmed","TakerPaymentSpent","TakerPaymentSpendConfirmStarted","TakerPaymentSpendConfirmed","Finished"],"error_events":["StartFailed","NegotiateFailed","TakerFeeValidateFailed","MakerPaymentTransactionFailed","MakerPaymentDataSendFailed","MakerPaymentWaitConfirmFailed","TakerPaymentValidateFailed","TakerPaymentWaitConfirmFailed","TakerPaymentSpendFailed","TakerPaymentSpendConfirmFailed","MakerPaymentWaitRefundStarted","MakerPaymentRefunded","MakerPaymentRefundFailed"]}"#; + let maker_saved_swap: MakerSavedSwap = unwrap!(json::from_str(maker_saved_json)); + let key_pair = unwrap!(key_pair_from_seed( + "spice describe gravity federal blast come thank unfair canal monkey style afraid" + )); + let ctx = MmCtxBuilder::default().with_secp256k1_key_pair(key_pair).into_mm_arc(); + + TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); + static mut SWAP_CONTRACT_ADDRESS_CALLED: usize = 0; + TestCoin::swap_contract_address.mock_safe(|_| { + unsafe { SWAP_CONTRACT_ADDRESS_CALLED += 1 }; + MockResult::Return(Some(BytesJson::default())) + }); + let maker_coin = MmCoinEnum::Test(TestCoin {}); + let taker_coin = MmCoinEnum::Test(TestCoin {}); + let (maker_swap, _) = unwrap!(MakerSwap::load_from_saved( + ctx.clone(), + maker_coin, + taker_coin, + maker_saved_swap + )); + + assert_eq!(unsafe { SWAP_CONTRACT_ADDRESS_CALLED }, 1); + let expected_addr = addr_from_str("0xa09ad3cd7e96586ebd05a2607ee56b56fb2db8fd").unwrap(); + let expected = BytesJson::from(expected_addr.0.as_ref()); + assert_eq!(maker_swap.r().data.maker_coin_swap_contract_address, Some(expected)); + assert_eq!( + maker_swap.r().data.taker_coin_swap_contract_address, + Some(BytesJson::default()) + ); + } + #[test] fn test_maker_swap_event_should_ban() { let event = MakerSwapEvent::TakerPaymentWaitConfirmFailed("err".into()); diff --git a/mm2src/lp_swap/taker_swap.rs b/mm2src/lp_swap/taker_swap.rs index c04788adba..604f1cb777 100644 --- a/mm2src/lp_swap/taker_swap.rs +++ b/mm2src/lp_swap/taker_swap.rs @@ -1,12 +1,11 @@ #![cfg_attr(not(feature = "native"), allow(dead_code))] -use super::{ban_pubkey, broadcast_my_swap_status, dex_fee_amount, get_locked_amount, get_locked_amount_by_other_swaps, - my_swap_file_path, my_swaps_dir, AtomicSwap, LockedAmount, MySwapInfo, RecoveredSwap, RecoveredSwapAction, - SavedSwap, SwapConfirmationsSettings, SwapError, SwapsContext, TransactionIdentifier, +use super::{ban_pubkey, broadcast_my_swap_status, broadcast_swap_message_every, dex_fee_amount, dex_fee_rate, + get_locked_amount, get_locked_amount_by_other_swaps, my_swap_file_path, my_swaps_dir, recv_swap_msg, + swap_topic, AtomicSwap, LockedAmount, MySwapInfo, NegotiationDataMsg, RecoveredSwap, RecoveredSwapAction, + SavedSwap, SwapConfirmationsSettings, SwapError, SwapMsg, SwapsContext, TransactionIdentifier, WAIT_CONFIRM_INTERVAL}; use crate::mm2::lp_network::subscribe_to_topic; -use crate::mm2::lp_swap::{broadcast_swap_message_every, dex_fee_rate, recv_swap_msg, swap_topic, NegotiationDataMsg, - SwapMsg}; use atomic::Atomic; use bigdecimal::BigDecimal; use coins::{lp_coinfindᔃ, FoundSwapTxSpend, MmCoinEnum, TradeFee}; @@ -17,7 +16,7 @@ use futures01::Future; use http::Response; use parking_lot::Mutex as PaMutex; use primitives::hash::H264; -use rpc::v1::types::{H160 as H160Json, H256 as H256Json, H264 as H264Json}; +use rpc::v1::types::{Bytes as BytesJson, H160 as H160Json, H256 as H256Json, H264 as H264Json}; use serde_json::{self as json, Value as Json}; use std::path::PathBuf; use std::sync::{atomic::Ordering, Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; @@ -368,6 +367,10 @@ pub struct TakerSwapData { maker_payment_wait: u64, maker_coin_start_block: u64, taker_coin_start_block: u64, + #[serde(skip_serializing_if = "Option::is_none")] + maker_coin_swap_contract_address: Option, + #[serde(skip_serializing_if = "Option::is_none")] + taker_coin_swap_contract_address: Option, } pub struct TakerSwapMut { @@ -646,6 +649,9 @@ impl TakerSwap { }, }; + let maker_coin_swap_contract_address = self.maker_coin.swap_contract_address(); + let taker_coin_swap_contract_address = self.taker_coin.swap_contract_address(); + let data = TakerSwapData { taker_coin: self.taker_coin.ticker().to_owned(), maker_coin: self.maker_coin.ticker().to_owned(), @@ -664,6 +670,8 @@ impl TakerSwap { maker_payment_wait: started_at + (self.payment_locktime * 2) / 5, maker_coin_start_block, taker_coin_start_block, + maker_coin_swap_contract_address, + taker_coin_swap_contract_address, }; Ok((Some(TakerSwapCommand::Negotiate), vec![TakerSwapEvent::Started(data)])) @@ -868,6 +876,7 @@ impl TakerSwap { &*self.r().other_persistent_pub, &self.r().secret_hash.0, self.maker_amount.to_decimal(), + &self.r().data.maker_coin_swap_contract_address, ); let validated = validated_f.compat().await; @@ -896,6 +905,7 @@ impl TakerSwap { &*self.r().other_persistent_pub, &self.r().secret_hash.0, self.r().data.taker_coin_start_block, + &self.r().data.taker_coin_swap_contract_address, ); let transaction = match f.compat().await { Ok(res) => match res { @@ -906,6 +916,7 @@ impl TakerSwap { &*self.r().other_persistent_pub, &self.r().secret_hash.0, self.taker_amount.to_decimal(), + &self.r().data.taker_coin_swap_contract_address, ); match payment_fut.compat().await { @@ -969,6 +980,7 @@ impl TakerSwap { &self.r().taker_payment.clone().unwrap().tx_hex, self.r().data.taker_payment_lock, self.r().data.taker_coin_start_block, + &self.r().data.taker_coin_swap_contract_address, ); let tx = match f.compat().await { Ok(t) => t, @@ -1014,6 +1026,7 @@ impl TakerSwap { self.maker_payment_lock.load(Ordering::Relaxed) as u32, &*self.r().other_persistent_pub, &self.r().secret.0, + &self.r().data.maker_coin_swap_contract_address, ); let transaction = match spend_fut.compat().await { Ok(t) => t, @@ -1050,6 +1063,7 @@ impl TakerSwap { self.r().data.taker_payment_lock as u32, &*self.r().other_persistent_pub, &self.r().secret_hash.0, + &self.r().data.taker_coin_swap_contract_address, ); let transaction = match refund_fut.compat().await { @@ -1092,48 +1106,56 @@ impl TakerSwap { ctx: MmArc, maker_coin: MmCoinEnum, taker_coin: MmCoinEnum, - saved: TakerSavedSwap, + mut saved: TakerSavedSwap, ) -> Result<(Self, Option), String> { if saved.events.is_empty() { return ERR!("Can't restore swap from empty events set"); }; - match &saved.events[0].event { - TakerSwapEvent::Started(data) => { - let mut maker = bits256::from([0; 32]); - maker.bytes = data.maker.0; - let my_persistent_pub = H264::from(&**ctx.secp256k1_key_pair().public()); - let conf_settings = SwapConfirmationsSettings { - maker_coin_confs: data.maker_payment_confirmations, - maker_coin_nota: data - .maker_payment_requires_nota - .unwrap_or_else(|| maker_coin.requires_notarization()), - taker_coin_confs: data.taker_payment_confirmations, - taker_coin_nota: data - .taker_payment_requires_nota - .unwrap_or_else(|| taker_coin.requires_notarization()), - }; + let data = match saved.events[0].event { + TakerSwapEvent::Started(ref mut data) => data, + _ => return ERR!("First swap event must be Started"), + }; - let swap = TakerSwap::new( - ctx, - maker, - data.maker_amount.clone().into(), - data.taker_amount.clone().into(), - my_persistent_pub, - saved.uuid, - conf_settings, - maker_coin, - taker_coin, - data.lock_duration, - ); - let command = saved.events.last().unwrap().get_command(); - for saved_event in saved.events { - try_s!(swap.apply_event(saved_event.event)); - } - Ok((swap, command)) - }, - _ => ERR!("First swap event must be Started"), + // refresh swap contract addresses if the swap file is out-dated (doesn't contain the fields yet) + if data.maker_coin_swap_contract_address.is_none() { + data.maker_coin_swap_contract_address = maker_coin.swap_contract_address(); } + if data.taker_coin_swap_contract_address.is_none() { + data.taker_coin_swap_contract_address = taker_coin.swap_contract_address(); + } + + let mut maker = bits256::from([0; 32]); + maker.bytes = data.maker.0; + let my_persistent_pub = H264::from(&**ctx.secp256k1_key_pair().public()); + let conf_settings = SwapConfirmationsSettings { + maker_coin_confs: data.maker_payment_confirmations, + maker_coin_nota: data + .maker_payment_requires_nota + .unwrap_or_else(|| maker_coin.requires_notarization()), + taker_coin_confs: data.taker_payment_confirmations, + taker_coin_nota: data + .taker_payment_requires_nota + .unwrap_or_else(|| taker_coin.requires_notarization()), + }; + + let swap = TakerSwap::new( + ctx, + maker, + data.maker_amount.clone().into(), + data.taker_amount.clone().into(), + my_persistent_pub, + saved.uuid, + conf_settings, + maker_coin, + taker_coin, + data.lock_duration, + ); + let command = saved.events.last().unwrap().get_command(); + for saved_event in saved.events { + try_s!(swap.apply_event(saved_event.event)); + } + Ok((swap, command)) } pub fn recover_funds(&self) -> Result { @@ -1163,6 +1185,7 @@ impl TakerSwap { &self.r().secret_hash.0, &maker_payment, self.r().data.maker_coin_start_block, + &self.r().data.maker_coin_swap_contract_address, ) { Ok(Some(FoundSwapTxSpend::Spent(tx))) => { return ERR!( @@ -1194,6 +1217,7 @@ impl TakerSwap { &*self.r().other_persistent_pub, &self.r().secret_hash.0, self.r().data.taker_coin_start_block, + &self.r().data.taker_coin_swap_contract_address, ) .wait()); match maybe_sent { @@ -1212,6 +1236,7 @@ impl TakerSwap { self.maker_payment_lock.load(Ordering::Relaxed) as u32, &*self.r().other_persistent_pub, &self.r().secret.0, + &self.r().data.maker_coin_swap_contract_address, ) .wait()); @@ -1228,6 +1253,7 @@ impl TakerSwap { &self.r().secret_hash.0, &taker_payment, self.r().data.taker_coin_start_block, + &self.r().data.taker_coin_swap_contract_address, )); match taker_payment_spend { @@ -1242,6 +1268,7 @@ impl TakerSwap { self.maker_payment_lock.load(Ordering::Relaxed) as u32, &*self.r().other_persistent_pub, &secret, + &self.r().data.maker_coin_swap_contract_address, ) .wait()); @@ -1271,6 +1298,7 @@ impl TakerSwap { self.r().data.taker_payment_lock as u32, &*self.r().other_persistent_pub, &self.r().secret_hash.0, + &self.r().data.taker_coin_swap_contract_address, ) .wait()); @@ -1413,9 +1441,9 @@ pub async fn max_taker_vol(ctx: MmArc, req: Json) -> Result>, S #[cfg(test)] mod taker_swap_tests { use super::*; - use coins::eth::{signed_eth_tx_from_bytes, SignedEthTx}; + use coins::eth::{addr_from_str, signed_eth_tx_from_bytes, SignedEthTx}; use coins::utxo::UtxoTx; - use coins::{FoundSwapTxSpend, MarketCoinOps, SwapOps, TestCoin}; + use coins::{FoundSwapTxSpend, MarketCoinOps, MmCoin, SwapOps, TestCoin}; use common::mm_ctx::MmCtxBuilder; use common::privkey::key_pair_from_seed; use mocktopus::mocking::*; @@ -1447,12 +1475,14 @@ mod taker_swap_tests { let ctx = MmCtxBuilder::default().with_secp256k1_key_pair(key_pair).into_mm_arc(); TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); + TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); + static mut MAKER_PAYMENT_SPEND_CALLED: bool = false; - TestCoin::send_taker_spends_maker_payment.mock_safe(|_, _, _, _, _| { + TestCoin::send_taker_spends_maker_payment.mock_safe(|_, _, _, _, _, _| { unsafe { MAKER_PAYMENT_SPEND_CALLED = true }; MockResult::Return(Box::new(futures01::future::ok(eth_tx_for_test().into()))) }); - TestCoin::search_for_swap_tx_spend_other.mock_safe(|_, _, _, _, _, _| MockResult::Return(Ok(None))); + TestCoin::search_for_swap_tx_spend_other.mock_safe(|_, _, _, _, _, _, _| MockResult::Return(Ok(None))); let maker_coin = MmCoinEnum::Test(TestCoin {}); let taker_coin = MmCoinEnum::Test(TestCoin {}); let (taker_swap, _) = unwrap!(TakerSwap::load_from_saved( @@ -1481,21 +1511,22 @@ mod taker_swap_tests { let ctx = MmCtxBuilder::default().with_secp256k1_key_pair(key_pair).into_mm_arc(); TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); + TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); static mut MY_PAYMENT_SENT_CALLED: bool = false; - TestCoin::check_if_my_payment_sent.mock_safe(|_, _, _, _, _| { + TestCoin::check_if_my_payment_sent.mock_safe(|_, _, _, _, _, _| { unsafe { MY_PAYMENT_SENT_CALLED = true }; MockResult::Return(Box::new(futures01::future::ok(Some(eth_tx_for_test().into())))) }); static mut TX_SPEND_CALLED: bool = false; - TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _, _, _, _, _| { + TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _, _, _, _, _, _| { unsafe { TX_SPEND_CALLED = true }; MockResult::Return(Ok(None)) }); static mut TAKER_PAYMENT_REFUND_CALLED: bool = false; - TestCoin::send_taker_refunds_payment.mock_safe(|_, _, _, _, _| { + TestCoin::send_taker_refunds_payment.mock_safe(|_, _, _, _, _, _| { unsafe { TAKER_PAYMENT_REFUND_CALLED = true }; MockResult::Return(Box::new(futures01::future::ok(eth_tx_for_test().into()))) }); @@ -1529,25 +1560,26 @@ mod taker_swap_tests { let ctx = MmCtxBuilder::default().with_secp256k1_key_pair(key_pair).into_mm_arc(); TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); + TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); TestCoin::extract_secret.mock_safe(|_, _, _| MockResult::Return(Ok(vec![]))); static mut MY_PAYMENT_SENT_CALLED: bool = false; - TestCoin::check_if_my_payment_sent.mock_safe(|_, _, _, _, _| { + TestCoin::check_if_my_payment_sent.mock_safe(|_, _, _, _, _, _| { unsafe { MY_PAYMENT_SENT_CALLED = true }; MockResult::Return(Box::new(futures01::future::ok(Some(eth_tx_for_test().into())))) }); static mut SEARCH_TX_SPEND_CALLED: bool = false; - TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _, _, _, _, _| { + TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _, _, _, _, _, _| { unsafe { SEARCH_TX_SPEND_CALLED = true }; let tx: UtxoTx = "0100000001de7aa8d29524906b2b54ee2e0281f3607f75662cbc9080df81d1047b78e21dbc00000000d7473044022079b6c50820040b1fbbe9251ced32ab334d33830f6f8d0bf0a40c7f1336b67d5b0220142ccf723ddabb34e542ed65c395abc1fbf5b6c3e730396f15d25c49b668a1a401209da937e5609680cb30bff4a7661364ca1d1851c2506fa80c443f00a3d3bf7365004c6b6304f62b0e5cb175210270e75970bb20029b3879ec76c4acd320a8d0589e003636264d01a7d566504bfbac6782012088a9142fb610d856c19fd57f2d0cffe8dff689074b3d8a882103f368228456c940ac113e53dad5c104cf209f2f102a409207269383b6ab9b03deac68ffffffff01d0dc9800000000001976a9146d9d2b554d768232320587df75c4338ecc8bf37d88ac40280e5c".into(); MockResult::Return(Ok(Some(FoundSwapTxSpend::Spent(tx.into())))) }); - TestCoin::search_for_swap_tx_spend_other.mock_safe(|_, _, _, _, _, _| MockResult::Return(Ok(None))); + TestCoin::search_for_swap_tx_spend_other.mock_safe(|_, _, _, _, _, _, _| MockResult::Return(Ok(None))); static mut MAKER_PAYMENT_SPEND_CALLED: bool = false; - TestCoin::send_taker_spends_maker_payment.mock_safe(|_, _, _, _, _| { + TestCoin::send_taker_spends_maker_payment.mock_safe(|_, _, _, _, _, _| { unsafe { MAKER_PAYMENT_SPEND_CALLED = true }; MockResult::Return(Box::new(futures01::future::ok(eth_tx_for_test().into()))) }); @@ -1581,15 +1613,16 @@ mod taker_swap_tests { let ctx = MmCtxBuilder::default().with_secp256k1_key_pair(key_pair).into_mm_arc(); TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); + TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); static mut SEARCH_TX_SPEND_CALLED: bool = false; - TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _, _, _, _, _| { + TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _, _, _, _, _, _| { unsafe { SEARCH_TX_SPEND_CALLED = true }; MockResult::Return(Ok(None)) }); static mut REFUND_CALLED: bool = false; - TestCoin::send_taker_refunds_payment.mock_safe(|_, _, _, _, _| { + TestCoin::send_taker_refunds_payment.mock_safe(|_, _, _, _, _, _| { unsafe { REFUND_CALLED = true }; MockResult::Return(Box::new(futures01::future::ok(eth_tx_for_test().into()))) }); @@ -1622,9 +1655,10 @@ mod taker_swap_tests { let ctx = MmCtxBuilder::default().with_secp256k1_key_pair(key_pair).into_mm_arc(); TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); + TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); static mut SEARCH_TX_SPEND_CALLED: bool = false; - TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _, _, _, _, _| { + TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _, _, _, _, _, _| { unsafe { SEARCH_TX_SPEND_CALLED = true }; MockResult::Return(Ok(None)) }); @@ -1651,19 +1685,20 @@ mod taker_swap_tests { let ctx = MmCtxBuilder::default().with_secp256k1_key_pair(key_pair).into_mm_arc(); TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); + TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); TestCoin::extract_secret.mock_safe(|_, _, _| MockResult::Return(Ok(vec![]))); static mut SEARCH_TX_SPEND_CALLED: bool = false; - TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _, _, _, _, _| { + TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _, _, _, _, _, _| { unsafe { SEARCH_TX_SPEND_CALLED = true }; let tx: UtxoTx = "0100000001de7aa8d29524906b2b54ee2e0281f3607f75662cbc9080df81d1047b78e21dbc00000000d7473044022079b6c50820040b1fbbe9251ced32ab334d33830f6f8d0bf0a40c7f1336b67d5b0220142ccf723ddabb34e542ed65c395abc1fbf5b6c3e730396f15d25c49b668a1a401209da937e5609680cb30bff4a7661364ca1d1851c2506fa80c443f00a3d3bf7365004c6b6304f62b0e5cb175210270e75970bb20029b3879ec76c4acd320a8d0589e003636264d01a7d566504bfbac6782012088a9142fb610d856c19fd57f2d0cffe8dff689074b3d8a882103f368228456c940ac113e53dad5c104cf209f2f102a409207269383b6ab9b03deac68ffffffff01d0dc9800000000001976a9146d9d2b554d768232320587df75c4338ecc8bf37d88ac40280e5c".into(); MockResult::Return(Ok(Some(FoundSwapTxSpend::Spent(tx.into())))) }); - TestCoin::search_for_swap_tx_spend_other.mock_safe(|_, _, _, _, _, _| MockResult::Return(Ok(None))); + TestCoin::search_for_swap_tx_spend_other.mock_safe(|_, _, _, _, _, _, _| MockResult::Return(Ok(None))); static mut MAKER_PAYMENT_SPEND_CALLED: bool = false; - TestCoin::send_taker_spends_maker_payment.mock_safe(|_, _, _, _, _| { + TestCoin::send_taker_spends_maker_payment.mock_safe(|_, _, _, _, _, _| { unsafe { MAKER_PAYMENT_SPEND_CALLED = true }; MockResult::Return(Box::new(futures01::future::ok(eth_tx_for_test().into()))) }); @@ -1697,6 +1732,7 @@ mod taker_swap_tests { let ctx = MmCtxBuilder::default().with_secp256k1_key_pair(key_pair).into_mm_arc(); TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); + TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); let maker_coin = MmCoinEnum::Test(TestCoin {}); let taker_coin = MmCoinEnum::Test(TestCoin {}); let (taker_swap, _) = unwrap!(TakerSwap::load_from_saved( @@ -1726,6 +1762,77 @@ mod taker_swap_tests { assert!(event.should_ban_maker()); } + #[test] + fn test_recheck_swap_contract_address_if_none() { + // swap file contains neither maker_coin_swap_contract_address nor taker_coin_swap_contract_address + let taker_saved_json = r#"{"error_events":["StartFailed","NegotiateFailed","TakerFeeSendFailed","MakerPaymentValidateFailed","TakerPaymentTransactionFailed","TakerPaymentDataSendFailed","TakerPaymentWaitForSpendFailed","MakerPaymentSpendFailed","TakerPaymentRefunded","TakerPaymentRefundFailed"],"events":[{"event":{"data":{"lock_duration":7800,"maker":"1bb83b58ec130e28e0a6d5d2acf2eb01b0d3f1670e021d47d31db8a858219da8","maker_amount":"0.58610590","maker_coin":"KMD","maker_coin_start_block":1450923,"maker_payment_confirmations":1,"maker_payment_wait":1563623475,"my_persistent_pub":"02713015d3fa4d30259e90be5f131beb593bf0131f3af2dcdb304e3322d8d52b91","started_at":1563620875,"taker_amount":"0.0077700000552410000000000","taker_coin":"LTC","taker_coin_start_block":1670837,"taker_payment_confirmations":1,"taker_payment_lock":1563628675,"uuid":"9db641f5-4300-4527-9fa6-f1c391d42c35"},"type":"Started"},"timestamp":1563620875766},{"event":{"data":{"maker_payment_locktime":1563636475,"maker_pubkey":"031bb83b58ec130e28e0a6d5d2acf2eb01b0d3f1670e021d47d31db8a858219da8","secret_hash":"7ed38daab6085c1a1e4426e61dc87a3c2c081a95"},"type":"Negotiated"},"timestamp":1563620955014},{"event":{"data":{"tx_hash":"6740136eaaa615d9d231969e3a9599d0fc59e53989237a8d31cd6fc86c160013","tx_hex":"0100000001a2586ea8294cedc55741bef625ba72c646399903391a7f6c604a58c6263135f2000000006b4830450221009c78c8ba4a7accab6b09f9a95da5bc59c81f4fc1e60b288ec3c5462b4d02ef01022056b63be1629cf17751d3cc5ffec51bcb1d7f9396e9ce9ca254d0f34104f7263a012102713015d3fa4d30259e90be5f131beb593bf0131f3af2dcdb304e3322d8d52b91ffffffff0210270000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ac78aa1900000000001976a91406ccabfd5f9075ecd5e8d0d31c0e973a54d51e8288ac5bf6325d"},"type":"TakerFeeSent"},"timestamp":1563620958220},{"event":{"data":{"tx_hash":"d0f6e664cea9d89fe7b5cf8005fdca070d1ab1d05a482aaef95c08cdaecddf0a","tx_hex":"0400008085202f89019f1cbda354342cdf982046b331bbd3791f53b692efc6e4becc36be495b2977d9000000006b483045022100fa9d4557394141f6a8b9bfb8cd594a521fd8bcd1965dbf8bc4e04abc849ac66e0220589f521814c10a7561abfd5e432f7a2ee60d4875fe4604618af3207dae531ac00121031bb83b58ec130e28e0a6d5d2acf2eb01b0d3f1670e021d47d31db8a858219da8ffffffff029e537e030000000017a9145534898009f1467191065f6890b96914b39a1c018791857702000000001976a914c3f710deb7320b0efa6edb14e3ebeeb9155fa90d88ac72ee325d000000000000000000000000000000"},"type":"MakerPaymentReceived"},"timestamp":1563620999307},{"event":{"type":"MakerPaymentWaitConfirmStarted"},"timestamp":1563620999310},{"event":{"type":"MakerPaymentValidatedAndConfirmed"},"timestamp":1563621244153},{"event":{"data":{"tx_hash":"1e883eb2f3991e84ba27f53651f89b7dda708678a5b9813d043577f222b9ca30","tx_hex":"01000000011300166cc86fcd318d7a238939e559fcd099953a9e9631d2d915a6aa6e134067010000006a47304402206781d5f2db2ff13d2ec7e266f774ea5630cc2dba4019e18e9716131b8b026051022006ebb33857b6d180f13aa6be2fc532f9734abde9d00ae14757e7d7ba3741c08c012102713015d3fa4d30259e90be5f131beb593bf0131f3af2dcdb304e3322d8d52b91ffffffff0228db0b000000000017a91483818667161bf94adda3964a81a231cbf6f5338187b0480c00000000001976a91406ccabfd5f9075ecd5e8d0d31c0e973a54d51e8288ac7cf7325d"},"type":"TakerPaymentSent"},"timestamp":1563621246370},{"event":{"data":{"error":"utxo:1145] rpc_clients:782] Waited too long until 1563628675 for output TransactionOutput { value: 777000, script_pubkey: a91483818667161bf94adda3964a81a231cbf6f5338187 } to be spent "},"type":"TakerPaymentWaitForSpendFailed"},"timestamp":1563638060370},{"event":{"data":{"error":"lp_swap:2025] utxo:938] rpc_clients:719] JsonRpcError { request: JsonRpcRequest { jsonrpc: \"2.0\", id: \"9\", method: \"blockchain.transaction.broadcast\", params: [String(\"010000000130cab922f27735043d81b9a5788670da7d9bf85136f527ba841e99f3b23e881e00000000b6473044022058a0c1da6bcf8c1418899ff8475f3ab6dddbff918528451c1fe71c2f7dad176302204c2e0bcf8f9b5f09e02ccfeb9256e9b34fb355ea655a5704a8a3fa920079b91501514c6b63048314335db1752102713015d3fa4d30259e90be5f131beb593bf0131f3af2dcdb304e3322d8d52b91ac6782012088a9147ed38daab6085c1a1e4426e61dc87a3c2c081a958821031bb83b58ec130e28e0a6d5d2acf2eb01b0d3f1670e021d47d31db8a858219da8ac68feffffff0188540a00000000001976a91406ccabfd5f9075ecd5e8d0d31c0e973a54d51e8288ac1c2b335d\")] }, error: Response(Object({\"code\": Number(1), \"message\": String(\"the transaction was rejected by network rules.\\n\\nMissing inputs\\n[010000000130cab922f27735043d81b9a5788670da7d9bf85136f527ba841e99f3b23e881e00000000b6473044022058a0c1da6bcf8c1418899ff8475f3ab6dddbff918528451c1fe71c2f7dad176302204c2e0bcf8f9b5f09e02ccfeb9256e9b34fb355ea655a5704a8a3fa920079b91501514c6b63048314335db1752102713015d3fa4d30259e90be5f131beb593bf0131f3af2dcdb304e3322d8d52b91ac6782012088a9147ed38daab6085c1a1e4426e61dc87a3c2c081a958821031bb83b58ec130e28e0a6d5d2acf2eb01b0d3f1670e021d47d31db8a858219da8ac68feffffff0188540a00000000001976a91406ccabfd5f9075ecd5e8d0d31c0e973a54d51e8288ac1c2b335d]\")})) }"},"type":"TakerPaymentRefundFailed"},"timestamp":1563638060583},{"event":{"type":"Finished"},"timestamp":1563638060585}],"success_events":["Started","Negotiated","TakerFeeSent","MakerPaymentReceived","MakerPaymentWaitConfirmStarted","MakerPaymentValidatedAndConfirmed","TakerPaymentSent","TakerPaymentSpent","MakerPaymentSpent","Finished"],"uuid":"9db641f5-4300-4527-9fa6-f1c391d42c35"}"#; + let taker_saved_swap: TakerSavedSwap = unwrap!(json::from_str(taker_saved_json)); + let key_pair = unwrap!(key_pair_from_seed( + "spice describe gravity federal blast come thank unfair canal monkey style afraid" + )); + let ctx = MmCtxBuilder::default().with_secp256k1_key_pair(key_pair).into_mm_arc(); + + TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); + static mut SWAP_CONTRACT_ADDRESS_CALLED: usize = 0; + TestCoin::swap_contract_address.mock_safe(|_| { + unsafe { SWAP_CONTRACT_ADDRESS_CALLED += 1 }; + MockResult::Return(Some(BytesJson::default())) + }); + let maker_coin = MmCoinEnum::Test(TestCoin {}); + let taker_coin = MmCoinEnum::Test(TestCoin {}); + let (taker_swap, _) = unwrap!(TakerSwap::load_from_saved( + ctx.clone(), + maker_coin, + taker_coin, + taker_saved_swap + )); + + assert_eq!(unsafe { SWAP_CONTRACT_ADDRESS_CALLED }, 2); + assert_eq!( + taker_swap.r().data.maker_coin_swap_contract_address, + Some(BytesJson::default()) + ); + assert_eq!( + taker_swap.r().data.taker_coin_swap_contract_address, + Some(BytesJson::default()) + ); + } + + #[test] + fn test_recheck_only_one_swap_contract_address() { + // swap file contains only maker_coin_swap_contract_address + let taker_saved_json = r#"{"type":"Taker","uuid":"49c79ea4-e1eb-4fb2-a0ef-265bded0b77f","events":[{"timestamp":1608542326909,"event":{"type":"Started","data":{"taker_coin":"RICK","maker_coin":"ETH","maker":"c6a78589e18b482aea046975e6d0acbdea7bf7dbf04d9d5bd67fda917815e3ed","my_persistent_pub":"02031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3","lock_duration":7800,"maker_amount":"0.1","taker_amount":"0.1","maker_payment_confirmations":1,"maker_payment_requires_nota":false,"taker_payment_confirmations":0,"taker_payment_requires_nota":false,"taker_payment_lock":1608550126,"uuid":"49c79ea4-e1eb-4fb2-a0ef-265bded0b77f","started_at":1608542326,"maker_payment_wait":1608545446,"maker_coin_start_block":14360,"taker_coin_start_block":723123,"maker_coin_swap_contract_address":"a09ad3cd7e96586ebd05a2607ee56b56fb2db8fd"}}},{"timestamp":1608542327416,"event":{"type":"Negotiated","data":{"maker_payment_locktime":1608557926,"maker_pubkey":"03c6a78589e18b482aea046975e6d0acbdea7bf7dbf04d9d5bd67fda917815e3ed","secret_hash":"8b0221f3b977c1c65dddf17c1c28e2bbced9e7b4"}}},{"timestamp":1608542332604,"event":{"type":"TakerFeeSent","data":{"tx_hex":"0400008085202f89011ca964f77200b73d64b481f47de84098041d3470d6256e44f2741f080e2b11cf020000006b4830450221008a064f5e51ef8281d43eb7bcd016fed7e560ea1eb7b0713ec977602c96d8f79b02205bfaa6655b849b9922c03276b938273f2edb8fb9ffcaa2a9212d7220560f6060012102031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3ffffffff0246320000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ac62752e27000000001976a91405aab5342166f8594baf17a7d9bef5d56744332788ac7768e05f000000000000000000000000000000","tx_hash":"3793df28ed2aac6188d2c48ec65eff12eea301089d60da655fc96f598326d708"}}},{"timestamp":1608542334018,"event":{"type":"MakerPaymentReceived","data":{"tx_hex":"f8ef82021c80830249f094a09ad3cd7e96586ebd05a2607ee56b56fb2db8fd88016345785d8a0000b884152cf3af50aebafeaf827c62c2eed09e265fa5aa9e013c0f27f0a88259f1aaa1279f0c32000000000000000000000000bab36286672fbdc7b250804bf6d14be0df69fa298b0221f3b977c1c65dddf17c1c28e2bbced9e7b4000000000000000000000000000000000000000000000000000000000000000000000000000000005fe0a5661ba0f18a0c5c349462b51dacd1a0761e4997d4572a01e48480c4e310d69a40308ad3a04510513f01a79c59f22c9cb79952547c8dfc4c74785b630f512d64369323e0c1","tx_hash":"6782323490584a2bc768cd5199506bfa1ed91e7515b35bb72fa269604b7dc0aa"}}},{"timestamp":1608542334019,"event":{"type":"MakerPaymentWaitConfirmStarted"}},{"timestamp":1608542334825,"event":{"type":"MakerPaymentValidatedAndConfirmed"}},{"timestamp":1608542337671,"event":{"type":"TakerPaymentSent","data":{"tx_hex":"0400008085202f890108d72683596fc95f65da609d0801a3ee12ff5ec68ec4d28861ac2aed28df9337010000006b48304502210086a03db599438b243bee2b02af56e23447f85d09854416b51305536b9ca5890e02204b288acdea4cdc7ab1ffbd9766a7bdf95f5bd02d2917dfb7089dbf29032591b0012102031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3ffffffff03809698000000000017a914888e9e1816214c3960eac7b55e35521ca4426b0c870000000000000000166a148b0221f3b977c1c65dddf17c1c28e2bbced9e7b4fada9526000000001976a91405aab5342166f8594baf17a7d9bef5d56744332788ac7f68e05f000000000000000000000000000000","tx_hash":"44fa493757df5fdca823bbac05a8b8feb5862d799d4947fd544abcd129feceea"}}},{"timestamp":1608542348271,"event":{"type":"TakerPaymentSpent","data":{"transaction":{"tx_hex":"0400008085202f8901eacefe29d1bc4a54fd47499d792d86b5feb8a805acbb23a8dc5fdf573749fa4400000000d74730440220508c853cc4f1fcb9e6aa00e704eef99adaee9a4ea63a1fd6393bb7ff18da02c802200396bb5d52157bd77ff26ac521ed75aca388d3ec1e5e3ebb7b3aed73c3d33ec50120df871242dcbcc4fe9ed4d3413e21b2f8ce606a3ee7128c9b2d2e31fcedc1848e004c6b6304ee86e05fb1752102031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3ac6782012088a9148b0221f3b977c1c65dddf17c1c28e2bbced9e7b4882103c6a78589e18b482aea046975e6d0acbdea7bf7dbf04d9d5bd67fda917815e3edac68ffffffff0198929800000000001976a9146d9d2b554d768232320587df75c4338ecc8bf37d88ac725ae05f000000000000000000000000000000","tx_hash":"9376dde62249802a0aba8259f51def9bb2e509af85a5ec7df04b479a9da28a29"},"secret":"df871242dcbcc4fe9ed4d3413e21b2f8ce606a3ee7128c9b2d2e31fcedc1848e"}}},{"timestamp":1608542349372,"event":{"type":"MakerPaymentSpent","data":{"tx_hex":"f90107821fb980830249f094a09ad3cd7e96586ebd05a2607ee56b56fb2db8fd80b8a402ed292b50aebafeaf827c62c2eed09e265fa5aa9e013c0f27f0a88259f1aaa1279f0c32000000000000000000000000000000000000000000000000016345785d8a0000df871242dcbcc4fe9ed4d3413e21b2f8ce606a3ee7128c9b2d2e31fcedc1848e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000004b2d0d6c2c785217457b69b922a2a9cea98f71e91ca0ed6a4942a78c7ae6eb3c9dec496459a9ef68b34cb389acd939d13d3ecaf7e4aca021bb77e80fc60acf25a7a01cc1272b1b76594a521fb1abe1322d650e58a672c2","tx_hash":"c2d206e665aee159a5ab9aff60f76444e97bdad8f9152eccb6ca07d9204974ca"}}},{"timestamp":1608542349373,"event":{"type":"Finished"}}],"maker_amount":"0.1","maker_coin":"ETH","taker_amount":"0.1","taker_coin":"RICK","gui":"nogui","mm_version":"1a6082121","success_events":["Started","Negotiated","TakerFeeSent","MakerPaymentReceived","MakerPaymentWaitConfirmStarted","MakerPaymentValidatedAndConfirmed","TakerPaymentSent","TakerPaymentSpent","MakerPaymentSpent","Finished"],"error_events":["StartFailed","NegotiateFailed","TakerFeeSendFailed","MakerPaymentValidateFailed","MakerPaymentWaitConfirmFailed","TakerPaymentTransactionFailed","TakerPaymentWaitConfirmFailed","TakerPaymentDataSendFailed","TakerPaymentWaitForSpendFailed","MakerPaymentSpendFailed","TakerPaymentWaitRefundStarted","TakerPaymentRefunded","TakerPaymentRefundFailed"]}"#; + let taker_saved_swap: TakerSavedSwap = unwrap!(json::from_str(taker_saved_json)); + let key_pair = unwrap!(key_pair_from_seed( + "spice describe gravity federal blast come thank unfair canal monkey style afraid" + )); + let ctx = MmCtxBuilder::default().with_secp256k1_key_pair(key_pair).into_mm_arc(); + + TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); + static mut SWAP_CONTRACT_ADDRESS_CALLED: usize = 0; + TestCoin::swap_contract_address.mock_safe(|_| { + unsafe { SWAP_CONTRACT_ADDRESS_CALLED += 1 }; + MockResult::Return(Some(BytesJson::default())) + }); + let maker_coin = MmCoinEnum::Test(TestCoin {}); + let taker_coin = MmCoinEnum::Test(TestCoin {}); + let (taker_swap, _) = unwrap!(TakerSwap::load_from_saved( + ctx.clone(), + maker_coin, + taker_coin, + taker_saved_swap + )); + + assert_eq!(unsafe { SWAP_CONTRACT_ADDRESS_CALLED }, 1); + let expected_addr = addr_from_str("0xa09ad3cd7e96586ebd05a2607ee56b56fb2db8fd").unwrap(); + let expected = BytesJson::from(expected_addr.0.as_ref()); + assert_eq!(taker_swap.r().data.maker_coin_swap_contract_address, Some(expected)); + assert_eq!( + taker_swap.r().data.taker_coin_swap_contract_address, + Some(BytesJson::default()) + ); + } + #[test] // https://github.com/KomodoPlatform/atomicDEX-API/issues/647 fn test_recoverable() { diff --git a/mm2src/mm2_tests.rs b/mm2src/mm2_tests.rs index b3fa70f005..9f1f08adbd 100644 --- a/mm2src/mm2_tests.rs +++ b/mm2src/mm2_tests.rs @@ -5,9 +5,10 @@ use bigdecimal::BigDecimal; #[cfg(not(feature = "native"))] use common::call_back; use common::executor::Timer; #[cfg(feature = "native")] use common::for_tests::mm_dump; -use common::for_tests::{enable_electrum, enable_native, from_env_file, get_passphrase, mm_spat, LocalStart, - MarketMakerIt, RaiiDump}; -use common::for_tests::{enable_qrc20, find_metrics_in_json}; +use common::for_tests::{check_my_swap_status, check_recent_swaps, check_stats_swap_status, enable_electrum, + enable_native, enable_qrc20, find_metrics_in_json, from_env_file, get_passphrase, mm_spat, + LocalStart, MarketMakerIt, RaiiDump, MAKER_ERROR_EVENTS, MAKER_SUCCESS_EVENTS, + TAKER_ERROR_EVENTS, TAKER_SUCCESS_EVENTS}; use common::mm_metrics::{MetricType, MetricsJson}; use common::mm_number::Fraction; use common::privkey::key_pair_from_seed; @@ -725,91 +726,6 @@ fn test_rpc_password_from_json_no_userpass() { ); } -/// Helper function requesting my swap status and checking it's events -async fn check_my_swap_status( - mm: &MarketMakerIt, - uuid: &str, - expected_success_events: &Vec<&str>, - expected_error_events: &Vec<&str>, - maker_amount: BigDecimal, - taker_amount: BigDecimal, -) { - let response = unwrap!( - mm.rpc(json! ({ - "userpass": mm.userpass, - "method": "my_swap_status", - "params": { - "uuid": uuid, - } - })) - .await - ); - assert!(response.0.is_success(), "!status of {}: {}", uuid, response.1); - let status_response: Json = unwrap!(json::from_str(&response.1)); - let success_events: Vec = unwrap!(json::from_value(status_response["result"]["success_events"].clone())); - assert_eq!(expected_success_events, &success_events); - let error_events: Vec = unwrap!(json::from_value(status_response["result"]["error_events"].clone())); - assert_eq!(expected_error_events, &error_events); - - let events_array = unwrap!(status_response["result"]["events"].as_array()); - let actual_maker_amount = unwrap!(json::from_value( - events_array[0]["event"]["data"]["maker_amount"].clone() - )); - assert_eq!(maker_amount, actual_maker_amount); - let actual_taker_amount = unwrap!(json::from_value( - events_array[0]["event"]["data"]["taker_amount"].clone() - )); - assert_eq!(taker_amount, actual_taker_amount); - let actual_events = events_array.iter().map(|item| unwrap!(item["event"]["type"].as_str())); - let actual_events: Vec<&str> = actual_events.collect(); - assert_eq!(expected_success_events, &actual_events); -} - -async fn check_stats_swap_status( - mm: &MarketMakerIt, - uuid: &str, - maker_expected_events: &Vec<&str>, - taker_expected_events: &Vec<&str>, -) { - let response = unwrap!( - mm.rpc(json! ({ - "method": "stats_swap_status", - "params": { - "uuid": uuid, - } - })) - .await - ); - assert!(response.0.is_success(), "!status of {}: {}", uuid, response.1); - let status_response: Json = unwrap!(json::from_str(&response.1)); - let maker_events_array = unwrap!(status_response["result"]["maker"]["events"].as_array()); - let taker_events_array = unwrap!(status_response["result"]["taker"]["events"].as_array()); - let maker_actual_events = maker_events_array - .iter() - .map(|item| unwrap!(item["event"]["type"].as_str())); - let maker_actual_events: Vec<&str> = maker_actual_events.collect(); - let taker_actual_events = taker_events_array - .iter() - .map(|item| unwrap!(item["event"]["type"].as_str())); - let taker_actual_events: Vec<&str> = taker_actual_events.collect(); - assert_eq!(maker_expected_events, &maker_actual_events); - assert_eq!(taker_expected_events, &taker_actual_events); -} - -async fn check_recent_swaps(mm: &MarketMakerIt, expected_len: usize) { - let response = unwrap!( - mm.rpc(json! ({ - "method": "my_recent_swaps", - "userpass": mm.userpass, - })) - .await - ); - assert!(response.0.is_success(), "!status of my_recent_swaps {}", response.1); - let swaps_response: Json = unwrap!(json::from_str(&response.1)); - let swaps: &Vec = unwrap!(swaps_response["result"]["swaps"].as_array()); - assert_eq!(expected_len, swaps.len()); -} - /// 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)>) { @@ -951,65 +867,6 @@ async fn trade_base_rel_electrum(pairs: Vec<(&'static str, &'static str)>) { ); } - let maker_success_events = vec![ - "Started", - "Negotiated", - "TakerFeeValidated", - "MakerPaymentSent", - "TakerPaymentReceived", - "TakerPaymentWaitConfirmStarted", - "TakerPaymentValidatedAndConfirmed", - "TakerPaymentSpent", - "TakerPaymentSpendConfirmStarted", - "TakerPaymentSpendConfirmed", - "Finished", - ]; - - let maker_error_events = vec![ - "StartFailed", - "NegotiateFailed", - "TakerFeeValidateFailed", - "MakerPaymentTransactionFailed", - "MakerPaymentDataSendFailed", - "MakerPaymentWaitConfirmFailed", - "TakerPaymentValidateFailed", - "TakerPaymentWaitConfirmFailed", - "TakerPaymentSpendFailed", - "TakerPaymentSpendConfirmFailed", - "MakerPaymentWaitRefundStarted", - "MakerPaymentRefunded", - "MakerPaymentRefundFailed", - ]; - - let taker_success_events = vec![ - "Started", - "Negotiated", - "TakerFeeSent", - "MakerPaymentReceived", - "MakerPaymentWaitConfirmStarted", - "MakerPaymentValidatedAndConfirmed", - "TakerPaymentSent", - "TakerPaymentSpent", - "MakerPaymentSpent", - "Finished", - ]; - - let taker_error_events = vec![ - "StartFailed", - "NegotiateFailed", - "TakerFeeSendFailed", - "MakerPaymentValidateFailed", - "MakerPaymentWaitConfirmFailed", - "TakerPaymentTransactionFailed", - "TakerPaymentWaitConfirmFailed", - "TakerPaymentDataSendFailed", - "TakerPaymentWaitForSpendFailed", - "MakerPaymentSpendFailed", - "TakerPaymentWaitRefundStarted", - "TakerPaymentRefunded", - "TakerPaymentRefundFailed", - ]; - for uuid in uuids.iter() { unwrap!( mm_bob @@ -1032,8 +889,8 @@ async fn trade_base_rel_electrum(pairs: Vec<(&'static str, &'static str)>) { check_my_swap_status( &mm_alice, &uuid, - &taker_success_events, - &taker_error_events, + &TAKER_SUCCESS_EVENTS, + &TAKER_ERROR_EVENTS, "0.1".parse().unwrap(), "0.1".parse().unwrap(), ) @@ -1043,8 +900,8 @@ async fn trade_base_rel_electrum(pairs: Vec<(&'static str, &'static str)>) { check_my_swap_status( &mm_bob, &uuid, - &maker_success_events, - &maker_error_events, + &MAKER_SUCCESS_EVENTS, + &MAKER_ERROR_EVENTS, "0.1".parse().unwrap(), "0.1".parse().unwrap(), ) @@ -1056,10 +913,10 @@ async fn trade_base_rel_electrum(pairs: Vec<(&'static str, &'static str)>) { for uuid in uuids.iter() { log!("Checking alice status.."); - check_stats_swap_status(&mm_alice, &uuid, &maker_success_events, &taker_success_events).await; + check_stats_swap_status(&mm_alice, &uuid, &MAKER_SUCCESS_EVENTS, &TAKER_SUCCESS_EVENTS).await; log!("Checking bob status.."); - check_stats_swap_status(&mm_bob, &uuid, &maker_success_events, &taker_success_events).await; + check_stats_swap_status(&mm_bob, &uuid, &MAKER_SUCCESS_EVENTS, &TAKER_SUCCESS_EVENTS).await; } log!("Checking alice recent swaps.."); @@ -3751,7 +3608,7 @@ fn test_convert_qrc20_address() { &mm, "QRC20", &["95.217.83.126:10001"], - "0xd362e096e873eb7907e205fadc6175c6fec7bc44", + "0xba8b71f3544b93e2f681f996da519a98ace0107a", )); // test wallet to contract @@ -4044,7 +3901,7 @@ fn qrc20_activate_electrum() { &mm, "QRC20", &["95.217.83.126:10001"], - "0xd362e096e873eb7907e205fadc6175c6fec7bc44", + "0xba8b71f3544b93e2f681f996da519a98ace0107a", )); assert_eq!( electrum_json["address"].as_str(), @@ -4089,7 +3946,7 @@ fn test_qrc20_withdraw() { &mm, "QRC20", &["95.217.83.126:10001"], - "0xd362e096e873eb7907e205fadc6175c6fec7bc44", + "0xba8b71f3544b93e2f681f996da519a98ace0107a", )); assert_eq!( electrum_json["address"].as_str(), @@ -4164,7 +4021,7 @@ fn test_qrc20_withdraw_error() { &mm, "QRC20", &["95.217.83.126:10001"], - "0xd362e096e873eb7907e205fadc6175c6fec7bc44", + "0xba8b71f3544b93e2f681f996da519a98ace0107a", )); let balance = electrum_json["balance"].as_str().unwrap(); assert_eq!(balance, "10"); diff --git a/mm2src/ordermatch_tests.rs b/mm2src/ordermatch_tests.rs index c85986956a..0db4f04b73 100644 --- a/mm2src/ordermatch_tests.rs +++ b/mm2src/ordermatch_tests.rs @@ -2171,7 +2171,7 @@ fn test_process_sync_pubkey_orderbook_state_points_to_not_uptodate_trie_root() { let alb_pair = alb_ordered_pair("RICK", "MORTY"); // update trie root by adding a new order and do not update history - let (old_root, new_root) = { + let (old_root, _new_root) = { let ordermatch_ctx = OrdermatchContext::from_ctx(&ctx).unwrap(); let mut orderbook = block_on(ordermatch_ctx.orderbook.lock());