diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index ccbbb09..e48c508 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -355,7 +355,13 @@ class BasicSwap(BaseApp): # TODO: Set dynamically self.balance_only_coins = (Coins.LTC_MWEB,) - self.scriptless_coins = (Coins.XMR, Coins.WOW, Coins.PART_ANON, Coins.FIRO) + self.scriptless_coins = ( + Coins.XMR, + Coins.WOW, + Coins.PART_ANON, + Coins.FIRO, + Coins.DOGE, + ) self.adaptor_swap_only_coins = self.scriptless_coins + ( Coins.PART_BLIND, Coins.BCH, @@ -822,6 +828,10 @@ class BasicSwap(BaseApp): self.coin_clients[coin], self.chain, self ) return interface + elif coin == Coins.DOGE: + from .interface.doge import DOGEInterface + + return DOGEInterface(self.coin_clients[coin], self.chain, self) elif coin == Coins.DCR: from .interface.dcr import DCRInterface @@ -882,6 +892,7 @@ class BasicSwap(BaseApp): if cc["name"] in ( "bitcoin", "litecoin", + "dogecoin", "namecoin", "dash", "firo", diff --git a/basicswap/bin/prepare.py b/basicswap/bin/prepare.py index e07ad67..f1f3218 100755 --- a/basicswap/bin/prepare.py +++ b/basicswap/bin/prepare.py @@ -56,9 +56,6 @@ LITECOIN_VERSION_TAG = os.getenv("LITECOIN_VERSION_TAG", "") BITCOIN_VERSION = os.getenv("BITCOIN_VERSION", "26.0") BITCOIN_VERSION_TAG = os.getenv("BITCOIN_VERSION_TAG", "") -BITCOINCASH_VERSION = os.getenv("BITCOINCASH_VERSION", "27.1.0") -BITCOINCASH_VERSION_TAG = os.getenv("BITCOINCASH_VERSION_TAG", "") - MONERO_VERSION = os.getenv("MONERO_VERSION", "0.18.3.4") MONERO_VERSION_TAG = os.getenv("MONERO_VERSION_TAG", "") XMR_SITE_COMMIT = ( @@ -86,6 +83,12 @@ NAV_VERSION_TAG = os.getenv("NAV_VERSION_TAG", "") DCR_VERSION = os.getenv("DCR_VERSION", "1.8.1") DCR_VERSION_TAG = os.getenv("DCR_VERSION_TAG", "") +BITCOINCASH_VERSION = os.getenv("BITCOINCASH_VERSION", "27.1.0") +BITCOINCASH_VERSION_TAG = os.getenv("BITCOINCASH_VERSION_TAG", "") + +DOGECOIN_VERSION = os.getenv("DOGECOIN_VERSION", "23.2.1") +DOGECOIN_VERSION_TAG = os.getenv("DOGECOIN_VERSION_TAG", "") + GUIX_SSL_CERT_DIR = None ADD_PUBKEY_URL = os.getenv("ADD_PUBKEY_URL", "") @@ -98,7 +101,6 @@ SKIP_GPG_VALIDATION = toBool(os.getenv("SKIP_GPG_VALIDATION", "false")) known_coins = { "particl": (PARTICL_VERSION, PARTICL_VERSION_TAG, ("tecnovert",)), "bitcoin": (BITCOIN_VERSION, BITCOIN_VERSION_TAG, ("laanwj",)), - "bitcoincash": (BITCOINCASH_VERSION, BITCOINCASH_VERSION_TAG, ("Calin_Culianu",)), "litecoin": (LITECOIN_VERSION, LITECOIN_VERSION_TAG, ("davidburkett38",)), "decred": (DCR_VERSION, DCR_VERSION_TAG, ("decred_release",)), "namecoin": ("0.18.0", "", ("JeremyRand",)), @@ -108,6 +110,8 @@ known_coins = { "dash": (DASH_VERSION, DASH_VERSION_TAG, ("pasta",)), "firo": (FIRO_VERSION, FIRO_VERSION_TAG, ("reuben",)), "navcoin": (NAV_VERSION, NAV_VERSION_TAG, ("nav_builder",)), + "bitcoincash": (BITCOINCASH_VERSION, BITCOINCASH_VERSION_TAG, ("Calin_Culianu",)), + "dogecoin": (DOGECOIN_VERSION, DOGECOIN_VERSION_TAG, ("tecnovert",)), } disabled_coins = [ @@ -123,6 +127,8 @@ expected_key_ids = { "binaryfate": ("F0AF4D462A0BDF92",), "wowario": ("793504B449C69220",), "davidburkett38": ("3620E9D387E55666",), + "xanimo": ("6E8F17C1B1BCDCBE",), + "patricklodder": ("2D3A345B98D0DC1F",), "fuzzbawls": ("C1ABA64407731FD9",), "pasta": ("52527BEDABE87984",), "reuben": ("1290A1D0FA7EE109",), @@ -203,13 +209,6 @@ 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", "") -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_RPC_USER = os.getenv("BCH_RPC_USER", "") -BCH_RPC_PWD = os.getenv("BCH_RPC_PWD", "") - DCR_RPC_HOST = os.getenv("DCR_RPC_HOST", "127.0.0.1") DCR_RPC_PORT = int(os.getenv("DCR_RPC_PORT", 9109)) DCR_WALLET_RPC_HOST = os.getenv("DCR_WALLET_RPC_HOST", "127.0.0.1") @@ -247,6 +246,19 @@ NAV_ONION_PORT = int(os.getenv("NAV_ONION_PORT", 8334)) # TODO? NAV_RPC_USER = os.getenv("NAV_RPC_USER", "") 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_RPC_USER = os.getenv("BCH_RPC_USER", "") +BCH_RPC_PWD = os.getenv("BCH_RPC_PWD", "") + +DOGE_RPC_HOST = os.getenv("DOGE_RPC_HOST", "127.0.0.1") +DOGE_RPC_PORT = int(os.getenv("DOGE_RPC_PORT", 42069)) +DOGE_ONION_PORT = int(os.getenv("DOGE_ONION_PORT", 6969)) +DOGE_RPC_USER = os.getenv("DOGE_RPC_USER", "") +DOGE_RPC_PWD = os.getenv("DOGE_RPC_PWD", "") + TOR_PROXY_HOST = os.getenv("TOR_PROXY_HOST", "127.0.0.1") TOR_PROXY_PORT = int(os.getenv("TOR_PROXY_PORT", 9050)) TOR_CONTROL_PORT = int(os.getenv("TOR_CONTROL_PORT", 9051)) @@ -802,6 +814,15 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}): "https://raw.githubusercontent.com/litecoin-project/gitian.sigs.ltc/master/%s-%s/%s/%s" % (version, os_dir_name, signing_key_name, assert_filename) ) + elif coin == "dogecoin": + release_url = ( + "https://github.com/tecnovert/dogecoin/releases/download/v{}/{}".format( + version + version_tag, release_filename + ) + ) + assert_filename = "{}-{}-{}-build.assert".format(coin, os_name, version) + assert_url = f"https://raw.githubusercontent.com/tecnovert/guix.sigs/dogecoin/{version}/{signing_key_name}/noncodesigned.SHA256SUMS" + elif coin == "bitcoin": release_url = "https://bitcoincore.org/bin/bitcoin-core-{}/{}".format( version, release_filename @@ -973,6 +994,8 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}): pubkey_filename = "{}_builder.pgp".format(coin) elif coin in ("decred",): pubkey_filename = "{}_release.pgp".format(coin) + elif coin in ("dogecoin",): + pubkey_filename = "particl_{}.pgp".format(signing_key_name) else: pubkey_filename = "{}_{}.pgp".format(coin, signing_key_name) pubkeyurls = [ @@ -1276,6 +1299,14 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}): LTC_RPC_USER, salt, password_to_hmac(salt, LTC_RPC_PWD) ) ) + elif coin == "dogecoin": + fp.write("prune=4000\n") + if DOGE_RPC_USER != "": + fp.write( + "rpcauth={}:{}${}\n".format( + DOGE_RPC_USER, salt, password_to_hmac(salt, DOGE_RPC_PWD) + ) + ) elif coin == "bitcoin": fp.write("deprecatedrpc=create_bdb\n") fp.write("prune=2000\n") @@ -1485,6 +1516,8 @@ def modify_tor_config( default_onionport = PART_ONION_PORT elif coin == "litecoin": default_onionport = LTC_ONION_PORT + elif coin == "dogecoin": + default_onionport = DOGE_ONION_PORT elif coin in ("decred",): pass else: @@ -1697,6 +1730,7 @@ def initialise_wallets( Coins.PART, Coins.BTC, Coins.LTC, + Coins.DOGE, Coins.DCR, Coins.DASH, ) @@ -1803,7 +1837,7 @@ def initialise_wallets( "Creating wallet.dat for {}.".format(getCoinName(c)) ) - if c in (Coins.BTC, Coins.LTC, Coins.DASH): + if c in (Coins.BTC, Coins.LTC, Coins.DOGE, Coins.DASH): # wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors swap_client.callcoinrpc( c, @@ -2256,6 +2290,21 @@ def main(): "core_version_group": 21, "min_relay_fee": 0.00001, }, + "dogecoin": { + "connection_type": "rpc", + "manage_daemon": shouldManageDaemon("DOGE"), + "rpchost": DOGE_RPC_HOST, + "rpcport": DOGE_RPC_PORT + port_offset, + "onionport": DOGE_ONION_PORT + port_offset, + "datadir": os.getenv("DOGE_DATA_DIR", os.path.join(data_dir, "dogecoin")), + "bindir": os.path.join(bin_dir, "dogecoin"), + "use_segwit": False, + "use_csv": False, + "blocks_confirmed": 2, + "conf_target": 2, + "core_version_group": 23, + "min_relay_fee": 0.01, # RECOMMENDED_MIN_TX_FEE + }, "decred": { "connection_type": "rpc", "manage_daemon": shouldManageDaemon("DCR"), @@ -2403,6 +2452,9 @@ def main(): if LTC_RPC_USER != "": chainclients["litecoin"]["rpcuser"] = LTC_RPC_USER chainclients["litecoin"]["rpcpassword"] = LTC_RPC_PWD + if DOGE_RPC_USER != "": + chainclients["dogecoin"]["rpcuser"] = DOGE_RPC_USER + chainclients["dogecoin"]["rpcpassword"] = DOGE_RPC_PWD if BTC_RPC_USER != "": chainclients["bitcoin"]["rpcuser"] = BTC_RPC_USER chainclients["bitcoin"]["rpcpassword"] = BTC_RPC_PWD diff --git a/basicswap/chainparams.py b/basicswap/chainparams.py index c1ce0ef..3f3eca4 100644 --- a/basicswap/chainparams.py +++ b/basicswap/chainparams.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Copyright (c) 2019-2024 tecnovert +# Copyright (c) 2024 The Basicswap developers # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -31,6 +32,7 @@ class Coins(IntEnum): LTC_MWEB = 15 # ZANO = 16 BCH = 17 + DOGE = 18 chainparams = { @@ -153,6 +155,44 @@ chainparams = { "max_amount": 10000000 * COIN, }, }, + Coins.DOGE: { + "name": "dogecoin", + "ticker": "DOGE", + "message_magic": "Dogecoin Signed Message:\n", + "blocks_target": 60 * 1, + "decimal_places": 8, + "mainnet": { + "rpcport": 22555, + "pubkey_address": 30, + "script_address": 22, + "key_prefix": 158, + "hrp": "doge", + "bip44": 3, + "min_amount": 100000, # TODO increase above fee + "max_amount": 10000000 * COIN, + }, + "testnet": { + "rpcport": 44555, + "pubkey_address": 113, + "script_address": 196, + "key_prefix": 241, + "hrp": "tdge", + "bip44": 1, + "min_amount": 100000, + "max_amount": 10000000 * COIN, + "name": "testnet4", + }, + "regtest": { + "rpcport": 18332, + "pubkey_address": 111, + "script_address": 196, + "key_prefix": 239, + "hrp": "rdge", + "bip44": 1, + "min_amount": 100000, + "max_amount": 10000000 * COIN, + }, + }, Coins.DCR: { "name": "decred", "ticker": "DCR", diff --git a/basicswap/config.py b/basicswap/config.py index 2ee7648..9c8d06d 100644 --- a/basicswap/config.py +++ b/basicswap/config.py @@ -36,6 +36,10 @@ LITECOIND = os.getenv("LITECOIND", "litecoind" + bin_suffix) LITECOIN_CLI = os.getenv("LITECOIN_CLI", "litecoin-cli" + bin_suffix) LITECOIN_TX = os.getenv("LITECOIN_TX", "litecoin-tx" + bin_suffix) +DOGECOIND = os.getenv("DOGECOIND", "dogecoind" + bin_suffix) +DOGECOIN_CLI = os.getenv("DOGECOIN_CLI", "dogecoin-cli" + bin_suffix) +DOGECOIN_TX = os.getenv("DOGECOIN_TX", "dogecoin-tx" + bin_suffix) + NAMECOIN_BINDIR = os.path.expanduser( os.getenv("NAMECOIN_BINDIR", os.path.join(DEFAULT_TEST_BINDIR, "namecoin")) ) diff --git a/basicswap/interface/base.py b/basicswap/interface/base.py index bb1ae63..ec821c2 100644 --- a/basicswap/interface/base.py +++ b/basicswap/interface/base.py @@ -188,7 +188,7 @@ class Secp256k1Interface(CoinInterface, AdaptorSigInterface): def curve_type(): return Curves.secp256k1 - def getNewSecretKey(self) -> bytes: + def getNewRandomKey(self) -> bytes: return i2b(getSecretInt()) def getPubkey(self, privkey: bytes) -> bytes: diff --git a/basicswap/interface/btc.py b/basicswap/interface/btc.py index 3bd7b6c..7f37245 100644 --- a/basicswap/interface/btc.py +++ b/basicswap/interface/btc.py @@ -1296,7 +1296,7 @@ class BTCInterface(Secp256k1Interface): def getWalletTransaction(self, txid: bytes): try: - return bytes.fromhex(self.rpc_wallet("gettransaction", [txid.hex()])) + return bytes.fromhex(self.rpc_wallet("gettransaction", [txid.hex()])["hex"]) except Exception as e: # noqa: F841 # TODO: filter errors return None @@ -1466,7 +1466,6 @@ class BTCInterface(Secp256k1Interface): vout: int = -1, ): # Add watchonly address and rescan if required - if not self.isAddressMine(dest_address, or_watch_only=True): self.importWatchOnlyAddress(dest_address, "bid") self._log.info("Imported watch-only addr: {}".format(dest_address)) diff --git a/basicswap/interface/doge.py b/basicswap/interface/doge.py new file mode 100644 index 0000000..caa5b49 --- /dev/null +++ b/basicswap/interface/doge.py @@ -0,0 +1,58 @@ +#!/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. + +from .btc import BTCInterface +from basicswap.chainparams import Coins +from basicswap.util.crypto import hash160 + +from basicswap.contrib.test_framework.script import ( + CScript, + OP_DUP, + OP_CHECKSIG, + OP_HASH160, + OP_EQUAL, + OP_EQUALVERIFY, +) + + +class DOGEInterface(BTCInterface): + @staticmethod + def coin_type(): + return Coins.DOGE + + @staticmethod + def xmr_swap_b_lock_spend_tx_vsize() -> int: + return 192 + + def __init__(self, coin_settings, network, swap_client=None): + super(DOGEInterface, self).__init__(coin_settings, network, swap_client) + + def getScriptDest(self, script: bytearray) -> bytearray: + # P2SH + + script_hash = hash160(script) + assert len(script_hash) == 20 + + return CScript([OP_HASH160, script_hash, OP_EQUAL]) + + def getScriptForPubkeyHash(self, pkh: bytes) -> bytearray: + # Return P2PKH + return CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG]) + + def encodeScriptDest(self, script_dest: bytes) -> str: + # Extract hash from script + script_hash = script_dest[2:-1] + return self.sh_to_address(script_hash) + + def getBLockSpendTxFee(self, tx, fee_rate: int) -> int: + add_bytes = 107 + size = len(tx.serialize_with_witness()) + add_bytes + pay_fee = round(fee_rate * size / 1000) + self._log.info( + f"BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}." + ) + return pay_fee diff --git a/basicswap/interface/part.py b/basicswap/interface/part.py index 49caad2..894a371 100644 --- a/basicswap/interface/part.py +++ b/basicswap/interface/part.py @@ -240,7 +240,7 @@ class PARTInterfaceBlind(PARTInterface): def createSCLockTx(self, value: int, script: bytearray, vkbv: bytes) -> bytes: # Nonce is derived from vkbv, ephemeral_key isn't used - ephemeral_key = self.getNewSecretKey() + ephemeral_key = self.getNewRandomKey() ephemeral_pubkey = self.getPubkey(ephemeral_key) assert len(ephemeral_pubkey) == 33 nonce = self.getScriptLockTxNonce(vkbv) @@ -307,7 +307,7 @@ class PARTInterfaceBlind(PARTInterface): lock_tx_obj = self.rpc("decoderawtransaction", [tx_lock_bytes.hex()]) assert self.getTxid(tx_lock_bytes).hex() == lock_tx_obj["txid"] # Nonce is derived from vkbv, ephemeral_key isn't used - ephemeral_key = self.getNewSecretKey() + ephemeral_key = self.getNewRandomKey() ephemeral_pubkey = self.getPubkey(ephemeral_key) assert len(ephemeral_pubkey) == 33 nonce = self.getScriptLockTxNonce(vkbv) @@ -348,7 +348,7 @@ class PARTInterfaceBlind(PARTInterface): dummy_witness_stack = [x.hex() for x in dummy_witness_stack] # Use a junk change pubkey to avoid adding unused keys to the wallet - zero_change_key = self.getNewSecretKey() + zero_change_key = self.getNewRandomKey() zero_change_pubkey = self.getPubkey(zero_change_key) inputs_info = { "0": { @@ -428,7 +428,7 @@ class PARTInterfaceBlind(PARTInterface): dummy_witness_stack = [x.hex() for x in dummy_witness_stack] # Use a junk change pubkey to avoid adding unused keys to the wallet - zero_change_key = self.getNewSecretKey() + zero_change_key = self.getNewRandomKey() zero_change_pubkey = self.getPubkey(zero_change_key) inputs_info = { "0": { @@ -745,7 +745,7 @@ class PARTInterfaceBlind(PARTInterface): dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock) # Use a junk change pubkey to avoid adding unused keys to the wallet - zero_change_key = self.getNewSecretKey() + zero_change_key = self.getNewRandomKey() zero_change_pubkey = self.getPubkey(zero_change_key) inputs_info = { "0": { @@ -949,7 +949,7 @@ class PARTInterfaceBlind(PARTInterface): dummy_witness_stack = [x.hex() for x in dummy_witness_stack] # Use a junk change pubkey to avoid adding unused keys to the wallet - zero_change_key = self.getNewSecretKey() + zero_change_key = self.getNewRandomKey() zero_change_pubkey = self.getPubkey(zero_change_key) inputs_info = { "0": { diff --git a/basicswap/interface/xmr.py b/basicswap/interface/xmr.py index 8614822..b947643 100644 --- a/basicswap/interface/xmr.py +++ b/basicswap/interface/xmr.py @@ -326,7 +326,7 @@ class XMRInterface(CoinInterface): return float(self.format_amount(fee_per_k_bytes)), "get_fee_estimate" - def getNewSecretKey(self) -> bytes: + def getNewRandomKey(self) -> bytes: # Note: Returned bytes are in big endian order return i2b(edu.get_secret()) diff --git a/basicswap/static/js/offerstable.js b/basicswap/static/js/offerstable.js index cec0722..db64b3b 100644 --- a/basicswap/static/js/offerstable.js +++ b/basicswap/static/js/offerstable.js @@ -12,7 +12,7 @@ let filterTimeout = null; // Time Constants const MIN_REFRESH_INTERVAL = 60; // 60 sec -const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes +const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes const FALLBACK_CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours // Application Constants @@ -67,7 +67,8 @@ const coinNameToDisplayName = { const coinIdToName = { 1: 'particl', 2: 'bitcoin', 3: 'litecoin', 4: 'decred', 6: 'monero', 7: 'particl blind', 8: 'particl anon', - 9: 'wownero', 11: 'pivx', 13: 'firo', 17: 'bitcoincash' + 9: 'wownero', 11: 'pivx', 13: 'firo', 17: 'bitcoincash', + 18: 'dogecoin' }; // DOM ELEMENT REFERENCES @@ -92,7 +93,7 @@ const WebSocketManager = { reconnectDelay: 5000, maxQueueSize: 1000, isIntentionallyClosed: false, - + connectionState: { isConnecting: false, lastConnectAttempt: null, @@ -271,7 +272,7 @@ const WebSocketManager = { try { const response = await fetch(endpoint); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); - + const newData = await response.json(); const fetchedOffers = Array.isArray(newData) ? newData : Object.values(newData); @@ -300,7 +301,7 @@ const WebSocketManager = { this.reconnectAttempts++; if (this.reconnectAttempts <= this.maxReconnectAttempts) { console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})`); - + const delay = Math.min( this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1), 30000 @@ -324,11 +325,11 @@ const WebSocketManager = { cleanup() { console.log('Cleaning up WebSocket resources'); - + clearTimeout(this.debounceTimeout); clearTimeout(this.reconnectTimeout); clearTimeout(this.connectionState.connectTimeout); - + this.messageQueue = []; this.processingQueue = false; this.connectionState.isConnecting = false; @@ -365,7 +366,7 @@ const CacheManager = { set: function(key, value, customTtl = null) { try { this.cleanup(); - + const item = { value: value, timestamp: Date.now(), @@ -396,7 +397,7 @@ const CacheManager = { return false; } }, - + get: function(key) { try { const itemStr = localStorage.getItem(key); @@ -404,14 +405,14 @@ const CacheManager = { const item = JSON.parse(itemStr); const now = Date.now(); - + if (now < item.expiresAt) { return { value: item.value, remainingTime: item.expiresAt - now }; } - + localStorage.removeItem(key); } catch (error) { localStorage.removeItem(key); @@ -433,7 +434,7 @@ const CacheManager = { const itemStr = localStorage.getItem(key); const size = new Blob([itemStr]).size; const item = JSON.parse(itemStr); - + if (now >= item.expiresAt) { localStorage.removeItem(key); continue; @@ -445,7 +446,7 @@ const CacheManager = { expiresAt: item.expiresAt, timestamp: item.timestamp }); - + totalSize += size; itemCount++; } catch (error) { @@ -455,7 +456,7 @@ const CacheManager = { if (aggressive || totalSize > this.maxSize || itemCount > this.maxItems) { items.sort((a, b) => b.timestamp - a.timestamp); - + while ((totalSize > this.maxSize || itemCount > this.maxItems) && items.length > 0) { const item = items.pop(); localStorage.removeItem(item.key); @@ -473,7 +474,7 @@ const CacheManager = { keys.push(key); } } - + keys.forEach(key => localStorage.removeItem(key)); }, @@ -491,10 +492,10 @@ const CacheManager = { const itemStr = localStorage.getItem(key); const size = new Blob([itemStr]).size; const item = JSON.parse(itemStr); - + totalSize += size; itemCount++; - + if (now >= item.expiresAt) { expiredCount++; } @@ -528,7 +529,7 @@ window.tableRateModule = { 'Bitcoin Cash': 'BCH', 'Dogecoin': 'DOGE' }, - + cache: {}, processedOffers: new Set(), @@ -564,7 +565,7 @@ window.tableRateModule = { this.processedOffers.add(offerId); return true; }, - + formatUSD(value) { if (Math.abs(value) < 0.000001) { return value.toExponential(8) + ' USD'; @@ -654,23 +655,23 @@ async function initializePriceData() { while (retryCount < PRICE_INIT_RETRIES) { try { prices = await fetchLatestPrices(); - + if (prices && Object.keys(prices).length > 0) { console.log('Successfully fetched initial price data'); latestPrices = prices; CacheManager.set(PRICES_CACHE_KEY, prices, CACHE_DURATION); return true; } - + retryCount++; - + if (retryCount < PRICE_INIT_RETRIES) { await new Promise(resolve => setTimeout(resolve, PRICE_INIT_RETRY_DELAY)); } } catch (error) { console.error(`Error fetching prices (attempt ${retryCount + 1}):`, error); retryCount++; - + if (retryCount < PRICE_INIT_RETRIES) { await new Promise(resolve => setTimeout(resolve, PRICE_INIT_RETRY_DELAY)); } @@ -706,7 +707,7 @@ function checkOfferAgainstFilters(offer, filters) { const currentTime = Math.floor(Date.now() / 1000); const isExpired = offer.expire_at <= currentTime; const isRevoked = Boolean(offer.is_revoked); - + switch (filters.status) { case 'active': return !isExpired && !isRevoked; @@ -726,7 +727,7 @@ function initializeFlowbiteTooltips() { console.warn('Tooltip is not defined. Make sure the required library is loaded.'); return; } - + const tooltipElements = document.querySelectorAll('[data-tooltip-target]'); tooltipElements.forEach((el) => { const tooltipId = el.getAttribute('data-tooltip-target'); @@ -740,10 +741,10 @@ function initializeFlowbiteTooltips() { // DATA PROCESSING FUNCTIONS async function checkExpiredAndFetchNew() { if (isSentOffers) return Promise.resolve(); - + console.log('Starting checkExpiredAndFetchNew'); const OFFERS_CACHE_KEY = 'offers_received'; - + try { const response = await fetch('/json/offers'); const data = await response.json(); @@ -772,9 +773,9 @@ async function checkExpiredAndFetchNew() { CacheManager.set(OFFERS_CACHE_KEY, newListings, CACHE_DURATION); const currentFilters = new FormData(filterForm); - const hasActiveFilters = currentFilters.get('coin_to') !== 'any' || + const hasActiveFilters = currentFilters.get('coin_to') !== 'any' || currentFilters.get('coin_from') !== 'any'; - + if (hasActiveFilters) { jsonData = filterAndSortData(); } else { @@ -784,7 +785,7 @@ async function checkExpiredAndFetchNew() { updateOffersTable(); updateJsonView(); updatePaginationInfo(); - + if (jsonData.length === 0) { handleNoOffersScenario(); } @@ -810,7 +811,7 @@ function getValidOffers() { function filterAndSortData() { //console.log('[Debug] Starting filter with data length:', originalJsonData.length); - + const formData = new FormData(filterForm); const filters = Object.fromEntries(formData); //console.log('[Debug] Active filters:', filters); @@ -825,7 +826,7 @@ function filterAndSortData() { let filteredData = [...originalJsonData]; const sentFromFilter = filters.sent_from || 'any'; - + filteredData = filteredData.filter(offer => { if (sentFromFilter === 'public') { return offer.is_public; @@ -842,13 +843,13 @@ function filterAndSortData() { const coinFrom = (offer.coin_from || '').toLowerCase(); const coinTo = (offer.coin_to || '').toLowerCase(); - + if (filters.coin_to !== 'any') { if (!coinMatches(coinTo, filters.coin_to)) { return false; } } - + if (filters.coin_from !== 'any') { if (!coinMatches(coinFrom, filters.coin_from)) { return false; @@ -859,7 +860,7 @@ function filterAndSortData() { const currentTime = Math.floor(Date.now() / 1000); const isExpired = offer.expire_at <= currentTime; const isRevoked = Boolean(offer.is_revoked); - + switch (filters.status) { case 'active': return !isExpired && !isRevoked; @@ -878,7 +879,7 @@ function filterAndSortData() { if (currentSortColumn !== null) { filteredData.sort((a, b) => { let comparison = 0; - + switch(currentSortColumn) { case 0: // Time comparison = a.created_at - b.created_at; @@ -891,32 +892,32 @@ function filterAndSortData() { const aToSymbol = getCoinSymbolLowercase(a.coin_to); const bFromSymbol = getCoinSymbolLowercase(b.coin_from); const bToSymbol = getCoinSymbolLowercase(b.coin_to); - + const aFromPrice = latestPrices[aFromSymbol]?.usd || 0; const aToPrice = latestPrices[aToSymbol]?.usd || 0; const bFromPrice = latestPrices[bFromSymbol]?.usd || 0; const bToPrice = latestPrices[bToSymbol]?.usd || 0; - + const aMarketRate = aToPrice / aFromPrice; const bMarketRate = bToPrice / bFromPrice; - + const aOfferedRate = parseFloat(a.rate); const bOfferedRate = parseFloat(b.rate); - + const aPercentDiff = ((aOfferedRate - aMarketRate) / aMarketRate) * 100; const bPercentDiff = ((bOfferedRate - bMarketRate) / bMarketRate) * 100; - + comparison = aPercentDiff - bPercentDiff; break; case 7: // Trade comparison = a.offer_id.localeCompare(b.offer_id); break; } - + return currentSortDirection === 'desc' ? -comparison : comparison; }); } - + //console.log(`[Debug] Filtered data length: ${filteredData.length}`); return filteredData; } @@ -1019,7 +1020,7 @@ async function fetchLatestPrices() { } const url = `${config.apiEndpoints.coinGecko}/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zano,wownero,zcoin&vs_currencies=USD,BTC&api_key=${config.apiKeys.coinGecko}`; - + try { console.log('Fetching fresh price data...'); const response = await fetch('/json/readurl', { @@ -1041,7 +1042,7 @@ async function fetchLatestPrices() { if (data.Error) { throw new Error(data.Error); } - + if (data && Object.keys(data).length > 0) { console.log('Fresh price data received'); @@ -1052,7 +1053,7 @@ async function fetchLatestPrices() { Object.entries(data).forEach(([coin, prices]) => { tableRateModule.setFallbackValue(coin, prices.usd); }); - + return data; } else { //console.warn('Received empty price data'); @@ -1075,18 +1076,18 @@ async function fetchOffers(manualRefresh = false) { refreshIcon.classList.add('animate-spin'); refreshText.textContent = 'Refreshing...'; refreshButton.classList.add('opacity-75', 'cursor-wait'); - + const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers'; const response = await fetch(endpoint); const data = await response.json(); - + jsonData = formatInitialData(data); originalJsonData = [...jsonData]; await updateOffersTable(); updateJsonView(); updatePaginationInfo(); - + } catch (error) { console.error('[Debug] Error fetching offers:', error); ui.displayErrorMessage('Failed to fetch offers. Please try again later.'); @@ -1120,12 +1121,12 @@ function formatInitialData(data) { function updateConnectionStatus(status) { const dot = document.getElementById('status-dot'); const text = document.getElementById('status-text'); - + if (!dot || !text) { //console.warn('Status indicators not found in DOM'); return; } - + switch(status) { case 'connected': dot.className = 'w-2.5 h-2.5 rounded-full bg-green-500 mr-2'; @@ -1159,10 +1160,10 @@ function updateRowTimes() { const newPostedTime = formatTime(offer.created_at, true); const newExpiresIn = formatTimeLeft(offer.expire_at); - + const postedElement = row.querySelector('.text-xs:first-child'); const expiresElement = row.querySelector('.text-xs:last-child'); - + if (postedElement && postedElement.textContent !== `Posted: ${newPostedTime}`) { postedElement.textContent = `Posted: ${newPostedTime}`; } @@ -1212,14 +1213,14 @@ function updatePaginationInfo() { const showPrev = currentPage > 1; const showNext = currentPage < totalPages && totalItems > 0; - + prevPageButton.style.display = showPrev ? 'inline-flex' : 'none'; nextPageButton.style.display = showNext ? 'inline-flex' : 'none'; if (lastRefreshTime) { lastRefreshTimeSpan.textContent = new Date(lastRefreshTime).toLocaleTimeString(); } - + if (newEntriesCountSpan) { newEntriesCountSpan.textContent = totalItems; } @@ -1255,13 +1256,13 @@ function updateProfitLoss(row, fromCoin, toCoin, fromAmount, toAmount, isOwnOffe } const formattedPercentDiff = percentDiff.toFixed(2); - const percentDiffDisplay = formattedPercentDiff === "0.00" ? "0.00" : + const percentDiffDisplay = formattedPercentDiff === "0.00" ? "0.00" : (percentDiff > 0 ? `+${formattedPercentDiff}` : formattedPercentDiff); const colorClass = getProfitColorClass(percentDiff); profitLossElement.textContent = `${percentDiffDisplay}%`; profitLossElement.className = `profit-loss text-lg font-bold ${colorClass}`; - + const tooltipId = `percentage-tooltip-${row.getAttribute('data-offer-id')}`; const tooltipElement = document.getElementById(tooltipId); if (tooltipElement) { @@ -1310,7 +1311,7 @@ function updateClearFiltersButton() { const hasFilters = hasActiveFilters(); clearButton.classList.toggle('opacity-50', !hasFilters); clearButton.disabled = !hasFilters; - + // Update button styles based on state if (hasFilters) { clearButton.classList.add('hover:bg-green-600', 'hover:text-white'); @@ -1325,10 +1326,10 @@ function updateClearFiltersButton() { function handleNoOffersScenario() { const formData = new FormData(filterForm); const filters = Object.fromEntries(formData); - const hasActiveFilters = filters.coin_to !== 'any' || + const hasActiveFilters = filters.coin_to !== 'any' || filters.coin_from !== 'any' || (filters.status && filters.status !== 'any'); - + stopRefreshAnimation(); if (hasActiveFilters) { @@ -1336,7 +1337,7 @@ function handleNoOffersScenario() {
- No offers match the selected filters. Try different filter options or + No offers match the selected filters. Try different filter options or @@ -1357,7 +1358,7 @@ async function updateOffersTable() { try { const PRICES_CACHE_KEY = 'prices_coingecko'; const cachedPrices = CacheManager.get(PRICES_CACHE_KEY); - + if (!cachedPrices || !cachedPrices.remainingTime || cachedPrices.remainingTime < 60000) { console.log('Fetching fresh price data...'); const priceData = await fetchLatestPrices(); @@ -1374,12 +1375,12 @@ async function updateOffersTable() { const endIndex = Math.min(startIndex + itemsPerPage, validOffers.length); const itemsToDisplay = validOffers.slice(startIndex, endIndex); - const identityPromises = itemsToDisplay.map(offer => + const identityPromises = itemsToDisplay.map(offer => offer.addr_from ? getIdentityData(offer.addr_from) : Promise.resolve(null) ); - + const identities = await Promise.all(identityPromises); - + if (validOffers.length === 0) { handleNoOffersScenario(); return; @@ -1405,7 +1406,7 @@ async function updateOffersTable() { initializeFlowbiteTooltips(); updateRowTimes(); updatePaginationControls(totalPages); - + if (tableRateModule?.initializeTable) { tableRateModule.initializeTable(); } @@ -1482,7 +1483,7 @@ function getIdentityInfo(address, identity) { function createTableRow(offer, identity = null) { const row = document.createElement('tr'); const uniqueId = `${offer.offer_id}_${offer.created_at}`; - + row.className = 'relative opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600'; row.setAttribute('data-offer-id', uniqueId); @@ -1504,7 +1505,7 @@ function createTableRow(offer, identity = null) { const coinToDisplay = getDisplayName(coinTo); const postedTime = formatTime(createdAt, true); const expiresIn = formatTime(expireAt); - + const currentTime = Math.floor(Date.now() / 1000); const isActuallyExpired = currentTime > expireAt; const fromAmount = parseFloat(amountFrom) || 0; @@ -1585,19 +1586,19 @@ function shouldShowPublicTag(offers) { function truncateText(text, maxLength = 15) { if (typeof text !== 'string') return ''; - return text.length > maxLength - ? text.slice(0, maxLength) + '...' + return text.length > maxLength + ? text.slice(0, maxLength) + '...' : text; } function createDetailsColumn(offer, identity = null) { const addrFrom = offer.addr_from || ''; const identityInfo = getIdentityInfo(addrFrom, identity); - + const showPublicPrivateTags = originalJsonData.some(o => o.is_public !== offer.is_public); - const tagClass = offer.is_public - ? 'bg-green-600 dark:bg-green-600' + const tagClass = offer.is_public + ? 'bg-green-600 dark:bg-green-600' : 'bg-red-500 dark:bg-red-500'; const tagText = offer.is_public ? 'Public' : 'Private'; @@ -1605,16 +1606,16 @@ function createDetailsColumn(offer, identity = null) { identityInfo.label || addrFrom || 'Unspecified' ); - const identifierTextClass = identityInfo.label - ? 'text-white dark:text-white' + const identifierTextClass = identityInfo.label + ? 'text-white dark:text-white' : 'monospace'; - + return `
${showPublicPrivateTags ? `${tagText} ` : ''} - + @@ -1635,8 +1636,8 @@ function createTakerAmountColumn(offer, coinTo, coinFrom) {
-
-
${fromAmount.toFixed(4)}
+
+
${fromAmount.toFixed(4)}
${coinTo}
@@ -1679,8 +1680,8 @@ function createOrderbookColumn(offer, coinFrom, coinTo) {
-
-
${toAmount.toFixed(4)}
+
+
${toAmount.toFixed(4)}
${coinFrom}
@@ -1694,7 +1695,7 @@ function createRateColumn(offer, coinFrom, coinTo) { const inverseRate = 1 / rate; const fromSymbol = getCoinSymbol(coinFrom); const toSymbol = getCoinSymbol(coinTo); - + const getPriceKey = (coin) => { const lowerCoin = coin.toLowerCase(); if (lowerCoin === 'firo' || lowerCoin === 'zcoin') { @@ -1746,9 +1747,9 @@ function createPercentageColumn(offer) { function createActionColumn(offer, isActuallyExpired = false) { const isRevoked = Boolean(offer.is_revoked); const isTreatedAsSentOffer = offer.is_own_offer; - + let buttonClass, buttonText; - + if (isRevoked) { buttonClass = 'bg-red-500 text-white hover:bg-red-600 transition duration-200'; buttonText = 'Revoked'; @@ -1786,14 +1787,14 @@ function createTooltips(offer, treatAsSentOffer, coinFrom, coinTo, fromAmount, t const identityInfo = getIdentityInfo(addrFrom, identity); const totalBids = identity ? ( - identityInfo.stats.sentBidsSuccessful + - identityInfo.stats.recvBidsSuccessful + - identityInfo.stats.sentBidsFailed + - identityInfo.stats.recvBidsFailed + - identityInfo.stats.sentBidsRejected + + identityInfo.stats.sentBidsSuccessful + + identityInfo.stats.recvBidsSuccessful + + identityInfo.stats.sentBidsFailed + + identityInfo.stats.recvBidsFailed + + identityInfo.stats.sentBidsRejected + identityInfo.stats.recvBidsRejected ) : 0; - + const successRate = totalBids ? ( ((identityInfo.stats.sentBidsSuccessful + identityInfo.stats.recvBidsSuccessful) / totalBids) * 100 ).toFixed(1) : 0; @@ -1846,14 +1847,14 @@ function createTooltips(offer, treatAsSentOffer, coinFrom, coinTo, fromAmount, t
- + - + - + - +