Files
basicswap/basicswap/ui/page_offers.py

1045 lines
38 KiB
Python

# -*- coding: utf-8 -*-
# Copyright (c) 2022-2024 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.
import traceback
import time
from urllib import parse
from .util import (
getCoinType,
get_data_entry,
get_data_entry_or,
have_data_entry,
inputAmount,
listAvailableCoins,
PAGE_LIMIT,
setCoinFilter,
set_pagination_filters,
)
from basicswap.db import (
Concepts,
)
from basicswap.util import (
ensure,
format_amount,
)
from basicswap.basicswap_util import (
SwapTypes,
DebugTypes,
getLockName,
get_api_key_setting,
strBidState,
strSwapDesc,
strSwapType,
TxLockTypes,
strOfferState,
)
from basicswap.chainparams import (
Coins,
ticker_map,
)
from basicswap.explorers import (
default_chart_api_key,
default_coingecko_api_key,
)
def value_or_none(v):
if v == -1 or v == "-1":
return None
return v
def decode_offer_id(v):
try:
offer_id = bytes.fromhex(v)
ensure(len(offer_id) == 28, "Bad offer ID")
return offer_id
except Exception:
raise ValueError("Bad offer ID")
def swap_type_from_string(str_swap_type: str) -> SwapTypes:
if str_swap_type == "seller_first" or str_swap_type == "secret_hash":
return SwapTypes.SELLER_FIRST
elif str_swap_type == "xmr_swap" or str_swap_type == "adaptor_sig":
return SwapTypes.XMR_SWAP
else:
raise ValueError("Unknown swap type")
def parseOfferFormData(swap_client, form_data, page_data, options={}):
errors = []
parsed_data = {}
if have_data_entry(form_data, "addr_to"):
page_data["addr_to"] = get_data_entry(form_data, "addr_to")
addr_to = value_or_none(page_data["addr_to"])
if addr_to is not None:
parsed_data["addr_to"] = addr_to
if have_data_entry(form_data, "addr_from"):
page_data["addr_from"] = get_data_entry(form_data, "addr_from")
parsed_data["addr_from"] = value_or_none(page_data["addr_from"])
else:
parsed_data["addr_from"] = None
try:
page_data["coin_from"] = getCoinType(get_data_entry(form_data, "coin_from"))
coin_from = Coins(page_data["coin_from"])
ci_from = swap_client.ci(coin_from)
if coin_from not in (Coins.XMR, Coins.WOW):
page_data["fee_from_conf"] = ci_from._conf_target # Set default value
parsed_data["coin_from"] = coin_from
except Exception:
errors.append("Unknown Coin From")
try:
page_data["coin_to"] = getCoinType(get_data_entry(form_data, "coin_to"))
coin_to = Coins(page_data["coin_to"])
ci_to = swap_client.ci(coin_to)
if coin_to not in (Coins.XMR, Coins.WOW):
page_data["fee_to_conf"] = ci_to._conf_target # Set default value
parsed_data["coin_to"] = coin_to
except Exception:
errors.append("Unknown Coin To")
if coin_from == coin_to:
errors.append("Coins from and to must be different.")
try:
page_data["amt_from"] = get_data_entry(form_data, "amt_from")
parsed_data["amt_from"] = inputAmount(page_data["amt_from"], ci_from)
except Exception:
errors.append("Amount From")
try:
if have_data_entry(form_data, "amt_bid_min") is False:
if options.get("add_min_bid_amt", False) is True:
parsed_data["amt_bid_min"] = ci_from.chainparams_network()["min_amount"]
else:
raise ValueError("missing")
else:
page_data["amt_bid_min"] = get_data_entry(form_data, "amt_bid_min")
parsed_data["amt_bid_min"] = inputAmount(page_data["amt_bid_min"], ci_from)
if (
parsed_data["amt_bid_min"] < 0
or parsed_data["amt_bid_min"] > parsed_data["amt_from"]
):
errors.append("Minimum Purchase Quantity out of range")
except Exception:
errors.append("Minimum Purchase Quantity")
if have_data_entry(form_data, "rate") and not have_data_entry(form_data, "amt_to"):
parsed_data["rate"] = ci_to.make_int(form_data["rate"], r=1)
page_data["rate"] = ci_to.format_amount(parsed_data["rate"])
else:
try:
page_data["amt_to"] = get_data_entry(form_data, "amt_to")
parsed_data["amt_to"] = inputAmount(page_data["amt_to"], ci_to)
except Exception:
errors.append("Amount To")
if "amt_to" in parsed_data and "amt_from" in parsed_data:
parsed_data["rate"] = ci_from.make_int(
parsed_data["amt_to"] / parsed_data["amt_from"], r=1
)
page_data["rate"] = ci_to.format_amount(parsed_data["rate"])
if (
"amt_to" not in parsed_data
and "rate" in parsed_data
and "amt_from" in parsed_data
):
parsed_data["amt_to"] = int(
(parsed_data["amt_from"] * parsed_data["rate"]) // ci_from.COIN()
)
if swap_client.debug:
page_data["amt_var"] = True if have_data_entry(form_data, "amt_var") else False
parsed_data["amt_var"] = page_data["amt_var"]
page_data["rate_var"] = (
True if have_data_entry(form_data, "rate_var") else False
)
parsed_data["rate_var"] = page_data["rate_var"]
else:
page_data["amt_var"] = True
page_data["rate_var"] = False
parsed_data["amt_var"] = True
parsed_data["rate_var"] = False
page_data["automation_strat_id"] = int(
get_data_entry_or(form_data, "automation_strat_id", -1)
)
parsed_data["automation_strat_id"] = page_data["automation_strat_id"]
swap_type = -1
if have_data_entry(form_data, "subfee"):
parsed_data["subfee"] = True
if have_data_entry(form_data, "swap_type"):
page_data["swap_type"] = get_data_entry(form_data, "swap_type")
parsed_data["swap_type"] = page_data["swap_type"]
swap_type = swap_type_from_string(parsed_data["swap_type"])
elif (
parsed_data["coin_from"] in swap_client.adaptor_swap_only_coins
or parsed_data["coin_to"] in swap_client.adaptor_swap_only_coins
):
parsed_data["swap_type"] = strSwapType(SwapTypes.XMR_SWAP)
swap_type = SwapTypes.XMR_SWAP
else:
parsed_data["swap_type"] = strSwapType(SwapTypes.SELLER_FIRST)
swap_type = SwapTypes.SELLER_FIRST
if swap_type == SwapTypes.XMR_SWAP:
page_data["swap_style"] = "xmr"
else:
page_data["swap_style"] = "atomic"
if "swap_type" in parsed_data:
try:
swap_client.validateSwapType(coin_from, coin_to, swap_type)
except Exception as e:
errors.append(f"{e}")
if have_data_entry(form_data, "step1"):
if len(errors) == 0 and have_data_entry(form_data, "continue"):
page_data["step2"] = True
return parsed_data, errors
page_data["step2"] = True
if have_data_entry(form_data, "fee_from_conf"):
page_data["fee_from_conf"] = int(get_data_entry(form_data, "fee_from_conf"))
parsed_data["fee_from_conf"] = page_data["fee_from_conf"]
if have_data_entry(form_data, "fee_from_extra"):
page_data["fee_from_extra"] = int(get_data_entry(form_data, "fee_from_extra"))
parsed_data["fee_from_extra"] = page_data["fee_from_extra"]
if have_data_entry(form_data, "fee_to_conf"):
page_data["fee_to_conf"] = int(get_data_entry(form_data, "fee_to_conf"))
parsed_data["fee_to_conf"] = page_data["fee_to_conf"]
if have_data_entry(form_data, "fee_to_extra"):
page_data["fee_to_extra"] = int(get_data_entry(form_data, "fee_to_extra"))
parsed_data["fee_to_extra"] = page_data["fee_to_extra"]
if have_data_entry(form_data, "check_offer"):
page_data["check_offer"] = True
if have_data_entry(form_data, "submit_offer"):
page_data["submit_offer"] = True
if have_data_entry(form_data, "lockhrs"):
page_data["lockhrs"] = int(get_data_entry(form_data, "lockhrs"))
parsed_data["lock_seconds"] = page_data["lockhrs"] * 60 * 60
elif have_data_entry(form_data, "lockmins"):
page_data["lockmins"] = int(get_data_entry(form_data, "lockmins"))
parsed_data["lock_seconds"] = page_data["lockmins"] * 60
elif have_data_entry(form_data, "lockseconds"):
parsed_data["lock_seconds"] = int(get_data_entry(form_data, "lockseconds"))
if have_data_entry(form_data, "validhrs"):
page_data["validhrs"] = int(get_data_entry(form_data, "validhrs"))
parsed_data["valid_for_seconds"] = page_data["validhrs"] * 60 * 60
elif have_data_entry(form_data, "valid_for_seconds"):
parsed_data["valid_for_seconds"] = int(
get_data_entry(form_data, "valid_for_seconds")
)
try:
if len(errors) == 0 and page_data["swap_style"] == "xmr":
reverse_bid: bool = swap_client.is_reverse_ads_bid(coin_from, coin_to)
if have_data_entry(form_data, "fee_rate_from"):
page_data["from_fee_override"] = get_data_entry(
form_data, "fee_rate_from"
)
parsed_data["from_fee_override"] = page_data["from_fee_override"]
else:
from_fee_override, page_data["from_fee_src"] = (
swap_client.getFeeRateForCoin(
parsed_data["coin_from"], page_data["fee_from_conf"]
)
)
if page_data["fee_from_extra"] > 0:
from_fee_override += from_fee_override * (
float(page_data["fee_from_extra"]) / 100.0
)
page_data["from_fee_override"] = ci_from.format_amount(
ci_from.make_int(from_fee_override, r=1)
)
parsed_data["from_fee_override"] = page_data["from_fee_override"]
lock_spend_tx_vsize = (
ci_from.xmr_swap_b_lock_spend_tx_vsize()
if reverse_bid
else ci_from.xmr_swap_a_lock_spend_tx_vsize()
)
lock_spend_tx_fee = ci_from.make_int(
ci_from.make_int(from_fee_override, r=1)
* lock_spend_tx_vsize
/ 1000,
r=1,
)
page_data["amt_from_lock_spend_tx_fee"] = ci_from.format_amount(
lock_spend_tx_fee // ci_from.COIN()
)
page_data["tla_from"] = ci_from.ticker()
if ci_to in (Coins.XMR, Coins.WOW):
if have_data_entry(form_data, "fee_rate_to"):
page_data["to_fee_override"] = get_data_entry(
form_data, "fee_rate_to"
)
parsed_data["to_fee_override"] = page_data["to_fee_override"]
else:
to_fee_override, page_data["to_fee_src"] = (
swap_client.getFeeRateForCoin(
parsed_data["coin_to"], page_data["fee_to_conf"]
)
)
if page_data["fee_to_extra"] > 0:
to_fee_override += to_fee_override * (
float(page_data["fee_to_extra"]) / 100.0
)
page_data["to_fee_override"] = ci_to.format_amount(
ci_to.make_int(to_fee_override, r=1)
)
parsed_data["to_fee_override"] = page_data["to_fee_override"]
except Exception as e:
# Error is expected if missing fields
if swap_client.debug is True:
swap_client.log.warning(f"parseOfferFormData failed to set fee: Error {e}")
return parsed_data, errors
def postNewOfferFromParsed(swap_client, parsed_data):
swap_type = SwapTypes.SELLER_FIRST
if "swap_type" in parsed_data:
str_swap_type = parsed_data["swap_type"].lower()
swap_type = swap_type_from_string(str_swap_type)
elif (
parsed_data["coin_from"] in swap_client.scriptless_coins
or parsed_data["coin_to"] in swap_client.scriptless_coins
):
swap_type = SwapTypes.XMR_SWAP
if swap_type == SwapTypes.XMR_SWAP:
# All coins capable of segwit should be capable of csv
lock_type = TxLockTypes.SEQUENCE_LOCK_TIME
else:
if (
swap_client.coin_clients[parsed_data["coin_from"]]["use_csv"]
and swap_client.coin_clients[parsed_data["coin_to"]]["use_csv"]
):
lock_type = TxLockTypes.SEQUENCE_LOCK_TIME
else:
lock_type = TxLockTypes.ABS_LOCK_TIME
extra_options = {}
if "fee_from_conf" in parsed_data:
extra_options["from_fee_conf_target"] = parsed_data["fee_from_conf"]
if "from_fee_multiplier_percent" in parsed_data:
extra_options["from_fee_multiplier_percent"] = parsed_data["fee_from_extra"]
if "from_fee_override" in parsed_data:
extra_options["from_fee_override"] = parsed_data["from_fee_override"]
if "fee_to_conf" in parsed_data:
extra_options["to_fee_conf_target"] = parsed_data["fee_to_conf"]
if "to_fee_multiplier_percent" in parsed_data:
extra_options["to_fee_multiplier_percent"] = parsed_data["fee_to_extra"]
if "to_fee_override" in parsed_data:
extra_options["to_fee_override"] = parsed_data["to_fee_override"]
if "valid_for_seconds" in parsed_data:
extra_options["valid_for_seconds"] = parsed_data["valid_for_seconds"]
if "addr_to" in parsed_data:
extra_options["addr_send_to"] = parsed_data["addr_to"]
if parsed_data.get("amt_var", False):
extra_options["amount_negotiable"] = parsed_data["amt_var"]
if parsed_data.get("rate_var", False):
extra_options["rate_negotiable"] = parsed_data["rate_var"]
if parsed_data.get("rate_var", None) is not None:
extra_options["rate_negotiable"] = parsed_data["rate_var"]
if parsed_data.get("automation_strat_id", None) is not None:
extra_options["automation_id"] = parsed_data["automation_strat_id"]
swap_value = parsed_data["amt_from"]
if parsed_data.get("amt_to", None) is not None:
extra_options["amount_to"] = parsed_data["amt_to"]
if parsed_data.get("subfee", False):
ci_from = swap_client.ci(parsed_data["coin_from"])
pi = swap_client.pi(swap_type)
itx = pi.getFundedInitiateTxTemplate(ci_from, swap_value, True)
itx_decoded = ci_from.describeTx(itx.hex())
n = pi.findMockVout(ci_from, itx_decoded)
swap_value = ci_from.make_int(itx_decoded["vout"][n]["value"])
extra_options = {"prefunded_itx": itx}
offer_id = swap_client.postOffer(
parsed_data["coin_from"],
parsed_data["coin_to"],
swap_value,
parsed_data["rate"],
parsed_data["amt_bid_min"],
swap_type,
lock_type=lock_type,
lock_value=parsed_data["lock_seconds"],
addr_send_from=parsed_data["addr_from"],
extra_options=extra_options,
)
return offer_id
def postNewOffer(swap_client, form_data):
page_data = {}
parsed_data, errors = parseOfferFormData(
swap_client, form_data, page_data, options={"add_min_bid_amt": True}
)
if len(errors) > 0:
raise ValueError("Parse errors: " + " ".join(errors))
return postNewOfferFromParsed(swap_client, parsed_data)
def offer_to_post_string(self, swap_client, offer_id):
offer, xmr_offer = swap_client.getXmrOffer(offer_id)
ensure(offer, "Unknown offer ID")
ci_from = swap_client.ci(offer.coin_from)
ci_to = swap_client.ci(offer.coin_to)
amount_to: int = offer.amount_to
if amount_to is None:
amount_to = (offer.amount_from * offer.rate) // ci_from.COIN()
offer_data = {
"formid": self.generate_form_id(),
"addr_to": offer.addr_to,
"addr_from": offer.addr_from,
"coin_from": offer.coin_from,
"coin_to": offer.coin_to,
# TODO store fee conf, or pass directly
# 'fee_from_conf'
# 'fee_to_conf'
"amt_from": ci_from.format_amount(offer.amount_from),
"amt_bid_min": ci_from.format_amount(offer.min_bid_amount),
"rate": ci_to.format_amount(offer.rate),
"amt_to": ci_to.format_amount(amount_to),
"validhrs": offer.time_valid // (60 * 60),
"swap_type": strSwapType(offer.swap_type),
}
if offer.amount_negotiable:
offer_data["amt_var"] = True
if offer.rate_negotiable:
offer_data["rate_var"] = True
if (
offer.lock_type == TxLockTypes.SEQUENCE_LOCK_TIME
or offer.lock_type == TxLockTypes.ABS_LOCK_TIME
):
if offer.lock_value > 60 * 60:
offer_data["lockhrs"] = offer.lock_value // (60 * 60)
else:
offer_data["lockhrs"] = offer.lock_value // 60
try:
strategy = swap_client.getLinkedStrategy(Concepts.OFFER, offer.offer_id)
offer_data["automation_strat_id"] = strategy[0]
except Exception:
pass # None found
return parse.urlencode(offer_data).encode()
def page_newoffer(self, url_split, post_string):
server = self.server
swap_client = self.server.swap_client
swap_client.checkSystemStatus()
summary = swap_client.getSummary()
messages = []
err_messages = []
page_data = {
# Set defaults
"addr_to": -1,
"fee_from_conf": 2,
"fee_to_conf": 2,
"validhrs": 1,
"lockhrs": 32,
"lockmins": 30, # used in debug mode
"debug_ui": swap_client.debug_ui,
"automation_strat_id": -1,
"amt_bid_min": format_amount(1, 3),
"swap_type": strSwapType(SwapTypes.SELLER_FIRST),
}
post_data = parse.parse_qs(post_string)
if "offer_from" in post_data:
offer_from_id_hex = post_data["offer_from"][0]
offer_from_id = decode_offer_id(offer_from_id_hex)
post_string = offer_to_post_string(self, swap_client, offer_from_id)
form_data = self.checkForm(post_string, "newoffer", err_messages)
if form_data:
try:
parsed_data, errors = parseOfferFormData(swap_client, form_data, page_data)
for e in errors:
err_messages.append(str(e))
except Exception as e:
if swap_client.debug is True:
swap_client.log.error(traceback.format_exc())
err_messages.append(str(e))
if len(err_messages) == 0 and "submit_offer" in page_data:
try:
offer_id = postNewOfferFromParsed(swap_client, parsed_data)
messages.append(
'<a href="/offer/'
+ offer_id.hex()
+ '">Sent Offer {}</a>'.format(offer_id.hex())
)
page_data = {}
except Exception as e:
if swap_client.debug is True:
swap_client.log.error(traceback.format_exc())
err_messages.append(str(e))
if len(err_messages) == 0 and "check_offer" in page_data:
template = server.env.get_template("offer_confirm.html")
elif "step2" in page_data:
template = server.env.get_template("offer_new_2.html")
else:
template = server.env.get_template("offer_new_1.html")
if swap_client.debug_ui:
messages.append("Debug mode active.")
coins_from, coins_to = listAvailableCoins(swap_client, split_from=True)
addrs_from_raw = swap_client.listSMSGAddresses("offer_send_from")
addrs_to_raw = swap_client.listSMSGAddresses("offer_send_to")
all_addresses = swap_client.listAllSMSGAddresses({})
addr_notes = {addr["addr"]: addr["note"] for addr in all_addresses}
addrs_from = [(addr[0], addr_notes.get(addr[0], "")) for addr in addrs_from_raw]
addrs_to = [(addr[0], addr_notes.get(addr[0], "")) for addr in addrs_to_raw]
automation_filters = {"type_ind": Concepts.OFFER, "sort_by": "label"}
automation_strategies = swap_client.listAutomationStrategies(automation_filters)
chart_api_key = swap_client.settings.get("chart_api_key", "")
if chart_api_key == "":
chart_api_key_enc = swap_client.settings.get("chart_api_key_enc", "")
chart_api_key = (
default_chart_api_key
if chart_api_key_enc == ""
else bytes.fromhex(chart_api_key_enc).decode("utf-8")
)
coingecko_api_key = swap_client.settings.get("coingecko_api_key", "")
if coingecko_api_key == "":
coingecko_api_key_enc = swap_client.settings.get("coingecko_api_key_enc", "")
coingecko_api_key = (
default_coingecko_api_key
if coingecko_api_key_enc == ""
else bytes.fromhex(coingecko_api_key_enc).decode("utf-8")
)
return self.render_template(
template,
{
"messages": messages,
"err_messages": err_messages,
"coins_from": coins_from,
"coins": coins_to,
"addrs": addrs_from,
"addrs_to": addrs_to,
"data": page_data,
"automation_strategies": automation_strategies,
"summary": summary,
"swap_types": [
(strSwapType(x), strSwapDesc(x)) for x in SwapTypes if strSwapType(x)
],
"show_chart": swap_client.settings.get("show_chart", True),
"chart_api_key": chart_api_key,
"coingecko_api_key": coingecko_api_key,
},
)
def page_offer(self, url_split, post_string):
ensure(len(url_split) > 2, "Offer ID not specified")
offer_id = decode_offer_id(url_split[2])
server = self.server
swap_client = server.swap_client
swap_client.checkSystemStatus()
summary = swap_client.getSummary()
offer, xmr_offer = swap_client.getXmrOffer(offer_id)
ensure(offer, "Unknown offer ID")
extend_data = {
"nb_validmins": 10,
}
messages = []
err_messages = []
if swap_client.debug_ui:
messages.append("Debug mode active.")
sent_bid_id = None
show_bid_form = None
show_edit_form = None
form_data = self.checkForm(post_string, "offer", err_messages)
ci_from = swap_client.ci(Coins(offer.coin_from))
ci_to = swap_client.ci(Coins(offer.coin_to))
reverse_bid: bool = True if offer.bid_reversed else False
debugind = -1
bid_amount = ci_from.format_amount(offer.amount_from)
bid_rate = ci_to.format_amount(offer.rate)
if form_data:
if b"archive_offer" in form_data:
try:
swap_client.archiveOffer(offer_id)
messages.append("Offer archived")
except Exception as ex:
err_messages.append("Archive offer failed: " + str(ex))
if b"revoke_offer" in form_data:
try:
swap_client.revokeOffer(offer_id)
messages.append("Offer revoked")
except Exception as ex:
err_messages.append("Revoke offer failed: " + str(ex))
elif b"repeat_offer" in form_data:
self.send_response(302)
self.send_header("Location", "/newoffer?offer_from=" + offer_id.hex())
self.end_headers()
return bytes()
elif b"edit_offer" in form_data:
show_edit_form = True
automation_filters = {"type_ind": Concepts.OFFER, "sort_by": "label"}
extend_data["automation_strategies"] = swap_client.listAutomationStrategies(
automation_filters
)
elif b"edit_offer_submit" in form_data:
change_data = {}
change_data["automation_strat_id"] = int(
get_data_entry_or(form_data, "automation_strat_id", -1)
)
swap_client.editOffer(offer_id, change_data)
elif b"newbid" in form_data:
show_bid_form = True
elif (
b"sendbid" in form_data
and b"confirm" in form_data
and b"formid" in form_data
):
try:
addr_from = form_data[b"addr_from"][0].decode("utf-8")
extend_data["nb_addr_from"] = addr_from
if addr_from == "-1":
addr_from = None
minutes_valid = int(form_data[b"validmins"][0].decode("utf-8"))
extend_data["nb_validmins"] = minutes_valid
extra_options = {
"valid_for_seconds": minutes_valid * 60,
}
if have_data_entry(form_data, "bid_rate"):
bid_rate = get_data_entry(form_data, "bid_rate")
extra_options["bid_rate"] = ci_to.make_int(bid_rate, r=1)
if have_data_entry(form_data, "bid_amount"):
bid_amount = get_data_entry(form_data, "bid_amount")
amount_from = inputAmount(bid_amount, ci_from)
else:
amount_from = offer.amount_from
debugind = int(get_data_entry_or(form_data, "debugind", -1))
sent_bid_id = swap_client.postBid(
offer_id,
amount_from,
addr_send_from=addr_from,
extra_options=extra_options,
).hex()
if debugind > -1:
swap_client.setBidDebugInd(bytes.fromhex(sent_bid_id), debugind)
self.send_response(302)
self.send_header("Location", f"/bid/{sent_bid_id}")
self.end_headers()
return bytes()
except Exception as ex:
if self.server.swap_client.debug is True:
self.server.swap_client.log.error(traceback.format_exc())
err_messages.append("Send bid failed: " + str(ex))
show_bid_form = True
elif b"sendbid" in form_data:
show_bid_form = True
amount_to: int = offer.amount_to
if amount_to is None:
amount_to = (offer.amount_from * offer.rate) // ci_from.COIN()
now: int = swap_client.getTime()
data = {
"tla_from": ci_from.ticker(),
"tla_to": ci_to.ticker(),
"state": strOfferState(offer.state),
"coin_from": ci_from.coin_name(),
"coin_to": ci_to.coin_name(),
"coin_from_ind": int(ci_from.coin_type()),
"coin_to_ind": int(ci_to.coin_type()),
"amt_from": ci_from.format_amount(offer.amount_from),
"amt_to": ci_to.format_amount(amount_to),
"amt_bid_min": ci_from.format_amount(offer.min_bid_amount),
"rate": ci_to.format_amount(offer.rate),
"lock_type": getLockName(offer.lock_type),
"lock_value": offer.lock_value,
"addr_from": offer.addr_from,
"addr_to": (
"Public" if offer.addr_to == swap_client.network_addr else offer.addr_to
),
"created_at": offer.created_at,
"expired_at": offer.expire_at,
"sent": offer.was_sent,
"was_revoked": True if offer.active_ind == 2 else False,
"show_bid_form": show_bid_form,
"show_edit_form": show_edit_form,
"amount_negotiable": offer.amount_negotiable,
"rate_negotiable": offer.rate_negotiable,
"bid_amount": bid_amount,
"bid_rate": bid_rate,
"debug_ui": swap_client.debug_ui,
"automation_strat_id": -1,
"is_expired": offer.expire_at <= now,
"active_ind": offer.active_ind,
"swap_type": strSwapDesc(offer.swap_type),
"reverse": reverse_bid,
"form_id": get_data_entry_or(form_data, "formid", "") if form_data else "",
"auto_accept_type": (
offer.auto_accept_type if hasattr(offer, "auto_accept_type") else 0
),
}
data.update(extend_data)
if (
offer.lock_type == TxLockTypes.SEQUENCE_LOCK_TIME
or offer.lock_type == TxLockTypes.ABS_LOCK_TIME
):
if offer.lock_value > 60 * 60:
data["lock_value_hr"] = " ({} hours)".format(offer.lock_value / (60 * 60))
else:
data["lock_value_hr"] = " ({} minutes)".format(offer.lock_value / 60)
addr_from_label, addr_to_label = swap_client.getAddressLabel(
[offer.addr_from, offer.addr_to]
)
if len(addr_from_label) > 0:
data["addr_from_label"] = "(" + addr_from_label + ")"
if len(addr_to_label) > 0:
data["addr_to_label"] = "(" + addr_to_label + ")"
if swap_client.debug_ui:
data["debug_ind"] = debugind
data["debug_options"] = [(int(t), t.name) for t in DebugTypes]
ci_leader = ci_to if reverse_bid else ci_from
if xmr_offer:
int_fee_rate_now, fee_source = ci_leader.get_fee_rate()
data["xmr_type"] = True
data["a_fee_rate"] = ci_leader.format_amount(xmr_offer.a_fee_rate)
data["a_fee_rate_verify"] = ci_leader.format_amount(
int_fee_rate_now, conv_int=True
)
data["a_fee_rate_verify_src"] = fee_source
data["a_fee_warn"] = xmr_offer.a_fee_rate < int_fee_rate_now
from_fee_rate = xmr_offer.b_fee_rate if reverse_bid else xmr_offer.a_fee_rate
lock_spend_tx_vsize = (
ci_from.xmr_swap_b_lock_spend_tx_vsize()
if reverse_bid
else ci_from.xmr_swap_a_lock_spend_tx_vsize()
)
lock_spend_tx_fee = ci_from.make_int(
from_fee_rate * lock_spend_tx_vsize / 1000, r=1
)
data["amt_from_lock_spend_tx_fee"] = ci_from.format_amount(
lock_spend_tx_fee // ci_from.COIN()
)
if offer.was_sent:
try:
strategy = swap_client.getLinkedStrategy(Concepts.OFFER, offer_id)
data["automation_strat_id"] = strategy[0]
data["automation_strat_label"] = strategy[1]
except Exception:
pass # None found
bids = swap_client.listBids(offer_id=offer_id)
formatted_bids = []
amt_swapped = 0
for b in bids:
amount_from = b[4]
rate = b[10]
amt_swapped += amount_from
formatted_bids.append(
(
b[2].hex(),
ci_from.format_amount(amount_from),
strBidState(b[5]),
ci_to.format_amount(rate),
b[11],
)
)
data["amt_swapped"] = ci_from.format_amount(amt_swapped)
template = server.env.get_template("offer.html")
return self.render_template(
template,
{
"offer_id": offer_id.hex(),
"sent_bid_id": sent_bid_id,
"messages": messages,
"err_messages": err_messages,
"data": data,
"bids": formatted_bids,
"addrs": (
None if show_bid_form is None else swap_client.listSMSGAddresses("bid")
),
"summary": summary,
},
)
def format_timestamp(timestamp, with_ago=True, is_expired=False):
current_time = int(time.time())
if is_expired:
time_diff = timestamp - current_time
if time_diff <= 0:
return "Expired"
else:
time_diff = current_time - timestamp
if time_diff <= 172800:
hours_ago = time_diff // 3600
minutes_ago = (time_diff % 3600) // 60
if hours_ago == 0:
if minutes_ago == 1:
return "1 min ago" if with_ago else "1 min"
else:
return f"{minutes_ago} mins ago" if with_ago else f"{minutes_ago} mins"
elif hours_ago == 1:
if minutes_ago == 0:
return "1h ago" if with_ago else "1h"
else:
return (
f"1h {minutes_ago}min ago" if with_ago else f"1h {minutes_ago}min"
)
else:
if minutes_ago == 0:
return f"{int(hours_ago)}h ago" if with_ago else f"{int(hours_ago)}h"
else:
return (
f"{int(hours_ago)}h {minutes_ago}min ago"
if with_ago
else f"{int(hours_ago)}h {minutes_ago}min"
)
else:
return time.strftime("%Y-%m-%d", time.localtime(timestamp))
def page_offers(self, url_split, post_string, sent=False):
server = self.server
swap_client = server.swap_client
swap_client.checkSystemStatus()
summary = swap_client.getSummary()
filters = {
"coin_from": -1,
"coin_to": -1,
"page_no": 1,
"limit": PAGE_LIMIT,
"sort_by": "created_at",
"sort_dir": "desc",
"sent_from": "any" if sent is False else "only",
"active": "any",
}
filter_prefix = "page_offers_sent" if sent else "page_offers"
messages = []
form_data = self.checkForm(post_string, "offers", messages)
if form_data:
if have_data_entry(form_data, "clearfilters"):
swap_client.clearFilters(filter_prefix)
else:
filters["coin_from"] = setCoinFilter(form_data, "coin_from")
filters["coin_to"] = setCoinFilter(form_data, "coin_to")
if have_data_entry(form_data, "sort_by"):
sort_by = get_data_entry(form_data, "sort_by")
ensure(sort_by in ["created_at", "rate"], "Invalid sort by")
filters["sort_by"] = sort_by
if have_data_entry(form_data, "sort_dir"):
sort_dir = get_data_entry(form_data, "sort_dir")
ensure(sort_dir in ["asc", "desc"], "Invalid sort dir")
filters["sort_dir"] = sort_dir
if have_data_entry(form_data, "sent_from"):
sent_from = get_data_entry(form_data, "sent_from")
ensure(sent_from in ["any", "only"], "Invalid sent filter")
filters["sent_from"] = sent_from
if have_data_entry(form_data, "active"):
active_filter = get_data_entry(form_data, "active")
ensure(
active_filter
in ["any", "active", "expired", "revoked", "archived"],
"Invalid active filter",
)
filters["active"] = active_filter
set_pagination_filters(form_data, filters)
if have_data_entry(form_data, "applyfilters"):
swap_client.setFilters(filter_prefix, filters)
else:
saved_filters = swap_client.getFilters(filter_prefix)
if saved_filters:
filters.update(saved_filters)
if filters["sent_from"] == "only":
sent = True
else:
sent = False
offers = swap_client.listOffers(sent, filters)
now: int = swap_client.getTime()
formatted_offers = []
tla_from = ""
tla_to = ""
for row in offers:
o = row
ci_from = swap_client.ci(Coins(o.coin_from))
ci_to = swap_client.ci(Coins(o.coin_to))
is_expired = o.expire_at <= now
amount_negotiable = "Yes" if o.amount_negotiable else "No"
formatted_created_at = format_timestamp(o.created_at, with_ago=True)
formatted_expired_at = format_timestamp(
o.expire_at, with_ago=False, is_expired=True
)
tla_from = ci_from.ticker()
tla_to = ci_to.ticker()
amount_to: int = o.amount_to
if amount_to is None:
amount_to = (o.amount_from * o.rate) // ci_from.COIN()
formatted_offers.append(
(
formatted_created_at,
o.offer_id.hex(),
ci_from.coin_name(),
ci_to.coin_name(),
ci_from.format_amount(o.amount_from),
ci_to.format_amount(amount_to),
ci_to.format_amount(o.rate),
"Public" if o.addr_to == swap_client.network_addr else o.addr_to,
o.addr_from,
o.was_sent,
None, # placeholder, was completed_amount
is_expired,
o.active_ind,
formatted_expired_at,
strSwapDesc(o.swap_type),
amount_negotiable,
tla_from,
tla_to,
)
)
coins_from, coins_to = listAvailableCoins(swap_client, split_from=True)
chart_api_key = get_api_key_setting(
swap_client.settings, "chart_api_key", default_chart_api_key
)
coingecko_api_key = get_api_key_setting(
swap_client.settings, "coingecko_api_key", default_coingecko_api_key
)
offers_count = len(formatted_offers)
enabled_chart_coins = []
enabled_chart_coins_setting = swap_client.settings.get("enabled_chart_coins", "")
if enabled_chart_coins_setting.lower() == "all":
for coin_ticker in ticker_map:
enabled_chart_coins.append(coin_ticker.upper())
elif enabled_chart_coins_setting.strip() == "":
for coin_id in swap_client.coin_clients:
if not swap_client.isCoinActive(coin_id):
continue
try:
enabled_ticker = swap_client.ci(coin_id).ticker_mainnet()
except Exception:
continue
if (
enabled_ticker not in enabled_chart_coins
and enabled_ticker.lower() in ticker_map
):
enabled_chart_coins.append(enabled_ticker)
else:
for ticker in enabled_chart_coins_setting.split(","):
upcased_ticker = ticker.strip().upper()
if (
upcased_ticker not in enabled_chart_coins
and upcased_ticker.lower() in ticker_map
):
enabled_chart_coins.append(upcased_ticker)
template = server.env.get_template("offers.html")
return self.render_template(
template,
{
"page_type": "Your Offers" if sent else "Network Order Book",
"page_button": "hidden" if sent or offers_count <= 30 else "",
"page_type_description": (
"Your entire offer history."
if sent
else "Consult available offers in the order book and initiate a coin swap."
),
"show_chart": (
False if sent else swap_client.settings.get("show_chart", True)
),
"chart_api_key": chart_api_key,
"coingecko_api_key": coingecko_api_key,
"coins_from": coins_from,
"coins": coins_to,
"messages": messages,
"filters": filters,
"offers": formatted_offers,
"summary": summary,
"sent_offers": sent,
"offers_count": offers_count,
"tla_from": tla_from,
"tla_to": tla_to,
"enabled_chart_coins": enabled_chart_coins,
},
)