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);
};