diff --git a/e2e/submarineSwap.spec.ts b/e2e/submarineSwap.spec.ts index 5c7479c8..0809de10 100644 --- a/e2e/submarineSwap.spec.ts +++ b/e2e/submarineSwap.spec.ts @@ -4,6 +4,7 @@ import { bitcoinSendToAddress, generateBitcoinBlock, generateInvoiceLnd, + lookupInvoiceLnd, } from "./utils"; test.describe("Submarine swap", () => { @@ -28,7 +29,8 @@ test.describe("Submarine swap", () => { await expect(inputSendAmount).toHaveValue(sendAmount); const invoiceInput = page.locator("textarea[data-testid='invoice']"); - await invoiceInput.fill(await generateInvoiceLnd(1000000)); + const invoice = await generateInvoiceLnd(1000000); + await invoiceInput.fill(invoice); const buttonCreateSwap = page.locator( "button[data-testid='create-swap-button']", ); @@ -48,7 +50,15 @@ test.describe("Submarine swap", () => { await bitcoinSendToAddress(sendAddress, sendAmount); await generateBitcoinBlock(); - // TODO: verify amounts + + await page.getByText("Copy preimage").click(); + const preimage = await page.evaluate(() => { + return navigator.clipboard.readText(); + }); + + const lookupRes = await lookupInvoiceLnd(invoice); + expect(lookupRes.state).toEqual("SETTLED"); + expect(lookupRes.r_preimage).toEqual(preimage); }); test("Create with LNURL", async ({ page }) => { diff --git a/e2e/utils.ts b/e2e/utils.ts index 45d46ab8..4a08a3e2 100644 --- a/e2e/utils.ts +++ b/e2e/utils.ts @@ -1,3 +1,4 @@ +import bolt11 from "bolt11"; import { exec } from "child_process"; import { promisify } from "util"; @@ -60,6 +61,25 @@ export const generateInvoiceLnd = async (amount: number): Promise => { ).payment_request as string; }; +export const lookupInvoiceLnd = async ( + invoice: string, +): Promise<{ state: string; r_preimage: string }> => { + const decoded = bolt11.decode(invoice); + let paymentHash: string | undefined; + + for (const tag of decoded.tags) { + switch (tag.tagName) { + case "payment_hash": + paymentHash = tag.data as string; + break; + } + } + + return JSON.parse( + await execCommand(`lncli-sim 1 lookupinvoice ${paymentHash}`), + ) as never; +}; + export const getBolt12Offer = async (): Promise => { return JSON.parse(await execCommand("lightning-cli-sim 1 offer any ''")) .bolt12 as string; diff --git a/regtest b/regtest index c6df76dc..27099691 160000 --- a/regtest +++ b/regtest @@ -1 +1 @@ -Subproject commit c6df76dcfdeb8b28a2f3114166df8669f87d3657 +Subproject commit 27099691e52acf4bba8993941b4be0afccd63aa9 diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts index 9034788b..f9b51974 100644 --- a/src/i18n/i18n.ts +++ b/src/i18n/i18n.ts @@ -219,6 +219,7 @@ const dict = { timeout: "Timeout", wallet_connect_failed: "Wallet connection failed: {{ error }}", ledger_open_app_prompt: "Open Ethereum or RSK app", + copy_preimage: "Copy preimage", }, de: { language: "Deutsch", diff --git a/src/status/TransactionClaimed.tsx b/src/status/TransactionClaimed.tsx index 9d8d5cf4..e5a02a1d 100644 --- a/src/status/TransactionClaimed.tsx +++ b/src/status/TransactionClaimed.tsx @@ -1,13 +1,19 @@ import { useNavigate } from "@solidjs/router"; import { BigNumber } from "bignumber.js"; -import { Show, createEffect, createSignal } from "solid-js"; +import log from "loglevel"; +import { Show, createEffect, createResource, createSignal } from "solid-js"; +import CopyButton from "../components/CopyButton"; import LoadingSpinner from "../components/LoadingSpinner"; import { RBTC } from "../consts/Assets"; import { SwapType } from "../consts/Enums"; import { useGlobalContext } from "../context/Global"; import { usePayContext } from "../context/Pay"; +import { getSubmarinePreimage } from "../utils/boltzClient"; import { formatAmount } from "../utils/denomination"; +import { formatError } from "../utils/errors"; +import { checkInvoicePreimage } from "../utils/invoice"; +import { SubmarineSwap } from "../utils/swapCreator"; const Broadcasting = () => { const { t } = useGlobalContext(); @@ -22,13 +28,38 @@ const Broadcasting = () => { const TransactionClaimed = () => { const navigate = useNavigate(); + + const { notify } = useGlobalContext(); const { swap } = usePayContext(); - const { t, denomination, separator } = useGlobalContext(); + const { t, denomination, separator, setSwapStorage } = useGlobalContext(); const [claimBroadcast, setClaimBroadcast] = createSignal< boolean | undefined >(undefined); + const [preimage] = createResource(async () => { + const submarine = swap() as SubmarineSwap; + if (submarine?.type !== SwapType.Submarine) { + return undefined; + } + + if (submarine.preimage !== undefined) { + return submarine.preimage; + } + + const res = await getSubmarinePreimage(submarine.id); + try { + await checkInvoicePreimage(submarine.invoice, res.preimage); + } catch (e) { + log.error("Preimage check failed", e); + notify("error", formatError(e)); + } + + submarine.preimage = res.preimage; + await setSwapStorage(submarine); + return res.preimage; + }); + createEffect(() => { const s = swap(); if (s === undefined || s === null) { @@ -62,6 +93,9 @@ const TransactionClaimed = () => { navigate("/swap")}> {t("new_swap")} + + + ); diff --git a/src/utils/boltzClient.ts b/src/utils/boltzClient.ts index 1b0d1b8e..2512e510 100644 --- a/src/utils/boltzClient.ts +++ b/src/utils/boltzClient.ts @@ -443,6 +443,9 @@ export const getChainSwapNewQuote = (id: string) => export const acceptChainSwapNewQuote = (id: string, amount: number) => fetcher(`/v2/swap/chain/${id}/quote`, { amount }); +export const getSubmarinePreimage = (id: string) => + fetcher<{ preimage: string }>(`/v2/swap/submarine/${id}/preimage`); + export { Pairs, Contracts, diff --git a/src/utils/invoice.ts b/src/utils/invoice.ts index 96419068..1ef88308 100644 --- a/src/utils/invoice.ts +++ b/src/utils/invoice.ts @@ -1,5 +1,6 @@ import { bech32, utf8 } from "@scure/base"; import { BigNumber } from "bignumber.js"; +import { crypto } from "bitcoinjs-lib"; import bolt11 from "bolt11"; import log from "loglevel"; @@ -302,3 +303,15 @@ export const validateInvoiceForOffer = async ( throw "invoice does not belong to offer"; }; + +export const checkInvoicePreimage = async ( + invoice: string, + preimage: string, +) => { + const dec = await decodeInvoice(invoice); + const hash = crypto.sha256(Buffer.from(preimage, "hex")).toString("hex"); + + if (hash !== dec.preimageHash) { + throw "invalid preimage"; + } +}; diff --git a/src/utils/swapCreator.ts b/src/utils/swapCreator.ts index de9c1d04..e4c4752e 100644 --- a/src/utils/swapCreator.ts +++ b/src/utils/swapCreator.ts @@ -42,6 +42,7 @@ export type SwapBase = { export type SubmarineSwap = SwapBase & SubmarineCreatedResponse & { invoice: string; + preimage?: string; refundPrivateKey?: string; }; diff --git a/tests/status/TransactionClaimed.spec.tsx b/tests/status/TransactionClaimed.spec.tsx index 6b48cabf..4d570fd3 100644 --- a/tests/status/TransactionClaimed.spec.tsx +++ b/tests/status/TransactionClaimed.spec.tsx @@ -1,6 +1,7 @@ import { render, screen } from "@solidjs/testing-library"; import { BTC, LBTC, RBTC } from "../../src/consts/Assets"; +import { SwapType } from "../../src/consts/Enums"; import i18n from "../../src/i18n/i18n"; import TransactionClaimed from "../../src/status/TransactionClaimed"; import { TestComponent, contextWrapper, payContext } from "../helper"; @@ -18,13 +19,13 @@ describe("TransactionClaimed", () => { test.each` name | swap - ${"normal swaps"} | ${{ reverse: false }} + ${"normal swaps"} | ${{ type: SwapType.Submarine }} ${"reverse swaps to RBTC"} | ${{ - reverse: true, - asset: RBTC, + type: SwapType.Reverse, + assetReceive: RBTC, }} - ${"reverse swaps to BTC with claim transactions"} | ${{ reverse: true, asset: BTC, claimTx: "txid" }} - ${"reverse swaps to L-BTC with claim transactions"} | ${{ reverse: true, asset: LBTC, claimTx: "txid" }} + ${"reverse swaps to BTC with claim transactions"} | ${{ type: SwapType.Reverse, assetReceive: BTC, claimTx: "txid" }} + ${"reverse swaps to L-BTC with claim transactions"} | ${{ type: SwapType.Reverse, assetReceive: LBTC, claimTx: "txid" }} `("should show success for $name", async ({ swap }) => { render( () => ( diff --git a/tests/utils/invoice.spec.ts b/tests/utils/invoice.spec.ts index 903f9b24..4a3a7d44 100644 --- a/tests/utils/invoice.spec.ts +++ b/tests/utils/invoice.spec.ts @@ -2,6 +2,7 @@ import bolt11 from "bolt11"; import { setConfig } from "../../src/config"; import { + checkInvoicePreimage, decodeInvoice, extractAddress, extractInvoice, @@ -153,4 +154,16 @@ describe("invoice", () => { }, ); }); + + describe("checkInvoicePreimage", () => { + test.each` + preimage | invoice + ${"4c8176e16eab0a2282bb47ac8dfceddabffa73e849ea9b6662c44f868a2b4d2a"} | ${"lnbcrt1m1pnnvd2tpp55g70uxevgu3ddpkkn8yvwu6ehvme7lr464gdv7thelmzjz7fyqsqdqqcqzzsxqyz5vqsp5l6n65lrnqmp7lhx9wen493zvkg9gfrksx0lren5m34wy5ge2x6fq9p4gqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpqysgqdxrky9sy3sk6zuh6x9gtga2trtqapyj69zel0lp6xv8xmrqzst3ja2mxsfsaq7ccffnl27pyzyhj8t8eylq792khl3ha3x8qgxca87qpfyqk7l"} + `( + "should check preimage for invoice", + async ({ invoice, preimage }) => { + await checkInvoicePreimage(invoice, preimage); + }, + ); + }); });