system: Allow preselecting inputs for atomic swaps.

This commit is contained in:
tecnovert 2022-12-05 17:04:23 +02:00
parent 23e89882a4
commit c90fa6f2c6
No known key found for this signature in database
GPG Key ID: 8ED6D8750C4E3F93
16 changed files with 334 additions and 72 deletions

View File

@ -1,3 +1,3 @@
name = "basicswap" name = "basicswap"
__version__ = "0.11.51" __version__ = "0.11.52"

View File

@ -26,6 +26,8 @@ import sqlalchemy as sa
import collections import collections
import concurrent.futures import concurrent.futures
from typing import Optional
from sqlalchemy.orm import sessionmaker, scoped_session from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy.orm.session import close_all_sessions from sqlalchemy.orm.session import close_all_sessions
@ -92,6 +94,7 @@ from .db import (
Offer, Offer,
Bid, Bid,
SwapTx, SwapTx,
PrefundedTx,
PooledAddress, PooledAddress,
SentOffer, SentOffer,
SmsgAddress, SmsgAddress,
@ -116,6 +119,7 @@ from .explorers import (
import basicswap.config as cfg import basicswap.config as cfg
import basicswap.network as bsn import basicswap.network as bsn
import basicswap.protocols.atomic_swap_1 as atomic_swap_1 import basicswap.protocols.atomic_swap_1 as atomic_swap_1
import basicswap.protocols.xmr_swap_1 as xmr_swap_1
from .basicswap_util import ( from .basicswap_util import (
KeyTypes, KeyTypes,
TxLockTypes, TxLockTypes,
@ -140,9 +144,6 @@ from .basicswap_util import (
isActiveBidState, isActiveBidState,
NotificationTypes as NT, NotificationTypes as NT,
) )
from .protocols.xmr_swap_1 import (
addLockRefundSigs,
recoverNoScriptTxnWithKey)
non_script_type_coins = (Coins.XMR, Coins.PART_ANON) non_script_type_coins = (Coins.XMR, Coins.PART_ANON)
@ -218,6 +219,10 @@ class WatchedTransaction():
class BasicSwap(BaseApp): class BasicSwap(BaseApp):
ws_server = None ws_server = None
protocolInterfaces = {
SwapTypes.SELLER_FIRST: atomic_swap_1.AtomicSwapInterface(),
SwapTypes.XMR_SWAP: xmr_swap_1.XmrSwapInterface(),
}
def __init__(self, fp, data_dir, settings, chain, log_name='BasicSwap'): def __init__(self, fp, data_dir, settings, chain, log_name='BasicSwap'):
super().__init__(fp, data_dir, settings, chain, log_name) super().__init__(fp, data_dir, settings, chain, log_name)
@ -548,6 +553,11 @@ class BasicSwap(BaseApp):
return self.coin_clients[use_coinid][interface_ind] return self.coin_clients[use_coinid][interface_ind]
def pi(self, protocol_ind):
if protocol_ind not in self.protocolInterfaces:
raise ValueError('Unknown protocol_ind {}'.format(int(protocol_ind)))
return self.protocolInterfaces[protocol_ind]
def createInterface(self, coin): def createInterface(self, coin):
if coin == Coins.PART: if coin == Coins.PART:
return PARTInterface(self.coin_clients[coin], self.chain, self) return PARTInterface(self.coin_clients[coin], self.chain, self)
@ -651,10 +661,8 @@ class BasicSwap(BaseApp):
self.log.info('%s Core version %d', ci.coin_name(), core_version) self.log.info('%s Core version %d', ci.coin_name(), core_version)
self.coin_clients[c]['core_version'] = core_version self.coin_clients[c]['core_version'] = core_version
if c == Coins.XMR: thread_func = threadPollXMRChainState if c == Coins.XMR else threadPollChainState
t = threading.Thread(target=threadPollXMRChainState, args=(self, c)) t = threading.Thread(target=thread_func, args=(self, c))
else:
t = threading.Thread(target=threadPollChainState, args=(self, c))
self.threads.append(t) self.threads.append(t)
t.start() t.start()
@ -851,7 +859,7 @@ class BasicSwap(BaseApp):
finally: finally:
self.closeSession(session) self.closeSession(session)
def updateIdentityBidState(self, session, address, bid): def updateIdentityBidState(self, session, address: str, bid) -> None:
identity_stats = session.query(KnownIdentity).filter_by(address=address).first() identity_stats = session.query(KnownIdentity).filter_by(address=address).first()
if not identity_stats: if not identity_stats:
identity_stats = KnownIdentity(address=address, created_at=int(time.time())) identity_stats = KnownIdentity(address=address, created_at=int(time.time()))
@ -870,7 +878,7 @@ class BasicSwap(BaseApp):
identity_stats.updated_at = int(time.time()) identity_stats.updated_at = int(time.time())
session.add(identity_stats) session.add(identity_stats)
def setIntKVInSession(self, str_key, int_val, session): def setIntKVInSession(self, str_key: str, int_val: int, session) -> None:
kv = session.query(DBKVInt).filter_by(key=str_key).first() kv = session.query(DBKVInt).filter_by(key=str_key).first()
if not kv: if not kv:
kv = DBKVInt(key=str_key, value=int_val) kv = DBKVInt(key=str_key, value=int_val)
@ -878,7 +886,7 @@ class BasicSwap(BaseApp):
kv.value = int_val kv.value = int_val
session.add(kv) session.add(kv)
def setIntKV(self, str_key, int_val): def setIntKV(self, str_key: str, int_val: int) -> None:
self.mxDB.acquire() self.mxDB.acquire()
try: try:
session = scoped_session(self.session_factory) session = scoped_session(self.session_factory)
@ -889,7 +897,7 @@ class BasicSwap(BaseApp):
session.remove() session.remove()
self.mxDB.release() self.mxDB.release()
def setStringKV(self, str_key, str_val, session=None): def setStringKV(self, str_key: str, str_val: str, session=None) -> None:
try: try:
use_session = self.openSession(session) use_session = self.openSession(session)
kv = use_session.query(DBKVString).filter_by(key=str_key).first() kv = use_session.query(DBKVString).filter_by(key=str_key).first()
@ -902,7 +910,7 @@ class BasicSwap(BaseApp):
if session is None: if session is None:
self.closeSession(use_session) self.closeSession(use_session)
def getStringKV(self, str_key): def getStringKV(self, str_key: str) -> Optional[str]:
self.mxDB.acquire() self.mxDB.acquire()
try: try:
session = scoped_session(self.session_factory) session = scoped_session(self.session_factory)
@ -915,7 +923,7 @@ class BasicSwap(BaseApp):
session.remove() session.remove()
self.mxDB.release() self.mxDB.release()
def clearStringKV(self, str_key, str_val): def clearStringKV(self, str_key: str, str_val: str) -> None:
with self.mxDB: with self.mxDB:
try: try:
session = scoped_session(self.session_factory) session = scoped_session(self.session_factory)
@ -925,6 +933,19 @@ class BasicSwap(BaseApp):
session.close() session.close()
session.remove() session.remove()
def getPreFundedTx(self, linked_type: int, linked_id: bytes, tx_type: int, session=None) -> Optional[bytes]:
try:
use_session = self.openSession(session)
tx = use_session.query(PrefundedTx).filter_by(linked_type=linked_type, linked_id=linked_id, tx_type=tx_type, used_by=None).first()
if not tx:
return None
tx.used_by = linked_id
use_session.add(tx)
return tx.tx_data
finally:
if session is None:
self.closeSession(use_session)
def activateBid(self, session, bid): def activateBid(self, session, bid):
if bid.bid_id in self.swaps_in_progress: if bid.bid_id in self.swaps_in_progress:
self.log.debug('Bid %s is already in progress', bid.bid_id.hex()) self.log.debug('Bid %s is already in progress', bid.bid_id.hex())
@ -1366,6 +1387,16 @@ class BasicSwap(BaseApp):
repeat_count=0) repeat_count=0)
session.add(auto_link) session.add(auto_link)
if 'prefunded_itx' in extra_options:
prefunded_tx = PrefundedTx(
active_ind=1,
created_at=offer_created_at,
linked_type=Concepts.OFFER,
linked_id=offer_id,
tx_type=TxTypes.ITX_PRE_FUNDED,
tx_data=extra_options['prefunded_itx'])
session.add(prefunded_tx)
session.add(offer) session.add(offer)
session.add(SentOffer(offer_id=offer_id)) session.add(SentOffer(offer_id=offer_id))
session.commit() session.commit()
@ -2147,7 +2178,8 @@ class BasicSwap(BaseApp):
bid.pkhash_seller = pkhash_refund bid.pkhash_seller = pkhash_refund
txn = self.createInitiateTxn(coin_from, bid_id, bid, script) prefunded_tx = self.getPreFundedTx(Concepts.OFFER, offer.offer_id, TxTypes.ITX_PRE_FUNDED)
txn = self.createInitiateTxn(coin_from, bid_id, bid, script, prefunded_tx)
# 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, offer, bid, script) refund_txn = self.createRefundTxn(coin_from, txn, offer, bid, script)
@ -2532,14 +2564,14 @@ class BasicSwap(BaseApp):
session.remove() session.remove()
self.mxDB.release() self.mxDB.release()
def setBidError(self, bid_id, bid, error_str, save_bid=True, xmr_swap=None): def setBidError(self, bid_id, bid, error_str, save_bid=True, xmr_swap=None) -> None:
self.log.error('Bid %s - Error: %s', bid_id.hex(), error_str) self.log.error('Bid %s - Error: %s', bid_id.hex(), error_str)
bid.setState(BidStates.BID_ERROR) bid.setState(BidStates.BID_ERROR)
bid.state_note = 'error msg: ' + error_str bid.state_note = 'error msg: ' + error_str
if save_bid: if save_bid:
self.saveBid(bid_id, bid, xmr_swap=xmr_swap) self.saveBid(bid_id, bid, xmr_swap=xmr_swap)
def createInitiateTxn(self, coin_type, bid_id, bid, initiate_script): def createInitiateTxn(self, coin_type, bid_id, bid, initiate_script, prefunded_tx=None) -> Optional[str]:
if self.coin_clients[coin_type]['connection_type'] != 'rpc': if self.coin_clients[coin_type]['connection_type'] != 'rpc':
return None return None
ci = self.ci(coin_type) ci = self.ci(coin_type)
@ -2550,6 +2582,10 @@ class BasicSwap(BaseApp):
addr_to = ci.encode_p2sh(initiate_script) addr_to = ci.encode_p2sh(initiate_script)
self.log.debug('Create initiate txn for coin %s to %s for bid %s', str(coin_type), addr_to, bid_id.hex()) self.log.debug('Create initiate txn for coin %s to %s for bid %s', str(coin_type), addr_to, bid_id.hex())
if prefunded_tx:
pi = self.pi(SwapTypes.SELLER_FIRST)
txn_signed = pi.promoteMockTx(ci, prefunded_tx, initiate_script).hex()
else:
txn_signed = ci.createRawSignedTransaction(addr_to, bid.amount) txn_signed = ci.createRawSignedTransaction(addr_to, bid.amount)
return txn_signed return txn_signed
@ -4560,7 +4596,7 @@ class BasicSwap(BaseApp):
prevout_amount = ci_from.getLockTxSwapOutputValue(bid, xmr_swap) prevout_amount = ci_from.getLockTxSwapOutputValue(bid, xmr_swap)
xmr_swap.af_lock_refund_tx_sig = ci_from.signTx(kaf, xmr_swap.a_lock_refund_tx, 0, xmr_swap.a_lock_tx_script, prevout_amount) xmr_swap.af_lock_refund_tx_sig = ci_from.signTx(kaf, xmr_swap.a_lock_refund_tx, 0, xmr_swap.a_lock_tx_script, prevout_amount)
addLockRefundSigs(self, xmr_swap, ci_from) xmr_swap_1.addLockRefundSigs(self, xmr_swap, ci_from)
msg_buf = XmrBidLockTxSigsMessage( msg_buf = XmrBidLockTxSigsMessage(
bid_msg_id=bid_id, bid_msg_id=bid_id,
@ -4988,7 +5024,7 @@ class BasicSwap(BaseApp):
v = ci_from.verifyTxSig(xmr_swap.a_lock_refund_spend_tx, xmr_swap.af_lock_refund_spend_tx_sig, xmr_swap.pkaf, 0, xmr_swap.a_lock_refund_tx_script, prevout_amount) v = ci_from.verifyTxSig(xmr_swap.a_lock_refund_spend_tx, xmr_swap.af_lock_refund_spend_tx_sig, xmr_swap.pkaf, 0, xmr_swap.a_lock_refund_tx_script, prevout_amount)
ensure(v, 'Invalid signature for lock refund spend txn') ensure(v, 'Invalid signature for lock refund spend txn')
addLockRefundSigs(self, xmr_swap, ci_from) xmr_swap_1.addLockRefundSigs(self, xmr_swap, ci_from)
delay = random.randrange(self.min_delay_event, self.max_delay_event) delay = random.randrange(self.min_delay_event, self.max_delay_event)
self.log.info('Sending coin A lock tx for xmr bid %s in %d seconds', bid_id.hex(), delay) self.log.info('Sending coin A lock tx for xmr bid %s in %d seconds', bid_id.hex(), delay)
@ -5268,7 +5304,7 @@ class BasicSwap(BaseApp):
has_changed = True has_changed = True
if data['kbs_other'] is not None: if data['kbs_other'] is not None:
return recoverNoScriptTxnWithKey(self, bid_id, data['kbs_other']) return xmr_swap_1.recoverNoScriptTxnWithKey(self, bid_id, data['kbs_other'])
if has_changed: if has_changed:
session = scoped_session(self.session_factory) session = scoped_session(self.session_factory)

View File

@ -123,6 +123,8 @@ class TxTypes(IntEnum):
XMR_SWAP_A_LOCK_REFUND_SWIPE = auto() XMR_SWAP_A_LOCK_REFUND_SWIPE = auto()
XMR_SWAP_B_LOCK = auto() XMR_SWAP_B_LOCK = auto()
ITX_PRE_FUNDED = auto()
class ActionTypes(IntEnum): class ActionTypes(IntEnum):
ACCEPT_BID = auto() ACCEPT_BID = auto()
@ -289,6 +291,8 @@ def strTxType(tx_type):
return 'Chain A Lock Refund Swipe Tx' return 'Chain A Lock Refund Swipe Tx'
if tx_type == TxTypes.XMR_SWAP_B_LOCK: if tx_type == TxTypes.XMR_SWAP_B_LOCK:
return 'Chain B Lock Tx' return 'Chain B Lock Tx'
if tx_type == TxTypes.ITX_PRE_FUNDED:
return 'Funded mock initiate tx'
return 'Unknown' return 'Unknown'

View File

@ -12,7 +12,7 @@ from enum import IntEnum, auto
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
CURRENT_DB_VERSION = 16 CURRENT_DB_VERSION = 17
CURRENT_DB_DATA_VERSION = 2 CURRENT_DB_DATA_VERSION = 2
Base = declarative_base() Base = declarative_base()
@ -221,6 +221,19 @@ class SwapTx(Base):
self.states = (self.states if self.states is not None else bytes()) + struct.pack('<iq', new_state, int(time.time())) self.states = (self.states if self.states is not None else bytes()) + struct.pack('<iq', new_state, int(time.time()))
class PrefundedTx(Base):
__tablename__ = 'prefunded_transactions'
record_id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
active_ind = sa.Column(sa.Integer)
created_at = sa.Column(sa.BigInteger)
linked_type = sa.Column(sa.Integer)
linked_id = sa.Column(sa.LargeBinary)
tx_type = sa.Column(sa.Integer) # TxTypes
tx_data = sa.Column(sa.LargeBinary)
used_by = sa.Column(sa.LargeBinary)
class PooledAddress(Base): class PooledAddress(Base):
__tablename__ = 'addresspool' __tablename__ = 'addresspool'

View File

@ -225,6 +225,19 @@ def upgradeDatabase(self, db_version):
event_data BLOB, event_data BLOB,
created_at BIGINT, created_at BIGINT,
PRIMARY KEY (record_id))''') PRIMARY KEY (record_id))''')
elif current_version == 16:
db_version += 1
session.execute('''
CREATE TABLE prefunded_transactions (
record_id INTEGER NOT NULL,
active_ind INTEGER,
created_at BIGINT,
linked_type INTEGER,
linked_id BLOB,
tx_type INTEGER,
tx_data BLOB,
used_by BLOB,
PRIMARY KEY (record_id))''')
if current_version != db_version: if current_version != db_version:
self.db_version = db_version self.db_version = db_version

View File

@ -12,6 +12,7 @@ import hashlib
import logging import logging
import traceback import traceback
from io import BytesIO from io import BytesIO
from basicswap.contrib.test_framework import segwit_addr from basicswap.contrib.test_framework import segwit_addr
from basicswap.util import ( from basicswap.util import (
@ -64,6 +65,7 @@ from basicswap.contrib.test_framework.script import (
OP_CHECKMULTISIG, OP_CHECKMULTISIG,
OP_CHECKSEQUENCEVERIFY, OP_CHECKSEQUENCEVERIFY,
OP_DROP, OP_DROP,
OP_HASH160, OP_EQUAL,
SIGHASH_ALL, SIGHASH_ALL,
SegwitV0SignatureHash, SegwitV0SignatureHash,
hash160) hash160)
@ -244,7 +246,7 @@ class BTCInterface(CoinInterface):
def getBlockHeader(self, block_hash): def getBlockHeader(self, block_hash):
return self.rpc_callback('getblockheader', [block_hash]) return self.rpc_callback('getblockheader', [block_hash])
def getBlockHeaderAt(self, time, block_after=False): def getBlockHeaderAt(self, time: int, block_after=False):
blockchaininfo = self.rpc_callback('getblockchaininfo') blockchaininfo = self.rpc_callback('getblockchaininfo')
last_block_header = self.rpc_callback('getblockheader', [blockchaininfo['bestblockhash']]) last_block_header = self.rpc_callback('getblockheader', [blockchaininfo['bestblockhash']])
@ -294,24 +296,24 @@ class BTCInterface(CoinInterface):
finally: finally:
self.close_rpc(rpc_conn) self.close_rpc(rpc_conn)
def getWalletSeedID(self): def getWalletSeedID(self) -> str:
return self.rpc_callback('getwalletinfo')['hdseedid'] return self.rpc_callback('getwalletinfo')['hdseedid']
def checkExpectedSeed(self, expect_seedid): def checkExpectedSeed(self, expect_seedid) -> bool:
self._expect_seedid_hex = expect_seedid self._expect_seedid_hex = expect_seedid
return expect_seedid == self.getWalletSeedID() return expect_seedid == self.getWalletSeedID()
def getNewAddress(self, use_segwit, label='swap_receive'): def getNewAddress(self, use_segwit: bool, label: str = 'swap_receive') -> str:
args = [label] args = [label]
if use_segwit: if use_segwit:
args.append('bech32') args.append('bech32')
return self.rpc_callback('getnewaddress', args) return self.rpc_callback('getnewaddress', args)
def isAddressMine(self, address): def isAddressMine(self, address: str) -> bool:
addr_info = self.rpc_callback('getaddressinfo', [address]) addr_info = self.rpc_callback('getaddressinfo', [address])
return addr_info['ismine'] return addr_info['ismine']
def checkAddressMine(self, address): def checkAddressMine(self, address: str) -> None:
addr_info = self.rpc_callback('getaddressinfo', [address]) addr_info = self.rpc_callback('getaddressinfo', [address])
ensure(addr_info['ismine'], 'ismine is false') ensure(addr_info['ismine'], 'ismine is false')
ensure(addr_info['hdseedid'] == self._expect_seedid_hex, 'unexpected seedid') ensure(addr_info['hdseedid'] == self._expect_seedid_hex, 'unexpected seedid')
@ -914,7 +916,7 @@ class BTCInterface(CoinInterface):
def encodeTx(self, tx): def encodeTx(self, tx):
return tx.serialize() return tx.serialize()
def loadTx(self, tx_bytes): def loadTx(self, tx_bytes) -> CTransaction:
# Load tx from bytes to internal representation # Load tx from bytes to internal representation
tx = CTransaction() tx = CTransaction()
tx.deserialize(BytesIO(tx_bytes)) tx.deserialize(BytesIO(tx_bytes))
@ -963,23 +965,23 @@ class BTCInterface(CoinInterface):
# TODO: filter errors # TODO: filter errors
return None return None
def setTxSignature(self, tx_bytes, stack): def setTxSignature(self, tx_bytes, stack) -> bytes:
tx = self.loadTx(tx_bytes) tx = self.loadTx(tx_bytes)
tx.wit.vtxinwit.clear() tx.wit.vtxinwit.clear()
tx.wit.vtxinwit.append(CTxInWitness()) tx.wit.vtxinwit.append(CTxInWitness())
tx.wit.vtxinwit[0].scriptWitness.stack = stack tx.wit.vtxinwit[0].scriptWitness.stack = stack
return tx.serialize() return tx.serialize()
def stripTxSignature(self, tx_bytes): def stripTxSignature(self, tx_bytes) -> bytes:
tx = self.loadTx(tx_bytes) tx = self.loadTx(tx_bytes)
tx.wit.vtxinwit.clear() tx.wit.vtxinwit.clear()
return tx.serialize() return tx.serialize()
def extractLeaderSig(self, tx_bytes): def extractLeaderSig(self, tx_bytes) -> bytes:
tx = self.loadTx(tx_bytes) tx = self.loadTx(tx_bytes)
return tx.wit.vtxinwit[0].scriptWitness.stack[1] return tx.wit.vtxinwit[0].scriptWitness.stack[1]
def extractFollowerSig(self, tx_bytes): def extractFollowerSig(self, tx_bytes) -> bytes:
tx = self.loadTx(tx_bytes) tx = self.loadTx(tx_bytes)
return tx.wit.vtxinwit[0].scriptWitness.stack[2] return tx.wit.vtxinwit[0].scriptWitness.stack[2]
@ -1142,7 +1144,7 @@ class BTCInterface(CoinInterface):
rv = pubkey.verify_compact(sig, message_hash, hasher=None) rv = pubkey.verify_compact(sig, message_hash, hasher=None)
assert (rv is True) assert (rv is True)
def verifyMessage(self, address, message, signature, message_magic=None) -> bool: def verifyMessage(self, address: str, message: str, signature: str, message_magic: str = None) -> bool:
if message_magic is None: if message_magic is None:
message_magic = self.chainparams()['message_magic'] message_magic = self.chainparams()['message_magic']
@ -1209,13 +1211,13 @@ class BTCInterface(CoinInterface):
length += 1 # flags length += 1 # flags
return length return length
def describeTx(self, tx_hex): def describeTx(self, tx_hex: str):
return self.rpc_callback('decoderawtransaction', [tx_hex]) return self.rpc_callback('decoderawtransaction', [tx_hex])
def getSpendableBalance(self): def getSpendableBalance(self):
return self.make_int(self.rpc_callback('getbalances')['mine']['trusted']) return self.make_int(self.rpc_callback('getbalances')['mine']['trusted'])
def createUTXO(self, value_sats): def createUTXO(self, value_sats: int):
# Create a new address and send value_sats to it # Create a new address and send value_sats to it
spendable_balance = self.getSpendableBalance() spendable_balance = self.getSpendableBalance()
@ -1225,18 +1227,22 @@ class BTCInterface(CoinInterface):
address = self.getNewAddress(self._use_segwit, 'create_utxo') address = self.getNewAddress(self._use_segwit, 'create_utxo')
return self.withdrawCoin(self.format_amount(value_sats), address, False), address return self.withdrawCoin(self.format_amount(value_sats), address, False), address
def createRawSignedTransaction(self, addr_to, amount): def createRawFundedTransaction(self, addr_to: str, amount: int, sub_fee: bool = False, lock_unspents: bool = True) -> str:
txn = self.rpc_callback('createrawtransaction', [[], {addr_to: self.format_amount(amount)}]) txn = self.rpc_callback('createrawtransaction', [[], {addr_to: self.format_amount(amount)}])
options = { options = {
'lockUnspents': True, 'lockUnspents': lock_unspents,
'conf_target': self._conf_target, 'conf_target': self._conf_target,
} }
txn_funded = self.rpc_callback('fundrawtransaction', [txn, options])['hex'] if sub_fee:
txn_signed = self.rpc_callback('signrawtransactionwithwallet', [txn_funded])['hex'] options['subtractFeeFromOutputs'] = [0,]
return txn_signed return self.rpc_callback('fundrawtransaction', [txn, options])['hex']
def getBlockWithTxns(self, block_hash): def createRawSignedTransaction(self, addr_to, amount) -> str:
txn_funded = self.createRawFundedTransaction(addr_to, amount)
return self.rpc_callback('signrawtransactionwithwallet', [txn_funded])['hex']
def getBlockWithTxns(self, block_hash: str):
return self.rpc_callback('getblock', [block_hash, 2]) return self.rpc_callback('getblock', [block_hash, 2])
def getUnspentsByAddr(self): def getUnspentsByAddr(self):
@ -1248,7 +1254,7 @@ class BTCInterface(CoinInterface):
unspent_addr[u['address']] = unspent_addr.get(u['address'], 0) + self.make_int(u['amount'], r=1) unspent_addr[u['address']] = unspent_addr.get(u['address'], 0) + self.make_int(u['amount'], r=1)
return unspent_addr return unspent_addr
def getUTXOBalance(self, address): def getUTXOBalance(self, address: str):
num_blocks = self.rpc_callback('getblockcount') num_blocks = self.rpc_callback('getblockcount')
sum_unspent = 0 sum_unspent = 0
@ -1292,11 +1298,11 @@ class BTCInterface(CoinInterface):
return self.getUTXOBalance(address) return self.getUTXOBalance(address)
def isWalletEncrypted(self): def isWalletEncrypted(self) -> bool:
wallet_info = self.rpc_callback('getwalletinfo') wallet_info = self.rpc_callback('getwalletinfo')
return 'unlocked_until' in wallet_info return 'unlocked_until' in wallet_info
def isWalletLocked(self): def isWalletLocked(self) -> bool:
wallet_info = self.rpc_callback('getwalletinfo') wallet_info = self.rpc_callback('getwalletinfo')
if 'unlocked_until' in wallet_info and wallet_info['unlocked_until'] <= 0: if 'unlocked_until' in wallet_info and wallet_info['unlocked_until'] <= 0:
return True return True
@ -1308,7 +1314,7 @@ class BTCInterface(CoinInterface):
locked = encrypted and wallet_info['unlocked_until'] <= 0 locked = encrypted and wallet_info['unlocked_until'] <= 0
return encrypted, locked return encrypted, locked
def changeWalletPassword(self, old_password, new_password): def changeWalletPassword(self, old_password: str, new_password: str):
self._log.info('changeWalletPassword - {}'.format(self.ticker())) self._log.info('changeWalletPassword - {}'.format(self.ticker()))
if old_password == '': if old_password == '':
if self.isWalletEncrypted(): if self.isWalletEncrypted():
@ -1316,7 +1322,7 @@ class BTCInterface(CoinInterface):
return self.rpc_callback('encryptwallet', [new_password]) return self.rpc_callback('encryptwallet', [new_password])
self.rpc_callback('walletpassphrasechange', [old_password, new_password]) self.rpc_callback('walletpassphrasechange', [old_password, new_password])
def unlockWallet(self, password): def unlockWallet(self, password: str):
if password == '': if password == '':
return return
self._log.info('unlockWallet - {}'.format(self.ticker())) self._log.info('unlockWallet - {}'.format(self.ticker()))
@ -1327,6 +1333,14 @@ class BTCInterface(CoinInterface):
self._log.info('lockWallet - {}'.format(self.ticker())) self._log.info('lockWallet - {}'.format(self.ticker()))
self.rpc_callback('walletlock') self.rpc_callback('walletlock')
def get_p2sh_script_pubkey(self, script: bytearray) -> bytearray:
script_hash = hash160(script)
assert len(script_hash) == 20
return CScript([OP_HASH160, script_hash, OP_EQUAL])
def get_p2wsh_script_pubkey(self, script: bytearray) -> bytearray:
return CScript([OP_0, hashlib.sha256(script).digest()])
def testBTCInterface(): def testBTCInterface():
print('TODO: testBTCInterface') print('TODO: testBTCInterface')

View File

@ -184,11 +184,18 @@ def ser_string_vector(l):
return r return r
# Deserialize from bytes
def FromBytes(obj, tx_bytes):
obj.deserialize(BytesIO(tx_bytes))
return obj
# Deserialize from a hex string representation (eg from RPC) # Deserialize from a hex string representation (eg from RPC)
def FromHex(obj, hex_string): def FromHex(obj, hex_string):
obj.deserialize(BytesIO(hex_str_to_bytes(hex_string))) obj.deserialize(BytesIO(hex_str_to_bytes(hex_string)))
return obj return obj
# Convert a binary-serializable object to hex (eg for submission via RPC) # Convert a binary-serializable object to hex (eg for submission via RPC)
def ToHex(obj): def ToHex(obj):
return bytes_to_hex_str(obj.serialize()) return bytes_to_hex_str(obj.serialize())

View File

@ -132,26 +132,28 @@ class FIROInterface(BTCInterface):
rv = self.rpc_callback('signrawtransaction', [tx.hex()]) rv = self.rpc_callback('signrawtransaction', [tx.hex()])
return bytes.fromhex(rv['hex']) return bytes.fromhex(rv['hex'])
def createRawSignedTransaction(self, addr_to, amount): def createRawFundedTransaction(self, addr_to: str, amount: int, sub_fee: bool = False, lock_unspents: bool = True) -> str:
txn = self.rpc_callback('createrawtransaction', [[], {addr_to: self.format_amount(amount)}]) txn = self.rpc_callback('createrawtransaction', [[], {addr_to: self.format_amount(amount)}])
fee_rate, fee_src = self.get_fee_rate(self._conf_target) fee_rate, fee_src = self.get_fee_rate(self._conf_target)
self._log.debug(f'Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}') self._log.debug(f'Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}')
options = { options = {
'lockUnspents': True, 'lockUnspents': lock_unspents,
'feeRate': fee_rate, 'feeRate': fee_rate,
} }
txn_funded = self.rpc_callback('fundrawtransaction', [txn, options])['hex'] if sub_fee:
txn_signed = self.rpc_callback('signrawtransaction', [txn_funded])['hex'] options['subtractFeeFromOutputs'] = [0,]
return txn_signed return self.rpc_callback('fundrawtransaction', [txn, options])['hex']
def getScriptForPubkeyHash(self, pkh): def createRawSignedTransaction(self, addr_to, amount) -> str:
# Return P2WPKH nested in BIP16 P2SH txn_funded = self.createRawFundedTransaction(addr_to, amount)
return self.rpc_callback('signrawtransaction', [txn_funded])['hex']
def getScriptForPubkeyHash(self, pkh: bytes) -> bytearray:
# Return P2PKH
return CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG]) return CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG])
def getScriptDest(self, script): def getScriptDest(self, script: bytearray) -> bytearray:
# P2WSH nested in BIP16_P2SH # P2WSH nested in BIP16_P2SH
script_hash = hashlib.sha256(script).digest() script_hash = hashlib.sha256(script).digest()

View File

@ -1,17 +1,20 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020 tecnovert # Copyright (c) 2022 tecnovert
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
from io import BytesIO
from .btc import BTCInterface from .btc import BTCInterface
from basicswap.chainparams import Coins from basicswap.chainparams import Coins
from basicswap.util.address import decodeAddress from basicswap.util.address import decodeAddress
from .contrib.pivx_test_framework.messages import ( from .contrib.pivx_test_framework.messages import (
CBlock, CBlock,
ToHex, ToHex,
FromHex) FromHex,
CTransaction)
class PIVXInterface(BTCInterface): class PIVXInterface(BTCInterface):
@ -19,19 +22,25 @@ class PIVXInterface(BTCInterface):
def coin_type(): def coin_type():
return Coins.PIVX return Coins.PIVX
def createRawSignedTransaction(self, addr_to, amount): def signTxWithWallet(self, tx):
txn = self.rpc_callback('createrawtransaction', [[], {addr_to: self.format_amount(amount)}]) rv = self.rpc_callback('signrawtransaction', [tx.hex()])
return bytes.fromhex(rv['hex'])
def createRawFundedTransaction(self, addr_to: str, amount: int, sub_fee: bool = False, lock_unspents: bool = True) -> str:
txn = self.rpc_callback('createrawtransaction', [[], {addr_to: self.format_amount(amount)}])
fee_rate, fee_src = self.get_fee_rate(self._conf_target) fee_rate, fee_src = self.get_fee_rate(self._conf_target)
self._log.debug(f'Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}') self._log.debug(f'Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}')
options = { options = {
'lockUnspents': True, 'lockUnspents': lock_unspents,
'feeRate': fee_rate, 'feeRate': fee_rate,
} }
txn_funded = self.rpc_callback('fundrawtransaction', [txn, options])['hex'] if sub_fee:
txn_signed = self.rpc_callback('signrawtransaction', [txn_funded])['hex'] options['subtractFeeFromOutputs'] = [0,]
return txn_signed return self.rpc_callback('fundrawtransaction', [txn, options])['hex']
def createRawSignedTransaction(self, addr_to, amount) -> str:
txn_funded = self.createRawFundedTransaction(addr_to, amount)
return self.rpc_callback('signrawtransaction', [txn_funded])['hex']
def decodeAddress(self, address): def decodeAddress(self, address):
return decodeAddress(address)[1:] return decodeAddress(address)[1:]
@ -65,3 +74,9 @@ class PIVXInterface(BTCInterface):
def getSpendableBalance(self): def getSpendableBalance(self):
return self.make_int(self.rpc_callback('getwalletinfo')['balance']) return self.make_int(self.rpc_callback('getwalletinfo')['balance'])
def loadTx(self, tx_bytes):
# Load tx from bytes to internal representation
tx = CTransaction()
tx.deserialize(BytesIO(tx_bytes))
return tx

View File

@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
class ProtocolInterface:
swap_type = None
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
raise ValueError('base class')

View File

@ -10,12 +10,17 @@ from basicswap.db import (
from basicswap.util import ( from basicswap.util import (
SerialiseNum, SerialiseNum,
) )
from basicswap.util.script import (
getP2WSH,
)
from basicswap.script import ( from basicswap.script import (
OpCodes, OpCodes,
) )
from basicswap.basicswap_util import ( from basicswap.basicswap_util import (
SwapTypes,
EventLogTypes, EventLogTypes,
) )
from . import ProtocolInterface
INITIATE_TX_TIMEOUT = 40 * 60 # TODO: make variable per coin INITIATE_TX_TIMEOUT = 40 * 60 # TODO: make variable per coin
ABS_LOCK_TIME_LEEWAY = 10 * 60 ABS_LOCK_TIME_LEEWAY = 10 * 60
@ -66,3 +71,43 @@ def redeemITx(self, bid_id, session):
bid.initiate_tx.spend_txid = bytes.fromhex(txid) bid.initiate_tx.spend_txid = bytes.fromhex(txid)
self.log.debug('Submitted initiate redeem txn %s to %s chain for bid %s', txid, ci_from.coin_name(), bid_id.hex()) self.log.debug('Submitted initiate redeem txn %s to %s chain for bid %s', txid, ci_from.coin_name(), bid_id.hex())
self.logEvent(Concepts.BID, bid_id, EventLogTypes.ITX_REDEEM_PUBLISHED, '', session) self.logEvent(Concepts.BID, bid_id, EventLogTypes.ITX_REDEEM_PUBLISHED, '', session)
class AtomicSwapInterface(ProtocolInterface):
swap_type = SwapTypes.SELLER_FIRST
def getMockScript(self) -> bytearray:
return bytearray([
OpCodes.OP_RETURN, OpCodes.OP_1])
def getMockScriptScriptPubkey(self, ci) -> bytearray:
script = self.getMockScript()
return ci.get_p2wsh_script_pubkey(script) if ci._use_segwit else ci.get_p2sh_script_pubkey(script)
def promoteMockTx(self, ci, mock_tx: bytes, script: bytearray) -> bytearray:
mock_txo_script = self.getMockScriptScriptPubkey(ci)
real_txo_script = ci.get_p2wsh_script_pubkey(script) if ci._use_segwit else ci.get_p2sh_script_pubkey(script)
found: int = 0
ctx = ci.loadTx(mock_tx)
for txo in ctx.vout:
if txo.scriptPubKey == mock_txo_script:
txo.scriptPubKey = real_txo_script
found += 1
if found < 1:
raise ValueError('Mocked output not found')
if found > 1:
raise ValueError('Too many mocked outputs found')
ctx.nLockTime = 0
funded_tx = ctx.serialize()
return ci.signTxWithWallet(funded_tx)
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
script = self.getMockScript()
addr_to = ci.encode_p2wsh(getP2WSH(script)) if ci._use_segwit else ci.encode_p2sh(script)
funded_tx = ci.createRawFundedTransaction(addr_to, amount, sub_fee, lock_unspents=False)
return bytes.fromhex(funded_tx)

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020 tecnovert # Copyright (c) 2020-2022 tecnovert
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@ -14,8 +14,10 @@ from basicswap.chainparams import (
) )
from basicswap.basicswap_util import ( from basicswap.basicswap_util import (
KeyTypes, KeyTypes,
SwapTypes,
EventLogTypes, EventLogTypes,
) )
from . import ProtocolInterface
def addLockRefundSigs(self, xmr_swap, ci): def addLockRefundSigs(self, xmr_swap, ci):
@ -84,3 +86,7 @@ def getChainBSplitKey(swap_client, bid, xmr_swap, offer):
key_type = KeyTypes.KBSF if bid.was_sent else KeyTypes.KBSL key_type = KeyTypes.KBSF if bid.was_sent else KeyTypes.KBSL
return ci_to.encodeKey(swap_client.getPathKey(offer.coin_from, offer.coin_to, bid.created_at, xmr_swap.contract_count, key_type, True if offer.coin_to == Coins.XMR else False)) return ci_to.encodeKey(swap_client.getPathKey(offer.coin_from, offer.coin_to, bid.created_at, xmr_swap.contract_count, key_type, True if offer.coin_to == Coins.XMR else False))
class XmrSwapInterface(ProtocolInterface):
swap_type = SwapTypes.XMR_SWAP

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2019 tecnovert # Copyright (c) 2019-2022 tecnovert
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@ -15,6 +15,7 @@ class OpCodes(IntEnum):
OP_IF = 0x63, OP_IF = 0x63,
OP_ELSE = 0x67, OP_ELSE = 0x67,
OP_ENDIF = 0x68, OP_ENDIF = 0x68,
OP_RETURN = 0x6a,
OP_DROP = 0x75, OP_DROP = 0x75,
OP_DUP = 0x76, OP_DUP = 0x76,
OP_SIZE = 0x82, OP_SIZE = 0x82,

View File

@ -50,12 +50,28 @@ In chainclients.monero:
On the remote machine open an ssh tunnel to port 18081: On the remote machine open an ssh tunnel to port 18081:
ssh -R 18081:localhost:18081 -N user@LOCAL_NODE_IP ssh -N -R 18081:localhost:18081 user@LOCAL_NODE_IP
And start monerod And start monerod
## Installing on windows natively ## SSH Tunnel to Remote BasicSwap Node
While basicswap can be configured to host on an external interface:
If not using docker by changing 'htmlhost' and 'wshost' in basicswap.json
For docker change 'HTML_PORT' and 'WS_PORT' in the .env file in the same dir as docker-compose.yml
A better solution is to use ssh to forward the required ports from the machine running bascswap to the client.
ssh -N -L 5555:localhost:12700 -L 11700:localhost:11700 BASICSWAP_HOST
Run from the client machine (not running basicswap) will forward the basicswap ui on port 12700 to port 5555
on the local machine and also the websocket port at 11700.
The ui port on the client machine can be anything but the websocket port must match 'wsport' in basicswap.json.
## Installing on Windows Natively
This is not a supported installation method! This is not a supported installation method!

View File

@ -261,18 +261,32 @@ def waitForNumSwapping(delay_event, port, bids, wait_for=60):
raise ValueError('waitForNumSwapping failed') raise ValueError('waitForNumSwapping failed')
def wait_for_balance(delay_event, url, balance_key, expect_amount, iterations=20, delay_time=3): def wait_for_balance(delay_event, url, balance_key, expect_amount, iterations=20, delay_time=3) -> None:
i = 0 i = 0
while not delay_event.is_set(): while not delay_event.is_set():
rv_js = json.loads(urlopen(url).read()) rv_js = json.loads(urlopen(url).read())
if float(rv_js[balance_key]) >= expect_amount: if float(rv_js[balance_key]) >= expect_amount:
break return
delay_event.wait(delay_time) delay_event.wait(delay_time)
i += 1 i += 1
if i > iterations: if i > iterations:
raise ValueError('Expect {} {}'.format(balance_key, expect_amount)) raise ValueError('Expect {} {}'.format(balance_key, expect_amount))
def wait_for_unspent(delay_event, ci, expect_amount, iterations=20, delay_time=1) -> None:
logging.info(f'Waiting for unspent balance: {expect_amount}')
i = 0
while not delay_event.is_set():
unspent_addr = ci.getUnspentsByAddr()
for _, value in unspent_addr.items():
if value >= expect_amount:
return
delay_event.wait(delay_time)
i += 1
if i > iterations:
raise ValueError('wait_for_unspent {}'.format(expect_amount))
def delay_for(delay_event, delay_for=60): def delay_for(delay_event, delay_for=60):
logging.info('Delaying for {} seconds.'.format(delay_for)) logging.info('Delaying for {} seconds.'.format(delay_for))
delay_event.wait(delay_for) delay_event.wait(delay_for)

View File

@ -43,6 +43,7 @@ from tests.basicswap.common import (
wait_for_offer, wait_for_offer,
wait_for_bid, wait_for_bid,
wait_for_balance, wait_for_balance,
wait_for_unspent,
wait_for_bid_tx_state, wait_for_bid_tx_state,
wait_for_in_progress, wait_for_in_progress,
TEST_HTTP_PORT, TEST_HTTP_PORT,
@ -496,6 +497,69 @@ class Test(BaseTest):
assert (compare_bid_states(offerer_states, self.states_offerer[2]) is True) assert (compare_bid_states(offerer_states, self.states_offerer[2]) is True)
assert (compare_bid_states(bidder_states, self.states_bidder[2], exact_match=False) is True) assert (compare_bid_states(bidder_states, self.states_bidder[2], exact_match=False) is True)
def test_14_sweep_balance(self):
logging.info('---------- Test sweep balance offer')
swap_clients = self.swap_clients
# Disable staking
walletsettings = callnoderpc(2, 'walletsettings', ['stakingoptions', ])
walletsettings['enabled'] = False
walletsettings = callnoderpc(2, 'walletsettings', ['stakingoptions', walletsettings])
walletsettings = callnoderpc(2, 'walletsettings', ['stakingoptions', ])
assert (walletsettings['stakingoptions']['enabled'] is False)
# Prepare balance
js_w2 = read_json_api(1802, 'wallets')
if float(js_w2['PART']['balance']) < 100.0:
post_json = {
'value': 100,
'address': js_w2['PART']['deposit_address'],
'subfee': False,
}
json_rv = read_json_api(TEST_HTTP_PORT + 0, 'wallets/part/withdraw', post_json)
assert (len(json_rv['txid']) == 64)
wait_for_balance(test_delay_event, 'http://127.0.0.1:1802/json/wallets/part', 'balance', 100.0)
js_w2 = read_json_api(1802, 'wallets')
assert (float(js_w2['PART']['balance']) >= 100.0)
js_w2 = read_json_api(1802, 'wallets')
post_json = {
'value': float(js_w2['PART']['balance']),
'address': read_json_api(1802, 'wallets/part/nextdepositaddr'),
'subfee': True,
}
json_rv = read_json_api(TEST_HTTP_PORT + 2, 'wallets/part/withdraw', post_json)
wait_for_balance(test_delay_event, 'http://127.0.0.1:1802/json/wallets/part', 'balance', 10.0)
assert (len(json_rv['txid']) == 64)
# Create prefunded ITX
ci = swap_clients[2].ci(Coins.PART)
pi = swap_clients[2].pi(SwapTypes.SELLER_FIRST)
js_w2 = read_json_api(1802, 'wallets')
swap_value = ci.make_int(js_w2['PART']['balance'])
itx = pi.getFundedInitiateTxTemplate(ci, swap_value, True)
itx_decoded = ci.describeTx(itx.hex())
value_after_subfee = ci.make_int(itx_decoded['vout'][0]['value'])
assert (value_after_subfee < swap_value)
swap_value = value_after_subfee
wait_for_unspent(test_delay_event, ci, swap_value)
# Create swap with prefunded ITX
extra_options = {'prefunded_itx': itx}
offer_id = swap_clients[2].postOffer(Coins.PART, Coins.BTC, swap_value, 2 * COIN, swap_value, SwapTypes.SELLER_FIRST, extra_options=extra_options)
wait_for_offer(test_delay_event, swap_clients[1], offer_id)
offer = swap_clients[1].getOffer(offer_id)
bid_id = swap_clients[1].postBid(offer_id, offer.amount_from)
wait_for_bid(test_delay_event, swap_clients[2], bid_id)
swap_clients[2].acceptBid(bid_id)
wait_for_bid(test_delay_event, swap_clients[2], bid_id, BidStates.SWAP_COMPLETED, wait_for=60)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=60)
def pass_99_delay(self): def pass_99_delay(self):
logging.info('Delay') logging.info('Delay')
for i in range(60 * 10): for i in range(60 * 10):