system: Allow preselecting inputs for atomic swaps.
This commit is contained in:
		
							parent
							
								
									23e89882a4
								
							
						
					
					
						commit
						c90fa6f2c6
					
				@ -1,3 +1,3 @@
 | 
			
		||||
name = "basicswap"
 | 
			
		||||
 | 
			
		||||
__version__ = "0.11.51"
 | 
			
		||||
__version__ = "0.11.52"
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -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'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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')
 | 
			
		||||
 | 
			
		||||
@ -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())
 | 
			
		||||
 | 
			
		||||
@ -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()
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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')
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										20
									
								
								doc/notes.md
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								doc/notes.md
									
									
									
									
									
								
							@ -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!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -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):
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user