basicswap_miserver/basicswap/interface_xmr.py
tecnovert deb71856e8
Poll chainstates.
Litecoin download link changed.
Fix fee comparison tx weight difference.
Remove format8.
New stalled for test bid state.
Moved sequence code to coin interfaces.
Display estimated time lock refund tx will be valid.
2021-02-03 16:01:27 +02:00

417 lines
15 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2021 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import time
import logging
import basicswap.contrib.ed25519_fast as edf
import basicswap.ed25519_fast_util as edu
import basicswap.util_xmr as xmr_util
from coincurve.ed25519 import (
ed25519_get_pubkey,
ed25519_scalar_add,
ed25519_add)
from coincurve.keys import PrivateKey
from coincurve.dleag import (
verify_ed25519_point,
dleag_proof_len,
dleag_verify,
dleag_prove)
from .util import (
dumpj,
make_int,
format_amount)
from .rpc_xmr import (
make_xmr_rpc_func,
make_xmr_rpc2_func,
make_xmr_wallet_rpc_func)
from .ecc_util import (
b2i, b2h)
from .chainparams import CoinInterface, Coins
XMR_COIN = 10 ** 12
class XMRInterface(CoinInterface):
@staticmethod
def coin_type():
return Coins.XMR
@staticmethod
def COIN():
return XMR_COIN
@staticmethod
def exp():
return 12
@staticmethod
def nbk():
return 32
@staticmethod
def nbK(): # No. of bytes requires to encode a public key
return 32
def __init__(self, coin_settings, network, swap_client=None):
super().__init__()
self.rpc_cb = make_xmr_rpc_func(coin_settings['rpcport'], host=coin_settings.get('rpchost', '127.0.0.1'))
self.rpc_cb2 = make_xmr_rpc2_func(coin_settings['rpcport'], host=coin_settings.get('rpchost', '127.0.0.1')) # non-json endpoint
self.rpc_wallet_cb = make_xmr_wallet_rpc_func(coin_settings['walletrpcport'], coin_settings['walletrpcauth'])
self._network = network
self.blocks_confirmed = coin_settings['blocks_confirmed']
self._restore_height = coin_settings.get('restore_height', 0)
self.setFeePriority(coin_settings.get('fee_priority', 0))
self._sc = swap_client
self._log = self._sc.log if self._sc and self._sc.log else logging
def setFeePriority(self, new_priority):
assert(new_priority >= 0 and new_priority < 4), 'Invalid fee_priority value'
self._fee_priority = new_priority
def setWalletFilename(self, wallet_filename):
self._wallet_filename = wallet_filename
def initialiseWallet(self, key_view, key_spend, restore_height=None):
try:
self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename})
# TODO: Check address
return # Wallet exists
except Exception as e:
pass
Kbv = self.getPubkey(key_view)
Kbs = self.getPubkey(key_spend)
address_b58 = xmr_util.encode_address(Kbv, Kbs)
params = {
'filename': self._wallet_filename,
'address': address_b58,
'viewkey': b2h(key_view[::-1]),
'spendkey': b2h(key_spend[::-1]),
'restore_height': self._restore_height,
}
rv = self.rpc_wallet_cb('generate_from_keys', params)
self._log.info('generate_from_keys %s', dumpj(rv))
self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename})
def ensureWalletExists(self):
self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename})
def testDaemonRPC(self):
self.rpc_wallet_cb('get_languages')
def getDaemonVersion(self):
return self.rpc_wallet_cb('get_version')['version']
def getBlockchainInfo(self):
rv = {}
# get_block_count returns "Internal error" if bootstrap-daemon is active
# rv['blocks'] = self.rpc_cb('get_block_count')['count']
rv['blocks'] = self.rpc_cb2('get_height')['height']
rv['verificationprogress'] = 0 # TODO
return rv
def getChainHeight(self):
# get_block_count returns "Internal error" if bootstrap-daemon is active
# return self.rpc_cb('get_info')['height']
# return self.rpc_cb('get_block_count')['count']
return self.rpc_cb2('get_height')['height']
def getWalletInfo(self):
self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename})
rv = {}
self.rpc_wallet_cb('refresh')
balance_info = self.rpc_wallet_cb('get_balance')
rv['balance'] = format_amount(balance_info['unlocked_balance'], XMRInterface.exp())
rv['unconfirmed_balance'] = format_amount(balance_info['balance'] - balance_info['unlocked_balance'], XMRInterface.exp())
return rv
def getMainWalletAddress(self):
self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename})
return self.rpc_wallet_cb('get_address')['address']
def getNewAddress(self, placeholder):
self._log.warning('TODO - subaddress?')
self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename})
return self.rpc_wallet_cb('get_address')['address']
def get_fee_rate(self, conf_target=2):
self._log.warning('TODO - estimate fee rate?')
return 0.0, 'unused'
def isValidKey(self, key_bytes):
ki = b2i(key_bytes)
return ki < edf.l and ki > 8
def getNewSecretKey(self):
return edu.get_secret()
def pubkey(self, key):
return edf.scalarmult_B(key)
def encodePubkey(self, pk):
return edu.encodepoint(pk)
def decodePubkey(self, pke):
return edf.decodepoint(pke)
def getPubkey(self, privkey):
return ed25519_get_pubkey(privkey)
def getAddressFromKeys(self, key_view, key_spend):
pk_view = self.getPubkey(key_view)
pk_spend = self.getPubkey(key_spend)
return xmr_util.encode_address(pk_view, pk_spend)
def verifyKey(self, k):
i = b2i(k)
return(i < edf.l and i > 8)
def verifyPubkey(self, pubkey_bytes):
# Calls ed25519_decode_check_point() in secp256k1
# Checks for small order
return verify_ed25519_point(pubkey_bytes)
def proveDLEAG(self, key):
privkey = PrivateKey(key)
return dleag_prove(privkey)
def verifyDLEAG(self, dleag_bytes):
return dleag_verify(dleag_bytes)
def lengthDLEAG(self):
return dleag_proof_len()
def decodeKey(self, k):
i = b2i(k)
assert(i < edf.l and i > 8)
return i
def sumKeys(self, ka, kb):
return ed25519_scalar_add(ka, kb)
def sumPubkeys(self, Ka, Kb):
return ed25519_add(Ka, Kb)
def publishBLockTx(self, Kbv, Kbs, output_amount, feerate):
self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename})
shared_addr = xmr_util.encode_address(Kbv, Kbs)
params = {'destinations': [{'amount': output_amount, 'address': shared_addr}]}
if self._fee_priority > 0:
params['priority'] = self._fee_priority
rv = self.rpc_wallet_cb('transfer', params)
self._log.info('publishBLockTx %s to address_b58 %s', rv['tx_hash'], shared_addr)
tx_hash = bytes.fromhex(rv['tx_hash'])
# Debug
for i in range(10):
params = {'out': True, 'pending': True, 'failed': True, 'pool': True, }
rv = self.rpc_wallet_cb('get_transfers', params)
self._log.info('[rm] get_transfers {}'.format(dumpj(rv)))
if 'pending' not in rv:
break
time.sleep(1)
return tx_hash
def findTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height):
Kbv = self.getPubkey(kbv)
address_b58 = xmr_util.encode_address(Kbv, Kbs)
try:
self.rpc_wallet_cb('close_wallet')
except Exception as e:
self._log.warning('close_wallet failed %s', str(e))
kbv_le = kbv[::-1]
params = {
'restore_height': restore_height,
'filename': address_b58,
'address': address_b58,
'viewkey': b2h(kbv_le),
}
try:
rv = self.rpc_wallet_cb('open_wallet', {'filename': address_b58})
except Exception as e:
rv = self.rpc_wallet_cb('generate_from_keys', params)
self._log.info('generate_from_keys %s', dumpj(rv))
rv = self.rpc_wallet_cb('open_wallet', {'filename': address_b58})
rv = self.rpc_wallet_cb('refresh', timeout=600)
'''
# Debug
try:
current_height = self.rpc_wallet_cb('get_height')['height']
self._log.info('findTxB XMR current_height %d\nAddress: %s', current_height, address_b58)
except Exception as e:
self._log.info('rpc_cb failed %s', str(e))
current_height = None # If the transfer is available it will be deep enough
# and (current_height is None or current_height - transfer['block_height'] > cb_block_confirmed):
'''
params = {'transfer_type': 'available'}
rv = self.rpc_wallet_cb('incoming_transfers', params)
if 'transfers' in rv:
for transfer in rv['transfers']:
if transfer['amount'] == cb_swap_value:
return {'txid': transfer['tx_hash'], 'amount': transfer['amount'], 'height': 0 if 'block_height' not in transfer else transfer['block_height']}
else:
self._log.warning('Incorrect amount detected for coin b lock txn: {}'.format(transfer['tx_hash']))
return -1
return None
def waitForLockTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height):
Kbv_enc = self.encodePubkey(self.pubkey(kbv))
address_b58 = xmr_util.encode_address(Kbv_enc, self.encodePubkey(Kbs))
try:
self.rpc_wallet_cb('close_wallet')
except Exception as e:
self._log.warning('close_wallet failed %s', str(e))
params = {
'filename': address_b58,
'address': address_b58,
'viewkey': b2h(kbv[::-1]),
'restore_height': restore_height,
}
self.rpc_wallet_cb('generate_from_keys', params)
self.rpc_wallet_cb('open_wallet', {'filename': address_b58})
# For a while after opening the wallet rpc cmds return empty data
num_tries = 40
for i in range(num_tries + 1):
try:
current_height = self.rpc_cb2('get_height')['height']
print('current_height', current_height)
except Exception as e:
self._log.warning('rpc_cb failed %s', str(e))
current_height = None # If the transfer is available it will be deep enough
# TODO: Make accepting current_height == None a user selectable option
# Or look for all transfers and check height
params = {'transfer_type': 'available'}
rv = self.rpc_wallet_cb('incoming_transfers', params)
print('rv', rv)
if 'transfers' in rv:
for transfer in rv['transfers']:
if transfer['amount'] == cb_swap_value \
and (current_height is None or current_height - transfer['block_height'] > cb_block_confirmed):
return True
# TODO: Is it necessary to check the address?
'''
rv = self.rpc_wallet_cb('get_balance')
print('get_balance', rv)
if 'per_subaddress' in rv:
for sub_addr in rv['per_subaddress']:
if sub_addr['address'] == address_b58:
'''
if i >= num_tries:
raise ValueError('Balance not confirming on node')
time.sleep(1)
return False
def findTxnByHash(self, txid):
self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename})
self.rpc_wallet_cb('refresh')
try:
current_height = self.rpc_cb2('get_height')['height']
self._log.info('findTxnByHash XMR current_height %d\nhash: %s', current_height, txid)
except Exception as e:
self._log.info('rpc_cb failed %s', str(e))
current_height = None # If the transfer is available it will be deep enough
params = {'transfer_type': 'available'}
rv = self.rpc_wallet_cb('incoming_transfers', params)
if 'transfers' in rv:
for transfer in rv['transfers']:
print('[rm] transfer', transfer)
if transfer['tx_hash'] == txid \
and (current_height is None or current_height - transfer['block_height'] > self.blocks_confirmed):
return {'txid': transfer['tx_hash'], 'amount': transfer['amount'], 'height': transfer['block_height']}
return None
def spendBLockTx(self, address_to, kbv, kbs, cb_swap_value, b_fee_rate, restore_height):
Kbv = self.getPubkey(kbv)
Kbs = self.getPubkey(kbs)
address_b58 = xmr_util.encode_address(Kbv, Kbs)
try:
self.rpc_wallet_cb('close_wallet')
except Exception as e:
self._log.warning('close_wallet failed %s', str(e))
wallet_filename = address_b58 + '_spend'
params = {
'filename': wallet_filename,
'address': address_b58,
'viewkey': b2h(kbv[::-1]),
'spendkey': b2h(kbs[::-1]),
'restore_height': restore_height,
}
try:
self.rpc_wallet_cb('open_wallet', {'filename': wallet_filename})
except Exception as e:
rv = self.rpc_wallet_cb('generate_from_keys', params)
self._log.info('generate_from_keys %s', dumpj(rv))
self.rpc_wallet_cb('open_wallet', {'filename': wallet_filename})
# For a while after opening the wallet rpc cmds return empty data
for i in range(10):
rv = self.rpc_wallet_cb('get_balance')
print('get_balance', rv)
if rv['balance'] >= cb_swap_value:
break
time.sleep(1 + i)
if rv['balance'] < cb_swap_value:
self._log.error('wallet {} balance {}, expected {}'.format(wallet_filename, rv['balance'], cb_swap_value))
raise ValueError('Invalid balance')
if rv['unlocked_balance'] < cb_swap_value:
self._log.error('wallet {} balance {}, expected {}, blocks_to_unlock {}'.format(wallet_filename, rv['unlocked_balance'], cb_swap_value, rv['blocks_to_unlock']))
raise ValueError('Invalid unlocked_balance')
params = {'address': address_to}
if self._fee_priority > 0:
params['priority'] = self._fee_priority
rv = self.rpc_wallet_cb('sweep_all', params)
print('sweep_all', rv)
return bytes.fromhex(rv['tx_hash_list'][0])
def withdrawCoin(self, value, addr_to, subfee):
self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename})
value_sats = make_int(value, self.exp())
params = {'destinations': [{'amount': value_sats, 'address': addr_to}]}
if self._fee_priority > 0:
params['priority'] = self._fee_priority
rv = self.rpc_wallet_cb('transfer', params)
return rv['tx_hash']