diff --git a/e2e/arbitrum/lbtcUsdt0.spec.ts b/e2e/arbitrum/lbtcUsdt0.spec.ts
index 32e5288da..292592895 100644
--- a/e2e/arbitrum/lbtcUsdt0.spec.ts
+++ b/e2e/arbitrum/lbtcUsdt0.spec.ts
@@ -1,18 +1,30 @@
import type { Page } from "@playwright/test";
+import { oftAbi } from "boltz-swaps/oft";
import {
type Address,
+ type Hex,
type PublicClient,
+ createPublicClient,
+ createWalletClient,
+ defineChain,
getAddress,
+ http,
+ isAddressEqual,
parseAbi,
+ parseEventLogs,
parseUnits,
} from "viem";
import { config } from "../../src/config";
+import dict from "../../src/i18n/i18n";
import { expect, shouldRunArbitrumE2e, test } from "../fixtures/arbitrum";
+import { injectWalletProvider } from "../fixtures/ethereum";
import {
elementsSendToAddress,
generateBitcoinBlock,
generateLiquidBlock,
+ getCurrentSwapId,
+ getLiquidAddress,
verifyRescueFile,
} from "../utils";
@@ -29,12 +41,37 @@ const erc20Abi = parseAbi([
]);
const lbtcSendAmount = "0.001";
-const quoteRequestTimeout = 60_000;
-const quoteReadinessTimeout = 90_000;
+const usdt0EthSendAmount = "40";
const swapClaimTimeout = 75_000;
const swapClaimTestTimeout = 150_000;
+const rpcTimeout = 30_000;
+const ethereumRpcReadyTimeout = 15_000;
+const quoteRequestTimeout = 10_000;
+const quoteReadinessTimeout = 30_000;
+const networkSelectorProbeTimeout = 500;
+const assetSelectTimeout = 3_000;
+const uiQuoteTimeout = 30_000;
+const uiActionTimeout = 10_000;
+const walletConnectTimeout = 10_000;
+const providerModalTimeout = 1_000;
+const swapCreateTimeout = 30_000;
+const bridgeActionTimeout = 20_000;
+const bridgeTxTimeout = 20_000;
+const stablesBridgeTestTimeout = 90_000;
-const getRegtestTokenAddress = (asset: "USDT0" | "TBTC"): Address => {
+const ethereumRpcUrl = () =>
+ `http://127.0.0.1:${process.env.ETHEREUM_E2E_PORT ?? "18546"}`;
+
+const ethereumE2eChain = defineChain({
+ id: 1,
+ name: "Ethereum E2E",
+ nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
+ rpcUrls: { default: { http: [ethereumRpcUrl()] } },
+});
+
+const getRegtestTokenAddress = (
+ asset: "USDT0" | "USDT0-ETH" | "TBTC",
+): Address => {
const address = config.assets?.[asset]?.token?.address;
if (address === undefined) {
throw new Error(`missing ${asset} token address`);
@@ -43,6 +80,61 @@ const getRegtestTokenAddress = (asset: "USDT0" | "TBTC"): Address => {
return getAddress(address);
};
+const createEthereumClient = () => {
+ const rpcUrl = ethereumRpcUrl();
+ return createPublicClient({
+ chain: {
+ ...ethereumE2eChain,
+ rpcUrls: { default: { http: [rpcUrl] } },
+ },
+ transport: http(rpcUrl, { timeout: rpcTimeout }),
+ });
+};
+
+const waitForEthereumRpc = async (publicClient: PublicClient) => {
+ await expect
+ .poll(
+ async () => {
+ try {
+ return await publicClient.getChainId();
+ } catch {
+ return 0;
+ }
+ },
+ {
+ timeout: ethereumRpcReadyTimeout,
+ message: "Ethereum e2e RPC is ready",
+ },
+ )
+ .toBe(ethereumE2eChain.id);
+};
+
+const getStablesE2eAccountIndex = () => {
+ const index = Number(process.env.STABLES_E2E_ACCOUNT_INDEX ?? "1");
+ if (!Number.isInteger(index) || index < 0) {
+ throw new Error(
+ "STABLES_E2E_ACCOUNT_INDEX must be a non-negative integer",
+ );
+ }
+
+ return index;
+};
+
+const getStablesE2eWalletAddress = async (
+ publicClient: PublicClient,
+): Promise
=> {
+ const accounts = (await publicClient.request({
+ method: "eth_accounts",
+ params: [],
+ } as never)) as Address[];
+ const walletAddress = accounts[getStablesE2eAccountIndex()];
+ if (walletAddress === undefined) {
+ throw new Error("STABLES_E2E_ACCOUNT_INDEX is not available in Anvil");
+ }
+
+ return getAddress(walletAddress);
+};
+
const waitForDexQuote = async (args: {
tokenIn: Address;
tokenOut: Address;
@@ -80,10 +172,67 @@ const waitForDexQuote = async (args: {
throw new Error(`quote not ready for ${args.label}: ${String(lastError)}`);
};
+type StoredBridgeSwap = {
+ bridge?: { txHash?: Hex };
+};
+
+const getStoredSwap = async (
+ page: Page,
+ id: string,
+): Promise =>
+ await page.evaluate(
+ async ({ id }) =>
+ await new Promise((resolve, reject) => {
+ const request = indexedDB.open("swaps");
+ request.onerror = () => reject(request.error);
+ request.onsuccess = () => {
+ const db = request.result;
+ const transaction = db.transaction(
+ "keyvaluepairs",
+ "readonly",
+ );
+ const getRequest = transaction
+ .objectStore("keyvaluepairs")
+ .get(id);
+ getRequest.onsuccess = () => {
+ db.close();
+ resolve(getRequest.result ?? null);
+ };
+ getRequest.onerror = () => reject(getRequest.error);
+ };
+ }),
+ { id },
+ );
+
+const waitForBridgeTxHash = async (page: Page, id: string): Promise => {
+ await expect
+ .poll(async () => (await getStoredSwap(page, id))?.bridge?.txHash, {
+ timeout: bridgeTxTimeout,
+ })
+ .toMatch(/^0x[0-9a-fA-F]{64}$/);
+
+ return (await getStoredSwap(page, id))!.bridge!.txHash!;
+};
+
+const bridgeCanonicalAsset = (asset: string) =>
+ asset.startsWith("USDT0") ? "USDT0" : asset;
+
const chooseAsset = async (page: Page, asset: string) => {
- await page.getByTestId(`select-${asset}`).click();
+ const canonical = bridgeCanonicalAsset(asset);
+ await page.getByTestId(`select-${canonical}`).click();
+
+ if (
+ canonical !== asset ||
+ (await page
+ .getByTestId("network-back")
+ .isVisible({ timeout: networkSelectorProbeTimeout })
+ .catch(() => false))
+ ) {
+ await page.getByTestId(`select-${asset}`).click();
+ }
+
await expect(page.locator(".asset-select-overlay")).toBeHidden({
- timeout: 5_000,
+ timeout: assetSelectTimeout,
});
};
@@ -105,7 +254,7 @@ const createSwap = async (
receiveAsset: string,
destinationAddress: string,
sendAmount: string,
- options?: { skipGoto?: boolean },
+ options?: { skipGoto?: boolean; walletAddress?: Address },
) => {
if (options?.skipGoto !== true) {
await page.goto("/");
@@ -115,17 +264,37 @@ const createSwap = async (
await page.getByTestId("sendAmount").fill(sendAmount);
const receiveAmount = page.getByTestId("receiveAmount");
- await expect(receiveAmount).not.toHaveValue("", { timeout: 60_000 });
- await expect(receiveAmount).not.toHaveValue("0", { timeout: 60_000 });
+ await expect(receiveAmount).not.toHaveValue("", {
+ timeout: uiQuoteTimeout,
+ });
+ await expect(receiveAmount).not.toHaveValue("0", {
+ timeout: uiQuoteTimeout,
+ });
+
+ if (options?.walletAddress !== undefined) {
+ await connectWallet(page, options.walletAddress);
+ }
const createButton = page.getByTestId("create-swap-button");
- await expect(createButton).toBeEnabled({ timeout: 60_000 });
+ await expect(createButton).toBeEnabled({ timeout: uiActionTimeout });
await createButton.click();
- await verifyRescueFile(page);
- await expect(page.locator("div[data-status='swap.created']")).toBeVisible({
- timeout: 60_000,
+ const downloadButton = page.getByRole("button", {
+ name: dict.en.download_new_key,
});
+ const swapReady = page
+ .locator("div[data-status='swap.created']")
+ .or(page.locator("div[data-status='invoice.set']"));
+
+ await expect(swapReady.or(downloadButton)).toBeVisible({
+ timeout: swapCreateTimeout,
+ });
+ if (await downloadButton.isVisible().catch(() => false)) {
+ await verifyRescueFile(page);
+ }
+ await expect(swapReady).toBeVisible({ timeout: swapCreateTimeout });
+
+ return getCurrentSwapId(page);
};
const getTokenBalance = async (
@@ -140,6 +309,102 @@ const getTokenBalance = async (
args: [owner],
});
+const expectEthereumWalletReady = async (
+ publicClient: PublicClient,
+ owner: Address,
+) => {
+ const token = getRegtestTokenAddress("USDT0-ETH");
+ const tokenCode = await publicClient.getCode({ address: token });
+ if (tokenCode === undefined || tokenCode === "0x") {
+ throw new Error("Ethereum e2e fork is missing the USDT0-ETH contract");
+ }
+
+ expect(await publicClient.getBalance({ address: owner })).toBeGreaterThan(
+ 0n,
+ );
+ expect(
+ await getTokenBalance(publicClient, token, owner),
+ ).toBeGreaterThanOrEqual(parseUnits(usdt0EthSendAmount, 6));
+};
+
+const connectWallet = async (page: Page, walletAddress: Address) => {
+ const connect = page.getByRole("button", {
+ name: new RegExp(dict.en.connect_wallet, "i"),
+ });
+ const connectedAddress = page.locator(`text=${walletAddress.slice(0, 8)}`);
+
+ await expect(connectedAddress.or(connect)).toBeVisible({
+ timeout: walletConnectTimeout,
+ });
+
+ if (await connect.isVisible().catch(() => false)) {
+ await connect.click();
+
+ const modal = page.locator("[data-testid='wallet-connect-modal']");
+ if (
+ await modal
+ .isVisible({ timeout: providerModalTimeout })
+ .catch(() => false)
+ ) {
+ await modal
+ .locator(".provider-modal-entry-wrapper")
+ .filter({ hasText: /metamask|browser native/i })
+ .first()
+ .click();
+ }
+ }
+
+ await expect(connectedAddress).toBeVisible({
+ timeout: walletConnectTimeout,
+ });
+};
+
+const clickSendBridge = async (page: Page, walletAddress: Address) => {
+ await connectWallet(page, walletAddress);
+
+ const approve = page.getByRole("button", { name: /^approve$/i });
+ const send = page.getByRole("button", { name: /^send$/i });
+
+ await expect(send.or(approve)).toBeVisible({
+ timeout: bridgeActionTimeout,
+ });
+ if (await approve.isVisible().catch(() => false)) {
+ await approve.click();
+ }
+
+ await expect(send).toBeEnabled({ timeout: bridgeActionTimeout });
+ await send.click();
+};
+
+const expectOftSendTx = async (
+ publicClient: PublicClient,
+ txHash: Hex,
+ walletAddress: Address,
+) => {
+ const transaction = await publicClient.getTransaction({
+ hash: txHash,
+ });
+ const receipt = await publicClient.waitForTransactionReceipt({
+ hash: txHash,
+ timeout: bridgeTxTimeout,
+ });
+ expect(receipt.status).toBe("success");
+
+ const [sent] = parseEventLogs({
+ abi: oftAbi,
+ eventName: "OFTSent",
+ logs: receipt.logs,
+ });
+
+ expect(sent).toBeDefined();
+ expect(isAddressEqual(sent.address, getAddress(transaction.to!))).toBe(
+ true,
+ );
+ expect(isAddressEqual(sent.args.fromAddress, walletAddress)).toBe(true);
+ expect(sent.args.amountSentLD).toBeGreaterThan(0n);
+ expect(sent.args.amountReceivedLD).toBeGreaterThan(0n);
+};
+
describeArbitrumE2e("Arbitrum stablecoin e2e", () => {
test.describe.configure({ mode: "serial" });
@@ -200,4 +465,50 @@ describeArbitrumE2e("Arbitrum stablecoin e2e", () => {
);
expect(balanceAfter).toBeGreaterThan(balanceBefore);
});
+
+ test("sends USDT0-ETH OFT bridge tx for an L-BTC chain swap", async ({
+ page,
+ }) => {
+ test.setTimeout(stablesBridgeTestTimeout);
+
+ const ethereum = createEthereumClient();
+ const walletAddress = await getStablesE2eWalletAddress(ethereum);
+ const walletClient = createWalletClient({
+ account: walletAddress,
+ chain: ethereumE2eChain,
+ transport: http(ethereumRpcUrl(), { timeout: rpcTimeout }),
+ });
+
+ await waitForEthereumRpc(ethereum);
+ await expectEthereumWalletReady(ethereum, walletAddress);
+ await injectWalletProvider({
+ page,
+ publicClient: ethereum,
+ walletClient,
+ chain: ethereumE2eChain,
+ });
+
+ await waitForDexQuote({
+ tokenIn: getRegtestTokenAddress("USDT0"),
+ tokenOut: getRegtestTokenAddress("TBTC"),
+ amountIn: parseUnits("39.996", 6),
+ label: "USDT0 -> TBTC",
+ });
+
+ const swapId = await createSwap(
+ page,
+ "USDT0-ETH",
+ "L-BTC",
+ await getLiquidAddress(),
+ usdt0EthSendAmount,
+ { walletAddress },
+ );
+ await clickSendBridge(page, walletAddress);
+
+ await expectOftSendTx(
+ ethereum,
+ await waitForBridgeTxHash(page, swapId),
+ walletAddress,
+ );
+ });
});
diff --git a/e2e/fixtures/ethereum.ts b/e2e/fixtures/ethereum.ts
index 73cfc0b24..3e6653458 100644
--- a/e2e/fixtures/ethereum.ts
+++ b/e2e/fixtures/ethereum.ts
@@ -1,5 +1,7 @@
-import { test as base } from "@playwright/test";
+import { type Page, test as base } from "@playwright/test";
import {
+ type Chain,
+ type PublicClient,
createPublicClient,
createWalletClient,
defineChain,
@@ -20,6 +22,170 @@ type EthereumFixtures = {
injectProvider: () => Promise;
};
+export const injectWalletProvider = async ({
+ page,
+ publicClient,
+ walletClient,
+ chain,
+}: {
+ page: Page;
+ publicClient: PublicClient;
+ walletClient: ReturnType;
+ chain: Chain;
+}) => {
+ await page.exposeFunction(
+ "__rpc",
+ async (method: string, params: unknown[]) => {
+ return await publicClient.request({
+ method,
+ params,
+ } as never);
+ },
+ );
+
+ await page.exposeFunction(
+ "__sendTx",
+ async (tx: {
+ to?: string;
+ value?: string;
+ data?: string;
+ gas?: string;
+ gasPrice?: string;
+ maxFeePerGas?: string;
+ maxPriorityFeePerGas?: string;
+ nonce?: string;
+ }) => {
+ return await walletClient.sendTransaction({
+ account: walletClient.account,
+ chain,
+ to: tx.to as `0x${string}` | undefined,
+ value: tx.value ? BigInt(tx.value) : undefined,
+ data: tx.data as `0x${string}` | undefined,
+ gas: tx.gas ? BigInt(tx.gas) : undefined,
+ gasPrice: tx.gasPrice ? BigInt(tx.gasPrice) : undefined,
+ maxFeePerGas: tx.maxFeePerGas
+ ? BigInt(tx.maxFeePerGas)
+ : undefined,
+ maxPriorityFeePerGas: tx.maxPriorityFeePerGas
+ ? BigInt(tx.maxPriorityFeePerGas)
+ : undefined,
+ nonce: tx.nonce ? Number(tx.nonce) : undefined,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ } as any);
+ },
+ );
+
+ const walletAddress = walletClient.account!.address;
+ const chainIdHex = "0x" + chain.id.toString(16);
+
+ await page.addInitScript(
+ ({ addr, chainId }) => {
+ const listeners: Record<
+ string,
+ Array<(...args: unknown[]) => void>
+ > = {};
+
+ const provider = {
+ isMetaMask: true,
+ isConnected: () => true,
+ chainId,
+ selectedAddress: addr,
+
+ request: async ({
+ method,
+ params,
+ }: {
+ method: string;
+ params?: unknown[];
+ }) => {
+ switch (method) {
+ case "eth_requestAccounts":
+ case "eth_accounts":
+ return [addr];
+ case "eth_chainId":
+ return chainId;
+ case "net_version":
+ return String(parseInt(chainId, 16));
+ case "wallet_switchEthereumChain":
+ setTimeout(() => {
+ (listeners["chainChanged"] || []).forEach(
+ (cb) => cb(chainId),
+ );
+ }, 100);
+ return null;
+ case "wallet_addEthereumChain":
+ return null;
+ case "eth_sendTransaction":
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return
+ return await (window as any).__sendTx(
+ (params as unknown[])[0],
+ );
+ default:
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return
+ return await (window as any).__rpc(
+ method,
+ params || [],
+ );
+ }
+ },
+
+ send: (method: string, params?: unknown[]) =>
+ provider.request({ method, params }),
+
+ on: (event: string, callback: (...args: unknown[]) => void) => {
+ if (!listeners[event]) listeners[event] = [];
+ listeners[event].push(callback);
+ },
+ removeListener: (
+ event: string,
+ callback: (...args: unknown[]) => void,
+ ) => {
+ if (listeners[event]) {
+ listeners[event] = listeners[event].filter(
+ (cb) => cb !== callback,
+ );
+ }
+ },
+ removeAllListeners: (event?: string) => {
+ if (event) {
+ delete listeners[event];
+ } else {
+ Object.keys(listeners).forEach(
+ (key) => delete listeners[key],
+ );
+ }
+ },
+ };
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (window as any).ethereum = provider;
+
+ const announceProvider = () => {
+ window.dispatchEvent(
+ new CustomEvent("eip6963:announceProvider", {
+ detail: Object.freeze({
+ info: {
+ uuid: "playwright-mock",
+ name: "MetaMask",
+ icon: "data:image/svg+xml,",
+ rdns: "io.metamask",
+ },
+ provider,
+ }),
+ }),
+ );
+ };
+
+ window.addEventListener(
+ "eip6963:requestProvider",
+ announceProvider,
+ );
+ announceProvider();
+ },
+ { addr: walletAddress, chainId: chainIdHex },
+ );
+};
+
export const test = base.extend({
// eslint-disable-next-line no-empty-pattern
walletClient: async ({}, use) => {
@@ -54,148 +220,12 @@ export const test = base.extend({
transport: http(),
});
- await page.exposeFunction(
- "__rpc",
- async (method: string, params: unknown[]) => {
- return await publicClient.request({
- method,
- params,
- } as never);
- },
- );
-
- await page.exposeFunction(
- "__sendTx",
- async (tx: {
- to?: string;
- value?: string;
- data?: string;
- gas?: string;
- }) => {
- return await walletClient.sendTransaction({
- account: walletClient.account,
- chain: rskRegtest,
- to: tx.to as `0x${string}`,
- value: tx.value ? BigInt(tx.value) : undefined,
- data: tx.data as `0x${string}`,
- gas: tx.gas ? BigInt(tx.gas) : undefined,
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- } as any);
- },
- );
-
- const walletAddress = walletClient.account!.address;
- const chainIdHex = "0x" + rskRegtest.id.toString(16);
-
- await page.addInitScript(
- ({ addr, chainId }) => {
- const listeners: Record<
- string,
- Array<(...args: unknown[]) => void>
- > = {};
-
- const provider = {
- isMetaMask: true,
- isConnected: () => true,
- chainId,
- selectedAddress: addr,
-
- request: async ({
- method,
- params,
- }: {
- method: string;
- params?: unknown[];
- }) => {
- switch (method) {
- case "eth_requestAccounts":
- case "eth_accounts":
- return [addr];
- case "eth_chainId":
- return chainId;
- case "net_version":
- return String(parseInt(chainId, 16));
- case "wallet_switchEthereumChain":
- setTimeout(() => {
- (
- listeners["chainChanged"] || []
- ).forEach((cb) => cb(chainId));
- }, 100);
- return null;
- case "wallet_addEthereumChain":
- return null;
- case "eth_sendTransaction":
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return
- return await (window as any).__sendTx(
- (params as unknown[])[0],
- );
- default:
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return
- return await (window as any).__rpc(
- method,
- params || [],
- );
- }
- },
-
- send: (method: string, params?: unknown[]) =>
- provider.request({ method, params }),
-
- on: (
- event: string,
- callback: (...args: unknown[]) => void,
- ) => {
- if (!listeners[event]) listeners[event] = [];
- listeners[event].push(callback);
- },
- removeListener: (
- event: string,
- callback: (...args: unknown[]) => void,
- ) => {
- if (listeners[event]) {
- listeners[event] = listeners[event].filter(
- (cb) => cb !== callback,
- );
- }
- },
- removeAllListeners: (event?: string) => {
- if (event) {
- delete listeners[event];
- } else {
- Object.keys(listeners).forEach(
- (key) => delete listeners[key],
- );
- }
- },
- };
-
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (window as any).ethereum = provider;
-
- const announceProvider = () => {
- window.dispatchEvent(
- new CustomEvent("eip6963:announceProvider", {
- detail: Object.freeze({
- info: {
- uuid: "playwright-mock",
- name: "MetaMask",
- icon: "data:image/svg+xml,",
- rdns: "io.metamask",
- },
- provider,
- }),
- }),
- );
- };
-
- window.addEventListener(
- "eip6963:requestProvider",
- announceProvider,
- );
- announceProvider();
- },
- { addr: walletAddress, chainId: chainIdHex },
- );
+ await injectWalletProvider({
+ page,
+ publicClient,
+ walletClient,
+ chain: rskRegtest,
+ });
};
await use(inject);
diff --git a/src/components/Fees.tsx b/src/components/Fees.tsx
index aa9d84cfb..958ec3181 100644
--- a/src/components/Fees.tsx
+++ b/src/components/Fees.tsx
@@ -183,6 +183,13 @@ const Fees = () => {
}
const initiatingPair = pair();
+ if (initiatingPair.needsNetworkForQuote) {
+ setMinimum(0);
+ setMaximum(Number.MAX_SAFE_INTEGER);
+ setLimitsLoading(false);
+ return;
+ }
+
setLimitsLoading(true);
void Promise.all([
initiatingPair.getMinimum(),
diff --git a/src/components/LockupEvm.tsx b/src/components/LockupEvm.tsx
index 9800b60bf..f4170d854 100644
--- a/src/components/LockupEvm.tsx
+++ b/src/components/LockupEvm.tsx
@@ -588,6 +588,7 @@ const LockupEvm = (props: {
timeoutBlockHeight: number;
hops?: EncodedHop[];
bridge?: BridgeDetail;
+ bridgeAmount?: bigint;
}) => {
const { slippage } = useGlobalContext();
const { getErc20Swap, signer } = useWeb3Signer();
@@ -623,6 +624,11 @@ const LockupEvm = (props: {
createEffect(() => {
void (async () => {
if (props.bridge !== undefined) {
+ if (props.bridgeAmount !== undefined) {
+ setBridgeValue(props.bridgeAmount);
+ return;
+ }
+
if (!hasHopsBefore() || props.hops === undefined) {
throw new Error(
`bridge swap ${props.swapId} is missing a lockup-side DEX hop`,
diff --git a/src/configs/regtest.ts b/src/configs/regtest.ts
index e0f69eddc..6a2bd5acd 100644
--- a/src/configs/regtest.ts
+++ b/src/configs/regtest.ts
@@ -3,9 +3,11 @@ import { AssetKind, Explorer, NetworkTransport } from "boltz-swaps/types";
import { type Config, baseConfig, chooseUrl } from "src/configs/base";
const mainnetPreset = buildMainnetConfig({
- filterAssets: (asset) => asset === "TBTC" || asset === "USDT0",
+ filterAssets: (asset) =>
+ asset === "TBTC" || asset === "USDT0" || asset === "USDT0-ETH",
rpcUrls: {
ARB: ["http://127.0.0.1:18545"],
+ ETH: ["http://127.0.0.1:18546"],
},
});
@@ -18,6 +20,7 @@ const stablecoins = {
},
},
USDT0: mainnetPreset.assets.USDT0,
+ "USDT0-ETH": mainnetPreset.assets["USDT0-ETH"],
};
const config = {
diff --git a/src/status/InvoiceSet.tsx b/src/status/InvoiceSet.tsx
index 4185a70b1..e0eef3fa1 100644
--- a/src/status/InvoiceSet.tsx
+++ b/src/status/InvoiceSet.tsx
@@ -11,6 +11,7 @@ import {
type SubmarineSwap,
getLockupGasAbstraction,
getPreBridgeDetail,
+ getPreDexQuoteAmount,
} from "../utils/swapCreator";
const InvoiceSet = () => {
@@ -48,6 +49,7 @@ const InvoiceSet = () => {
: undefined
}
bridge={getPreBridgeDetail(submarine.bridge)}
+ bridgeAmount={getPreDexQuoteAmount(submarine.dex)}
/>
);
diff --git a/src/status/SwapCreated.tsx b/src/status/SwapCreated.tsx
index 662877045..ccf91d1d7 100644
--- a/src/status/SwapCreated.tsx
+++ b/src/status/SwapCreated.tsx
@@ -14,6 +14,7 @@ import {
type ReverseSwap,
getLockupGasAbstraction,
getPreBridgeDetail,
+ getPreDexQuoteAmount,
} from "../utils/swapCreator";
const SwapCreated = () => {
@@ -59,6 +60,7 @@ const SwapCreated = () => {
: undefined
}
bridge={getPreBridgeDetail(chain.bridge)}
+ bridgeAmount={getPreDexQuoteAmount(chain.dex)}
/>
diff --git a/src/utils/swapCreator.ts b/src/utils/swapCreator.ts
index 725f14c96..2bb034ca0 100644
--- a/src/utils/swapCreator.ts
+++ b/src/utils/swapCreator.ts
@@ -222,6 +222,16 @@ export const getPostBridgeDetail = (
): BridgeDetail | undefined =>
bridge?.position === SwapPosition.Post ? bridge : undefined;
+export const getPreDexQuoteAmount = (dex?: DexDetail): bigint | undefined => {
+ if (dex?.position !== SwapPosition.Pre) {
+ return undefined;
+ }
+
+ return typeof dex.quoteAmount === "string"
+ ? BigInt(dex.quoteAmount)
+ : BigInt(Math.round(dex.quoteAmount));
+};
+
const generatePreimage = ({
asset,
keyIndex,
diff --git a/tests/utils/swapCreator.spec.ts b/tests/utils/swapCreator.spec.ts
index 75a067886..a327f18dc 100644
--- a/tests/utils/swapCreator.spec.ts
+++ b/tests/utils/swapCreator.spec.ts
@@ -3,11 +3,13 @@ import { BridgeKind, SwapPosition, SwapType } from "boltz-swaps/types";
import { BTC, LBTC, LN, USDT0 } from "../../src/consts/Assets";
import {
type BridgeDetail,
+ type DexDetail,
type SwapBase,
getFinalAssetReceive,
getFinalAssetSend,
getPostBridgeDetail,
getPreBridgeDetail,
+ getPreDexQuoteAmount,
noGasAbstraction,
} from "../../src/utils/swapCreator";
@@ -22,6 +24,13 @@ const makeBridge = (
position,
});
+const makeDex = (position: SwapPosition, quoteAmount: number | string) =>
+ ({
+ hops: [],
+ position,
+ quoteAmount,
+ }) as DexDetail;
+
// Minimal SwapBase builder — only fields read by the tested helpers matter.
const makeSwap = (overrides: Partial): SwapBase =>
({
@@ -68,6 +77,25 @@ describe("getPostBridgeDetail", () => {
});
});
+describe("getPreDexQuoteAmount", () => {
+ test("returns the quote amount for pre-DEX details", () => {
+ expect(
+ getPreDexQuoteAmount(makeDex(SwapPosition.Pre, "40000000")),
+ ).toBe(40000000n);
+ });
+
+ test("rounds legacy numeric quote amounts", () => {
+ expect(getPreDexQuoteAmount(makeDex(SwapPosition.Pre, 1.6))).toBe(2n);
+ });
+
+ test("returns undefined for post-DEX or missing details", () => {
+ expect(
+ getPreDexQuoteAmount(makeDex(SwapPosition.Post, "40000000")),
+ ).toBeUndefined();
+ expect(getPreDexQuoteAmount(undefined)).toBeUndefined();
+ });
+});
+
describe("getFinalAssetSend", () => {
test("returns bridge.sourceAsset when a pre-bridge is attached", () => {
const swap = makeSwap({