Skip to content

Commit

Permalink
Add unspendable_balance to my_balance, electrum, enable RPC calls (#837)
Browse files Browse the repository at this point in the history
* MarketCoinOps::my_balance returns spendable and unspendable balances
  • Loading branch information
sergeyboyko0791 authored Feb 26, 2021
1 parent 9670720 commit a5e7af4
Show file tree
Hide file tree
Showing 19 changed files with 241 additions and 127 deletions.
27 changes: 13 additions & 14 deletions mm2src/coins/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ use web3::types::{Action as TraceAction, BlockId, BlockNumber, Bytes, CallReques
TraceFilterBuilder, Transaction as Web3Transaction, TransactionId};
use web3::{self, Web3};

use super::{CoinProtocol, CoinTransportMetrics, CoinsContext, FeeApproxStage, FoundSwapTxSpend, HistorySyncState,
MarketCoinOps, MmCoin, RpcClientType, RpcTransportEventHandler, RpcTransportEventHandlerShared, SwapOps,
TradeFee, TradePreimageError, TradePreimageValue, Transaction, TransactionDetails, TransactionEnum,
TransactionFut, ValidateAddressResult, WithdrawFee, WithdrawRequest};
use super::{CoinBalance, CoinProtocol, CoinTransportMetrics, CoinsContext, FeeApproxStage, FoundSwapTxSpend,
HistorySyncState, MarketCoinOps, MmCoin, RpcClientType, RpcTransportEventHandler,
RpcTransportEventHandlerShared, SwapOps, TradeFee, TradePreimageError, TradePreimageValue, Transaction,
TransactionDetails, TransactionEnum, TransactionFut, ValidateAddressResult, WithdrawFee, WithdrawRequest};

pub use ethcore_transaction::SignedTransaction as SignedEthTx;
pub use rlp;
Expand Down Expand Up @@ -880,12 +880,16 @@ impl MarketCoinOps for EthCoin {

fn my_address(&self) -> Result<String, String> { Ok(checksum_address(&format!("{:#02x}", self.my_address))) }

fn my_balance(&self) -> Box<dyn Future<Item = BigDecimal, Error = String> + Send> {
fn my_balance(&self) -> Box<dyn Future<Item = CoinBalance, Error = String> + Send> {
let decimals = self.decimals;
Box::new(
self.my_balance()
.and_then(move |result| Ok(try_s!(u256_to_big_decimal(result, decimals)))),
)
let fut = self
.my_balance()
.and_then(move |result| Ok(try_s!(u256_to_big_decimal(result, decimals))))
.map(|spendable| CoinBalance {
spendable,
unspendable: BigDecimal::from(0),
});
Box::new(fut)
}

fn base_coin_balance(&self) -> Box<dyn Future<Item = BigDecimal, Error = String> + Send> {
Expand Down Expand Up @@ -2576,11 +2580,6 @@ impl MmCoin for EthCoin {
log!("Warning: set_requires_notarization doesn't take any effect on ETH/ERC20 coins");
}

fn my_unspendable_balance(&self) -> Box<dyn Future<Item = BigDecimal, Error = String> + Send> {
// Eth has not unspendable outputs
Box::new(futures01::future::ok(0.into()))
}

fn swap_contract_address(&self) -> Option<BytesJson> {
Some(BytesJson::from(self.swap_contract_address.0.as_ref()))
}
Expand Down
19 changes: 13 additions & 6 deletions mm2src/coins/lp_coins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,11 @@ pub trait MarketCoinOps {

fn my_address(&self) -> Result<String, String>;

fn my_balance(&self) -> Box<dyn Future<Item = BigDecimal, Error = String> + Send>;
fn my_balance(&self) -> Box<dyn Future<Item = CoinBalance, Error = String> + Send>;

fn my_spendable_balance(&self) -> Box<dyn Future<Item = BigDecimal, Error = String> + Send> {
Box::new(self.my_balance().map(|CoinBalance { spendable, .. }| spendable))
}

/// Base coin balance for tokens, e.g. ETH balance in ERC20 case
fn base_coin_balance(&self) -> Box<dyn Future<Item = BigDecimal, Error = String> + Send>;
Expand Down Expand Up @@ -429,6 +433,12 @@ pub struct TradeFee {
pub amount: MmNumber,
}

#[derive(Clone, Debug, PartialEq, PartialOrd)]
pub struct CoinBalance {
pub spendable: BigDecimal,
pub unspendable: BigDecimal,
}

/// The approximation is needed to cover the dynamic miner fee changing during a swap.
#[derive(Clone, Debug)]
pub enum FeeApproxStage {
Expand Down Expand Up @@ -597,9 +607,6 @@ pub trait MmCoin: SwapOps + MarketCoinOps + fmt::Debug + Send + Sync + 'static {
/// set requires notarization
fn set_requires_notarization(&self, requires_nota: bool);

/// Get unspendable balance (sum of non-mature output values).
fn my_unspendable_balance(&self) -> Box<dyn Future<Item = BigDecimal, Error = String> + Send>;

/// Get swap contract address if the coin uses it in Atomic Swaps.
fn swap_contract_address(&self) -> Option<BytesJson>;

Expand Down Expand Up @@ -1234,8 +1241,8 @@ pub async fn check_balance_update_loop(ctx: MmArc, ticker: String) {
Timer::sleep(10.).await;
match lp_coinfind(&ctx, &ticker).await {
Ok(Some(coin)) => {
let balance = match coin.my_balance().compat().await {
Ok(b) => b,
let balance = match coin.my_spendable_balance().compat().await {
Ok(balance) => balance,
Err(_) => continue,
};
if Some(&balance) != current_balance.as_ref() {
Expand Down
23 changes: 13 additions & 10 deletions mm2src/coins/qrc20.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::utxo::utxo_common::{self, big_decimal_from_sat, check_all_inputs_sign
use crate::utxo::{coin_daemon_data_dir, qtum, sign_tx, ActualTxFee, AdditionalTxData, FeePolicy,
GenerateTransactionError, RecentlySpentOutPoints, UtxoCoinBuilder, UtxoCoinFields, UtxoCommonOps,
UtxoTx, VerboseTransactionFrom, UTXO_LOCK};
use crate::{FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MarketCoinOps, MmCoin, SwapOps, TradeFee,
use crate::{CoinBalance, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MarketCoinOps, MmCoin, SwapOps, TradeFee,
TradePreimageError, TradePreimageValue, TransactionDetails, TransactionEnum, TransactionFut,
ValidateAddressResult, WithdrawFee, WithdrawRequest};
use async_trait::async_trait;
Expand Down Expand Up @@ -829,7 +829,7 @@ impl MarketCoinOps for Qrc20Coin {

fn my_address(&self) -> Result<String, String> { utxo_common::my_address(self) }

fn my_balance(&self) -> Box<dyn Future<Item = BigDecimal, Error = String> + Send> {
fn my_balance(&self) -> Box<dyn Future<Item = CoinBalance, Error = String> + Send> {
let my_address = self.my_addr_as_contract_addr();
let params = &[Token::Address(my_address)];
let contract_address = self.contract_address;
Expand All @@ -844,13 +844,21 @@ impl MarketCoinOps for Qrc20Coin {
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"#),
})
.map(|spendable| CoinBalance {
spendable,
unspendable: BigDecimal::from(0),
});
Box::new(fut)
}

fn base_coin_balance(&self) -> Box<dyn Future<Item = BigDecimal, Error = String> + Send> {
// use standard UTXO my_balance implementation that returns Qtum balance instead of QRC20
utxo_common::my_balance(&self.utxo)
let selfi = self.clone();
let fut = async move {
let CoinBalance { spendable, .. } = try_s!(selfi.qtum_balance().await);
Ok(spendable)
};
Box::new(fut.boxed().compat())
}

fn send_raw_tx(&self, tx: &str) -> Box<dyn Future<Item = String, Error = String> + Send> {
Expand Down Expand Up @@ -1084,11 +1092,6 @@ impl MmCoin for Qrc20Coin {
utxo_common::set_requires_notarization(&self.utxo, requires_nota)
}

fn my_unspendable_balance(&self) -> Box<dyn Future<Item = BigDecimal, Error = String> + Send> {
// QRC20 cannot have unspendable balance
Box::new(futures01::future::ok(0.into()))
}

fn swap_contract_address(&self) -> Option<BytesJson> {
Some(BytesJson::from(self.swap_contract_address.0.as_ref()))
}
Expand Down Expand Up @@ -1131,7 +1134,7 @@ async fn qrc20_withdraw(coin: Qrc20Coin, req: WithdrawRequest) -> Result<Transac

let _utxo_lock = UTXO_LOCK.lock().await;

let qrc20_balance = try_s!(coin.my_balance().compat().await);
let qrc20_balance = try_s!(coin.my_spendable_balance().compat().await);

// the qrc20_amount_sat is used only within smart contract calls
let (qrc20_amount_sat, qrc20_amount) = if req.max {
Expand Down
6 changes: 3 additions & 3 deletions mm2src/coins/qrc20/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ enum ProcessCachedTransferMapResult {

impl Qrc20Coin {
pub fn history_loop(&self, ctx: MmArc) {
let mut my_balance: Option<BigDecimal> = None;
let mut my_balance: Option<CoinBalance> = None;
let mut history_map = self.try_load_history_from_file(&ctx);

let mut success_iteration = 0i32;
Expand Down Expand Up @@ -361,8 +361,8 @@ impl Qrc20Coin {
fn check_if_history_update_is_needed(
&self,
history: &HistoryMapByHash,
last_balance: &Option<BigDecimal>,
actual_balance: &BigDecimal,
last_balance: &Option<CoinBalance>,
actual_balance: &CoinBalance,
) -> bool {
let need_update = history
.iter()
Expand Down
6 changes: 5 additions & 1 deletion mm2src/coins/qrc20/qrc20_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -718,7 +718,11 @@ fn test_taker_fee_tx_fee() {
let (_ctx, coin) = qrc20_coin_for_test(&priv_key);
// check if the coin's tx fee is expected
check_tx_fee(&coin, ActualTxFee::Fixed(EXPECTED_TX_FEE as u64));
assert_eq!(coin.my_balance().wait().expect("!my_balance"), BigDecimal::from(5));
let expected_balance = CoinBalance {
spendable: BigDecimal::from(5),
unspendable: BigDecimal::from(0),
};
assert_eq!(coin.my_balance().wait().expect("!my_balance"), expected_balance);

let dex_fee_amount = BigDecimal::from(5);
let actual = coin
Expand Down
2 changes: 1 addition & 1 deletion mm2src/coins/qrc20/swap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ impl Qrc20Coin {
receiver_addr: H160,
swap_contract_address: H160,
) -> Result<TransactionEnum, String> {
let balance = try_s!(self.my_balance().compat().await);
let balance = try_s!(self.my_spendable_balance().compat().await);
let balance = try_s!(wei_from_big_decimal(&balance, self.utxo.decimals));

let outputs = try_s!(
Expand Down
8 changes: 3 additions & 5 deletions mm2src/coins/test_coin.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::{HistorySyncState, MarketCoinOps, MmCoin, SwapOps, TradeFee, TransactionDetails, TransactionEnum,
TransactionFut};
use super::{CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, SwapOps, TradeFee, TransactionDetails,
TransactionEnum, TransactionFut};
use crate::{FeeApproxStage, FoundSwapTxSpend, TradePreimageError, TradePreimageValue, ValidateAddressResult,
WithdrawRequest};
use bigdecimal::BigDecimal;
Expand All @@ -21,7 +21,7 @@ impl MarketCoinOps for TestCoin {

fn my_address(&self) -> Result<String, String> { unimplemented!() }

fn my_balance(&self) -> Box<dyn Future<Item = BigDecimal, Error = String> + Send> { unimplemented!() }
fn my_balance(&self) -> Box<dyn Future<Item = CoinBalance, Error = String> + Send> { unimplemented!() }

fn base_coin_balance(&self) -> Box<dyn Future<Item = BigDecimal, Error = String> + Send> { unimplemented!() }

Expand Down Expand Up @@ -259,8 +259,6 @@ impl MmCoin for TestCoin {

fn set_requires_notarization(&self, _requires_nota: bool) { unimplemented!() }

fn my_unspendable_balance(&self) -> Box<dyn Future<Item = BigDecimal, Error = String> + Send> { unimplemented!() }

fn swap_contract_address(&self) -> Option<BytesJson> { unimplemented!() }

fn mature_confirmations(&self) -> Option<u32> { unimplemented!() }
Expand Down
1 change: 0 additions & 1 deletion mm2src/coins/utxo/qrc20.rs

This file was deleted.

29 changes: 21 additions & 8 deletions mm2src/coins/utxo/qtum.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::*;
use crate::{eth, CanRefundHtlc, SwapOps, TradePreimageError, TradePreimageValue, ValidateAddressResult};
use crate::{eth, CanRefundHtlc, CoinBalance, SwapOps, TradePreimageError, TradePreimageValue, ValidateAddressResult};
use common::mm_metrics::MetricsArc;
use ethereum_types::H160;
use futures::{FutureExt, TryFutureExt};
Expand All @@ -18,7 +18,22 @@ pub enum QtumAddressFormat {
Contract,
}

pub trait QtumBasedCoin: AsRef<UtxoCoinFields> {
#[async_trait]
pub trait QtumBasedCoin: AsRef<UtxoCoinFields> + UtxoCommonOps + MarketCoinOps {
async fn qtum_balance(&self) -> Result<CoinBalance, String> {
let balance = try_s!(
self.as_ref()
.rpc_client
.display_balance(self.as_ref().my_address.clone(), self.as_ref().decimals)
.compat()
.await
);

let unspendable = try_s!(utxo_common::my_unspendable_balance(self, &balance).await);
let spendable = &balance - &unspendable;
Ok(CoinBalance { spendable, unspendable })
}

fn convert_to_address(&self, from: &str, to_address_format: Json) -> Result<String, String> {
let to_address_format: QtumAddressFormat =
json::from_value(to_address_format).map_err(|e| ERRL!("Error on parse Qtum address format {:?}", e))?;
Expand Down Expand Up @@ -419,8 +434,10 @@ impl MarketCoinOps for QtumCoin {

fn my_address(&self) -> Result<String, String> { utxo_common::my_address(self) }

fn my_balance(&self) -> Box<dyn Future<Item = BigDecimal, Error = String> + Send> {
utxo_common::my_balance(&self.utxo_arc)
fn my_balance(&self) -> Box<dyn Future<Item = CoinBalance, Error = String> + Send> {
let selfi = self.clone();
let fut = async move { Ok(try_s!(selfi.qtum_balance().await)) };
Box::new(fut.boxed().compat())
}

fn base_coin_balance(&self) -> Box<dyn Future<Item = BigDecimal, Error = String> + Send> {
Expand Down Expand Up @@ -537,10 +554,6 @@ impl MmCoin for QtumCoin {
utxo_common::set_requires_notarization(&self.utxo_arc, requires_nota)
}

fn my_unspendable_balance(&self) -> Box<dyn Future<Item = BigDecimal, Error = String> + Send> {
Box::new(utxo_common::my_unspendable_balance(self.clone()).boxed().compat())
}

fn swap_contract_address(&self) -> Option<BytesJson> { utxo_common::swap_contract_address() }

fn mature_confirmations(&self) -> Option<u32> { Some(self.utxo_arc.conf.mature_confirmations) }
Expand Down
33 changes: 21 additions & 12 deletions mm2src/coins/utxo/utxo_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ 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::log::{error, info, warn};
use common::mm_ctx::MmArc;
use common::mm_metrics::MetricsArc;
use futures::compat::Future01CompatExt;
Expand All @@ -32,7 +32,7 @@ pub use chain::Transaction as UtxoTx;

use self::rpc_clients::{electrum_script_hash, UnspentInfo, UtxoRpcClientEnum};
use crate::utxo::rpc_clients::UtxoRpcClientOps;
use crate::{CanRefundHtlc, FeeApproxStage, TradePreimageError, TradePreimageValue, ValidateAddressResult};
use crate::{CanRefundHtlc, CoinBalance, FeeApproxStage, TradePreimageError, TradePreimageValue, ValidateAddressResult};
use common::{block_on, Traceable};

macro_rules! true_or {
Expand Down Expand Up @@ -209,7 +209,7 @@ pub fn base_coin_balance<T>(coin: &T) -> Box<dyn Future<Item = BigDecimal, Error
where
T: MarketCoinOps,
{
coin.my_balance()
Box::new(coin.my_spendable_balance())
}

pub fn display_address(conf: &UtxoCoinConf, address: &Address) -> Result<String, String> {
Expand Down Expand Up @@ -1210,10 +1210,15 @@ where
coin.display_address(&coin.as_ref().my_address)
}

pub fn my_balance(coin: &UtxoCoinFields) -> Box<dyn Future<Item = BigDecimal, Error = String> + Send> {
pub fn my_balance(coin: &UtxoCoinFields) -> Box<dyn Future<Item = CoinBalance, Error = String> + Send> {
Box::new(
coin.rpc_client
.display_balance(coin.my_address.clone(), coin.decimals)
// at the moment standard UTXO coins do not have an unspendable balance
.map(|spendable| CoinBalance {
spendable,
unspendable: BigDecimal::from(0),
})
.map_err(|e| ERRL!("{}", e)),
)
}
Expand Down Expand Up @@ -1460,7 +1465,7 @@ pub fn process_history_loop<T>(coin: &T, ctx: MmArc)
where
T: AsRef<UtxoCoinFields> + UtxoStandardOps + UtxoCommonOps + MmCoin + MarketCoinOps,
{
let mut my_balance: Option<BigDecimal> = None;
let mut my_balance: Option<CoinBalance> = None;
let history = coin.load_history_from_file(&ctx);
let mut history_map: HashMap<H256Json, TransactionDetails> = history
.into_iter()
Expand Down Expand Up @@ -2176,29 +2181,33 @@ pub async fn cache_transaction_if_possible(_coin: &UtxoCoinFields, _tx: &RpcTran
Ok(())
}

pub async fn my_unspendable_balance<T>(coin: T) -> Result<BigDecimal, String>
pub async fn my_unspendable_balance<T>(coin: &T, total_balance: &BigDecimal) -> Result<BigDecimal, String>
where
T: AsRef<UtxoCoinFields> + UtxoCommonOps + MarketCoinOps,
T: AsRef<UtxoCoinFields> + UtxoCommonOps + MarketCoinOps + ?Sized,
{
let mut attempts = 0i32;
loop {
let balance = try_s!(coin.my_balance().compat().await);
let (mature_unspents, _) = try_s!(coin.ordered_mature_unspents(&coin.as_ref().my_address).await);
let spendable_balance = mature_unspents.iter().fold(BigDecimal::zero(), |acc, x| {
acc + big_decimal_from_sat(x.value as i64, coin.as_ref().decimals)
});
if balance >= spendable_balance {
return Ok(balance - spendable_balance);
if total_balance >= &spendable_balance {
return Ok(total_balance - spendable_balance);
}

if attempts == 2 {
return ERR!(
"spendable balance {} more than total balance {}",
"Spendable balance {} greater than total balance {}",
spendable_balance,
balance
total_balance
);
}

warn!(
"Attempt N{}: spendable balance {} greater than total balance {}",
attempts, spendable_balance, total_balance
);

// the balance could be changed by other instance between my_balance() and ordered_mature_unspents() calls
// try again
attempts += 1;
Expand Down
Loading

0 comments on commit a5e7af4

Please sign in to comment.