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"
__version__ = "0.11.51"
__version__ = "0.11.52"

View file

@ -26,6 +26,8 @@ import sqlalchemy as sa
import collections
import concurrent.futures
from typing import Optional
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy.orm.session import close_all_sessions
@ -92,6 +94,7 @@ from .db import (
Offer,
Bid,
SwapTx,
PrefundedTx,
PooledAddress,
SentOffer,
SmsgAddress,
@ -116,6 +119,7 @@ from .explorers import (
import basicswap.config as cfg
import basicswap.network as bsn
import basicswap.protocols.atomic_swap_1 as atomic_swap_1
import basicswap.protocols.xmr_swap_1 as xmr_swap_1
from .basicswap_util import (
KeyTypes,
TxLockTypes,
@ -140,9 +144,6 @@ from .basicswap_util import (
isActiveBidState,
NotificationTypes as NT,
)
from .protocols.xmr_swap_1 import (
addLockRefundSigs,
recoverNoScriptTxnWithKey)
non_script_type_coins = (Coins.XMR, Coins.PART_ANON)
@ -218,6 +219,10 @@ class WatchedTransaction():
class BasicSwap(BaseApp):
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'):
super().__init__(fp, data_dir, settings, chain, log_name)
@ -548,6 +553,11 @@ class BasicSwap(BaseApp):
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):
if coin == Coins.PART:
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.coin_clients[c]['core_version'] = core_version
if c == Coins.XMR:
t = threading.Thread(target=threadPollXMRChainState, args=(self, c))
else:
t = threading.Thread(target=threadPollChainState, args=(self, c))
thread_func = threadPollXMRChainState if c == Coins.XMR else threadPollChainState
t = threading.Thread(target=thread_func, args=(self, c))
self.threads.append(t)
t.start()
@ -851,7 +859,7 @@ class BasicSwap(BaseApp):
finally:
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()
if not identity_stats:
identity_stats = KnownIdentity(address=address, created_at=int(time.time()))
@ -870,7 +878,7 @@ class BasicSwap(BaseApp):
identity_stats.updated_at = int(time.time())
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()
if not kv:
kv = DBKVInt(key=str_key, value=int_val)
@ -878,7 +886,7 @@ class BasicSwap(BaseApp):
kv.value = int_val
session.add(kv)
def setIntKV(self, str_key, int_val):
def setIntKV(self, str_key: str, int_val: int) -> None:
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
@ -889,7 +897,7 @@ class BasicSwap(BaseApp):
session.remove()
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:
use_session = self.openSession(session)
kv = use_session.query(DBKVString).filter_by(key=str_key).first()
@ -902,7 +910,7 @@ class BasicSwap(BaseApp):
if session is None:
self.closeSession(use_session)
def getStringKV(self, str_key):
def getStringKV(self, str_key: str) -> Optional[str]:
self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
@ -915,7 +923,7 @@ class BasicSwap(BaseApp):
session.remove()
self.mxDB.release()
def clearStringKV(self, str_key, str_val):
def clearStringKV(self, str_key: str, str_val: str) -> None:
with self.mxDB:
try:
session = scoped_session(self.session_factory)
@ -925,6 +933,19 @@ class BasicSwap(BaseApp):
session.close()
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):
if bid.bid_id in self.swaps_in_progress:
self.log.debug('Bid %s is already in progress', bid.bid_id.hex())
@ -1366,6 +1387,16 @@ class BasicSwap(BaseApp):
repeat_count=0)
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(SentOffer(offer_id=offer_id))
session.commit()
@ -2147,7 +2178,8 @@ class BasicSwap(BaseApp):
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
refund_txn = self.createRefundTxn(coin_from, txn, offer, bid, script)
@ -2532,14 +2564,14 @@ class BasicSwap(BaseApp):
session.remove()
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)
bid.setState(BidStates.BID_ERROR)
bid.state_note = 'error msg: ' + error_str
if save_bid:
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':
return None
ci = self.ci(coin_type)
@ -2550,7 +2582,11 @@ class BasicSwap(BaseApp):
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())
txn_signed = ci.createRawSignedTransaction(addr_to, bid.amount)
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)
return txn_signed
def deriveParticipateScript(self, bid_id, bid, offer):
@ -4560,7 +4596,7 @@ class BasicSwap(BaseApp):
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)
addLockRefundSigs(self, xmr_swap, ci_from)
xmr_swap_1.addLockRefundSigs(self, xmr_swap, ci_from)
msg_buf = XmrBidLockTxSigsMessage(
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)
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)
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
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:
session = scoped_session(self.session_factory)

View file

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

View file

@ -12,7 +12,7 @@ from enum import IntEnum, auto
from sqlalchemy.ext.declarative import declarative_base
CURRENT_DB_VERSION = 16
CURRENT_DB_VERSION = 17
CURRENT_DB_DATA_VERSION = 2
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()))
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):
__tablename__ = 'addresspool'

View file

@ -225,6 +225,19 @@ def upgradeDatabase(self, db_version):
event_data BLOB,
created_at BIGINT,
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:
self.db_version = db_version

View file

@ -12,6 +12,7 @@ import hashlib
import logging
import traceback
from io import BytesIO
from basicswap.contrib.test_framework import segwit_addr
from basicswap.util import (
@ -64,6 +65,7 @@ from basicswap.contrib.test_framework.script import (
OP_CHECKMULTISIG,
OP_CHECKSEQUENCEVERIFY,
OP_DROP,
OP_HASH160, OP_EQUAL,
SIGHASH_ALL,
SegwitV0SignatureHash,
hash160)
@ -244,7 +246,7 @@ class BTCInterface(CoinInterface):
def getBlockHeader(self, 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')
last_block_header = self.rpc_callback('getblockheader', [blockchaininfo['bestblockhash']])
@ -294,24 +296,24 @@ class BTCInterface(CoinInterface):
finally:
self.close_rpc(rpc_conn)
def getWalletSeedID(self):
def getWalletSeedID(self) -> str:
return self.rpc_callback('getwalletinfo')['hdseedid']
def checkExpectedSeed(self, expect_seedid):
def checkExpectedSeed(self, expect_seedid) -> bool:
self._expect_seedid_hex = expect_seedid
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]
if use_segwit:
args.append('bech32')
return self.rpc_callback('getnewaddress', args)
def isAddressMine(self, address):
def isAddressMine(self, address: str) -> bool:
addr_info = self.rpc_callback('getaddressinfo', [address])
return addr_info['ismine']
def checkAddressMine(self, address):
def checkAddressMine(self, address: str) -> None:
addr_info = self.rpc_callback('getaddressinfo', [address])
ensure(addr_info['ismine'], 'ismine is false')
ensure(addr_info['hdseedid'] == self._expect_seedid_hex, 'unexpected seedid')
@ -914,7 +916,7 @@ class BTCInterface(CoinInterface):
def encodeTx(self, tx):
return tx.serialize()
def loadTx(self, tx_bytes):
def loadTx(self, tx_bytes) -> CTransaction:
# Load tx from bytes to internal representation
tx = CTransaction()
tx.deserialize(BytesIO(tx_bytes))
@ -963,23 +965,23 @@ class BTCInterface(CoinInterface):
# TODO: filter errors
return None
def setTxSignature(self, tx_bytes, stack):
def setTxSignature(self, tx_bytes, stack) -> bytes:
tx = self.loadTx(tx_bytes)
tx.wit.vtxinwit.clear()
tx.wit.vtxinwit.append(CTxInWitness())
tx.wit.vtxinwit[0].scriptWitness.stack = stack
return tx.serialize()
def stripTxSignature(self, tx_bytes):
def stripTxSignature(self, tx_bytes) -> bytes:
tx = self.loadTx(tx_bytes)
tx.wit.vtxinwit.clear()
return tx.serialize()
def extractLeaderSig(self, tx_bytes):
def extractLeaderSig(self, tx_bytes) -> bytes:
tx = self.loadTx(tx_bytes)
return tx.wit.vtxinwit[0].scriptWitness.stack[1]
def extractFollowerSig(self, tx_bytes):
def extractFollowerSig(self, tx_bytes) -> bytes:
tx = self.loadTx(tx_bytes)
return tx.wit.vtxinwit[0].scriptWitness.stack[2]
@ -1142,7 +1144,7 @@ class BTCInterface(CoinInterface):
rv = pubkey.verify_compact(sig, message_hash, hasher=None)
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:
message_magic = self.chainparams()['message_magic']
@ -1209,13 +1211,13 @@ class BTCInterface(CoinInterface):
length += 1 # flags
return length
def describeTx(self, tx_hex):
def describeTx(self, tx_hex: str):
return self.rpc_callback('decoderawtransaction', [tx_hex])
def getSpendableBalance(self):
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
spendable_balance = self.getSpendableBalance()
@ -1225,18 +1227,22 @@ class BTCInterface(CoinInterface):
address = self.getNewAddress(self._use_segwit, 'create_utxo')
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)}])
options = {
'lockUnspents': True,
'lockUnspents': lock_unspents,
'conf_target': self._conf_target,
}
txn_funded = self.rpc_callback('fundrawtransaction', [txn, options])['hex']
txn_signed = self.rpc_callback('signrawtransactionwithwallet', [txn_funded])['hex']
return txn_signed
if sub_fee:
options['subtractFeeFromOutputs'] = [0,]
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])
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)
return unspent_addr
def getUTXOBalance(self, address):
def getUTXOBalance(self, address: str):
num_blocks = self.rpc_callback('getblockcount')
sum_unspent = 0
@ -1292,11 +1298,11 @@ class BTCInterface(CoinInterface):
return self.getUTXOBalance(address)
def isWalletEncrypted(self):
def isWalletEncrypted(self) -> bool:
wallet_info = self.rpc_callback('getwalletinfo')
return 'unlocked_until' in wallet_info
def isWalletLocked(self):
def isWalletLocked(self) -> bool:
wallet_info = self.rpc_callback('getwalletinfo')
if 'unlocked_until' in wallet_info and wallet_info['unlocked_until'] <= 0:
return True
@ -1308,7 +1314,7 @@ class BTCInterface(CoinInterface):
locked = encrypted and wallet_info['unlocked_until'] <= 0
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()))
if old_password == '':
if self.isWalletEncrypted():
@ -1316,7 +1322,7 @@ class BTCInterface(CoinInterface):
return self.rpc_callback('encryptwallet', [new_password])
self.rpc_callback('walletpassphrasechange', [old_password, new_password])
def unlockWallet(self, password):
def unlockWallet(self, password: str):
if password == '':
return
self._log.info('unlockWallet - {}'.format(self.ticker()))
@ -1327,6 +1333,14 @@ class BTCInterface(CoinInterface):
self._log.info('lockWallet - {}'.format(self.ticker()))
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():
print('TODO: testBTCInterface')

View file

@ -184,11 +184,18 @@ def ser_string_vector(l):
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)
def FromHex(obj, hex_string):
obj.deserialize(BytesIO(hex_str_to_bytes(hex_string)))
return obj
# Convert a binary-serializable object to hex (eg for submission via RPC)
def ToHex(obj):
return bytes_to_hex_str(obj.serialize())

View file

@ -132,26 +132,28 @@ class FIROInterface(BTCInterface):
rv = self.rpc_callback('signrawtransaction', [tx.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)}])
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}')
options = {
'lockUnspents': True,
'lockUnspents': lock_unspents,
'feeRate': fee_rate,
}
txn_funded = self.rpc_callback('fundrawtransaction', [txn, options])['hex']
txn_signed = self.rpc_callback('signrawtransaction', [txn_funded])['hex']
return txn_signed
if sub_fee:
options['subtractFeeFromOutputs'] = [0,]
return self.rpc_callback('fundrawtransaction', [txn, options])['hex']
def getScriptForPubkeyHash(self, pkh):
# Return P2WPKH nested in BIP16 P2SH
def createRawSignedTransaction(self, addr_to, amount) -> str:
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])
def getScriptDest(self, script):
def getScriptDest(self, script: bytearray) -> bytearray:
# P2WSH nested in BIP16_P2SH
script_hash = hashlib.sha256(script).digest()

View file

@ -1,17 +1,20 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2020 tecnovert
# Copyright (c) 2022 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
from io import BytesIO
from .btc import BTCInterface
from basicswap.chainparams import Coins
from basicswap.util.address import decodeAddress
from .contrib.pivx_test_framework.messages import (
CBlock,
ToHex,
FromHex)
FromHex,
CTransaction)
class PIVXInterface(BTCInterface):
@ -19,19 +22,25 @@ class PIVXInterface(BTCInterface):
def coin_type():
return Coins.PIVX
def createRawSignedTransaction(self, addr_to, amount):
txn = self.rpc_callback('createrawtransaction', [[], {addr_to: self.format_amount(amount)}])
def signTxWithWallet(self, tx):
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)
self._log.debug(f'Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}')
options = {
'lockUnspents': True,
'lockUnspents': lock_unspents,
'feeRate': fee_rate,
}
txn_funded = self.rpc_callback('fundrawtransaction', [txn, options])['hex']
txn_signed = self.rpc_callback('signrawtransaction', [txn_funded])['hex']
return txn_signed
if sub_fee:
options['subtractFeeFromOutputs'] = [0,]
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):
return decodeAddress(address)[1:]
@ -65,3 +74,9 @@ class PIVXInterface(BTCInterface):
def getSpendableBalance(self):
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 (
SerialiseNum,
)
from basicswap.util.script import (
getP2WSH,
)
from basicswap.script import (
OpCodes,
)
from basicswap.basicswap_util import (
SwapTypes,
EventLogTypes,
)
from . import ProtocolInterface
INITIATE_TX_TIMEOUT = 40 * 60 # TODO: make variable per coin
ABS_LOCK_TIME_LEEWAY = 10 * 60
@ -66,3 +71,43 @@ def redeemITx(self, bid_id, session):
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.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 -*-
# Copyright (c) 2020 tecnovert
# 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.
@ -14,8 +14,10 @@ from basicswap.chainparams import (
)
from basicswap.basicswap_util import (
KeyTypes,
SwapTypes,
EventLogTypes,
)
from . import ProtocolInterface
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
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 -*-
# Copyright (c) 2019 tecnovert
# Copyright (c) 2019-2022 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@ -15,6 +15,7 @@ class OpCodes(IntEnum):
OP_IF = 0x63,
OP_ELSE = 0x67,
OP_ENDIF = 0x68,
OP_RETURN = 0x6a,
OP_DROP = 0x75,
OP_DUP = 0x76,
OP_SIZE = 0x82,

View file

@ -50,12 +50,28 @@ In chainclients.monero:
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
## 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!

View file

@ -261,18 +261,32 @@ def waitForNumSwapping(delay_event, port, bids, wait_for=60):
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
while not delay_event.is_set():
rv_js = json.loads(urlopen(url).read())
if float(rv_js[balance_key]) >= expect_amount:
break
return
delay_event.wait(delay_time)
i += 1
if i > iterations:
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):
logging.info('Delaying for {} seconds.'.format(delay_for))
delay_event.wait(delay_for)

View file

@ -43,6 +43,7 @@ from tests.basicswap.common import (
wait_for_offer,
wait_for_bid,
wait_for_balance,
wait_for_unspent,
wait_for_bid_tx_state,
wait_for_in_progress,
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(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):
logging.info('Delay')
for i in range(60 * 10):