Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions e2e/arbitrum/lbtcUsdt0.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,61 @@ describeArbitrumE2e("Arbitrum stablecoin e2e", () => {
await generateLiquidBlock();
});

test("claims an L-BTC to TBTC chain swap to a pasted destination without a connected wallet", async ({
arbitrum,
recipientAddress,
page,
}) => {
test.setTimeout(fullFlowTestTimeout);

const token = getRegtestTokenAddress("TBTC");
expect(
await getTokenBalance(
arbitrum.publicClient,
token,
recipientAddress,
),
).toEqual(0n);

const swapId = await createSwap(
page,
"L-BTC",
"TBTC",
recipientAddress,
lbtcSendAmount,
);

expect(await page.evaluate(() => "ethereum" in window)).toBe(false);

const storedSwap = await getStoredSwap(page, swapId);
expect(storedSwap).not.toBeNull();
expect(storedSwap!.dex).toBeUndefined();

await page
.locator("div[data-testid='pay-onchain-buttons']")
.getByText("address")
.click();
const lockupAddress = await page.evaluate(() =>
navigator.clipboard.readText(),
);
expect(lockupAddress).toBeDefined();

await elementsSendToAddress(lockupAddress, lbtcSendAmount);
await generateLiquidBlock();

await expect(
page.locator("div[data-status='transaction.claimed']"),
).toBeVisible({ timeout: fullFlowTestTimeout });

expect(
await getTokenBalance(
arbitrum.publicClient,
token,
recipientAddress,
),
).toBeGreaterThan(0n);
});

test("claims an L-BTC to USDT0-Arbitrum chain swap", async ({
arbitrum,
recipientAddress,
Expand Down
10 changes: 7 additions & 3 deletions src/status/TransactionConfirmed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1081,8 +1081,12 @@ export const ClaimEvm = (props: {
receiveAmount: execution.minAmountOut,
};
} else {
const sig = signer();
if (sig === undefined) {
const claimSigner = getSignerForGasAbstraction(
props.gasAbstraction,
signer(),
getGasAbstractionSigner(props.assetReceive),
);
if (claimSigner === undefined) {
throw new Error("missing signer for claim");
}
result = await claimAsset({
Expand All @@ -1094,7 +1098,7 @@ export const ClaimEvm = (props: {
refundAddress: getAddress(props.refundAddress),
timeoutBlockHeight: props.timeoutBlockHeight,
destination: getAddress(props.signerAddress),
signer: () => sig,
signer: () => claimSigner,
gasAbstractionSigner: getGasAbstractionSigner(
props.assetReceive,
),
Expand Down
55 changes: 54 additions & 1 deletion tests/status/AutoClaimHops.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const modifySwap = vi.fn();
const getErc20Swap = vi.fn();
const getGasAbstractionSigner = vi.fn();
const fetchDexQuote = vi.fn<(...args: unknown[]) => Promise<ClaimQuote>>();
const gasTopUpSupported = vi.fn<(asset: string) => boolean>(() => false);
const sendPopulatedTransaction =
vi.fn<(...args: unknown[]) => Promise<string>>();
const claimAsset =
Expand Down Expand Up @@ -62,7 +63,7 @@ vi.mock("../../src/hooks/useModifySwap", () => ({

vi.mock("../../src/utils/quoter", () => ({
fetchDexQuote,
gasTopUpSupported: () => false,
gasTopUpSupported: (asset: string) => gasTopUpSupported(asset),
getGasTopUpNativeAmount: vi.fn(),
fetchGasTokenQuote: vi.fn(),
}));
Expand Down Expand Up @@ -297,6 +298,7 @@ describe("ClaimEvm", () => {
beforeEach(() => {
vi.clearAllMocks();
setSigner(makeSigner(signerAddress));
getGasAbstractionSigner.mockReturnValue(makeSigner(signerAddress));
claimAsset.mockResolvedValue({
transactionHash: "0xclaimtx",
receiveAmount: 991n,
Expand All @@ -317,6 +319,57 @@ describe("ClaimEvm", () => {
expect(claimAsset.mock.calls[0][0].amount).toBe(777);
});

// L-BTC -> TBTC chain swap created with a pasted destination and gas
// top-up left off: the claim is sponsored by the rescue key derived
// signer, so it must not require a connected wallet
test("claims an ERC20 chain swap to a pasted destination without a connected wallet", async () => {
const destination = "0x0B99060995946051af5ac29148C2b33808b734e9";
const gasSigner = makeSigner(
"0xa90D48e66F145851f4E047f1bc14ebe33c7a156d",
);
setSigner(undefined);
getGasAbstractionSigner.mockReturnValue(gasSigner);
gasTopUpSupported.mockReturnValue(true);
getSwap.mockResolvedValue({
id: "swap-1",
type: SwapType.Chain,
claimDetails: { amount: 99_723 },
} as SomeSwap);

render(() => (
<ClaimEvm
swapId="swap-1"
gasAbstraction={GasAbstractionType.Signer}
preimage="0xpreimage"
assetSend="L-BTC"
assetReceive="TBTC"
signerAddress={destination}
claimAddress={destination}
refundAddress={signerAddress}
timeoutBlockHeight={100}
finalReceive="TBTC"
getGasToken={false}
autoClaimEnabled={true}
/>
));

await waitFor(() => expect(claimAsset).toHaveBeenCalledTimes(1));
expect(screen.queryByText(/missing signer/)).toBeNull();

const args = claimAsset.mock.calls[0][0] as unknown as {
amount: number;
claimAddress: string;
destination: string;
signer: () => Signer;
};
expect(args.signer()).toBe(gasSigner);
// Funds stay bound to the address the user pasted, so no forwarding
// transfer is appended on top of the claim
expect(args.claimAddress).toBe(destination);
expect(args.destination).toBe(destination);
expect(args.amount).toBe(99_723);
});

test("does not submit another claim when one is already persisted", async () => {
getSwap.mockResolvedValue({
id: "swap-1",
Expand Down