diff --git a/CHANGELOG.md b/CHANGELOG.md index 0146ec77..3e0c4323 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - GUI + SWAP + ASB: Reduce the confirmations required to spend a Monero transaction from 22 to 15. We believe the risks of re-orgs is low again and this is safe to do. This may increase the chances of swap being successful and will reduce the time a swap takes. - GUI: Fix an issue where we a manual resume of a swap would be necessary if we failed to fetch certain Bitcoin transactions due to network issues. +- GUI: Remove the following default Electrum servers: `tcp://electrum.blockstream.info:50001`, `tcp://electrum.coinucopia.io:50001`, `tcp://se-mma-crypto-payments-001.mullvad.net:50001`, `tcp://electrum2.bluewallet.io:50777` due to them being unreliable. Add the following new default Electrum servers: `tcp://electrum1.bluewallet.io:50001`, `tcp://electrum2.bluewallet.io:50001`, `tcp://electrum3.bluewallet.io:50001`, `ssl://btc-electrum.cakewallet.com:50002`, `tcp://bitcoin.aranguren.org:50001`. ## [3.2.9] - 2025-11-05 diff --git a/dev-scripts/health_check_default_electrum_servers.py b/dev-scripts/health_check_default_electrum_servers.py new file mode 100644 index 00000000..49713833 --- /dev/null +++ b/dev-scripts/health_check_default_electrum_servers.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +# /// script +# requires-python = ">=3.11" +# dependencies = [] +# /// + +""" +Check electrum server availability. +Usage: uv run check_electrum_servers.py +""" + +import json +import re +import socket +import ssl +import sys +from pathlib import Path +from urllib.parse import urlparse +from typing import List, Tuple +from concurrent.futures import ThreadPoolExecutor, as_completed + + +def extract_servers_from_rust(file_path: Path) -> List[str]: + """Extract electrum server URLs from Rust defaults.rs file.""" + servers = [] + content = file_path.read_text() + + # Find all Url::parse() calls + pattern = r'Url::parse\("([^"]+)"\)' + matches = re.findall(pattern, content) + servers.extend(matches) + + return servers + + +def extract_servers_from_typescript(file_path: Path) -> List[str]: + """Extract electrum server URLs from TypeScript defaults.ts file.""" + servers = [] + content = file_path.read_text() + + # Find all server strings in the arrays + pattern = r'"((?:tcp|ssl)://[^"]+)"' + matches = re.findall(pattern, content) + servers.extend(matches) + + return servers + + +def check_tcp_server(host: str, port: int, timeout: int = 2) -> Tuple[bool, str]: + """Check TCP electrum server.""" + request = json.dumps({"id": 1, "method": "blockchain.headers.subscribe", "params": []}) + "\n" + + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.settimeout(timeout) + sock.connect((host, port)) + sock.sendall(request.encode()) + + response = b"" + while True: + chunk = sock.recv(4096) + if not chunk: + break + response += chunk + if b"\n" in response: + break + + response_str = response.decode().strip() + if response_str: + data = json.loads(response_str) + if 'result' in data or 'error' in data: + return True, "OK" + + return False, "No valid response" + except socket.timeout: + return False, "Timeout" + except ConnectionRefusedError: + return False, "Connection refused" + except Exception as e: + return False, str(e) + + +def check_ssl_server(host: str, port: int, timeout: int = 2) -> Tuple[bool, str]: + """Check SSL/TLS electrum server.""" + request = json.dumps({"id": 1, "method": "blockchain.headers.subscribe", "params": []}) + "\n" + + try: + context = ssl.create_default_context() + # Don't verify certificates for testing purposes + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.settimeout(timeout) + with context.wrap_socket(sock, server_hostname=host) as ssock: + ssock.connect((host, port)) + ssock.sendall(request.encode()) + + response = b"" + while True: + chunk = ssock.recv(4096) + if not chunk: + break + response += chunk + if b"\n" in response: + break + + response_str = response.decode().strip() + if response_str: + data = json.loads(response_str) + if 'result' in data or 'error' in data: + return True, "OK" + + return False, "No valid response" + except socket.timeout: + return False, "Timeout" + except ConnectionRefusedError: + return False, "Connection refused" + except ssl.SSLError as e: + return False, f"SSL error: {str(e)}" + except Exception as e: + return False, str(e) + + +def check_server(url: str) -> Tuple[str, bool, str]: + """Check a single electrum server.""" + parsed = urlparse(url) + protocol = parsed.scheme + host = parsed.hostname + port = parsed.port + + if not host or not port: + return url, False, "Invalid URL format" + + if protocol == "tcp": + success, message = check_tcp_server(host, port) + elif protocol == "ssl": + success, message = check_ssl_server(host, port) + else: + return url, False, f"Unknown protocol: {protocol}" + + return url, success, message + + +def main(): + # Find the files + base_dir = Path(__file__).parent + rust_file = base_dir / "swap-env" / "src" / "defaults.rs" + ts_file = base_dir / "src-gui" / "src" / "store" / "features" / "defaults.ts" + + if not rust_file.exists(): + print(f"āŒ Rust defaults file not found: {rust_file}") + sys.exit(1) + + if not ts_file.exists(): + print(f"āŒ TypeScript defaults file not found: {ts_file}") + sys.exit(1) + + # Extract servers + print("šŸ“‹ Extracting server URLs...") + rust_servers = extract_servers_from_rust(rust_file) + ts_servers = extract_servers_from_typescript(ts_file) + + # Combine and deduplicate + all_servers = list(set(rust_servers + ts_servers)) + all_servers.sort() + + print(f"\nšŸ“Š Found {len(all_servers)} unique servers\n") + + # Check servers in parallel + print("šŸ” Checking servers (this may take a minute)...\n") + + working = [] + broken = [] + + with ThreadPoolExecutor(max_workers=10) as executor: + futures = {executor.submit(check_server, url): url for url in all_servers} + + for future in as_completed(futures): + url, success, message = future.result() + + status = "āœ…" if success else "āŒ" + print(f"{status} {url:60s} {message}") + + if success: + working.append(url) + else: + broken.append((url, message)) + + # Summary + print("\n" + "="*80) + print(f"\nšŸ“Š Summary:") + print(f" āœ… Working: {len(working)}/{len(all_servers)}") + print(f" āŒ Broken: {len(broken)}/{len(all_servers)}") + + if broken: + print(f"\nāš ļø Broken servers:") + for url, reason in broken: + print(f" • {url} - {reason}") + + # Check if the newly added server is working + new_server = "tcp://bitcoin.aranguren.org:50001" + if new_server in all_servers: + is_working = new_server in working + status = "āœ… WORKING" if is_working else "āŒ BROKEN" + print(f"\nšŸ†• Newly added server: {new_server} - {status}") + + sys.exit(0 if len(broken) == 0 else 1) + + +if __name__ == "__main__": + main() diff --git a/src-gui/eslint.config.js b/src-gui/eslint.config.js index 0e94c834..e1fc3498 100644 --- a/src-gui/eslint.config.js +++ b/src-gui/eslint.config.js @@ -2,6 +2,7 @@ import globals from "globals"; import js from "@eslint/js"; import tseslint from "typescript-eslint"; import pluginReact from "eslint-plugin-react"; +import importPlugin from "eslint-plugin-import"; export default [ { ignores: ["node_modules", "dist"] }, @@ -12,12 +13,16 @@ export default [ languageOptions: { globals: globals.browser, }, + plugins: { + import: importPlugin, + }, rules: { "react/react-in-jsx-scope": "off", "react/no-unescaped-entities": "off", "react/no-children-prop": "off", "@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-empty-object-type": "off", + "import/no-cycle": ["error", { maxDepth: 10 }], "no-restricted-globals": [ "warn", { diff --git a/src-gui/package.json b/src-gui/package.json index 2d66e3e9..961d008c 100644 --- a/src-gui/package.json +++ b/src-gui/package.json @@ -48,7 +48,6 @@ "virtua": "^0.33.2" }, "devDependencies": { - "@types/react-redux": "^7.1.34", "@eslint/js": "^9.9.0", "@redux-devtools/remote": "^0.9.5", "@tauri-apps/cli": "^2.0.0", @@ -60,9 +59,11 @@ "@types/react": "^19.1.6", "@types/react-dom": "^19.1.5", "@types/react-is": "^19.0.0", + "@types/react-redux": "^7.1.34", "@types/semver": "^7.5.8", "@vitejs/plugin-react": "^4.2.1", "eslint": "^9.9.0", + "eslint-plugin-import": "^2.32.0", "eslint-plugin-react": "^7.35.0", "globals": "^15.9.0", "internal-ip": "^7.0.0", diff --git a/src-gui/src/renderer/background.ts b/src-gui/src/renderer/background.ts index 8a814740..40eab676 100644 --- a/src-gui/src/renderer/background.ts +++ b/src-gui/src/renderer/background.ts @@ -34,6 +34,12 @@ import { setHistory, setSyncProgress, } from "store/features/walletSlice"; +import { applyDefaultNodes } from "store/features/settingsSlice"; +import { + DEFAULT_NODES, + NEGATIVE_NODES_MAINNET, + NEGATIVE_NODES_TESTNET, +} from "store/defaults"; const TAURI_UNIFIED_EVENT_CHANNEL_NAME = "tauri-unified-event"; @@ -64,6 +70,15 @@ function setIntervalImmediate(callback: () => void, interval: number): void { } export async function setupBackgroundTasks(): Promise { + // Apply default nodes on startup (removes broken nodes, adds new ones) + store.dispatch( + applyDefaultNodes({ + defaultNodes: DEFAULT_NODES, + negativeNodesMainnet: NEGATIVE_NODES_MAINNET, + negativeNodesTestnet: NEGATIVE_NODES_TESTNET, + }), + ); + // Setup periodic fetch tasks setIntervalImmediate(updatePublicRegistry, PROVIDER_UPDATE_INTERVAL); setIntervalImmediate(updateAllNodeStatuses, STATUS_UPDATE_INTERVAL); diff --git a/src-gui/src/renderer/components/pages/help/SettingsBox.tsx b/src-gui/src/renderer/components/pages/help/SettingsBox.tsx index 62765c88..0e8203cb 100644 --- a/src-gui/src/renderer/components/pages/help/SettingsBox.tsx +++ b/src-gui/src/renderer/components/pages/help/SettingsBox.tsx @@ -26,11 +26,9 @@ import { import { addNode, addRendezvousPoint, - Blockchain, DonateToDevelopmentTip, FiatCurrency, moveUpNode, - Network, removeNode, removeRendezvousPoint, resetSettings, @@ -48,6 +46,7 @@ import { RedeemPolicy, RefundPolicy, } from "store/features/settingsSlice"; +import { Blockchain, Network } from "store/types"; import { useAppDispatch, useNodes, useSettings } from "store/hooks"; import ValidatedTextField from "renderer/components/other/ValidatedTextField"; import HelpIcon from "@mui/icons-material/HelpOutline"; diff --git a/src-gui/src/renderer/rpc.ts b/src-gui/src/renderer/rpc.ts index 8317fe6a..597f781e 100644 --- a/src-gui/src/renderer/rpc.ts +++ b/src-gui/src/renderer/rpc.ts @@ -72,15 +72,13 @@ import { MoneroRecoveryResponse } from "models/rpcModel"; import { ListSellersResponse } from "../models/tauriModel"; import logger from "utils/logger"; import { getNetwork, isTestnet } from "store/config"; -import { - Blockchain, - DonateToDevelopmentTip, - Network, -} from "store/features/settingsSlice"; +import { DonateToDevelopmentTip } from "store/features/settingsSlice"; +import { Blockchain, Network } from "store/types"; import { setStatus } from "store/features/nodesSlice"; import { discoveredMakersByRendezvous } from "store/features/makersSlice"; import { CliLog } from "models/cliModel"; import { logsToRawString, parseLogsFromString } from "utils/parseUtils"; +import { DEFAULT_RENDEZVOUS_POINTS } from "store/defaults"; /// These are the official donation address for the eigenwallet/core project const DONATION_ADDRESS_MAINNET = @@ -115,17 +113,6 @@ KM0Pp53f -----END PGP SIGNATURE----- `; -export const PRESET_RENDEZVOUS_POINTS = [ - "/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE", - "/dns4/discover2.unstoppableswap.net/tcp/8888/p2p/12D3KooWGRvf7qVQDrNR5nfYD6rKrbgeTi9x8RrbdxbmsPvxL4mw", - "/dns4/darkness.su/tcp/8888/p2p/12D3KooWFQAgVVS9t9UgL6v1sLprJVM7am5hFK7vy9iBCCoCBYmU", - "/dns4/eigen.center/tcp/8888/p2p/12D3KooWS5RaYJt4ANKMH4zczGVhNcw5W214e2DDYXnjs5Mx5zAT", - "/dns4/swapanarchy.cfd/tcp/8888/p2p/12D3KooWRtyVpmyvwzPYXuWyakFbRKhyXGrjhq6tP7RrBofpgQGp", - "/dns4/rendezvous.observer/tcp/8888/p2p/12D3KooWMjceGXrYuGuDMGrfmJxALnSDbK4km6s1i1sJEgDTgGQa", - "/dns4/aswap.click/tcp/8888/p2p/12D3KooWQzW52mdsLHTMu1EPiz3APumG6vGwpCuyy494MAQoEa5X", - "/dns4/getxmr.st/tcp/8888/p2p/12D3KooWHHwiz6WDThPT8cEurstomg3kDSxzL2L8pwxfyX2fpxVk", -]; - async function invoke( command: string, args: ARGS, @@ -141,7 +128,7 @@ async function invokeNoArgs(command: string): Promise { export async function fetchSellersAtPresetRendezvousPoints() { await Promise.all( - PRESET_RENDEZVOUS_POINTS.map(async (rendezvousPoint) => { + DEFAULT_RENDEZVOUS_POINTS.map(async (rendezvousPoint) => { const response = await listSellersAtRendezvousPoint([rendezvousPoint]); store.dispatch(discoveredMakersByRendezvous(response.sellers)); @@ -241,7 +228,7 @@ export async function buyXmr() { } await invoke("buy_xmr", { - rendezvous_points: PRESET_RENDEZVOUS_POINTS, + rendezvous_points: DEFAULT_RENDEZVOUS_POINTS, sellers, monero_receive_pool: address_pool, // We convert null to undefined because typescript diff --git a/src-gui/src/store/config.ts b/src-gui/src/store/config.ts index daf7ba32..7d7f274f 100644 --- a/src-gui/src/store/config.ts +++ b/src-gui/src/store/config.ts @@ -1,7 +1,7 @@ import { ExtendedMakerStatus } from "models/apiModel"; import { splitPeerIdFromMultiAddress } from "utils/parseUtils"; import { CliMatches, getMatches } from "@tauri-apps/plugin-cli"; -import { Network } from "./features/settingsSlice"; +import { Network } from "./types"; let matches: CliMatches; try { diff --git a/src-gui/src/store/defaults.ts b/src-gui/src/store/defaults.ts new file mode 100644 index 00000000..e09f5cbe --- /dev/null +++ b/src-gui/src/store/defaults.ts @@ -0,0 +1,62 @@ +import { Network, Blockchain } from "./types"; + +export const DEFAULT_RENDEZVOUS_POINTS = [ + "/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE", + "/dns4/discover2.unstoppableswap.net/tcp/8888/p2p/12D3KooWGRvf7qVQDrNR5nfYD6rKrbgeTi9x8RrbdxbmsPvxL4mw", + "/dns4/darkness.su/tcp/8888/p2p/12D3KooWFQAgVVS9t9UgL6v1sLprJVM7am5hFK7vy9iBCCoCBYmU", + "/dns4/eigen.center/tcp/8888/p2p/12D3KooWS5RaYJt4ANKMH4zczGVhNcw5W214e2DDYXnjs5Mx5zAT", + "/dns4/swapanarchy.cfd/tcp/8888/p2p/12D3KooWRtyVpmyvwzPYXuWyakFbRKhyXGrjhq6tP7RrBofpgQGp", + "/dns4/rendezvous.observer/tcp/8888/p2p/12D3KooWMjceGXrYuGuDMGrfmJxALnSDbK4km6s1i1sJEgDTgGQa", + "/dns4/aswap.click/tcp/8888/p2p/12D3KooWQzW52mdsLHTMu1EPiz3APumG6vGwpCuyy494MAQoEa5X", + "/dns4/getxmr.st/tcp/8888/p2p/12D3KooWHHwiz6WDThPT8cEurstomg3kDSxzL2L8pwxfyX2fpxVk", +]; + +// Known broken nodes to remove when applying defaults +export const NEGATIVE_NODES_MAINNET = [ + "tcp://electrum.blockstream.info:50001", + "tcp://electrum.coinucopia.io:50001", + "tcp://se-mma-crypto-payments-001.mullvad.net:50001", + "tcp://electrum2.bluewallet.io:50777", +]; + +export const NEGATIVE_NODES_TESTNET = [ + "ssl://ax101.blockeng.ch:60002", + "tcp://electrum.blockstream.info:60001", + "tcp://blockstream.info:143", + "ssl://testnet.qtornado.com:50002", + "ssl://testnet.qtornado.com:51002", + "tcp://testnet.qtornado.com:51001", +]; + +export const DEFAULT_NODES: Record> = { + [Network.Testnet]: { + [Blockchain.Bitcoin]: [ + "ssl://blackie.c3-soft.com:57006", + "ssl://v22019051929289916.bestsrv.de:50002", + "tcp://v22019051929289916.bestsrv.de:50001", + "ssl://electrum.blockstream.info:60002", + "ssl://blockstream.info:993", + "tcp://testnet.aranguren.org:51001", + "ssl://testnet.aranguren.org:51002", + "ssl://bitcoin.devmole.eu:5010", + "tcp://bitcoin.devmole.eu:5000", + ], + [Blockchain.Monero]: [], + }, + [Network.Mainnet]: { + [Blockchain.Bitcoin]: [ + "ssl://electrum.blockstream.info:50002", + "ssl://bitcoin.stackwallet.com:50002", + "ssl://b.1209k.com:50002", + "ssl://mainnet.foundationdevices.com:50002", + "tcp://bitcoin.lu.ke:50001", + "ssl://electrum.coinfinity.co:50002", + "tcp://electrum1.bluewallet.io:50001", + "tcp://electrum2.bluewallet.io:50001", + "tcp://electrum3.bluewallet.io:50001", + "ssl://btc-electrum.cakewallet.com:50002", + "tcp://bitcoin.aranguren.org:50001", + ], + [Blockchain.Monero]: [], + }, +}; diff --git a/src-gui/src/store/features/nodesSlice.ts b/src-gui/src/store/features/nodesSlice.ts index 18086d0c..109bbb3c 100644 --- a/src-gui/src/store/features/nodesSlice.ts +++ b/src-gui/src/store/features/nodesSlice.ts @@ -1,5 +1,5 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import { Blockchain } from "./settingsSlice"; +import { Blockchain } from "../types"; export interface NodesSlice { nodes: Record>; diff --git a/src-gui/src/store/features/settingsSlice.ts b/src-gui/src/store/features/settingsSlice.ts index ea32c6c4..8be89726 100644 --- a/src-gui/src/store/features/settingsSlice.ts +++ b/src-gui/src/store/features/settingsSlice.ts @@ -1,16 +1,11 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { Theme } from "renderer/components/theme"; +import { DEFAULT_NODES, DEFAULT_RENDEZVOUS_POINTS } from "../defaults"; +import { Network, Blockchain } from "../types"; export type DonateToDevelopmentTip = false | 0.0005 | 0.0075; -const DEFAULT_RENDEZVOUS_POINTS = [ - "/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE", - "/dns4/discover2.unstoppableswap.net/tcp/8888/p2p/12D3KooWGRvf7qVQDrNR5nfYD6rKrbgeTi9x8RrbdxbmsPvxL4mw", - "/dns4/darkness.su/tcp/8888/p2p/12D3KooWFQAgVVS9t9UgL6v1sLprJVM7am5hFK7vy9iBCCoCBYmU", - "/dns4/eigen.center/tcp/8888/p2p/12D3KooWS5RaYJt4ANKMH4zczGVhNcw5W214e2DDYXnjs5Mx5zAT", - "/dns4/swapanarchy.cfd/tcp/8888/p2p/12D3KooWRtyVpmyvwzPYXuWyakFbRKhyXGrjhq6tP7RrBofpgQGp", - "/dns4/rendezvous.observer/tcp/8888/p2p/12D3KooWMjceGXrYuGuDMGrfmJxALnSDbK4km6s1i1sJEgDTgGQa", -]; +const MIN_TIME_BETWEEN_DEFAULT_NODES_APPLY = 14 * 24 * 60 * 60 * 1000; // 14 days export interface SettingsState { /// This is an ordered list of node urls for each network and blockchain @@ -42,6 +37,8 @@ export interface SettingsState { externalMoneroRedeemAddress: string; /// The external Bitcoin refund address externalBitcoinRefundAddress: string; + /// UTC timestamp (in milliseconds) when default nodes were last applied + lastAppliedDefaultNodes?: number | null; } export enum RedeemPolicy { @@ -104,53 +101,8 @@ export enum FiatCurrency { Zar = "ZAR", } -export enum Network { - Testnet = "testnet", - Mainnet = "mainnet", -} - -export enum Blockchain { - Bitcoin = "bitcoin", - Monero = "monero", -} - const initialState: SettingsState = { - nodes: { - [Network.Testnet]: { - [Blockchain.Bitcoin]: [ - "ssl://ax101.blockeng.ch:60002", - "ssl://blackie.c3-soft.com:57006", - "ssl://v22019051929289916.bestsrv.de:50002", - "tcp://v22019051929289916.bestsrv.de:50001", - "tcp://electrum.blockstream.info:60001", - "ssl://electrum.blockstream.info:60002", - "ssl://blockstream.info:993", - "tcp://blockstream.info:143", - "ssl://testnet.qtornado.com:51002", - "tcp://testnet.qtornado.com:51001", - "tcp://testnet.aranguren.org:51001", - "ssl://testnet.aranguren.org:51002", - "ssl://testnet.qtornado.com:50002", - "ssl://bitcoin.devmole.eu:5010", - "tcp://bitcoin.devmole.eu:5000", - ], - [Blockchain.Monero]: [], - }, - [Network.Mainnet]: { - [Blockchain.Bitcoin]: [ - "ssl://electrum.blockstream.info:50002", - "tcp://electrum.blockstream.info:50001", - "ssl://bitcoin.stackwallet.com:50002", - "ssl://b.1209k.com:50002", - "tcp://electrum.coinucopia.io:50001", - "ssl://mainnet.foundationdevices.com:50002", - "tcp://bitcoin.lu.ke:50001", - "tcp://se-mma-crypto-payments-001.mullvad.net:50001", - "ssl://electrum.coinfinity.co:50002", - ], - [Blockchain.Monero]: [], - }, - }, + nodes: DEFAULT_NODES, theme: Theme.Dark, fetchFiatPrices: false, fiatCurrency: FiatCurrency.Usd, @@ -158,12 +110,14 @@ const initialState: SettingsState = { enableMoneroTor: false, // Default to not routing Monero traffic through Tor useMoneroRpcPool: true, // Default to using RPC pool userHasSeenIntroduction: false, + // TODO: Apply these regularly (like the default nodes) rendezvousPoints: DEFAULT_RENDEZVOUS_POINTS, donateToDevelopment: false, // Default to no donation moneroRedeemPolicy: RedeemPolicy.Internal, bitcoinRefundPolicy: RefundPolicy.Internal, externalMoneroRedeemAddress: "", externalBitcoinRefundAddress: "", + lastAppliedDefaultNodes: null, }; const alertsSlice = createSlice({ @@ -273,6 +227,62 @@ const alertsSlice = createSlice({ setBitcoinRefundAddress(slice, action: PayloadAction) { slice.externalBitcoinRefundAddress = action.payload; }, + applyDefaultNodes( + slice, + action: PayloadAction<{ + defaultNodes: Record>; + negativeNodesMainnet: string[]; + negativeNodesTestnet: string[]; + }>, + ) { + const now = Date.now(); + const twoWeeksInMs = 14 * 24 * 60 * 60 * 1000; + + // Check if we should apply defaults (first time or more than 2 weeks) + if ( + slice.lastAppliedDefaultNodes == null || + now - slice.lastAppliedDefaultNodes > MIN_TIME_BETWEEN_DEFAULT_NODES_APPLY + ) { + // Remove negative nodes from mainnet + slice.nodes[Network.Mainnet][Blockchain.Bitcoin] = slice.nodes[ + Network.Mainnet + ][Blockchain.Bitcoin].filter( + (node) => !action.payload.negativeNodesMainnet.includes(node), + ); + + // Remove negative nodes from testnet + slice.nodes[Network.Testnet][Blockchain.Bitcoin] = slice.nodes[ + Network.Testnet + ][Blockchain.Bitcoin].filter( + (node) => !action.payload.negativeNodesTestnet.includes(node), + ); + + // Add new default nodes if they don't exist (mainnet) + action.payload.defaultNodes[Network.Mainnet][ + Blockchain.Bitcoin + ].forEach((node) => { + if ( + !slice.nodes[Network.Mainnet][Blockchain.Bitcoin].includes(node) + ) { + slice.nodes[Network.Mainnet][Blockchain.Bitcoin].push(node); + } + }); + + // Add new default nodes if they don't exist (testnet) + action.payload.defaultNodes[Network.Testnet][ + Blockchain.Bitcoin + ].forEach((node) => { + if ( + !slice.nodes[Network.Testnet][Blockchain.Bitcoin].includes(node) + ) { + slice.nodes[Network.Testnet][Blockchain.Bitcoin].push(node); + } + }); + + // Update the timestamp + slice.lastAppliedDefaultNodes = now; + } + }, }, }); @@ -295,6 +305,7 @@ export const { setBitcoinRefundPolicy, setMoneroRedeemAddress, setBitcoinRefundAddress, + applyDefaultNodes, } = alertsSlice.actions; export default alertsSlice.reducer; diff --git a/src-gui/src/store/middleware/storeListener.ts b/src-gui/src/store/middleware/storeListener.ts index 3d546abc..b0917d47 100644 --- a/src-gui/src/store/middleware/storeListener.ts +++ b/src-gui/src/store/middleware/storeListener.ts @@ -23,9 +23,8 @@ import { setFetchFiatPrices, setFiatCurrency, setUseMoneroRpcPool, - Blockchain, - Network, } from "store/features/settingsSlice"; +import { Blockchain, Network } from "store/types"; import { fetchFeedbackMessagesViaHttp, updateRates } from "renderer/api"; import { RootState, store } from "renderer/store/storeRenderer"; import { swapProgressEventReceived } from "store/features/swapSlice"; diff --git a/src-gui/src/store/types.ts b/src-gui/src/store/types.ts new file mode 100644 index 00000000..ec6a46c7 --- /dev/null +++ b/src-gui/src/store/types.ts @@ -0,0 +1,9 @@ +export enum Network { + Testnet = "testnet", + Mainnet = "mainnet", +} + +export enum Blockchain { + Bitcoin = "bitcoin", + Monero = "monero", +} diff --git a/src-gui/yarn.lock b/src-gui/yarn.lock index bff3501d..6ce90148 100644 --- a/src-gui/yarn.lock +++ b/src-gui/yarn.lock @@ -826,6 +826,11 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.47.1.tgz#e5e0a0bae2c9d4858cc9b8dc508b2e10d7f0df8b" integrity sha512-CpKnYa8eHthJa3c+C38v/E+/KZyF1Jdh2Cz3DyKZqEWYgrM1IHFArXNWvBLPQCKUEsAqqKX27tTqVEFbDNUcOA== +"@rtsao/scc@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" + integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== + "@standard-schema/spec@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.0.0.tgz#f193b73dc316c4170f2e82a881da0f550d551b9c" @@ -1127,6 +1132,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== + "@types/lodash@^4.17.6": version "4.17.20" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.20.tgz#1ca77361d7363432d29f5e55950d9ec1e1c6ea93" @@ -1424,7 +1434,7 @@ array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2: call-bound "^1.0.3" is-array-buffer "^3.0.5" -array-includes@^3.1.6, array-includes@^3.1.8: +array-includes@^3.1.6, array-includes@^3.1.8, array-includes@^3.1.9: version "3.1.9" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.9.tgz#1f0ccaa08e90cdbc3eb433210f903ad0f17c3f3a" integrity sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ== @@ -1450,7 +1460,20 @@ array.prototype.findlast@^1.2.5: es-object-atoms "^1.0.0" es-shim-unscopables "^1.0.2" -array.prototype.flat@^1.3.1: +array.prototype.findlastindex@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz#cfa1065c81dcb64e34557c9b81d012f6a421c564" + integrity sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.4" + define-properties "^1.2.1" + es-abstract "^1.23.9" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + es-shim-unscopables "^1.1.0" + +array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz#534aaf9e6e8dd79fb6b9a9917f839ef1ec63afe5" integrity sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg== @@ -1783,6 +1806,13 @@ dayjs@^1.11.13: resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c" integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== +debug@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.7: version "4.4.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" @@ -1996,7 +2026,7 @@ es-set-tostringtag@^2.0.3, es-set-tostringtag@^2.1.0: has-tostringtag "^1.0.2" hasown "^2.0.2" -es-shim-unscopables@^1.0.2: +es-shim-unscopables@^1.0.2, es-shim-unscopables@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz#438df35520dac5d105f3943d927549ea3b00f4b5" integrity sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw== @@ -2051,6 +2081,47 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +eslint-import-resolver-node@^0.3.9: + version "0.3.9" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" + integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== + dependencies: + debug "^3.2.7" + is-core-module "^2.13.0" + resolve "^1.22.4" + +eslint-module-utils@^2.12.1: + version "2.12.1" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz#f76d3220bfb83c057651359295ab5854eaad75ff" + integrity sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw== + dependencies: + debug "^3.2.7" + +eslint-plugin-import@^2.32.0: + version "2.32.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz#602b55faa6e4caeaa5e970c198b5c00a37708980" + integrity sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA== + dependencies: + "@rtsao/scc" "^1.1.0" + array-includes "^3.1.9" + array.prototype.findlastindex "^1.2.6" + array.prototype.flat "^1.3.3" + array.prototype.flatmap "^1.3.3" + debug "^3.2.7" + doctrine "^2.1.0" + eslint-import-resolver-node "^0.3.9" + eslint-module-utils "^2.12.1" + hasown "^2.0.2" + is-core-module "^2.16.1" + is-glob "^4.0.3" + minimatch "^3.1.2" + object.fromentries "^2.0.8" + object.groupby "^1.0.3" + object.values "^1.2.1" + semver "^6.3.1" + string.prototype.trimend "^1.0.9" + tsconfig-paths "^3.15.0" + eslint-plugin-react@^7.35.0: version "7.37.5" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz#2975511472bdda1b272b34d779335c9b0e877065" @@ -2578,7 +2649,7 @@ is-callable@^1.2.7: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-core-module@^2.13.0, is-core-module@^2.16.0: +is-core-module@^2.13.0, is-core-module@^2.16.0, is-core-module@^2.16.1: version "2.16.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== @@ -2813,6 +2884,13 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== +json5@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== + dependencies: + minimist "^1.2.0" + json5@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" @@ -3018,6 +3096,11 @@ minimatch@^9.0.4: dependencies: brace-expansion "^2.0.1" +minimist@^1.2.0, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" @@ -3127,6 +3210,15 @@ object.fromentries@^2.0.8: es-abstract "^1.23.2" es-object-atoms "^1.0.0" +object.groupby@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.3.tgz#9b125c36238129f6f7b61954a1e7176148d5002e" + integrity sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + object.values@^1.1.6, object.values@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.1.tgz#deed520a50809ff7f75a7cfd4bc64c7a038c6216" @@ -3458,6 +3550,15 @@ resolve@^1.19.0: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^1.22.4: + version "1.22.11" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.11.tgz#aad857ce1ffb8bfa9b0b1ac29f1156383f68c262" + integrity sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ== + dependencies: + is-core-module "^2.16.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + resolve@^2.0.0-next.5: version "2.0.0-next.5" resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.5.tgz#6b0ec3107e671e52b68cd068ef327173b90dc03c" @@ -3802,6 +3903,11 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + strip-final-newline@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" @@ -3879,6 +3985,16 @@ tsconfck@^3.0.3: resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.1.6.tgz#da1f0b10d82237ac23422374b3fce1edb23c3ead" integrity sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w== +tsconfig-paths@^3.15.0: + version "3.15.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" + integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" diff --git a/swap-env/src/defaults.rs b/swap-env/src/defaults.rs index c9d1f811..86465e62 100644 --- a/swap-env/src/defaults.rs +++ b/swap-env/src/defaults.rs @@ -57,49 +57,42 @@ pub fn default_electrum_servers_mainnet() -> Vec { vec![ Url::parse("ssl://electrum.blockstream.info:50002") .expect("default electrum server url to be valid"), - Url::parse("tcp://electrum.blockstream.info:50001") - .expect("default electrum server url to be valid"), Url::parse("ssl://bitcoin.stackwallet.com:50002") .expect("default electrum server url to be valid"), Url::parse("ssl://b.1209k.com:50002").expect("default electrum server url to be valid"), - Url::parse("tcp://electrum.coinucopia.io:50001") - .expect("default electrum server url to be valid"), Url::parse("ssl://mainnet.foundationdevices.com:50002") .expect("default electrum server url to be valid"), Url::parse("tcp://bitcoin.lu.ke:50001").expect("default electrum server url to be valid"), - Url::parse("tcp://se-mma-crypto-payments-001.mullvad.net:50001") - .expect("default electrum server url to be valid"), Url::parse("ssl://electrum.coinfinity.co:50002") .expect("default electrum server url to be valid"), + Url::parse("tcp://electrum1.bluewallet.io:50001") + .expect("default electrum server url to be valid"), + Url::parse("tcp://electrum2.bluewallet.io:50001") + .expect("default electrum server url to be valid"), + Url::parse("tcp://electrum3.bluewallet.io:50001") + .expect("default electrum server url to be valid"), + Url::parse("ssl://btc-electrum.cakewallet.com:50002") + .expect("default electrum server url to be valid"), + Url::parse("tcp://bitcoin.aranguren.org:50001") + .expect("default electrum server url to be valid"), ] } pub fn default_electrum_servers_testnet() -> Vec { vec![ - Url::parse("ssl://ax101.blockeng.ch:60002") - .expect("default electrum server url to be valid"), Url::parse("ssl://blackie.c3-soft.com:57006") .expect("default electrum server url to be valid"), Url::parse("ssl://v22019051929289916.bestsrv.de:50002") .expect("default electrum server url to be valid"), Url::parse("tcp://v22019051929289916.bestsrv.de:50001") .expect("default electrum server url to be valid"), - Url::parse("tcp://electrum.blockstream.info:60001") - .expect("default electrum server url to be valid"), Url::parse("ssl://electrum.blockstream.info:60002") .expect("default electrum server url to be valid"), Url::parse("ssl://blockstream.info:993").expect("default electrum server url to be valid"), - Url::parse("tcp://blockstream.info:143").expect("default electrum server url to be valid"), - Url::parse("ssl://testnet.qtornado.com:51002") - .expect("default electrum server url to be valid"), - Url::parse("tcp://testnet.qtornado.com:51001") - .expect("default electrum server url to be valid"), Url::parse("tcp://testnet.aranguren.org:51001") .expect("default electrum server url to be valid"), Url::parse("ssl://testnet.aranguren.org:51002") .expect("default electrum server url to be valid"), - Url::parse("ssl://testnet.qtornado.com:50002") - .expect("default electrum server url to be valid"), Url::parse("ssl://bitcoin.devmole.eu:5010") .expect("default electrum server url to be valid"), Url::parse("tcp://bitcoin.devmole.eu:5000")