mirror of
https://github.com/basicswap/basicswap.git
synced 2025-01-08 19:59:33 +00:00
NMC and CLTV, abs lock values still to be verified
This commit is contained in:
parent
222fbb492f
commit
307b8ab8bf
6 changed files with 638 additions and 45 deletions
|
@ -126,6 +126,9 @@ class TxTypes(IntEnum): # For PooledAddress
|
||||||
|
|
||||||
SEQUENCE_LOCK_BLOCKS = 1
|
SEQUENCE_LOCK_BLOCKS = 1
|
||||||
SEQUENCE_LOCK_TIME = 2
|
SEQUENCE_LOCK_TIME = 2
|
||||||
|
ABS_LOCK_BLOCKS = 3
|
||||||
|
ABS_LOCK_TIME = 4
|
||||||
|
|
||||||
SEQUENCE_LOCKTIME_GRANULARITY = 9 # 512 seconds
|
SEQUENCE_LOCKTIME_GRANULARITY = 9 # 512 seconds
|
||||||
SEQUENCE_LOCKTIME_TYPE_FLAG = (1 << 22)
|
SEQUENCE_LOCKTIME_TYPE_FLAG = (1 << 22)
|
||||||
SEQUENCE_LOCKTIME_MASK = 0x0000ffff
|
SEQUENCE_LOCKTIME_MASK = 0x0000ffff
|
||||||
|
@ -183,6 +186,10 @@ def getLockName(lock_type):
|
||||||
return 'Sequence lock, blocks'
|
return 'Sequence lock, blocks'
|
||||||
if lock_type == SEQUENCE_LOCK_TIME:
|
if lock_type == SEQUENCE_LOCK_TIME:
|
||||||
return 'Sequence lock, time'
|
return 'Sequence lock, time'
|
||||||
|
if lock_type == ABS_LOCK_BLOCKS:
|
||||||
|
return 'blocks'
|
||||||
|
if lock_type == ABS_LOCK_TIME:
|
||||||
|
return 'time'
|
||||||
|
|
||||||
|
|
||||||
def getExpectedSequence(lockType, lockVal, coin_type):
|
def getExpectedSequence(lockType, lockVal, coin_type):
|
||||||
|
@ -206,7 +213,7 @@ def decodeSequence(lock_value):
|
||||||
return lock_value & SEQUENCE_LOCKTIME_MASK
|
return lock_value & SEQUENCE_LOCKTIME_MASK
|
||||||
|
|
||||||
|
|
||||||
def buildContractScriptCSV(sequence, secret_hash, pkh_redeem, pkh_refund):
|
def buildContractScript(lock_val, secret_hash, pkh_redeem, pkh_refund, op_lock=OpCodes.OP_CHECKSEQUENCEVERIFY):
|
||||||
script = bytearray([
|
script = bytearray([
|
||||||
OpCodes.OP_IF,
|
OpCodes.OP_IF,
|
||||||
OpCodes.OP_SIZE,
|
OpCodes.OP_SIZE,
|
||||||
|
@ -222,9 +229,9 @@ def buildContractScriptCSV(sequence, secret_hash, pkh_redeem, pkh_refund):
|
||||||
0x14]) \
|
0x14]) \
|
||||||
+ pkh_redeem \
|
+ pkh_redeem \
|
||||||
+ bytearray([OpCodes.OP_ELSE, ]) \
|
+ bytearray([OpCodes.OP_ELSE, ]) \
|
||||||
+ SerialiseNum(sequence) \
|
+ SerialiseNum(lock_val) \
|
||||||
+ bytearray([
|
+ bytearray([
|
||||||
OpCodes.OP_CHECKSEQUENCEVERIFY,
|
op_lock,
|
||||||
OpCodes.OP_DROP,
|
OpCodes.OP_DROP,
|
||||||
OpCodes.OP_DUP,
|
OpCodes.OP_DUP,
|
||||||
OpCodes.OP_HASH160,
|
OpCodes.OP_HASH160,
|
||||||
|
@ -590,6 +597,7 @@ class BasicSwap():
|
||||||
'watched_outputs': [],
|
'watched_outputs': [],
|
||||||
'last_height_checked': last_height_checked,
|
'last_height_checked': last_height_checked,
|
||||||
'use_segwit': use_segwit,
|
'use_segwit': use_segwit,
|
||||||
|
'use_csv': chain_client_settings.get('use_csv', True),
|
||||||
}
|
}
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
|
@ -710,8 +718,16 @@ class BasicSwap():
|
||||||
def validateOfferLockValue(self, coin_from, coin_to, lock_type, lock_value):
|
def validateOfferLockValue(self, coin_from, coin_to, lock_type, lock_value):
|
||||||
if lock_type == OfferMessage.SEQUENCE_LOCK_TIME:
|
if lock_type == OfferMessage.SEQUENCE_LOCK_TIME:
|
||||||
assert(lock_value >= 2 * 60 * 60 and lock_value <= 96 * 60 * 60), 'Invalid lock_value time'
|
assert(lock_value >= 2 * 60 * 60 and lock_value <= 96 * 60 * 60), 'Invalid lock_value time'
|
||||||
|
assert(self.coin_clients[coin_from]['use_csv'] and self.coin_clients[coin_to]['use_csv']), 'Both coins need CSV activated.'
|
||||||
elif lock_type == OfferMessage.SEQUENCE_LOCK_BLOCKS:
|
elif lock_type == OfferMessage.SEQUENCE_LOCK_BLOCKS:
|
||||||
assert(lock_value >= 5 and lock_value <= 1000), 'Invalid lock_value blocks'
|
assert(lock_value >= 5 and lock_value <= 1000), 'Invalid lock_value blocks'
|
||||||
|
assert(self.coin_clients[coin_from]['use_csv'] and self.coin_clients[coin_to]['use_csv']), 'Both coins need CSV activated.'
|
||||||
|
elif lock_type == ABS_LOCK_TIME:
|
||||||
|
# TODO: range?
|
||||||
|
assert(lock_value >= 4 * 60 * 60 and lock_value <= 96 * 60 * 60), 'Invalid lock_value time'
|
||||||
|
elif lock_type == ABS_LOCK_BLOCKS:
|
||||||
|
# TODO: range?
|
||||||
|
assert(lock_value >= 10 and lock_value <= 1000), 'Invalid lock_value blocks'
|
||||||
else:
|
else:
|
||||||
raise ValueError('Unknown locktype')
|
raise ValueError('Unknown locktype')
|
||||||
|
|
||||||
|
@ -872,7 +888,7 @@ class BasicSwap():
|
||||||
def getReceiveAddressForCoin(self, coin_type):
|
def getReceiveAddressForCoin(self, coin_type):
|
||||||
if coin_type == Coins.PART:
|
if coin_type == Coins.PART:
|
||||||
new_addr = self.callcoinrpc(Coins.PART, 'getnewaddress')
|
new_addr = self.callcoinrpc(Coins.PART, 'getnewaddress')
|
||||||
elif coin_type == Coins.LTC or coin_type == Coins.BTC:
|
elif coin_type == Coins.LTC or coin_type == Coins.BTC or coin_type == Coins.NMC:
|
||||||
args = []
|
args = []
|
||||||
if self.coin_clients[coin_type]['use_segwit']:
|
if self.coin_clients[coin_type]['use_segwit']:
|
||||||
args = ['swap_receive', 'bech32']
|
args = ['swap_receive', 'bech32']
|
||||||
|
@ -1123,15 +1139,22 @@ class BasicSwap():
|
||||||
coin_from = Coins(offer.coin_from)
|
coin_from = Coins(offer.coin_from)
|
||||||
bid_date = dt.datetime.fromtimestamp(bid.created_at).date()
|
bid_date = dt.datetime.fromtimestamp(bid.created_at).date()
|
||||||
|
|
||||||
# TODO: Use CLTV for coins without CSV
|
|
||||||
sequence = getExpectedSequence(offer.lock_type, offer.lock_value, coin_from)
|
|
||||||
secret = self.getContractSecret(bid_date, bid.contract_count)
|
secret = self.getContractSecret(bid_date, bid.contract_count)
|
||||||
secret_hash = hashlib.sha256(secret).digest()
|
secret_hash = hashlib.sha256(secret).digest()
|
||||||
|
|
||||||
pubkey_refund = self.getContractPubkey(bid_date, bid.contract_count)
|
pubkey_refund = self.getContractPubkey(bid_date, bid.contract_count)
|
||||||
pkhash_refund = getKeyID(pubkey_refund)
|
pkhash_refund = getKeyID(pubkey_refund)
|
||||||
|
|
||||||
script = buildContractScriptCSV(sequence, secret_hash, bid.pkhash_buyer, pkhash_refund)
|
if offer.lock_type < ABS_LOCK_BLOCKS:
|
||||||
|
sequence = getExpectedSequence(offer.lock_type, offer.lock_value, coin_from)
|
||||||
|
script = buildContractScript(sequence, secret_hash, bid.pkhash_buyer, pkhash_refund)
|
||||||
|
else:
|
||||||
|
if offer.lock_type == ABS_LOCK_BLOCKS:
|
||||||
|
lock_value = self.callcoinrpc(coin_from, 'getblockchaininfo')['blocks'] + offer.lock_value
|
||||||
|
else:
|
||||||
|
lock_value = int(time.time()) + offer.lock_value
|
||||||
|
logging.debug('initiate %s lock_value %d %d', coin_from, offer.lock_value, lock_value)
|
||||||
|
script = buildContractScript(lock_value, secret_hash, bid.pkhash_buyer, pkhash_refund, OpCodes.OP_CHECKLOCKTIMEVERIFY)
|
||||||
|
|
||||||
p2sh = self.callcoinrpc(Coins.PART, 'decodescript', [script.hex()])['p2sh']
|
p2sh = self.callcoinrpc(Coins.PART, 'decodescript', [script.hex()])['p2sh']
|
||||||
|
|
||||||
|
@ -1141,7 +1164,7 @@ class BasicSwap():
|
||||||
txn = self.createInitiateTxn(coin_from, bid_id, bid)
|
txn = self.createInitiateTxn(coin_from, bid_id, bid)
|
||||||
|
|
||||||
# Store the signed refund txn in case wallet is locked when refund is possible
|
# Store the signed refund txn in case wallet is locked when refund is possible
|
||||||
refund_txn = self.createRefundTxn(coin_from, txn, bid, script)
|
refund_txn = self.createRefundTxn(coin_from, txn, offer, bid, script)
|
||||||
bid.initiate_txn_refund = bytes.fromhex(refund_txn)
|
bid.initiate_txn_refund = bytes.fromhex(refund_txn)
|
||||||
|
|
||||||
txid = self.submitTxn(coin_from, txn)
|
txid = self.submitTxn(coin_from, txn)
|
||||||
|
@ -1153,7 +1176,7 @@ class BasicSwap():
|
||||||
txid = self.submitTxn(coin_from, bid.initiate_txn_refund.hex())
|
txid = self.submitTxn(coin_from, bid.initiate_txn_refund.hex())
|
||||||
self.log.error('Submit refund_txn unexpectedly worked: ' + txid)
|
self.log.error('Submit refund_txn unexpectedly worked: ' + txid)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if 'non-BIP68-final' not in str(e):
|
if 'non-BIP68-final' not in str(e) and 'non-final' not in str(e):
|
||||||
self.log.error('Submit refund_txn unexpected error' + str(e))
|
self.log.error('Submit refund_txn unexpected error' + str(e))
|
||||||
|
|
||||||
if txid is not None:
|
if txid is not None:
|
||||||
|
@ -1269,14 +1292,22 @@ class BasicSwap():
|
||||||
|
|
||||||
bid_date = dt.datetime.fromtimestamp(bid.created_at).date()
|
bid_date = dt.datetime.fromtimestamp(bid.created_at).date()
|
||||||
|
|
||||||
# Participate txn is locked for half the time of the initiate txn
|
|
||||||
lock_value = decodeSequence(offer.lock_value) // 2
|
|
||||||
sequence = getExpectedSequence(offer.lock_type, lock_value, coin_to)
|
|
||||||
|
|
||||||
secret_hash = extractScriptSecretHash(bid.initiate_script)
|
secret_hash = extractScriptSecretHash(bid.initiate_script)
|
||||||
pkhash_seller = bid.pkhash_seller
|
pkhash_seller = bid.pkhash_seller
|
||||||
pkhash_buyer_refund = bid.pkhash_buyer
|
pkhash_buyer_refund = bid.pkhash_buyer
|
||||||
bid.participate_script = buildContractScriptCSV(sequence, secret_hash, pkhash_seller, pkhash_buyer_refund)
|
|
||||||
|
# Participate txn is locked for half the time of the initiate txn
|
||||||
|
lock_value = offer.lock_value // 2
|
||||||
|
if offer.lock_type < ABS_LOCK_BLOCKS:
|
||||||
|
sequence = getExpectedSequence(offer.lock_type, lock_value, coin_to)
|
||||||
|
bid.participate_script = buildContractScript(sequence, secret_hash, pkhash_seller, pkhash_buyer_refund)
|
||||||
|
else:
|
||||||
|
if offer.lock_type == ABS_LOCK_BLOCKS:
|
||||||
|
contract_lock_value = self.callcoinrpc(coin_to, 'getblockchaininfo')['blocks'] + lock_value
|
||||||
|
else:
|
||||||
|
contract_lock_value = int(time.time()) + lock_value
|
||||||
|
logging.debug('participate %s lock_value %d %d', coin_to, lock_value, contract_lock_value)
|
||||||
|
bid.participate_script = buildContractScript(contract_lock_value, secret_hash, pkhash_seller, pkhash_buyer_refund, OpCodes.OP_CHECKLOCKTIMEVERIFY)
|
||||||
|
|
||||||
def createParticipateTxn(self, bid_id, bid, offer):
|
def createParticipateTxn(self, bid_id, bid, offer):
|
||||||
self.log.debug('createParticipateTxn')
|
self.log.debug('createParticipateTxn')
|
||||||
|
@ -1302,7 +1333,7 @@ class BasicSwap():
|
||||||
|
|
||||||
txn_signed = self.callcoinrpc(coin_to, 'signrawtransactionwithwallet', [txn_funded])['hex']
|
txn_signed = self.callcoinrpc(coin_to, 'signrawtransactionwithwallet', [txn_funded])['hex']
|
||||||
|
|
||||||
refund_txn = self.createRefundTxn(coin_to, txn_signed, bid, bid.participate_script, tx_type=TxTypes.PTX_REFUND)
|
refund_txn = self.createRefundTxn(coin_to, txn_signed, offer, bid, bid.participate_script, tx_type=TxTypes.PTX_REFUND)
|
||||||
bid.participate_txn_refund = bytes.fromhex(refund_txn)
|
bid.participate_txn_refund = bytes.fromhex(refund_txn)
|
||||||
|
|
||||||
chain_height = self.callcoinrpc(coin_to, 'getblockchaininfo')['blocks']
|
chain_height = self.callcoinrpc(coin_to, 'getblockchaininfo')['blocks']
|
||||||
|
@ -1433,7 +1464,7 @@ class BasicSwap():
|
||||||
|
|
||||||
return redeem_txn
|
return redeem_txn
|
||||||
|
|
||||||
def createRefundTxn(self, coin_type, txn, bid, txn_script, addr_refund_out=None, tx_type=TxTypes.ITX_REFUND):
|
def createRefundTxn(self, coin_type, txn, offer, bid, txn_script, addr_refund_out=None, tx_type=TxTypes.ITX_REFUND):
|
||||||
self.log.debug('createRefundTxn')
|
self.log.debug('createRefundTxn')
|
||||||
if self.coin_clients[coin_type]['connection_type'] != 'rpc':
|
if self.coin_clients[coin_type]['connection_type'] != 'rpc':
|
||||||
return None
|
return None
|
||||||
|
@ -1459,7 +1490,11 @@ class BasicSwap():
|
||||||
'redeemScript': txn_script.hex(),
|
'redeemScript': txn_script.hex(),
|
||||||
'amount': prev_amount}
|
'amount': prev_amount}
|
||||||
|
|
||||||
sequence = DeserialiseNum(txn_script, 64)
|
lock_value = DeserialiseNum(txn_script, 64)
|
||||||
|
if offer.lock_type < ABS_LOCK_BLOCKS:
|
||||||
|
sequence = lock_value
|
||||||
|
else:
|
||||||
|
sequence = 1
|
||||||
prevout_s = ' in={}:{}:{}'.format(txjs['txid'], vout, sequence)
|
prevout_s = ' in={}:{}:{}'.format(txjs['txid'], vout, sequence)
|
||||||
|
|
||||||
fee_rate = self.getFeeRateForCoin(coin_type)
|
fee_rate = self.getFeeRateForCoin(coin_type)
|
||||||
|
@ -1488,6 +1523,9 @@ class BasicSwap():
|
||||||
else:
|
else:
|
||||||
refund_txn = self.calltx('-btcmode -create nversion=2' + prevout_s + output_to)
|
refund_txn = self.calltx('-btcmode -create nversion=2' + prevout_s + output_to)
|
||||||
|
|
||||||
|
if offer.lock_type == ABS_LOCK_BLOCKS or offer.lock_type == ABS_LOCK_TIME:
|
||||||
|
refund_txn = self.calltx('{} locktime={}'.format(refund_txn, lock_value))
|
||||||
|
|
||||||
options = {}
|
options = {}
|
||||||
if self.coin_clients[coin_type]['use_segwit']:
|
if self.coin_clients[coin_type]['use_segwit']:
|
||||||
options['force_segwit'] = True
|
options['force_segwit'] = True
|
||||||
|
@ -1691,7 +1729,7 @@ class BasicSwap():
|
||||||
self.initiateTxnConfirmed(bid_id, bid, offer)
|
self.initiateTxnConfirmed(bid_id, bid, offer)
|
||||||
save_bid = True
|
save_bid = True
|
||||||
|
|
||||||
# Bid times out if buyer doesn't see tx in chain within
|
# Bid times out if buyer doesn't see tx in chain within INITIATE_TX_TIMEOUT seconds
|
||||||
if bid.state_time + INITIATE_TX_TIMEOUT < int(time.time()):
|
if bid.state_time + INITIATE_TX_TIMEOUT < int(time.time()):
|
||||||
self.log.info('Swap timed out waiting for initiate tx for bid %s', bid_id.hex())
|
self.log.info('Swap timed out waiting for initiate tx for bid %s', bid_id.hex())
|
||||||
bid.setState(BidStates.SWAP_TIMEDOUT)
|
bid.setState(BidStates.SWAP_TIMEDOUT)
|
||||||
|
@ -1757,7 +1795,7 @@ class BasicSwap():
|
||||||
self.log.debug('Submitted initiate refund txn %s to %s chain for bid %s', txid, chainparams[coin_from]['name'], bid_id.hex())
|
self.log.debug('Submitted initiate refund txn %s to %s chain for bid %s', txid, chainparams[coin_from]['name'], bid_id.hex())
|
||||||
# State will update when spend is detected
|
# State will update when spend is detected
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if 'non-BIP68-final (code 64)' not in str(e):
|
if 'non-BIP68-final (code 64)' not in str(e) and 'non-final' not in str(e):
|
||||||
self.log.warning('Error trying to submit initiate refund txn: %s', str(e))
|
self.log.warning('Error trying to submit initiate refund txn: %s', str(e))
|
||||||
if (bid.participate_txn_state == TxStates.TX_SENT or bid.participate_txn_state == TxStates.TX_CONFIRMED) \
|
if (bid.participate_txn_state == TxStates.TX_SENT or bid.participate_txn_state == TxStates.TX_CONFIRMED) \
|
||||||
and bid.participate_txn_refund is not None:
|
and bid.participate_txn_refund is not None:
|
||||||
|
@ -1766,7 +1804,7 @@ class BasicSwap():
|
||||||
self.log.debug('Submitted participate refund txn %s to %s chain for bid %s', txid, chainparams[coin_to]['name'], bid_id.hex())
|
self.log.debug('Submitted participate refund txn %s to %s chain for bid %s', txid, chainparams[coin_to]['name'], bid_id.hex())
|
||||||
# State will update when spend is detected
|
# State will update when spend is detected
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if 'non-BIP68-final (code 64)' not in str(e):
|
if 'non-BIP68-final (code 64)' not in str(e) and 'non-final' not in str(e):
|
||||||
self.log.warning('Error trying to submit participate refund txn: %s', str(e))
|
self.log.warning('Error trying to submit participate refund txn: %s', str(e))
|
||||||
return False # Bid is still active
|
return False # Bid is still active
|
||||||
|
|
||||||
|
@ -2090,19 +2128,11 @@ class BasicSwap():
|
||||||
|
|
||||||
self.log.debug('for bid %s', bid_accept_data.bid_msg_id.hex())
|
self.log.debug('for bid %s', bid_accept_data.bid_msg_id.hex())
|
||||||
|
|
||||||
decoded_script = self.callcoinrpc(Coins.PART, 'decodescript', [bid_accept_data.contract_script.hex()])
|
|
||||||
|
|
||||||
# TODO: Verify script without decoding?
|
|
||||||
prog = re.compile('OP_IF OP_SIZE 32 OP_EQUALVERIFY OP_SHA256 (\w+) OP_EQUALVERIFY OP_DUP OP_HASH160 (\w+) OP_ELSE (\d+) OP_CHECKSEQUENCEVERIFY OP_DROP OP_DUP OP_HASH160 (\w+) OP_ENDIF OP_EQUALVERIFY OP_CHECKSIG')
|
|
||||||
rr = prog.match(decoded_script['asm'])
|
|
||||||
if not rr:
|
|
||||||
raise ValueError('Bad script')
|
|
||||||
scriptvalues = rr.groups()
|
|
||||||
|
|
||||||
bid_id = bid_accept_data.bid_msg_id
|
bid_id = bid_accept_data.bid_msg_id
|
||||||
bid, offer = self.getBidAndOffer(bid_id)
|
bid, offer = self.getBidAndOffer(bid_id)
|
||||||
assert(bid is not None and bid.was_sent is True), 'Unknown bidid'
|
assert(bid is not None and bid.was_sent is True), 'Unknown bidid'
|
||||||
assert(offer), 'Offer not found ' + bid.offer_id.hex()
|
assert(offer), 'Offer not found ' + bid.offer_id.hex()
|
||||||
|
coin_from = Coins(offer.coin_from)
|
||||||
|
|
||||||
# assert(bid.expire_at > now), 'Bid expired' # How much time over to accept
|
# assert(bid.expire_at > now), 'Bid expired' # How much time over to accept
|
||||||
|
|
||||||
|
@ -2112,12 +2142,26 @@ class BasicSwap():
|
||||||
return
|
return
|
||||||
raise ValueError('Wrong bid state: {}'.format(str(BidStates(bid.state))))
|
raise ValueError('Wrong bid state: {}'.format(str(BidStates(bid.state))))
|
||||||
|
|
||||||
coin_from = Coins(offer.coin_from)
|
use_csv = True if offer.lock_type < ABS_LOCK_BLOCKS else False
|
||||||
expect_sequence = getExpectedSequence(offer.lock_type, offer.lock_value, coin_from)
|
|
||||||
|
# TODO: Verify script without decoding?
|
||||||
|
decoded_script = self.callcoinrpc(Coins.PART, 'decodescript', [bid_accept_data.contract_script.hex()])
|
||||||
|
lock_check_op = 'OP_CHECKSEQUENCEVERIFY' if use_csv else 'OP_CHECKLOCKTIMEVERIFY'
|
||||||
|
prog = re.compile('OP_IF OP_SIZE 32 OP_EQUALVERIFY OP_SHA256 (\w+) OP_EQUALVERIFY OP_DUP OP_HASH160 (\w+) OP_ELSE (\d+) {} OP_DROP OP_DUP OP_HASH160 (\w+) OP_ENDIF OP_EQUALVERIFY OP_CHECKSIG'.format(lock_check_op))
|
||||||
|
rr = prog.match(decoded_script['asm'])
|
||||||
|
if not rr:
|
||||||
|
raise ValueError('Bad script')
|
||||||
|
scriptvalues = rr.groups()
|
||||||
|
|
||||||
assert(len(scriptvalues[0]) == 64), 'Bad secret_hash length'
|
assert(len(scriptvalues[0]) == 64), 'Bad secret_hash length'
|
||||||
assert(bytes.fromhex(scriptvalues[1]) == bid.pkhash_buyer), 'pkhash_buyer mismatch'
|
assert(bytes.fromhex(scriptvalues[1]) == bid.pkhash_buyer), 'pkhash_buyer mismatch'
|
||||||
assert(int(scriptvalues[2]) == expect_sequence), 'sequence mismatch'
|
|
||||||
|
if use_csv:
|
||||||
|
expect_sequence = getExpectedSequence(offer.lock_type, offer.lock_value, coin_from)
|
||||||
|
assert(int(scriptvalues[2]) == expect_sequence), 'sequence mismatch'
|
||||||
|
else:
|
||||||
|
self.log.warning('TODO: validate absolute lock values')
|
||||||
|
|
||||||
assert(len(scriptvalues[3]) == 40), 'pkhash_refund bad length'
|
assert(len(scriptvalues[3]) == 40), 'pkhash_refund bad length'
|
||||||
|
|
||||||
assert(bid.accept_msg_id is None), 'Bid already accepted'
|
assert(bid.accept_msg_id is None), 'Bid already accepted'
|
||||||
|
|
|
@ -22,3 +22,8 @@ LITECOIN_BINDIR = os.path.expanduser(os.getenv('LITECOIN_BINDIR', ''))
|
||||||
LITECOIND = os.getenv('LITECOIND', 'litecoind' + ('.exe' if os.name == 'nt' else ''))
|
LITECOIND = os.getenv('LITECOIND', 'litecoind' + ('.exe' if os.name == 'nt' else ''))
|
||||||
LITECOIN_CLI = os.getenv('LITECOIN_CLI', 'litecoin-cli' + ('.exe' if os.name == 'nt' else ''))
|
LITECOIN_CLI = os.getenv('LITECOIN_CLI', 'litecoin-cli' + ('.exe' if os.name == 'nt' else ''))
|
||||||
LITECOIN_TX = os.getenv('LITECOIN_TX', 'litecoin-tx' + ('.exe' if os.name == 'nt' else ''))
|
LITECOIN_TX = os.getenv('LITECOIN_TX', 'litecoin-tx' + ('.exe' if os.name == 'nt' else ''))
|
||||||
|
|
||||||
|
NAMECOIN_BINDIR = os.path.expanduser(os.getenv('NAMECOIN_BINDIR', ''))
|
||||||
|
NAMECOIND = os.getenv('NAMECOIND', 'namecoind' + ('.exe' if os.name == 'nt' else ''))
|
||||||
|
NAMECOIN_CLI = os.getenv('NAMECOIN_CLI', 'namecoin-cli' + ('.exe' if os.name == 'nt' else ''))
|
||||||
|
NAMECOIN_TX = os.getenv('NAMECOIN_TX', 'namecoin-tx' + ('.exe' if os.name == 'nt' else ''))
|
||||||
|
|
|
@ -14,6 +14,8 @@ message OfferMessage {
|
||||||
NOT_SET = 0;
|
NOT_SET = 0;
|
||||||
SEQUENCE_LOCK_BLOCKS = 1;
|
SEQUENCE_LOCK_BLOCKS = 1;
|
||||||
SEQUENCE_LOCK_TIME = 2;
|
SEQUENCE_LOCK_TIME = 2;
|
||||||
|
ABS_LOCK_BLOCKS = 3;
|
||||||
|
ABS_LOCK_TIME = 4;
|
||||||
}
|
}
|
||||||
LockType lock_type = 7;
|
LockType lock_type = 7;
|
||||||
uint32 lock_value = 8;
|
uint32 lock_value = 8;
|
||||||
|
|
|
@ -23,6 +23,7 @@ import time
|
||||||
from urllib.request import urlretrieve
|
from urllib.request import urlretrieve
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import logging
|
import logging
|
||||||
|
import platform
|
||||||
|
|
||||||
import gnupg
|
import gnupg
|
||||||
|
|
||||||
|
@ -30,7 +31,10 @@ import basicswap.config as cfg
|
||||||
from basicswap.util import callrpc_cli
|
from basicswap.util import callrpc_cli
|
||||||
from bin.basicswap_run import startDaemon
|
from bin.basicswap_run import startDaemon
|
||||||
|
|
||||||
BIN_ARCH = 'x86_64-linux-gnu.tar.gz'
|
if platform.system() == 'Darwin':
|
||||||
|
BIN_ARCH = 'osx64.tar.gz'
|
||||||
|
else:
|
||||||
|
BIN_ARCH = 'x86_64-linux-gnu.tar.gz'
|
||||||
|
|
||||||
known_coins = {
|
known_coins = {
|
||||||
'particl': '0.18.1.0',
|
'particl': '0.18.1.0',
|
||||||
|
@ -358,6 +362,7 @@ def main():
|
||||||
'datadir': os.path.join(data_dir, 'namecoin'),
|
'datadir': os.path.join(data_dir, 'namecoin'),
|
||||||
'bindir': os.path.join(data_dir, 'bins', 'namecoin'),
|
'bindir': os.path.join(data_dir, 'bins', 'namecoin'),
|
||||||
'use_segwit': False,
|
'use_segwit': False,
|
||||||
|
'use_csv': False,
|
||||||
'blocks_confirmed': 1
|
'blocks_confirmed': 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,6 @@ import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import basicswap.config as cfg
|
|
||||||
from basicswap import __version__
|
from basicswap import __version__
|
||||||
from basicswap.basicswap import BasicSwap
|
from basicswap.basicswap import BasicSwap
|
||||||
from basicswap.http_server import HttpThread
|
from basicswap.http_server import HttpThread
|
||||||
|
@ -66,17 +65,10 @@ def runClient(fp, dataDir, chain):
|
||||||
for c, v in settings['chainclients'].items():
|
for c, v in settings['chainclients'].items():
|
||||||
if v['manage_daemon'] is True:
|
if v['manage_daemon'] is True:
|
||||||
logger.info('Starting {} daemon'.format(c.capitalize()))
|
logger.info('Starting {} daemon'.format(c.capitalize()))
|
||||||
if c == 'particl':
|
|
||||||
daemons.append(startDaemon(v['datadir'], v['bindir'], cfg.PARTICLD))
|
filename = c + 'd' + ('.exe' if os.name == 'nt' else '')
|
||||||
logger.info('Started {} {}'.format(cfg.PARTICLD, daemons[-1].pid))
|
daemons.append(startDaemon(v['datadir'], v['bindir'], filename))
|
||||||
elif c == 'bitcoin':
|
logger.info('Started {} {}'.format(filename, daemons[-1].pid))
|
||||||
daemons.append(startDaemon(v['datadir'], v['bindir'], cfg.BITCOIND))
|
|
||||||
logger.info('Started {} {}'.format(cfg.BITCOIND, daemons[-1].pid))
|
|
||||||
elif c == 'litecoin':
|
|
||||||
daemons.append(startDaemon(v['datadir'], v['bindir'], cfg.LITECOIND))
|
|
||||||
logger.info('Started {} {}'.format(cfg.LITECOIND, daemons[-1].pid))
|
|
||||||
else:
|
|
||||||
logger.warning('Unknown chain', c)
|
|
||||||
|
|
||||||
swap_client = BasicSwap(fp, dataDir, settings, chain)
|
swap_client = BasicSwap(fp, dataDir, settings, chain)
|
||||||
|
|
||||||
|
|
545
tests/test_nmc.py
Normal file
545
tests/test_nmc.py
Normal file
|
@ -0,0 +1,545 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2019 tecnovert
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file LICENSE.txt or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
|
"""
|
||||||
|
basicswap]$ python tests/test_nmc.py
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import signal
|
||||||
|
import threading
|
||||||
|
from urllib.request import urlopen
|
||||||
|
|
||||||
|
from basicswap.basicswap import (
|
||||||
|
BasicSwap,
|
||||||
|
Coins,
|
||||||
|
SwapTypes,
|
||||||
|
BidStates,
|
||||||
|
TxStates,
|
||||||
|
ABS_LOCK_BLOCKS,
|
||||||
|
ABS_LOCK_TIME,
|
||||||
|
)
|
||||||
|
from basicswap.util import (
|
||||||
|
COIN,
|
||||||
|
toWIF,
|
||||||
|
callrpc_cli,
|
||||||
|
)
|
||||||
|
from basicswap.key import (
|
||||||
|
ECKey,
|
||||||
|
)
|
||||||
|
from basicswap.http_server import (
|
||||||
|
HttpThread,
|
||||||
|
)
|
||||||
|
|
||||||
|
import basicswap.config as cfg
|
||||||
|
|
||||||
|
logger = logging.getLogger()
|
||||||
|
logger.level = logging.DEBUG
|
||||||
|
if not len(logger.handlers):
|
||||||
|
logger.addHandler(logging.StreamHandler(sys.stdout))
|
||||||
|
|
||||||
|
NUM_NODES = 3
|
||||||
|
BASE_PORT = 14792
|
||||||
|
BASE_RPC_PORT = 19792
|
||||||
|
BASE_ZMQ_PORT = 20792
|
||||||
|
PREFIX_SECRET_KEY_REGTEST = 0x2e
|
||||||
|
TEST_HTML_PORT = 1800
|
||||||
|
NMC_NODE = 3
|
||||||
|
BTC_NODE = 4
|
||||||
|
stop_test = False
|
||||||
|
|
||||||
|
|
||||||
|
def prepareOtherDir(datadir, nodeId, conf_file='namecoin.conf'):
|
||||||
|
node_dir = os.path.join(datadir, str(nodeId))
|
||||||
|
if not os.path.exists(node_dir):
|
||||||
|
os.makedirs(node_dir)
|
||||||
|
filePath = os.path.join(node_dir, conf_file)
|
||||||
|
|
||||||
|
with open(filePath, 'w+') as fp:
|
||||||
|
fp.write('regtest=1\n')
|
||||||
|
fp.write('[regtest]\n')
|
||||||
|
fp.write('port=' + str(BASE_PORT + nodeId) + '\n')
|
||||||
|
fp.write('rpcport=' + str(BASE_RPC_PORT + nodeId) + '\n')
|
||||||
|
|
||||||
|
fp.write('daemon=0\n')
|
||||||
|
fp.write('printtoconsole=0\n')
|
||||||
|
fp.write('server=1\n')
|
||||||
|
fp.write('discover=0\n')
|
||||||
|
fp.write('listenonion=0\n')
|
||||||
|
fp.write('bind=127.0.0.1\n')
|
||||||
|
fp.write('findpeers=0\n')
|
||||||
|
fp.write('debug=1\n')
|
||||||
|
fp.write('debugexclude=libevent\n')
|
||||||
|
|
||||||
|
fp.write('acceptnonstdtxn=0\n')
|
||||||
|
|
||||||
|
|
||||||
|
def prepareDir(datadir, nodeId, network_key, network_pubkey):
|
||||||
|
node_dir = os.path.join(datadir, str(nodeId))
|
||||||
|
if not os.path.exists(node_dir):
|
||||||
|
os.makedirs(node_dir)
|
||||||
|
filePath = os.path.join(node_dir, 'particl.conf')
|
||||||
|
|
||||||
|
with open(filePath, 'w+') as fp:
|
||||||
|
fp.write('regtest=1\n')
|
||||||
|
fp.write('[regtest]\n')
|
||||||
|
fp.write('port=' + str(BASE_PORT + nodeId) + '\n')
|
||||||
|
fp.write('rpcport=' + str(BASE_RPC_PORT + nodeId) + '\n')
|
||||||
|
|
||||||
|
fp.write('daemon=0\n')
|
||||||
|
fp.write('printtoconsole=0\n')
|
||||||
|
fp.write('server=1\n')
|
||||||
|
fp.write('discover=0\n')
|
||||||
|
fp.write('listenonion=0\n')
|
||||||
|
fp.write('bind=127.0.0.1\n')
|
||||||
|
fp.write('findpeers=0\n')
|
||||||
|
fp.write('debug=1\n')
|
||||||
|
fp.write('debugexclude=libevent\n')
|
||||||
|
fp.write('zmqpubsmsg=tcp://127.0.0.1:' + str(BASE_ZMQ_PORT + nodeId) + '\n')
|
||||||
|
|
||||||
|
fp.write('acceptnonstdtxn=0\n')
|
||||||
|
fp.write('minstakeinterval=5\n')
|
||||||
|
|
||||||
|
for i in range(0, NUM_NODES):
|
||||||
|
if nodeId == i:
|
||||||
|
continue
|
||||||
|
fp.write('addnode=127.0.0.1:%d\n' % (BASE_PORT + i))
|
||||||
|
|
||||||
|
if nodeId < 2:
|
||||||
|
fp.write('spentindex=1\n')
|
||||||
|
fp.write('txindex=1\n')
|
||||||
|
|
||||||
|
basicswap_dir = os.path.join(datadir, str(nodeId), 'basicswap')
|
||||||
|
if not os.path.exists(basicswap_dir):
|
||||||
|
os.makedirs(basicswap_dir)
|
||||||
|
|
||||||
|
nmcdatadir = os.path.join(datadir, str(NMC_NODE))
|
||||||
|
btcdatadir = os.path.join(datadir, str(BTC_NODE))
|
||||||
|
settings_path = os.path.join(basicswap_dir, 'basicswap.json')
|
||||||
|
settings = {
|
||||||
|
'zmqhost': 'tcp://127.0.0.1',
|
||||||
|
'zmqport': BASE_ZMQ_PORT + nodeId,
|
||||||
|
'htmlhost': 'localhost',
|
||||||
|
'htmlport': 12700 + nodeId,
|
||||||
|
'network_key': network_key,
|
||||||
|
'network_pubkey': network_pubkey,
|
||||||
|
'chainclients': {
|
||||||
|
'particl': {
|
||||||
|
'connection_type': 'rpc',
|
||||||
|
'manage_daemon': False,
|
||||||
|
'rpcport': BASE_RPC_PORT + nodeId,
|
||||||
|
'datadir': node_dir,
|
||||||
|
'bindir': cfg.PARTICL_BINDIR,
|
||||||
|
'blocks_confirmed': 2, # Faster testing
|
||||||
|
},
|
||||||
|
'namecoin': {
|
||||||
|
'connection_type': 'rpc',
|
||||||
|
'manage_daemon': False,
|
||||||
|
'rpcport': BASE_RPC_PORT + NMC_NODE,
|
||||||
|
'datadir': nmcdatadir,
|
||||||
|
'bindir': cfg.NAMECOIN_BINDIR,
|
||||||
|
'use_csv': False,
|
||||||
|
# 'use_segwit': True,
|
||||||
|
},
|
||||||
|
'bitcoin': {
|
||||||
|
'connection_type': 'rpc',
|
||||||
|
'manage_daemon': False,
|
||||||
|
'rpcport': BASE_RPC_PORT + BTC_NODE,
|
||||||
|
'datadir': btcdatadir,
|
||||||
|
'bindir': cfg.BITCOIN_BINDIR,
|
||||||
|
'use_segwit': True,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'check_progress_seconds': 2,
|
||||||
|
'check_watched_seconds': 4,
|
||||||
|
'check_expired_seconds': 60
|
||||||
|
}
|
||||||
|
with open(settings_path, 'w') as fp:
|
||||||
|
json.dump(settings, fp, indent=4)
|
||||||
|
|
||||||
|
|
||||||
|
def startDaemon(nodeId, bin_dir=cfg.PARTICL_BINDIR, daemon_bin=cfg.PARTICLD):
|
||||||
|
node_dir = os.path.join(cfg.DATADIRS, str(nodeId))
|
||||||
|
daemon_bin = os.path.join(bin_dir, daemon_bin)
|
||||||
|
|
||||||
|
args = [daemon_bin, '-datadir=' + node_dir]
|
||||||
|
logging.info('Starting node ' + str(nodeId) + ' ' + daemon_bin + ' ' + '-datadir=' + node_dir)
|
||||||
|
return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
|
||||||
|
|
||||||
|
def partRpc(cmd, node_id=0):
|
||||||
|
return callrpc_cli(cfg.PARTICL_BINDIR, os.path.join(cfg.DATADIRS, str(node_id)), 'regtest', cmd, cfg.PARTICL_CLI)
|
||||||
|
|
||||||
|
|
||||||
|
def btcRpc(cmd):
|
||||||
|
return callrpc_cli(cfg.BITCOIN_BINDIR, os.path.join(cfg.DATADIRS, str(BTC_NODE)), 'regtest', cmd, cfg.BITCOIN_CLI)
|
||||||
|
|
||||||
|
|
||||||
|
def nmcRpc(cmd):
|
||||||
|
return callrpc_cli(cfg.NAMECOIN_BINDIR, os.path.join(cfg.DATADIRS, str(NMC_NODE)), 'regtest', cmd, cfg.NAMECOIN_CLI)
|
||||||
|
|
||||||
|
|
||||||
|
def signal_handler(sig, frame):
|
||||||
|
global stop_test
|
||||||
|
print('signal {} detected.'.format(sig))
|
||||||
|
stop_test = True
|
||||||
|
|
||||||
|
|
||||||
|
def run_loop(self):
|
||||||
|
while not stop_test:
|
||||||
|
time.sleep(1)
|
||||||
|
for c in self.swap_clients:
|
||||||
|
c.update()
|
||||||
|
nmcRpc('generatetoaddress 1 {}'.format(self.nmc_addr))
|
||||||
|
btcRpc('generatetoaddress 1 {}'.format(self.btc_addr))
|
||||||
|
|
||||||
|
|
||||||
|
def waitForRPC(rpc_func, wallet=None):
|
||||||
|
for i in range(5):
|
||||||
|
try:
|
||||||
|
rpc_func('getwalletinfo')
|
||||||
|
return
|
||||||
|
except Exception as ex:
|
||||||
|
logging.warning('Can\'t connect to daemon RPC: %s. Trying again in %d second/s.', str(ex), (1 + i))
|
||||||
|
time.sleep(1 + i)
|
||||||
|
raise ValueError('waitForRPC failed')
|
||||||
|
|
||||||
|
|
||||||
|
class Test(unittest.TestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super(Test, cls).setUpClass()
|
||||||
|
|
||||||
|
eckey = ECKey()
|
||||||
|
eckey.generate()
|
||||||
|
cls.network_key = toWIF(PREFIX_SECRET_KEY_REGTEST, eckey.get_bytes())
|
||||||
|
cls.network_pubkey = eckey.get_pubkey().get_bytes().hex()
|
||||||
|
|
||||||
|
if os.path.isdir(cfg.DATADIRS):
|
||||||
|
logging.info('Removing ' + cfg.DATADIRS)
|
||||||
|
shutil.rmtree(cfg.DATADIRS)
|
||||||
|
|
||||||
|
for i in range(NUM_NODES):
|
||||||
|
prepareDir(cfg.DATADIRS, i, cls.network_key, cls.network_pubkey)
|
||||||
|
|
||||||
|
prepareOtherDir(cfg.DATADIRS, NMC_NODE)
|
||||||
|
prepareOtherDir(cfg.DATADIRS, BTC_NODE, 'bitcoin.conf')
|
||||||
|
|
||||||
|
cls.daemons = []
|
||||||
|
cls.swap_clients = []
|
||||||
|
|
||||||
|
cls.daemons.append(startDaemon(BTC_NODE, cfg.BITCOIN_BINDIR, cfg.BITCOIND))
|
||||||
|
logging.info('Started %s %d', cfg.BITCOIND, cls.daemons[-1].pid)
|
||||||
|
cls.daemons.append(startDaemon(NMC_NODE, cfg.NAMECOIN_BINDIR, cfg.NAMECOIND))
|
||||||
|
logging.info('Started %s %d', cfg.NAMECOIND, cls.daemons[-1].pid)
|
||||||
|
|
||||||
|
for i in range(NUM_NODES):
|
||||||
|
cls.daemons.append(startDaemon(i))
|
||||||
|
logging.info('Started %s %d', cfg.PARTICLD, cls.daemons[-1].pid)
|
||||||
|
time.sleep(1)
|
||||||
|
for i in range(NUM_NODES):
|
||||||
|
basicswap_dir = os.path.join(os.path.join(cfg.DATADIRS, str(i)), 'basicswap')
|
||||||
|
settings_path = os.path.join(basicswap_dir, 'basicswap.json')
|
||||||
|
with open(settings_path) as fs:
|
||||||
|
settings = json.load(fs)
|
||||||
|
fp = open(os.path.join(basicswap_dir, 'basicswap.log'), 'w')
|
||||||
|
cls.swap_clients.append(BasicSwap(fp, basicswap_dir, settings, 'regtest', log_name='BasicSwap{}'.format(i)))
|
||||||
|
cls.swap_clients[-1].start()
|
||||||
|
cls.swap_clients[0].callrpc('extkeyimportmaster', ['abandon baby cabbage dad eager fabric gadget habit ice kangaroo lab absorb'])
|
||||||
|
cls.swap_clients[1].callrpc('extkeyimportmaster', ['pact mammal barrel matrix local final lecture chunk wasp survey bid various book strong spread fall ozone daring like topple door fatigue limb olympic', '', 'true'])
|
||||||
|
cls.swap_clients[1].callrpc('getnewextaddress', ['lblExtTest'])
|
||||||
|
cls.swap_clients[1].callrpc('rescanblockchain')
|
||||||
|
|
||||||
|
waitForRPC(nmcRpc)
|
||||||
|
num_blocks = 500
|
||||||
|
logging.info('Mining %d namecoin blocks', num_blocks)
|
||||||
|
cls.nmc_addr = nmcRpc('getnewaddress mining_addr legacy')
|
||||||
|
nmcRpc('generatetoaddress {} {}'.format(num_blocks, cls.nmc_addr))
|
||||||
|
|
||||||
|
ro = nmcRpc('getblockchaininfo')
|
||||||
|
try:
|
||||||
|
assert(ro['bip9_softforks']['csv']['status'] == 'active')
|
||||||
|
except Exception:
|
||||||
|
logging.info('nmc: csv is not active')
|
||||||
|
try:
|
||||||
|
assert(ro['bip9_softforks']['segwit']['status'] == 'active')
|
||||||
|
except Exception:
|
||||||
|
logging.info('nmc: segwit is not active')
|
||||||
|
|
||||||
|
waitForRPC(btcRpc)
|
||||||
|
cls.btc_addr = btcRpc('getnewaddress mining_addr bech32')
|
||||||
|
logging.info('Mining %d bitcoin blocks to %s', num_blocks, cls.btc_addr)
|
||||||
|
btcRpc('generatetoaddress {} {}'.format(num_blocks, cls.btc_addr))
|
||||||
|
|
||||||
|
ro = btcRpc('getblockchaininfo')
|
||||||
|
assert(ro['bip9_softforks']['csv']['status'] == 'active')
|
||||||
|
assert(ro['bip9_softforks']['segwit']['status'] == 'active')
|
||||||
|
|
||||||
|
ro = nmcRpc('getwalletinfo')
|
||||||
|
print('nmcRpc', ro)
|
||||||
|
|
||||||
|
cls.http_threads = []
|
||||||
|
host = '0.0.0.0' # All interfaces (docker)
|
||||||
|
for i in range(3):
|
||||||
|
t = HttpThread(cls.swap_clients[i].fp, host, TEST_HTML_PORT + i, False, cls.swap_clients[i])
|
||||||
|
cls.http_threads.append(t)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
cls.update_thread = threading.Thread(target=run_loop, args=(cls,))
|
||||||
|
cls.update_thread.start()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
global stop_test
|
||||||
|
logging.info('Finalising')
|
||||||
|
stop_test = True
|
||||||
|
cls.update_thread.join()
|
||||||
|
for t in cls.http_threads:
|
||||||
|
t.stop()
|
||||||
|
t.join()
|
||||||
|
for c in cls.swap_clients:
|
||||||
|
c.fp.close()
|
||||||
|
for d in cls.daemons:
|
||||||
|
logging.info('Terminating %d', d.pid)
|
||||||
|
d.terminate()
|
||||||
|
d.wait(timeout=10)
|
||||||
|
if d.stdout:
|
||||||
|
d.stdout.close()
|
||||||
|
if d.stderr:
|
||||||
|
d.stderr.close()
|
||||||
|
if d.stdin:
|
||||||
|
d.stdin.close()
|
||||||
|
|
||||||
|
super(Test, cls).tearDownClass()
|
||||||
|
|
||||||
|
def wait_for_offer(self, swap_client, offer_id):
|
||||||
|
logging.info('wait_for_offer %s', offer_id.hex())
|
||||||
|
for i in range(20):
|
||||||
|
time.sleep(1)
|
||||||
|
offers = swap_client.listOffers()
|
||||||
|
for offer in offers:
|
||||||
|
if offer.offer_id == offer_id:
|
||||||
|
return
|
||||||
|
raise ValueError('wait_for_offer timed out.')
|
||||||
|
|
||||||
|
def wait_for_bid(self, swap_client, bid_id):
|
||||||
|
logging.info('wait_for_bid %s', bid_id.hex())
|
||||||
|
for i in range(20):
|
||||||
|
time.sleep(1)
|
||||||
|
bids = swap_client.listBids()
|
||||||
|
for bid in bids:
|
||||||
|
if bid.bid_id == bid_id and bid.was_received:
|
||||||
|
return
|
||||||
|
raise ValueError('wait_for_bid timed out.')
|
||||||
|
|
||||||
|
def wait_for_in_progress(self, swap_client, bid_id, sent=False):
|
||||||
|
logging.info('wait_for_in_progress %s', bid_id.hex())
|
||||||
|
for i in range(20):
|
||||||
|
time.sleep(1)
|
||||||
|
swaps = swap_client.listSwapsInProgress()
|
||||||
|
for b in swaps:
|
||||||
|
if b[0] == bid_id:
|
||||||
|
return
|
||||||
|
raise ValueError('wait_for_in_progress timed out.')
|
||||||
|
|
||||||
|
def wait_for_bid_state(self, swap_client, bid_id, state, sent=False, seconds_for=30):
|
||||||
|
logging.info('wait_for_bid_state %s %s', bid_id.hex(), str(state))
|
||||||
|
for i in range(seconds_for):
|
||||||
|
time.sleep(1)
|
||||||
|
bid = swap_client.getBid(bid_id)
|
||||||
|
if bid.state >= state:
|
||||||
|
return
|
||||||
|
raise ValueError('wait_for_bid_state timed out.')
|
||||||
|
|
||||||
|
def wait_for_bid_tx_state(self, swap_client, bid_id, initiate_state, participate_state, seconds_for=30):
|
||||||
|
logging.info('wait_for_bid_tx_state %s %s %s', bid_id.hex(), str(initiate_state), str(participate_state))
|
||||||
|
for i in range(seconds_for):
|
||||||
|
time.sleep(1)
|
||||||
|
bid = swap_client.getBid(bid_id)
|
||||||
|
if (initiate_state is None or bid.initiate_txn_state == initiate_state) \
|
||||||
|
and (participate_state is None or bid.participate_txn_state == participate_state):
|
||||||
|
return
|
||||||
|
raise ValueError('wait_for_bid_tx_state timed out.')
|
||||||
|
|
||||||
|
def test_02_part_ltc(self):
|
||||||
|
swap_clients = self.swap_clients
|
||||||
|
|
||||||
|
logging.info('---------- Test PART to NMC')
|
||||||
|
offer_id = swap_clients[0].postOffer(Coins.PART, Coins.NMC, 100 * COIN, 0.1 * COIN, 100 * COIN, SwapTypes.SELLER_FIRST, ABS_LOCK_TIME)
|
||||||
|
|
||||||
|
self.wait_for_offer(swap_clients[1], offer_id)
|
||||||
|
offers = swap_clients[1].listOffers()
|
||||||
|
assert(len(offers) == 1)
|
||||||
|
for offer in offers:
|
||||||
|
if offer.offer_id == offer_id:
|
||||||
|
bid_id = swap_clients[1].postBid(offer_id, offer.amount_from)
|
||||||
|
|
||||||
|
self.wait_for_bid(swap_clients[0], bid_id)
|
||||||
|
|
||||||
|
swap_clients[0].acceptBid(bid_id)
|
||||||
|
|
||||||
|
self.wait_for_in_progress(swap_clients[1], bid_id, sent=True)
|
||||||
|
|
||||||
|
self.wait_for_bid_state(swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, seconds_for=60)
|
||||||
|
self.wait_for_bid_state(swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, seconds_for=60)
|
||||||
|
|
||||||
|
js_0 = json.loads(urlopen('http://localhost:1800/json').read())
|
||||||
|
js_1 = json.loads(urlopen('http://localhost:1801/json').read())
|
||||||
|
assert(js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
|
||||||
|
assert(js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0)
|
||||||
|
|
||||||
|
def test_03_nmc_part(self):
|
||||||
|
swap_clients = self.swap_clients
|
||||||
|
|
||||||
|
logging.info('---------- Test NMC to PART')
|
||||||
|
offer_id = swap_clients[1].postOffer(Coins.NMC, Coins.PART, 10 * COIN, 9.0 * COIN, 10 * COIN, SwapTypes.SELLER_FIRST, ABS_LOCK_TIME)
|
||||||
|
|
||||||
|
self.wait_for_offer(swap_clients[0], offer_id)
|
||||||
|
offers = swap_clients[0].listOffers()
|
||||||
|
for offer in offers:
|
||||||
|
if offer.offer_id == offer_id:
|
||||||
|
bid_id = swap_clients[0].postBid(offer_id, offer.amount_from)
|
||||||
|
|
||||||
|
self.wait_for_bid(swap_clients[1], bid_id)
|
||||||
|
swap_clients[1].acceptBid(bid_id)
|
||||||
|
|
||||||
|
self.wait_for_in_progress(swap_clients[0], bid_id, sent=True)
|
||||||
|
|
||||||
|
self.wait_for_bid_state(swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, sent=True, seconds_for=60)
|
||||||
|
self.wait_for_bid_state(swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, seconds_for=60)
|
||||||
|
|
||||||
|
js_0 = json.loads(urlopen('http://localhost:1800/json').read())
|
||||||
|
js_1 = json.loads(urlopen('http://localhost:1801/json').read())
|
||||||
|
assert(js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
|
||||||
|
assert(js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0)
|
||||||
|
|
||||||
|
def test_04_nmc_btc(self):
|
||||||
|
swap_clients = self.swap_clients
|
||||||
|
|
||||||
|
logging.info('---------- Test NMC to BTC')
|
||||||
|
offer_id = swap_clients[0].postOffer(Coins.NMC, Coins.BTC, 10 * COIN, 0.1 * COIN, 10 * COIN, SwapTypes.SELLER_FIRST, ABS_LOCK_TIME)
|
||||||
|
|
||||||
|
self.wait_for_offer(swap_clients[1], offer_id)
|
||||||
|
offers = swap_clients[1].listOffers()
|
||||||
|
for offer in offers:
|
||||||
|
if offer.offer_id == offer_id:
|
||||||
|
bid_id = swap_clients[1].postBid(offer_id, offer.amount_from)
|
||||||
|
|
||||||
|
self.wait_for_bid(swap_clients[0], bid_id)
|
||||||
|
swap_clients[0].acceptBid(bid_id)
|
||||||
|
|
||||||
|
self.wait_for_in_progress(swap_clients[1], bid_id, sent=True)
|
||||||
|
|
||||||
|
self.wait_for_bid_state(swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, seconds_for=60)
|
||||||
|
self.wait_for_bid_state(swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, seconds_for=60)
|
||||||
|
|
||||||
|
js_0bid = json.loads(urlopen('http://localhost:1800/json/bids/{}'.format(bid_id.hex())).read())
|
||||||
|
|
||||||
|
js_0 = json.loads(urlopen('http://localhost:1800/json').read())
|
||||||
|
js_1 = json.loads(urlopen('http://localhost:1801/json').read())
|
||||||
|
|
||||||
|
assert(js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
|
||||||
|
assert(js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0)
|
||||||
|
|
||||||
|
def test_05_refund(self):
|
||||||
|
# Seller submits initiate txn, buyer doesn't respond
|
||||||
|
swap_clients = self.swap_clients
|
||||||
|
|
||||||
|
logging.info('---------- Test refund, NMC to BTC')
|
||||||
|
|
||||||
|
# Note the lock value is absolute.
|
||||||
|
offer_id = swap_clients[0].postOffer(Coins.NMC, Coins.BTC, 10 * COIN, 0.1 * COIN, 10 * COIN, SwapTypes.SELLER_FIRST,
|
||||||
|
ABS_LOCK_BLOCKS, 20)
|
||||||
|
|
||||||
|
self.wait_for_offer(swap_clients[1], offer_id)
|
||||||
|
offers = swap_clients[1].listOffers()
|
||||||
|
for offer in offers:
|
||||||
|
if offer.offer_id == offer_id:
|
||||||
|
bid_id = swap_clients[1].postBid(offer_id, offer.amount_from)
|
||||||
|
|
||||||
|
self.wait_for_bid(swap_clients[0], bid_id)
|
||||||
|
swap_clients[1].abandonBid(bid_id)
|
||||||
|
swap_clients[0].acceptBid(bid_id)
|
||||||
|
|
||||||
|
self.wait_for_bid_state(swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, seconds_for=60)
|
||||||
|
self.wait_for_bid_state(swap_clients[1], bid_id, BidStates.BID_ABANDONED, sent=True, seconds_for=60)
|
||||||
|
|
||||||
|
js_0 = json.loads(urlopen('http://localhost:1800/json').read())
|
||||||
|
js_1 = json.loads(urlopen('http://localhost:1801/json').read())
|
||||||
|
assert(js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
|
||||||
|
assert(js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0)
|
||||||
|
|
||||||
|
def test_06_self_bid(self):
|
||||||
|
swap_clients = self.swap_clients
|
||||||
|
|
||||||
|
logging.info('---------- Test same client, BTC to NMC')
|
||||||
|
|
||||||
|
js_0_before = json.loads(urlopen('http://localhost:1800/json').read())
|
||||||
|
|
||||||
|
offer_id = swap_clients[0].postOffer(Coins.NMC, Coins.BTC, 10 * COIN, 10 * COIN, 10 * COIN, SwapTypes.SELLER_FIRST, ABS_LOCK_TIME)
|
||||||
|
|
||||||
|
self.wait_for_offer(swap_clients[0], offer_id)
|
||||||
|
offers = swap_clients[0].listOffers()
|
||||||
|
for offer in offers:
|
||||||
|
if offer.offer_id == offer_id:
|
||||||
|
bid_id = swap_clients[0].postBid(offer_id, offer.amount_from)
|
||||||
|
|
||||||
|
self.wait_for_bid(swap_clients[0], bid_id)
|
||||||
|
swap_clients[0].acceptBid(bid_id)
|
||||||
|
|
||||||
|
self.wait_for_bid_tx_state(swap_clients[0], bid_id, TxStates.TX_REDEEMED, TxStates.TX_REDEEMED, seconds_for=60)
|
||||||
|
self.wait_for_bid_state(swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, seconds_for=60)
|
||||||
|
|
||||||
|
js_0 = json.loads(urlopen('http://localhost:1800/json').read())
|
||||||
|
assert(js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
|
||||||
|
assert(js_0['num_recv_bids'] == js_0_before['num_recv_bids'] + 1 and js_0['num_sent_bids'] == js_0_before['num_sent_bids'] + 1)
|
||||||
|
|
||||||
|
def test_07_error(self):
|
||||||
|
swap_clients = self.swap_clients
|
||||||
|
|
||||||
|
logging.info('---------- Test error, BTC to NMC, set fee above bid value')
|
||||||
|
|
||||||
|
js_0_before = json.loads(urlopen('http://localhost:1800/json').read())
|
||||||
|
|
||||||
|
offer_id = swap_clients[0].postOffer(Coins.NMC, Coins.BTC, 0.001 * COIN, 1.0 * COIN, 0.001 * COIN, SwapTypes.SELLER_FIRST, ABS_LOCK_TIME)
|
||||||
|
|
||||||
|
self.wait_for_offer(swap_clients[0], offer_id)
|
||||||
|
offers = swap_clients[0].listOffers()
|
||||||
|
for offer in offers:
|
||||||
|
if offer.offer_id == offer_id:
|
||||||
|
bid_id = swap_clients[0].postBid(offer_id, offer.amount_from)
|
||||||
|
|
||||||
|
self.wait_for_bid(swap_clients[0], bid_id)
|
||||||
|
swap_clients[0].acceptBid(bid_id)
|
||||||
|
swap_clients[0].coin_clients[Coins.BTC]['override_feerate'] = 10.0
|
||||||
|
swap_clients[0].coin_clients[Coins.NMC]['override_feerate'] = 10.0
|
||||||
|
|
||||||
|
self.wait_for_bid_state(swap_clients[0], bid_id, BidStates.BID_ERROR, seconds_for=60)
|
||||||
|
|
||||||
|
def pass_99_delay(self):
|
||||||
|
global stop_test
|
||||||
|
logging.info('Delay')
|
||||||
|
for i in range(60 * 5):
|
||||||
|
if stop_test:
|
||||||
|
break
|
||||||
|
time.sleep(1)
|
||||||
|
print('delay', i)
|
||||||
|
stop_test = True
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
Loading…
Reference in a new issue