mirror of
https://github.com/basicswap/basicswap.git
synced 2025-11-06 02:38:11 +01:00
refactor: Move all coin interfaces to a dir
This commit is contained in:
0
basicswap/interface/__init__.py
Normal file
0
basicswap/interface/__init__.py
Normal file
1159
basicswap/interface/btc.py
Normal file
1159
basicswap/interface/btc.py
Normal file
File diff suppressed because it is too large
Load Diff
15
basicswap/interface/ltc.py
Normal file
15
basicswap/interface/ltc.py
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2020 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
from .btc import BTCInterface
|
||||
from basicswap.chainparams import Coins
|
||||
|
||||
|
||||
class LTCInterface(BTCInterface):
|
||||
@staticmethod
|
||||
def coin_type():
|
||||
return Coins.LTC
|
||||
42
basicswap/interface/nmc.py
Normal file
42
basicswap/interface/nmc.py
Normal file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2020-2022 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
from .btc import BTCInterface
|
||||
from basicswap.chainparams import Coins
|
||||
from basicswap.util import (
|
||||
make_int,
|
||||
)
|
||||
|
||||
|
||||
class NMCInterface(BTCInterface):
|
||||
@staticmethod
|
||||
def coin_type():
|
||||
return Coins.NMC
|
||||
|
||||
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index=False):
|
||||
self._log.debug('[rm] scantxoutset start') # scantxoutset is slow
|
||||
ro = self.rpc_callback('scantxoutset', ['start', ['addr({})'.format(dest_address)]]) # TODO: Use combo(address) where possible
|
||||
self._log.debug('[rm] scantxoutset end')
|
||||
return_txid = True if txid is None else False
|
||||
for o in ro['unspents']:
|
||||
if txid and o['txid'] != txid.hex():
|
||||
continue
|
||||
# Verify amount
|
||||
if make_int(o['amount']) != int(bid_amount):
|
||||
self._log.warning('Found output to lock tx address of incorrect value: %s, %s', str(o['amount']), o['txid'])
|
||||
continue
|
||||
|
||||
rv = {
|
||||
'depth': 0,
|
||||
'height': o['height']}
|
||||
if o['height'] > 0:
|
||||
rv['depth'] = ro['height'] - o['height']
|
||||
if find_index:
|
||||
rv['index'] = o['vout']
|
||||
if return_txid:
|
||||
rv['txid'] = o['txid']
|
||||
return rv
|
||||
743
basicswap/interface/part.py
Normal file
743
basicswap/interface/part.py
Normal file
@@ -0,0 +1,743 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2020-2022 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import hashlib
|
||||
from enum import IntEnum
|
||||
|
||||
from basicswap.contrib.test_framework.messages import (
|
||||
CTxOutPart,
|
||||
)
|
||||
from basicswap.contrib.test_framework.script import (
|
||||
CScript,
|
||||
OP_0,
|
||||
OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG
|
||||
)
|
||||
from basicswap.util import (
|
||||
i2b,
|
||||
ensure,
|
||||
make_int,
|
||||
TemporaryError,
|
||||
)
|
||||
from basicswap.util.script import (
|
||||
getP2WSH,
|
||||
getCompactSizeLen,
|
||||
getWitnessElementLen,
|
||||
)
|
||||
from basicswap.util.address import (
|
||||
toWIF,
|
||||
encodeStealthAddress)
|
||||
from basicswap.chainparams import Coins, chainparams
|
||||
from .btc import BTCInterface
|
||||
|
||||
|
||||
class BalanceTypes(IntEnum):
|
||||
PLAIN = 1
|
||||
BLIND = 2
|
||||
ANON = 3
|
||||
|
||||
|
||||
class PARTInterface(BTCInterface):
|
||||
@staticmethod
|
||||
def coin_type():
|
||||
return Coins.PART
|
||||
|
||||
@staticmethod
|
||||
def balance_type():
|
||||
return BalanceTypes.PLAIN
|
||||
|
||||
@staticmethod
|
||||
def witnessScaleFactor() -> int:
|
||||
return 2
|
||||
|
||||
@staticmethod
|
||||
def txVersion() -> int:
|
||||
return 0xa0
|
||||
|
||||
@staticmethod
|
||||
def xmr_swap_alock_spend_tx_vsize() -> int:
|
||||
return 213
|
||||
|
||||
@staticmethod
|
||||
def txoType():
|
||||
return CTxOutPart
|
||||
|
||||
def __init__(self, coin_settings, network, swap_client=None):
|
||||
super().__init__(coin_settings, network, swap_client)
|
||||
self.setAnonTxRingSize(int(coin_settings.get('anon_tx_ring_size', 12)))
|
||||
|
||||
def setAnonTxRingSize(self, value):
|
||||
ensure(value >= 3 and value < 33, 'Invalid anon_tx_ring_size value')
|
||||
self._anon_tx_ring_size = value
|
||||
|
||||
def knownWalletSeed(self):
|
||||
# TODO: Double check
|
||||
return True
|
||||
|
||||
def getNewAddress(self, use_segwit, label='swap_receive'):
|
||||
return self.rpc_callback('getnewaddress', [label])
|
||||
|
||||
def getNewStealthAddress(self, label='swap_stealth'):
|
||||
return self.rpc_callback('getnewstealthaddress', [label])
|
||||
|
||||
def haveSpentIndex(self):
|
||||
version = self.getDaemonVersion()
|
||||
index_info = self.rpc_callback('getinsightinfo' if int(str(version)[:2]) > 19 else 'getindexinfo')
|
||||
return index_info['spentindex']
|
||||
|
||||
def initialiseWallet(self, key):
|
||||
raise ValueError('TODO')
|
||||
|
||||
def withdrawCoin(self, value, addr_to, subfee):
|
||||
params = [addr_to, value, '', '', subfee, '', True, self._conf_target]
|
||||
return self.rpc_callback('sendtoaddress', params)
|
||||
|
||||
def sendTypeTo(self, type_from, type_to, value, addr_to, subfee):
|
||||
params = [type_from, type_to,
|
||||
[{'address': addr_to, 'amount': value, 'subfee': subfee}, ],
|
||||
'', '', self._anon_tx_ring_size, 1, False,
|
||||
{'conf_target': self._conf_target}]
|
||||
return self.rpc_callback('sendtypeto', params)
|
||||
|
||||
def getScriptForPubkeyHash(self, pkh):
|
||||
return CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG])
|
||||
|
||||
def formatStealthAddress(self, scan_pubkey, spend_pubkey):
|
||||
prefix_byte = chainparams[self.coin_type()][self._network]['stealth_key_prefix']
|
||||
|
||||
return encodeStealthAddress(prefix_byte, scan_pubkey, spend_pubkey)
|
||||
|
||||
def getWitnessStackSerialisedLength(self, witness_stack):
|
||||
length = getCompactSizeLen(len(witness_stack))
|
||||
for e in witness_stack:
|
||||
length += getWitnessElementLen(len(e) // 2) # hex -> bytes
|
||||
return length
|
||||
|
||||
def getWalletRestoreHeight(self):
|
||||
start_time = self.rpc_callback('getwalletinfo')['keypoololdest']
|
||||
|
||||
blockchaininfo = self.rpc_callback('getblockchaininfo')
|
||||
best_block = blockchaininfo['bestblockhash']
|
||||
|
||||
chain_synced = round(blockchaininfo['verificationprogress'], 3)
|
||||
if chain_synced < 1.0:
|
||||
raise ValueError('{} chain isn\'t synced.'.format(self.coin_name()))
|
||||
|
||||
self._log.debug('Finding block at time: {}'.format(start_time))
|
||||
block_hash = self.rpc_callback('getblockhashafter', [start_time])
|
||||
block_header = self.rpc_callback('getblockheader', [block_hash])
|
||||
return block_header['height']
|
||||
|
||||
|
||||
class PARTInterfaceBlind(PARTInterface):
|
||||
@staticmethod
|
||||
def balance_type():
|
||||
return BalanceTypes.BLIND
|
||||
|
||||
def coin_name(self):
|
||||
return super().coin_name() + ' Blind'
|
||||
|
||||
def getScriptLockTxNonce(self, data):
|
||||
return hashlib.sha256(data + bytes('locktx', 'utf-8')).digest()
|
||||
|
||||
def getScriptLockRefundTxNonce(self, data):
|
||||
return hashlib.sha256(data + bytes('lockrefundtx', 'utf-8')).digest()
|
||||
|
||||
def findOutputByNonce(self, tx_obj, nonce):
|
||||
blinded_info = None
|
||||
output_n = None
|
||||
for txo in tx_obj['vout']:
|
||||
if txo['type'] != 'blind':
|
||||
continue
|
||||
try:
|
||||
blinded_info = self.rpc_callback('rewindrangeproof', [txo['rangeproof'], txo['valueCommitment'], nonce.hex()])
|
||||
output_n = txo['n']
|
||||
|
||||
self.rpc_callback('rewindrangeproof', [txo['rangeproof'], txo['valueCommitment'], nonce.hex()])
|
||||
break
|
||||
except Exception as e:
|
||||
self._log.debug('Searching for locked output: {}'.format(str(e)))
|
||||
continue
|
||||
# Should not be possible for commitment not to match
|
||||
v = self.rpc_callback('verifycommitment', [txo['valueCommitment'], blinded_info['blind'], blinded_info['amount']])
|
||||
ensure(v['result'] is True, 'verifycommitment failed')
|
||||
return output_n, blinded_info
|
||||
|
||||
def createScriptLockTx(self, value, Kal, Kaf, vkbv):
|
||||
script = self.genScriptLockTxScript(Kal, Kaf)
|
||||
|
||||
# Nonce is derived from vkbv, ephemeral_key isn't used
|
||||
ephemeral_key = i2b(self.getNewSecretKey())
|
||||
ephemeral_pubkey = self.getPubkey(ephemeral_key)
|
||||
assert (len(ephemeral_pubkey) == 33)
|
||||
nonce = self.getScriptLockTxNonce(vkbv)
|
||||
p2wsh_addr = self.encode_p2wsh(getP2WSH(script))
|
||||
inputs = []
|
||||
outputs = [{'type': 'blind', 'amount': self.format_amount(value), 'address': p2wsh_addr, 'nonce': nonce.hex(), 'data': ephemeral_pubkey.hex()}]
|
||||
params = [inputs, outputs]
|
||||
rv = self.rpc_callback('createrawparttransaction', params)
|
||||
|
||||
tx_bytes = bytes.fromhex(rv['hex'])
|
||||
return tx_bytes, script
|
||||
|
||||
def fundScriptLockTx(self, tx_bytes, feerate, vkbv):
|
||||
feerate_str = self.format_amount(feerate)
|
||||
# TODO: unlock unspents if bid cancelled
|
||||
|
||||
tx_hex = tx_bytes.hex()
|
||||
nonce = self.getScriptLockTxNonce(vkbv)
|
||||
|
||||
tx_obj = self.rpc_callback('decoderawtransaction', [tx_hex])
|
||||
|
||||
assert (len(tx_obj['vout']) == 1)
|
||||
txo = tx_obj['vout'][0]
|
||||
blinded_info = self.rpc_callback('rewindrangeproof', [txo['rangeproof'], txo['valueCommitment'], nonce.hex()])
|
||||
|
||||
outputs_info = {0: {'value': blinded_info['amount'], 'blind': blinded_info['blind'], 'nonce': nonce.hex()}}
|
||||
|
||||
options = {
|
||||
'lockUnspents': True,
|
||||
'feeRate': feerate_str,
|
||||
}
|
||||
rv = self.rpc_callback('fundrawtransactionfrom', ['blind', tx_hex, {}, outputs_info, options])
|
||||
return bytes.fromhex(rv['hex'])
|
||||
|
||||
def createScriptLockRefundTx(self, tx_lock_bytes, script_lock, Kal, Kaf, lock1_value, csv_val, tx_fee_rate, vkbv):
|
||||
lock_tx_obj = self.rpc_callback('decoderawtransaction', [tx_lock_bytes.hex()])
|
||||
assert (self.getTxid(tx_lock_bytes).hex() == lock_tx_obj['txid'])
|
||||
# Nonce is derived from vkbv, ephemeral_key isn't used
|
||||
ephemeral_key = i2b(self.getNewSecretKey())
|
||||
ephemeral_pubkey = self.getPubkey(ephemeral_key)
|
||||
assert (len(ephemeral_pubkey) == 33)
|
||||
nonce = self.getScriptLockTxNonce(vkbv)
|
||||
output_nonce = self.getScriptLockRefundTxNonce(vkbv)
|
||||
|
||||
# Find the output of the lock tx to spend
|
||||
spend_n, input_blinded_info = self.findOutputByNonce(lock_tx_obj, nonce)
|
||||
ensure(spend_n is not None, 'Output not found in tx')
|
||||
|
||||
locked_coin = input_blinded_info['amount']
|
||||
tx_lock_id = lock_tx_obj['txid']
|
||||
refund_script = self.genScriptLockRefundTxScript(Kal, Kaf, csv_val)
|
||||
p2wsh_addr = self.encode_p2wsh(getP2WSH(refund_script))
|
||||
|
||||
inputs = [{'txid': tx_lock_id, 'vout': spend_n, 'sequence': lock1_value, 'blindingfactor': input_blinded_info['blind']}]
|
||||
outputs = [{'type': 'blind', 'amount': locked_coin, 'address': p2wsh_addr, 'nonce': output_nonce.hex(), 'data': ephemeral_pubkey.hex()}]
|
||||
params = [inputs, outputs]
|
||||
rv = self.rpc_callback('createrawparttransaction', params)
|
||||
lock_refund_tx_hex = rv['hex']
|
||||
|
||||
# Set dummy witness data for fee estimation
|
||||
dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock)
|
||||
|
||||
# Use a junk change pubkey to avoid adding unused keys to the wallet
|
||||
zero_change_key = i2b(self.getNewSecretKey())
|
||||
zero_change_pubkey = self.getPubkey(zero_change_key)
|
||||
inputs_info = {'0': {'value': input_blinded_info['amount'], 'blind': input_blinded_info['blind'], 'witnessstack': dummy_witness_stack}}
|
||||
outputs_info = rv['amounts']
|
||||
options = {
|
||||
'changepubkey': zero_change_pubkey.hex(),
|
||||
'feeRate': self.format_amount(tx_fee_rate),
|
||||
'subtractFeeFromOutputs': [0, ]
|
||||
}
|
||||
rv = self.rpc_callback('fundrawtransactionfrom', ['blind', lock_refund_tx_hex, inputs_info, outputs_info, options])
|
||||
lock_refund_tx_hex = rv['hex']
|
||||
|
||||
for vout, txo in rv['output_amounts'].items():
|
||||
if txo['value'] > 0:
|
||||
refunded_value = txo['value']
|
||||
|
||||
return bytes.fromhex(lock_refund_tx_hex), refund_script, refunded_value
|
||||
|
||||
def createScriptLockRefundSpendTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_refund_to, tx_fee_rate, vkbv):
|
||||
# Returns the coinA locked coin to the leader
|
||||
# The follower will sign the multisig path with a signature encumbered by the leader's coinB spend pubkey
|
||||
# If the leader publishes the decrypted signature the leader's coinB spend privatekey will be revealed to the follower
|
||||
|
||||
lock_refund_tx_obj = self.rpc_callback('decoderawtransaction', [tx_lock_refund_bytes.hex()])
|
||||
# Nonce is derived from vkbv
|
||||
nonce = self.getScriptLockRefundTxNonce(vkbv)
|
||||
|
||||
# Find the output of the lock refund tx to spend
|
||||
spend_n, input_blinded_info = self.findOutputByNonce(lock_refund_tx_obj, nonce)
|
||||
ensure(spend_n is not None, 'Output not found in tx')
|
||||
|
||||
tx_lock_refund_id = lock_refund_tx_obj['txid']
|
||||
addr_out = self.pkh_to_address(pkh_refund_to)
|
||||
addr_info = self.rpc_callback('getaddressinfo', [addr_out])
|
||||
output_pubkey_hex = addr_info['pubkey']
|
||||
|
||||
# Follower won't be able to decode output to check amount, shouldn't matter as fee is public and output is to leader, sum has to balance
|
||||
|
||||
inputs = [{'txid': tx_lock_refund_id, 'vout': spend_n, 'sequence': 0, 'blindingfactor': input_blinded_info['blind']}]
|
||||
outputs = [{'type': 'blind', 'amount': input_blinded_info['amount'], 'address': addr_out, 'pubkey': output_pubkey_hex}]
|
||||
params = [inputs, outputs]
|
||||
rv = self.rpc_callback('createrawparttransaction', params)
|
||||
lock_refund_spend_tx_hex = rv['hex']
|
||||
|
||||
# Set dummy witness data for fee estimation
|
||||
dummy_witness_stack = self.getScriptLockRefundSpendTxDummyWitness(script_lock_refund)
|
||||
|
||||
# Use a junk change pubkey to avoid adding unused keys to the wallet
|
||||
zero_change_key = i2b(self.getNewSecretKey())
|
||||
zero_change_pubkey = self.getPubkey(zero_change_key)
|
||||
inputs_info = {'0': {'value': input_blinded_info['amount'], 'blind': input_blinded_info['blind'], 'witnessstack': dummy_witness_stack}}
|
||||
outputs_info = rv['amounts']
|
||||
options = {
|
||||
'changepubkey': zero_change_pubkey.hex(),
|
||||
'feeRate': self.format_amount(tx_fee_rate),
|
||||
'subtractFeeFromOutputs': [0, ]
|
||||
}
|
||||
|
||||
rv = self.rpc_callback('fundrawtransactionfrom', ['blind', lock_refund_spend_tx_hex, inputs_info, outputs_info, options])
|
||||
lock_refund_spend_tx_hex = rv['hex']
|
||||
|
||||
return bytes.fromhex(lock_refund_spend_tx_hex)
|
||||
|
||||
def verifyLockTx(self, tx_bytes, script_out,
|
||||
swap_value,
|
||||
Kal, Kaf,
|
||||
feerate,
|
||||
check_lock_tx_inputs, vkbv):
|
||||
lock_tx_obj = self.rpc_callback('decoderawtransaction', [tx_bytes.hex()])
|
||||
lock_txid_hex = lock_tx_obj['txid']
|
||||
self._log.info('Verifying lock tx: {}.'.format(lock_txid_hex))
|
||||
|
||||
ensure(lock_tx_obj['version'] == self.txVersion(), 'Bad version')
|
||||
ensure(lock_tx_obj['locktime'] == 0, 'Bad nLockTime')
|
||||
|
||||
# Find the output of the lock tx to verify
|
||||
nonce = self.getScriptLockTxNonce(vkbv)
|
||||
lock_output_n, blinded_info = self.findOutputByNonce(lock_tx_obj, nonce)
|
||||
ensure(lock_output_n is not None, 'Output not found in tx')
|
||||
|
||||
# Check value
|
||||
locked_txo_value = make_int(blinded_info['amount'])
|
||||
ensure(locked_txo_value == swap_value, 'Bad locked value')
|
||||
|
||||
# Check script
|
||||
lock_txo_scriptpk = bytes.fromhex(lock_tx_obj['vout'][lock_output_n]['scriptPubKey']['hex'])
|
||||
script_pk = CScript([OP_0, hashlib.sha256(script_out).digest()])
|
||||
ensure(lock_txo_scriptpk == script_pk, 'Bad output script')
|
||||
A, B = self.extractScriptLockScriptValues(script_out)
|
||||
ensure(A == Kal, 'Bad script leader pubkey')
|
||||
ensure(B == Kaf, 'Bad script follower pubkey')
|
||||
|
||||
# TODO: Check that inputs are unspent, rangeproofs and commitments sum
|
||||
# Verify fee rate
|
||||
vsize = lock_tx_obj['vsize']
|
||||
fee_paid = make_int(lock_tx_obj['vout'][0]['ct_fee'])
|
||||
|
||||
fee_rate_paid = fee_paid * 1000 // vsize
|
||||
|
||||
self._log.info('tx amount, vsize, feerate: %ld, %ld, %ld', locked_txo_value, vsize, fee_rate_paid)
|
||||
|
||||
if not self.compareFeeRates(fee_rate_paid, feerate):
|
||||
self._log.warning('feerate paid doesn\'t match expected: %ld, %ld', fee_rate_paid, feerate)
|
||||
# TODO: Display warning to user
|
||||
|
||||
return bytes.fromhex(lock_txid_hex), lock_output_n
|
||||
|
||||
def verifyLockRefundTx(self, tx_bytes, lock_tx_bytes, script_out,
|
||||
prevout_id, prevout_n, prevout_seq, prevout_script,
|
||||
Kal, Kaf, csv_val_expect, swap_value, feerate, vkbv):
|
||||
lock_refund_tx_obj = self.rpc_callback('decoderawtransaction', [tx_bytes.hex()])
|
||||
lock_refund_txid_hex = lock_refund_tx_obj['txid']
|
||||
self._log.info('Verifying lock refund tx: {}.'.format(lock_refund_txid_hex))
|
||||
|
||||
ensure(lock_refund_tx_obj['version'] == self.txVersion(), 'Bad version')
|
||||
ensure(lock_refund_tx_obj['locktime'] == 0, 'Bad nLockTime')
|
||||
ensure(len(lock_refund_tx_obj['vin']) == 1, 'tx doesn\'t have one input')
|
||||
|
||||
txin = lock_refund_tx_obj['vin'][0]
|
||||
ensure(txin['sequence'] == prevout_seq, 'Bad input nSequence')
|
||||
ensure(txin['scriptSig']['hex'] == '', 'Input scriptsig not empty')
|
||||
ensure(txin['txid'] == prevout_id.hex() and txin['vout'] == prevout_n, 'Input prevout mismatch')
|
||||
|
||||
ensure(len(lock_refund_tx_obj['vout']) == 3, 'tx doesn\'t have three outputs')
|
||||
|
||||
# Find the output of the lock refund tx to verify
|
||||
nonce = self.getScriptLockRefundTxNonce(vkbv)
|
||||
lock_refund_output_n, blinded_info = self.findOutputByNonce(lock_refund_tx_obj, nonce)
|
||||
ensure(lock_refund_output_n is not None, 'Output not found in tx')
|
||||
|
||||
lock_refund_txo_value = make_int(blinded_info['amount'])
|
||||
|
||||
# Check script
|
||||
lock_refund_txo_scriptpk = bytes.fromhex(lock_refund_tx_obj['vout'][lock_refund_output_n]['scriptPubKey']['hex'])
|
||||
script_pk = CScript([OP_0, hashlib.sha256(script_out).digest()])
|
||||
ensure(lock_refund_txo_scriptpk == script_pk, 'Bad output script')
|
||||
A, B, csv_val, C = self.extractScriptLockRefundScriptValues(script_out)
|
||||
ensure(A == Kal, 'Bad script pubkey')
|
||||
ensure(B == Kaf, 'Bad script pubkey')
|
||||
ensure(csv_val == csv_val_expect, 'Bad script csv value')
|
||||
ensure(C == Kaf, 'Bad script pubkey')
|
||||
|
||||
# Check rangeproofs and commitments sum
|
||||
lock_tx_obj = self.rpc_callback('decoderawtransaction', [lock_tx_bytes.hex()])
|
||||
prevout = lock_tx_obj['vout'][prevout_n]
|
||||
prevtxns = [{'txid': prevout_id.hex(), 'vout': prevout_n, 'scriptPubKey': prevout['scriptPubKey']['hex'], 'amount_commitment': prevout['valueCommitment']}]
|
||||
rv = self.rpc_callback('verifyrawtransaction', [tx_bytes.hex(), prevtxns])
|
||||
ensure(rv['outputs_valid'] is True, 'Invalid outputs')
|
||||
ensure(rv['inputs_valid'] is True, 'Invalid inputs')
|
||||
|
||||
# Check value
|
||||
fee_paid = make_int(lock_refund_tx_obj['vout'][0]['ct_fee'])
|
||||
ensure(swap_value - lock_refund_txo_value == fee_paid, 'Bad output value')
|
||||
|
||||
# Check fee rate
|
||||
dummy_witness_stack = self.getScriptLockTxDummyWitness(prevout_script)
|
||||
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
|
||||
vsize = self.getTxVSize(self.loadTx(tx_bytes), add_witness_bytes=witness_bytes)
|
||||
fee_rate_paid = fee_paid * 1000 // vsize
|
||||
self._log.info('vsize, feerate: %ld, %ld', vsize, fee_rate_paid)
|
||||
|
||||
ensure(self.compareFeeRates(fee_rate_paid, feerate), 'Bad fee rate, expected: {}'.format(feerate))
|
||||
|
||||
return bytes.fromhex(lock_refund_txid_hex), lock_refund_txo_value, lock_refund_output_n
|
||||
|
||||
def verifyLockRefundSpendTx(self, tx_bytes, lock_refund_tx_bytes,
|
||||
lock_refund_tx_id, prevout_script,
|
||||
Kal,
|
||||
prevout_n, prevout_value, feerate, vkbv):
|
||||
lock_refund_spend_tx_obj = self.rpc_callback('decoderawtransaction', [tx_bytes.hex()])
|
||||
lock_refund_spend_txid_hex = lock_refund_spend_tx_obj['txid']
|
||||
self._log.info('Verifying lock refund spend tx: {}.'.format(lock_refund_spend_txid_hex))
|
||||
|
||||
ensure(lock_refund_spend_tx_obj['version'] == self.txVersion(), 'Bad version')
|
||||
ensure(lock_refund_spend_tx_obj['locktime'] == 0, 'Bad nLockTime')
|
||||
ensure(len(lock_refund_spend_tx_obj['vin']) == 1, 'tx doesn\'t have one input')
|
||||
|
||||
txin = lock_refund_spend_tx_obj['vin'][0]
|
||||
ensure(txin['sequence'] == 0, 'Bad input nSequence')
|
||||
ensure(txin['scriptSig']['hex'] == '', 'Input scriptsig not empty')
|
||||
ensure(txin['txid'] == lock_refund_tx_id.hex() and txin['vout'] == prevout_n, 'Input prevout mismatch')
|
||||
|
||||
ensure(len(lock_refund_spend_tx_obj['vout']) == 3, 'tx doesn\'t have three outputs')
|
||||
|
||||
# Leader picks output destinations
|
||||
# Follower is not concerned with them as they pay to leader
|
||||
|
||||
# Check rangeproofs and commitments sum
|
||||
lock_refund_tx_obj = self.rpc_callback('decoderawtransaction', [lock_refund_tx_bytes.hex()])
|
||||
prevout = lock_refund_tx_obj['vout'][prevout_n]
|
||||
prevtxns = [{'txid': lock_refund_tx_id.hex(), 'vout': prevout_n, 'scriptPubKey': prevout['scriptPubKey']['hex'], 'amount_commitment': prevout['valueCommitment']}]
|
||||
rv = self.rpc_callback('verifyrawtransaction', [tx_bytes.hex(), prevtxns])
|
||||
ensure(rv['outputs_valid'] is True, 'Invalid outputs')
|
||||
ensure(rv['inputs_valid'] is True, 'Invalid inputs')
|
||||
|
||||
# Check fee rate
|
||||
dummy_witness_stack = self.getScriptLockRefundSpendTxDummyWitness(prevout_script)
|
||||
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
|
||||
vsize = self.getTxVSize(self.loadTx(tx_bytes), add_witness_bytes=witness_bytes)
|
||||
fee_paid = make_int(lock_refund_spend_tx_obj['vout'][0]['ct_fee'])
|
||||
fee_rate_paid = fee_paid * 1000 // vsize
|
||||
ensure(self.compareFeeRates(fee_rate_paid, feerate), 'Bad fee rate, expected: {}'.format(feerate))
|
||||
|
||||
return True
|
||||
|
||||
def getLockTxSwapOutputValue(self, bid, xmr_swap):
|
||||
lock_tx_obj = self.rpc_callback('decoderawtransaction', [xmr_swap.a_lock_tx.hex()])
|
||||
nonce = self.getScriptLockTxNonce(xmr_swap.vkbv)
|
||||
output_n, _ = self.findOutputByNonce(lock_tx_obj, nonce)
|
||||
ensure(output_n is not None, 'Output not found in tx')
|
||||
return bytes.fromhex(lock_tx_obj['vout'][output_n]['valueCommitment'])
|
||||
|
||||
def getLockRefundTxSwapOutputValue(self, bid, xmr_swap):
|
||||
lock_refund_tx_obj = self.rpc_callback('decoderawtransaction', [xmr_swap.a_lock_refund_tx.hex()])
|
||||
nonce = self.getScriptLockRefundTxNonce(xmr_swap.vkbv)
|
||||
output_n, _ = self.findOutputByNonce(lock_refund_tx_obj, nonce)
|
||||
ensure(output_n is not None, 'Output not found in tx')
|
||||
return bytes.fromhex(lock_refund_tx_obj['vout'][output_n]['valueCommitment'])
|
||||
|
||||
def getLockRefundTxSwapOutput(self, xmr_swap):
|
||||
lock_refund_tx_obj = self.rpc_callback('decoderawtransaction', [xmr_swap.a_lock_refund_tx.hex()])
|
||||
nonce = self.getScriptLockRefundTxNonce(xmr_swap.vkbv)
|
||||
output_n, _ = self.findOutputByNonce(lock_refund_tx_obj, nonce)
|
||||
ensure(output_n is not None, 'Output not found in tx')
|
||||
return output_n
|
||||
|
||||
def createScriptLockSpendTx(self, tx_lock_bytes, script_lock, pk_dest, tx_fee_rate, vkbv):
|
||||
lock_tx_obj = self.rpc_callback('decoderawtransaction', [tx_lock_bytes.hex()])
|
||||
lock_txid_hex = lock_tx_obj['txid']
|
||||
|
||||
ensure(lock_tx_obj['version'] == self.txVersion(), 'Bad version')
|
||||
ensure(lock_tx_obj['locktime'] == 0, 'Bad nLockTime')
|
||||
|
||||
# Find the output of the lock tx to verify
|
||||
nonce = self.getScriptLockTxNonce(vkbv)
|
||||
spend_n, blinded_info = self.findOutputByNonce(lock_tx_obj, nonce)
|
||||
ensure(spend_n is not None, 'Output not found in tx')
|
||||
|
||||
addr_out = self.pubkey_to_address(pk_dest)
|
||||
|
||||
inputs = [{'txid': lock_txid_hex, 'vout': spend_n, 'sequence': 0, 'blindingfactor': blinded_info['blind']}]
|
||||
outputs = [{'type': 'blind', 'amount': blinded_info['amount'], 'address': addr_out, 'pubkey': pk_dest.hex()}]
|
||||
params = [inputs, outputs]
|
||||
rv = self.rpc_callback('createrawparttransaction', params)
|
||||
lock_spend_tx_hex = rv['hex']
|
||||
|
||||
# Set dummy witness data for fee estimation
|
||||
dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock)
|
||||
|
||||
# Use a junk change pubkey to avoid adding unused keys to the wallet
|
||||
zero_change_key = i2b(self.getNewSecretKey())
|
||||
zero_change_pubkey = self.getPubkey(zero_change_key)
|
||||
inputs_info = {'0': {'value': blinded_info['amount'], 'blind': blinded_info['blind'], 'witnessstack': dummy_witness_stack}}
|
||||
outputs_info = rv['amounts']
|
||||
options = {
|
||||
'changepubkey': zero_change_pubkey.hex(),
|
||||
'feeRate': self.format_amount(tx_fee_rate),
|
||||
'subtractFeeFromOutputs': [0, ]
|
||||
}
|
||||
|
||||
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'])
|
||||
actual_tx_fee_rate = pay_fee * 1000 // vsize
|
||||
self._log.info('createScriptLockSpendTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.',
|
||||
lock_spend_tx_obj['txid'], actual_tx_fee_rate, vsize, pay_fee)
|
||||
|
||||
return bytes.fromhex(lock_spend_tx_hex)
|
||||
|
||||
def verifyLockSpendTx(self, tx_bytes,
|
||||
lock_tx_bytes, lock_tx_script,
|
||||
a_pk_f, feerate, vkbv):
|
||||
lock_spend_tx_obj = self.rpc_callback('decoderawtransaction', [tx_bytes.hex()])
|
||||
lock_spend_txid_hex = lock_spend_tx_obj['txid']
|
||||
self._log.info('Verifying lock spend tx: {}.'.format(lock_spend_txid_hex))
|
||||
|
||||
ensure(lock_spend_tx_obj['version'] == self.txVersion(), 'Bad version')
|
||||
ensure(lock_spend_tx_obj['locktime'] == 0, 'Bad nLockTime')
|
||||
ensure(len(lock_spend_tx_obj['vin']) == 1, 'tx doesn\'t have one input')
|
||||
|
||||
lock_tx_obj = self.rpc_callback('decoderawtransaction', [lock_tx_bytes.hex()])
|
||||
lock_txid_hex = lock_tx_obj['txid']
|
||||
|
||||
# Find the output of the lock tx to verify
|
||||
nonce = self.getScriptLockTxNonce(vkbv)
|
||||
spend_n, input_blinded_info = self.findOutputByNonce(lock_tx_obj, nonce)
|
||||
ensure(spend_n is not None, 'Output not found in tx')
|
||||
|
||||
txin = lock_spend_tx_obj['vin'][0]
|
||||
ensure(txin['sequence'] == 0, 'Bad input nSequence')
|
||||
ensure(txin['scriptSig']['hex'] == '', 'Input scriptsig not empty')
|
||||
ensure(txin['txid'] == lock_txid_hex and txin['vout'] == spend_n, 'Input prevout mismatch')
|
||||
|
||||
ensure(len(lock_spend_tx_obj['vout']) == 3, 'tx doesn\'t have three outputs')
|
||||
|
||||
addr_out = self.pubkey_to_address(a_pk_f)
|
||||
privkey = self.rpc_callback('dumpprivkey', [addr_out])
|
||||
|
||||
# Find output:
|
||||
output_blinded_info = None
|
||||
output_n = None
|
||||
for txo in lock_spend_tx_obj['vout']:
|
||||
if txo['type'] != 'blind':
|
||||
continue
|
||||
try:
|
||||
output_blinded_info = self.rpc_callback('rewindrangeproof', [txo['rangeproof'], txo['valueCommitment'], privkey, txo['data_hex']])
|
||||
output_n = txo['n']
|
||||
break
|
||||
except Exception as e:
|
||||
self._log.debug('Searching for locked output: {}'.format(str(e)))
|
||||
pass
|
||||
ensure(output_n is not None, 'Output not found in tx')
|
||||
|
||||
# Commitment
|
||||
v = self.rpc_callback('verifycommitment', [lock_spend_tx_obj['vout'][output_n]['valueCommitment'], output_blinded_info['blind'], output_blinded_info['amount']])
|
||||
ensure(v['result'] is True, 'verifycommitment failed')
|
||||
|
||||
# Check rangeproofs and commitments sum
|
||||
prevout = lock_tx_obj['vout'][spend_n]
|
||||
prevtxns = [{'txid': lock_txid_hex, 'vout': spend_n, 'scriptPubKey': prevout['scriptPubKey']['hex'], 'amount_commitment': prevout['valueCommitment']}]
|
||||
rv = self.rpc_callback('verifyrawtransaction', [tx_bytes.hex(), prevtxns])
|
||||
ensure(rv['outputs_valid'] is True, 'Invalid outputs')
|
||||
ensure(rv['inputs_valid'] is True, 'Invalid inputs')
|
||||
|
||||
# Check amount
|
||||
fee_paid = make_int(lock_spend_tx_obj['vout'][0]['ct_fee'])
|
||||
amount_difference = make_int(input_blinded_info['amount']) - make_int(output_blinded_info['amount'])
|
||||
ensure(fee_paid == amount_difference, 'Invalid output amount')
|
||||
|
||||
# Check fee
|
||||
dummy_witness_stack = self.getScriptLockTxDummyWitness(lock_tx_script)
|
||||
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
|
||||
|
||||
vsize = self.getTxVSize(self.loadTx(tx_bytes), add_witness_bytes=witness_bytes)
|
||||
fee_rate_paid = fee_paid * 1000 // vsize
|
||||
self._log.info('vsize, feerate: %ld, %ld', vsize, fee_rate_paid)
|
||||
if not self.compareFeeRates(fee_rate_paid, feerate):
|
||||
raise ValueError('Bad fee rate, expected: {}'.format(feerate))
|
||||
|
||||
return True
|
||||
|
||||
def createScriptLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv):
|
||||
# lock refund swipe tx
|
||||
# Sends the coinA locked coin to the follower
|
||||
lock_refund_tx_obj = self.rpc_callback('decoderawtransaction', [tx_lock_refund_bytes.hex()])
|
||||
nonce = self.getScriptLockRefundTxNonce(vkbv)
|
||||
|
||||
# Find the output of the lock refund tx to spend
|
||||
spend_n, input_blinded_info = self.findOutputByNonce(lock_refund_tx_obj, nonce)
|
||||
ensure(spend_n is not None, 'Output not found in tx')
|
||||
|
||||
tx_lock_refund_id = lock_refund_tx_obj['txid']
|
||||
addr_out = self.pkh_to_address(pkh_dest)
|
||||
addr_info = self.rpc_callback('getaddressinfo', [addr_out])
|
||||
output_pubkey_hex = addr_info['pubkey']
|
||||
|
||||
A, B, lock2_value, C = self.extractScriptLockRefundScriptValues(script_lock_refund)
|
||||
|
||||
# Follower won't be able to decode output to check amount, shouldn't matter as fee is public and output is to leader, sum has to balance
|
||||
|
||||
inputs = [{'txid': tx_lock_refund_id, 'vout': spend_n, 'sequence': lock2_value, 'blindingfactor': input_blinded_info['blind']}]
|
||||
outputs = [{'type': 'blind', 'amount': input_blinded_info['amount'], 'address': addr_out, 'pubkey': output_pubkey_hex}]
|
||||
params = [inputs, outputs]
|
||||
rv = self.rpc_callback('createrawparttransaction', params)
|
||||
|
||||
lock_refund_swipe_tx_hex = rv['hex']
|
||||
|
||||
# Set dummy witness data for fee estimation
|
||||
dummy_witness_stack = self.getScriptLockRefundSwipeTxDummyWitness(script_lock_refund)
|
||||
|
||||
# Use a junk change pubkey to avoid adding unused keys to the wallet
|
||||
zero_change_key = i2b(self.getNewSecretKey())
|
||||
zero_change_pubkey = self.getPubkey(zero_change_key)
|
||||
inputs_info = {'0': {'value': input_blinded_info['amount'], 'blind': input_blinded_info['blind'], 'witnessstack': dummy_witness_stack}}
|
||||
outputs_info = rv['amounts']
|
||||
options = {
|
||||
'changepubkey': zero_change_pubkey.hex(),
|
||||
'feeRate': self.format_amount(tx_fee_rate),
|
||||
'subtractFeeFromOutputs': [0, ]
|
||||
}
|
||||
|
||||
rv = self.rpc_callback('fundrawtransactionfrom', ['blind', lock_refund_swipe_tx_hex, inputs_info, outputs_info, options])
|
||||
lock_refund_swipe_tx_hex = rv['hex']
|
||||
|
||||
return bytes.fromhex(lock_refund_swipe_tx_hex)
|
||||
|
||||
def getSpendableBalance(self):
|
||||
return self.make_int(self.rpc_callback('getbalances')['mine']['blind_trusted'])
|
||||
|
||||
|
||||
class PARTInterfaceAnon(PARTInterface):
|
||||
@staticmethod
|
||||
def balance_type():
|
||||
return BalanceTypes.ANON
|
||||
|
||||
@staticmethod
|
||||
def depth_spendable() -> int:
|
||||
return 12
|
||||
|
||||
def coin_name(self):
|
||||
return super().coin_name() + ' Anon'
|
||||
|
||||
def publishBLockTx(self, Kbv, Kbs, output_amount, feerate):
|
||||
sx_addr = self.formatStealthAddress(Kbv, Kbs)
|
||||
self._log.debug('sx_addr: {}'.format(sx_addr))
|
||||
|
||||
# TODO: Fund from other balances
|
||||
params = ['anon', 'anon',
|
||||
[{'address': sx_addr, 'amount': self.format_amount(output_amount)}, ],
|
||||
'', '', self._anon_tx_ring_size, 1, False,
|
||||
{'conf_target': self._conf_target, 'blind_watchonly_visible': True}]
|
||||
|
||||
txid = self.rpc_callback('sendtypeto', params)
|
||||
return bytes.fromhex(txid)
|
||||
|
||||
def findTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender):
|
||||
Kbv = self.getPubkey(kbv)
|
||||
sx_addr = self.formatStealthAddress(Kbv, Kbs)
|
||||
self._log.debug('sx_addr: {}'.format(sx_addr))
|
||||
|
||||
# Tx recipient must import the stealth address as watch only
|
||||
if bid_sender:
|
||||
cb_swap_value *= -1
|
||||
else:
|
||||
addr_info = self.rpc_callback('getaddressinfo', [sx_addr])
|
||||
if not addr_info['iswatchonly']:
|
||||
wif_prefix = self.chainparams_network()['key_prefix']
|
||||
wif_scan_key = toWIF(wif_prefix, kbv)
|
||||
self.rpc_callback('importstealthaddress', [wif_scan_key, Kbs.hex()])
|
||||
self._log.info('Imported watch-only sx_addr: {}'.format(sx_addr))
|
||||
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), restore_height))
|
||||
self.rpc_callback('rescanblockchain', [restore_height])
|
||||
|
||||
params = [{'include_watchonly': True, 'search': sx_addr}]
|
||||
txns = self.rpc_callback('filtertransactions', params)
|
||||
|
||||
if len(txns) == 1:
|
||||
tx = txns[0]
|
||||
assert (tx['outputs'][0]['stealth_address'] == sx_addr) # Should not be possible
|
||||
ensure(tx['outputs'][0]['type'] == 'anon', 'Output is not anon')
|
||||
|
||||
if make_int(tx['outputs'][0]['amount']) == cb_swap_value:
|
||||
height = 0
|
||||
if tx['confirmations'] > 0:
|
||||
chain_height = self.rpc_callback('getblockcount')
|
||||
height = chain_height - (tx['confirmations'] - 1)
|
||||
return {'txid': tx['txid'], 'amount': cb_swap_value, 'height': height}
|
||||
else:
|
||||
self._log.warning('Incorrect amount detected for coin b lock txn: {}'.format(tx['txid']))
|
||||
return -1
|
||||
return None
|
||||
|
||||
def spendBLockTx(self, chain_b_lock_txid, address_to, kbv, kbs, cb_swap_value, b_fee, restore_height, spend_actual_balance=False):
|
||||
Kbv = self.getPubkey(kbv)
|
||||
Kbs = self.getPubkey(kbs)
|
||||
sx_addr = self.formatStealthAddress(Kbv, Kbs)
|
||||
addr_info = self.rpc_callback('getaddressinfo', [sx_addr])
|
||||
if not addr_info['ismine']:
|
||||
wif_prefix = self.chainparams_network()['key_prefix']
|
||||
wif_scan_key = toWIF(wif_prefix, kbv)
|
||||
wif_spend_key = toWIF(wif_prefix, kbs)
|
||||
self.rpc_callback('importstealthaddress', [wif_scan_key, wif_spend_key])
|
||||
self._log.info('Imported spend key for sx_addr: {}'.format(sx_addr))
|
||||
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), restore_height))
|
||||
self.rpc_callback('rescanblockchain', [restore_height])
|
||||
|
||||
autxos = self.rpc_callback('listunspentanon', [1, 9999999, [sx_addr]])
|
||||
|
||||
if len(autxos) < 1:
|
||||
raise TemporaryError('No spendable outputs')
|
||||
elif len(autxos) > 1:
|
||||
raise ValueError('Too many spendable outputs')
|
||||
|
||||
utxo = autxos[0]
|
||||
utxo_sats = make_int(utxo['amount'])
|
||||
|
||||
if spend_actual_balance and utxo_sats != cb_swap_value:
|
||||
self._log.warning('Spending actual balance {}, not swap value {}.'.format(utxo_sats, cb_swap_value))
|
||||
cb_swap_value = utxo_sats
|
||||
|
||||
inputs = [{'tx': utxo['txid'], 'n': utxo['vout']}, ]
|
||||
params = ['anon', 'anon',
|
||||
[{'address': address_to, 'amount': self.format_amount(cb_swap_value), 'subfee': True}, ],
|
||||
'', '', self._anon_tx_ring_size, 1, False,
|
||||
{'conf_target': self._conf_target, 'inputs': inputs, 'show_fee': True}]
|
||||
rv = self.rpc_callback('sendtypeto', params)
|
||||
return bytes.fromhex(rv['txid'])
|
||||
|
||||
def findTxnByHash(self, txid_hex):
|
||||
# txindex is enabled for Particl
|
||||
|
||||
try:
|
||||
rv = self.rpc_callback('getrawtransaction', [txid_hex, True])
|
||||
except Exception as ex:
|
||||
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
|
||||
return None
|
||||
|
||||
if 'confirmations' in rv and rv['confirmations'] >= self.blocks_confirmed:
|
||||
return {'txid': txid_hex, 'amount': 0, 'height': rv['height']}
|
||||
|
||||
return None
|
||||
|
||||
def getSpendableBalance(self):
|
||||
return self.make_int(self.rpc_callback('getbalances')['mine']['anon_trusted'])
|
||||
19
basicswap/interface/passthrough_btc.py
Normal file
19
basicswap/interface/passthrough_btc.py
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2021 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
from .btc import BTCInterface
|
||||
from basicswap.contrib.test_framework.messages import (
|
||||
CTxOut)
|
||||
|
||||
|
||||
class PassthroughBTCInterface(BTCInterface):
|
||||
def __init__(self, coin_settings, network):
|
||||
super().__init__(coin_settings, network)
|
||||
self.txoType = CTxOut
|
||||
self._network = network
|
||||
self.blocks_confirmed = coin_settings['blocks_confirmed']
|
||||
self.setConfTarget(coin_settings['conf_target'])
|
||||
501
basicswap/interface/xmr.py
Normal file
501
basicswap/interface/xmr.py
Normal file
@@ -0,0 +1,501 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2020-2022 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import json
|
||||
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_add,
|
||||
ed25519_get_pubkey,
|
||||
ed25519_scalar_add,
|
||||
)
|
||||
from coincurve.keys import PrivateKey
|
||||
from coincurve.dleag import (
|
||||
dleag_prove,
|
||||
dleag_verify,
|
||||
dleag_proof_len,
|
||||
verify_ed25519_point,
|
||||
)
|
||||
|
||||
from basicswap.util import (
|
||||
dumpj,
|
||||
ensure,
|
||||
make_int,
|
||||
TemporaryError)
|
||||
from basicswap.rpc_xmr import (
|
||||
make_xmr_rpc_func,
|
||||
make_xmr_rpc2_func,
|
||||
make_xmr_wallet_rpc_func)
|
||||
from basicswap.util import (
|
||||
b2i, b2h)
|
||||
from basicswap.chainparams import XMR_COIN, CoinInterface, Coins
|
||||
|
||||
|
||||
class XMRInterface(CoinInterface):
|
||||
@staticmethod
|
||||
def coin_type():
|
||||
return Coins.XMR
|
||||
|
||||
@staticmethod
|
||||
def COIN():
|
||||
return XMR_COIN
|
||||
|
||||
@staticmethod
|
||||
def exp() -> int:
|
||||
return 12
|
||||
|
||||
@staticmethod
|
||||
def nbk() -> int:
|
||||
return 32
|
||||
|
||||
@staticmethod
|
||||
def nbK() -> int: # No. of bytes requires to encode a public key
|
||||
return 32
|
||||
|
||||
@staticmethod
|
||||
def depth_spendable() -> int:
|
||||
return 10
|
||||
|
||||
def __init__(self, coin_settings, network, swap_client=None):
|
||||
super().__init__(network)
|
||||
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'], host=coin_settings.get('walletrpchost', '127.0.0.1'))
|
||||
|
||||
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):
|
||||
ensure(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):
|
||||
with self._mx_wallet:
|
||||
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):
|
||||
with self._mx_wallet:
|
||||
self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename})
|
||||
|
||||
def testDaemonRPC(self, with_wallet=True):
|
||||
self.rpc_wallet_cb('get_languages')
|
||||
|
||||
def getDaemonVersion(self):
|
||||
return self.rpc_wallet_cb('get_version')['version']
|
||||
|
||||
def getBlockchainInfo(self):
|
||||
get_height = self.rpc_cb2('get_height', timeout=30)
|
||||
rv = {
|
||||
'blocks': get_height['height'],
|
||||
'verificationprogress': 0.0,
|
||||
}
|
||||
|
||||
try:
|
||||
# get_block_count.block_count is how many blocks are in the longest chain known to the node.
|
||||
# get_block_count returns "Internal error" if bootstrap-daemon is active
|
||||
if get_height['untrusted'] is True:
|
||||
rv['bootstrapping'] = True
|
||||
get_info = self.rpc_cb2('get_info', timeout=30)
|
||||
if 'height_without_bootstrap' in get_info:
|
||||
rv['blocks'] = get_info['height_without_bootstrap']
|
||||
|
||||
rv['known_block_count'] = get_info['height']
|
||||
if rv['known_block_count'] > rv['blocks']:
|
||||
rv['verificationprogress'] = rv['blocks'] / rv['known_block_count']
|
||||
else:
|
||||
rv['known_block_count'] = self.rpc_cb('get_block_count', timeout=30)['count']
|
||||
rv['verificationprogress'] = rv['blocks'] / rv['known_block_count']
|
||||
except Exception as e:
|
||||
self._log.warning('XMR get_block_count failed with: %s', str(e))
|
||||
rv['verificationprogress'] = 0.0
|
||||
|
||||
return rv
|
||||
|
||||
def getChainHeight(self):
|
||||
return self.rpc_cb2('get_height', timeout=30)['height']
|
||||
|
||||
def getWalletInfo(self):
|
||||
with self._mx_wallet:
|
||||
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'] = self.format_amount(balance_info['unlocked_balance'])
|
||||
rv['unconfirmed_balance'] = self.format_amount(balance_info['balance'] - balance_info['unlocked_balance'])
|
||||
return rv
|
||||
|
||||
def walletRestoreHeight(self):
|
||||
return self._restore_height
|
||||
|
||||
def getMainWalletAddress(self):
|
||||
with self._mx_wallet:
|
||||
self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename})
|
||||
return self.rpc_wallet_cb('get_address')['address']
|
||||
|
||||
def getNewAddress(self, placeholder):
|
||||
with self._mx_wallet:
|
||||
self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename})
|
||||
return self.rpc_wallet_cb('create_address', {'account_index': 0})['address']
|
||||
|
||||
def get_fee_rate(self, conf_target=2):
|
||||
self._log.warning('TODO - estimate fee rate?')
|
||||
return 0.0, 'unused'
|
||||
|
||||
def getNewSecretKey(self):
|
||||
return edu.get_secret()
|
||||
|
||||
def pubkey(self, key):
|
||||
return edf.scalarmult_B(key)
|
||||
|
||||
def encodeKey(self, vk):
|
||||
return vk.hex()
|
||||
|
||||
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_hex):
|
||||
return bytes.fromhex(k_hex)
|
||||
|
||||
def sumKeys(self, ka, kb):
|
||||
return ed25519_scalar_add(ka, kb)
|
||||
|
||||
def sumPubkeys(self, Ka, Kb):
|
||||
return ed25519_add(Ka, Kb)
|
||||
|
||||
def encodeSharedAddress(self, Kbv, Kbs):
|
||||
return xmr_util.encode_address(Kbv, Kbs)
|
||||
|
||||
def publishBLockTx(self, Kbv, Kbs, output_amount, feerate, delay_for=10):
|
||||
with self._mx_wallet:
|
||||
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'])
|
||||
|
||||
if self._sc.debug:
|
||||
i = 0
|
||||
while not self._sc.delay_event.is_set():
|
||||
params = {'out': True, 'pending': True, 'failed': True, 'pool': True, }
|
||||
rv = self.rpc_wallet_cb('get_transfers', params)
|
||||
self._log.debug('get_transfers {}'.format(dumpj(rv)))
|
||||
if 'pending' not in rv:
|
||||
break
|
||||
if i >= delay_for:
|
||||
break
|
||||
self._sc.delay_event.wait(1.0)
|
||||
|
||||
return tx_hash
|
||||
|
||||
def findTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender):
|
||||
with self._mx_wallet:
|
||||
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})
|
||||
|
||||
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):
|
||||
with self._mx_wallet:
|
||||
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')
|
||||
self._sc.delay_event.wait(1.0)
|
||||
if self._sc.delay_event.is_set():
|
||||
raise ValueError('Stopped')
|
||||
|
||||
return False
|
||||
|
||||
def findTxnByHash(self, txid):
|
||||
with self._mx_wallet:
|
||||
self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename})
|
||||
self.rpc_wallet_cb('refresh')
|
||||
|
||||
try:
|
||||
current_height = self.rpc_cb2('get_height', timeout=30)['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']:
|
||||
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, chain_b_lock_txid, address_to, kbv, kbs, cb_swap_value, b_fee_rate, restore_height, spend_actual_balance=False):
|
||||
with self._mx_wallet:
|
||||
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})
|
||||
|
||||
self.rpc_wallet_cb('refresh')
|
||||
rv = self.rpc_wallet_cb('get_balance')
|
||||
|
||||
if rv['balance'] < cb_swap_value:
|
||||
self._log.warning('Balance is too low, checking for existing spend.')
|
||||
txns = self.rpc_wallet_cb('get_transfers', {'out': True})
|
||||
if 'out' in txns:
|
||||
txns = txns['out']
|
||||
if len(txns) > 0:
|
||||
txid = txns[0]['txid']
|
||||
self._log.warning(f'spendBLockTx detected spending tx: {txid}.')
|
||||
if txns[0]['address'] == address_b58:
|
||||
return bytes.fromhex(txid)
|
||||
|
||||
self._log.error('wallet {} balance {}, expected {}'.format(wallet_filename, rv['balance'], cb_swap_value))
|
||||
|
||||
if not spend_actual_balance:
|
||||
raise TemporaryError('Invalid balance')
|
||||
|
||||
if spend_actual_balance and rv['balance'] != cb_swap_value:
|
||||
self._log.warning('Spending actual balance {}, not swap value {}.'.format(rv['balance'], cb_swap_value))
|
||||
cb_swap_value = rv['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 TemporaryError('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)
|
||||
self._log.debug('sweep_all {}'.format(json.dumps(rv)))
|
||||
|
||||
return bytes.fromhex(rv['tx_hash_list'][0])
|
||||
|
||||
def withdrawCoin(self, value, addr_to, subfee):
|
||||
with self._mx_wallet:
|
||||
value_sats = make_int(value, self.exp())
|
||||
|
||||
self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename})
|
||||
|
||||
if subfee:
|
||||
balance = self.rpc_wallet_cb('get_balance')
|
||||
if balance['unlocked_balance'] - value_sats <= 10:
|
||||
self._log.info('subfee enabled and value close to total, using sweep_all.')
|
||||
params = {'address': addr_to}
|
||||
if self._fee_priority > 0:
|
||||
params['priority'] = self._fee_priority
|
||||
rv = self.rpc_wallet_cb('sweep_all', params)
|
||||
return rv['tx_hash_list'][0]
|
||||
raise ValueError('Withdraw value must be close to total to use subfee/sweep_all.')
|
||||
|
||||
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']
|
||||
|
||||
def showLockTransfers(self, Kbv, Kbs):
|
||||
with self._mx_wallet:
|
||||
try:
|
||||
address_b58 = xmr_util.encode_address(Kbv, Kbs)
|
||||
wallet_file = address_b58 + '_spend'
|
||||
try:
|
||||
self.rpc_wallet_cb('open_wallet', {'filename': wallet_file})
|
||||
except Exception:
|
||||
wallet_file = address_b58
|
||||
self.rpc_wallet_cb('open_wallet', {'filename': wallet_file})
|
||||
|
||||
self.rpc_wallet_cb('refresh')
|
||||
|
||||
rv = self.rpc_wallet_cb('get_transfers', {'in': True, 'out': True, 'pending': True, 'failed': True})
|
||||
rv['filename'] = wallet_file
|
||||
return rv
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
def getSpendableBalance(self):
|
||||
with self._mx_wallet:
|
||||
self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename})
|
||||
|
||||
self.rpc_wallet_cb('refresh')
|
||||
balance_info = self.rpc_wallet_cb('get_balance')
|
||||
return balance_info['unlocked_balance']
|
||||
Reference in New Issue
Block a user