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 bid_rate = best_bid_rate
return amount, amount_to, 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( def postBid(
self, offer_id: bytes, amount: int, addr_send_from: str = None, extra_options={} self, offer_id: bytes, amount: int, addr_send_from: str = None, extra_options={}
) -> bytes: ) -> bytes:
@@ -6016,46 +6073,66 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
"Incompatible offer protocol version", "Incompatible offer protocol version",
) )
ensure(offer.expire_at > self.getTime(), "Offer has expired") 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_from = Coins(offer.coin_from)
coin_to = Coins(offer.coin_to) coin_to = Coins(offer.coin_to)
ci_from = self.ci(coin_from) ci_from = self.ci(coin_from)
ci_to = self.ci(coin_to) 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) self.checkCoinsReady(coin_from, coin_to)
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}")
ci_from.validateFeeRate(xmr_offer.a_fee_rate) ci_from.validateFeeRate(xmr_offer.a_fee_rate)
ci_to.validateFeeRate(xmr_offer.b_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)): if not (self.debug and extra_options.get("debug_skip_validation", False)):
self.validateBidValidTime( self.validateBidValidTime(
offer.swap_type, coin_from, coin_to, valid_for_seconds offer.swap_type, coin_from, coin_to, valid_for_seconds
) )
self.validateBidAmount(offer, amount, bid_rate) self.validateBidAmount(offer, amount, bid_rate)
self.checkCoinsReady(coin_from, coin_to)
# TODO: Better tx size estimate # TODO: Better tx size estimate
fee_rate, fee_src = self.getFeeRateForCoin(coin_to, conf_target=2) fee_rate_to = xmr_offer.b_fee_rate
fee_rate_to = ci_to.make_int(fee_rate)
estimated_fee: int = fee_rate_to * ci_to.est_lock_tx_vsize() // 1000 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( bid_addr: str = self.prepareSMSGAddress(
addr_send_from, AddressTypes.BID, cursor addr_send_from, AddressTypes.BID, cursor
) )
# return id of route waiting to be established # Return id of route waiting to be established
request_data = { request_data = {
"offer_id": offer_id.hex(), "offer_id": offer_id.hex(),
"amount_from": amount, "amount_from": amount,
@@ -6073,7 +6150,6 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
valid_for_seconds, valid_for_seconds,
) )
reverse_bid: bool = self.is_reverse_ads_bid(coin_from, coin_to)
if reverse_bid: if reverse_bid:
reversed_rate: int = ci_to.make_int(amount / amount_to, r=1) 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 bid.bid_id = bid_id
xmr_swap.bid_id = bid.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.saveBidInSession(xmr_swap.bid_id, bid, cursor, xmr_swap)
self.commitDB() self.commitDB()
@@ -6248,6 +6335,16 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
self.log.warning( self.log.warning(
f"Adaptor-sig swap restore height clamped to {wallet_restore_height}" 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.saveBidInSession(bid.bid_id, bid, cursor, xmr_swap)
self.log.info(f"Sent XMR_BID_FL {self.logIDB(xmr_swap.bid_id)}") 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( xmr_swap.a_lock_tx_script = pi.genScriptLockTxScript(
ci_from, xmr_swap.pkal, xmr_swap.pkaf, **lockExtraArgs ci_from, xmr_swap.pkal, xmr_swap.pkaf, **lockExtraArgs
) )
prefunded_tx = self.getPreFundedTx( if reverse_bid and bid.was_sent:
Concepts.OFFER, prefunded_tx = self.getPreFundedTx(
bid.offer_id, Concepts.BID,
TxTypes.ITX_PRE_FUNDED, bid_id,
cursor=use_cursor, 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: if prefunded_tx:
xmr_swap.a_lock_tx = pi.promoteMockTx( xmr_swap.a_lock_tx = pi.promoteMockTx(
ci_from, prefunded_tx, xmr_swap.a_lock_tx_script ci_from, prefunded_tx, xmr_swap.a_lock_tx_script
) )
self.log.info("Using pre-funded tx")
else: else:
xmr_swap.a_lock_tx = ci_from.createSCLockTx( xmr_swap.a_lock_tx = ci_from.createSCLockTx(
bid.amount, xmr_swap.a_lock_tx_script, xmr_swap.vkbv 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) addr_to = ci.encodeScriptDest(p2wsh)
else: else:
addr_to = ci.encode_p2sh(initiate_script) 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: 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) pi = self.pi(SwapTypes.SELLER_FIRST)
txn_signed = pi.promoteMockTx(ci, prefunded_tx, initiate_script).hex() txn_signed = pi.promoteMockTx(ci, prefunded_tx, initiate_script).hex()
else: 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) txn_signed = ci.createRawSignedTransaction(addr_to, bid.amount)
txjs = ci.describeTx(txn_signed) txjs = ci.describeTx(txn_signed)
@@ -11607,19 +11716,38 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
cursor, cursor,
) )
prefunded_tx = self.getPreFundedTx(
Concepts.BID,
bid.bid_id,
TxTypes.PTX_PRE_FUNDED,
cursor=cursor,
)
try: try:
b_lock_vout = 0 b_lock_vout = 0
result = ci_to.publishBLockTx( if prefunded_tx:
xmr_swap.vkbv, self.log.info("Using pre-funded tx")
xmr_swap.pkbs, pi = self.pi(offer.swap_type)
bid.amount_to, b_lock_tx = pi.promoteMockPTx(
b_fee_rate, ci_to,
unlock_time=unlock_time, prefunded_tx,
) xmr_swap.vkbv,
if isinstance(result, tuple): xmr_swap.pkbs,
b_lock_tx_id, b_lock_vout = result )
b_lock_tx = ci_to.signTxWithWallet(b_lock_tx)
b_lock_tx_id = bytes.fromhex(ci_to.publishTx(b_lock_tx))
else: 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: if bid.debug_ind == DebugTypes.B_LOCK_TX_MISSED_SEND:
self.log.debug( 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)}." 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( def updateWalletsInfo(
self, self,
force_update: bool = False, force_update: bool = False,
only_coin: bool = None, only_coin: int = None,
wait_for_complete: bool = False, wait_for_complete: bool = False,
) -> None: ) -> None:
now: int = self.getTime() now: int = self.getTime()
+2
View File
@@ -161,6 +161,8 @@ class TxTypes(IntEnum):
BCH_MERCY = auto() BCH_MERCY = auto()
PTX_PRE_FUNDED = auto()
class ActionTypes(IntEnum): class ActionTypes(IntEnum):
ACCEPT_BID = auto() ACCEPT_BID = auto()
+1 -1
View File
@@ -213,7 +213,7 @@ class HttpHandler(BaseHTTPRequestHandler):
status_code=200, status_code=200,
version=__version__, version=__version__,
extra_headers=None, extra_headers=None,
): ) -> bytes:
swap_client = self.server.swap_client swap_client = self.server.swap_client
if swap_client.ws_server: if swap_client.ws_server:
args_dict["ws_port"] = swap_client.ws_server.client_port args_dict["ws_port"] = swap_client.ws_server.client_port
+10 -1
View File
@@ -160,14 +160,23 @@ class BCHInterface(BTCInterface):
amount: int, amount: int,
sub_fee: bool = False, sub_fee: bool = False,
lock_unspents: bool = True, lock_unspents: bool = True,
feerate: int = None,
) -> str: ) -> str:
txn = self.rpc( txn = self.rpc(
"createrawtransaction", [[], {addr_to: self.format_amount(amount)}] "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 = { options = {
"lockUnspents": lock_unspents, "lockUnspents": lock_unspents,
# 'conf_target': self._conf_target, "feeRate": fee_rate,
} }
if sub_fee: if sub_fee:
options["subtractFeeFromOutputs"] = [ options["subtractFeeFromOutputs"] = [
+64 -7
View File
@@ -1873,7 +1873,9 @@ class BTCInterface(FeeValidator, Secp256k1Interface):
pubkey = PublicKey(K) pubkey = PublicKey(K)
return pubkey.verify(sig[:-1], sig_hash, hasher=None) # Pop the hashtype byte 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(): if self.useBackend():
return self._fundTxElectrum(tx, feerate) return self._fundTxElectrum(tx, feerate)
@@ -1881,9 +1883,14 @@ class BTCInterface(FeeValidator, Secp256k1Interface):
# TODO: Unlock unspents if bid cancelled # TODO: Unlock unspents if bid cancelled
# TODO: Manually select only segwit prevouts # TODO: Manually select only segwit prevouts
options = { options = {
"lockUnspents": True, "lockUnspents": lock_unspents,
"feeRate": feerate_str, "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]) rv = self.rpc_wallet("fundrawtransaction", [tx.hex(), options])
tx_bytes: bytes = bytes.fromhex(rv["hex"]) tx_bytes: bytes = bytes.fromhex(rv["hex"])
return tx_bytes return tx_bytes
@@ -2705,10 +2712,17 @@ class BTCInterface(FeeValidator, Secp256k1Interface):
tx.vout.append(self.txoType()(output_amount, p2wpkh_script_pk)) tx.vout.append(self.txoType()(output_amount, p2wpkh_script_pk))
return tx.serialize() return tx.serialize()
def encodeSharedAddress(self, Kbv, Kbs): def encodeSharedAddress(self, Kbv: bytes, Kbs: bytes) -> str:
return self.pubkey_to_segwit_address(Kbs) 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.createBLockTx(Kbs, output_amount)
b_lock_tx = self.fundTx(b_lock_tx, feerate) b_lock_tx = self.fundTx(b_lock_tx, feerate)
@@ -2731,8 +2745,8 @@ class BTCInterface(FeeValidator, Secp256k1Interface):
def findTxB( def findTxB(
self, self,
kbv, kbv: bytes,
Kbs, Kbs: bytes,
cb_swap_value: int, cb_swap_value: int,
cb_block_confirmed: int, cb_block_confirmed: int,
restore_height: int, restore_height: int,
@@ -3461,6 +3475,7 @@ class BTCInterface(FeeValidator, Secp256k1Interface):
amount: int, amount: int,
sub_fee: bool = False, sub_fee: bool = False,
lock_unspents: bool = True, lock_unspents: bool = True,
feerate: int = None,
) -> str: ) -> str:
if self.useBackend(): if self.useBackend():
return self._createRawFundedTransactionElectrum(addr_to, amount, sub_fee) return self._createRawFundedTransactionElectrum(addr_to, amount, sub_fee)
@@ -3468,10 +3483,18 @@ class BTCInterface(FeeValidator, Secp256k1Interface):
txn = self.rpc( txn = self.rpc(
"createrawtransaction", [[], {addr_to: self.format_amount(amount)}] "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 = { options = {
"lockUnspents": lock_unspents, "lockUnspents": lock_unspents,
"conf_target": self._conf_target, "feeRate": fee_rate,
} }
if sub_fee: if sub_fee:
options["subtractFeeFromOutputs"] = [ options["subtractFeeFromOutputs"] = [
@@ -4492,6 +4515,40 @@ class BTCInterface(FeeValidator, Secp256k1Interface):
return tx["txid"] 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(): def testBTCInterface():
print("TODO: testBTCInterface") print("TODO: testBTCInterface")
+9 -2
View File
@@ -856,12 +856,17 @@ class DCRInterface(FeeValidator, Secp256k1Interface):
amount: int, amount: int,
sub_fee: bool = False, sub_fee: bool = False,
lock_unspents: bool = True, lock_unspents: bool = True,
feerate: int = None,
) -> str: ) -> str:
# amount can't be a string, else: Failed to parse request: parameter #2 'amounts' must be type float64 (got string) # 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)) float_amount = float(self.format_amount(amount))
txn = self.rpc("createrawtransaction", [[], {addr_to: float_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( self._log.debug(
f"Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}" 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): def decodeRawTransaction(self, tx_hex: str):
return self.rpc("decoderawtransaction", [tx_hex]) 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)) feerate_str = float(self.format_amount(feerate))
# TODO: unlock unspents if bid cancelled # TODO: unlock unspents if bid cancelled
options = { options = {
+6 -1
View File
@@ -305,11 +305,16 @@ class FIROInterface(BTCInterface):
amount: int, amount: int,
sub_fee: bool = False, sub_fee: bool = False,
lock_unspents: bool = True, lock_unspents: bool = True,
feerate: int = None,
) -> str: ) -> str:
txn = self.rpc( txn = self.rpc(
"createrawtransaction", [[], {addr_to: self.format_amount(amount)}] "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( self._log.debug(
f"Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}" 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, amount: int,
sub_fee: bool = False, sub_fee: bool = False,
lock_unspents: bool = True, lock_unspents: bool = True,
feerate: int = None,
) -> str: ) -> str:
txn = self.rpc( txn = self.rpc(
"createrawtransaction", [[], {addr_to: self.format_amount(amount)}] "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( self._log.debug(
f"Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}" f"Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}"
) )
@@ -756,7 +761,13 @@ class NAVInterface(BTCInterface):
return tx.serialize() 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) feerate_str = self.format_amount(feerate)
# TODO: unlock unspents if bid cancelled # TODO: unlock unspents if bid cancelled
options = { options = {
+10 -1
View File
@@ -1240,6 +1240,7 @@ class PARTInterfaceBlind(PARTInterface):
amount: int, amount: int,
sub_fee: bool = False, sub_fee: bool = False,
lock_unspents: bool = True, lock_unspents: bool = True,
feerate: int = None,
) -> str: ) -> str:
# Estimate lock tx size / fee # 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 = { options = {
"lockUnspents": lock_unspents, "lockUnspents": lock_unspents,
"conf_target": self._conf_target, "feeRate": fee_rate,
} }
if sub_fee: if sub_fee:
options["subtractFeeFromOutputs"] = [ options["subtractFeeFromOutputs"] = [
+6 -1
View File
@@ -79,11 +79,16 @@ class PIVXInterface(BTCInterface):
amount: int, amount: int,
sub_fee: bool = False, sub_fee: bool = False,
lock_unspents: bool = True, lock_unspents: bool = True,
feerate: int = None,
) -> str: ) -> str:
txn = self.rpc( txn = self.rpc(
"createrawtransaction", [[], {addr_to: self.format_amount(amount)}] "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( self._log.debug(
f"Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}" 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 = { endpoints = {
"coins": js_coins, "coins": js_coins,
"walletbalances": js_walletbalances, "walletbalances": js_walletbalances,
@@ -1981,6 +2007,7 @@ endpoints = {
"messageroutes": js_messageroutes, "messageroutes": js_messageroutes,
"electrumdiscover": js_electrum_discover, "electrumdiscover": js_electrum_discover,
"modeswitchinfo": js_modeswitchinfo, "modeswitchinfo": js_modeswitchinfo,
"getsubfeebidtx": js_getsubfeebidtx,
} }
+2 -5
View File
@@ -15,9 +15,6 @@ from basicswap.interface.btc import (
class ProtocolInterface: class ProtocolInterface:
swap_type = None swap_type = None
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
raise ValueError("base class")
def getMockScript(self) -> bytearray: def getMockScript(self) -> bytearray:
return bytearray([OpCodes.OP_RETURN, OpCodes.OP_1]) return bytearray([OpCodes.OP_RETURN, OpCodes.OP_1])
@@ -29,7 +26,7 @@ class ProtocolInterface:
else ci.get_p2sh_script_pubkey(script) else ci.get_p2sh_script_pubkey(script)
) )
def getMockAddrTo(self, ci): def getMockScriptAddr(self, ci):
script = self.getMockScript() script = self.getMockScript()
return ( return (
ci.encodeScriptDest(ci.getScriptDest(script)) ci.encodeScriptDest(ci.getScriptDest(script))
@@ -38,5 +35,5 @@ class ProtocolInterface:
) )
def findMockVout(self, ci, itx_decoded): 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) 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): class AtomicSwapInterface(ProtocolInterface):
swap_type = SwapTypes.SELLER_FIRST swap_type = SwapTypes.SELLER_FIRST
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes: def getFundedInitiateTxTemplate(
addr_to = self.getMockAddrTo(ci) self, ci, amount: int, sub_fee: bool, feerate: int = None
) -> bytes:
addr_to = self.getMockScriptAddr(ci)
funded_tx = ci.createRawFundedTransaction( 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) return bytes.fromhex(funded_tx)
+64 -7
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert # 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 # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -11,6 +11,7 @@ from basicswap.util import (
ensure, ensure,
) )
from basicswap.interface.base import Curves from basicswap.interface.base import Curves
from basicswap.interface.btc import findOutput
from basicswap.chainparams import ( from basicswap.chainparams import (
Coins, Coins,
) )
@@ -203,6 +204,9 @@ def setDLEAG(xmr_swap, ci_to, kbsf: bytes) -> None:
class XmrSwapInterface(ProtocolInterface): class XmrSwapInterface(ProtocolInterface):
swap_type = SwapTypes.XMR_SWAP swap_type = SwapTypes.XMR_SWAP
_mock_key: bytes = bytes.fromhex(
"e6b8e7c2ca3a88fe4f28591aa0f91fec340179346559e4ec430c2531aecc19aa"
)
def genScriptLockTxScript(self, ci, Kal: bytes, Kaf: bytes, **kwargs) -> CScript: def genScriptLockTxScript(self, ci, Kal: bytes, Kaf: bytes, **kwargs) -> CScript:
# fallthrough to ci if genScriptLockTxScript is implemented there # fallthrough to ci if genScriptLockTxScript is implemented there
@@ -214,20 +218,40 @@ class XmrSwapInterface(ProtocolInterface):
return CScript([2, Kal, Kaf, 2, CScriptOp(OP_CHECKMULTISIG)]) return CScript([2, Kal, Kaf, 2, CScriptOp(OP_CHECKMULTISIG)])
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes: def getFundedInitiateTxTemplate(
addr_to = self.getMockAddrTo(ci) self, ci, amount: int, sub_fee: bool, feerate: int = None
) -> bytes:
addr_to = self.getMockScriptAddr(ci)
funded_tx = ci.createRawFundedTransaction( 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) 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: def promoteMockTx(self, ci, mock_tx: bytes, script: bytearray) -> bytearray:
mock_txo_script = self.getMockScriptScriptPubkey(ci) mock_txo_script = self.getMockScriptScriptPubkey(ci)
real_txo_script = ci.getScriptDest(script) real_txo_script = ci.getScriptDest(script)
found: int = 0 found: int = 0
ctx = ci.loadTx(mock_tx) ctx = ci.loadTx(mock_tx, allow_witness=False)
for txo in ctx.vout: for txo in ctx.vout:
if txo.scriptPubKey == mock_txo_script: if txo.scriptPubKey == mock_txo_script:
txo.scriptPubKey = real_txo_script txo.scriptPubKey = real_txo_script
@@ -239,4 +263,37 @@ class XmrSwapInterface(ProtocolInterface):
raise ValueError("Too many mocked outputs found") raise ValueError("Too many mocked outputs found")
ctx.nLockTime = 0 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() { hideConfirmModal: function() {
const modal = document.getElementById('confirmModal'); const modal = document.getElementById('confirmModal');
if (modal) { if (modal) {
@@ -192,7 +241,6 @@
}, },
lookup_rates: function() { lookup_rates: function() {
if (window.lookup_rates && typeof window.lookup_rates === 'function') { if (window.lookup_rates && typeof window.lookup_rates === 'function') {
window.lookup_rates(); window.lookup_rates();
} else { } 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) => { document.addEventListener('click', (e) => {
const target = e.target.closest('[data-reset-form]'); const target = e.target.closest('[data-reset-form]');
if (target) { if (target) {
@@ -419,6 +477,7 @@
window.fillDonationAddress = EventHandlers.fillDonationAddress.bind(EventHandlers); window.fillDonationAddress = EventHandlers.fillDonationAddress.bind(EventHandlers);
window.setAmmAmount = EventHandlers.setAmmAmount.bind(EventHandlers); window.setAmmAmount = EventHandlers.setAmmAmount.bind(EventHandlers);
window.setOfferAmount = EventHandlers.setOfferAmount.bind(EventHandlers); window.setOfferAmount = EventHandlers.setOfferAmount.bind(EventHandlers);
window.setBidAmount = EventHandlers.setBidAmount.bind(EventHandlers);
window.resetForm = EventHandlers.resetForm.bind(EventHandlers); window.resetForm = EventHandlers.resetForm.bind(EventHandlers);
window.hideConfirmModal = EventHandlers.hideConfirmModal.bind(EventHandlers); window.hideConfirmModal = EventHandlers.hideConfirmModal.bind(EventHandlers);
window.toggleNotificationDropdown = EventHandlers.toggleNotificationDropdown.bind(EventHandlers); window.toggleNotificationDropdown = EventHandlers.toggleNotificationDropdown.bind(EventHandlers);
+51 -2
View File
@@ -4,10 +4,12 @@
const OfferPage = { const OfferPage = {
xhr_rates: null, xhr_rates: null,
xhr_bid_params: null, xhr_bid_params: null,
xhr_bid_prefund: null,
init: function() { init: function() {
this.xhr_rates = new XMLHttpRequest(); this.xhr_rates = new XMLHttpRequest();
this.xhr_bid_params = new XMLHttpRequest(); this.xhr_bid_params = new XMLHttpRequest();
this.xhr_bid_prefund = new XMLHttpRequest();
this.setupXHRHandlers(); this.setupXHRHandlers();
this.setupEventListeners(); this.setupEventListeners();
@@ -36,6 +38,20 @@
this.updateModalValues(); 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() { setupEventListeners: function() {
@@ -156,6 +172,7 @@
const bidAmountSendInput = document.getElementById('bid_amount_send'); const bidAmountSendInput = document.getElementById('bid_amount_send');
const bidRateInput = document.getElementById('bid_rate'); const bidRateInput = document.getElementById('bid_rate');
const offerRateInput = document.getElementById('offer_rate'); const offerRateInput = document.getElementById('offer_rate');
const bidSubfee = document.getElementById('subfee_bid');
if (!coin_from || !coin_to || !amt_var || !rate_var) return; if (!coin_from || !coin_to || !amt_var || !rate_var) return;
@@ -171,7 +188,7 @@
const receiveAmount = (sendAmount / rate).toFixed(coin_from_exp); const receiveAmount = (sendAmount / rate).toFixed(coin_from_exp);
bidAmountInput.value = receiveAmount; bidAmountInput.value = receiveAmount;
} }
} else if (value_changed === 'sending') { } else if (value_changed === 'sending' || value_changed === 'subfee') {
if (bidAmountSendInput && bidAmountInput) { if (bidAmountSendInput && bidAmountInput) {
const sendAmount = parseFloat(bidAmountSendInput.value) || 0; const sendAmount = parseFloat(bidAmountSendInput.value) || 0;
const receiveAmount = (sendAmount / rate).toFixed(coin_from_exp); const receiveAmount = (sendAmount / rate).toFixed(coin_from_exp);
@@ -187,8 +204,30 @@
this.validateAmountsAfterChange(); 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.open('POST', '/json/rate');
this.xhr_bid_params.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); 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.xhr_bid_params.send(`coin_from=${coin_from}&coin_to=${coin_to}&rate=${rate}&amt_from=${bidAmountInput?.value || '0'}`);
this.updateModalValues(); this.updateModalValues();
@@ -253,6 +292,11 @@
this.showErrorModal('Validation Error', 'Please enter valid amounts for both sending and receiving.'); this.showErrorModal('Validation Error', 'Please enter valid amounts for both sending and receiving.');
return false; 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 coinFrom = document.getElementById('coin_from_name')?.value || '';
const coinTo = document.getElementById('coin_to_name')?.value || ''; const coinTo = document.getElementById('coin_to_name')?.value || '';
@@ -273,7 +317,12 @@
if (modalAmtReceive) modalAmtReceive.textContent = receiveAmount.toFixed(8); if (modalAmtReceive) modalAmtReceive.textContent = receiveAmount.toFixed(8);
if (modalReceiveCurrency) modalReceiveCurrency.textContent = ` ${tlaFrom}`; if (modalReceiveCurrency) modalReceiveCurrency.textContent = ` ${tlaFrom}`;
if (modalAmtSend) modalAmtSend.textContent = sendAmount.toFixed(8); 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 (modalAddrFrom) modalAddrFrom.textContent = addrFrom || 'Default';
if (modalValidMins) modalValidMins.textContent = validMins; if (modalValidMins) modalValidMins.textContent = validMins;
+13
View File
@@ -419,11 +419,22 @@
name="bid_amount_send" name="bid_amount_send"
value="" value=""
max="{{ data.amt_to }}" max="{{ data.amt_to }}"
haveamount="{{ data.coin_to_balance }}"
exp="{{ data.coin_to_exp }}"
onchange="validateMaxAmount(this, parseFloat('{{ data.amt_to }}')); updateBidParams('sending');"> 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"> <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 }}) max {{ data.amt_to }} ({{ data.tla_to }})
</div> </div>
</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> </td>
</tr> </tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600"> <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="coin_to_name" value="{{ data.coin_to }}">
<input type="hidden" id="tla_from" value="{{ data.tla_from }}"> <input type="hidden" id="tla_from" value="{{ data.tla_from }}">
<input type="hidden" id="tla_to" value="{{ data.tla_to }}"> <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 }}"> <input type="hidden" name="formid" value="{{ form_id }}">
</form> </form>
<p id="rates_display"></p> <p id="rates_display"></p>
+38 -3
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2022-2024 tecnovert # Copyright (c) 2022-2026 tecnovert
# Copyright (c) 2024 The Basicswap developers # Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -8,6 +8,7 @@
import traceback import traceback
import time import time
from typing import List
from urllib import parse from urllib import parse
from .util import ( from .util import (
getCoinType, 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") ensure(len(url_split) > 2, "Offer ID not specified")
offer_id = decode_offer_id(url_split[2]) offer_id = decode_offer_id(url_split[2])
server = self.server server = self.server
@@ -674,6 +675,11 @@ def page_offer(self, url_split, post_string):
amount_from = offer.amount_from amount_from = offer.amount_from
debugind = int(get_data_entry_or(form_data, "debugind", -1)) 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( sent_bid_id = swap_client.postBid(
offer_id, offer_id,
amount_from, amount_from,
@@ -823,6 +829,33 @@ def page_offer(self, url_split, post_string):
) )
data["amt_swapped"] = ci_from.format_amount(amt_swapped) 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") template = server.env.get_template("offer.html")
return self.render_template( return self.render_template(
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()) current_time = int(time.time())
if is_expired: if is_expired:
+4 -1
View File
@@ -13,11 +13,14 @@
- New setting "startup_delay" - New setting "startup_delay"
- Adjusts the time waited for coin daemons to start between "startup_tries". - 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. - 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: - UI:
- offer page: - offer page:
- Fixed feerate from other chain displayed for reversed swaps - Fixed feerate from other chain displayed for reversed swaps
- Added warning text for fee above 1.2 x local estimate. - Added warning text for fee above 1.2 x local estimate.
- Added subfee bid option.
0.14.5 0.14.5
+1 -1
View File
@@ -849,7 +849,7 @@ class Test(unittest.TestCase):
swap_value = ci_from.make_int(swap_value) swap_value = ci_from.make_int(swap_value)
assert swap_value > ci_from.make_int(9) 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( funded_tx = ci_from.createRawFundedTransaction(
addr_to, swap_value, True, lock_unspents=True 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) self.do_test_05_self_bid(Coins.XMR, self.test_coin_from)
def test_06_preselect_inputs(self): def test_06_preselect_inputs(self):
tla_from = self.test_coin_from.name tla_from: str = self.test_coin_from.name
logging.info("---------- Test {} Preselected inputs".format(tla_from)) logging.info(f"---------- Test {tla_from} Preselected inputs")
swap_clients = self.swap_clients swap_clients = self.swap_clients
self.prepare_balance(self.test_coin_from, 100.0, 1802, 1800) 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["txid"] == txin_after["txid"]
assert txin["vout"] == txin_after["vout"] 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): def test_07_expire_stuck_accepted(self):
coin_from, coin_to = (self.test_coin_from, Coins.XMR) coin_from, coin_to = (self.test_coin_from, Coins.XMR)
logging.info( logging.info(
"---------- Test {} to {} expires bid stuck on accepted".format( f"---------- Test {coin_from.name} to {coin_to.name} expires bid stuck on accepted"
coin_from.name, coin_to.name
)
) )
swap_clients = self.swap_clients swap_clients = self.swap_clients
@@ -2434,7 +2590,6 @@ class BasicSwapTest(TestFunctions):
ci_from._high_feerate = ( ci_from._high_feerate = (
80 # ci_from_settings["high_feerate"] = ci_from.format_amount(80) 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: try:
swap_clients[0].postXmrBid(offer_id, amt_swap) swap_clients[0].postXmrBid(offer_id, amt_swap)
except Exception as e: except Exception as e:
+1 -1
View File
@@ -156,7 +156,7 @@ class Test(unittest.TestCase):
assert str(e) == "Mantissa too long" assert str(e) == "Mantissa too long"
validate_amount("0.12345678") validate_amount("0.12345678")
# floor # Floor
assert make_int("0.123456789", r=-1) == 12345678 assert make_int("0.123456789", r=-1) == 12345678
# Round up # Round up
assert make_int("0.123456789", r=1) == 12345679 assert make_int("0.123456789", r=1) == 12345679