Skip to content

Commit

Permalink
feat: transaction broadcasts via block explorer (#802)
Browse files Browse the repository at this point in the history
* Add setting

* Add external broadcasting

* feat: add option for broadcasting claims and refunds via block explorer

* fix url

* fix error reporting

* Address comments

* Update setting description

* Broadcast to both explorer and backend

* remove dots

* fix ci

* abstract broadcasting logic into boltzClient
  • Loading branch information
SwapMarket authored Jan 20, 2025
1 parent 01e82e9 commit 47f89db
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 11 deletions.
3 changes: 3 additions & 0 deletions src/components/RefundButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export const RefundBtc = (props: {
setRefundAddress,
refundAddress,
notify,
externalBroadcast,
t,
} = useGlobalContext();
const { setSwap } = usePayContext();
Expand Down Expand Up @@ -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
Expand Down
13 changes: 11 additions & 2 deletions src/components/SwapChecker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand Down
28 changes: 28 additions & 0 deletions src/components/settings/BroadcastSetting.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<div
class="external-broadcast toggle"
title={t("broadcast_setting_tooltip")}
onClick={toggle}>
<span class={externalBroadcast() ? "active" : ""}>
{t("on")}
</span>
<span class={!externalBroadcast() ? "active" : ""}>
{t("off")}
</span>
</div>
</>
);
};

export default BroadcastSetting;
6 changes: 6 additions & 0 deletions src/components/settings/SettingsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -65,6 +66,11 @@ const SettingsMenu = () => {
tooltipLabel={"browsernotification_tooltip"}
settingElement={<BrowserNotification />}
/>
<Entry
label={"broadcast_setting"}
tooltipLabel={"broadcast_setting_tooltip"}
settingElement={<BroadcastSetting />}
/>
<Show when={config.network !== "mainnet"}>
<Entry
label={"reckless_mode_setting"}
Expand Down
14 changes: 14 additions & 0 deletions src/context/Global.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ export type GlobalContextType = {

setRdns: (address: string, rdns: string) => Promise<string>;
getRdnsForAddress: (address: string) => Promise<string | null>;

externalBroadcast: Accessor<boolean>;
setExternalBroadcast: Setter<boolean>;
};

const defaultReferral = () => {
Expand Down Expand Up @@ -366,6 +369,14 @@ const GlobalProvider = (props: { children: JSX.Element }) => {
values?: Record<string, unknown>,
) => string;

const [externalBroadcast, setExternalBroadcast] = makePersisted(
// eslint-disable-next-line solid/reactivity
createSignal<boolean>(false),
{
name: "externalBroadcast",
},
);

return (
<GlobalContext.Provider
value={{
Expand Down Expand Up @@ -425,6 +436,9 @@ const GlobalProvider = (props: { children: JSX.Element }) => {
getRdnsForAddress,
hardwareDerivationPath,
setHardwareDerivationPath,

externalBroadcast,
setExternalBroadcast,
}}>
{props.children}
</GlobalContext.Provider>
Expand Down
15 changes: 15 additions & 0 deletions src/i18n/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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: "中文",
Expand Down Expand Up @@ -961,6 +970,9 @@ const dict = {
no_wallet_connected: "未连接钱包",
no_lockup_transaction: "未找到锁仓交易",
routing_fee_limit: "最大路由费用",
broadcast_setting: "外部广播",
broadcast_setting_tooltip:
"除了Boltz后台外,还使用第三方区块浏览器广播认领和退款交易",
},
ja: {
language: "日本語",
Expand Down Expand Up @@ -1208,6 +1220,9 @@ const dict = {
no_wallet_connected: "財布はつながっていない!",
no_lockup_transaction: "ロックアップトランザクションが見つかりません",
routing_fee_limit: "ルーティング料金の上限",
broadcast_setting: "外部ブロードキャスト",
broadcast_setting_tooltip:
"Boltzバックエンドに加えて、サードパーティのブロックエクスプローラーを使用して請求および返金取引をブロードキャストします",
},
};

Expand Down
45 changes: 40 additions & 5 deletions src/utils/boltzClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -352,10 +352,45 @@ export const getNodeStats = () =>
export const getContracts = () =>
fetcher<Record<string, Contracts>>("/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,
Expand Down
10 changes: 8 additions & 2 deletions src/utils/claim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,8 @@ const claimChainSwap = async (
export const claim = async <T extends ReverseSwap | ChainSwap>(
swap: T,
swapStatusTransaction: { hex: string },
cooperative: boolean = true,
cooperative: boolean,
externalBroadcast: boolean,
): Promise<T | undefined> => {
const asset = getRelevantAssetForSwap(swap);
if (asset === RBTC) {
Expand All @@ -325,8 +326,13 @@ export const claim = async <T extends ReverseSwap | ChainSwap>(
}

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;
}
Expand Down
29 changes: 29 additions & 0 deletions src/utils/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
};
};
8 changes: 6 additions & 2 deletions src/utils/refund.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,11 +159,14 @@ const refundTaproot = async <T extends TransactionInterface>(
const broadcastRefund = async <T extends SubmarineSwap | ChainSwap>(
swap: T,
txConstructionResponse: Awaited<ReturnType<typeof refundTaproot>>,
externalBroadcast: boolean,
): Promise<T> => {
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) {
Expand All @@ -185,7 +188,8 @@ export const refund = async <T extends SubmarineSwap | ChainSwap>(
swap: T,
refundAddress: string,
transactionToRefund: { hex: string; timeoutBlockHeight: number },
cooperative: boolean = true,
cooperative: boolean,
externalBroadcast: boolean,
): Promise<T> => {
log.info(`Refunding swap ${swap.id}: `, swap);

Expand Down Expand Up @@ -246,5 +250,5 @@ export const refund = async <T extends SubmarineSwap | ChainSwap>(
};
}

return broadcastRefund(swap, refundTransaction);
return broadcastRefund(swap, refundTransaction, externalBroadcast);
};

0 comments on commit 47f89db

Please sign in to comment.