mirror of
https://github.com/basicswap/basicswap.git
synced 2026-06-07 19:51:41 +02:00
feat: add subfee bids
This commit is contained in:
+166
-38
@@ -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()
|
||||
|
||||
@@ -161,6 +161,8 @@ class TxTypes(IntEnum):
|
||||
|
||||
BCH_MERCY = auto()
|
||||
|
||||
PTX_PRE_FUNDED = auto()
|
||||
|
||||
|
||||
class ActionTypes(IntEnum):
|
||||
ACCEPT_BID = auto()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"] = [
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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"] = [
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user