diff --git a/src/components/RefundButton.tsx b/src/components/RefundButton.tsx index d7f8fc39..48ee6423 100644 --- a/src/components/RefundButton.tsx +++ b/src/components/RefundButton.tsx @@ -119,6 +119,7 @@ export const RefundBtc = (props: { setRefundAddress, refundAddress, notify, + externalBroadcast, t, } = useGlobalContext(); const { setSwap } = usePayContext(); @@ -165,6 +166,8 @@ export const RefundBtc = (props: { props.swap(), refundAddress(), lockupTransaction(), + true, + externalBroadcast(), ); // save refundTx into swaps json and set it to the current swap diff --git a/src/components/SwapChecker.tsx b/src/components/SwapChecker.tsx index 8695669f..a533f0a6 100644 --- a/src/components/SwapChecker.tsx +++ b/src/components/SwapChecker.tsx @@ -171,8 +171,15 @@ export const SwapChecker = () => { setSwapStatusTransaction, setFailureReason, } = usePayContext(); - const { notify, updateSwapStatus, getSwap, getSwaps, setSwapStorage, t } = - useGlobalContext(); + const { + notify, + updateSwapStatus, + getSwap, + getSwaps, + setSwapStorage, + externalBroadcast, + t, + } = useGlobalContext(); let ws: BoltzWebSocket | undefined = undefined; @@ -261,6 +268,8 @@ export const SwapChecker = () => { const res = await claim( currentSwap as ReverseSwap | ChainSwap, data.transaction as { hex: string }, + true, + externalBroadcast(), ); const claimedSwap = await getSwap(res.id); claimedSwap.claimTx = res.claimTx; diff --git a/src/components/settings/BroadcastSetting.tsx b/src/components/settings/BroadcastSetting.tsx new file mode 100644 index 00000000..c8f065c2 --- /dev/null +++ b/src/components/settings/BroadcastSetting.tsx @@ -0,0 +1,28 @@ +import { useGlobalContext } from "../../context/Global"; + +const BroadcastSetting = () => { + const { externalBroadcast, setExternalBroadcast, t } = useGlobalContext(); + + const toggle = (evt: MouseEvent) => { + setExternalBroadcast(!externalBroadcast()); + evt.stopPropagation(); + }; + + return ( + <> +
+ + {t("on")} + + + {t("off")} + +
+ + ); +}; + +export default BroadcastSetting; diff --git a/src/components/settings/SettingsMenu.tsx b/src/components/settings/SettingsMenu.tsx index 36824170..c380014b 100644 --- a/src/components/settings/SettingsMenu.tsx +++ b/src/components/settings/SettingsMenu.tsx @@ -7,6 +7,7 @@ import { useGlobalContext } from "../../context/Global"; import type { DictKey } from "../../i18n/i18n"; import "../../style/settings.scss"; import AudioNotificationSetting from "./AudioNotificationSetting"; +import BroadcastSetting from "./BroadcastSetting"; import BrowserNotification from "./BrowserNotification"; import Denomination from "./Denomination"; import Logs from "./Logs"; @@ -65,6 +66,11 @@ const SettingsMenu = () => { tooltipLabel={"browsernotification_tooltip"} settingElement={} /> + } + /> Promise; getRdnsForAddress: (address: string) => Promise; + + externalBroadcast: Accessor; + setExternalBroadcast: Setter; }; const defaultReferral = () => { @@ -366,6 +369,14 @@ const GlobalProvider = (props: { children: JSX.Element }) => { values?: Record, ) => string; + const [externalBroadcast, setExternalBroadcast] = makePersisted( + // eslint-disable-next-line solid/reactivity + createSignal(false), + { + name: "externalBroadcast", + }, + ); + return ( { getRdnsForAddress, hardwareDerivationPath, setHardwareDerivationPath, + + externalBroadcast, + setExternalBroadcast, }}> {props.children} diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts index 0930c805..6931cced 100644 --- a/src/i18n/i18n.ts +++ b/src/i18n/i18n.ts @@ -240,6 +240,9 @@ const dict = { no_wallet_connected: "No wallet connected", no_lockup_transaction: "No lockup transaction found", routing_fee_limit: "Routing fee limit", + broadcast_setting: "External Broadcast", + broadcast_setting_tooltip: + "Use third-party block explorers for broadcasting claim and refund transactions in addition to Boltz backend", }, de: { language: "Deutsch", @@ -492,6 +495,9 @@ const dict = { no_wallet_connected: "Kein Wallet verbunden", no_lockup_transaction: "Keine Lockup-Transaktion gefunden", routing_fee_limit: "Routing Gebühr Limit", + broadcast_setting: "Externe Sendung", + broadcast_setting_tooltip: + "Verwenden Sie Drittanbieter-Blockexplorer, um Anspruchs- und Rückerstattungstransaktionen zusätzlich zum Boltz-Backend zu senden", }, es: { language: "Español", @@ -740,6 +746,9 @@ const dict = { no_wallet_connected: "No hay monedero conectado", no_lockup_transaction: "No se encontró ninguna transacción de lockup", routing_fee_limit: "Límite de la tarifa de enrutamiento", + broadcast_setting: "Transmisión externa", + broadcast_setting_tooltip: + "Utilice exploradores de bloques de terceros para transmitir transacciones de reclamo y reembolso además del backend de Boltz", }, zh: { language: "中文", @@ -961,6 +970,9 @@ const dict = { no_wallet_connected: "未连接钱包", no_lockup_transaction: "未找到锁仓交易", routing_fee_limit: "最大路由费用", + broadcast_setting: "外部广播", + broadcast_setting_tooltip: + "除了Boltz后台外,还使用第三方区块浏览器广播认领和退款交易", }, ja: { language: "日本語", @@ -1208,6 +1220,9 @@ const dict = { no_wallet_connected: "財布はつながっていない!", no_lockup_transaction: "ロックアップトランザクションが見つかりません", routing_fee_limit: "ルーティング料金の上限", + broadcast_setting: "外部ブロードキャスト", + broadcast_setting_tooltip: + "Boltzバックエンドに加えて、サードパーティのブロックエクスプローラーを使用して請求および返金取引をブロードキャストします", }, }; diff --git a/src/utils/boltzClient.ts b/src/utils/boltzClient.ts index e1b9d8b1..6c188c28 100644 --- a/src/utils/boltzClient.ts +++ b/src/utils/boltzClient.ts @@ -5,7 +5,7 @@ import { Transaction as LiquidTransaction } from "liquidjs-lib"; import { config } from "../config"; import { SwapType } from "../consts/Enums"; -import { fetcher } from "./helper"; +import { broadcastToExplorer, fetcher } from "./helper"; import { validateInvoiceForOffer } from "./invoice"; const cooperativeErrorMessage = "cooperative signatures for swaps are disabled"; @@ -352,10 +352,45 @@ export const getNodeStats = () => export const getContracts = () => fetcher>("/v2/chain/contracts"); -export const broadcastTransaction = (asset: string, txHex: string) => - fetcher<{ id: string }>(`/v2/chain/${asset}/transaction`, { - hex: txHex, - }); +export const broadcastTransaction = async ( + asset: string, + txHex: string, + externalBroadcast: boolean, +): Promise<{ + id: string; +}> => { + const promises: Promise<{ + id: string; + }>[] = []; + + // broadcast to Boltz backend + promises.push( + fetcher<{ id: string }>(`/v2/chain/${asset}/transaction`, { + hex: txHex, + }), + ); + + // broadcast to block explorer + if (externalBroadcast) { + promises.push(broadcastToExplorer(asset, txHex)); + } + + // Wait for all promises to settle + const results = await Promise.allSettled(promises); + let reason = ""; + + // Process the results + for (const result of results) { + if (result.status === "fulfilled") { + // Return the first successful transaction ID + return result.value; + } + reason = result.reason; + } + + // If no promises resolved successfully, return the last reason + throw reason; +}; export const getLockupTransaction = async ( id: string, diff --git a/src/utils/claim.ts b/src/utils/claim.ts index c9c61ebc..dea1656b 100644 --- a/src/utils/claim.ts +++ b/src/utils/claim.ts @@ -298,7 +298,8 @@ const claimChainSwap = async ( export const claim = async ( swap: T, swapStatusTransaction: { hex: string }, - cooperative: boolean = true, + cooperative: boolean, + externalBroadcast: boolean, ): Promise => { const asset = getRelevantAssetForSwap(swap); if (asset === RBTC) { @@ -325,8 +326,13 @@ export const claim = async ( } log.debug("Broadcasting claim transaction"); - const res = await broadcastTransaction(asset, claimTransaction.toHex()); + const res = await broadcastTransaction( + asset, + claimTransaction.toHex(), + externalBroadcast, + ); log.debug("Claim transaction broadcast result", res); + if (res.id) { swap.claimTx = res.id; } diff --git a/src/utils/helper.ts b/src/utils/helper.ts index 5e086ce3..1c8b9de3 100644 --- a/src/utils/helper.ts +++ b/src/utils/helper.ts @@ -131,3 +131,32 @@ export const parsePrivateKey = (privateKey: string): ECPairInterface => { return ECPair.fromWIF(privateKey); } }; + +// posts transaction to a block explorer +export const broadcastToExplorer = async ( + asset: string, + txHex: string, +): Promise<{ id: string }> => { + const basePath = chooseUrl(config.assets[asset].blockExplorerUrl); + + const opts: RequestInit = { + method: "POST", + body: txHex, + }; + + const apiUrl = basePath + "/api/tx"; + const response = await fetch(apiUrl, opts); + if (!response.ok) { + try { + const body = await response.json(); + throw formatError(body); + } catch { + // If parsing JSON fails, throw a generic error with status text + throw response.statusText; + } + } + + return { + id: await response.text(), + }; +}; diff --git a/src/utils/refund.ts b/src/utils/refund.ts index ec219f01..6d35bd70 100644 --- a/src/utils/refund.ts +++ b/src/utils/refund.ts @@ -159,11 +159,14 @@ const refundTaproot = async ( const broadcastRefund = async ( swap: T, txConstructionResponse: Awaited>, + externalBroadcast: boolean, ): Promise => { try { + log.debug("Broadcasting refund transaction"); const res = await broadcastTransaction( swap.assetSend, txConstructionResponse.transaction.toHex(), + externalBroadcast, ); log.debug("Refund broadcast result", res); if (res.id) { @@ -185,7 +188,8 @@ export const refund = async ( swap: T, refundAddress: string, transactionToRefund: { hex: string; timeoutBlockHeight: number }, - cooperative: boolean = true, + cooperative: boolean, + externalBroadcast: boolean, ): Promise => { log.info(`Refunding swap ${swap.id}: `, swap); @@ -246,5 +250,5 @@ export const refund = async ( }; } - return broadcastRefund(swap, refundTransaction); + return broadcastRefund(swap, refundTransaction, externalBroadcast); };