Skip to content

test(e2e): Arbitrum USDT0 chain swaps#1499

Draft
kiocos wants to merge 1 commit into
mainfrom
usdt-usdc-e2e
Draft

test(e2e): Arbitrum USDT0 chain swaps#1499
kiocos wants to merge 1 commit into
mainfrom
usdt-usdc-e2e

Conversation

@kiocos

@kiocos kiocos commented May 22, 2026

Copy link
Copy Markdown
Member

Depends on BoltzExchange/regtest#105

Summary by CodeRabbit

  • Tests

    • Added comprehensive end-to-end test suite for Arbitrum stablecoin swap functionality, including L-BTC to USDT0 transaction flows.
  • Chores

    • Enhanced CI/CD pipeline with environment-specific E2E configuration.
    • Updated regtest configuration to support Arbitrum testing infrastructure.

@vercel

vercel Bot commented May 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
webapp-mainnet Ready Ready Preview Jun 10, 2026 4:33am
webapp-pro Ready Ready Preview Jun 10, 2026 4:33am

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds complete end-to-end test infrastructure for Arbitrum stablecoin swaps. It includes CI workflow updates to inject RPC URLs, Playwright environment loading, fixtures for Arbitrum chain setup with RPC polling and TBTC liquidity management, regtest configuration for TBTC/USDT0 assets, and a comprehensive E2E test validating L-BTC→USDT0 swaps through UI and onchain claim.

Changes

Arbitrum Stablecoin E2E Test Suite

Layer / File(s) Summary
Playwright environment and CI setup
playwright.config.ts, .github/workflows/CI.yml
Playwright configuration loads development environment variables at startup, and CI workflow injects ARBITRUM_E2E_RPC_URL and ETHEREUM_E2E_RPC_URL into both regtest startup and test execution, expanding compose profiles to webapp-ci,stables-e2e.
Arbitrum E2E test fixtures
e2e/fixtures/arbitrum.ts
Exports Playwright fixtures that configure Arbitrum chain/client from environment RPC URL, poll RPC readiness via JSON-RPC chainId check, enforce minimum TBTC liquidity in the funding source via anvil balance and impersonation, and generate random recipient addresses. Re-exports expect from Playwright.
Regtest configuration for stablecoin assets
src/configs/regtest.ts, regtest
Builds mainnet preset filtered to TBTC and USDT0 with a local ARB RPC endpoint at 127.0.0.1:18545, includes these stablecoins in config.assets with overridden TBTC deployHeight, adds gas sponsor endpoints at http://localhost:18547/alchemy, and submodule reference is updated to latest commit.
Arbitrum L-BTC→USDT0 swap and claim test
e2e/arbitrum/lbtcUsdt0.spec.ts
E2E test that generates regtest BTC/Liquid blocks, polls the quote API for a TBTC→USDT0 quote, drives the UI to create and submit an L-BTC→USDT0 swap, retrieves the lockup address from clipboard, sends required L-BTC funds to lockup, advances Liquid chain, waits for "transaction.claimed" UI status, and asserts the recipient's USDT0 balance increased from the baseline.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • BoltzExchange/boltz-web-app#1521: Updates the regtest git submodule pointer to a new pinned commit, directly related to this PR's submodule revision change.
  • BoltzExchange/boltz-web-app#1262: Modified UI asset selection/DOM attributes (e.g., L-BTCLBTC, USDT0USDT) that affect the new Arbitrum E2E test's asset selection helpers.
  • BoltzExchange/boltz-web-app#1194: Updates the regtest submodule pointer in the same way, making the regtest configuration changes directly related.

Suggested reviewers

  • michael1011
  • kilrau
  • jackstar12

Poem

🐰 Hoppy tests upon Arbitrum's chain,
TBTC flows to USDT again,
Swap and claim in sequence true,
E2E fixtures make the dream come through!
Regtest blocks, and liquidity reigns,
Boltz automation frees the veins! 🚀

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title mentions 'Arbitrum USDT0 chain swaps' which aligns well with the main changes—adding E2E test infrastructure and a new Arbitrum-specific stablecoin swap test.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch usdt-usdc-e2e

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@kiocos kiocos force-pushed the harden-usdc-rescue branch 5 times, most recently from 175c73c to 75e31e5 Compare May 28, 2026 14:02
Base automatically changed from harden-usdc-rescue to main May 28, 2026 18:55

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@e2e/arbitrum/lbtcUsdt0.spec.ts`:
- Around line 19-24: The suite gating currently uses shouldRunArbitrumE2e()
which only checks CI=true, but the arbitrumWorker fixture still requires RPC env
vars; update the skip condition in describeArbitrumE2e to also check for the
presence of the Arbitrum configuration (e.g. process.env.ARBITRUM_RPC_URL or
whatever secret env(s) your arbitrumWorker expects) or replace
shouldRunArbitrumE2e() with a new predicate like hasArbitrumConfig() that
returns true only when the required RPC envs/secrets exist; ensure
describeArbitrumE2e calls that predicate before deciding between test.describe
and test.describe.skip so the spec self-skips when RPC vars are missing (or
alternatively remove this spec from CI shards that don't inject the RPC vars).
- Around line 32-35: The outer test timeout (swapClaimTestTimeout) is too small
relative to nested wait budgets (quoteReadinessTimeout, quoteRequestTimeout,
swapClaimTimeout) and can cause Playwright to kill slow-but-valid runs; update
swapClaimTestTimeout to exceed the sum of the nested waits (or otherwise set it
to a safe margin above the largest combined waits used in the test) so the outer
timeout cannot fire before the condition-specific timeouts; change the
constant(s) referenced (quoteRequestTimeout, quoteReadinessTimeout,
swapClaimTimeout, swapClaimTestTimeout) where they are declared and also adjust
the other occurrences mentioned (lines around 117-128, 156, 192-194) to use the
new larger test timeout value.
- Around line 179-187: After clicking the "address" button (locator:
page.locator("div[data-testid='pay-onchain-buttons']").getByText("address").click()),
wait until the clipboard actually contains a non-empty lockup address before
reading into lockupAddress; replace the immediate navigator.clipboard.readText()
with a wait/poll (e.g. page.waitForFunction or a short loop with retries) that
calls navigator.clipboard.readText() and resolves only when the returned string
is non-empty (and optionally trimmed) so that the subsequent
expect(lockupAddress) assertion validates the real copied value rather than a
stale/empty string.

In `@e2e/fixtures/arbitrum.ts`:
- Around line 36-37: The function arbitrumRpcUrl currently only reads
ARBITRUM_E2E_PORT and builds a localhost URL, ignoring the CI-provided full RPC
URL; change arbitrumRpcUrl to first return process.env.ARBITRUM_E2E_RPC_URL if
present, otherwise fall back to constructing
`http://127.0.0.1:${process.env.ARBITRUM_E2E_PORT ?? "18545"}` so CI can supply
a full URL via ARBITRUM_E2E_RPC_URL while local runs still use the port-based
default.
- Around line 66-71: shouldRunArbitrumE2e() currently returns true when
process.env.CI === "true" even if ARBITRUM_E2E_RPC_URL or ETHEREUM_E2E_RPC_URL
are missing, causing arbitrumWorker to throw; change the guard so CI does not
bypass configuration checks by making shouldRunArbitrumE2e() return
hasArbitrumE2eConfig() only (i.e., require envValue("ARBITRUM_E2E_RPC_URL") and
envValue("ETHEREUM_E2E_RPC_URL") to be set), or alternatively modify
arbitrumWorker to check hasArbitrumE2eConfig() and skip/throw with a clear
message; update references to shouldRunArbitrumE2e, hasArbitrumE2eConfig,
envValue and arbitrumWorker to reflect the chosen behavior.

In `@src/configs/regtest.ts`:
- Around line 87-90: The regtest config sets gasSponsor.normal and
gasSponsor.tor to an unreachable URL (http://localhost:18547/alchemy); update
these values in the gasSponsor config (gasSponsor.normal, gasSponsor.tor) to a
reachable gas-sponsor endpoint or wire them to a configurable environment
variable (e.g., process.env.GAS_SPONSOR_URL) so E2E swaps don't fail when the
local service isn't running — ensure both keys point to the correct
host:port/path or fallback to a known-working default.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9db34ecb-1068-4554-95bc-5c77b75d2db8

📥 Commits

Reviewing files that changed from the base of the PR and between 37bc819 and 411db1f.

📒 Files selected for processing (6)
  • .github/workflows/CI.yml
  • e2e/arbitrum/lbtcUsdt0.spec.ts
  • e2e/fixtures/arbitrum.ts
  • playwright.config.ts
  • regtest
  • src/configs/regtest.ts

Comment on lines +19 to +24
const describeArbitrumE2e = (title: string, callback: () => void) => {
if (shouldRunArbitrumE2e()) {
test.describe(title, callback);
} else {
test.describe.skip(title, callback);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Gate this suite on actual Arbitrum config, not just CI=true.

shouldRunArbitrumE2e() currently enables the suite on any CI worker, but the arbitrumWorker fixture still throws when the RPC env is absent. That is the current CI failure path, so this spec is not self-skipping in shards/jobs that lack the Arbitrum secrets. Please make this suite’s skip condition config-aware or keep it out of CI jobs that do not inject the required RPC vars.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/arbitrum/lbtcUsdt0.spec.ts` around lines 19 - 24, The suite gating
currently uses shouldRunArbitrumE2e() which only checks CI=true, but the
arbitrumWorker fixture still requires RPC env vars; update the skip condition in
describeArbitrumE2e to also check for the presence of the Arbitrum configuration
(e.g. process.env.ARBITRUM_RPC_URL or whatever secret env(s) your arbitrumWorker
expects) or replace shouldRunArbitrumE2e() with a new predicate like
hasArbitrumConfig() that returns true only when the required RPC envs/secrets
exist; ensure describeArbitrumE2e calls that predicate before deciding between
test.describe and test.describe.skip so the spec self-skips when RPC vars are
missing (or alternatively remove this spec from CI shards that don't inject the
RPC vars).

Source: Pipeline failures

Comment thread e2e/arbitrum/lbtcUsdt0.spec.ts Outdated
Comment on lines +179 to +187
await page
.locator("div[data-testid='pay-onchain-buttons']")
.getByText("address")
.click();

const lockupAddress = await page.evaluate(() =>
navigator.clipboard.readText(),
);
expect(lockupAddress).toBeDefined();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Wait for a non-empty copied lockup address before funding.

The clipboard is read immediately after the click, and toBeDefined() still passes for an empty or stale string. That makes this step race the copy handler and shifts the failure to sendtoaddress instead of the actual clipboard handoff.

Suggested fix
         await page
             .locator("div[data-testid='pay-onchain-buttons']")
             .getByText("address")
             .click();

-        const lockupAddress = await page.evaluate(() =>
-            navigator.clipboard.readText(),
-        );
-        expect(lockupAddress).toBeDefined();
+        await expect
+            .poll(
+                () => page.evaluate(() => navigator.clipboard.readText()),
+                { timeout: 5_000 },
+            )
+            .not.toBe("");
+
+        const lockupAddress = await page.evaluate(() =>
+            navigator.clipboard.readText(),
+        );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/arbitrum/lbtcUsdt0.spec.ts` around lines 179 - 187, After clicking the
"address" button (locator:
page.locator("div[data-testid='pay-onchain-buttons']").getByText("address").click()),
wait until the clipboard actually contains a non-empty lockup address before
reading into lockupAddress; replace the immediate navigator.clipboard.readText()
with a wait/poll (e.g. page.waitForFunction or a short loop with retries) that
calls navigator.clipboard.readText() and resolves only when the returned string
is non-empty (and optionally trimmed) so that the subsequent
expect(lockupAddress) assertion validates the real copied value rather than a
stale/empty string.

Comment thread e2e/fixtures/arbitrum.ts
Comment on lines +36 to +37
const arbitrumRpcUrl = () =>
`http://127.0.0.1:${process.env.ARBITRUM_E2E_PORT ?? "18545"}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Environment variable mismatch: CI provides full URL but code expects port.

The CI workflow (.github/workflows/CI.yml lines 77, 96) sets ARBITRUM_E2E_RPC_URL containing a full RPC URL, but this function only reads ARBITRUM_E2E_PORT and constructs http://127.0.0.1:${port}. The full RPC URL from the environment is never used.

Either:

  1. Parse the port from ARBITRUM_E2E_RPC_URL when available, or
  2. Use ARBITRUM_E2E_RPC_URL directly if set, falling back to the constructed localhost URL
🔧 Proposed fix to use the full RPC URL from environment
 const arbitrumRpcUrl = () =>
-    `http://127.0.0.1:${process.env.ARBITRUM_E2E_PORT ?? "18545"}`;
+    process.env.ARBITRUM_E2E_RPC_URL ?? 
+    `http://127.0.0.1:${process.env.ARBITRUM_E2E_PORT ?? "18545"}`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const arbitrumRpcUrl = () =>
`http://127.0.0.1:${process.env.ARBITRUM_E2E_PORT ?? "18545"}`;
const arbitrumRpcUrl = () =>
process.env.ARBITRUM_E2E_RPC_URL ??
`http://127.0.0.1:${process.env.ARBITRUM_E2E_PORT ?? "18545"}`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/fixtures/arbitrum.ts` around lines 36 - 37, The function arbitrumRpcUrl
currently only reads ARBITRUM_E2E_PORT and builds a localhost URL, ignoring the
CI-provided full RPC URL; change arbitrumRpcUrl to first return
process.env.ARBITRUM_E2E_RPC_URL if present, otherwise fall back to constructing
`http://127.0.0.1:${process.env.ARBITRUM_E2E_PORT ?? "18545"}` so CI can supply
a full URL via ARBITRUM_E2E_RPC_URL while local runs still use the port-based
default.

Comment thread e2e/fixtures/arbitrum.ts
Comment on lines +66 to +71
export const hasArbitrumE2eConfig = () =>
envValue("ARBITRUM_E2E_RPC_URL") !== undefined &&
envValue("ETHEREUM_E2E_RPC_URL") !== undefined;

export const shouldRunArbitrumE2e = () =>
process.env.CI === "true" || hasArbitrumE2eConfig();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fixture execution guard is inconsistent with CI gating logic.

shouldRunArbitrumE2e() returns true whenever CI=true, regardless of whether ARBITRUM_E2E_RPC_URL and ETHEREUM_E2E_RPC_URL are actually set. This causes the arbitrumWorker fixture (line 191-192) to throw "missing Arbitrum e2e RPC config" when these variables are unset in CI, leading to the pipeline failures shown in the logs.

The guard should require valid config even in CI mode, or tests must be conditionally skipped at the suite level when config is absent.

🔧 Proposed fix to enforce config presence in CI
 export const shouldRunArbitrumE2e = () =>
-    process.env.CI === "true" || hasArbitrumE2eConfig();
+    hasArbitrumE2eConfig();

If CI runs should always attempt Arbitrum E2E (and fail when config is missing), then remove the hasArbitrumE2eConfig() check from the fixture:

         async ({}, use) => {
-            if (!hasArbitrumE2eConfig()) {
-                throw new Error("missing Arbitrum e2e RPC config");
-            }
-
             const arbitrum = createArbitrum();

And gate at the test level instead (the test already calls shouldRunArbitrumE2e() at line 20 in e2e/arbitrum/lbtcUsdt0.spec.ts).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const hasArbitrumE2eConfig = () =>
envValue("ARBITRUM_E2E_RPC_URL") !== undefined &&
envValue("ETHEREUM_E2E_RPC_URL") !== undefined;
export const shouldRunArbitrumE2e = () =>
process.env.CI === "true" || hasArbitrumE2eConfig();
export const hasArbitrumE2eConfig = () =>
envValue("ARBITRUM_E2E_RPC_URL") !== undefined &&
envValue("ETHEREUM_E2E_RPC_URL") !== undefined;
export const shouldRunArbitrumE2e = () =>
hasArbitrumE2eConfig();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/fixtures/arbitrum.ts` around lines 66 - 71, shouldRunArbitrumE2e()
currently returns true when process.env.CI === "true" even if
ARBITRUM_E2E_RPC_URL or ETHEREUM_E2E_RPC_URL are missing, causing arbitrumWorker
to throw; change the guard so CI does not bypass configuration checks by making
shouldRunArbitrumE2e() return hasArbitrumE2eConfig() only (i.e., require
envValue("ARBITRUM_E2E_RPC_URL") and envValue("ETHEREUM_E2E_RPC_URL") to be
set), or alternatively modify arbitrumWorker to check hasArbitrumE2eConfig() and
skip/throw with a clear message; update references to shouldRunArbitrumE2e,
hasArbitrumE2eConfig, envValue and arbitrumWorker to reflect the chosen
behavior.

Comment thread src/configs/regtest.ts
Comment on lines +87 to 90
gasSponsor: {
normal: "http://localhost:18547/alchemy",
tor: "http://localhost:18547/alchemy",
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check if the gas sponsor service is running and responsive

GAS_SPONSOR_URL="http://localhost:18547/alchemy"

echo "Testing gas sponsor endpoint: $GAS_SPONSOR_URL"
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$GAS_SPONSOR_URL" || echo "Connection failed")

if [ "$HTTP_CODE" = "Connection failed" ]; then
  echo "ERROR: Could not connect to gas sponsor service"
  exit 1
elif [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
  echo "SUCCESS: Gas sponsor service is reachable (HTTP $HTTP_CODE)"
elif [ "$HTTP_CODE" -ge 400 ]; then
  echo "WARNING: Gas sponsor service returned HTTP $HTTP_CODE"
fi

Repository: BoltzExchange/boltz-web-app

Length of output: 296


🏁 Script executed:

#!/bin/bash
set -euo pipefail

GAS_SPONSOR_URL="http://localhost:18547/alchemy"
echo "Testing gas sponsor endpoint: $GAS_SPONSOR_URL"

HTTP_CODE="$(curl -sS -o /dev/null -w "%{http_code}" --max-time 5 "$GAS_SPONSOR_URL" 2>/dev/null || true)"
CURL_EXIT=$?

if [ $CURL_EXIT -ne 0 ]; then
  echo "ERROR: Could not connect to gas sponsor service (curl exit code: $CURL_EXIT)"
  exit 1
fi

if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
  echo "SUCCESS: Gas sponsor service is reachable (HTTP $HTTP_CODE)"
else
  echo "WARNING: Gas sponsor service returned HTTP $HTTP_CODE"
fi

Repository: BoltzExchange/boltz-web-app

Length of output: 179


Fix/align regtest gasSponsor endpoint (http://localhost:18547/alchemy)

src/configs/regtest.ts sets both gasSponsor.normal and gasSponsor.tor to http://localhost:18547/alchemy, but the endpoint is not responding locally (curl reports HTTP 000), so gas sponsorship for swaps during E2E will fail until the gas sponsor service is running at that exact host/port/path or the config URL is updated.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/configs/regtest.ts` around lines 87 - 90, The regtest config sets
gasSponsor.normal and gasSponsor.tor to an unreachable URL
(http://localhost:18547/alchemy); update these values in the gasSponsor config
(gasSponsor.normal, gasSponsor.tor) to a reachable gas-sponsor endpoint or wire
them to a configurable environment variable (e.g., process.env.GAS_SPONSOR_URL)
so E2E swaps don't fail when the local service isn't running — ensure both keys
point to the correct host:port/path or fallback to a known-working default.

@kiocos kiocos changed the title test(e2e): USDT and USDC swaps test(e2e): Arbitrum USDT0 chain swaps Jun 10, 2026
@kilrau kilrau marked this pull request as draft June 11, 2026 17:23
@kilrau

kilrau commented Jun 11, 2026

Copy link
Copy Markdown
Member

converting to draft til dependency pr is merged

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants