mirror of
https://github.com/basicswap/basicswap.git
synced 2025-11-06 02:38:11 +01:00
ui: Improve fee estimation.
This commit is contained in:
@@ -155,9 +155,13 @@ class BTCInterface(CoinInterface):
|
||||
return abs(a - b) < 20
|
||||
|
||||
@staticmethod
|
||||
def xmr_swap_alock_spend_tx_vsize() -> int:
|
||||
def xmr_swap_a_lock_spend_tx_vsize() -> int:
|
||||
return 147
|
||||
|
||||
@staticmethod
|
||||
def xmr_swap_b_lock_spend_tx_vsize() -> int:
|
||||
return 110
|
||||
|
||||
@staticmethod
|
||||
def txoType():
|
||||
return CTxOut
|
||||
@@ -986,14 +990,14 @@ class BTCInterface(CoinInterface):
|
||||
def scanTxOutset(self, dest):
|
||||
return self.rpc_callback('scantxoutset', ['start', ['raw({})'.format(dest.hex())]])
|
||||
|
||||
def getTransaction(self, txid):
|
||||
def getTransaction(self, txid: bytes):
|
||||
try:
|
||||
return bytes.fromhex(self.rpc_callback('getrawtransaction', [txid.hex()]))
|
||||
except Exception as ex:
|
||||
# TODO: filter errors
|
||||
return None
|
||||
|
||||
def getWalletTransaction(self, txid):
|
||||
def getWalletTransaction(self, txid: bytes):
|
||||
try:
|
||||
return bytes.fromhex(self.rpc_callback('gettransaction', [txid.hex()]))
|
||||
except Exception as ex:
|
||||
@@ -1460,7 +1464,7 @@ class BTCInterface(CoinInterface):
|
||||
tx.rehash()
|
||||
return tx.serialize().hex()
|
||||
|
||||
def ensureFunds(self, amount):
|
||||
def ensureFunds(self, amount: int) -> None:
|
||||
if self.getSpendableBalance() < amount:
|
||||
raise ValueError('Balance too low')
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ class FIROInterface(BTCInterface):
|
||||
|
||||
return rv
|
||||
|
||||
def createSCLockTx(self, value: int, script: bytearray, vkbv=None) -> bytes:
|
||||
def createSCLockTx(self, value: int, script: bytearray, vkbv: bytes = None) -> bytes:
|
||||
tx = CTransaction()
|
||||
tx.nVersion = self.txVersion()
|
||||
tx.vout.append(self.txoType()(value, self.getScriptDest(script)))
|
||||
|
||||
@@ -59,8 +59,12 @@ class PARTInterface(BTCInterface):
|
||||
return 0xa0
|
||||
|
||||
@staticmethod
|
||||
def xmr_swap_alock_spend_tx_vsize() -> int:
|
||||
return 213
|
||||
def xmr_swap_a_lock_spend_tx_vsize() -> int:
|
||||
return 200
|
||||
|
||||
@staticmethod
|
||||
def xmr_swap_b_lock_spend_tx_vsize() -> int:
|
||||
return 138
|
||||
|
||||
@staticmethod
|
||||
def txoType():
|
||||
@@ -111,8 +115,8 @@ class PARTInterface(BTCInterface):
|
||||
|
||||
return encodeStealthAddress(prefix_byte, scan_pubkey, spend_pubkey)
|
||||
|
||||
def getWitnessStackSerialisedLength(self, witness_stack):
|
||||
length = getCompactSizeLen(len(witness_stack))
|
||||
def getWitnessStackSerialisedLength(self, witness_stack) -> int:
|
||||
length: int = getCompactSizeLen(len(witness_stack))
|
||||
for e in witness_stack:
|
||||
length += getWitnessElementLen(len(e))
|
||||
return length
|
||||
@@ -138,7 +142,15 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
def balance_type():
|
||||
return BalanceTypes.BLIND
|
||||
|
||||
def coin_name(self):
|
||||
@staticmethod
|
||||
def xmr_swap_a_lock_spend_tx_vsize() -> int:
|
||||
return 1032
|
||||
|
||||
@staticmethod
|
||||
def xmr_swap_b_lock_spend_tx_vsize() -> int:
|
||||
return 980
|
||||
|
||||
def coin_name(self) -> str:
|
||||
return super().coin_name() + ' Blind'
|
||||
|
||||
def getScriptLockTxNonce(self, data):
|
||||
@@ -167,7 +179,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
ensure(v['result'] is True, 'verifycommitment failed')
|
||||
return output_n, blinded_info
|
||||
|
||||
def createSCLockTx(self, value: int, script: bytearray, vkbv) -> bytes:
|
||||
def createSCLockTx(self, value: int, script: bytearray, vkbv: bytes) -> bytes:
|
||||
|
||||
# Nonce is derived from vkbv, ephemeral_key isn't used
|
||||
ephemeral_key = self.getNewSecretKey()
|
||||
@@ -183,7 +195,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
tx_bytes = bytes.fromhex(rv['hex'])
|
||||
return tx_bytes
|
||||
|
||||
def fundSCLockTx(self, tx_bytes, feerate, vkbv):
|
||||
def fundSCLockTx(self, tx_bytes: bytes, feerate: int, vkbv: bytes) -> bytes:
|
||||
feerate_str = self.format_amount(feerate)
|
||||
# TODO: unlock unspents if bid cancelled
|
||||
|
||||
@@ -462,7 +474,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
ensure(output_n is not None, 'Output not found in tx')
|
||||
return output_n
|
||||
|
||||
def createSCLockSpendTx(self, tx_lock_bytes, script_lock, pk_dest, tx_fee_rate, vkbv):
|
||||
def createSCLockSpendTx(self, tx_lock_bytes: bytes, script_lock: bytes, pk_dest: bytes, tx_fee_rate: int, vkbv: bytes, fee_info={}) -> bytes:
|
||||
lock_tx_obj = self.rpc_callback('decoderawtransaction', [tx_lock_bytes.hex()])
|
||||
lock_txid_hex = lock_tx_obj['txid']
|
||||
|
||||
@@ -499,13 +511,20 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
rv = self.rpc_callback('fundrawtransactionfrom', ['blind', lock_spend_tx_hex, inputs_info, outputs_info, options])
|
||||
lock_spend_tx_hex = rv['hex']
|
||||
lock_spend_tx_obj = self.rpc_callback('decoderawtransaction', [lock_spend_tx_hex])
|
||||
|
||||
vsize = lock_spend_tx_obj['vsize']
|
||||
pay_fee = make_int(lock_spend_tx_obj['vout'][0]['ct_fee'])
|
||||
|
||||
# lock_spend_tx_hex does not include the dummy witness stack
|
||||
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
|
||||
vsize = self.getTxVSize(self.loadTx(bytes.fromhex(lock_spend_tx_hex)), add_witness_bytes=witness_bytes)
|
||||
actual_tx_fee_rate = pay_fee * 1000 // vsize
|
||||
self._log.info('createSCLockSpendTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.',
|
||||
lock_spend_tx_obj['txid'], actual_tx_fee_rate, vsize, pay_fee)
|
||||
|
||||
fee_info['vsize'] = vsize
|
||||
fee_info['fee_paid'] = pay_fee
|
||||
fee_info['rate_input'] = tx_fee_rate
|
||||
fee_info['rate_actual'] = actual_tx_fee_rate
|
||||
|
||||
return bytes.fromhex(lock_spend_tx_hex)
|
||||
|
||||
def verifySCLockSpendTx(self, tx_bytes,
|
||||
@@ -629,7 +648,7 @@ class PARTInterfaceBlind(PARTInterface):
|
||||
def getSpendableBalance(self) -> int:
|
||||
return self.make_int(self.rpc_callback('getbalances')['mine']['blind_trusted'])
|
||||
|
||||
def publishBLockTx(self, vkbv, Kbs, output_amount, feerate, delay_for: int = 10, unlock_time: int = 0) -> bytes:
|
||||
def publishBLockTx(self, vkbv: bytes, Kbs: bytes, output_amount: int, feerate: int, delay_for: int = 10, unlock_time: int = 0) -> bytes:
|
||||
Kbv = self.getPubkey(vkbv)
|
||||
sx_addr = self.formatStealthAddress(Kbv, Kbs)
|
||||
self._log.debug('sx_addr: {}'.format(sx_addr))
|
||||
@@ -751,14 +770,23 @@ class PARTInterfaceAnon(PARTInterface):
|
||||
def balance_type():
|
||||
return BalanceTypes.ANON
|
||||
|
||||
@staticmethod
|
||||
def xmr_swap_a_lock_spend_tx_vsize() -> int:
|
||||
raise ValueError('Not possible')
|
||||
|
||||
@staticmethod
|
||||
def xmr_swap_b_lock_spend_tx_vsize() -> int:
|
||||
# TODO: Estimate with ringsize
|
||||
return 1153
|
||||
|
||||
@staticmethod
|
||||
def depth_spendable() -> int:
|
||||
return 12
|
||||
|
||||
def coin_name(self):
|
||||
def coin_name(self) -> str:
|
||||
return super().coin_name() + ' Anon'
|
||||
|
||||
def publishBLockTx(self, kbv, Kbs, output_amount, feerate, delay_for: int = 10, unlock_time: int = 0) -> bytes:
|
||||
def publishBLockTx(self, kbv: bytes, Kbs: bytes, output_amount: int, feerate: int, delay_for: int = 10, unlock_time: int = 0) -> bytes:
|
||||
Kbv = self.getPubkey(kbv)
|
||||
sx_addr = self.formatStealthAddress(Kbv, Kbs)
|
||||
|
||||
|
||||
@@ -69,6 +69,15 @@ class XMRInterface(CoinInterface):
|
||||
def depth_spendable() -> int:
|
||||
return 10
|
||||
|
||||
@staticmethod
|
||||
def xmr_swap_a_lock_spend_tx_vsize() -> int:
|
||||
raise ValueError('Not possible')
|
||||
|
||||
@staticmethod
|
||||
def xmr_swap_b_lock_spend_tx_vsize() -> int:
|
||||
# TODO: Estimate with ringsize
|
||||
return 1507
|
||||
|
||||
def __init__(self, coin_settings, network, swap_client=None):
|
||||
super().__init__(network)
|
||||
daemon_login = None
|
||||
@@ -266,7 +275,7 @@ class XMRInterface(CoinInterface):
|
||||
def encodeSharedAddress(self, Kbv: bytes, Kbs: bytes) -> str:
|
||||
return xmr_util.encode_address(Kbv, Kbs)
|
||||
|
||||
def publishBLockTx(self, kbv, Kbs, output_amount, feerate, delay_for: int = 10, unlock_time: int = 0) -> bytes:
|
||||
def publishBLockTx(self, kbv: bytes, Kbs: bytes, output_amount: int, feerate: int, delay_for: int = 10, unlock_time: int = 0) -> bytes:
|
||||
with self._mx_wallet:
|
||||
self.openWallet(self._wallet_filename)
|
||||
self.rpc_wallet_cb('refresh')
|
||||
@@ -368,7 +377,7 @@ class XMRInterface(CoinInterface):
|
||||
|
||||
return None
|
||||
|
||||
def spendBLockTx(self, chain_b_lock_txid, address_to, kbv, kbs, cb_swap_value, b_fee_rate, restore_height, spend_actual_balance=False):
|
||||
def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee_rate: int, restore_height: int, spend_actual_balance: bool = False) -> bytes:
|
||||
'''
|
||||
Notes:
|
||||
"Error: No unlocked balance in the specified subaddress(es)" can mean not enough funds after tx fee.
|
||||
@@ -520,6 +529,9 @@ class XMRInterface(CoinInterface):
|
||||
# TODO
|
||||
return True
|
||||
|
||||
def ensureFunds(self, amount):
|
||||
def ensureFunds(self, amount: int) -> None:
|
||||
if self.getSpendableBalance() < amount:
|
||||
raise ValueError('Balance too low')
|
||||
|
||||
def getTransaction(self, txid: bytes):
|
||||
return self.rpc_cb2('get_transactions', {'txs_hashes': [txid.hex(), ]})
|
||||
|
||||
@@ -203,6 +203,18 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
|
||||
if with_extra_info:
|
||||
offer_data['amount_negotiable'] = o.amount_negotiable
|
||||
offer_data['rate_negotiable'] = o.rate_negotiable
|
||||
|
||||
if o.swap_type == SwapTypes.XMR_SWAP:
|
||||
_, xmr_offer = swap_client.getXmrOffer(o.offer_id)
|
||||
offer_data['lock_time_1'] = xmr_offer.lock_time_1
|
||||
offer_data['lock_time_2'] = xmr_offer.lock_time_2
|
||||
|
||||
offer_data['feerate_from'] = xmr_offer.a_fee_rate
|
||||
offer_data['feerate_to'] = xmr_offer.b_fee_rate
|
||||
else:
|
||||
offer_data['feerate_from'] = o.from_feerate
|
||||
offer_data['feerate_to'] = o.to_feerate
|
||||
|
||||
rv.append(offer_data)
|
||||
return bytes(json.dumps(rv), 'UTF-8')
|
||||
|
||||
|
||||
@@ -37,12 +37,15 @@ class JsonrpcDigest():
|
||||
self.__verbose = verbose
|
||||
self.__allow_none = allow_none
|
||||
|
||||
self.__request_id = 1
|
||||
self.__request_id = 0
|
||||
|
||||
def close(self):
|
||||
if self.__transport is not None:
|
||||
self.__transport.close()
|
||||
|
||||
def request_id(self):
|
||||
return self.__request_id
|
||||
|
||||
def post_request(self, method, params, timeout=None):
|
||||
try:
|
||||
connection = self.__transport.make_connection(self.__host)
|
||||
@@ -66,7 +69,7 @@ class JsonrpcDigest():
|
||||
self.__transport.close()
|
||||
raise
|
||||
|
||||
def json_request(self, method, params, username='', password='', timeout=None):
|
||||
def json_request(self, request_body, username='', password='', timeout=None):
|
||||
try:
|
||||
connection = self.__transport.make_connection(self.__host)
|
||||
if timeout:
|
||||
@@ -74,18 +77,11 @@ class JsonrpcDigest():
|
||||
|
||||
headers = self.__transport._extra_headers[:]
|
||||
|
||||
request_body = {
|
||||
'method': method,
|
||||
'params': params,
|
||||
'jsonrpc': '2.0',
|
||||
'id': self.__request_id
|
||||
}
|
||||
|
||||
connection.putrequest('POST', self.__handler)
|
||||
headers.append(('Content-Type', 'application/json'))
|
||||
headers.append(('Connection', 'keep-alive'))
|
||||
self.__transport.send_headers(connection, headers)
|
||||
self.__transport.send_content(connection, json.dumps(request_body, default=jsonDecimal).encode('utf-8'))
|
||||
self.__transport.send_content(connection, json.dumps(request_body, default=jsonDecimal).encode('utf-8') if request_body else '')
|
||||
resp = connection.getresponse()
|
||||
|
||||
if resp.status == 401:
|
||||
@@ -135,18 +131,11 @@ class JsonrpcDigest():
|
||||
headers = self.__transport._extra_headers[:]
|
||||
headers.append(('Authorization', header_value))
|
||||
|
||||
request_body = {
|
||||
'method': method,
|
||||
'params': params,
|
||||
'jsonrpc': '2.0',
|
||||
'id': self.__request_id
|
||||
}
|
||||
|
||||
connection.putrequest('POST', self.__handler)
|
||||
headers.append(('Content-Type', 'application/json'))
|
||||
headers.append(('Connection', 'keep-alive'))
|
||||
self.__transport.send_headers(connection, headers)
|
||||
self.__transport.send_content(connection, json.dumps(request_body, default=jsonDecimal).encode('utf-8'))
|
||||
self.__transport.send_content(connection, json.dumps(request_body, default=jsonDecimal).encode('utf-8') if request_body else '')
|
||||
resp = connection.getresponse()
|
||||
|
||||
self.__request_id += 1
|
||||
@@ -168,10 +157,16 @@ def callrpc_xmr(rpc_port, method, params=[], rpc_host='127.0.0.1', path='json_rp
|
||||
url = 'http://{}:{}/{}'.format(rpc_host, rpc_port, path)
|
||||
|
||||
x = JsonrpcDigest(url)
|
||||
request_body = {
|
||||
'method': method,
|
||||
'params': params,
|
||||
'jsonrpc': '2.0',
|
||||
'id': x.request_id()
|
||||
}
|
||||
if auth:
|
||||
v = x.json_request(method, params, username=auth[0], password=auth[1], timeout=timeout)
|
||||
v = x.json_request(request_body, username=auth[0], password=auth[1], timeout=timeout)
|
||||
else:
|
||||
v = x.json_request(method, params, timeout=timeout)
|
||||
v = x.json_request(request_body, timeout=timeout)
|
||||
x.close()
|
||||
r = json.loads(v.decode('utf-8'))
|
||||
except Exception as ex:
|
||||
@@ -183,7 +178,7 @@ def callrpc_xmr(rpc_port, method, params=[], rpc_host='127.0.0.1', path='json_rp
|
||||
return r['result']
|
||||
|
||||
|
||||
def callrpc_xmr2(rpc_port, method, params=None, auth=None, rpc_host='127.0.0.1', timeout=120):
|
||||
def callrpc_xmr2(rpc_port: int, method: str, params=None, auth=None, rpc_host='127.0.0.1', timeout=120):
|
||||
try:
|
||||
if rpc_host.count('://') > 0:
|
||||
url = '{}:{}/{}'.format(rpc_host, rpc_port, method)
|
||||
@@ -192,9 +187,9 @@ def callrpc_xmr2(rpc_port, method, params=None, auth=None, rpc_host='127.0.0.1',
|
||||
|
||||
x = JsonrpcDigest(url)
|
||||
if auth:
|
||||
v = x.json_request(method, params, username=auth[0], password=auth[1], timeout=timeout)
|
||||
v = x.json_request(params, username=auth[0], password=auth[1], timeout=timeout)
|
||||
else:
|
||||
v = x.json_request(method, params, timeout=timeout)
|
||||
v = x.json_request(params, timeout=timeout)
|
||||
x.close()
|
||||
r = json.loads(v.decode('utf-8'))
|
||||
except Exception as ex:
|
||||
|
||||
@@ -420,7 +420,7 @@
|
||||
</thead>
|
||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600 h-20">
|
||||
<td class="py-3 px-6">Amount you will get <span class="bold" id="bid_amt_from">{{ data.amt_from }}</span> {{ data.tla_from }}
|
||||
{% if data.xmr_type == true %} (excluding {{ data.amt_from_lock_spend_tx_fee }} {{ data.tla_from }} in tx fees).
|
||||
{% if data.xmr_type == true %} (excluding estimated {{ data.amt_from_lock_spend_tx_fee }} {{ data.tla_from }} in tx fees).
|
||||
{% else %} (excluding a tx fee).
|
||||
{% endif %}</td>
|
||||
<td class="py-3 px-6">Amount you will send <span class="bold" id="bid_amt_to">{{ data.amt_to }}</span> {{ data.tla_to }}
|
||||
|
||||
@@ -231,12 +231,12 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}):
|
||||
page_data['from_fee_override'] = ci_from.format_amount(ci_from.make_int(from_fee_override, r=1))
|
||||
parsed_data['from_fee_override'] = page_data['from_fee_override']
|
||||
|
||||
lock_spend_tx_vsize = ci_leader.xmr_swap_alock_spend_tx_vsize()
|
||||
lock_spend_tx_fee = ci_leader.make_int(ci_from.make_int(from_fee_override, r=1) * lock_spend_tx_vsize / 1000, r=1)
|
||||
page_data['amt_from_lock_spend_tx_fee'] = ci_from.format_amount(lock_spend_tx_fee // ci_leader.COIN())
|
||||
page_data['tla_from'] = ci_leader.ticker()
|
||||
lock_spend_tx_vsize = ci_from.xmr_swap_b_lock_spend_tx_vsize() if reverse_bid else ci_from.xmr_swap_a_lock_spend_tx_vsize()
|
||||
lock_spend_tx_fee = ci_from.make_int(ci_from.make_int(from_fee_override, r=1) * lock_spend_tx_vsize / 1000, r=1)
|
||||
page_data['amt_from_lock_spend_tx_fee'] = ci_from.format_amount(lock_spend_tx_fee // ci_from.COIN())
|
||||
page_data['tla_from'] = ci_from.ticker()
|
||||
|
||||
if ci_follower == Coins.XMR:
|
||||
if ci_to == Coins.XMR:
|
||||
if have_data_entry(form_data, 'fee_rate_to'):
|
||||
page_data['to_fee_override'] = get_data_entry(form_data, 'fee_rate_to')
|
||||
parsed_data['to_fee_override'] = page_data['to_fee_override']
|
||||
@@ -244,7 +244,7 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}):
|
||||
to_fee_override, page_data['to_fee_src'] = swap_client.getFeeRateForCoin(parsed_data['coin_to'], page_data['fee_to_conf'])
|
||||
if page_data['fee_to_extra'] > 0:
|
||||
to_fee_override += to_fee_override * (float(page_data['fee_to_extra']) / 100.0)
|
||||
page_data['to_fee_override'] = ci_follower.format_amount(ci_follower.make_int(to_fee_override, r=1))
|
||||
page_data['to_fee_override'] = ci_to.format_amount(ci_to.make_int(to_fee_override, r=1))
|
||||
parsed_data['to_fee_override'] = page_data['to_fee_override']
|
||||
except Exception as e:
|
||||
print('Error setting fee', str(e)) # Expected if missing fields
|
||||
@@ -612,14 +612,15 @@ def page_offer(self, url_split, post_string):
|
||||
int_fee_rate_now, fee_source = ci_leader.get_fee_rate()
|
||||
|
||||
data['xmr_type'] = True
|
||||
data['a_fee_rate'] = ci_leader.format_amount(xmr_offer.a_fee_rate)
|
||||
data['a_fee_rate'] = ci_from.format_amount(xmr_offer.a_fee_rate)
|
||||
data['a_fee_rate_verify'] = ci_leader.format_amount(int_fee_rate_now, conv_int=True)
|
||||
data['a_fee_rate_verify_src'] = fee_source
|
||||
data['a_fee_warn'] = xmr_offer.a_fee_rate < int_fee_rate_now
|
||||
|
||||
lock_spend_tx_vsize = ci_leader.xmr_swap_alock_spend_tx_vsize()
|
||||
lock_spend_tx_fee = ci_leader.make_int(xmr_offer.a_fee_rate * lock_spend_tx_vsize / 1000, r=1)
|
||||
data['amt_from_lock_spend_tx_fee'] = ci_leader.format_amount(lock_spend_tx_fee // ci_leader.COIN())
|
||||
from_fee_rate = xmr_offer.b_fee_rate if reverse_bid else xmr_offer.a_fee_rate
|
||||
lock_spend_tx_vsize = ci_from.xmr_swap_b_lock_spend_tx_vsize() if reverse_bid else ci_from.xmr_swap_a_lock_spend_tx_vsize()
|
||||
lock_spend_tx_fee = ci_from.make_int(from_fee_rate * lock_spend_tx_vsize / 1000, r=1)
|
||||
data['amt_from_lock_spend_tx_fee'] = ci_from.format_amount(lock_spend_tx_fee // ci_from.COIN())
|
||||
|
||||
if offer.was_sent:
|
||||
try:
|
||||
@@ -633,7 +634,9 @@ def page_offer(self, url_split, post_string):
|
||||
formatted_bids = []
|
||||
amt_swapped = 0
|
||||
for b in bids:
|
||||
amt_swapped += b[4]
|
||||
amount_from = b[4]
|
||||
rate = b[10]
|
||||
amt_swapped += amount_from
|
||||
formatted_bids.append((b[2].hex(), ci_from.format_amount(amount_from), strBidState(b[5]), ci_to.format_amount(rate), b[11]))
|
||||
data['amt_swapped'] = ci_from.format_amount(amt_swapped)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user