mirror of
https://github.com/basicswap/basicswap.git
synced 2025-03-12 09:38:03 +00:00
system: Allow preselecting inputs for atomic swaps.
This commit is contained in:
parent
23e89882a4
commit
c90fa6f2c6
16 changed files with 334 additions and 72 deletions
|
@ -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 a new issue