From b6709d0cdcc57aee5fc6eddb5f13fec0d37908cb Mon Sep 17 00:00:00 2001 From: tecnovert Date: Wed, 22 Jan 2025 00:47:50 +0200 Subject: [PATCH 1/9] prepare: Update BTC fastsync file. Allow specifying a custom URL to look for the snapshot signature with: BITCOIN_FASTSYNC_SIG_URL. Reduce gnupg module logging level. --- basicswap/bin/prepare.py | 32 +++++++++++++------ ...xo-snapshot-bitcoin-mainnet-867690.tar.asc | 16 ++++++++++ 2 files changed, 39 insertions(+), 9 deletions(-) create mode 100644 pgp/sigs/utxo-snapshot-bitcoin-mainnet-867690.tar.asc diff --git a/basicswap/bin/prepare.py b/basicswap/bin/prepare.py index bef1bfd..c82aa01 100755 --- a/basicswap/bin/prepare.py +++ b/basicswap/bin/prepare.py @@ -162,6 +162,7 @@ LOG_LEVEL = logging.DEBUG logger.level = LOG_LEVEL if not len(logger.handlers): logger.addHandler(logging.StreamHandler(sys.stdout)) +logging.getLogger("gnupg").setLevel(logging.INFO) BSX_DOCKER_MODE = toBool(os.getenv("BSX_DOCKER_MODE", "false")) BSX_LOCAL_TOR = toBool(os.getenv("BSX_LOCAL_TOR", "false")) @@ -288,10 +289,14 @@ TEST_ONION_LINK = toBool(os.getenv("TEST_ONION_LINK", "false")) BITCOIN_FASTSYNC_URL = os.getenv( "BITCOIN_FASTSYNC_URL", - "https://eu2.contabostorage.com/1f50a74c9dc14888a8664415dad3d020:utxosets/", + "https://snapshots.btcpay.tech/", ) BITCOIN_FASTSYNC_FILE = os.getenv( - "BITCOIN_FASTSYNC_FILE", "utxo-snapshot-bitcoin-mainnet-820852.tar" + "BITCOIN_FASTSYNC_FILE", "utxo-snapshot-bitcoin-mainnet-867690.tar" +) +BITCOIN_FASTSYNC_SIG_URL = os.getenv( + "BITCOIN_FASTSYNC_SIG_URL", + None, ) # Encrypt new wallets with this password, must match the Particl wallet password when adding coins @@ -539,6 +544,8 @@ def ensureValidSignatureBy(result, signing_key_name): if result.key_id not in expected_key_ids[signing_key_name]: raise ValueError("Signature made by unexpected keyid: " + result.key_id) + logger.debug(f"Found valid signature by {signing_key_name} ({result.key_id}).") + def extractCore(coin, version_data, settings, bin_dir, release_path, extra_opts={}): version, version_tag, signers = version_data @@ -1416,7 +1423,7 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}): # Double check if extra_opts.get("check_btc_fastsync", True): - check_btc_fastsync_data(base_dir, sync_file_path) + check_btc_fastsync_data(base_dir, BITCOIN_FASTSYNC_FILE) with tarfile.open(sync_file_path) as ft: ft.extractall(path=data_dir) @@ -1961,25 +1968,32 @@ def load_config(config_path): def signal_handler(sig, frame): - logger.info("Signal %d detected" % (sig)) + logger.info(f"Signal {sig} detected") -def check_btc_fastsync_data(base_dir, sync_file_path): +def check_btc_fastsync_data(base_dir, sync_filename): + logger.info("Validating signature for: " + sync_filename) + github_pgp_url = "https://raw.githubusercontent.com/basicswap/basicswap/master/pgp" gitlab_pgp_url = "https://gitlab.com/particl/basicswap/-/raw/master/pgp" - asc_filename = BITCOIN_FASTSYNC_FILE + ".asc" + asc_filename = sync_filename + ".asc" asc_file_path = os.path.join(base_dir, asc_filename) + sync_file_path = os.path.join(base_dir, sync_filename) if not os.path.exists(asc_file_path): - asc_file_urls = ( + asc_file_urls = [ github_pgp_url + "/sigs/" + asc_filename, gitlab_pgp_url + "/sigs/" + asc_filename, - ) + ] + if BITCOIN_FASTSYNC_SIG_URL: + asc_file_urls.append("/".join([BITCOIN_FASTSYNC_SIG_URL, asc_filename])) for url in asc_file_urls: try: downloadFile(url, asc_file_path) break except Exception as e: logging.warning("Download failed: %s", str(e)) + if not os.path.exists(asc_file_path): + raise ValueError("Unable to find snapshot signature file.") gpg = gnupg.GPG() pubkey_filename = "{}_{}.pgp".format("particl", "tecnovert") pubkeyurls = [ @@ -2251,7 +2265,7 @@ def main(): check_sig = True if check_sig: - check_btc_fastsync_data(data_dir, sync_file_path) + check_btc_fastsync_data(data_dir, BITCOIN_FASTSYNC_FILE) except Exception as e: logger.error( f"Failed to download BTC fastsync file: {e}\nRe-running the command should resume the download or try manually downloading from {sync_file_url}" diff --git a/pgp/sigs/utxo-snapshot-bitcoin-mainnet-867690.tar.asc b/pgp/sigs/utxo-snapshot-bitcoin-mainnet-867690.tar.asc new file mode 100644 index 0000000..0198210 --- /dev/null +++ b/pgp/sigs/utxo-snapshot-bitcoin-mainnet-867690.tar.asc @@ -0,0 +1,16 @@ +-----BEGIN PGP SIGNATURE----- + +iQIzBAABCAAdFiEEjlF9wS7BzDf2QjqKE/E2UcnPDWsFAmeP/40ACgkQE/E2UcnP +DWuraw/9HCuAZG+D6xSLWmjA9Dtj9OZMEOIxqvxw+1e2KQ5ek4d1waL63NWFQfMi +fDlKKeFbZoL6Dfjbx0GoUJKTfrIVKog6DlVzIi5PuUwPOCBFuLl0g5kHlC20jbPw +nu7T6fj6/oD/lqo0rzFDkbsX7Fk4GGC7rYLKfdtYhDgMq9ro7QhSxAOJanRyqzXL +dvPNxlyksOyttJLSAZI9BOkrpTWoyb3asOli5oHgdcheHd/2fjby69huS3UWEjdO +9Bm73UFlxF2hxCTc2Fqvvb3SBDmNCLlFM0f+DDJNMJGUQViVCar0YRw3R+/NBo83 +ptutp3bpabHijQFEEpIx/19nh9RQMJjaHHHqdPcTeg8bU/Yeq36TI7gsCenK0mQT +75MscvJAG0enoKVrTZez5ner9ZwLOevAKzRe4huRJZZjM8gM6sb2OKslJLqTxEVt +G3b8BLB9IUAxCeyuvGSG/3RV3MgZLnLy5MLYjh72+Kmo6HpuajJwPuvUck5ZYcGE +jjeRFZmqZj0FtCrcfStau/0liyAxU5k/43RwMvujO1uTTgOVHw1QhhMEkZ9bYhhO +JgeCEkwL1Bjjved1NSySjZbt2sFbG89as14ezHxgc4HaujJ6bGkINnkPOPWM1tk4 +DjjEO/0PY9i0m/ivQUXf5ZPSnlkAR8x6Ve2S2MvQd7nFoS/YfLs= +=0pTn +-----END PGP SIGNATURE----- From c79ed493aa189f173143a891bd0bc04909d33b25 Mon Sep 17 00:00:00 2001 From: tecnovert Date: Wed, 22 Jan 2025 18:07:20 +0200 Subject: [PATCH 2/9] Add estimated tx fee to amount check when posting bid. Add more log messages around balance checks. --- .github/workflows/ci.yml | 6 +- basicswap/basicswap.py | 114 ++++++++------ basicswap/bin/run.py | 4 +- basicswap/interface/btc.py | 4 + basicswap/interface/dcr/dcr.py | 4 + basicswap/interface/doge.py | 4 + basicswap/interface/part.py | 12 ++ basicswap/interface/xmr.py | 5 + tests/basicswap/test_btc_xmr.py | 258 ++++++++++++++++---------------- 9 files changed, 235 insertions(+), 176 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb93fa7..66de431 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,9 +56,9 @@ jobs: - name: Running test_xmr run: | export PYTHONPATH=$(pwd) - export PARTICL_BINDIR="$BIN_DIR/particl"; - export BITCOIN_BINDIR="$BIN_DIR/bitcoin"; - export XMR_BINDIR="$BIN_DIR/monero"; + export PARTICL_BINDIR="$BIN_DIR/particl" + export BITCOIN_BINDIR="$BIN_DIR/bitcoin" + export XMR_BINDIR="$BIN_DIR/monero" pytest tests/basicswap/test_btc_xmr.py::TestBTC -k "test_003_api or test_02_a_leader_recover_a_lock_tx" - name: Running test_encrypted_xmr_reload run: | diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 7f67079..8685173 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -1974,6 +1974,32 @@ class BasicSwap(BaseApp): if not offer.rate_negotiable: ensure(offer.rate == bid_rate, "Bid rate must match offer rate.") + def ensureWalletCanSend( + self, ci, swap_type, ensure_balance: int, estimated_fee: int, for_offer=True + ) -> None: + balance_msg: str = ( + f"{ci.format_amount(ensure_balance)} {ci.coin_name()} with estimated fee {ci.format_amount(estimated_fee)}" + ) + self.log.debug(f"Ensuring wallet can send {balance_msg}.") + try: + if ci.interface_type() in self.scriptless_coins: + ci.ensureFunds(ensure_balance + estimated_fee) + else: + pi = self.pi(swap_type) + _ = pi.getFundedInitiateTxTemplate(ci, ensure_balance, False) + # TODO: Save the prefunded tx so the fee can't change, complicates multiple offers at the same time. + except Exception as e: + type_str = "offer" if for_offer else "bid" + err_msg = f"Insufficient funds for {type_str} of {balance_msg}." + if self.debug: + self.log.error(f"ensureWalletCanSend failed {e}") + current_balance: int = ci.getSpendableBalance() + err_msg += ( + f" Debug: Spendable balance: {ci.format_amount(current_balance)}." + ) + self.log.error(err_msg) + raise ValueError(err_msg) + def getOfferAddressTo(self, extra_options) -> str: if "addr_send_to" in extra_options: return extra_options["addr_send_to"] @@ -2007,25 +2033,25 @@ class BasicSwap(BaseApp): except Exception: raise ValueError("Unknown coin to type") - valid_for_seconds: int = extra_options.get("valid_for_seconds", 60 * 60) - amount_to: int = extra_options.get( - "amount_to", int((amount * rate) // ci_from.COIN()) - ) - - # Recalculate the rate so it will match the bid rate - rate = ci_from.make_int(amount_to / amount, r=1) - self.validateSwapType(coin_from_t, coin_to_t, swap_type) - self.validateOfferAmounts( - coin_from_t, coin_to_t, amount, amount_to, min_bid_amount - ) self.validateOfferLockValue( swap_type, coin_from_t, coin_to_t, lock_type, lock_value ) + + valid_for_seconds: int = extra_options.get("valid_for_seconds", 60 * 60) self.validateOfferValidTime( swap_type, coin_from_t, coin_to_t, valid_for_seconds ) + amount_to: int = extra_options.get( + "amount_to", int((amount * rate) // ci_from.COIN()) + ) + self.validateOfferAmounts( + coin_from_t, coin_to_t, amount, amount_to, min_bid_amount + ) + # Recalculate the rate so it will match the bid rate + rate: int = ci_from.make_int(amount_to / amount, r=1) + offer_addr_to = self.getOfferAddressTo(extra_options) reverse_bid: bool = self.is_reverse_ads_bid(coin_from, coin_to) @@ -2066,8 +2092,8 @@ class BasicSwap(BaseApp): ) if "from_fee_override" in extra_options: - msg_buf.fee_rate_from = make_int( - extra_options["from_fee_override"], self.ci(coin_from).exp() + msg_buf.fee_rate_from = ci_from.make_int( + extra_options["from_fee_override"] ) else: # TODO: conf_target = ci_from.settings.get('conf_target', 2) @@ -2077,12 +2103,10 @@ class BasicSwap(BaseApp): fee_rate, fee_src = self.getFeeRateForCoin(coin_from, conf_target) if "from_fee_multiplier_percent" in extra_options: fee_rate *= extra_options["fee_multiplier"] / 100.0 - msg_buf.fee_rate_from = make_int(fee_rate, self.ci(coin_from).exp()) + msg_buf.fee_rate_from = ci_from.make_int(fee_rate) if "to_fee_override" in extra_options: - msg_buf.fee_rate_to = make_int( - extra_options["to_fee_override"], self.ci(coin_to).exp() - ) + msg_buf.fee_rate_to = ci_to.make_int(extra_options["to_fee_override"]) else: # TODO: conf_target = ci_to.settings.get('conf_target', 2) conf_target = 2 @@ -2091,7 +2115,7 @@ class BasicSwap(BaseApp): fee_rate, fee_src = self.getFeeRateForCoin(coin_to, conf_target) if "to_fee_multiplier_percent" in extra_options: fee_rate *= extra_options["fee_multiplier"] / 100.0 - msg_buf.fee_rate_to = make_int(fee_rate, self.ci(coin_to).exp()) + msg_buf.fee_rate_to = ci_to.make_int(fee_rate) if swap_type == SwapTypes.XMR_SWAP: xmr_offer = XmrOffer() @@ -2116,27 +2140,19 @@ class BasicSwap(BaseApp): msg_buf.fee_rate_to ) # Unused: TODO - Set priority? - ensure_balance: int = int(amount) - if coin_from in self.scriptless_coins: + # If a prefunded txn is not used, check that the wallet balance can cover the tx fee. + if "prefunded_itx" not in extra_options: # TODO: Better tx size estimate, xmr_swap_b_lock_tx_vsize could be larger than xmr_swap_b_lock_spend_tx_vsize estimated_fee: int = ( - msg_buf.fee_rate_from - * ci_from.xmr_swap_b_lock_spend_tx_vsize() - / 1000 + msg_buf.fee_rate_from * ci_from.est_lock_tx_vsize() // 1000 ) - ci_from.ensureFunds(msg_buf.amount_from + estimated_fee) - else: - # If a prefunded txn is not used, check that the wallet balance can cover the tx fee. - if "prefunded_itx" not in extra_options: - pi = self.pi(SwapTypes.XMR_SWAP) - _ = pi.getFundedInitiateTxTemplate(ci_from, ensure_balance, False) - # TODO: Save the prefunded tx so the fee can't change, complicates multiple offers at the same time. + self.ensureWalletCanSend(ci_from, swap_type, int(amount), estimated_fee) - # TODO: Send proof of funds with offer - # proof_of_funds_hash = getOfferProofOfFundsHash(msg_buf, offer_addr) - # proof_addr, proof_sig, proof_utxos = self.getProofOfFunds( - # coin_from_t, ensure_balance, proof_of_funds_hash - # ) + # TODO: Send proof of funds with offer + # proof_of_funds_hash = getOfferProofOfFundsHash(msg_buf, offer_addr) + # proof_addr, proof_sig, proof_utxos = self.getProofOfFunds( + # coin_from_t, ensure_balance, proof_of_funds_hash + # ) offer_bytes = msg_buf.to_bytes() payload_hex = str.format("{:02x}", MessageTypes.OFFER) + offer_bytes.hex() @@ -2174,6 +2190,8 @@ class BasicSwap(BaseApp): was_sent=True, bid_reversed=bid_reversed, security_token=security_token, + from_feerate=msg_buf.fee_rate_from, + to_feerate=msg_buf.fee_rate_to, ) offer.setState(OfferStates.OFFER_SENT) @@ -3525,14 +3543,12 @@ class BasicSwap(BaseApp): self.checkCoinsReady(coin_from, coin_to) - balance_to: int = ci_to.getSpendableBalance() - ensure( - balance_to > amount_to, - "{} spendable balance is too low: {} < {}".format( - ci_to.coin_name(), - ci_to.format_amount(balance_to), - ci_to.format_amount(amount_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) + 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 ) reverse_bid: bool = self.is_reverse_ads_bid(coin_from, coin_to) @@ -4069,6 +4085,18 @@ class BasicSwap(BaseApp): ci_from = self.ci(coin_from) ci_to = self.ci(coin_to) + # TODO: Better tx size estimate + fee_rate, fee_src = self.getFeeRateForCoin(coin_to, conf_target=2) + fee_rate_from = ci_to.make_int(fee_rate) + estimated_fee: int = fee_rate_from * ci_to.est_lock_tx_vsize() // 1000 + self.ensureWalletCanSend( + ci_to, + offer.swap_type, + offer.amount_from, + estimated_fee, + for_offer=False, + ) + if xmr_swap.contract_count is None: xmr_swap.contract_count = self.getNewContractId(use_cursor) diff --git a/basicswap/bin/run.py b/basicswap/bin/run.py index 3400941..7e15779 100755 --- a/basicswap/bin/run.py +++ b/basicswap/bin/run.py @@ -440,7 +440,9 @@ def runClient(fp, data_dir, chain, start_only_coins): swap_client.log.info(f"Starting {display_name} daemon") filename: str = getCoreBinName(coin_id, v, c + "d") - extra_opts = getCoreBinArgs(coin_id, v, use_tor_proxy=swap_client.use_tor_proxy) + extra_opts = getCoreBinArgs( + coin_id, v, use_tor_proxy=swap_client.use_tor_proxy + ) daemons.append( startDaemon(v["datadir"], v["bindir"], filename, opts=extra_opts) ) diff --git a/basicswap/interface/btc.py b/basicswap/interface/btc.py index 4b8bc8b..0caa59b 100644 --- a/basicswap/interface/btc.py +++ b/basicswap/interface/btc.py @@ -217,6 +217,10 @@ class BTCInterface(Secp256k1Interface): rv += output.nValue return rv + @staticmethod + def est_lock_tx_vsize() -> int: + return 110 + @staticmethod def xmr_swap_a_lock_spend_tx_vsize() -> int: return 147 diff --git a/basicswap/interface/dcr/dcr.py b/basicswap/interface/dcr/dcr.py index 614a73d..9bd6843 100644 --- a/basicswap/interface/dcr/dcr.py +++ b/basicswap/interface/dcr/dcr.py @@ -211,6 +211,10 @@ class DCRInterface(Secp256k1Interface): def txoType(): return CTxOut + @staticmethod + def est_lock_tx_vsize() -> int: + return 224 + @staticmethod def xmr_swap_a_lock_spend_tx_vsize() -> int: return 327 diff --git a/basicswap/interface/doge.py b/basicswap/interface/doge.py index caa5b49..4ef829d 100644 --- a/basicswap/interface/doge.py +++ b/basicswap/interface/doge.py @@ -24,6 +24,10 @@ class DOGEInterface(BTCInterface): def coin_type(): return Coins.DOGE + @staticmethod + def est_lock_tx_vsize() -> int: + return 192 + @staticmethod def xmr_swap_b_lock_spend_tx_vsize() -> int: return 192 diff --git a/basicswap/interface/part.py b/basicswap/interface/part.py index d7ef2b5..80db020 100644 --- a/basicswap/interface/part.py +++ b/basicswap/interface/part.py @@ -66,6 +66,10 @@ class PARTInterface(BTCInterface): def txVersion() -> int: return 0xA0 + @staticmethod + def est_lock_tx_vsize() -> int: + return 138 + @staticmethod def xmr_swap_a_lock_spend_tx_vsize() -> int: return 200 @@ -195,6 +199,10 @@ class PARTInterfaceBlind(PARTInterface): def balance_type(): return BalanceTypes.BLIND + @staticmethod + def est_lock_tx_vsize() -> int: + return 980 + @staticmethod def xmr_swap_a_lock_spend_tx_vsize() -> int: return 1032 @@ -1220,6 +1228,10 @@ class PARTInterfaceAnon(PARTInterface): def balance_type(): return BalanceTypes.ANON + @staticmethod + def est_lock_tx_vsize() -> int: + return 1153 + @staticmethod def xmr_swap_a_lock_spend_tx_vsize() -> int: raise ValueError("Not possible") diff --git a/basicswap/interface/xmr.py b/basicswap/interface/xmr.py index b947643..0ce3d5c 100644 --- a/basicswap/interface/xmr.py +++ b/basicswap/interface/xmr.py @@ -71,6 +71,11 @@ class XMRInterface(CoinInterface): def xmr_swap_a_lock_spend_tx_vsize() -> int: raise ValueError("Not possible") + @staticmethod + def est_lock_tx_vsize() -> int: + # TODO: Estimate with ringsize + return 1604 + @staticmethod def xmr_swap_b_lock_spend_tx_vsize() -> int: # TODO: Estimate with ringsize diff --git a/tests/basicswap/test_btc_xmr.py b/tests/basicswap/test_btc_xmr.py index 9deebc3..54d0145 100644 --- a/tests/basicswap/test_btc_xmr.py +++ b/tests/basicswap/test_btc_xmr.py @@ -10,9 +10,6 @@ import random import logging import unittest -from basicswap.db import ( - Concepts, -) from basicswap.basicswap import ( BidStates, Coins, @@ -23,6 +20,9 @@ from basicswap.basicswap_util import ( TxLockTypes, EventLogTypes, ) +from basicswap.db import ( + Concepts, +) from basicswap.util import ( make_int, ) @@ -32,10 +32,10 @@ from tests.basicswap.util import ( ) from tests.basicswap.common import ( abandon_all_swaps, + wait_for_balance, wait_for_bid, wait_for_event, wait_for_offer, - wait_for_balance, wait_for_unspent, wait_for_none_active, BTC_BASE_RPC_PORT, @@ -640,6 +640,129 @@ class TestFunctions(BaseTest): wait_for=(self.extra_wait_time + 180), ) + def do_test_08_insufficient_funds(self, coin_from, coin_to): + logging.info( + "---------- Test {} to {} Insufficient Funds".format( + coin_from.name, coin_to.name + ) + ) + swap_clients = self.swap_clients + reverse_bid: bool = swap_clients[0].is_reverse_ads_bid(coin_from, coin_to) + + id_offerer: int = self.node_c_id + id_bidder: int = self.node_b_id + + self.prepare_balance( + coin_from, + 10.0, + 1800 + id_offerer, + 1801 if coin_from in (Coins.XMR,) else 1800, + ) + jsw = read_json_api(1800 + id_offerer, "wallets") + balance_from_before: float = self.getBalance(jsw, coin_from) + self.prepare_balance( + coin_to, + balance_from_before + 1, + 1800 + id_bidder, + 1801 if coin_to in (Coins.XMR,) else 1800, + ) + + 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(balance_from_before, r=1) + rate_swap: int = ci_to.make_int(2.0, r=1) + + try: + offer_id = swap_clients[id_offerer].postOffer( + coin_from, + coin_to, + amt_swap, + rate_swap, + amt_swap, + SwapTypes.XMR_SWAP, + auto_accept_bids=True, + ) + except Exception as e: + assert "Insufficient funds" in str(e) + else: + assert False, "Should fail" + + # Test that postbid errors when offer is for the full balance + id_offerer_test_bid = id_bidder + id_bidder_test_bid = id_offerer + amt_swap_test_bid_to: int = ci_from.make_int(balance_from_before, r=1) + amt_swap_test_bid_from: int = ci_to.make_int(1.0) + offer_id = swap_clients[id_offerer_test_bid].postOffer( + coin_to, + coin_from, + amt_swap_test_bid_from, + 0, + amt_swap_test_bid_from, + SwapTypes.XMR_SWAP, + extra_options={"amount_to": amt_swap_test_bid_to}, + ) + wait_for_offer(test_delay_event, swap_clients[id_bidder_test_bid], offer_id) + try: + bid_id = swap_clients[id_bidder_test_bid].postBid( + offer_id, amt_swap_test_bid_from + ) + except Exception as e: + assert "Insufficient funds" in str(e) + else: + assert False, "Should fail" + + amt_swap -= ci_from.make_int(1) + offer_id = swap_clients[id_offerer].postOffer( + coin_from, + coin_to, + amt_swap, + rate_swap, + amt_swap, + SwapTypes.XMR_SWAP, + auto_accept_bids=True, + ) + wait_for_offer(test_delay_event, swap_clients[id_bidder], offer_id) + + # First bid should work + bid_id = swap_clients[id_bidder].postXmrBid(offer_id, amt_swap) + wait_for_bid( + test_delay_event, + swap_clients[id_offerer], + bid_id, + ( + (BidStates.SWAP_COMPLETED, BidStates.XMR_SWAP_NOSCRIPT_COIN_LOCKED) + if reverse_bid + else (BidStates.BID_ACCEPTED, BidStates.XMR_SWAP_SCRIPT_COIN_LOCKED) + ), + wait_for=120, + ) + + # Should be out of funds for second bid (over remaining offer value causes a hard auto accept fail) + bid_id = swap_clients[id_bidder].postXmrBid(offer_id, amt_swap) + wait_for_bid( + test_delay_event, + swap_clients[id_offerer], + bid_id, + BidStates.BID_AACCEPT_FAIL, + wait_for=40, + ) + event = wait_for_event( + test_delay_event, + swap_clients[id_offerer], + Concepts.BID, + bid_id, + event_type=EventLogTypes.AUTOMATION_CONSTRAINT, + ) + assert "Over remaining offer value" in event.event_msg + try: + swap_clients[id_offerer].acceptBid(bid_id) + except Exception as e: + assert "Insufficient funds" in str(e) or "Balance too low" in str(e) + else: + assert False, "Should fail" + class BasicSwapTest(TestFunctions): @@ -1714,133 +1837,10 @@ class BasicSwapTest(TestFunctions): swap_clients[0].setMockTimeOffset(0) def test_08_insufficient_funds(self): - tla_from = self.test_coin_from.name - logging.info("---------- Test {} Insufficient Funds".format(tla_from)) - swap_clients = self.swap_clients - coin_from = self.test_coin_from - coin_to = Coins.XMR - - self.prepare_balance(coin_from, 10.0, 1802, 1800) - - id_offerer: int = self.node_c_id - id_bidder: int = self.node_b_id - - swap_clients = self.swap_clients - ci_from = swap_clients[id_offerer].ci(coin_from) - ci_to = swap_clients[id_bidder].ci(coin_to) - - jsw = read_json_api(1800 + id_offerer, "wallets") - balance_from_before: float = self.getBalance(jsw, coin_from) - - amt_swap: int = ci_from.make_int(balance_from_before, r=1) - rate_swap: int = ci_to.make_int(2.0, r=1) - - try: - offer_id = swap_clients[id_offerer].postOffer( - coin_from, - coin_to, - amt_swap, - rate_swap, - amt_swap, - SwapTypes.XMR_SWAP, - auto_accept_bids=True, - ) - except Exception as e: - assert "Insufficient funds" in str(e) - else: - assert False, "Should fail" - amt_swap -= ci_from.make_int(1) - offer_id = swap_clients[id_offerer].postOffer( - coin_from, - coin_to, - amt_swap, - rate_swap, - amt_swap, - SwapTypes.XMR_SWAP, - auto_accept_bids=True, - ) - wait_for_offer(test_delay_event, swap_clients[id_bidder], offer_id) - - # First bid should work - bid_id = swap_clients[id_bidder].postXmrBid(offer_id, amt_swap) - wait_for_bid( - test_delay_event, - swap_clients[id_offerer], - bid_id, - (BidStates.BID_ACCEPTED, BidStates.XMR_SWAP_SCRIPT_COIN_LOCKED), - wait_for=40, - ) - - # Should be out of funds for second bid (over remaining offer value causes a hard auto accept fail) - bid_id = swap_clients[id_bidder].postXmrBid(offer_id, amt_swap) - wait_for_bid( - test_delay_event, - swap_clients[id_offerer], - bid_id, - BidStates.BID_AACCEPT_FAIL, - wait_for=40, - ) - try: - swap_clients[id_offerer].acceptBid(bid_id) - except Exception as e: - assert "Insufficient funds" in str(e) - else: - assert False, "Should fail" + self.do_test_08_insufficient_funds(self.test_coin_from, Coins.XMR) def test_08_insufficient_funds_rev(self): - tla_from = self.test_coin_from.name - logging.info("---------- Test {} Insufficient Funds (reverse)".format(tla_from)) - swap_clients = self.swap_clients - coin_from = Coins.XMR - coin_to = self.test_coin_from - - self.prepare_balance(coin_to, 10.0, 1802, 1800) - - id_offerer: int = self.node_b_id - id_bidder: int = self.node_c_id - - swap_clients = self.swap_clients - ci_from = swap_clients[id_offerer].ci(coin_from) - ci_to = swap_clients[id_bidder].ci(coin_to) - - jsw = read_json_api(1800 + id_bidder, "wallets") - balance_to_before: float = self.getBalance(jsw, coin_to) - - amt_swap: int = ci_from.make_int(balance_to_before, r=1) - rate_swap: int = ci_to.make_int(1.0, r=1) - - amt_swap -= 1 - offer_id = swap_clients[id_offerer].postOffer( - coin_from, - coin_to, - amt_swap, - rate_swap, - amt_swap, - SwapTypes.XMR_SWAP, - auto_accept_bids=True, - ) - wait_for_offer(test_delay_event, swap_clients[id_bidder], offer_id) - - bid_id = swap_clients[id_bidder].postXmrBid(offer_id, amt_swap) - - event = wait_for_event( - test_delay_event, - swap_clients[id_bidder], - Concepts.BID, - bid_id, - event_type=EventLogTypes.ERROR, - wait_for=60, - ) - assert "Insufficient funds" in event.event_msg - - wait_for_bid( - test_delay_event, - swap_clients[id_bidder], - bid_id, - BidStates.BID_ERROR, - sent=True, - wait_for=20, - ) + self.do_test_08_insufficient_funds(Coins.XMR, self.test_coin_from) class TestBTC(BasicSwapTest): From f13c481b51d61b36d4d699e90e2c3c4b3468ce34 Mon Sep 17 00:00:00 2001 From: tecnovert Date: Wed, 22 Jan 2025 20:08:46 +0200 Subject: [PATCH 3/9] tests: Fix test_xmr_persistent with BTC v28. --- basicswap/bin/prepare.py | 4 +++- basicswap/bin/run.py | 2 +- tests/basicswap/common_xmr.py | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/basicswap/bin/prepare.py b/basicswap/bin/prepare.py index c82aa01..5afef2a 100755 --- a/basicswap/bin/prepare.py +++ b/basicswap/bin/prepare.py @@ -211,6 +211,7 @@ LTC_RPC_PWD = os.getenv("LTC_RPC_PWD", "") BTC_RPC_HOST = os.getenv("BTC_RPC_HOST", "127.0.0.1") BTC_RPC_PORT = int(os.getenv("BTC_RPC_PORT", 19996)) +BTC_PORT = int(os.getenv("BTC_PORT", 8333)) BTC_ONION_PORT = int(os.getenv("BTC_ONION_PORT", 8334)) BTC_RPC_USER = os.getenv("BTC_RPC_USER", "") BTC_RPC_PWD = os.getenv("BTC_RPC_PWD", "") @@ -254,8 +255,8 @@ NAV_RPC_PWD = os.getenv("NAV_RPC_PWD", "") BCH_RPC_HOST = os.getenv("BCH_RPC_HOST", "127.0.0.1") BCH_RPC_PORT = int(os.getenv("BCH_RPC_PORT", 19997)) -BCH_ONION_PORT = int(os.getenv("BCH_ONION_PORT", 8335)) BCH_PORT = int(os.getenv("BCH_PORT", 19798)) +BCH_ONION_PORT = int(os.getenv("BCH_ONION_PORT", 8335)) BCH_RPC_USER = os.getenv("BCH_RPC_USER", "") BCH_RPC_PWD = os.getenv("BCH_RPC_PWD", "") @@ -2296,6 +2297,7 @@ def main(): "onionport": BTC_ONION_PORT + port_offset, "datadir": os.getenv("BTC_DATA_DIR", os.path.join(data_dir, "bitcoin")), "bindir": os.path.join(bin_dir, "bitcoin"), + "port": BTC_PORT + port_offset, "use_segwit": True, "blocks_confirmed": 1, "conf_target": 2, diff --git a/basicswap/bin/run.py b/basicswap/bin/run.py index 7e15779..5499140 100755 --- a/basicswap/bin/run.py +++ b/basicswap/bin/run.py @@ -251,7 +251,7 @@ def getCoreBinArgs(coin_id: int, coin_settings, prepare=False, use_tor_proxy=Fal extra_args = [] if "config_filename" in coin_settings: extra_args.append("--conf=" + coin_settings["config_filename"]) - if "port" in coin_settings: + if "port" in coin_settings and coin_id != Coins.BTC: if prepare is False and use_tor_proxy: if coin_id == Coins.BCH: # Without this BCH (27.1) will bind to the default BTC port, even with proxy set diff --git a/tests/basicswap/common_xmr.py b/tests/basicswap/common_xmr.py index d0f8426..68189a1 100644 --- a/tests/basicswap/common_xmr.py +++ b/tests/basicswap/common_xmr.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # Copyright (c) 2020-2024 tecnovert -# Copyright (c) 2024 The Basicswap developers +# Copyright (c) 2024-2025 The Basicswap developers # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -132,6 +132,7 @@ def run_prepare( os.environ["BSX_TEST_MODE"] = "true" os.environ["PART_RPC_PORT"] = str(PARTICL_RPC_PORT_BASE) os.environ["BTC_RPC_PORT"] = str(BITCOIN_RPC_PORT_BASE) + os.environ["BTC_PORT"] = str(BITCOIN_PORT_BASE) os.environ["LTC_RPC_PORT"] = str(LITECOIN_RPC_PORT_BASE) os.environ["DCR_RPC_PORT"] = str(DECRED_RPC_PORT_BASE) os.environ["FIRO_RPC_PORT"] = str(FIRO_RPC_PORT_BASE) @@ -229,8 +230,7 @@ def run_prepare( for line in lines: if not line.startswith("prune"): fp.write(line) - fp.write("port={}\n".format(BITCOIN_PORT_BASE + node_id + port_ofs)) - fp.write("bind=127.0.0.1\n") + # fp.write("bind=127.0.0.1\n") # Causes BTC v28 to try and bind to bind=127.0.0.1:8444, even with a bind...=onion present # listenonion=0 does not stop the node from trying to bind to the tor port # https://github.com/bitcoin/bitcoin/issues/22726 fp.write( From c76fe798482716821e73fe35f926f43f58b641ad Mon Sep 17 00:00:00 2001 From: tecnovert Date: Wed, 22 Jan 2025 20:13:18 +0200 Subject: [PATCH 4/9] scripts: Fix createoffers, identities api changed. --- scripts/createoffers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/createoffers.py b/scripts/createoffers.py index ac7bc33..07bc2ab 100755 --- a/scripts/createoffers.py +++ b/scripts/createoffers.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # Copyright (c) 2023-2024 tecnovert -# Copyright (c) 2024 The Basicswap developers +# Copyright (c) 2024-2025 The Basicswap developers # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -657,8 +657,8 @@ def main(): offer_identity = read_json_api( "identities/{}".format(offer["addr_from"]) ) - if len(offer_identity) > 0: - id_offer_from = offer_identity[0] + if "address" in offer_identity: + id_offer_from = offer_identity automation_override = id_offer_from["automation_override"] if automation_override == 2: if args.debug: From 7ee47207385bb95297b479a23525955d1a493c98 Mon Sep 17 00:00:00 2001 From: nahuhh <50635951+nahuhh@users.noreply.github.com> Date: Wed, 22 Jan 2025 18:19:36 +0000 Subject: [PATCH 5/9] wallet: fix reseed regression --- basicswap/templates/wallet.html | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/basicswap/templates/wallet.html b/basicswap/templates/wallet.html index 2a97623..8eb4143 100644 --- a/basicswap/templates/wallet.html +++ b/basicswap/templates/wallet.html @@ -222,8 +222,7 @@ - - + {% if block_unknown_seeds and w.expected_seed != true %} {# Only show addresses if wallet seed is correct #}
@@ -244,7 +243,9 @@
- {% else %} + + +{% else %}
From aac2f51b883ed48335e337a6cd3dbb53a11d508c Mon Sep 17 00:00:00 2001 From: nahuhh <50635951+nahuhh@users.noreply.github.com> Date: Wed, 22 Jan 2025 23:30:33 +0000 Subject: [PATCH 6/9] =?UTF-8?q?=C2=A0xmr:=20make=20earliest=20fork=20heigh?= =?UTF-8?q?t=20a=20transient=20error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- basicswap/interface/xmr.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/basicswap/interface/xmr.py b/basicswap/interface/xmr.py index 0ce3d5c..6fcd9b6 100644 --- a/basicswap/interface/xmr.py +++ b/basicswap/interface/xmr.py @@ -82,9 +82,9 @@ class XMRInterface(CoinInterface): return 1604 def is_transient_error(self, ex) -> bool: - # str_error: str = str(ex).lower() - # if "failed to get output distribution" in str_error: - # return True + str_error: str = str(ex).lower() + if "failed to get earliest fork height" in str_error: + return True return super().is_transient_error(ex) def __init__(self, coin_settings, network, swap_client=None): From ff2fc35f720ad7269ad18545e4bd0780ad983082 Mon Sep 17 00:00:00 2001 From: tecnovert Date: Sun, 26 Jan 2025 23:09:49 +0200 Subject: [PATCH 7/9] Add wallet_name option to basicswap.json. Removed "walletfile" setting for XMR and WOW, replaced with "wallet_name". Set wallet_name in prepare script with eg: BTC_WALLET_NAME env var. --- basicswap/basicswap.py | 17 ++++----- basicswap/bin/prepare.py | 54 +++++++++++++++++++++++----- basicswap/chainparams.py | 17 +++++++-- basicswap/interface/btc.py | 26 +++++++------- basicswap/interface/dcr/dcr.py | 3 ++ basicswap/interface/firo.py | 3 ++ basicswap/interface/ltc.py | 6 ++-- basicswap/interface/nav.py | 3 ++ basicswap/interface/wow.py | 1 + basicswap/interface/xmr.py | 6 ++-- tests/basicswap/extended/test_wow.py | 2 +- tests/basicswap/test_xmr.py | 2 +- 12 files changed, 101 insertions(+), 39 deletions(-) diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 8685173..d68cc4c 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -598,6 +598,13 @@ class BasicSwap(BaseApp): "chain_median_time": None, } + # Passthrough settings + for setting_name in ("wallet_name", "mweb_wallet_name"): + if setting_name in chain_client_settings: + self.coin_clients[coin][setting_name] = chain_client_settings[ + setting_name + ] + if coin in (Coins.FIRO, Coins.LTC): if not chain_client_settings.get("min_relay_fee"): chain_client_settings["min_relay_fee"] = 0.00001 @@ -842,17 +849,11 @@ class BasicSwap(BaseApp): elif coin == Coins.XMR: from .interface.xmr import XMRInterface - xmr_i = XMRInterface(self.coin_clients[coin], self.chain, self) - chain_client_settings = self.getChainClientSettings(coin) - xmr_i.setWalletFilename(chain_client_settings["walletfile"]) - return xmr_i + return XMRInterface(self.coin_clients[coin], self.chain, self) elif coin == Coins.WOW: from .interface.wow import WOWInterface - wow_i = WOWInterface(self.coin_clients[coin], self.chain, self) - chain_client_settings = self.getChainClientSettings(coin) - wow_i.setWalletFilename(chain_client_settings["walletfile"]) - return wow_i + return WOWInterface(self.coin_clients[coin], self.chain, self) elif coin == Coins.PIVX: from .interface.pivx import PIVXInterface diff --git a/basicswap/bin/prepare.py b/basicswap/bin/prepare.py index 5afef2a..c1e85f8 100755 --- a/basicswap/bin/prepare.py +++ b/basicswap/bin/prepare.py @@ -33,7 +33,7 @@ import basicswap.config as cfg from basicswap import __version__ from basicswap.base import getaddrinfo_tor from basicswap.basicswap import BasicSwap -from basicswap.chainparams import Coins +from basicswap.chainparams import Coins, chainparams, getCoinIdFromName from basicswap.contrib.rpcauth import generate_salt, password_to_hmac from basicswap.ui.util import getCoinName from basicswap.util import toBool @@ -363,6 +363,18 @@ def shouldManageDaemon(prefix: str) -> bool: return toBool(manage_daemon) +def getWalletName(coin_params: str, default_name: str, prefix_override=None) -> str: + prefix: str = coin_params["ticker"] if prefix_override is None else prefix_override + env_var_name: str = prefix + "_WALLET_NAME" + + if env_var_name in os.environ and coin_params.get("has_multiwallet", True) is False: + raise ValueError("Can't set wallet name for {}.".format(coin_params["ticker"])) + + wallet_name: str = os.getenv(env_var_name, default_name) + assert len(wallet_name) > 0 + return wallet_name + + def getKnownVersion(coin_name: str) -> str: version, version_tag, _ = known_coins[coin_name] return version + version_tag @@ -1121,6 +1133,8 @@ def writeTorSettings(fp, coin, coin_settings, tor_control_password): def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}): core_settings = settings["chainclients"][coin] + wallet_name = core_settings.get("wallet_name", "wallet.dat") + assert len(wallet_name) > 0 data_dir = core_settings["datadir"] tor_control_password = extra_opts.get("tor_control_password", None) @@ -1301,7 +1315,7 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}): fp.write("rpcport={}\n".format(core_settings["rpcport"])) fp.write("printtoconsole=0\n") fp.write("daemon=0\n") - fp.write("wallet=wallet.dat\n") + fp.write(f"wallet={wallet_name}\n") if tor_control_password is not None: writeTorSettings(fp, coin, core_settings, tor_control_password) @@ -1771,6 +1785,7 @@ def initialise_wallets( ] + [c for c in with_coins if c != "particl"] for coin_name in start_daemons: coin_settings = settings["chainclients"][coin_name] + wallet_name = coin_settings.get("wallet_name", "wallet.dat") c = swap_client.getCoinIdFromName(coin_name) if c == Coins.XMR: @@ -1862,9 +1877,9 @@ def initialise_wallets( swap_client.waitForDaemonRPC(c, with_wallet=False) # Create wallet if it doesn't exist yet wallets = swap_client.callcoinrpc(c, "listwallets") - if len(wallets) < 1: + if wallet_name not in wallets: logger.info( - "Creating wallet.dat for {}.".format(getCoinName(c)) + f'Creating wallet "{wallet_name}" for {getCoinName(c)}.' ) if c in (Coins.BTC, Coins.LTC, Coins.DOGE, Coins.DASH): @@ -1873,7 +1888,7 @@ def initialise_wallets( c, "createwallet", [ - "wallet.dat", + wallet_name, False, True, WALLET_ENCRYPTION_PWD, @@ -1883,7 +1898,13 @@ def initialise_wallets( ) swap_client.ci(c).unlockWallet(WALLET_ENCRYPTION_PWD) else: - swap_client.callcoinrpc(c, "createwallet", ["wallet.dat"]) + swap_client.callcoinrpc( + c, + "createwallet", + [ + wallet_name, + ], + ) if WALLET_ENCRYPTION_PWD != "": encrypt_wallet(swap_client, c) @@ -2400,7 +2421,6 @@ def main(): "walletrpchost": XMR_WALLET_RPC_HOST, "walletrpcuser": XMR_WALLET_RPC_USER, "walletrpcpassword": XMR_WALLET_RPC_PWD, - "walletfile": "swap_wallet", "datadir": os.getenv("XMR_DATA_DIR", os.path.join(data_dir, "monero")), "bindir": os.path.join(bin_dir, "monero"), "restore_height": xmr_restore_height, @@ -2487,7 +2507,6 @@ def main(): "walletrpchost": WOW_WALLET_RPC_HOST, "walletrpcuser": WOW_WALLET_RPC_USER, "walletrpcpassword": WOW_WALLET_RPC_PWD, - "walletfile": "swap_wallet", "datadir": os.getenv("WOW_DATA_DIR", os.path.join(data_dir, "wownero")), "bindir": os.path.join(bin_dir, "wownero"), "restore_height": wow_restore_height, @@ -2500,6 +2519,25 @@ def main(): }, } + for coin_name, coin_settings in chainclients.items(): + coin_id = getCoinIdFromName(coin_name) + coin_params = chainparams[coin_id] + if coin_settings.get("core_type_group", "") == "xmr": + default_name = "swap_wallet" + else: + default_name = "wallet.dat" + + if coin_name == "litecoin": + set_name: str = getWalletName( + coin_params, "mweb", prefix_override="LTC_MWEB" + ) + if set_name != "mweb": + coin_settings["mweb_wallet_name"] = set_name + + set_name: str = getWalletName(coin_params, default_name) + if set_name != default_name: + coin_settings["wallet_name"] = set_name + if PART_RPC_USER != "": chainclients["particl"]["rpcuser"] = PART_RPC_USER chainclients["particl"]["rpcpassword"] = PART_RPC_PWD diff --git a/basicswap/chainparams.py b/basicswap/chainparams.py index 3f3eca4..07b2376 100644 --- a/basicswap/chainparams.py +++ b/basicswap/chainparams.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Copyright (c) 2019-2024 tecnovert -# Copyright (c) 2024 The Basicswap developers +# Copyright (c) 2024-2025 The Basicswap developers # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -199,6 +199,7 @@ chainparams = { "message_magic": "Decred Signed Message:\n", "blocks_target": 60 * 5, "decimal_places": 8, + "has_multiwallet": False, "mainnet": { "rpcport": 9109, "pubkey_address": 0x073F, @@ -404,6 +405,7 @@ chainparams = { "has_cltv": False, "has_csv": False, "has_segwit": False, + "has_multiwallet": False, "mainnet": { "rpcport": 8888, "pubkey_address": 82, @@ -443,6 +445,7 @@ chainparams = { "decimal_places": 8, "has_csv": True, "has_segwit": True, + "has_multiwallet": False, "mainnet": { "rpcport": 44444, "pubkey_address": 53, @@ -519,10 +522,13 @@ chainparams = { }, }, } + +name_map = {} ticker_map = {} for c, params in chainparams.items(): + name_map[params["name"].lower()] = c ticker_map[params["ticker"].lower()] = c @@ -530,4 +536,11 @@ def getCoinIdFromTicker(ticker: str) -> str: try: return ticker_map[ticker.lower()] except Exception: - raise ValueError("Unknown coin") + raise ValueError(f"Unknown coin {ticker}") + + +def getCoinIdFromName(name: str) -> str: + try: + return name_map[name.lower()] + except Exception: + raise ValueError(f"Unknown coin {name}") diff --git a/basicswap/interface/btc.py b/basicswap/interface/btc.py index 0caa59b..3041f18 100644 --- a/basicswap/interface/btc.py +++ b/basicswap/interface/btc.py @@ -266,7 +266,7 @@ class BTCInterface(Secp256k1Interface): self._rpcport = coin_settings["rpcport"] self._rpcauth = coin_settings["rpcauth"] self.rpc = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host) - self._rpc_wallet = "wallet.dat" + self._rpc_wallet = coin_settings.get("wallet_name", "wallet.dat") self.rpc_wallet = make_rpc_func( self._rpcport, self._rpcauth, host=self._rpc_host, wallet=self._rpc_wallet ) @@ -301,16 +301,14 @@ class BTCInterface(Secp256k1Interface): # Wallet name is "" for some LTC and PART installs on older cores if self._rpc_wallet not in wallets and len(wallets) > 0: - self._log.debug("Changing {} wallet name.".format(self.ticker())) + self._log.debug(f"Changing {self.ticker()} wallet name.") for wallet_name in wallets: # Skip over other expected wallets if wallet_name in ("mweb",): continue self._rpc_wallet = wallet_name self._log.info( - "Switched {} wallet name to {}.".format( - self.ticker(), self._rpc_wallet - ) + f"Switched {self.ticker()} wallet name to {self._rpc_wallet}." ) self.rpc_wallet = make_rpc_func( self._rpcport, @@ -381,9 +379,9 @@ class BTCInterface(Secp256k1Interface): chain_synced = round(blockchaininfo["verificationprogress"], 3) if chain_synced < 1.0: - raise ValueError("{} chain isn't synced.".format(self.coin_name())) + raise ValueError(f"{self.coin_name()} chain isn't synced.") - self._log.debug("Finding block at time: {}".format(start_time)) + self._log.debug(f"Finding block at time: {start_time}") rpc_conn = self.open_rpc() try: @@ -397,7 +395,7 @@ class BTCInterface(Secp256k1Interface): block_hash = block_header["previousblockhash"] finally: self.close_rpc(rpc_conn) - raise ValueError("{} wallet restore height not found.".format(self.coin_name())) + raise ValueError(f"{self.coin_name()} wallet restore height not found.") def getWalletSeedID(self) -> str: wi = self.rpc_wallet("getwalletinfo") @@ -1806,16 +1804,20 @@ class BTCInterface(Secp256k1Interface): def unlockWallet(self, password: str): if password == "": return - self._log.info("unlockWallet - {}".format(self.ticker())) + self._log.info(f"unlockWallet - {self.ticker()}") if self.coin_type() == Coins.BTC: # Recreate wallet if none found # Required when encrypting an existing btc wallet, workaround is to delete the btc wallet and recreate wallets = self.rpc("listwallets") if len(wallets) < 1: - self._log.info("Creating wallet.dat for {}.".format(self.coin_name())) + self._log.info( + f'Creating wallet "{self._rpc_wallet}" for {self.coin_name()}.' + ) # wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors - self.rpc("createwallet", ["wallet.dat", False, True, "", False, False]) + self.rpc( + "createwallet", [self._rpc_wallet, False, True, "", False, False] + ) self.rpc_wallet("encryptwallet", [password]) # Max timeout value, ~3 years @@ -1823,7 +1825,7 @@ class BTCInterface(Secp256k1Interface): self._sc.checkWalletSeed(self.coin_type()) def lockWallet(self): - self._log.info("lockWallet - {}".format(self.ticker())) + self._log.info(f"lockWallet - {self.ticker()}") self.rpc_wallet("walletlock") def get_p2sh_script_pubkey(self, script: bytearray) -> bytearray: diff --git a/basicswap/interface/dcr/dcr.py b/basicswap/interface/dcr/dcr.py index 9bd6843..ac9ecf9 100644 --- a/basicswap/interface/dcr/dcr.py +++ b/basicswap/interface/dcr/dcr.py @@ -277,6 +277,9 @@ class DCRInterface(Secp256k1Interface): self._connection_type = coin_settings["connection_type"] self._altruistic = coin_settings.get("altruistic", True) + if "wallet_name" in coin_settings: + raise ValueError(f"Invalid setting for {self.coin_name()}: wallet_name") + def open_rpc(self): return openrpc(self._rpcport, self._rpcauth, host=self._rpc_host) diff --git a/basicswap/interface/firo.py b/basicswap/interface/firo.py index 033592c..2d9747d 100644 --- a/basicswap/interface/firo.py +++ b/basicswap/interface/firo.py @@ -45,6 +45,9 @@ class FIROInterface(BTCInterface): self._rpcport, self._rpcauth, host=self._rpc_host ) + if "wallet_name" in coin_settings: + raise ValueError(f"Invalid setting for {self.coin_name()}: wallet_name") + def getExchangeName(self, exchange_name: str) -> str: return "zcoin" diff --git a/basicswap/interface/ltc.py b/basicswap/interface/ltc.py index 3e1d554..b5e6eee 100644 --- a/basicswap/interface/ltc.py +++ b/basicswap/interface/ltc.py @@ -18,7 +18,7 @@ class LTCInterface(BTCInterface): def __init__(self, coin_settings, network, swap_client=None): super(LTCInterface, self).__init__(coin_settings, network, swap_client) - self._rpc_wallet_mweb = "mweb" + self._rpc_wallet_mweb = coin_settings.get("mweb_wallet_name", "mweb") self.rpc_wallet_mweb = make_rpc_func( self._rpcport, self._rpcauth, @@ -94,7 +94,7 @@ class LTCInterfaceMWEB(LTCInterface): def __init__(self, coin_settings, network, swap_client=None): super(LTCInterfaceMWEB, self).__init__(coin_settings, network, swap_client) - self._rpc_wallet = "mweb" + self._rpc_wallet = coin_settings.get("mweb_wallet_name", "mweb") self.rpc_wallet = make_rpc_func( self._rpcport, self._rpcauth, host=self._rpc_host, wallet=self._rpc_wallet ) @@ -128,7 +128,7 @@ class LTCInterfaceMWEB(LTCInterface): self._log.info("init_wallet - {}".format(self.ticker())) - self._log.info("Creating mweb wallet for {}.".format(self.coin_name())) + self._log.info(f"Creating wallet {self._rpc_wallet} for {self.coin_name()}.") # wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors, load_on_startup self.rpc("createwallet", ["mweb", False, True, password, False, False, True]) diff --git a/basicswap/interface/nav.py b/basicswap/interface/nav.py index ba8f190..1211e1f 100644 --- a/basicswap/interface/nav.py +++ b/basicswap/interface/nav.py @@ -81,6 +81,9 @@ class NAVInterface(BTCInterface): self._rpcport, self._rpcauth, host=self._rpc_host ) + if "wallet_name" in coin_settings: + raise ValueError(f"Invalid setting for {self.coin_name()}: wallet_name") + def use_p2shp2wsh(self) -> bool: # p2sh-p2wsh return True diff --git a/basicswap/interface/wow.py b/basicswap/interface/wow.py index 2156e23..d615b01 100644 --- a/basicswap/interface/wow.py +++ b/basicswap/interface/wow.py @@ -1,6 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +# 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. diff --git a/basicswap/interface/xmr.py b/basicswap/interface/xmr.py index 0ce3d5c..bae0cd1 100644 --- a/basicswap/interface/xmr.py +++ b/basicswap/interface/xmr.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # Copyright (c) 2020-2024 tecnovert -# Copyright (c) 2024 The Basicswap developers +# Copyright (c) 2024-2025 The Basicswap developers # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -99,6 +99,7 @@ class XMRInterface(CoinInterface): self._log = self._sc.log if self._sc and self._sc.log else logging self._wallet_password = None self._have_checked_seed = False + self._wallet_filename = coin_settings.get("wallet_name", "swap_wallet") daemon_login = None if coin_settings.get("rpcuser", "") != "": @@ -175,9 +176,6 @@ class XMRInterface(CoinInterface): ensure(new_priority >= 0 and new_priority < 4, "Invalid fee_priority value") self._fee_priority = new_priority - def setWalletFilename(self, wallet_filename): - self._wallet_filename = wallet_filename - def createWallet(self, params): if self._wallet_password is not None: params["password"] = self._wallet_password diff --git a/tests/basicswap/extended/test_wow.py b/tests/basicswap/extended/test_wow.py index b231bb3..8a022e0 100644 --- a/tests/basicswap/extended/test_wow.py +++ b/tests/basicswap/extended/test_wow.py @@ -189,7 +189,7 @@ class Test(BaseTest): "walletrpcport": WOW_BASE_WALLET_RPC_PORT + node_id, "walletrpcuser": "test" + str(node_id), "walletrpcpassword": "test_pass" + str(node_id), - "walletfile": "testwallet", + "wallet_name": "testwallet", "datadir": os.path.join(datadir, "xmr_" + str(node_id)), "bindir": WOW_BINDIR, } diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py index 0552c9f..c483b49 100644 --- a/tests/basicswap/test_xmr.py +++ b/tests/basicswap/test_xmr.py @@ -197,7 +197,7 @@ def prepare_swapclient_dir( "walletrpcport": XMR_BASE_WALLET_RPC_PORT + node_id, "walletrpcuser": "test" + str(node_id), "walletrpcpassword": "test_pass" + str(node_id), - "walletfile": "testwallet", + "wallet_name": "testwallet", "datadir": os.path.join(datadir, "xmr_" + str(node_id)), "bindir": cfg.XMR_BINDIR, } From 473e4fd40031dfc2edceac5c41a677db50f9cfa6 Mon Sep 17 00:00:00 2001 From: tecnovert Date: Wed, 29 Jan 2025 07:55:02 +0200 Subject: [PATCH 8/9] Fix CI caching --- .github/workflows/ci.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66de431..47fc544 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,9 @@ jobs: - name: Install run: | pip install . + # Print the core versions to a file for caching + basicswap-prepare --version --withcoins=bitcoin | tail -n +2 > core_versions.txt + cat core_versions.txt - name: Running flake8 run: | flake8 --ignore=E203,E501,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,.eggs,.tox,bin/install_certifi.py @@ -44,12 +47,11 @@ jobs: uses: actions/cache@v3 env: cache-name: cache-cores - CACHE_KEY: $(printf $(python bin/basicswap-prepare.py --version --withcoins=bitcoin) | sha256sum | head -c 64) with: - path: $BIN_DIR - key: $CACHE_KEY + path: /tmp/cached_bin + key: cores-${{ runner.os }}-${{ hashFiles('**/core_versions.txt') }} - - if: ${{ steps.cache-yarn.outputs.cache-hit != 'true' }} + - if: ${{ steps.cache-cores.outputs.cache-hit != 'true' }} name: Running basicswap-prepare run: | basicswap-prepare --bindir="$BIN_DIR" --preparebinonly --withcoins=particl,bitcoin,monero From 713577d8688594f3d32af73018baebfa862b9d2a Mon Sep 17 00:00:00 2001 From: Gerlof van Ek Date: Thu, 30 Jan 2025 13:16:41 +0100 Subject: [PATCH 9/9] JS/UI: Tooltips + Sorting table + Memory fix and new header. (#253) * JS/UI: Tooltips + Sorting table + Memory fix and new header. * LINT * Light theme fix * JS: Global / standalone Tooltips. * Unminimized versions of tippy and popper js libs * Formatting / Cleanup --- basicswap/http_server.py | 6 +- basicswap/static/css/style.css | 376 ++-- basicswap/static/js/libs/popper.js | 1825 ++++++++++++++++ basicswap/static/js/libs/tippy.js | 2516 +++++++++++++++++++++++ basicswap/static/js/offerstable.js | 650 +++--- basicswap/static/js/pricechart.js | 42 +- basicswap/static/js/tooltips.js | 563 +++-- basicswap/templates/active.html | 1 - basicswap/templates/bids.html | 428 ++-- basicswap/templates/bids_available.html | 200 ++ basicswap/templates/header.html | 1356 +++++++----- basicswap/templates/offers.html | 8 +- basicswap/ui/page_bids.py | 88 +- 13 files changed, 6516 insertions(+), 1543 deletions(-) create mode 100644 basicswap/static/js/libs/popper.js create mode 100644 basicswap/static/js/libs/tippy.js create mode 100644 basicswap/templates/bids_available.html diff --git a/basicswap/http_server.py b/basicswap/http_server.py index bbcbdea..60df1f6 100644 --- a/basicswap/http_server.py +++ b/basicswap/http_server.py @@ -578,10 +578,8 @@ class HttpHandler(BaseHTTPRequestHandler): return page_offers(self, url_split, post_string, sent=True) if page == "bid": return page_bid(self, url_split, post_string) - if page == "receivedbids": - return page_bids(self, url_split, post_string, received=True) - if page == "sentbids": - return page_bids(self, url_split, post_string, sent=True) + if page == "bids": + return page_bids(self, url_split, post_string) if page == "availablebids": return page_bids(self, url_split, post_string, available=True) if page == "watched": diff --git a/basicswap/static/css/style.css b/basicswap/static/css/style.css index fd764b2..8897efb 100644 --- a/basicswap/static/css/style.css +++ b/basicswap/static/css/style.css @@ -1,153 +1,157 @@ /* General Styles */ .bold { - font-weight: bold; + font-weight: bold; } .monospace { - font-family: monospace; + font-family: monospace; } .floatright { - position: fixed; - top: 1.25rem; - right: 1.25rem; - z-index: 9999; + position: fixed; + top: 1.25rem; + right: 1.25rem; + z-index: 9999; } /* Table Styles */ .padded_row td { - padding-top: 1.5em; + padding-top: 1.5em; } /* Modal Styles */ .modal-highest { - z-index: 9999; + z-index: 9999; } /* Animation */ #hide { - -moz-animation: cssAnimation 0s ease-in 15s forwards; - -webkit-animation: cssAnimation 0s ease-in 15s forwards; - -o-animation: cssAnimation 0s ease-in 15s forwards; - animation: cssAnimation 0s ease-in 15s forwards; - -webkit-animation-fill-mode: forwards; - animation-fill-mode: forwards; + -moz-animation: cssAnimation 0s ease-in 15s forwards; + -webkit-animation: cssAnimation 0s ease-in 15s forwards; + -o-animation: cssAnimation 0s ease-in 15s forwards; + animation: cssAnimation 0s ease-in 15s forwards; + -webkit-animation-fill-mode: forwards; + animation-fill-mode: forwards; } @keyframes cssAnimation { - to { - width: 0; - height: 0; - overflow: hidden; - } + to { + width: 0; + height: 0; + overflow: hidden; + } } @-webkit-keyframes cssAnimation { - to { - width: 0; - height: 0; - visibility: hidden; - } + to { + width: 0; + height: 0; + visibility: hidden; + } } /* Custom Select Styles */ .custom-select .select { - appearance: none; - background-image: url('/static/images/other/coin.png'); - background-position: 10px center; - background-repeat: no-repeat; - position: relative; + appearance: none; + background-image: url('/static/images/other/coin.png'); + background-position: 10px center; + background-repeat: no-repeat; + position: relative; } .custom-select select::-webkit-scrollbar { - width: 0; + width: 0; } .custom-select .select option { - padding-left: 0; - text-indent: 0; - background-repeat: no-repeat; - background-position: 0 50%; + padding-left: 0; + text-indent: 0; + background-repeat: no-repeat; + background-position: 0 50%; } .custom-select .select option.no-space { - padding-left: 0; + padding-left: 0; } .custom-select .select option[data-image] { - background-image: url(''); + background-image: url(''); } .custom-select .select-icon { - position: absolute; - top: 50%; - left: 10px; - transform: translateY(-50%); + position: absolute; + top: 50%; + left: 10px; + transform: translateY(-50%); } .custom-select .select-image { - display: none; - margin-top: 10px; + display: none; + margin-top: 10px; } .custom-select .select:focus + .select-dropdown .select-image { - display: block; + display: block; } /* Blur and Overlay Styles */ .blurred { - filter: blur(3px); - pointer-events: none; - user-select: none; + filter: blur(3px); + pointer-events: none; + user-select: none; } .error-overlay.non-blurred { - filter: none; - pointer-events: auto; - user-select: auto; + filter: none; + pointer-events: auto; + user-select: auto; } /* Form Element Styles */ -@media screen and (-webkit-min-device-pixel-ratio:0) { - select:disabled, - input:disabled, - textarea:disabled { - opacity: 1 !important; - } +@media screen and (-webkit-min-device-pixel-ratio: 0) { + select:disabled, + input:disabled, + textarea:disabled { + opacity: 1 !important; + } } .error { - border: 1px solid red !important; + border: 1px solid red !important; } /* Active Container Styles */ .active-container { - position: relative; - border-radius: 10px; + position: relative; + border-radius: 10px; } .active-container::before { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - border: 1px solid rgb(77, 132, 240); - border-radius: inherit; - pointer-events: none; + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border: 1px solid rgb(77, 132, 240); + border-radius: inherit; + pointer-events: none; } /* Center Spin Animation */ .center-spin { - display: flex; - justify-content: center; - align-items: center; + display: flex; + justify-content: center; + align-items: center; } @keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } } /* Hover Container Styles */ @@ -155,215 +159,209 @@ .hover-container:hover #coin_to, .hover-container:hover #coin_from_button, .hover-container:hover #coin_from { - border-color: #3b82f6; + border-color: #3b82f6; } -#coin_to_button, #coin_from_button { - background-repeat: no-repeat; - background-position: center; - background-size: 20px 20px; +#coin_to_button, +#coin_from_button { + background-repeat: no-repeat; + background-position: center; + background-size: 20px 20px; } /* Input-like Container Styles */ .input-like-container { - max-width: 100%; - background-color: #ffffff; - width: 360px; - padding: 1rem; - color: #374151; - border-radius: 0.375rem; - font-size: 0.875rem; - line-height: 1.25rem; - outline: none; - word-wrap: break-word; - overflow-wrap: break-word; - word-break: break-all; - height: auto; - min-height: 90px; - max-height: 150px; - display: flex; - align-items: center; - justify-content: center; - position: relative; - overflow-y: auto; + max-width: 100%; + background-color: #ffffff; + width: 360px; + padding: 1rem; + color: #374151; + border-radius: 0.375rem; + font-size: 0.875rem; + line-height: 1.25rem; + outline: none; + word-wrap: break-word; + overflow-wrap: break-word; + word-break: break-all; + height: auto; + min-height: 90px; + max-height: 150px; + display: flex; + align-items: center; + justify-content: center; + position: relative; + overflow-y: auto; } .input-like-container.dark { - background-color: #374151; - color: #ffffff; + background-color: #374151; + color: #ffffff; } .input-like-container.copying { - width: inherit; + width: inherit; } /* QR Code Styles */ .qrcode { - position: relative; - display: inline-block; - padding: 10px; - overflow: hidden; + position: relative; + display: inline-block; + padding: 10px; + overflow: hidden; } .qrcode-border { - border: 2px solid; - background-color: #ffffff; - border-radius: 0px; + border: 2px solid; + background-color: #ffffff; + border-radius: 0px; } .qrcode img { - width: 100%; - height: auto; - border-radius: 0px; + width: 100%; + height: auto; + border-radius: 0px; } #showQR { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - height: 25px; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + height: 25px; } .qrcode-container { - margin-top: 25px; + margin-top: 25px; } /* Disabled Element Styles */ select.select-disabled, .disabled-input-enabled, select.disabled-select-enabled { - opacity: 0.40 !important; + opacity: 0.40 !important; } /* Shutdown Modal Styles */ #shutdownModal { - z-index: 50; + z-index: 50; } #shutdownModal > div:first-child { - z-index: 40; + z-index: 40; } #shutdownModal > div:last-child { - z-index: 50; + z-index: 50; } #shutdownModal > div { - transition: opacity 0.3s ease-out; + transition: opacity 0.3s ease-out; } #shutdownModal.hidden > div { - opacity: 0; + opacity: 0; } #shutdownModal:not(.hidden) > div { - opacity: 1; + opacity: 1; } .shutdown-button { - transition: all 0.3s ease; + transition: all 0.3s ease; } .shutdown-button.shutdown-disabled { - opacity: 0.6; - cursor: not-allowed; - color: #a0aec0; + opacity: 0.6; + cursor: not-allowed; + color: #a0aec0; } .shutdown-button.shutdown-disabled:hover { - background-color: #4a5568; + background-color: #4a5568; } .shutdown-button.shutdown-disabled svg { - opacity: 0.5; + opacity: 0.5; } - -/* Loading line animation */ +/* Loading Line Animation */ .loading-line { - width: 100%; - height: 2px; - background-color: #ccc; - overflow: hidden; - position: relative; + width: 100%; + height: 2px; + background-color: #ccc; + overflow: hidden; + position: relative; } + .loading-line::before { - content: ''; - display: block; - width: 100%; - height: 100%; - background: linear-gradient(to right, transparent, #007bff, transparent); - animation: loading 1.5s infinite; + content: ''; + display: block; + width: 100%; + height: 100%; + background: linear-gradient(to right, transparent, #007bff, transparent); + animation: loading 1.5s infinite; } + @keyframes loading { - 0% { - transform: translateX(-100%); - } - 100% { - transform: translateX(100%); - } + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(100%); + } } -/* Hide the loading line once data is loaded */ + .usd-value:not(.loading) .loading-line, .profit-loss:not(.loading) .loading-line { - display: none; + display: none; } - .resolution-button { +/* Resolution Button Styles */ +.resolution-button { background: none; border: none; - color: #4B5563; /* gray-600 */ - font-size: 0.875rem; /* text-sm */ - font-weight: 500; /* font-medium */ + color: #4B5563; + font-size: 0.875rem; + font-weight: 500; padding: 0.25rem 0.5rem; border-radius: 0.25rem; transition: all 0.2s; outline: 2px solid transparent; outline-offset: 2px; - } +} - .resolution-button:hover { - color: #1F2937; /* gray-800 */ - } +.resolution-button:hover { + color: #1F2937; +} - .resolution-button:focus { - outline: 2px solid #3B82F6; /* blue-500 */ - } +.resolution-button:focus { + outline: 2px solid #3B82F6; +} - .resolution-button.active { - color: #3B82F6; /* blue-500 */ - outline: 2px solid #3B82F6; /* blue-500 */ - } +.resolution-button.active { + color: #3B82F6; + outline: 2px solid #3B82F6; +} - .dark .resolution-button { - color: #9CA3AF; /* gray-400 */ - } +.dark .resolution-button { + color: #9CA3AF; +} - .dark .resolution-button:hover { - color: #F3F4F6; /* gray-100 */ - } +.dark .resolution-button:hover { + color: #F3F4F6; +} - .dark .resolution-button.active { - color: #60A5FA; /* blue-400 */ - outline-color: #60A5FA; /* blue-400 */ +.dark .resolution-button.active { + color: #60A5FA; + outline-color: #60A5FA; color: #fff; - } - - #toggle-volume.active { - @apply bg-green-500 hover:bg-green-600 focus:ring-green-300; - } - #toggle-auto-refresh[data-enabled="true"] { - @apply bg-green-500 hover:bg-green-600 focus:ring-green-300; - } - - [data-popper-placement] { - will-change: transform; - transform: translateZ(0); } -.tooltip { - backface-visibility: hidden; - -webkit-backface-visibility: hidden; +/* Toggle Button Styles */ +#toggle-volume.active { + @apply bg-green-500 hover:bg-green-600 focus:ring-green-300; } +#toggle-auto-refresh[data-enabled="true"] { + @apply bg-green-500 hover:bg-green-600 focus:ring-green-300; +} diff --git a/basicswap/static/js/libs/popper.js b/basicswap/static/js/libs/popper.js new file mode 100644 index 0000000..a00f139 --- /dev/null +++ b/basicswap/static/js/libs/popper.js @@ -0,0 +1,1825 @@ +/** + * @popperjs/core v2.11.8 - MIT License + */ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Popper = {})); +}(this, (function (exports) { 'use strict'; + + function getWindow(node) { + if (node == null) { + return window; + } + + if (node.toString() !== '[object Window]') { + var ownerDocument = node.ownerDocument; + return ownerDocument ? ownerDocument.defaultView || window : window; + } + + return node; + } + + function isElement(node) { + var OwnElement = getWindow(node).Element; + return node instanceof OwnElement || node instanceof Element; + } + + function isHTMLElement(node) { + var OwnElement = getWindow(node).HTMLElement; + return node instanceof OwnElement || node instanceof HTMLElement; + } + + function isShadowRoot(node) { + // IE 11 has no ShadowRoot + if (typeof ShadowRoot === 'undefined') { + return false; + } + + var OwnElement = getWindow(node).ShadowRoot; + return node instanceof OwnElement || node instanceof ShadowRoot; + } + + var max = Math.max; + var min = Math.min; + var round = Math.round; + + function getUAString() { + var uaData = navigator.userAgentData; + + if (uaData != null && uaData.brands && Array.isArray(uaData.brands)) { + return uaData.brands.map(function (item) { + return item.brand + "/" + item.version; + }).join(' '); + } + + return navigator.userAgent; + } + + function isLayoutViewport() { + return !/^((?!chrome|android).)*safari/i.test(getUAString()); + } + + function getBoundingClientRect(element, includeScale, isFixedStrategy) { + if (includeScale === void 0) { + includeScale = false; + } + + if (isFixedStrategy === void 0) { + isFixedStrategy = false; + } + + var clientRect = element.getBoundingClientRect(); + var scaleX = 1; + var scaleY = 1; + + if (includeScale && isHTMLElement(element)) { + scaleX = element.offsetWidth > 0 ? round(clientRect.width) / element.offsetWidth || 1 : 1; + scaleY = element.offsetHeight > 0 ? round(clientRect.height) / element.offsetHeight || 1 : 1; + } + + var _ref = isElement(element) ? getWindow(element) : window, + visualViewport = _ref.visualViewport; + + var addVisualOffsets = !isLayoutViewport() && isFixedStrategy; + var x = (clientRect.left + (addVisualOffsets && visualViewport ? visualViewport.offsetLeft : 0)) / scaleX; + var y = (clientRect.top + (addVisualOffsets && visualViewport ? visualViewport.offsetTop : 0)) / scaleY; + var width = clientRect.width / scaleX; + var height = clientRect.height / scaleY; + return { + width: width, + height: height, + top: y, + right: x + width, + bottom: y + height, + left: x, + x: x, + y: y + }; + } + + function getWindowScroll(node) { + var win = getWindow(node); + var scrollLeft = win.pageXOffset; + var scrollTop = win.pageYOffset; + return { + scrollLeft: scrollLeft, + scrollTop: scrollTop + }; + } + + function getHTMLElementScroll(element) { + return { + scrollLeft: element.scrollLeft, + scrollTop: element.scrollTop + }; + } + + function getNodeScroll(node) { + if (node === getWindow(node) || !isHTMLElement(node)) { + return getWindowScroll(node); + } else { + return getHTMLElementScroll(node); + } + } + + function getNodeName(element) { + return element ? (element.nodeName || '').toLowerCase() : null; + } + + function getDocumentElement(element) { + // $FlowFixMe[incompatible-return]: assume body is always available + return ((isElement(element) ? element.ownerDocument : // $FlowFixMe[prop-missing] + element.document) || window.document).documentElement; + } + + function getWindowScrollBarX(element) { + // If has a CSS width greater than the viewport, then this will be + // incorrect for RTL. + // Popper 1 is broken in this case and never had a bug report so let's assume + // it's not an issue. I don't think anyone ever specifies width on + // anyway. + // Browsers where the left scrollbar doesn't cause an issue report `0` for + // this (e.g. Edge 2019, IE11, Safari) + return getBoundingClientRect(getDocumentElement(element)).left + getWindowScroll(element).scrollLeft; + } + + function getComputedStyle(element) { + return getWindow(element).getComputedStyle(element); + } + + function isScrollParent(element) { + // Firefox wants us to check `-x` and `-y` variations as well + var _getComputedStyle = getComputedStyle(element), + overflow = _getComputedStyle.overflow, + overflowX = _getComputedStyle.overflowX, + overflowY = _getComputedStyle.overflowY; + + return /auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX); + } + + function isElementScaled(element) { + var rect = element.getBoundingClientRect(); + var scaleX = round(rect.width) / element.offsetWidth || 1; + var scaleY = round(rect.height) / element.offsetHeight || 1; + return scaleX !== 1 || scaleY !== 1; + } // Returns the composite rect of an element relative to its offsetParent. + // Composite means it takes into account transforms as well as layout. + + + function getCompositeRect(elementOrVirtualElement, offsetParent, isFixed) { + if (isFixed === void 0) { + isFixed = false; + } + + var isOffsetParentAnElement = isHTMLElement(offsetParent); + var offsetParentIsScaled = isHTMLElement(offsetParent) && isElementScaled(offsetParent); + var documentElement = getDocumentElement(offsetParent); + var rect = getBoundingClientRect(elementOrVirtualElement, offsetParentIsScaled, isFixed); + var scroll = { + scrollLeft: 0, + scrollTop: 0 + }; + var offsets = { + x: 0, + y: 0 + }; + + if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) { + if (getNodeName(offsetParent) !== 'body' || // https://github.com/popperjs/popper-core/issues/1078 + isScrollParent(documentElement)) { + scroll = getNodeScroll(offsetParent); + } + + if (isHTMLElement(offsetParent)) { + offsets = getBoundingClientRect(offsetParent, true); + offsets.x += offsetParent.clientLeft; + offsets.y += offsetParent.clientTop; + } else if (documentElement) { + offsets.x = getWindowScrollBarX(documentElement); + } + } + + return { + x: rect.left + scroll.scrollLeft - offsets.x, + y: rect.top + scroll.scrollTop - offsets.y, + width: rect.width, + height: rect.height + }; + } + + // means it doesn't take into account transforms. + + function getLayoutRect(element) { + var clientRect = getBoundingClientRect(element); // Use the clientRect sizes if it's not been transformed. + // Fixes https://github.com/popperjs/popper-core/issues/1223 + + var width = element.offsetWidth; + var height = element.offsetHeight; + + if (Math.abs(clientRect.width - width) <= 1) { + width = clientRect.width; + } + + if (Math.abs(clientRect.height - height) <= 1) { + height = clientRect.height; + } + + return { + x: element.offsetLeft, + y: element.offsetTop, + width: width, + height: height + }; + } + + function getParentNode(element) { + if (getNodeName(element) === 'html') { + return element; + } + + return (// this is a quicker (but less type safe) way to save quite some bytes from the bundle + // $FlowFixMe[incompatible-return] + // $FlowFixMe[prop-missing] + element.assignedSlot || // step into the shadow DOM of the parent of a slotted node + element.parentNode || ( // DOM Element detected + isShadowRoot(element) ? element.host : null) || // ShadowRoot detected + // $FlowFixMe[incompatible-call]: HTMLElement is a Node + getDocumentElement(element) // fallback + + ); + } + + function getScrollParent(node) { + if (['html', 'body', '#document'].indexOf(getNodeName(node)) >= 0) { + // $FlowFixMe[incompatible-return]: assume body is always available + return node.ownerDocument.body; + } + + if (isHTMLElement(node) && isScrollParent(node)) { + return node; + } + + return getScrollParent(getParentNode(node)); + } + + /* + given a DOM element, return the list of all scroll parents, up the list of ancesors + until we get to the top window object. This list is what we attach scroll listeners + to, because if any of these parent elements scroll, we'll need to re-calculate the + reference element's position. + */ + + function listScrollParents(element, list) { + var _element$ownerDocumen; + + if (list === void 0) { + list = []; + } + + var scrollParent = getScrollParent(element); + var isBody = scrollParent === ((_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body); + var win = getWindow(scrollParent); + var target = isBody ? [win].concat(win.visualViewport || [], isScrollParent(scrollParent) ? scrollParent : []) : scrollParent; + var updatedList = list.concat(target); + return isBody ? updatedList : // $FlowFixMe[incompatible-call]: isBody tells us target will be an HTMLElement here + updatedList.concat(listScrollParents(getParentNode(target))); + } + + function isTableElement(element) { + return ['table', 'td', 'th'].indexOf(getNodeName(element)) >= 0; + } + + function getTrueOffsetParent(element) { + if (!isHTMLElement(element) || // https://github.com/popperjs/popper-core/issues/837 + getComputedStyle(element).position === 'fixed') { + return null; + } + + return element.offsetParent; + } // `.offsetParent` reports `null` for fixed elements, while absolute elements + // return the containing block + + + function getContainingBlock(element) { + var isFirefox = /firefox/i.test(getUAString()); + var isIE = /Trident/i.test(getUAString()); + + if (isIE && isHTMLElement(element)) { + // In IE 9, 10 and 11 fixed elements containing block is always established by the viewport + var elementCss = getComputedStyle(element); + + if (elementCss.position === 'fixed') { + return null; + } + } + + var currentNode = getParentNode(element); + + if (isShadowRoot(currentNode)) { + currentNode = currentNode.host; + } + + while (isHTMLElement(currentNode) && ['html', 'body'].indexOf(getNodeName(currentNode)) < 0) { + var css = getComputedStyle(currentNode); // This is non-exhaustive but covers the most common CSS properties that + // create a containing block. + // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block + + if (css.transform !== 'none' || css.perspective !== 'none' || css.contain === 'paint' || ['transform', 'perspective'].indexOf(css.willChange) !== -1 || isFirefox && css.willChange === 'filter' || isFirefox && css.filter && css.filter !== 'none') { + return currentNode; + } else { + currentNode = currentNode.parentNode; + } + } + + return null; + } // Gets the closest ancestor positioned element. Handles some edge cases, + // such as table ancestors and cross browser bugs. + + + function getOffsetParent(element) { + var window = getWindow(element); + var offsetParent = getTrueOffsetParent(element); + + while (offsetParent && isTableElement(offsetParent) && getComputedStyle(offsetParent).position === 'static') { + offsetParent = getTrueOffsetParent(offsetParent); + } + + if (offsetParent && (getNodeName(offsetParent) === 'html' || getNodeName(offsetParent) === 'body' && getComputedStyle(offsetParent).position === 'static')) { + return window; + } + + return offsetParent || getContainingBlock(element) || window; + } + + var top = 'top'; + var bottom = 'bottom'; + var right = 'right'; + var left = 'left'; + var auto = 'auto'; + var basePlacements = [top, bottom, right, left]; + var start = 'start'; + var end = 'end'; + var clippingParents = 'clippingParents'; + var viewport = 'viewport'; + var popper = 'popper'; + var reference = 'reference'; + var variationPlacements = /*#__PURE__*/basePlacements.reduce(function (acc, placement) { + return acc.concat([placement + "-" + start, placement + "-" + end]); + }, []); + var placements = /*#__PURE__*/[].concat(basePlacements, [auto]).reduce(function (acc, placement) { + return acc.concat([placement, placement + "-" + start, placement + "-" + end]); + }, []); // modifiers that need to read the DOM + + var beforeRead = 'beforeRead'; + var read = 'read'; + var afterRead = 'afterRead'; // pure-logic modifiers + + var beforeMain = 'beforeMain'; + var main = 'main'; + var afterMain = 'afterMain'; // modifier with the purpose to write to the DOM (or write into a framework state) + + var beforeWrite = 'beforeWrite'; + var write = 'write'; + var afterWrite = 'afterWrite'; + var modifierPhases = [beforeRead, read, afterRead, beforeMain, main, afterMain, beforeWrite, write, afterWrite]; + + function order(modifiers) { + var map = new Map(); + var visited = new Set(); + var result = []; + modifiers.forEach(function (modifier) { + map.set(modifier.name, modifier); + }); // On visiting object, check for its dependencies and visit them recursively + + function sort(modifier) { + visited.add(modifier.name); + var requires = [].concat(modifier.requires || [], modifier.requiresIfExists || []); + requires.forEach(function (dep) { + if (!visited.has(dep)) { + var depModifier = map.get(dep); + + if (depModifier) { + sort(depModifier); + } + } + }); + result.push(modifier); + } + + modifiers.forEach(function (modifier) { + if (!visited.has(modifier.name)) { + // check for visited object + sort(modifier); + } + }); + return result; + } + + function orderModifiers(modifiers) { + // order based on dependencies + var orderedModifiers = order(modifiers); // order based on phase + + return modifierPhases.reduce(function (acc, phase) { + return acc.concat(orderedModifiers.filter(function (modifier) { + return modifier.phase === phase; + })); + }, []); + } + + function debounce(fn) { + var pending; + return function () { + if (!pending) { + pending = new Promise(function (resolve) { + Promise.resolve().then(function () { + pending = undefined; + resolve(fn()); + }); + }); + } + + return pending; + }; + } + + function mergeByName(modifiers) { + var merged = modifiers.reduce(function (merged, current) { + var existing = merged[current.name]; + merged[current.name] = existing ? Object.assign({}, existing, current, { + options: Object.assign({}, existing.options, current.options), + data: Object.assign({}, existing.data, current.data) + }) : current; + return merged; + }, {}); // IE11 does not support Object.values + + return Object.keys(merged).map(function (key) { + return merged[key]; + }); + } + + function getViewportRect(element, strategy) { + var win = getWindow(element); + var html = getDocumentElement(element); + var visualViewport = win.visualViewport; + var width = html.clientWidth; + var height = html.clientHeight; + var x = 0; + var y = 0; + + if (visualViewport) { + width = visualViewport.width; + height = visualViewport.height; + var layoutViewport = isLayoutViewport(); + + if (layoutViewport || !layoutViewport && strategy === 'fixed') { + x = visualViewport.offsetLeft; + y = visualViewport.offsetTop; + } + } + + return { + width: width, + height: height, + x: x + getWindowScrollBarX(element), + y: y + }; + } + + // of the `` and `` rect bounds if horizontally scrollable + + function getDocumentRect(element) { + var _element$ownerDocumen; + + var html = getDocumentElement(element); + var winScroll = getWindowScroll(element); + var body = (_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body; + var width = max(html.scrollWidth, html.clientWidth, body ? body.scrollWidth : 0, body ? body.clientWidth : 0); + var height = max(html.scrollHeight, html.clientHeight, body ? body.scrollHeight : 0, body ? body.clientHeight : 0); + var x = -winScroll.scrollLeft + getWindowScrollBarX(element); + var y = -winScroll.scrollTop; + + if (getComputedStyle(body || html).direction === 'rtl') { + x += max(html.clientWidth, body ? body.clientWidth : 0) - width; + } + + return { + width: width, + height: height, + x: x, + y: y + }; + } + + function contains(parent, child) { + var rootNode = child.getRootNode && child.getRootNode(); // First, attempt with faster native method + + if (parent.contains(child)) { + return true; + } // then fallback to custom implementation with Shadow DOM support + else if (rootNode && isShadowRoot(rootNode)) { + var next = child; + + do { + if (next && parent.isSameNode(next)) { + return true; + } // $FlowFixMe[prop-missing]: need a better way to handle this... + + + next = next.parentNode || next.host; + } while (next); + } // Give up, the result is false + + + return false; + } + + function rectToClientRect(rect) { + return Object.assign({}, rect, { + left: rect.x, + top: rect.y, + right: rect.x + rect.width, + bottom: rect.y + rect.height + }); + } + + function getInnerBoundingClientRect(element, strategy) { + var rect = getBoundingClientRect(element, false, strategy === 'fixed'); + rect.top = rect.top + element.clientTop; + rect.left = rect.left + element.clientLeft; + rect.bottom = rect.top + element.clientHeight; + rect.right = rect.left + element.clientWidth; + rect.width = element.clientWidth; + rect.height = element.clientHeight; + rect.x = rect.left; + rect.y = rect.top; + return rect; + } + + function getClientRectFromMixedType(element, clippingParent, strategy) { + return clippingParent === viewport ? rectToClientRect(getViewportRect(element, strategy)) : isElement(clippingParent) ? getInnerBoundingClientRect(clippingParent, strategy) : rectToClientRect(getDocumentRect(getDocumentElement(element))); + } // A "clipping parent" is an overflowable container with the characteristic of + // clipping (or hiding) overflowing elements with a position different from + // `initial` + + + function getClippingParents(element) { + var clippingParents = listScrollParents(getParentNode(element)); + var canEscapeClipping = ['absolute', 'fixed'].indexOf(getComputedStyle(element).position) >= 0; + var clipperElement = canEscapeClipping && isHTMLElement(element) ? getOffsetParent(element) : element; + + if (!isElement(clipperElement)) { + return []; + } // $FlowFixMe[incompatible-return]: https://github.com/facebook/flow/issues/1414 + + + return clippingParents.filter(function (clippingParent) { + return isElement(clippingParent) && contains(clippingParent, clipperElement) && getNodeName(clippingParent) !== 'body'; + }); + } // Gets the maximum area that the element is visible in due to any number of + // clipping parents + + + function getClippingRect(element, boundary, rootBoundary, strategy) { + var mainClippingParents = boundary === 'clippingParents' ? getClippingParents(element) : [].concat(boundary); + var clippingParents = [].concat(mainClippingParents, [rootBoundary]); + var firstClippingParent = clippingParents[0]; + var clippingRect = clippingParents.reduce(function (accRect, clippingParent) { + var rect = getClientRectFromMixedType(element, clippingParent, strategy); + accRect.top = max(rect.top, accRect.top); + accRect.right = min(rect.right, accRect.right); + accRect.bottom = min(rect.bottom, accRect.bottom); + accRect.left = max(rect.left, accRect.left); + return accRect; + }, getClientRectFromMixedType(element, firstClippingParent, strategy)); + clippingRect.width = clippingRect.right - clippingRect.left; + clippingRect.height = clippingRect.bottom - clippingRect.top; + clippingRect.x = clippingRect.left; + clippingRect.y = clippingRect.top; + return clippingRect; + } + + function getBasePlacement(placement) { + return placement.split('-')[0]; + } + + function getVariation(placement) { + return placement.split('-')[1]; + } + + function getMainAxisFromPlacement(placement) { + return ['top', 'bottom'].indexOf(placement) >= 0 ? 'x' : 'y'; + } + + function computeOffsets(_ref) { + var reference = _ref.reference, + element = _ref.element, + placement = _ref.placement; + var basePlacement = placement ? getBasePlacement(placement) : null; + var variation = placement ? getVariation(placement) : null; + var commonX = reference.x + reference.width / 2 - element.width / 2; + var commonY = reference.y + reference.height / 2 - element.height / 2; + var offsets; + + switch (basePlacement) { + case top: + offsets = { + x: commonX, + y: reference.y - element.height + }; + break; + + case bottom: + offsets = { + x: commonX, + y: reference.y + reference.height + }; + break; + + case right: + offsets = { + x: reference.x + reference.width, + y: commonY + }; + break; + + case left: + offsets = { + x: reference.x - element.width, + y: commonY + }; + break; + + default: + offsets = { + x: reference.x, + y: reference.y + }; + } + + var mainAxis = basePlacement ? getMainAxisFromPlacement(basePlacement) : null; + + if (mainAxis != null) { + var len = mainAxis === 'y' ? 'height' : 'width'; + + switch (variation) { + case start: + offsets[mainAxis] = offsets[mainAxis] - (reference[len] / 2 - element[len] / 2); + break; + + case end: + offsets[mainAxis] = offsets[mainAxis] + (reference[len] / 2 - element[len] / 2); + break; + } + } + + return offsets; + } + + function getFreshSideObject() { + return { + top: 0, + right: 0, + bottom: 0, + left: 0 + }; + } + + function mergePaddingObject(paddingObject) { + return Object.assign({}, getFreshSideObject(), paddingObject); + } + + function expandToHashMap(value, keys) { + return keys.reduce(function (hashMap, key) { + hashMap[key] = value; + return hashMap; + }, {}); + } + + function detectOverflow(state, options) { + if (options === void 0) { + options = {}; + } + + var _options = options, + _options$placement = _options.placement, + placement = _options$placement === void 0 ? state.placement : _options$placement, + _options$strategy = _options.strategy, + strategy = _options$strategy === void 0 ? state.strategy : _options$strategy, + _options$boundary = _options.boundary, + boundary = _options$boundary === void 0 ? clippingParents : _options$boundary, + _options$rootBoundary = _options.rootBoundary, + rootBoundary = _options$rootBoundary === void 0 ? viewport : _options$rootBoundary, + _options$elementConte = _options.elementContext, + elementContext = _options$elementConte === void 0 ? popper : _options$elementConte, + _options$altBoundary = _options.altBoundary, + altBoundary = _options$altBoundary === void 0 ? false : _options$altBoundary, + _options$padding = _options.padding, + padding = _options$padding === void 0 ? 0 : _options$padding; + var paddingObject = mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements)); + var altContext = elementContext === popper ? reference : popper; + var popperRect = state.rects.popper; + var element = state.elements[altBoundary ? altContext : elementContext]; + var clippingClientRect = getClippingRect(isElement(element) ? element : element.contextElement || getDocumentElement(state.elements.popper), boundary, rootBoundary, strategy); + var referenceClientRect = getBoundingClientRect(state.elements.reference); + var popperOffsets = computeOffsets({ + reference: referenceClientRect, + element: popperRect, + strategy: 'absolute', + placement: placement + }); + var popperClientRect = rectToClientRect(Object.assign({}, popperRect, popperOffsets)); + var elementClientRect = elementContext === popper ? popperClientRect : referenceClientRect; // positive = overflowing the clipping rect + // 0 or negative = within the clipping rect + + var overflowOffsets = { + top: clippingClientRect.top - elementClientRect.top + paddingObject.top, + bottom: elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom, + left: clippingClientRect.left - elementClientRect.left + paddingObject.left, + right: elementClientRect.right - clippingClientRect.right + paddingObject.right + }; + var offsetData = state.modifiersData.offset; // Offsets can be applied only to the popper element + + if (elementContext === popper && offsetData) { + var offset = offsetData[placement]; + Object.keys(overflowOffsets).forEach(function (key) { + var multiply = [right, bottom].indexOf(key) >= 0 ? 1 : -1; + var axis = [top, bottom].indexOf(key) >= 0 ? 'y' : 'x'; + overflowOffsets[key] += offset[axis] * multiply; + }); + } + + return overflowOffsets; + } + + var DEFAULT_OPTIONS = { + placement: 'bottom', + modifiers: [], + strategy: 'absolute' + }; + + function areValidElements() { + for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + return !args.some(function (element) { + return !(element && typeof element.getBoundingClientRect === 'function'); + }); + } + + function popperGenerator(generatorOptions) { + if (generatorOptions === void 0) { + generatorOptions = {}; + } + + var _generatorOptions = generatorOptions, + _generatorOptions$def = _generatorOptions.defaultModifiers, + defaultModifiers = _generatorOptions$def === void 0 ? [] : _generatorOptions$def, + _generatorOptions$def2 = _generatorOptions.defaultOptions, + defaultOptions = _generatorOptions$def2 === void 0 ? DEFAULT_OPTIONS : _generatorOptions$def2; + return function createPopper(reference, popper, options) { + if (options === void 0) { + options = defaultOptions; + } + + var state = { + placement: 'bottom', + orderedModifiers: [], + options: Object.assign({}, DEFAULT_OPTIONS, defaultOptions), + modifiersData: {}, + elements: { + reference: reference, + popper: popper + }, + attributes: {}, + styles: {} + }; + var effectCleanupFns = []; + var isDestroyed = false; + var instance = { + state: state, + setOptions: function setOptions(setOptionsAction) { + var options = typeof setOptionsAction === 'function' ? setOptionsAction(state.options) : setOptionsAction; + cleanupModifierEffects(); + state.options = Object.assign({}, defaultOptions, state.options, options); + state.scrollParents = { + reference: isElement(reference) ? listScrollParents(reference) : reference.contextElement ? listScrollParents(reference.contextElement) : [], + popper: listScrollParents(popper) + }; // Orders the modifiers based on their dependencies and `phase` + // properties + + var orderedModifiers = orderModifiers(mergeByName([].concat(defaultModifiers, state.options.modifiers))); // Strip out disabled modifiers + + state.orderedModifiers = orderedModifiers.filter(function (m) { + return m.enabled; + }); + runModifierEffects(); + return instance.update(); + }, + // Sync update – it will always be executed, even if not necessary. This + // is useful for low frequency updates where sync behavior simplifies the + // logic. + // For high frequency updates (e.g. `resize` and `scroll` events), always + // prefer the async Popper#update method + forceUpdate: function forceUpdate() { + if (isDestroyed) { + return; + } + + var _state$elements = state.elements, + reference = _state$elements.reference, + popper = _state$elements.popper; // Don't proceed if `reference` or `popper` are not valid elements + // anymore + + if (!areValidElements(reference, popper)) { + return; + } // Store the reference and popper rects to be read by modifiers + + + state.rects = { + reference: getCompositeRect(reference, getOffsetParent(popper), state.options.strategy === 'fixed'), + popper: getLayoutRect(popper) + }; // Modifiers have the ability to reset the current update cycle. The + // most common use case for this is the `flip` modifier changing the + // placement, which then needs to re-run all the modifiers, because the + // logic was previously ran for the previous placement and is therefore + // stale/incorrect + + state.reset = false; + state.placement = state.options.placement; // On each update cycle, the `modifiersData` property for each modifier + // is filled with the initial data specified by the modifier. This means + // it doesn't persist and is fresh on each update. + // To ensure persistent data, use `${name}#persistent` + + state.orderedModifiers.forEach(function (modifier) { + return state.modifiersData[modifier.name] = Object.assign({}, modifier.data); + }); + + for (var index = 0; index < state.orderedModifiers.length; index++) { + if (state.reset === true) { + state.reset = false; + index = -1; + continue; + } + + var _state$orderedModifie = state.orderedModifiers[index], + fn = _state$orderedModifie.fn, + _state$orderedModifie2 = _state$orderedModifie.options, + _options = _state$orderedModifie2 === void 0 ? {} : _state$orderedModifie2, + name = _state$orderedModifie.name; + + if (typeof fn === 'function') { + state = fn({ + state: state, + options: _options, + name: name, + instance: instance + }) || state; + } + } + }, + // Async and optimistically optimized update – it will not be executed if + // not necessary (debounced to run at most once-per-tick) + update: debounce(function () { + return new Promise(function (resolve) { + instance.forceUpdate(); + resolve(state); + }); + }), + destroy: function destroy() { + cleanupModifierEffects(); + isDestroyed = true; + } + }; + + if (!areValidElements(reference, popper)) { + return instance; + } + + instance.setOptions(options).then(function (state) { + if (!isDestroyed && options.onFirstUpdate) { + options.onFirstUpdate(state); + } + }); // Modifiers have the ability to execute arbitrary code before the first + // update cycle runs. They will be executed in the same order as the update + // cycle. This is useful when a modifier adds some persistent data that + // other modifiers need to use, but the modifier is run after the dependent + // one. + + function runModifierEffects() { + state.orderedModifiers.forEach(function (_ref) { + var name = _ref.name, + _ref$options = _ref.options, + options = _ref$options === void 0 ? {} : _ref$options, + effect = _ref.effect; + + if (typeof effect === 'function') { + var cleanupFn = effect({ + state: state, + name: name, + instance: instance, + options: options + }); + + var noopFn = function noopFn() {}; + + effectCleanupFns.push(cleanupFn || noopFn); + } + }); + } + + function cleanupModifierEffects() { + effectCleanupFns.forEach(function (fn) { + return fn(); + }); + effectCleanupFns = []; + } + + return instance; + }; + } + + var passive = { + passive: true + }; + + function effect$2(_ref) { + var state = _ref.state, + instance = _ref.instance, + options = _ref.options; + var _options$scroll = options.scroll, + scroll = _options$scroll === void 0 ? true : _options$scroll, + _options$resize = options.resize, + resize = _options$resize === void 0 ? true : _options$resize; + var window = getWindow(state.elements.popper); + var scrollParents = [].concat(state.scrollParents.reference, state.scrollParents.popper); + + if (scroll) { + scrollParents.forEach(function (scrollParent) { + scrollParent.addEventListener('scroll', instance.update, passive); + }); + } + + if (resize) { + window.addEventListener('resize', instance.update, passive); + } + + return function () { + if (scroll) { + scrollParents.forEach(function (scrollParent) { + scrollParent.removeEventListener('scroll', instance.update, passive); + }); + } + + if (resize) { + window.removeEventListener('resize', instance.update, passive); + } + }; + } // eslint-disable-next-line import/no-unused-modules + + + var eventListeners = { + name: 'eventListeners', + enabled: true, + phase: 'write', + fn: function fn() {}, + effect: effect$2, + data: {} + }; + + function popperOffsets(_ref) { + var state = _ref.state, + name = _ref.name; + // Offsets are the actual position the popper needs to have to be + // properly positioned near its reference element + // This is the most basic placement, and will be adjusted by + // the modifiers in the next step + state.modifiersData[name] = computeOffsets({ + reference: state.rects.reference, + element: state.rects.popper, + strategy: 'absolute', + placement: state.placement + }); + } // eslint-disable-next-line import/no-unused-modules + + + var popperOffsets$1 = { + name: 'popperOffsets', + enabled: true, + phase: 'read', + fn: popperOffsets, + data: {} + }; + + var unsetSides = { + top: 'auto', + right: 'auto', + bottom: 'auto', + left: 'auto' + }; // Round the offsets to the nearest suitable subpixel based on the DPR. + // Zooming can change the DPR, but it seems to report a value that will + // cleanly divide the values into the appropriate subpixels. + + function roundOffsetsByDPR(_ref, win) { + var x = _ref.x, + y = _ref.y; + var dpr = win.devicePixelRatio || 1; + return { + x: round(x * dpr) / dpr || 0, + y: round(y * dpr) / dpr || 0 + }; + } + + function mapToStyles(_ref2) { + var _Object$assign2; + + var popper = _ref2.popper, + popperRect = _ref2.popperRect, + placement = _ref2.placement, + variation = _ref2.variation, + offsets = _ref2.offsets, + position = _ref2.position, + gpuAcceleration = _ref2.gpuAcceleration, + adaptive = _ref2.adaptive, + roundOffsets = _ref2.roundOffsets, + isFixed = _ref2.isFixed; + var _offsets$x = offsets.x, + x = _offsets$x === void 0 ? 0 : _offsets$x, + _offsets$y = offsets.y, + y = _offsets$y === void 0 ? 0 : _offsets$y; + + var _ref3 = typeof roundOffsets === 'function' ? roundOffsets({ + x: x, + y: y + }) : { + x: x, + y: y + }; + + x = _ref3.x; + y = _ref3.y; + var hasX = offsets.hasOwnProperty('x'); + var hasY = offsets.hasOwnProperty('y'); + var sideX = left; + var sideY = top; + var win = window; + + if (adaptive) { + var offsetParent = getOffsetParent(popper); + var heightProp = 'clientHeight'; + var widthProp = 'clientWidth'; + + if (offsetParent === getWindow(popper)) { + offsetParent = getDocumentElement(popper); + + if (getComputedStyle(offsetParent).position !== 'static' && position === 'absolute') { + heightProp = 'scrollHeight'; + widthProp = 'scrollWidth'; + } + } // $FlowFixMe[incompatible-cast]: force type refinement, we compare offsetParent with window above, but Flow doesn't detect it + + + offsetParent = offsetParent; + + if (placement === top || (placement === left || placement === right) && variation === end) { + sideY = bottom; + var offsetY = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.height : // $FlowFixMe[prop-missing] + offsetParent[heightProp]; + y -= offsetY - popperRect.height; + y *= gpuAcceleration ? 1 : -1; + } + + if (placement === left || (placement === top || placement === bottom) && variation === end) { + sideX = right; + var offsetX = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.width : // $FlowFixMe[prop-missing] + offsetParent[widthProp]; + x -= offsetX - popperRect.width; + x *= gpuAcceleration ? 1 : -1; + } + } + + var commonStyles = Object.assign({ + position: position + }, adaptive && unsetSides); + + var _ref4 = roundOffsets === true ? roundOffsetsByDPR({ + x: x, + y: y + }, getWindow(popper)) : { + x: x, + y: y + }; + + x = _ref4.x; + y = _ref4.y; + + if (gpuAcceleration) { + var _Object$assign; + + return Object.assign({}, commonStyles, (_Object$assign = {}, _Object$assign[sideY] = hasY ? '0' : '', _Object$assign[sideX] = hasX ? '0' : '', _Object$assign.transform = (win.devicePixelRatio || 1) <= 1 ? "translate(" + x + "px, " + y + "px)" : "translate3d(" + x + "px, " + y + "px, 0)", _Object$assign)); + } + + return Object.assign({}, commonStyles, (_Object$assign2 = {}, _Object$assign2[sideY] = hasY ? y + "px" : '', _Object$assign2[sideX] = hasX ? x + "px" : '', _Object$assign2.transform = '', _Object$assign2)); + } + + function computeStyles(_ref5) { + var state = _ref5.state, + options = _ref5.options; + var _options$gpuAccelerat = options.gpuAcceleration, + gpuAcceleration = _options$gpuAccelerat === void 0 ? true : _options$gpuAccelerat, + _options$adaptive = options.adaptive, + adaptive = _options$adaptive === void 0 ? true : _options$adaptive, + _options$roundOffsets = options.roundOffsets, + roundOffsets = _options$roundOffsets === void 0 ? true : _options$roundOffsets; + var commonStyles = { + placement: getBasePlacement(state.placement), + variation: getVariation(state.placement), + popper: state.elements.popper, + popperRect: state.rects.popper, + gpuAcceleration: gpuAcceleration, + isFixed: state.options.strategy === 'fixed' + }; + + if (state.modifiersData.popperOffsets != null) { + state.styles.popper = Object.assign({}, state.styles.popper, mapToStyles(Object.assign({}, commonStyles, { + offsets: state.modifiersData.popperOffsets, + position: state.options.strategy, + adaptive: adaptive, + roundOffsets: roundOffsets + }))); + } + + if (state.modifiersData.arrow != null) { + state.styles.arrow = Object.assign({}, state.styles.arrow, mapToStyles(Object.assign({}, commonStyles, { + offsets: state.modifiersData.arrow, + position: 'absolute', + adaptive: false, + roundOffsets: roundOffsets + }))); + } + + state.attributes.popper = Object.assign({}, state.attributes.popper, { + 'data-popper-placement': state.placement + }); + } // eslint-disable-next-line import/no-unused-modules + + + var computeStyles$1 = { + name: 'computeStyles', + enabled: true, + phase: 'beforeWrite', + fn: computeStyles, + data: {} + }; + + // and applies them to the HTMLElements such as popper and arrow + + function applyStyles(_ref) { + var state = _ref.state; + Object.keys(state.elements).forEach(function (name) { + var style = state.styles[name] || {}; + var attributes = state.attributes[name] || {}; + var element = state.elements[name]; // arrow is optional + virtual elements + + if (!isHTMLElement(element) || !getNodeName(element)) { + return; + } // Flow doesn't support to extend this property, but it's the most + // effective way to apply styles to an HTMLElement + // $FlowFixMe[cannot-write] + + + Object.assign(element.style, style); + Object.keys(attributes).forEach(function (name) { + var value = attributes[name]; + + if (value === false) { + element.removeAttribute(name); + } else { + element.setAttribute(name, value === true ? '' : value); + } + }); + }); + } + + function effect$1(_ref2) { + var state = _ref2.state; + var initialStyles = { + popper: { + position: state.options.strategy, + left: '0', + top: '0', + margin: '0' + }, + arrow: { + position: 'absolute' + }, + reference: {} + }; + Object.assign(state.elements.popper.style, initialStyles.popper); + state.styles = initialStyles; + + if (state.elements.arrow) { + Object.assign(state.elements.arrow.style, initialStyles.arrow); + } + + return function () { + Object.keys(state.elements).forEach(function (name) { + var element = state.elements[name]; + var attributes = state.attributes[name] || {}; + var styleProperties = Object.keys(state.styles.hasOwnProperty(name) ? state.styles[name] : initialStyles[name]); // Set all values to an empty string to unset them + + var style = styleProperties.reduce(function (style, property) { + style[property] = ''; + return style; + }, {}); // arrow is optional + virtual elements + + if (!isHTMLElement(element) || !getNodeName(element)) { + return; + } + + Object.assign(element.style, style); + Object.keys(attributes).forEach(function (attribute) { + element.removeAttribute(attribute); + }); + }); + }; + } // eslint-disable-next-line import/no-unused-modules + + + var applyStyles$1 = { + name: 'applyStyles', + enabled: true, + phase: 'write', + fn: applyStyles, + effect: effect$1, + requires: ['computeStyles'] + }; + + function distanceAndSkiddingToXY(placement, rects, offset) { + var basePlacement = getBasePlacement(placement); + var invertDistance = [left, top].indexOf(basePlacement) >= 0 ? -1 : 1; + + var _ref = typeof offset === 'function' ? offset(Object.assign({}, rects, { + placement: placement + })) : offset, + skidding = _ref[0], + distance = _ref[1]; + + skidding = skidding || 0; + distance = (distance || 0) * invertDistance; + return [left, right].indexOf(basePlacement) >= 0 ? { + x: distance, + y: skidding + } : { + x: skidding, + y: distance + }; + } + + function offset(_ref2) { + var state = _ref2.state, + options = _ref2.options, + name = _ref2.name; + var _options$offset = options.offset, + offset = _options$offset === void 0 ? [0, 0] : _options$offset; + var data = placements.reduce(function (acc, placement) { + acc[placement] = distanceAndSkiddingToXY(placement, state.rects, offset); + return acc; + }, {}); + var _data$state$placement = data[state.placement], + x = _data$state$placement.x, + y = _data$state$placement.y; + + if (state.modifiersData.popperOffsets != null) { + state.modifiersData.popperOffsets.x += x; + state.modifiersData.popperOffsets.y += y; + } + + state.modifiersData[name] = data; + } // eslint-disable-next-line import/no-unused-modules + + + var offset$1 = { + name: 'offset', + enabled: true, + phase: 'main', + requires: ['popperOffsets'], + fn: offset + }; + + var hash$1 = { + left: 'right', + right: 'left', + bottom: 'top', + top: 'bottom' + }; + function getOppositePlacement(placement) { + return placement.replace(/left|right|bottom|top/g, function (matched) { + return hash$1[matched]; + }); + } + + var hash = { + start: 'end', + end: 'start' + }; + function getOppositeVariationPlacement(placement) { + return placement.replace(/start|end/g, function (matched) { + return hash[matched]; + }); + } + + function computeAutoPlacement(state, options) { + if (options === void 0) { + options = {}; + } + + var _options = options, + placement = _options.placement, + boundary = _options.boundary, + rootBoundary = _options.rootBoundary, + padding = _options.padding, + flipVariations = _options.flipVariations, + _options$allowedAutoP = _options.allowedAutoPlacements, + allowedAutoPlacements = _options$allowedAutoP === void 0 ? placements : _options$allowedAutoP; + var variation = getVariation(placement); + var placements$1 = variation ? flipVariations ? variationPlacements : variationPlacements.filter(function (placement) { + return getVariation(placement) === variation; + }) : basePlacements; + var allowedPlacements = placements$1.filter(function (placement) { + return allowedAutoPlacements.indexOf(placement) >= 0; + }); + + if (allowedPlacements.length === 0) { + allowedPlacements = placements$1; + } // $FlowFixMe[incompatible-type]: Flow seems to have problems with two array unions... + + + var overflows = allowedPlacements.reduce(function (acc, placement) { + acc[placement] = detectOverflow(state, { + placement: placement, + boundary: boundary, + rootBoundary: rootBoundary, + padding: padding + })[getBasePlacement(placement)]; + return acc; + }, {}); + return Object.keys(overflows).sort(function (a, b) { + return overflows[a] - overflows[b]; + }); + } + + function getExpandedFallbackPlacements(placement) { + if (getBasePlacement(placement) === auto) { + return []; + } + + var oppositePlacement = getOppositePlacement(placement); + return [getOppositeVariationPlacement(placement), oppositePlacement, getOppositeVariationPlacement(oppositePlacement)]; + } + + function flip(_ref) { + var state = _ref.state, + options = _ref.options, + name = _ref.name; + + if (state.modifiersData[name]._skip) { + return; + } + + var _options$mainAxis = options.mainAxis, + checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis, + _options$altAxis = options.altAxis, + checkAltAxis = _options$altAxis === void 0 ? true : _options$altAxis, + specifiedFallbackPlacements = options.fallbackPlacements, + padding = options.padding, + boundary = options.boundary, + rootBoundary = options.rootBoundary, + altBoundary = options.altBoundary, + _options$flipVariatio = options.flipVariations, + flipVariations = _options$flipVariatio === void 0 ? true : _options$flipVariatio, + allowedAutoPlacements = options.allowedAutoPlacements; + var preferredPlacement = state.options.placement; + var basePlacement = getBasePlacement(preferredPlacement); + var isBasePlacement = basePlacement === preferredPlacement; + var fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipVariations ? [getOppositePlacement(preferredPlacement)] : getExpandedFallbackPlacements(preferredPlacement)); + var placements = [preferredPlacement].concat(fallbackPlacements).reduce(function (acc, placement) { + return acc.concat(getBasePlacement(placement) === auto ? computeAutoPlacement(state, { + placement: placement, + boundary: boundary, + rootBoundary: rootBoundary, + padding: padding, + flipVariations: flipVariations, + allowedAutoPlacements: allowedAutoPlacements + }) : placement); + }, []); + var referenceRect = state.rects.reference; + var popperRect = state.rects.popper; + var checksMap = new Map(); + var makeFallbackChecks = true; + var firstFittingPlacement = placements[0]; + + for (var i = 0; i < placements.length; i++) { + var placement = placements[i]; + + var _basePlacement = getBasePlacement(placement); + + var isStartVariation = getVariation(placement) === start; + var isVertical = [top, bottom].indexOf(_basePlacement) >= 0; + var len = isVertical ? 'width' : 'height'; + var overflow = detectOverflow(state, { + placement: placement, + boundary: boundary, + rootBoundary: rootBoundary, + altBoundary: altBoundary, + padding: padding + }); + var mainVariationSide = isVertical ? isStartVariation ? right : left : isStartVariation ? bottom : top; + + if (referenceRect[len] > popperRect[len]) { + mainVariationSide = getOppositePlacement(mainVariationSide); + } + + var altVariationSide = getOppositePlacement(mainVariationSide); + var checks = []; + + if (checkMainAxis) { + checks.push(overflow[_basePlacement] <= 0); + } + + if (checkAltAxis) { + checks.push(overflow[mainVariationSide] <= 0, overflow[altVariationSide] <= 0); + } + + if (checks.every(function (check) { + return check; + })) { + firstFittingPlacement = placement; + makeFallbackChecks = false; + break; + } + + checksMap.set(placement, checks); + } + + if (makeFallbackChecks) { + // `2` may be desired in some cases – research later + var numberOfChecks = flipVariations ? 3 : 1; + + var _loop = function _loop(_i) { + var fittingPlacement = placements.find(function (placement) { + var checks = checksMap.get(placement); + + if (checks) { + return checks.slice(0, _i).every(function (check) { + return check; + }); + } + }); + + if (fittingPlacement) { + firstFittingPlacement = fittingPlacement; + return "break"; + } + }; + + for (var _i = numberOfChecks; _i > 0; _i--) { + var _ret = _loop(_i); + + if (_ret === "break") break; + } + } + + if (state.placement !== firstFittingPlacement) { + state.modifiersData[name]._skip = true; + state.placement = firstFittingPlacement; + state.reset = true; + } + } // eslint-disable-next-line import/no-unused-modules + + + var flip$1 = { + name: 'flip', + enabled: true, + phase: 'main', + fn: flip, + requiresIfExists: ['offset'], + data: { + _skip: false + } + }; + + function getAltAxis(axis) { + return axis === 'x' ? 'y' : 'x'; + } + + function within(min$1, value, max$1) { + return max(min$1, min(value, max$1)); + } + function withinMaxClamp(min, value, max) { + var v = within(min, value, max); + return v > max ? max : v; + } + + function preventOverflow(_ref) { + var state = _ref.state, + options = _ref.options, + name = _ref.name; + var _options$mainAxis = options.mainAxis, + checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis, + _options$altAxis = options.altAxis, + checkAltAxis = _options$altAxis === void 0 ? false : _options$altAxis, + boundary = options.boundary, + rootBoundary = options.rootBoundary, + altBoundary = options.altBoundary, + padding = options.padding, + _options$tether = options.tether, + tether = _options$tether === void 0 ? true : _options$tether, + _options$tetherOffset = options.tetherOffset, + tetherOffset = _options$tetherOffset === void 0 ? 0 : _options$tetherOffset; + var overflow = detectOverflow(state, { + boundary: boundary, + rootBoundary: rootBoundary, + padding: padding, + altBoundary: altBoundary + }); + var basePlacement = getBasePlacement(state.placement); + var variation = getVariation(state.placement); + var isBasePlacement = !variation; + var mainAxis = getMainAxisFromPlacement(basePlacement); + var altAxis = getAltAxis(mainAxis); + var popperOffsets = state.modifiersData.popperOffsets; + var referenceRect = state.rects.reference; + var popperRect = state.rects.popper; + var tetherOffsetValue = typeof tetherOffset === 'function' ? tetherOffset(Object.assign({}, state.rects, { + placement: state.placement + })) : tetherOffset; + var normalizedTetherOffsetValue = typeof tetherOffsetValue === 'number' ? { + mainAxis: tetherOffsetValue, + altAxis: tetherOffsetValue + } : Object.assign({ + mainAxis: 0, + altAxis: 0 + }, tetherOffsetValue); + var offsetModifierState = state.modifiersData.offset ? state.modifiersData.offset[state.placement] : null; + var data = { + x: 0, + y: 0 + }; + + if (!popperOffsets) { + return; + } + + if (checkMainAxis) { + var _offsetModifierState$; + + var mainSide = mainAxis === 'y' ? top : left; + var altSide = mainAxis === 'y' ? bottom : right; + var len = mainAxis === 'y' ? 'height' : 'width'; + var offset = popperOffsets[mainAxis]; + var min$1 = offset + overflow[mainSide]; + var max$1 = offset - overflow[altSide]; + var additive = tether ? -popperRect[len] / 2 : 0; + var minLen = variation === start ? referenceRect[len] : popperRect[len]; + var maxLen = variation === start ? -popperRect[len] : -referenceRect[len]; // We need to include the arrow in the calculation so the arrow doesn't go + // outside the reference bounds + + var arrowElement = state.elements.arrow; + var arrowRect = tether && arrowElement ? getLayoutRect(arrowElement) : { + width: 0, + height: 0 + }; + var arrowPaddingObject = state.modifiersData['arrow#persistent'] ? state.modifiersData['arrow#persistent'].padding : getFreshSideObject(); + var arrowPaddingMin = arrowPaddingObject[mainSide]; + var arrowPaddingMax = arrowPaddingObject[altSide]; // If the reference length is smaller than the arrow length, we don't want + // to include its full size in the calculation. If the reference is small + // and near the edge of a boundary, the popper can overflow even if the + // reference is not overflowing as well (e.g. virtual elements with no + // width or height) + + var arrowLen = within(0, referenceRect[len], arrowRect[len]); + var minOffset = isBasePlacement ? referenceRect[len] / 2 - additive - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis : minLen - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis; + var maxOffset = isBasePlacement ? -referenceRect[len] / 2 + additive + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis : maxLen + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis; + var arrowOffsetParent = state.elements.arrow && getOffsetParent(state.elements.arrow); + var clientOffset = arrowOffsetParent ? mainAxis === 'y' ? arrowOffsetParent.clientTop || 0 : arrowOffsetParent.clientLeft || 0 : 0; + var offsetModifierValue = (_offsetModifierState$ = offsetModifierState == null ? void 0 : offsetModifierState[mainAxis]) != null ? _offsetModifierState$ : 0; + var tetherMin = offset + minOffset - offsetModifierValue - clientOffset; + var tetherMax = offset + maxOffset - offsetModifierValue; + var preventedOffset = within(tether ? min(min$1, tetherMin) : min$1, offset, tether ? max(max$1, tetherMax) : max$1); + popperOffsets[mainAxis] = preventedOffset; + data[mainAxis] = preventedOffset - offset; + } + + if (checkAltAxis) { + var _offsetModifierState$2; + + var _mainSide = mainAxis === 'x' ? top : left; + + var _altSide = mainAxis === 'x' ? bottom : right; + + var _offset = popperOffsets[altAxis]; + + var _len = altAxis === 'y' ? 'height' : 'width'; + + var _min = _offset + overflow[_mainSide]; + + var _max = _offset - overflow[_altSide]; + + var isOriginSide = [top, left].indexOf(basePlacement) !== -1; + + var _offsetModifierValue = (_offsetModifierState$2 = offsetModifierState == null ? void 0 : offsetModifierState[altAxis]) != null ? _offsetModifierState$2 : 0; + + var _tetherMin = isOriginSide ? _min : _offset - referenceRect[_len] - popperRect[_len] - _offsetModifierValue + normalizedTetherOffsetValue.altAxis; + + var _tetherMax = isOriginSide ? _offset + referenceRect[_len] + popperRect[_len] - _offsetModifierValue - normalizedTetherOffsetValue.altAxis : _max; + + var _preventedOffset = tether && isOriginSide ? withinMaxClamp(_tetherMin, _offset, _tetherMax) : within(tether ? _tetherMin : _min, _offset, tether ? _tetherMax : _max); + + popperOffsets[altAxis] = _preventedOffset; + data[altAxis] = _preventedOffset - _offset; + } + + state.modifiersData[name] = data; + } // eslint-disable-next-line import/no-unused-modules + + + var preventOverflow$1 = { + name: 'preventOverflow', + enabled: true, + phase: 'main', + fn: preventOverflow, + requiresIfExists: ['offset'] + }; + + var toPaddingObject = function toPaddingObject(padding, state) { + padding = typeof padding === 'function' ? padding(Object.assign({}, state.rects, { + placement: state.placement + })) : padding; + return mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements)); + }; + + function arrow(_ref) { + var _state$modifiersData$; + + var state = _ref.state, + name = _ref.name, + options = _ref.options; + var arrowElement = state.elements.arrow; + var popperOffsets = state.modifiersData.popperOffsets; + var basePlacement = getBasePlacement(state.placement); + var axis = getMainAxisFromPlacement(basePlacement); + var isVertical = [left, right].indexOf(basePlacement) >= 0; + var len = isVertical ? 'height' : 'width'; + + if (!arrowElement || !popperOffsets) { + return; + } + + var paddingObject = toPaddingObject(options.padding, state); + var arrowRect = getLayoutRect(arrowElement); + var minProp = axis === 'y' ? top : left; + var maxProp = axis === 'y' ? bottom : right; + var endDiff = state.rects.reference[len] + state.rects.reference[axis] - popperOffsets[axis] - state.rects.popper[len]; + var startDiff = popperOffsets[axis] - state.rects.reference[axis]; + var arrowOffsetParent = getOffsetParent(arrowElement); + var clientSize = arrowOffsetParent ? axis === 'y' ? arrowOffsetParent.clientHeight || 0 : arrowOffsetParent.clientWidth || 0 : 0; + var centerToReference = endDiff / 2 - startDiff / 2; // Make sure the arrow doesn't overflow the popper if the center point is + // outside of the popper bounds + + var min = paddingObject[minProp]; + var max = clientSize - arrowRect[len] - paddingObject[maxProp]; + var center = clientSize / 2 - arrowRect[len] / 2 + centerToReference; + var offset = within(min, center, max); // Prevents breaking syntax highlighting... + + var axisProp = axis; + state.modifiersData[name] = (_state$modifiersData$ = {}, _state$modifiersData$[axisProp] = offset, _state$modifiersData$.centerOffset = offset - center, _state$modifiersData$); + } + + function effect(_ref2) { + var state = _ref2.state, + options = _ref2.options; + var _options$element = options.element, + arrowElement = _options$element === void 0 ? '[data-popper-arrow]' : _options$element; + + if (arrowElement == null) { + return; + } // CSS selector + + + if (typeof arrowElement === 'string') { + arrowElement = state.elements.popper.querySelector(arrowElement); + + if (!arrowElement) { + return; + } + } + + if (!contains(state.elements.popper, arrowElement)) { + return; + } + + state.elements.arrow = arrowElement; + } // eslint-disable-next-line import/no-unused-modules + + + var arrow$1 = { + name: 'arrow', + enabled: true, + phase: 'main', + fn: arrow, + effect: effect, + requires: ['popperOffsets'], + requiresIfExists: ['preventOverflow'] + }; + + function getSideOffsets(overflow, rect, preventedOffsets) { + if (preventedOffsets === void 0) { + preventedOffsets = { + x: 0, + y: 0 + }; + } + + return { + top: overflow.top - rect.height - preventedOffsets.y, + right: overflow.right - rect.width + preventedOffsets.x, + bottom: overflow.bottom - rect.height + preventedOffsets.y, + left: overflow.left - rect.width - preventedOffsets.x + }; + } + + function isAnySideFullyClipped(overflow) { + return [top, right, bottom, left].some(function (side) { + return overflow[side] >= 0; + }); + } + + function hide(_ref) { + var state = _ref.state, + name = _ref.name; + var referenceRect = state.rects.reference; + var popperRect = state.rects.popper; + var preventedOffsets = state.modifiersData.preventOverflow; + var referenceOverflow = detectOverflow(state, { + elementContext: 'reference' + }); + var popperAltOverflow = detectOverflow(state, { + altBoundary: true + }); + var referenceClippingOffsets = getSideOffsets(referenceOverflow, referenceRect); + var popperEscapeOffsets = getSideOffsets(popperAltOverflow, popperRect, preventedOffsets); + var isReferenceHidden = isAnySideFullyClipped(referenceClippingOffsets); + var hasPopperEscaped = isAnySideFullyClipped(popperEscapeOffsets); + state.modifiersData[name] = { + referenceClippingOffsets: referenceClippingOffsets, + popperEscapeOffsets: popperEscapeOffsets, + isReferenceHidden: isReferenceHidden, + hasPopperEscaped: hasPopperEscaped + }; + state.attributes.popper = Object.assign({}, state.attributes.popper, { + 'data-popper-reference-hidden': isReferenceHidden, + 'data-popper-escaped': hasPopperEscaped + }); + } // eslint-disable-next-line import/no-unused-modules + + + var hide$1 = { + name: 'hide', + enabled: true, + phase: 'main', + requiresIfExists: ['preventOverflow'], + fn: hide + }; + + var defaultModifiers$1 = [eventListeners, popperOffsets$1, computeStyles$1, applyStyles$1]; + var createPopper$1 = /*#__PURE__*/popperGenerator({ + defaultModifiers: defaultModifiers$1 + }); // eslint-disable-next-line import/no-unused-modules + + var defaultModifiers = [eventListeners, popperOffsets$1, computeStyles$1, applyStyles$1, offset$1, flip$1, preventOverflow$1, arrow$1, hide$1]; + var createPopper = /*#__PURE__*/popperGenerator({ + defaultModifiers: defaultModifiers + }); // eslint-disable-next-line import/no-unused-modules + + exports.applyStyles = applyStyles$1; + exports.arrow = arrow$1; + exports.computeStyles = computeStyles$1; + exports.createPopper = createPopper; + exports.createPopperLite = createPopper$1; + exports.defaultModifiers = defaultModifiers; + exports.detectOverflow = detectOverflow; + exports.eventListeners = eventListeners; + exports.flip = flip$1; + exports.hide = hide$1; + exports.offset = offset$1; + exports.popperGenerator = popperGenerator; + exports.popperOffsets = popperOffsets$1; + exports.preventOverflow = preventOverflow$1; + + Object.defineProperty(exports, '__esModule', { value: true }); + +}))); +//# sourceMappingURL=popper.js.map diff --git a/basicswap/static/js/libs/tippy.js b/basicswap/static/js/libs/tippy.js new file mode 100644 index 0000000..5f58c28 --- /dev/null +++ b/basicswap/static/js/libs/tippy.js @@ -0,0 +1,2516 @@ +/**! +* tippy.js v6.3.7 +* (c) 2017-2021 atomiks +* MIT License +*/ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('@popperjs/core')) : + typeof define === 'function' && define.amd ? define(['@popperjs/core'], factory) : + (global = global || self, global.tippy = factory(global.Popper)); +}(this, (function (core) { 'use strict'; + + var css = ".tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{position:relative;background-color:#333;color:#fff;border-radius:4px;font-size:14px;line-height:1.4;white-space:normal;outline:0;transition-property:transform,visibility,opacity}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{bottom:-7px;left:0;border-width:8px 8px 0;border-top-color:initial;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-7px;left:0;border-width:0 8px 8px;border-bottom-color:initial;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-width:8px 0 8px 8px;border-left-color:initial;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-7px;border-width:8px 8px 8px 0;border-right-color:initial;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{width:16px;height:16px;color:#333}.tippy-arrow:before{content:\"\";position:absolute;border-color:transparent;border-style:solid}.tippy-content{position:relative;padding:5px 9px;z-index:1}"; + + function injectCSS(css) { + var style = document.createElement('style'); + style.textContent = css; + style.setAttribute('data-tippy-stylesheet', ''); + var head = document.head; + var firstStyleOrLinkTag = document.querySelector('head>style,head>link'); + + if (firstStyleOrLinkTag) { + head.insertBefore(style, firstStyleOrLinkTag); + } else { + head.appendChild(style); + } + } + + var isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined'; + var isIE11 = isBrowser ? // @ts-ignore + !!window.msCrypto : false; + + var ROUND_ARROW = ''; + var BOX_CLASS = "tippy-box"; + var CONTENT_CLASS = "tippy-content"; + var BACKDROP_CLASS = "tippy-backdrop"; + var ARROW_CLASS = "tippy-arrow"; + var SVG_ARROW_CLASS = "tippy-svg-arrow"; + var TOUCH_OPTIONS = { + passive: true, + capture: true + }; + var TIPPY_DEFAULT_APPEND_TO = function TIPPY_DEFAULT_APPEND_TO() { + return document.body; + }; + + function hasOwnProperty(obj, key) { + return {}.hasOwnProperty.call(obj, key); + } + function getValueAtIndexOrReturn(value, index, defaultValue) { + if (Array.isArray(value)) { + var v = value[index]; + return v == null ? Array.isArray(defaultValue) ? defaultValue[index] : defaultValue : v; + } + + return value; + } + function isType(value, type) { + var str = {}.toString.call(value); + return str.indexOf('[object') === 0 && str.indexOf(type + "]") > -1; + } + function invokeWithArgsOrReturn(value, args) { + return typeof value === 'function' ? value.apply(void 0, args) : value; + } + function debounce(fn, ms) { + // Avoid wrapping in `setTimeout` if ms is 0 anyway + if (ms === 0) { + return fn; + } + + var timeout; + return function (arg) { + clearTimeout(timeout); + timeout = setTimeout(function () { + fn(arg); + }, ms); + }; + } + function removeProperties(obj, keys) { + var clone = Object.assign({}, obj); + keys.forEach(function (key) { + delete clone[key]; + }); + return clone; + } + function splitBySpaces(value) { + return value.split(/\s+/).filter(Boolean); + } + function normalizeToArray(value) { + return [].concat(value); + } + function pushIfUnique(arr, value) { + if (arr.indexOf(value) === -1) { + arr.push(value); + } + } + function unique(arr) { + return arr.filter(function (item, index) { + return arr.indexOf(item) === index; + }); + } + function getBasePlacement(placement) { + return placement.split('-')[0]; + } + function arrayFrom(value) { + return [].slice.call(value); + } + function removeUndefinedProps(obj) { + return Object.keys(obj).reduce(function (acc, key) { + if (obj[key] !== undefined) { + acc[key] = obj[key]; + } + + return acc; + }, {}); + } + + function div() { + return document.createElement('div'); + } + function isElement(value) { + return ['Element', 'Fragment'].some(function (type) { + return isType(value, type); + }); + } + function isNodeList(value) { + return isType(value, 'NodeList'); + } + function isMouseEvent(value) { + return isType(value, 'MouseEvent'); + } + function isReferenceElement(value) { + return !!(value && value._tippy && value._tippy.reference === value); + } + function getArrayOfElements(value) { + if (isElement(value)) { + return [value]; + } + + if (isNodeList(value)) { + return arrayFrom(value); + } + + if (Array.isArray(value)) { + return value; + } + + return arrayFrom(document.querySelectorAll(value)); + } + function setTransitionDuration(els, value) { + els.forEach(function (el) { + if (el) { + el.style.transitionDuration = value + "ms"; + } + }); + } + function setVisibilityState(els, state) { + els.forEach(function (el) { + if (el) { + el.setAttribute('data-state', state); + } + }); + } + function getOwnerDocument(elementOrElements) { + var _element$ownerDocumen; + + var _normalizeToArray = normalizeToArray(elementOrElements), + element = _normalizeToArray[0]; // Elements created via a