Skip to content

Commit b81802d

Browse files
authoredMar 27, 2025··
test: add E2E tests for refunding multiple UTXOs (#869)
1 parent a8b1661 commit b81802d

File tree

5 files changed

+182
-33
lines changed

5 files changed

+182
-33
lines changed
 

‎e2e/refund/refund.spec.ts

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { Page, expect, request, test } from "@playwright/test";
2+
import fs from "fs";
3+
import path from "path";
4+
5+
import { UTXO } from "../../src/utils/blockchain";
6+
import {
7+
createAndVerifySwap,
8+
decodeLiquidRawTransaction,
9+
elementsSendToAddress,
10+
generateLiquidBlock,
11+
getLiquidAddress,
12+
setFailedToPay,
13+
} from "../utils";
14+
15+
const getCurrentSwapId = (page: Page) => {
16+
const url = new URL(page.url());
17+
return url.pathname.split("/").pop();
18+
};
19+
20+
const waitForUTXOsInMempool = async (address: string, amount: number) => {
21+
const requestContext = request.newContext();
22+
await expect
23+
.poll(
24+
async () => {
25+
const res = await (
26+
await requestContext
27+
).get(`http://localhost:4003/api/address/${address}/utxo`);
28+
29+
const utxos = (await res.json()) as UTXO[];
30+
return utxos.length === amount;
31+
},
32+
{ timeout: 10_000 },
33+
)
34+
.toBe(true);
35+
};
36+
37+
const navigateToSwapDetails = async (page: Page, swapId: string) => {
38+
await page.getByRole("link", { name: "Refund" }).click();
39+
const swapItem = page.locator(`div[data-testid='swaplist-item-${swapId}']`);
40+
await expect(page.getByTestId("loading-spinner")).not.toBeVisible();
41+
await expect(swapItem.getByRole("link", { name: "Refund" })).toBeVisible();
42+
await swapItem.click();
43+
};
44+
45+
const validateRefundTxInputs = async (page: Page, expectedInputs: number) => {
46+
const refundRequest = await page.waitForRequest((req) =>
47+
req.url().includes("/refund"),
48+
);
49+
const broadcastedTx = JSON.parse(refundRequest.postData() || "{}");
50+
51+
const decodedTx = await decodeLiquidRawTransaction(
52+
broadcastedTx.transaction,
53+
);
54+
const tx = JSON.parse(decodedTx);
55+
56+
expect(tx.vin.length).toBe(expectedInputs);
57+
};
58+
59+
test.describe("Refund", () => {
60+
const refundFileJson = path.join(__dirname, "rescue.json");
61+
62+
test.beforeEach(async () => {
63+
await generateLiquidBlock();
64+
});
65+
66+
test.afterEach(() => {
67+
if (fs.existsSync(refundFileJson)) {
68+
fs.unlinkSync(refundFileJson);
69+
}
70+
});
71+
72+
test("Refunds all UTXOs of `invoice.failedToPay`", async ({ page }) => {
73+
await createAndVerifySwap(page, refundFileJson);
74+
75+
const swapId = getCurrentSwapId(page);
76+
77+
const address = await page.evaluate(() => {
78+
return navigator.clipboard.readText();
79+
});
80+
const amount = 0.005;
81+
const utxoCount = 3;
82+
83+
await setFailedToPay(swapId);
84+
85+
await elementsSendToAddress(address, amount);
86+
await elementsSendToAddress(address, amount);
87+
await elementsSendToAddress(address, amount);
88+
89+
await waitForUTXOsInMempool(address, utxoCount);
90+
91+
await navigateToSwapDetails(page, swapId);
92+
93+
await expect(page.getByText("invoice.failedToPay")).toBeVisible();
94+
await page.getByTestId("refundAddress").fill(await getLiquidAddress());
95+
await page.getByTestId("refundButton").click();
96+
97+
// Validate that the UTXOs are refunded on the same transaction
98+
await validateRefundTxInputs(page, utxoCount);
99+
100+
const refundTxLink = page.getByText("open refund transaction");
101+
const txId = (await refundTxLink.getAttribute("href")).split("/").pop();
102+
103+
expect(txId).toBeDefined();
104+
});
105+
106+
test("Refunds all UTXOs of `transaction.lockupFailed`", async ({
107+
page,
108+
}) => {
109+
await createAndVerifySwap(page, refundFileJson);
110+
111+
const swapId = getCurrentSwapId(page);
112+
113+
const address = await page.evaluate(() => {
114+
return navigator.clipboard.readText();
115+
});
116+
const amount = 0.01;
117+
const utxoCount = 2;
118+
119+
// Pay swap with incorrect amount & pay additional UTXO
120+
await elementsSendToAddress(address, amount);
121+
await elementsSendToAddress(address, amount);
122+
123+
await waitForUTXOsInMempool(address, utxoCount);
124+
125+
await navigateToSwapDetails(page, swapId);
126+
127+
await expect(page.getByText("transaction.lockupFailed")).toBeVisible();
128+
await page.getByTestId("refundAddress").fill(await getLiquidAddress());
129+
await page.getByTestId("refundButton").click();
130+
131+
// Validate that the UTXOs are refunded on the same transaction
132+
await validateRefundTxInputs(page, utxoCount);
133+
134+
const refundTxLink = page.getByText("open refund transaction");
135+
const txId = (await refundTxLink.getAttribute("href")).split("/").pop();
136+
137+
expect(txId).toBeDefined();
138+
});
139+
});

‎e2e/refund/rescueFile.spec.ts

+4-32
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,20 @@
1-
import { Page, expect, request, test } from "@playwright/test";
1+
import { expect, request, test } from "@playwright/test";
22
import fs from "fs";
33
import path from "path";
44

55
import dict from "../../src/i18n/i18n";
66
import { UTXO } from "../../src/utils/blockchain";
77
import { getRescuableSwaps } from "../boltzClient";
88
import {
9+
createAndVerifySwap,
910
elementsSendToAddress,
11+
fillSwapDetails,
1012
generateLiquidBlock,
11-
getBolt12Offer,
1213
getElementsWalletTx,
1314
getLiquidAddress,
15+
setupSwapAssets,
1416
} from "../utils";
1517

16-
const setupSwapAssets = async (page: Page) => {
17-
await page.locator(".arrow-down").first().click();
18-
await page.getByTestId("select-L-BTC").click();
19-
await page
20-
.locator(
21-
"div:nth-child(3) > .asset-wrap > .asset > .asset-selection > .arrow-down",
22-
)
23-
.click();
24-
await page.getByTestId("select-LN").click();
25-
};
26-
27-
const fillSwapDetails = async (page: Page) => {
28-
await page.getByTestId("invoice").fill(await getBolt12Offer());
29-
await page.getByTestId("sendAmount").fill("0.005");
30-
await page.getByTestId("create-swap-button").click();
31-
};
32-
33-
const createAndVerifySwap = async (page: Page, rescueFile: string) => {
34-
await page.goto("/");
35-
await setupSwapAssets(page);
36-
await fillSwapDetails(page);
37-
38-
const downloadPromise = page.waitForEvent("download");
39-
await page.getByRole("button", { name: dict.en.download_new_key }).click();
40-
await (await downloadPromise).saveAs(rescueFile);
41-
42-
await page.getByTestId("rescueFileUpload").setInputFiles(rescueFile);
43-
await page.getByText("address").click();
44-
};
45-
4618
test.describe("Rescue file", () => {
4719
const rescueFileJson = path.join(__dirname, "rescue.json");
4820
const existingFilePath = path.join(__dirname, "existingRescueFile.json");

‎e2e/utils.ts

+37
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ export const getElementsWalletTx = (txId: string): Promise<string> =>
8080
export const payInvoiceLnd = (invoice: string): Promise<string> =>
8181
execCommand(`lncli-sim 1 payinvoice -f ${invoice}`);
8282

83+
export const decodeLiquidRawTransaction = (tx: string): Promise<string> =>
84+
execCommand(`elements-cli-sim-client decoderawtransaction "${tx}"`);
85+
8386
export const generateInvoiceLnd = async (amount: number): Promise<string> => {
8487
return JSON.parse(
8588
await execCommand(`lncli-sim 1 addinvoice --amt ${amount}`),
@@ -117,6 +120,10 @@ export const waitForNodesToSync = async (): Promise<void> => {
117120
export const addReferral = (name: string): Promise<string> =>
118121
boltzCli(`addreferral ${name} 0`);
119122

123+
export const setFailedToPay = async (swapId: string): Promise<void> => {
124+
await boltzCli(`setswapstatus ${swapId} invoice.failedToPay`);
125+
};
126+
120127
export const getReferrals = async (): Promise<Record<string, unknown>> =>
121128
JSON.parse(await boltzCli(`getreferrals`)) as Record<string, unknown>;
122129

@@ -163,3 +170,33 @@ export const verifyRescueFile = async (page: Page) => {
163170
fs.unlinkSync(fileName);
164171
}
165172
};
173+
174+
export const setupSwapAssets = async (page: Page) => {
175+
await page.locator(".arrow-down").first().click();
176+
await page.getByTestId("select-L-BTC").click();
177+
await page
178+
.locator(
179+
"div:nth-child(3) > .asset-wrap > .asset > .asset-selection > .arrow-down",
180+
)
181+
.click();
182+
await page.getByTestId("select-LN").click();
183+
};
184+
185+
export const fillSwapDetails = async (page: Page) => {
186+
await page.getByTestId("invoice").fill(await getBolt12Offer());
187+
await page.getByTestId("sendAmount").fill("0.005");
188+
await page.getByTestId("create-swap-button").click();
189+
};
190+
191+
export const createAndVerifySwap = async (page: Page, rescueFile: string) => {
192+
await page.goto("/");
193+
await setupSwapAssets(page);
194+
await fillSwapDetails(page);
195+
196+
const downloadPromise = page.waitForEvent("download");
197+
await page.getByRole("button", { name: dict.en.download_new_key }).click();
198+
await (await downloadPromise).saveAs(rescueFile);
199+
200+
await page.getByTestId("rescueFileUpload").setInputFiles(rescueFile);
201+
await page.getByText("address").click();
202+
};

‎src/components/LoadingSpinner.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import "../style/loadingSpinner.scss";
22

33
const LoadingSpinner = () => {
44
return (
5-
<div class="spinner">
5+
<div class="spinner" data-testid="loading-spinner">
66
<div class="bounce1" />
77
<div class="bounce2" />
88
<div class="bounce3" />

‎src/components/SwapList.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ const SwapList = (props: {
7676
{(swap) => (
7777
<>
7878
<div
79+
data-testid={`swaplist-item-${swap.id}`}
7980
class={`swaplist-item ${swap.disabled ? "disabled" : ""}`}
8081
onClick={() => {
8182
if (swap.disabled) {

0 commit comments

Comments
 (0)
Please sign in to comment.