diff --git a/e2e/chainSwaps.spec.ts b/e2e/chainSwaps/chainSwaps.spec.ts similarity index 99% rename from e2e/chainSwaps.spec.ts rename to e2e/chainSwaps/chainSwaps.spec.ts index 8ba592f5..3911da4f 100644 --- a/e2e/chainSwaps.spec.ts +++ b/e2e/chainSwaps/chainSwaps.spec.ts @@ -5,7 +5,7 @@ import { generateBitcoinBlock, generateLiquidBlock, getBitcoinAddress, -} from "./utils"; +} from "../utils"; test.describe("Chain swap", () => { test.beforeEach(async () => { diff --git a/e2e/chainSwaps/overpayment.spec.ts b/e2e/chainSwaps/overpayment.spec.ts new file mode 100644 index 00000000..8f167ae7 --- /dev/null +++ b/e2e/chainSwaps/overpayment.spec.ts @@ -0,0 +1,84 @@ +import { expect, test } from "@playwright/test"; + +import { + elementsSendToAddress, + generateBitcoinBlock, + generateLiquidBlock, + getBitcoinAddress, + getLiquidAddress, +} from "../utils"; + +test.describe("ChainSwap overpayment", () => { + test.beforeEach(async () => { + await generateBitcoinBlock(); + }); + + test("accept new quote", async ({ page }) => { + await page.goto("/"); + + await page.locator(".arrow-down").first().click(); + await page.getByTestId("select-L-BTC").click(); + await page + .locator( + "div:nth-child(3) > .asset-wrap > .asset > .asset-selection > .arrow-down", + ) + .click(); + await page.getByTestId("select-BTC").click(); + await page + .getByTestId("onchainAddress") + .fill(await getBitcoinAddress()); + + await page.getByTestId("receiveAmount").fill("100 000"); + await page.getByTestId("create-swap-button").click(); + await page.getByRole("button", { name: "Skip download" }).click(); + await page + .getByTestId("pay-onchain-buttons") + .getByText("address") + .click(); + const lockupAddress = await page.evaluate(() => { + return navigator.clipboard.readText(); + }); + await elementsSendToAddress(lockupAddress, 0.01); + + await page.getByRole("button", { name: "Accept" }).click(); + await generateLiquidBlock(); + expect( + page.getByRole("heading", { name: "Congratulations!" }), + ).toBeDefined(); + }); + + test("should refund", async ({ page }) => { + await page.goto("/"); + + await page.locator(".arrow-down").first().click(); + await page.getByTestId("select-L-BTC").click(); + await page + .locator( + "div:nth-child(3) > .asset-wrap > .asset > .asset-selection > .arrow-down", + ) + .click(); + await page.getByTestId("select-BTC").click(); + await page + .getByTestId("onchainAddress") + .fill(await getBitcoinAddress()); + + await page.getByTestId("receiveAmount").fill("100 000"); + await page.getByTestId("create-swap-button").click(); + await page.getByRole("button", { name: "Skip download" }).click(); + await page + .getByTestId("pay-onchain-buttons") + .getByText("address") + .click(); + const lockupAddress = await page.evaluate(() => { + return navigator.clipboard.readText(); + }); + await elementsSendToAddress(lockupAddress, 0.01); + + await page.getByRole("button", { name: "Refund" }).click(); + + await page.getByTestId("refundAddress").fill(await getLiquidAddress()); + + await page.getByTestId("refundButton").click(); + expect(page.getByText("Swap has been refunded")).toBeDefined(); + }); +}); diff --git a/e2e/utils.ts b/e2e/utils.ts index 2762f8e0..0f434756 100644 --- a/e2e/utils.ts +++ b/e2e/utils.ts @@ -43,7 +43,7 @@ export const bitcoinSendToAddress = async ( export const elementsSendToAddress = async ( address: string, - amount: string, + amount: string | number, ): Promise => { return execCommand( `elements-cli-sim-client sendtoaddress "${address}" ${amount}`, diff --git a/src/context/Global.tsx b/src/context/Global.tsx index 8c0cadae..33592289 100644 --- a/src/context/Global.tsx +++ b/src/context/Global.tsx @@ -75,7 +75,7 @@ export type GlobalContextType = { audio?: boolean, ) => void; playNotificationSound: () => void; - fetchPairs: () => void; + fetchPairs: () => Promise; getLogs: () => Promise>; clearLogs: () => Promise; @@ -191,17 +191,16 @@ const GlobalProvider = (props: { children: any }) => { audio.play(); }; - const fetchPairs = () => { - getPairs() - .then((data) => { - log.debug("getpairs", data); - setOnline(true); - setPairs(data); - }) - .catch((error) => { - log.debug(error); - setOnline(false); - }); + const fetchPairs = async () => { + try { + const data = await getPairs(); + log.debug("getpairs", data); + setOnline(true); + setPairs(data); + } catch (error) { + log.debug(error); + setOnline(false); + } }; // Use IndexedDB if available; fallback to LocalStorage diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts index 7f28c070..e7daa900 100644 --- a/src/i18n/i18n.ts +++ b/src/i18n/i18n.ts @@ -210,6 +210,7 @@ const dict = { switch_network: "Switch network", block: "block", logs_scan_progress: "Scan progress {{ value }}%", + accept: "Accept", }, de: { language: "Deutsch", @@ -430,6 +431,7 @@ const dict = { switch_network: "Netzwerk wechseln", block: "Block", logs_scan_progress: "Scan-Fortschritt {{ value }}%", + accept: "Akzeptieren", }, es: { language: "Español", @@ -649,6 +651,7 @@ const dict = { switch_network: "Cambiar red", block: "bloque", logs_scan_progress: "Progreso del escaneo {{ value }}%", + accept: "Aceptar", }, zh: { language: "中文", @@ -841,6 +844,7 @@ const dict = { switch_network: "转换网络", block: "块", logs_scan_progress: "扫描进度{{ value }}%", + accept: "接受", }, }; diff --git a/src/pages/RefundEvm.tsx b/src/pages/RefundEvm.tsx index 7f4f1ad7..df00ffd3 100644 --- a/src/pages/RefundEvm.tsx +++ b/src/pages/RefundEvm.tsx @@ -15,7 +15,7 @@ const RefundEvm = () => { const { signer, getEtherSwap } = useWeb3Signer(); const [refundData] = createResource(async () => { - if (signer === undefined) { + if (signer() === undefined) { return undefined; } diff --git a/src/status/TransactionLockupFailed.tsx b/src/status/TransactionLockupFailed.tsx index 8e20b7e7..8220e904 100644 --- a/src/status/TransactionLockupFailed.tsx +++ b/src/status/TransactionLockupFailed.tsx @@ -1,28 +1,133 @@ -import { Accessor, Show } from "solid-js"; +import BigNumber from "bignumber.js"; +import log from "loglevel"; +import { + Accessor, + Match, + Show, + Switch, + createResource, + createSignal, +} from "solid-js"; import RefundButton from "../components/RefundButton"; +import { SwapType } from "../consts/Enums"; import { useGlobalContext } from "../context/Global"; import { usePayContext } from "../context/Pay"; import NotFound from "../pages/NotFound"; +import { + acceptChainSwapNewQuote, + getChainSwapNewQuote, +} from "../utils/boltzClient"; +import { formatAmount } from "../utils/denomination"; +import { formatError } from "../utils/errors"; import { ChainSwap, SubmarineSwap } from "../utils/swapCreator"; const TransactionLockupFailed = () => { - const { failureReason, swap } = usePayContext(); - const { t } = useGlobalContext(); + const { t, denomination, separator, fetchPairs, setSwapStorage, pairs } = + useGlobalContext(); + const { failureReason, swap, setSwap } = usePayContext(); + + const [newQuote, newQuoteActions] = createResource< + { quote: number; receiveAmount: number } | undefined + >(async () => { + if (swap() === null || swap().type !== SwapType.Chain) { + return undefined; + } + + try { + const [quote] = await Promise.all([ + getChainSwapNewQuote(swap().id), + fetchPairs(), + ]); + + const claimFee = + pairs()[SwapType.Chain][swap().assetSend][swap().assetReceive] + .fees.minerFees.user.claim + 1; + + return { + quote: quote.amount, + receiveAmount: quote.amount - claimFee, + }; + } catch (e) { + log.warn( + `Getting new quote for swap ${swap().id} failed: ${formatError(e)}`, + ); + } + + return undefined; + }); + + const [quoteRejected, setQuoteRejected] = createSignal(false); return ( }> -
-

{t("lockup_failed")}

-

- {t("failure_reason")}: {failureReason()} -

-
- } - /> -
-
+ +

{t("lockup_failed")}

+

+ {t("failure_reason")}: {failureReason()} +

+
+ } + /> +
+ + }> + +

+ New quote:{" "} + {formatAmount( + BigNumber(newQuote().receiveAmount), + denomination(), + separator(), + )} +

+

+ {t("failure_reason")}: {failureReason()} +

+
+ + +
+
+
+
); }; diff --git a/src/utils/boltzClient.ts b/src/utils/boltzClient.ts index 4e5131cf..5da0592a 100644 --- a/src/utils/boltzClient.ts +++ b/src/utils/boltzClient.ts @@ -412,6 +412,12 @@ export const getChainSwapTransactions = (id: string) => serverLock: ChainSwapTransaction; }>(`/v2/swap/chain/${id}/transactions`); +export const getChainSwapNewQuote = (id: string) => + fetcher<{ amount: number }>(`/v2/swap/chain/${id}/quote`); + +export const acceptChainSwapNewQuote = (id: string, amount: number) => + fetcher<{}>(`/v2/swap/chain/${id}/quote`, { amount }); + export { Pairs, Contracts, diff --git a/tests/pages/Create.spec.tsx b/tests/pages/Create.spec.tsx index e4ea8f3f..09659b47 100644 --- a/tests/pages/Create.spec.tsx +++ b/tests/pages/Create.spec.tsx @@ -238,6 +238,8 @@ describe("Create", () => { const createButton = (await screen.findByTestId( "create-swap-button", )) as HTMLButtonElement; + globalSignals.setOnline(true); + expect(createButton.disabled).toEqual(true); expect(createButton.innerHTML).toEqual("Invalid BTC address");