feat: add subfee bids

This commit is contained in:
tecnovert
2026-06-03 23:43:30 +02:00
parent 1f8d2f2eb8
commit 04e2020ff3
22 changed files with 718 additions and 88 deletions
+166 -38
View File
@@ -5199,6 +5199,63 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
bid_rate = best_bid_rate
return amount, amount_to, bid_rate
def createSubfeeBidTx(self, offer_id: bytes, amount_to: int, rate: int) -> dict:
self.log.debug(
f"createSubfeeBidTx for offer: {self.log.id(offer_id)}, amount to: {amount_to}"
)
offer, xmr_offer = self.getXmrOffer(offer_id)
ensure(offer, f"Offer not found: {self.log.id(offer_id)}.")
ensure(xmr_offer, f"Adaptor-sig offer not found: {self.log.id(offer_id)}.")
ensure(
offer.amount_negotiable,
f"Offer amounts are final: {self.log.id(offer_id)}.",
)
if offer.coin_to in (Coins.XMR, Coins.WOW):
raise ValueError("TODO")
if offer.swap_type != SwapTypes.XMR_SWAP:
raise ValueError("TODO")
ci_to = self.ci(offer.coin_to)
ci_from = self.ci(offer.coin_from)
pi = self.pi(SwapTypes.XMR_SWAP)
reverse_bid: bool = self.is_reverse_ads_bid(offer.coin_from, offer.coin_to)
feerate: int = xmr_offer.b_fee_rate
if reverse_bid:
# Create ITX
lock_txa: bytes = pi.getFundedInitiateTxTemplate(
ci_to, amount_to, sub_fee=True, feerate=feerate
)
tx_obj = ci_to.loadTx(lock_txa, allow_witness=False)
lock_vout: int = pi.getMockITxSwapVout(ci_to, tx_obj)
amount_to_out: int = tx_obj.vout[lock_vout].nValue
else:
# Create PTX
mock_pk: bytes = pi.getMockPubkey(ci_to)
lock_txb: bytes = ci_to.createBLockTx(mock_pk, amount_to)
lock_txb = ci_to.fundTx(lock_txb, feerate, lock_unspents=False, subfee=True)
tx_obj = ci_to.loadTx(lock_txb, allow_witness=False)
lock_vout: int = pi.getMockPTxSwapVout(ci_to, tx_obj)
amount_to_out: int = tx_obj.vout[lock_vout].nValue
amount_from_out: int = (amount_to_out * (10 ** ci_from.exp())) // rate
extra_options = {"bid_rate": rate}
amount_adjusted, amount_to_adjusted, bid_rate = self.setBidAmounts(
amount_from_out, offer, extra_options, ci_from
)
if amount_to_adjusted < amount_to_out:
tx_obj.vout[lock_vout].nValue = amount_to_adjusted
self.log.debug(
f"Amounts after subfee: to {ci_to.format_amount(amount_to_adjusted)} {ci_to.ticker()}, from {ci_from.format_amount(amount_to_adjusted)} {ci_from.ticker()}"
)
tx_data: bytes = tx_obj.serialize_without_witness()
return {
"amount_from": amount_adjusted,
"amount_to": amount_to_adjusted,
"bid_tx": tx_data,
}
def postBid(
self, offer_id: bytes, amount: int, addr_send_from: str = None, extra_options={}
) -> bytes:
@@ -6016,46 +6073,66 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
"Incompatible offer protocol version",
)
ensure(offer.expire_at > self.getTime(), "Offer has expired")
if offer.swap_type != SwapTypes.XMR_SWAP:
raise ValueError(f"TODO: Unknown swap type {offer.swap_type.name}")
coin_from = Coins(offer.coin_from)
coin_to = Coins(offer.coin_to)
ci_from = self.ci(coin_from)
ci_to = self.ci(coin_to)
reverse_bid: bool = self.is_reverse_ads_bid(coin_from, coin_to)
valid_for_seconds: int = extra_options.get("valid_for_seconds", 60 * 10)
amount, amount_to, bid_rate = self.setBidAmounts(
amount, offer, extra_options, ci_from
)
bid_created_at: int = self.getTime()
if offer.swap_type != SwapTypes.XMR_SWAP:
raise ValueError(f"TODO: Unknown swap type {offer.swap_type.name}")
self.checkCoinsReady(coin_from, coin_to)
ci_from.validateFeeRate(xmr_offer.a_fee_rate)
ci_to.validateFeeRate(xmr_offer.b_fee_rate)
bid_created_at: int = self.getTime()
valid_for_seconds: int = extra_options.get("valid_for_seconds", 60 * 10)
if "prefunded_tx" in extra_options:
pi = self.pi(SwapTypes.XMR_SWAP)
prefunded_tx_data: bytes = extra_options["prefunded_tx"]
if reverse_bid:
amount_to = pi.getMockITxSwapValue(ci_to, prefunded_tx_data)
else:
amount_to = pi.getMockPTxSwapValue(ci_to, prefunded_tx_data)
bid_rate: int = ci_from.make_int(amount_to / amount, r=1)
prefunded_txid, prefunded_tx_fee_rate = (
ci_to.validatePrefundedTxAmounts(prefunded_tx_data)
)
self.log.debug(f"Using prefunded tx: {self.log.id(prefunded_txid)}")
ci_to.validateFeeRate(prefunded_tx_fee_rate)
else:
amount, amount_to, bid_rate = self.setBidAmounts(
amount, offer, extra_options, ci_from
)
if not (self.debug and extra_options.get("debug_skip_validation", False)):
self.validateBidValidTime(
offer.swap_type, coin_from, coin_to, valid_for_seconds
)
self.validateBidAmount(offer, amount, bid_rate)
self.checkCoinsReady(coin_from, coin_to)
# TODO: Better tx size estimate
fee_rate, fee_src = self.getFeeRateForCoin(coin_to, conf_target=2)
fee_rate_to = ci_to.make_int(fee_rate)
fee_rate_to = xmr_offer.b_fee_rate
estimated_fee: int = fee_rate_to * ci_to.est_lock_tx_vsize() // 1000
self.ensureWalletCanSend(
ci_to, offer.swap_type, int(amount_to), estimated_fee, for_offer=False
)
if "prefunded_tx" not in extra_options:
self.ensureWalletCanSend(
ci_to,
offer.swap_type,
int(amount_to),
estimated_fee,
for_offer=False,
)
bid_addr: str = self.prepareSMSGAddress(
addr_send_from, AddressTypes.BID, cursor
)
# return id of route waiting to be established
# Return id of route waiting to be established
request_data = {
"offer_id": offer_id.hex(),
"amount_from": amount,
@@ -6073,7 +6150,6 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
valid_for_seconds,
)
reverse_bid: bool = self.is_reverse_ads_bid(coin_from, coin_to)
if reverse_bid:
reversed_rate: int = ci_to.make_int(amount / amount_to, r=1)
@@ -6127,6 +6203,17 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
bid.bid_id = bid_id
xmr_swap.bid_id = bid.bid_id
if "prefunded_tx" in extra_options:
prefunded_tx = PrefundedTx(
active_ind=1,
created_at=bid_created_at,
linked_type=Concepts.BID,
linked_id=bid.bid_id,
tx_type=TxTypes.ITX_PRE_FUNDED,
tx_data=extra_options["prefunded_tx"],
)
self.add(prefunded_tx, cursor)
self.saveBidInSession(xmr_swap.bid_id, bid, cursor, xmr_swap)
self.commitDB()
@@ -6248,6 +6335,16 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
self.log.warning(
f"Adaptor-sig swap restore height clamped to {wallet_restore_height}"
)
if "prefunded_tx" in extra_options:
prefunded_tx = PrefundedTx(
active_ind=1,
created_at=bid_created_at,
linked_type=Concepts.BID,
linked_id=bid.bid_id,
tx_type=TxTypes.PTX_PRE_FUNDED,
tx_data=extra_options["prefunded_tx"],
)
self.add(prefunded_tx, cursor)
self.saveBidInSession(bid.bid_id, bid, cursor, xmr_swap)
self.log.info(f"Sent XMR_BID_FL {self.logIDB(xmr_swap.bid_id)}")
@@ -6367,16 +6464,25 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
xmr_swap.a_lock_tx_script = pi.genScriptLockTxScript(
ci_from, xmr_swap.pkal, xmr_swap.pkaf, **lockExtraArgs
)
prefunded_tx = self.getPreFundedTx(
Concepts.OFFER,
bid.offer_id,
TxTypes.ITX_PRE_FUNDED,
cursor=use_cursor,
)
if reverse_bid and bid.was_sent:
prefunded_tx = self.getPreFundedTx(
Concepts.BID,
bid_id,
TxTypes.ITX_PRE_FUNDED,
cursor=use_cursor,
)
else:
prefunded_tx = self.getPreFundedTx(
Concepts.OFFER,
bid.offer_id,
TxTypes.ITX_PRE_FUNDED,
cursor=use_cursor,
)
if prefunded_tx:
xmr_swap.a_lock_tx = pi.promoteMockTx(
ci_from, prefunded_tx, xmr_swap.a_lock_tx_script
)
self.log.info("Using pre-funded tx")
else:
xmr_swap.a_lock_tx = ci_from.createSCLockTx(
bid.amount, xmr_swap.a_lock_tx_script, xmr_swap.vkbv
@@ -6779,14 +6885,17 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
addr_to = ci.encodeScriptDest(p2wsh)
else:
addr_to = ci.encode_p2sh(initiate_script)
self.log.debug(
f"Create initiate txn for coin {ci.coin_name()} to {addr_to} for bid {self.log.id(bid_id)}"
)
if prefunded_tx:
self.log.debug(
f"Using pre-funded initiate txn for coin {ci.coin_name()} to {addr_to} for bid {self.log.id(bid_id)}"
)
pi = self.pi(SwapTypes.SELLER_FIRST)
txn_signed = pi.promoteMockTx(ci, prefunded_tx, initiate_script).hex()
else:
self.log.debug(
f"Create initiate txn for coin {ci.coin_name()} to {addr_to} for bid {self.log.id(bid_id)}"
)
txn_signed = ci.createRawSignedTransaction(addr_to, bid.amount)
txjs = ci.describeTx(txn_signed)
@@ -11607,19 +11716,38 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
cursor,
)
prefunded_tx = self.getPreFundedTx(
Concepts.BID,
bid.bid_id,
TxTypes.PTX_PRE_FUNDED,
cursor=cursor,
)
try:
b_lock_vout = 0
result = ci_to.publishBLockTx(
xmr_swap.vkbv,
xmr_swap.pkbs,
bid.amount_to,
b_fee_rate,
unlock_time=unlock_time,
)
if isinstance(result, tuple):
b_lock_tx_id, b_lock_vout = result
if prefunded_tx:
self.log.info("Using pre-funded tx")
pi = self.pi(offer.swap_type)
b_lock_tx = pi.promoteMockPTx(
ci_to,
prefunded_tx,
xmr_swap.vkbv,
xmr_swap.pkbs,
)
b_lock_tx = ci_to.signTxWithWallet(b_lock_tx)
b_lock_tx_id = bytes.fromhex(ci_to.publishTx(b_lock_tx))
else:
b_lock_tx_id = result
result = ci_to.publishBLockTx(
xmr_swap.vkbv,
xmr_swap.pkbs,
bid.amount_to,
b_fee_rate,
unlock_time=unlock_time,
)
if isinstance(result, tuple):
b_lock_tx_id, b_lock_vout = result
else:
b_lock_tx_id = result
if bid.debug_ind == DebugTypes.B_LOCK_TX_MISSED_SEND:
self.log.debug(
f"Adaptor-sig bid {self.log.id(bid_id)}: Debug {bid.debug_ind} - Losing XMR lock tx {self.log.id(b_lock_tx_id)}."
@@ -14170,7 +14298,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
def updateWalletsInfo(
self,
force_update: bool = False,
only_coin: bool = None,
only_coin: int = None,
wait_for_complete: bool = False,
) -> None:
now: int = self.getTime()
+2
View File
@@ -161,6 +161,8 @@ class TxTypes(IntEnum):
BCH_MERCY = auto()
PTX_PRE_FUNDED = auto()
class ActionTypes(IntEnum):
ACCEPT_BID = auto()
+1 -1
View File
@@ -213,7 +213,7 @@ class HttpHandler(BaseHTTPRequestHandler):
status_code=200,
version=__version__,
extra_headers=None,
):
) -> bytes:
swap_client = self.server.swap_client
if swap_client.ws_server:
args_dict["ws_port"] = swap_client.ws_server.client_port
+10 -1
View File
@@ -160,14 +160,23 @@ class BCHInterface(BTCInterface):
amount: int,
sub_fee: bool = False,
lock_unspents: bool = True,
feerate: int = None,
) -> str:
txn = self.rpc(
"createrawtransaction", [[], {addr_to: self.format_amount(amount)}]
)
if feerate:
fee_rate = self.format_amount(feerate)
fee_src = "specified"
else:
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": lock_unspents,
# 'conf_target': self._conf_target,
"feeRate": fee_rate,
}
if sub_fee:
options["subtractFeeFromOutputs"] = [
+64 -7
View File
@@ -1873,7 +1873,9 @@ class BTCInterface(FeeValidator, Secp256k1Interface):
pubkey = PublicKey(K)
return pubkey.verify(sig[:-1], sig_hash, hasher=None) # Pop the hashtype byte
def fundTx(self, tx: bytes, feerate) -> bytes:
def fundTx(
self, tx: bytes, feerate: int, lock_unspents: bool = True, subfee: bool = False
) -> bytes:
if self.useBackend():
return self._fundTxElectrum(tx, feerate)
@@ -1881,9 +1883,14 @@ class BTCInterface(FeeValidator, Secp256k1Interface):
# TODO: Unlock unspents if bid cancelled
# TODO: Manually select only segwit prevouts
options = {
"lockUnspents": True,
"lockUnspents": lock_unspents,
"feeRate": feerate_str,
}
if subfee:
tx_obj = self.loadTx(tx, allow_witness=False)
num_vouts: int = len(tx_obj.vout)
ensure(num_vouts > 0, "Missing tx outputs")
options["subtractFeeFromOutputs"] = list(range(num_vouts))
rv = self.rpc_wallet("fundrawtransaction", [tx.hex(), options])
tx_bytes: bytes = bytes.fromhex(rv["hex"])
return tx_bytes
@@ -2705,10 +2712,17 @@ class BTCInterface(FeeValidator, Secp256k1Interface):
tx.vout.append(self.txoType()(output_amount, p2wpkh_script_pk))
return tx.serialize()
def encodeSharedAddress(self, Kbv, Kbs):
def encodeSharedAddress(self, Kbv: bytes, Kbs: bytes) -> str:
return self.pubkey_to_segwit_address(Kbs)
def publishBLockTx(self, kbv, Kbs, output_amount, feerate, unlock_time: int = 0):
def publishBLockTx(
self,
kbv: bytes,
Kbs: bytes,
output_amount: int,
feerate: int,
unlock_time: int = 0,
) -> (bytes, int):
b_lock_tx = self.createBLockTx(Kbs, output_amount)
b_lock_tx = self.fundTx(b_lock_tx, feerate)
@@ -2731,8 +2745,8 @@ class BTCInterface(FeeValidator, Secp256k1Interface):
def findTxB(
self,
kbv,
Kbs,
kbv: bytes,
Kbs: bytes,
cb_swap_value: int,
cb_block_confirmed: int,
restore_height: int,
@@ -3461,6 +3475,7 @@ class BTCInterface(FeeValidator, Secp256k1Interface):
amount: int,
sub_fee: bool = False,
lock_unspents: bool = True,
feerate: int = None,
) -> str:
if self.useBackend():
return self._createRawFundedTransactionElectrum(addr_to, amount, sub_fee)
@@ -3468,10 +3483,18 @@ class BTCInterface(FeeValidator, Secp256k1Interface):
txn = self.rpc(
"createrawtransaction", [[], {addr_to: self.format_amount(amount)}]
)
if feerate:
fee_rate = self.format_amount(feerate)
fee_src = "specified"
else:
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": lock_unspents,
"conf_target": self._conf_target,
"feeRate": fee_rate,
}
if sub_fee:
options["subtractFeeFromOutputs"] = [
@@ -4492,6 +4515,40 @@ class BTCInterface(FeeValidator, Secp256k1Interface):
return tx["txid"]
def validatePrefundedTxAmounts(self, tx_data: bytes) -> (bytes, int):
# unspent_utxos = self.listUtxos()
tx_obj = self.loadTx(tx_data, allow_witness=False)
total_out: int = 0
total_in: int = 0
for txo in tx_obj.vout:
total_out += txo.nValue
dummy_witness_stack = []
used_utxos = set()
for txi in tx_obj.vin:
txi_txid_hex: str = i2h(txi.prevout.hash)
txi_vout: int = txi.prevout.n
if (txi_txid_hex, txi_vout) in used_utxos:
raise ValueError(f"Duplicate txin {txi_txid_hex} {txi_vout}")
prev_tx = self.rpc_wallet("gettransaction", [txi_txid_hex])
prev_tx_obj = self.describeTx(prev_tx["hex"])
txo = prev_tx_obj["vout"][txi_vout]
total_in += self.make_int(txo["value"])
dummy_witness_stack.append(self.getP2WPKHDummyWitness())
used_utxos.add((txi_txid_hex, txi_vout))
fee: int = total_in - total_out
witness_bytes_len_est: int = self.getWitnessStackSerialisedLength(
dummy_witness_stack
)
vsize = self.getTxVSize(tx_obj, add_witness_bytes=witness_bytes_len_est)
fee_rate = fee * 1000 // vsize
return bytes.fromhex(txi_txid_hex), fee_rate
def testBTCInterface():
print("TODO: testBTCInterface")
+9 -2
View File
@@ -856,12 +856,17 @@ class DCRInterface(FeeValidator, Secp256k1Interface):
amount: int,
sub_fee: bool = False,
lock_unspents: bool = True,
feerate: int = None,
) -> str:
# amount can't be a string, else: Failed to parse request: parameter #2 'amounts' must be type float64 (got string)
float_amount = float(self.format_amount(amount))
txn = self.rpc("createrawtransaction", [[], {addr_to: float_amount}])
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
if feerate:
fee_rate = feerate
fee_src = "specified"
else:
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}"
)
@@ -1071,7 +1076,9 @@ class DCRInterface(FeeValidator, Secp256k1Interface):
def decodeRawTransaction(self, tx_hex: str):
return self.rpc("decoderawtransaction", [tx_hex])
def fundTx(self, tx: bytes, feerate) -> bytes:
def fundTx(
self, tx: bytes, feerate: int, lock_unspents: bool = True, subfee: bool = False
) -> bytes:
feerate_str = float(self.format_amount(feerate))
# TODO: unlock unspents if bid cancelled
options = {
+6 -1
View File
@@ -305,11 +305,16 @@ class FIROInterface(BTCInterface):
amount: int,
sub_fee: bool = False,
lock_unspents: bool = True,
feerate: int = None,
) -> str:
txn = self.rpc(
"createrawtransaction", [[], {addr_to: self.format_amount(amount)}]
)
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
if feerate:
fee_rate = self.format_amount(feerate)
fee_src = "specified"
else:
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}"
)
+13 -2
View File
@@ -316,11 +316,16 @@ class NAVInterface(BTCInterface):
amount: int,
sub_fee: bool = False,
lock_unspents: bool = True,
feerate: int = None,
) -> str:
txn = self.rpc(
"createrawtransaction", [[], {addr_to: self.format_amount(amount)}]
)
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
if feerate:
fee_rate = self.format_amount(feerate)
fee_src = "specified"
else:
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}"
)
@@ -756,7 +761,13 @@ class NAVInterface(BTCInterface):
return tx.serialize()
def fundTx(self, tx_hex: str, feerate: int, lock_unspents: bool = True):
def fundTx(
self,
tx_hex: str,
feerate: int,
lock_unspents: bool = True,
subfee: bool = False,
):
feerate_str = self.format_amount(feerate)
# TODO: unlock unspents if bid cancelled
options = {
+10 -1
View File
@@ -1240,6 +1240,7 @@ class PARTInterfaceBlind(PARTInterface):
amount: int,
sub_fee: bool = False,
lock_unspents: bool = True,
feerate: int = None,
) -> str:
# Estimate lock tx size / fee
@@ -1279,9 +1280,17 @@ class PARTInterfaceBlind(PARTInterface):
}
}
if feerate:
fee_rate = self.format_amount(feerate)
fee_src = "specified"
else:
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": lock_unspents,
"conf_target": self._conf_target,
"feeRate": fee_rate,
}
if sub_fee:
options["subtractFeeFromOutputs"] = [
+6 -1
View File
@@ -79,11 +79,16 @@ class PIVXInterface(BTCInterface):
amount: int,
sub_fee: bool = False,
lock_unspents: bool = True,
feerate: int = None,
) -> str:
txn = self.rpc(
"createrawtransaction", [[], {addr_to: self.format_amount(amount)}]
)
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
if feerate:
fee_rate = self.format_amount(feerate)
fee_src = "specified"
else:
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}"
)
+27
View File
@@ -1945,6 +1945,32 @@ def js_electrum_discover(self, url_split, post_string, is_json) -> bytes:
)
def js_getsubfeebidtx(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
swap_client.checkSystemStatus()
post_data = getFormData(post_string, is_json)
offer_id = bytes.fromhex(get_data_entry(post_data, "offer_id"))
offer = swap_client.getOffer(offer_id)
ensure(offer, "Offer not found.")
ci_from = swap_client.ci(offer.coin_from)
ci_to = swap_client.ci(offer.coin_to)
amount_to: int = inputAmount(get_data_entry(post_data, "amount_to"), ci_to)
bid_rate: int = ci_to.make_int(get_data_entry(post_data, "bid_rate"), r=1)
prefunded_data = swap_client.createSubfeeBidTx(offer_id, amount_to, bid_rate)
return bytes(
json.dumps(
{
"amount_from": ci_from.format_amount(prefunded_data["amount_from"]),
"amount_to": ci_to.format_amount(prefunded_data["amount_to"]),
"bid_tx": prefunded_data["bid_tx"].hex(),
}
),
"UTF-8",
)
endpoints = {
"coins": js_coins,
"walletbalances": js_walletbalances,
@@ -1981,6 +2007,7 @@ endpoints = {
"messageroutes": js_messageroutes,
"electrumdiscover": js_electrum_discover,
"modeswitchinfo": js_modeswitchinfo,
"getsubfeebidtx": js_getsubfeebidtx,
}
+2 -5
View File
@@ -15,9 +15,6 @@ from basicswap.interface.btc import (
class ProtocolInterface:
swap_type = None
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
raise ValueError("base class")
def getMockScript(self) -> bytearray:
return bytearray([OpCodes.OP_RETURN, OpCodes.OP_1])
@@ -29,7 +26,7 @@ class ProtocolInterface:
else ci.get_p2sh_script_pubkey(script)
)
def getMockAddrTo(self, ci):
def getMockScriptAddr(self, ci):
script = self.getMockScript()
return (
ci.encodeScriptDest(ci.getScriptDest(script))
@@ -38,5 +35,5 @@ class ProtocolInterface:
)
def findMockVout(self, ci, itx_decoded):
mock_addr = self.getMockAddrTo(ci)
mock_addr = self.getMockScriptAddr(ci)
return find_vout_for_address_from_txobj(itx_decoded, mock_addr)
+5 -3
View File
@@ -138,10 +138,12 @@ def redeemITx(self, bid_id: bytes, cursor):
class AtomicSwapInterface(ProtocolInterface):
swap_type = SwapTypes.SELLER_FIRST
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
addr_to = self.getMockAddrTo(ci)
def getFundedInitiateTxTemplate(
self, ci, amount: int, sub_fee: bool, feerate: int = None
) -> bytes:
addr_to = self.getMockScriptAddr(ci)
funded_tx = ci.createRawFundedTransaction(
addr_to, amount, sub_fee, lock_unspents=False
addr_to, amount, sub_fee, lock_unspents=False, feerate=feerate
)
return bytes.fromhex(funded_tx)
+64 -7
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers
# Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -11,6 +11,7 @@ from basicswap.util import (
ensure,
)
from basicswap.interface.base import Curves
from basicswap.interface.btc import findOutput
from basicswap.chainparams import (
Coins,
)
@@ -203,6 +204,9 @@ def setDLEAG(xmr_swap, ci_to, kbsf: bytes) -> None:
class XmrSwapInterface(ProtocolInterface):
swap_type = SwapTypes.XMR_SWAP
_mock_key: bytes = bytes.fromhex(
"e6b8e7c2ca3a88fe4f28591aa0f91fec340179346559e4ec430c2531aecc19aa"
)
def genScriptLockTxScript(self, ci, Kal: bytes, Kaf: bytes, **kwargs) -> CScript:
# fallthrough to ci if genScriptLockTxScript is implemented there
@@ -214,20 +218,40 @@ class XmrSwapInterface(ProtocolInterface):
return CScript([2, Kal, Kaf, 2, CScriptOp(OP_CHECKMULTISIG)])
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
addr_to = self.getMockAddrTo(ci)
def getFundedInitiateTxTemplate(
self, ci, amount: int, sub_fee: bool, feerate: int = None
) -> bytes:
addr_to = self.getMockScriptAddr(ci)
funded_tx = ci.createRawFundedTransaction(
addr_to, amount, sub_fee, lock_unspents=False
addr_to, amount, sub_fee, lock_unspents=False, feerate=feerate
)
return bytes.fromhex(funded_tx)
def getMockITxSwapValue(self, ci, tx_data: bytes) -> int:
script: bytes = self.getMockScript()
script_dest: bytes = ci.getScriptDest(script)
tx_obj = ci.loadTx(tx_data, allow_witness=False)
lock_vout = findOutput(tx_obj, script_dest)
if lock_vout < 0:
raise ValueError("swap output not found")
return tx_obj.vout[lock_vout].nValue
def getMockITxSwapVout(self, ci, tx_obj) -> int:
script: bytes = self.getMockScript()
script_dest: bytes = ci.getScriptDest(script)
lock_vout = findOutput(tx_obj, script_dest)
if lock_vout is None:
raise ValueError("swap output not found")
return lock_vout
def promoteMockTx(self, ci, mock_tx: bytes, script: bytearray) -> bytearray:
mock_txo_script = self.getMockScriptScriptPubkey(ci)
real_txo_script = ci.getScriptDest(script)
found: int = 0
ctx = ci.loadTx(mock_tx)
ctx = ci.loadTx(mock_tx, allow_witness=False)
for txo in ctx.vout:
if txo.scriptPubKey == mock_txo_script:
txo.scriptPubKey = real_txo_script
@@ -239,4 +263,37 @@ class XmrSwapInterface(ProtocolInterface):
raise ValueError("Too many mocked outputs found")
ctx.nLockTime = 0
return ctx.serialize()
return ctx.serialize_without_witness()
def getMockPubkey(self, ci) -> bytes:
return ci.getPubkey(self._mock_key)
def getMockPTxSwapValue(self, ci, tx_data: bytes) -> int:
mock_pk: bytes = self.getMockPubkey(ci)
script_pk = ci.getPkDest(mock_pk)
tx_obj = ci.loadTx(tx_data, allow_witness=False)
lock_vout = findOutput(tx_obj, script_pk)
if lock_vout < 0:
raise ValueError("swap output not found")
return tx_obj.vout[lock_vout].nValue
def getMockPTxSwapVout(self, ci, tx_obj) -> int:
mock_pk: bytes = self.getMockPubkey(ci)
script_pk = ci.getPkDest(mock_pk)
lock_vout = findOutput(tx_obj, script_pk)
if lock_vout is None:
raise ValueError("swap output not found")
return lock_vout
def promoteMockPTx(self, ci, tx_data: bytes, kbv: bytes, Kbs: bytes) -> bytes:
mock_pk: bytes = self.getMockPubkey(ci)
script_pk = ci.getPkDest(mock_pk)
tx_obj = ci.loadTx(tx_data)
lock_vout = findOutput(tx_obj, script_pk)
if lock_vout < 0:
raise ValueError("swap output not found")
tx_obj.vout[lock_vout].scriptPubKey = ci.getPkDest(Kbs)
return tx_obj.serialize_without_witness()
+60 -1
View File
@@ -183,6 +183,55 @@
}
},
setBidAmount: function(percent, inputId) {
const amountInput = window.DOMCache
? window.DOMCache.get(inputId)
: document.getElementById(inputId);
if (!amountInput) {
console.error('EventHandlers: Bid amount input not found:', inputId);
return;
}
const haveBalance = amountInput.getAttribute('haveamount');
if (!haveBalance) {
console.error('EventHandlers: Balance not found for bid');
return;
}
const floatBalance = parseFloat(haveBalance);
if (isNaN(floatBalance)) {
alert('Invalid bid balance');
return;
}
const maxAmount = amountInput.getAttribute('max');
if (!maxAmount) {
console.error('EventHandlers: Max amount not found for bid');
return;
}
const floatMax = parseFloat(maxAmount);
if (isNaN(floatMax) || floatMax <= 0) {
alert('Invalid bid max amount');
return;
}
const coinExp = amountInput.getAttribute('exp');
if (!coinExp) {
console.error('EventHandlers: Coin exp not found for bid');
return;
}
let calculatedAmount = maxAmount * percent;
if (floatBalance < calculatedAmount) {
calculatedAmount = floatBalance;
const checkbox = document.getElementById('subfee_bid');
if (checkbox) {
checkbox.checked = true;
}
}
amountInput.value = calculatedAmount.toFixed(coinExp);
window.updateBidParams('sending');
},
hideConfirmModal: function() {
const modal = document.getElementById('confirmModal');
if (modal) {
@@ -192,7 +241,6 @@
},
lookup_rates: function() {
if (window.lookup_rates && typeof window.lookup_rates === 'function') {
window.lookup_rates();
} else {
@@ -346,6 +394,16 @@
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-set-bid-amount]');
if (target) {
e.preventDefault();
const percent = parseFloat(target.getAttribute('data-set-bid-amount'));
const inputId = target.getAttribute('data-input-id');
this.setBidAmount(percent, inputId);
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-reset-form]');
if (target) {
@@ -419,6 +477,7 @@
window.fillDonationAddress = EventHandlers.fillDonationAddress.bind(EventHandlers);
window.setAmmAmount = EventHandlers.setAmmAmount.bind(EventHandlers);
window.setOfferAmount = EventHandlers.setOfferAmount.bind(EventHandlers);
window.setBidAmount = EventHandlers.setBidAmount.bind(EventHandlers);
window.resetForm = EventHandlers.resetForm.bind(EventHandlers);
window.hideConfirmModal = EventHandlers.hideConfirmModal.bind(EventHandlers);
window.toggleNotificationDropdown = EventHandlers.toggleNotificationDropdown.bind(EventHandlers);
+55 -6
View File
@@ -4,11 +4,13 @@
const OfferPage = {
xhr_rates: null,
xhr_bid_params: null,
xhr_bid_prefund: null,
init: function() {
this.xhr_rates = new XMLHttpRequest();
this.xhr_bid_params = new XMLHttpRequest();
this.xhr_bid_prefund = new XMLHttpRequest();
this.setupXHRHandlers();
this.setupEventListeners();
this.handleBidsPageAddress();
@@ -36,6 +38,20 @@
this.updateModalValues();
}
};
this.xhr_bid_prefund.onload = () => {
if (this.xhr_bid_prefund.status == 200) {
const obj = JSON.parse(this.xhr_bid_prefund.response);
const bidAmountInput = document.getElementById('bid_amount');
if (bidAmountInput) {
bidAmountInput.value = obj['amount_from'];
}
const prefundedBidInput = document.getElementById('prefunded_bid_tx');
if (prefundedBidInput) {
prefundedBidInput.value = obj['bid_tx'];
}
}
};
},
setupEventListeners: function() {
@@ -112,7 +128,7 @@
const bidRateInput = document.getElementById('bid_rate');
const validMinsInput = document.querySelector('input[name="validmins"]');
const amtVar = document.getElementById('amt_var')?.value === 'True';
if (bidAmountSendInput) {
bidAmountSendInput.value = amtVar ? '' : bidAmountSendInput.getAttribute('max');
}
@@ -130,7 +146,7 @@
this.updateBidParams('rate');
}
this.updateModalValues();
const errorMessages = document.querySelectorAll('.error-message');
errorMessages.forEach(msg => msg.remove());
@@ -156,6 +172,7 @@
const bidAmountSendInput = document.getElementById('bid_amount_send');
const bidRateInput = document.getElementById('bid_rate');
const offerRateInput = document.getElementById('offer_rate');
const bidSubfee = document.getElementById('subfee_bid');
if (!coin_from || !coin_to || !amt_var || !rate_var) return;
@@ -171,7 +188,7 @@
const receiveAmount = (sendAmount / rate).toFixed(coin_from_exp);
bidAmountInput.value = receiveAmount;
}
} else if (value_changed === 'sending') {
} else if (value_changed === 'sending' || value_changed === 'subfee') {
if (bidAmountSendInput && bidAmountInput) {
const sendAmount = parseFloat(bidAmountSendInput.value) || 0;
const receiveAmount = (sendAmount / rate).toFixed(coin_from_exp);
@@ -187,8 +204,30 @@
this.validateAmountsAfterChange();
if (bidSubfee && bidSubfee.checked) {
bidAmountInput.readOnly = true;
const offer_id = document.getElementById('offer_id')?.value || '';
if (!offer_id) {
console.log("offer_id not found!");
return;
}
this.xhr_bid_prefund.open('POST', '/json/getsubfeebidtx');
this.xhr_bid_prefund.setRequestHeader('Content-type', 'application/json;charset=UTF-8');
const data = { offer_id: offer_id, amount_to: bidAmountSendInput.value , bid_rate: rate};
this.xhr_bid_prefund.overrideMimeType("application/json");
this.xhr_bid_prefund.send(JSON.stringify(data));
return;
}
bidAmountInput.readOnly = false;
const prefundedBidInput = document.getElementById('prefunded_bid_tx');
if (prefundedBidInput) {
prefundedBidInput.value = "";
}
this.xhr_bid_params.open('POST', '/json/rate');
this.xhr_bid_params.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
this.xhr_bid_params.overrideMimeType("application/json");
this.xhr_bid_params.send(`coin_from=${coin_from}&coin_to=${coin_to}&rate=${rate}&amt_from=${bidAmountInput?.value || '0'}`);
this.updateModalValues();
@@ -253,6 +292,11 @@
this.showErrorModal('Validation Error', 'Please enter valid amounts for both sending and receiving.');
return false;
}
let subfee = false;
const checkbox = document.getElementById('subfee_bid');
if (checkbox) {
subfee = checkbox.checked;
}
const coinFrom = document.getElementById('coin_from_name')?.value || '';
const coinTo = document.getElementById('coin_to_name')?.value || '';
@@ -273,7 +317,12 @@
if (modalAmtReceive) modalAmtReceive.textContent = receiveAmount.toFixed(8);
if (modalReceiveCurrency) modalReceiveCurrency.textContent = ` ${tlaFrom}`;
if (modalAmtSend) modalAmtSend.textContent = sendAmount.toFixed(8);
if (modalSendCurrency) modalSendCurrency.textContent = ` ${tlaTo}`;
if (modalSendCurrency) {
modalSendCurrency.textContent = ` ${tlaTo}`;
if (subfee) {
modalSendCurrency.textContent += ` (incl fee)`;
}
}
if (modalAddrFrom) modalAddrFrom.textContent = addrFrom || 'Default';
if (modalValidMins) modalValidMins.textContent = validMins;
@@ -293,7 +342,7 @@
},
updateModalValues: function() {
},
handleBidsPageAddress: function() {
+13
View File
@@ -419,11 +419,22 @@
name="bid_amount_send"
value=""
max="{{ data.amt_to }}"
haveamount="{{ data.coin_to_balance }}"
exp="{{ data.coin_to_exp }}"
onchange="validateMaxAmount(this, parseFloat('{{ data.amt_to }}')); updateBidParams('sending');">
<div class="absolute inset-y-0 right-3 flex items-center pointer-events-none text-gray-400 dark:text-gray-300 text-sm">
max {{ data.amt_to }} ({{ data.tla_to }})
</div>
</div>
<div class="mt-2 flex space-x-2">
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" data-set-bid-amount="1" data-input-id="bid_amount_send">max</button>
{% if data.bid_can_subfee == true %}
<label>
<input type="checkbox" name="subfee_bid" id="subfee_bid" value="sfb" onchange="updateBidParams('subfee');"/>
<span for="subfee_bid">Subfee</span>
</label>
{% endif %}
</div>
</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
@@ -721,6 +732,8 @@
<input type="hidden" id="coin_to_name" value="{{ data.coin_to }}">
<input type="hidden" id="tla_from" value="{{ data.tla_from }}">
<input type="hidden" id="tla_to" value="{{ data.tla_to }}">
<input type="hidden" id="offer_id" value="{{ offer_id }}">
<input type="hidden" name="prefunded_bid_tx" id="prefunded_bid_tx" value="{{ data.prefunded_bid_tx }}">
<input type="hidden" name="formid" value="{{ form_id }}">
</form>
<p id="rates_display"></p>
+38 -3
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022-2024 tecnovert
# Copyright (c) 2022-2026 tecnovert
# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -8,6 +8,7 @@
import traceback
import time
from typing import List
from urllib import parse
from .util import (
getCoinType,
@@ -583,7 +584,7 @@ def page_newoffer(self, url_split, post_string):
)
def page_offer(self, url_split, post_string):
def page_offer(self, url_split: List[str], post_string: str) -> bytes:
ensure(len(url_split) > 2, "Offer ID not specified")
offer_id = decode_offer_id(url_split[2])
server = self.server
@@ -674,6 +675,11 @@ def page_offer(self, url_split, post_string):
amount_from = offer.amount_from
debugind = int(get_data_entry_or(form_data, "debugind", -1))
if have_data_entry(form_data, "subfee_bid"):
extra_options["prefunded_tx"] = bytes.fromhex(
get_data_entry(form_data, "prefunded_bid_tx")
)
sent_bid_id = swap_client.postBid(
offer_id,
amount_from,
@@ -823,6 +829,33 @@ def page_offer(self, url_split, post_string):
)
data["amt_swapped"] = ci_from.format_amount(amt_swapped)
if show_bid_form:
coin_to_id = int(ci_to.coin_type())
wallet_coin_to_id = coin_to_id
if coin_to_id in (Coins.PART_ANON, Coins.PART_BLIND):
wallet_coin_to_id = Coins.PART
swap_client.updateWalletsInfo(only_coin=wallet_coin_to_id)
coin_to_wallet = swap_client.getCachedWalletsInfo(
{"coin_id": wallet_coin_to_id}
)[wallet_coin_to_id]
if coin_to_id == Coins.PART_ANON:
balance_key = "anon_balance"
elif coin_to_id == Coins.PART_BLIND:
balance_key = "blind_balance"
else:
balance_key = "balance"
data["coin_to_balance"] = coin_to_wallet[balance_key]
bid_can_subfee: bool = True
if offer.swap_type != SwapTypes.XMR_SWAP:
bid_can_subfee = False
if coin_to_id in (Coins.XMR, Coins.WOW):
bid_can_subfee = False
if offer.amount_negotiable is False:
bid_can_subfee = False
data["bid_can_subfee"] = bid_can_subfee
template = server.env.get_template("offer.html")
return self.render_template(
template,
@@ -841,7 +874,9 @@ def page_offer(self, url_split, post_string):
)
def format_timestamp(timestamp, with_ago=True, is_expired=False):
def format_timestamp(
timestamp: int, with_ago: bool = True, is_expired: bool = False
) -> str:
current_time = int(time.time())
if is_expired:
+4 -1
View File
@@ -13,11 +13,14 @@
- New setting "startup_delay"
- Adjusts the time waited for coin daemons to start between "startup_tries".
- Valid as a base setting and can be overridden per coin with chainclients settings.
- Add subfee bids.
- Enables a user to create a bid specifying the amount before the lock tx fee.
- Currently only works when the coin to is not XMR like.
- UI:
- offer page:
- Fixed feerate from other chain displayed for reversed swaps
- Added warning text for fee above 1.2 x local estimate.
- Added subfee bid option.
0.14.5
+1 -1
View File
@@ -849,7 +849,7 @@ class Test(unittest.TestCase):
swap_value = ci_from.make_int(swap_value)
assert swap_value > ci_from.make_int(9)
addr_to = pi.getMockAddrTo(ci_from)
addr_to = pi.getMockScriptAddr(ci_from)
funded_tx = ci_from.createRawFundedTransaction(
addr_to, swap_value, True, lock_unspents=True
)
+161 -6
View File
@@ -2161,8 +2161,8 @@ class BasicSwapTest(TestFunctions):
self.do_test_05_self_bid(Coins.XMR, self.test_coin_from)
def test_06_preselect_inputs(self):
tla_from = self.test_coin_from.name
logging.info("---------- Test {} Preselected inputs".format(tla_from))
tla_from: str = self.test_coin_from.name
logging.info(f"---------- Test {tla_from} Preselected inputs")
swap_clients = self.swap_clients
self.prepare_balance(self.test_coin_from, 100.0, 1802, 1800)
@@ -2262,12 +2262,168 @@ class BasicSwapTest(TestFunctions):
assert txin["txid"] == txin_after["txid"]
assert txin["vout"] == txin_after["vout"]
def test_06_b_preselect_bid_inputs(self):
coin_from, coin_to = (Coins.PART, self.test_coin_from)
logging.info(
f"---------- Test {coin_from.name} to {coin_to.name} Preselected bid inputs"
)
id_offerer, id_bidder = (1, 0)
swap_clients = self.swap_clients
ci_from = swap_clients[id_offerer].ci(coin_from)
ci_to = swap_clients[id_bidder].ci(coin_to)
amt_swap: int = ci_from.make_int(random.uniform(0.1, 2.0), r=1)
min_swap: int = ci_from.make_int(0.0001)
rate_swap: int = ci_to.make_int(random.uniform(0.2, 20.0), r=1)
extra_options = {
"amount_negotiable": True,
"automation_id": 1,
}
offer_id = swap_clients[id_offerer].postOffer(
coin_from,
coin_to,
amt_swap,
rate_swap,
min_swap,
SwapTypes.XMR_SWAP,
extra_options=extra_options,
)
wait_for_offer(test_delay_event, swap_clients[id_bidder], offer_id)
offer = swap_clients[id_bidder].getOffer(offer_id)
amount_to = (offer.amount_from * offer.rate) // ci_from.COIN()
prefunded_tx_data = swap_clients[id_bidder].createSubfeeBidTx(
offer_id, amount_to, offer.rate
)
ptx_decoded = ci_to.describeTx(prefunded_tx_data["bid_tx"].hex())
ci_to.rpc_wallet("lockunspent", [False, ptx_decoded["vin"]])
bid_tx = prefunded_tx_data["bid_tx"]
amount_from = prefunded_tx_data["amount_from"]
extra_options = {"prefunded_tx": bid_tx}
bid_id = swap_clients[id_bidder].postBid(
offer_id, amount_from, extra_options=extra_options
)
wait_for_bid(
test_delay_event,
swap_clients[id_offerer],
bid_id,
BidStates.SWAP_COMPLETED,
wait_for=120,
)
wait_for_bid(
test_delay_event,
swap_clients[id_bidder],
bid_id,
BidStates.SWAP_COMPLETED,
sent=True,
wait_for=120,
)
# Verify expected inputs were used
bid, _, _, _, _ = swap_clients[id_bidder].getXmrBidAndOffer(bid_id)
assert bid.xmr_b_lock_tx
wtx = ci_to.rpc_wallet(
"gettransaction",
[
bid.xmr_b_lock_tx.txid.hex(),
],
)
ptx_after = ci_to.describeTx(wtx["hex"])
assert len(ptx_after["vin"]) == len(ptx_decoded["vin"])
for i, txin in enumerate(ptx_decoded["vin"]):
txin_after = ptx_after["vin"][i]
assert txin["txid"] == txin_after["txid"]
assert txin["vout"] == txin_after["vout"]
def test_06_c_preselect_reverse_bid_inputs(self):
coin_from, coin_to = (Coins.XMR, self.test_coin_from)
logging.info(
f"---------- Test {coin_from.name} to {coin_to.name} Preselected reverse bid inputs"
)
id_offerer, id_bidder = (1, 0)
swap_clients = self.swap_clients
ci_from = swap_clients[id_offerer].ci(coin_from)
ci_to = swap_clients[id_bidder].ci(coin_to)
amt_swap: int = ci_from.make_int(random.uniform(0.1, 2.0), r=1)
min_swap: int = ci_from.make_int(0.0001)
rate_swap: int = ci_to.make_int(random.uniform(0.2, 20.0), r=1)
extra_options = {
"amount_negotiable": True,
"automation_id": 1,
}
offer_id = swap_clients[id_offerer].postOffer(
coin_from,
coin_to,
amt_swap,
rate_swap,
min_swap,
SwapTypes.XMR_SWAP,
extra_options=extra_options,
)
wait_for_offer(test_delay_event, swap_clients[id_bidder], offer_id)
offer = swap_clients[id_bidder].getOffer(offer_id)
amount_to = (offer.amount_from * offer.rate) // ci_from.COIN()
prefunded_tx_data = swap_clients[id_bidder].createSubfeeBidTx(
offer_id, amount_to, offer.rate
)
ptx_decoded = ci_to.describeTx(prefunded_tx_data["bid_tx"].hex())
ci_to.rpc_wallet("lockunspent", [False, ptx_decoded["vin"]])
bid_tx = prefunded_tx_data["bid_tx"]
amount_from = prefunded_tx_data["amount_from"]
extra_options = {"prefunded_tx": bid_tx}
bid_id = swap_clients[id_bidder].postBid(
offer_id, amount_from, extra_options=extra_options
)
wait_for_bid(
test_delay_event,
swap_clients[id_offerer],
bid_id,
BidStates.SWAP_COMPLETED,
wait_for=180,
)
wait_for_bid(
test_delay_event,
swap_clients[id_bidder],
bid_id,
BidStates.SWAP_COMPLETED,
sent=True,
wait_for=120,
)
# Verify expected inputs were used
bid, _, _, _, _ = swap_clients[id_bidder].getXmrBidAndOffer(bid_id)
assert bid.xmr_a_lock_tx
wtx = ci_to.rpc_wallet(
"gettransaction",
[
bid.xmr_a_lock_tx.txid.hex(),
],
)
ptx_after = ci_to.describeTx(wtx["hex"])
assert len(ptx_after["vin"]) == len(ptx_decoded["vin"])
for i, txin in enumerate(ptx_decoded["vin"]):
txin_after = ptx_after["vin"][i]
assert txin["txid"] == txin_after["txid"]
assert txin["vout"] == txin_after["vout"]
def test_07_expire_stuck_accepted(self):
coin_from, coin_to = (self.test_coin_from, Coins.XMR)
logging.info(
"---------- Test {} to {} expires bid stuck on accepted".format(
coin_from.name, coin_to.name
)
f"---------- Test {coin_from.name} to {coin_to.name} expires bid stuck on accepted"
)
swap_clients = self.swap_clients
@@ -2434,7 +2590,6 @@ class BasicSwapTest(TestFunctions):
ci_from._high_feerate = (
80 # ci_from_settings["high_feerate"] = ci_from.format_amount(80)
)
logging.info(f"[rm] ci_from.get_fee_rate() {ci_from.get_fee_rate()}")
try:
swap_clients[0].postXmrBid(offer_id, amt_swap)
except Exception as e:
+1 -1
View File
@@ -156,7 +156,7 @@ class Test(unittest.TestCase):
assert str(e) == "Mantissa too long"
validate_amount("0.12345678")
# floor
# Floor
assert make_int("0.123456789", r=-1) == 12345678
# Round up
assert make_int("0.123456789", r=1) == 12345679