diff --git a/.github/actions/setup-nodejs/action.yml b/.github/actions/setup-nodejs/action.yml
index 78069873..15b1c0ad 100644
--- a/.github/actions/setup-nodejs/action.yml
+++ b/.github/actions/setup-nodejs/action.yml
@@ -27,6 +27,6 @@ runs:
node-version: ${{ inputs.node-version }}
cache: 'pnpm'
- - name: Npm install
+ - name: pnpm install
run: pnpm i ${{ inputs.pnpm-install-options }}
shell: bash
diff --git a/.github/workflows/bridge-ui-e2e-tests.yml b/.github/workflows/bridge-ui-e2e-tests.yml
index 42c502cc..b2fd5e49 100644
--- a/.github/workflows/bridge-ui-e2e-tests.yml
+++ b/.github/workflows/bridge-ui-e2e-tests.yml
@@ -24,28 +24,41 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
+ # Use for testing workflow only (and not publish to Docker)
+ - uses: actions/cache@v4.2.0
+ with:
+ path: |
+ ~/.npm
+ ~/.cache/ms-playwright
+ ~/_work/.pnpm-store/v3
+ ${{ github.workspace }}/bridge-ui/.next/cache
+ ${{ github.workspace }}/bridge-ui/.cache-synpress
+ key: ${{ runner.os }}-bridge-ui-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**/bridge-ui/src/**/*.js', '**/bridge-ui/src/**/*.ts', '**/bridge-ui/src/**/*.tsx') }}
+ restore-keys: |
+ ${{ runner.os }}-bridge-ui-${{ hashFiles('**/pnpm-lock.yaml') }}-
+
- name: Setup nodejs environment
uses: ./.github/actions/setup-nodejs
with:
- node-version: 18.17.0
+ node-version: 20.17.0
pnpm-install-options: '--frozen-lockfile --prefer-offline'
- name: Install Playwright
- run: pnpm dlx playwright@1.45.3 install --with-deps
+ run: pnpm dlx playwright@1.51.1 install --with-deps chromium
- name: Build Bridge UI
- run: pnpm run -F bridge-ui build;
+ run: pnpm run -F bridge-ui build
env:
NEXT_PUBLIC_WALLET_CONNECT_ID: ${{ secrets.PUBLIC_WALLET_CONNECT_ID }}
NEXT_PUBLIC_INFURA_ID: ${{ secrets.PUBLIC_BRIDGE_UI_INFURA_ID }}
- NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID: ${{ secrets.PUBLIC_DYNAMIC_ENVIRONMENT_ID }}
+ NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID: ${{ secrets.PUBLIC_DYNAMIC_SANDBOX_ENVIRONMENT_ID }}
NEXT_PUBLIC_LIFI_API_KEY: ${{ secrets.PUBLIC_LIFI_API_KEY }}
- name: Install linux dependencies
run: |
sudo apt-get install --no-install-recommends -y xvfb
- - name: Build synpress cache
+ - name: Initialize Metamask testing fixture
run: xvfb-run pnpm run -F bridge-ui build:cache
env:
E2E_TEST_PRIVATE_KEY: ${{ secrets.BRIDGE_UI_E2E_TESTS_PRIVATE_KEY }}
@@ -53,9 +66,21 @@ jobs:
E2E_TEST_WALLET_PASSWORD: "TestPassword!"
NEXT_PUBLIC_INFURA_ID: ${{ secrets.PUBLIC_BRIDGE_UI_INFURA_ID }}
- - name: Run tests
+ - name: Run unit tests
+ run: DOTENV_CONFIG_PATH=.env.production pnpm run -F bridge-ui test:unit
+ env:
+ # Can run unit tests in parallel
+ CI: "false"
+ NEXT_PUBLIC_INFURA_ID: ${{ secrets.PUBLIC_BRIDGE_UI_INFURA_ID }}
+ NEXT_PUBLIC_LIFI_API_KEY: "placeholder"
+ NEXT_PUBLIC_WALLET_CONNECT_ID: "placeholder"
+ NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID: "placeholder"
+
+ # Prerequisite - Testing wallet must have >0 ETH and USDC on Sepolia
+ - name: Run E2E tests
run: xvfb-run pnpm run -F bridge-ui test:e2e:headful
env:
+ # Do not run E2E tests in parallel. Especially blockchain tx where nonces can collide.
CI: "true"
E2E_TEST_PRIVATE_KEY: ${{ secrets.BRIDGE_UI_E2E_TESTS_PRIVATE_KEY }}
E2E_TEST_SEED_PHRASE: "test test test test test test test test test test test junk"
diff --git a/bridge-ui/package.json b/bridge-ui/package.json
index faaeeb08..265e7446 100644
--- a/bridge-ui/package.json
+++ b/bridge-ui/package.json
@@ -7,17 +7,20 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
- "lint": "next lint",
- "lint:fix": "pnpm run lint:ts:fix",
+ "lint": "next lint && pnpm run lint:test",
+ "lint:test": "npx eslint 'test/**/*.{js,ts}'",
+ "lint:fix": "pnpm run lint:ts:fix && pnpm run lint:test:ts:fix",
"lint:ts:fix": "next lint --fix",
+ "lint:test:ts:fix": "npx eslint --fix 'test/**/*.{js,ts}'",
"clean": "rimraf node_modules .next .next-env.d.ts",
"install:playwright": "playwright install --with-deps",
"build:cache": "synpress",
"build:cache:force": "synpress --force",
"build:cache:headless": "synpress --headless",
- "test:e2e:headful": "playwright test",
- "test:e2e:headless": "HEADLESS=true playwright test",
- "test:e2e:headless:ui": "HEADLESS=true playwright test --ui"
+ "test:unit": "HEADLESS=true playwright test src",
+ "test:e2e:headful": "playwright test test/e2e",
+ "test:e2e:headless": "HEADLESS=true playwright test test/e2e",
+ "test:e2e:headless:ui": "HEADLESS=true playwright test test/e2e --ui"
},
"dependencies": {
"@consensys/linea-sdk": "0.3.0",
@@ -48,15 +51,16 @@
"zustand": "4.5.4"
},
"devDependencies": {
- "@playwright/test": "1.45.3",
+ "@playwright/test": "1.51.1",
"@svgr/webpack": "8.1.0",
- "@synthetixio/synpress": "4.0.0-alpha.7",
+ "@synthetixio/synpress": "4.0.10",
"@types/fs-extra": "11.0.4",
"@types/react": "18.3.11",
"@types/react-dom": "18.3.0",
"autoprefixer": "10.4.21",
"dotenv": "16.4.7",
"eslint-config-next": "14.2.15",
+ "nock": "14.0.1",
"postcss": "8.5.3"
}
}
diff --git a/bridge-ui/playwright.config.ts b/bridge-ui/playwright.config.ts
index 001f5384..93530d0f 100644
--- a/bridge-ui/playwright.config.ts
+++ b/bridge-ui/playwright.config.ts
@@ -1,14 +1,21 @@
import { defineConfig, devices } from "@playwright/test";
+import "dotenv/config";
export default defineConfig({
- testDir: "./test/e2e",
- timeout: 60_000,
+ testDir: ".",
+ testMatch: '**/*.spec.ts',
+ // Timeout for tests that don't involve blockchain transactions
+ timeout: 40_000,
fullyParallel: true,
maxFailures: process.env.CI ? 1 : 0,
+ // To consider - cannot really run E2E tests involving blockchain tx in parallel. There is a high risk of reusing the same tx nonce -> leading to dropped transactions
workers: process.env.CI ? 1 : undefined,
reporter: process.env.CI
- ? [["html", { open: "never", outputFolder: `playwright-report-${process.env.HEADLESS ? "headless" : "headful"}` }]]
- : "html",
+ ? [
+ ["html", { open: "never", outputFolder: `playwright-report-${process.env.HEADLESS ? "headless" : "headful"}` }],
+ ["list"]
+ ]
+ : [["html"],["list"]],
use: {
baseURL: "http://localhost:3000",
trace: process.env.CI ? "on" : "retain-on-failure",
diff --git a/bridge-ui/src/components/bridge/claiming/received-amount/index.tsx b/bridge-ui/src/components/bridge/claiming/received-amount/index.tsx
index facab70e..cedbf194 100644
--- a/bridge-ui/src/components/bridge/claiming/received-amount/index.tsx
+++ b/bridge-ui/src/components/bridge/claiming/received-amount/index.tsx
@@ -42,7 +42,7 @@ export default function ReceivedAmount() {
return (
-
+
{formatBalance(receivedAmount, 6)} {token.symbol}
{tokenPrices?.[token[fromChain.layer].toLowerCase()] &&
diff --git a/bridge-ui/src/components/bridge/form/index.tsx b/bridge-ui/src/components/bridge/form/index.tsx
index 7f87221b..726eb1cf 100644
--- a/bridge-ui/src/components/bridge/form/index.tsx
+++ b/bridge-ui/src/components/bridge/form/index.tsx
@@ -58,10 +58,11 @@ export default function BridgeForm() {
setIsBridgeOpen(false);
setIsTransactionHistoryOpen(true);
}}
+ data-testid="native-bridge-transaction-history-icon"
>
-
+
diff --git a/bridge-ui/src/components/bridge/modal/confirm-destination-address/index.tsx b/bridge-ui/src/components/bridge/modal/confirm-destination-address/index.tsx
index 3b948f51..efaf8045 100644
--- a/bridge-ui/src/components/bridge/modal/confirm-destination-address/index.tsx
+++ b/bridge-ui/src/components/bridge/modal/confirm-destination-address/index.tsx
@@ -32,7 +32,7 @@ export default function ConfirmDestinationAddress({ isModalOpen, recipient, onCl
{formatAddress(recipient)}
-
diff --git a/bridge-ui/src/components/bridge/modal/token-modal/token-details/index.tsx b/bridge-ui/src/components/bridge/modal/token-modal/token-details/index.tsx
index 0baeef63..0def74d8 100644
--- a/bridge-ui/src/components/bridge/modal/token-modal/token-details/index.tsx
+++ b/bridge-ui/src/components/bridge/modal/token-modal/token-details/index.tsx
@@ -46,6 +46,7 @@ export default function TokenDetails({ isConnected, token, onTokenClick, tokenPr
return (
{isConnected && !tokenNotFromCurrentLayer && (
-
+
{formatBalance(formattedBalance, 8)} {token.symbol}
{totalValue !== undefined && (
diff --git a/bridge-ui/src/components/bridge/token-list/index.tsx b/bridge-ui/src/components/bridge/token-list/index.tsx
index 1fb119f4..05ef30c6 100644
--- a/bridge-ui/src/components/bridge/token-list/index.tsx
+++ b/bridge-ui/src/components/bridge/token-list/index.tsx
@@ -16,7 +16,11 @@ export default function TokenList() {
return (
{token && (
-
+
{token.symbol}
diff --git a/bridge-ui/src/components/bridge/transaction-history/index.tsx b/bridge-ui/src/components/bridge/transaction-history/index.tsx
index 050e1060..d95bcf9e 100644
--- a/bridge-ui/src/components/bridge/transaction-history/index.tsx
+++ b/bridge-ui/src/components/bridge/transaction-history/index.tsx
@@ -29,6 +29,7 @@ export default function TransactionHistory() {
setIsTransactionHistoryOpen(false);
setIsBridgeOpen(true);
}}
+ data-testid="transaction-history-close-btn"
>
@@ -51,6 +52,7 @@ export default function TransactionHistory() {
setIsTransactionHistoryOpen(false);
setIsBridgeOpen(true);
}}
+ data-testid="transaction-history-close-btn"
>
diff --git a/bridge-ui/src/components/bridge/transaction-history/list-transaction/index.tsx b/bridge-ui/src/components/bridge/transaction-history/list-transaction/index.tsx
index d2e071e5..4e6e6e40 100644
--- a/bridge-ui/src/components/bridge/transaction-history/list-transaction/index.tsx
+++ b/bridge-ui/src/components/bridge/transaction-history/list-transaction/index.tsx
@@ -21,7 +21,7 @@ export default function ListTransaction({ transactions }: Props) {
};
return (
<>
-
+
{transactions.map((item, index) => (
))}
diff --git a/bridge-ui/src/components/setting/index.tsx b/bridge-ui/src/components/setting/index.tsx
index 881bc5a6..abf1a519 100644
--- a/bridge-ui/src/components/setting/index.tsx
+++ b/bridge-ui/src/components/setting/index.tsx
@@ -3,13 +3,17 @@ import styles from "./setting.module.scss";
import clsx from "clsx";
import SettingIcon from "@/assets/icons/setting.svg";
import ToggleSwitch from "@/components/ui/toggle-switch";
-import { useEffect, useRef, useState } from "react";
+import { HTMLAttributes, useEffect, useRef, useState } from "react";
import CurrencyDropdown from "@/components/bridge/currency-dropdown";
import { useConfigStore, useChainStore, useFormStore } from "@/stores";
import { useChains } from "@/hooks";
import { ChainLayer } from "@/types";
-export default function Setting() {
+interface SettingProps extends HTMLAttributes {
+ "data-testid": string;
+}
+
+export default function Setting(props: SettingProps) {
const dropdownRef = useRef(null);
const [isDropdownVisible, setDropdownVisible] = useState(false);
const setShowTestnet = useConfigStore.useSetShowTestnet();
@@ -56,6 +60,7 @@ export default function Setting() {
[styles["visible"]]: isDropdownVisible,
})}
onClick={toggleDropdown}
+ data-testid={props["data-testid"]}
>
@@ -76,6 +81,7 @@ export default function Setting() {
onChange={(checked) => {
setShowTestnet(checked);
}}
+ data-testid="native-bridge-test-network-toggle"
/>
diff --git a/bridge-ui/src/components/ui/toggle-switch/index.tsx b/bridge-ui/src/components/ui/toggle-switch/index.tsx
index 0fa7fa5d..ed6ed32a 100644
--- a/bridge-ui/src/components/ui/toggle-switch/index.tsx
+++ b/bridge-ui/src/components/ui/toggle-switch/index.tsx
@@ -8,7 +8,7 @@ type Props = {
disabled?: boolean;
};
-export default function ToggleSwitch({ checked = false, onChange, disabled }: Props) {
+export default function ToggleSwitch({ checked = false, onChange, disabled, ...rest }: Props) {
const [isChecked, setIsChecked] = useState
(checked);
useEffect(() => {
@@ -32,7 +32,7 @@ export default function ToggleSwitch({ checked = false, onChange, disabled }: Pr
})}
>
-
+
);
}
diff --git a/bridge-ui/src/types/cctp.ts b/bridge-ui/src/types/cctp.ts
index 1fd2b622..bd6b9d9b 100644
--- a/bridge-ui/src/types/cctp.ts
+++ b/bridge-ui/src/types/cctp.ts
@@ -3,7 +3,7 @@ export enum CctpAttestationMessageStatus {
COMPLETE = "complete",
}
export type CctpAttestationMessage = {
- attestation: `0x${string}`;
+ attestation: `0x${string}` | "PENDING";
message: `0x${string}`;
eventNonce: `0x${string}`;
cctpVersion: 1 | 2;
diff --git a/bridge-ui/src/utils/cctp.spec.ts b/bridge-ui/src/utils/cctp.spec.ts
new file mode 100644
index 00000000..6e803704
--- /dev/null
+++ b/bridge-ui/src/utils/cctp.spec.ts
@@ -0,0 +1,165 @@
+import { test } from "@playwright/test";
+import nock from "nock";
+import { getCctpMessageExpiryBlock, getCctpTransactionStatus } from "./cctp";
+import {
+ CctpAttestationMessage,
+ CctpAttestationMessageStatus,
+ CctpV2ReattestationApiResponse,
+ Chain,
+ ChainLayer,
+ TransactionStatus,
+} from "@/types";
+
+const { expect, describe } = test;
+
+describe("getCctpMessageExpiryBlock", () => {
+ test("should return undefined for empty byte string", () => {
+ const message = "0x";
+ const resp = getCctpMessageExpiryBlock(message);
+ expect(resp).toBeUndefined();
+ });
+
+ test("should return 0 if 0 expiryBlock encoded", () => {
+ const message =
+ "0x00000001000000000000000b41d77498ae6f504499ff1ead8c1c2a3318d48063b8022f8215f7631153534d210000000000000000000000008fe6b999dc680ccfdd5bf7eb0974218be2542daa0000000000000000000000008fe6b999dc680ccfdd5bf7eb0974218be2542daa0000000000000000000000000000000000000000000000000000000000000000000003e8000007d0000000010000000000000000000000001c7d4b196cb0c7b01d743fbc6116a902379c7238000000000000000000000000558d9534cac58f743a3a9e5382f77575a2595dcb0000000000000000000000000000000000000000000000000000000000002710000000000000000000000000558d9534cac58f743a3a9e5382f77575a2595dcb000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000";
+ const resp = getCctpMessageExpiryBlock(message);
+ expect(resp).toBe(BigInt(0));
+ });
+
+ test("should parse encoded expiryBlock", () => {
+ const message =
+ "0x00000001000000000000000b41d77498ae6f504499ff1ead8c1c2a3318d48063b8022f8215f7631153534d210000000000000000000000008fe6b999dc680ccfdd5bf7eb0974218be2542daa0000000000000000000000008fe6b999dc680ccfdd5bf7eb0974218be2542daa0000000000000000000000000000000000000000000000000000000000000000000003e8000007d0000000010000000000000000000000001c7d4b196cb0c7b01d743fbc6116a902379c7238000000000000000000000000558d9534cac58f743a3a9e5382f77575a2595dcb0000000000000000000000000000000000000000000000000000000000002710000000000000000000000000558d9534cac58f743a3a9e5382f77575a2595dcb00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000151caa0";
+ const resp = getCctpMessageExpiryBlock(message);
+ expect(resp).toBe(BigInt(22137504));
+ });
+});
+
+describe("getCctpTransactionStatus", () => {
+ const toChainStub: Chain = {
+ id: 59141,
+ cctpDomain: 11,
+ cctpMessageTransmitterV2Address: "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275",
+ cctpTokenMessengerV2Address: "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA",
+ gasLimitSurplus: 6000n,
+ profitMargin: 2n,
+ testnet: true,
+ tokenBridgeAddress: "0x93DcAdf238932e6e6a85852caC89cBd71798F463",
+ iconPath: "/images/logo/linea-sepolia.svg",
+ messageServiceAddress: "0x971e727e956690b9957be6d51Ec16E73AcAC83A7",
+ name: "Linea Sepolia",
+ layer: ChainLayer.L2,
+ nativeCurrency: {
+ decimals: 18,
+ name: "Linea Ether",
+ symbol: "ETH",
+ },
+ blockExplorers: {
+ default: {
+ apiUrl: "https://api-sepolia.lineascan.build/api",
+ name: "Etherscan",
+ url: "https://sepolia.lineascan.build",
+ },
+ },
+ };
+
+ const cctpApiRespPending: CctpAttestationMessage = {
+ attestation: "PENDING",
+ message: "0x",
+ eventNonce: "0xaf75c4d910592bc593c34bf6eb89937b1326ad43d9e3cf45581512efcf3e7da7",
+ cctpVersion: 2,
+ status: CctpAttestationMessageStatus.PENDING_CONFIRMATIONS,
+ };
+
+ // Comment out because didn't find a use yet for test, but is a valid API response
+ // const cctpApiRespReady: CctpAttestationMessage = {
+ // attestation:
+ // "0x362dff8a6f0a2c55345242a652bc8bef85dd206d02660c606b34a1f89574c25a58918896870a036f3271da65a9bff30f87d65f50f212dda4a3034fb77d110f511b3d44e4f71c4cdbb53f4e1f6d3f6357b87d056722151259f00f7beaa5af3ee35d21e48a00f932ba0ea02b19f26cb9b5dcd328f491ea91cb6b9ad896f493abe9c91c",
+ // message:
+ // "0x00000001000000000000000baf75c4d910592bc593c34bf6eb89937b1326ad43d9e3cf45581512efcf3e7da70000000000000000000000008fe6b999dc680ccfdd5bf7eb0974218be2542daa0000000000000000000000008fe6b999dc680ccfdd5bf7eb0974218be2542daa0000000000000000000000000000000000000000000000000000000000000000000003e8000007d0000000010000000000000000000000001c7d4b196cb0c7b01d743fbc6116a902379c7238000000000000000000000000558d9534cac58f743a3a9e5382f77575a2595dcb000000000000000000000000000000000000000000000000000000000000c350000000000000000000000000558d9534cac58f743a3a9e5382f77575a2595dcb000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ // eventNonce: "0xaf75c4d910592bc593c34bf6eb89937b1326ad43d9e3cf45581512efcf3e7da7",
+ // cctpVersion: 2,
+ // status: CctpAttestationMessageStatus.COMPLETE,
+ // };
+
+ const cctpApiRespNoExpiry: CctpAttestationMessage = {
+ attestation:
+ "0x06207a5ca18bc3860b5c546e8a18f6032180b3792ece47747774b9de14f62b717c51f439f0263b733b223e507a1aa0e56c823633dbb6931f864598f5c428237b1ca042a1555747bf4d7dac0b4184dad34dfed9be62ce978c18d619c8c537254a2e7b8331068215677ea900e02dd00ffee67fb61a93280c0eec789a52eb03f385b51c",
+ message:
+ "0x00000001000000000000000b41d77498ae6f504499ff1ead8c1c2a3318d48063b8022f8215f7631153534d210000000000000000000000008fe6b999dc680ccfdd5bf7eb0974218be2542daa0000000000000000000000008fe6b999dc680ccfdd5bf7eb0974218be2542daa0000000000000000000000000000000000000000000000000000000000000000000003e8000007d0000000010000000000000000000000001c7d4b196cb0c7b01d743fbc6116a902379c7238000000000000000000000000558d9534cac58f743a3a9e5382f77575a2595dcb0000000000000000000000000000000000000000000000000000000000002710000000000000000000000000558d9534cac58f743a3a9e5382f77575a2595dcb000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000",
+ eventNonce: "0x41d77498ae6f504499ff1ead8c1c2a3318d48063b8022f8215f7631153534d21",
+ cctpVersion: 2,
+ status: CctpAttestationMessageStatus.COMPLETE,
+ };
+
+ // Used on Linea Sepolia
+ const usedCctpNonce = "0xaf75c4d910592bc593c34bf6eb89937b1326ad43d9e3cf45581512efcf3e7da7";
+
+ // Random nonce, should be unused
+ const randomUnusedNonce = "0x97bce01d925f4152bbdc464a774bb8fcfd161557946b33930f0be0294e97eedf";
+
+ test("should return PENDING for an empty message", async () => {
+ const resp = await getCctpTransactionStatus(toChainStub, cctpApiRespPending, cctpApiRespPending.eventNonce);
+ expect(resp).toBe(TransactionStatus.PENDING);
+ });
+
+ test("should return COMPLETE for a used nonce", async () => {
+ const resp = await getCctpTransactionStatus(toChainStub, cctpApiRespNoExpiry, usedCctpNonce);
+ expect(resp).toBe(TransactionStatus.COMPLETED);
+ });
+
+ test("should return PENDING for a truncated message", async () => {
+ // Immutable creation of new object
+ const corruptedCctpApiResp = { ...cctpApiRespNoExpiry };
+ // Chop off last character. Last 32-bytes of message should be expirationBlock as per https://developers.circle.com/stablecoins/message-format
+ corruptedCctpApiResp.message = corruptedCctpApiResp.message.slice(0, -1) as `0x${string}`;
+
+ const resp = await getCctpTransactionStatus(toChainStub, corruptedCctpApiResp, randomUnusedNonce);
+ expect(resp).toBe(TransactionStatus.PENDING);
+ });
+
+ // TODO later - Address edge case where 'parseInt("0ILLEGAL", 16) == 0',
+
+ test("should return PENDING for a corrupted message", async () => {
+ const corruptedCctpApiResp = { ...cctpApiRespNoExpiry };
+ // Replace hex characters with non-hex characters
+ corruptedCctpApiResp.message = (corruptedCctpApiResp.message.slice(0, -64) +
+ "ILLEGALILLEGALILLEGALILLEGALILLEGALILLEGALILLEGALILLEGALILLEGALI") as `0x${string}`;
+
+ const resp = await getCctpTransactionStatus(toChainStub, corruptedCctpApiResp, randomUnusedNonce);
+ expect(resp).toBe(TransactionStatus.PENDING);
+ });
+
+ test("should return PENDING if i.) CCTP API response has pending status, ii.) message has no expiry and iii.) nonce unused", async () => {
+ const stubbedCctpApiResp = { ...cctpApiRespNoExpiry };
+ stubbedCctpApiResp.status = CctpAttestationMessageStatus.PENDING_CONFIRMATIONS;
+ const resp = await getCctpTransactionStatus(toChainStub, stubbedCctpApiResp, randomUnusedNonce);
+ expect(resp).toBe(TransactionStatus.PENDING);
+ });
+
+ test("should return READY_TO_CLAIM if i.) CCTP API response has complete status, ii.) message has no expiry and iii.) nonce unused", async () => {
+ const stubbedCctpApiResp = { ...cctpApiRespNoExpiry };
+ stubbedCctpApiResp.status = CctpAttestationMessageStatus.COMPLETE;
+
+ const resp = await getCctpTransactionStatus(toChainStub, stubbedCctpApiResp, randomUnusedNonce);
+ expect(resp).toBe(TransactionStatus.READY_TO_CLAIM);
+ });
+
+ test("should return PENDING and call reattest API if i.) message is expired and ii.) nonce unused", async () => {
+ const expiredCctpApiResp = { ...cctpApiRespNoExpiry };
+ // Put expiry block of 1
+ expiredCctpApiResp.message = (expiredCctpApiResp.message.slice(0, -1) + "1") as `0x${string}`;
+ // Intercept request to CCTP reattest API
+ const stubbedReattestApiResp: CctpV2ReattestationApiResponse = {
+ message: "message",
+ nonce: randomUnusedNonce,
+ };
+ const interceptor = nock("https://iris-api-sandbox.circle.com")
+ .post(/.*/) // Intercept all POST requests
+ .reply(200, stubbedReattestApiResp);
+
+ const resp = await getCctpTransactionStatus(toChainStub, expiredCctpApiResp, randomUnusedNonce);
+ expect(resp).toBe(TransactionStatus.PENDING);
+ // Assert we made a request to CCTP reattest API
+ expect(interceptor.isDone()).toBe(true);
+ });
+});
diff --git a/bridge-ui/src/utils/cctp.ts b/bridge-ui/src/utils/cctp.ts
index d715643b..9fd362ab 100644
--- a/bridge-ui/src/utils/cctp.ts
+++ b/bridge-ui/src/utils/cctp.ts
@@ -1,13 +1,13 @@
-import MessageTransmitterV2 from "@/abis/MessageTransmitterV2.json";
+import MessageTransmitterV2 from "@/abis/MessageTransmitterV2.json" assert { type: "json" };
import { CctpAttestationMessage, Chain, TransactionStatus, CctpAttestationMessageStatus } from "@/types";
import { GetPublicClientReturnType } from "@wagmi/core";
import { fetchCctpAttestationByTxHash, reattestCctpV2PreFinalityMessage } from "@/services/cctp";
import { getPublicClient } from "@wagmi/core";
import { config as wagmiConfig } from "@/lib/wagmi";
import {
+ CCTP_V2_MESSAGE_HEADER_LENGTH,
CCTP_V2_EXPIRATION_BLOCK_LENGTH,
CCTP_V2_EXPIRATION_BLOCK_OFFSET,
- CCTP_V2_MESSAGE_HEADER_LENGTH,
} from "@/constants";
const isCctpNonceUsed = async (
@@ -25,12 +25,14 @@ const isCctpNonceUsed = async (
return resp === 1n;
};
-const getCctpMessageExpiryBlock = (message: string): bigint | undefined => {
+export const getCctpMessageExpiryBlock = (message: string): bigint | undefined => {
// See CCTPV2 message format at https://developers.circle.com/stablecoins/message-format
const expiryInHex = message.substring(
CCTP_V2_EXPIRATION_BLOCK_OFFSET,
CCTP_V2_EXPIRATION_BLOCK_OFFSET + CCTP_V2_EXPIRATION_BLOCK_LENGTH,
);
+ // Should be 32-bytes
+ if (expiryInHex.length !== 64) return undefined;
const expiryInInt = parseInt(expiryInHex, 16);
if (Number.isNaN(expiryInInt)) return undefined;
// Return bigint because this is also returned by Viem client.getBlockNumber()
diff --git a/bridge-ui/test/advancedFixtures.ts b/bridge-ui/test/advancedFixtures.ts
index 0a9406f3..329478c1 100644
--- a/bridge-ui/test/advancedFixtures.ts
+++ b/bridge-ui/test/advancedFixtures.ts
@@ -1,31 +1,150 @@
-import { metaMaskFixtures } from "@synthetixio/synpress";
+import { metaMaskFixtures } from "@synthetixio/synpress/playwright";
import setup from "./wallet-setup/metamask.setup";
-
+import { Locator } from "@playwright/test";
+import { getNativeBridgeTransactionsCountImpl, selectTokenAndWaitForBalance } from "./utils";
+import { LINEA_SEPOLIA_NETWORK, POLLING_INTERVAL } from "./constants";
+/**
+ * NB: There is an issue with Synpress `metaMaskFixtures` extension functions wherein extension functions
+ * may not be able to reuse other extension functions. This is especially the case when advanced operations
+ * on the 'Page' object are done. It seems that the 'Page' object does not remain the same in a nested
+ * extension function call between the different layers of nesting.
+ *
+ * Nested `Metamask` object uses however seem ok.
+ */
export const test = metaMaskFixtures(setup).extend<{
- initUI: (firstInit?: boolean) => Promise;
- waitForTransactionToConfirm: () => Promise;
- getBridgeTransactionsCount: () => Promise;
- sendTokens: (amount: string, isETH?: boolean) => Promise;
- waitForTransactionListUpdate: (txCountBeforeUpdate: number) => Promise;
- selectToken: (tokenName: string) => Promise;
-}>({
- initUI: async ({ page }, use) => {
- await use(async (firstInit: boolean = false) => {
- const nativeBridgeBtn = await page.waitForSelector("#native-bridge-btn");
- await nativeBridgeBtn.click();
+ // Bridge UI Actions
+ clickNativeBridgeButton: () => Promise;
+ openNativeBridgeTransactionHistory: () => Promise;
+ closeNativeBridgeTransactionHistory: () => Promise;
+ openNativeBridgeFormSettings: () => Promise;
+ toggleShowTestNetworksInNativeBridgeForm: () => Promise;
+ getNativeBridgeTransactionsCount: () => Promise;
+ selectTokenAndInputAmount: (tokenSymbol: string, amount: string) => Promise;
+ waitForNewTxAdditionToTxList: (txCountBeforeUpdate: number) => Promise;
+ waitForTxListUpdateForClaimTx: (claimTxCountBeforeUpdate: number) => Promise;
- if (firstInit) {
- const agreeTermsBtn = await page.waitForSelector("#agree-terms-btn");
- await agreeTermsBtn.click();
- }
+ // Metamask Actions - Should be ok to reuse within other fixture functions
+ connectMetamaskToDapp: () => Promise;
+ waitForTransactionToConfirm: () => Promise;
+ confirmTransactionAndWaitForInclusion: () => Promise;
+ switchToLineaSepolia: () => Promise;
+
+ // Composite Bridge UI + Metamask Actions
+ doTokenApprovalIfNeeded: () => Promise;
+ doInitiateBridgeTransaction: () => Promise;
+ doClaimTransaction: () => Promise;
+}>({
+ // Bridge UI Actions
+ clickNativeBridgeButton: async ({ page }, use) => {
+ await use(async () => {
+ const nativeBridgeBtn = page.getByRole("link", { name: "Native Bridge", exact: true });
+ await nativeBridgeBtn.click();
+ return nativeBridgeBtn;
+ });
+ },
+ openNativeBridgeTransactionHistory: async ({ page }, use) => {
+ await use(async () => {
+ const txHistoryIconButton = page.getByTestId("native-bridge-transaction-history-icon");
+ await txHistoryIconButton.click();
+ });
+ },
+ closeNativeBridgeTransactionHistory: async ({ page }, use) => {
+ await use(async () => {
+ const backButton = page.getByTestId("transaction-history-close-btn");
+ await backButton.click();
+ });
+ },
+ openNativeBridgeFormSettings: async ({ page }, use) => {
+ await use(async () => {
+ const formSettingsIconButton = page.getByTestId("native-bridge-form-settings-icon");
+ await formSettingsIconButton.click();
+ });
+ },
+ toggleShowTestNetworksInNativeBridgeForm: async ({ page }, use) => {
+ await use(async () => {
+ await page.getByTestId("native-bridge-test-network-toggle").click();
+ });
+ },
+ getNativeBridgeTransactionsCount: async ({ page }, use) => {
+ await use(async () => {
+ return await getNativeBridgeTransactionsCountImpl(page);
+ });
+ },
+ selectTokenAndInputAmount: async ({ page }, use) => {
+ await use(async (tokenSymbol: string, amount: string) => {
+ // Wait for page to retrieve blockchain token balance
+ await selectTokenAndWaitForBalance(tokenSymbol, page);
+
+ // Input amount
+ const amountInput = page.getByRole("textbox", { name: "0", exact: true });
+ await amountInput.fill(amount);
+
+ // Wait for "Receive amount" to populate, we need to fetch blockchain data before proceeding
+ const receivedAmountField = page.getByTestId("received-amount-text");
+ await receivedAmountField.waitFor({ state: "visible" });
+
+ // Check if there are sufficient funds available
+ const insufficientFundsButton = page.getByRole("button", { name: "Insufficient funds", exact: true });
+ if ((await insufficientFundsButton.count()) > 0)
+ throw "Insufficient funds available, please add some funds before running the test";
+ });
+ },
+ waitForNewTxAdditionToTxList: async ({ page }, use) => {
+ await use(async (txCountBeforeUpdate: number) => {
+ const maxTries = 10;
+ let tryCount = 0;
+ let listUpdated = false;
+ do {
+ const newTxCount = await getNativeBridgeTransactionsCountImpl(page);
+ listUpdated = newTxCount !== txCountBeforeUpdate;
+ tryCount++;
+ await page.waitForTimeout(POLLING_INTERVAL);
+ } while (!listUpdated && tryCount < maxTries);
+ });
+ },
+ waitForTxListUpdateForClaimTx: async ({ page }, use) => {
+ await use(async (claimTxCountBeforeUpdate: number) => {
+ const maxTries = 10;
+ const readyToClaimTx = page.getByRole("listitem").filter({ hasText: "Ready to claim" });
+ let tryCount = 0;
+ let listUpdated = false;
+ do {
+ const newReadyToClaimCount = await readyToClaimTx.count();
+ listUpdated = newReadyToClaimCount === claimTxCountBeforeUpdate + 1;
+ tryCount++;
+ await page.waitForTimeout(POLLING_INTERVAL);
+ } while (!listUpdated && tryCount < maxTries);
+ });
+ },
+
+ // Metamask Actions - Should be ok to reuse within other fixture functions
+ connectMetamaskToDapp: async ({ page, metamask }, use) => {
+ await use(async () => {
+ // Click Connect button
+ const connectBtn = page.getByRole("button", { name: "Connect", exact: true }).first();
+ await connectBtn.click();
+
+ // Click on 'Metamask' on the wallet dropdown menu
+ const metamaskBtnInDropdownList = page.getByRole("button").filter({ hasText: "MetaMask" }).first();
+ await metamaskBtnInDropdownList.click();
+
+ await metamask.connectToDapp();
+ await metamask.goBackToHomePage();
+ await page.bringToFront();
});
},
waitForTransactionToConfirm: async ({ metamask }, use) => {
await use(async () => {
await metamask.page.bringToFront();
await metamask.page.reload();
+
const activityButton = metamask.page.locator("button", { hasText: "Activity" });
await activityButton.waitFor();
+ // Sometimes a "What's new" modal pops up on Metamask. We assume this becomes visible at the same time as the Activity button
+ // This modal causes flaky tests because it appears unpredictably, and blocks other actions.
+ const gotItButton = metamask.page.locator("button", { hasText: "Got it" });
+ if (await gotItButton.isVisible()) await gotItButton.click();
+ // Click Activity button
await activityButton.click();
let txCount = await metamask.page
@@ -38,74 +157,72 @@ export const test = metaMaskFixtures(setup).extend<{
}
});
},
- getBridgeTransactionsCount: async ({ page }, use) => {
+ confirmTransactionAndWaitForInclusion: async ({ page, metamask, waitForTransactionToConfirm }, use) => {
await use(async () => {
- const transactionsCount = await page.locator("#transactions-list").locator("ul").count();
- return transactionsCount;
+ await metamask.confirmTransaction();
+ await waitForTransactionToConfirm();
+ await page.bringToFront();
});
},
- sendTokens: async ({ page, metamask, waitForTransactionToConfirm }, use) => {
- await use(async (amount: string, isETH = false) => {
- const amountInput = await page.waitForSelector("#amount-input");
-
- // Sending the smallest amount of USDC
- await amountInput.fill(amount);
-
- // Check if there are funds available
- const approveBtnDisabled = await page.locator("#approve-btn.btn-disabled").count();
- const submitBtnDisabled = await page.locator("#submit-erc-btn.btn-disabled").count();
- if (approveBtnDisabled === 1 && submitBtnDisabled === 1) {
- throw "No funds available, please add some funds before running the test";
- }
-
- const tokenType = isETH ? "eth" : "erc";
-
- // Check that this amount has been approved
- if (tokenType === "erc" && submitBtnDisabled === 1) {
- //We need to approve the amount first
- const approveBtn = await page.waitForSelector(`#approve-btn`);
- await approveBtn.click();
-
- await metamask.page.bringToFront();
- await metamask.page.reload();
- const nextBtn = metamask.page.locator("button", {
- hasText: "Next",
- });
- await nextBtn.waitFor();
- await nextBtn.click();
-
- const approveMMBtn = metamask.page.locator("button", { hasText: "Approve" });
- await approveMMBtn.waitFor();
- await approveMMBtn.click();
-
- await waitForTransactionToConfirm();
- await page.bringToFront();
- }
- const submitBtn = await page.waitForSelector(`#submit-${tokenType}-btn`);
- await submitBtn.click();
+ switchToLineaSepolia: async ({ metamask }, use) => {
+ await use(async () => {
+ await metamask.switchNetwork(LINEA_SEPOLIA_NETWORK.name, true);
});
},
- waitForTransactionListUpdate: async ({ getBridgeTransactionsCount }, use) => {
- await use(async (txCountBeforeUpdate: number) => {
- const maxTries = 20;
- let tryCount = 0;
- let listUpdated = false;
- do {
- const newTxCount = await getBridgeTransactionsCount();
- listUpdated = newTxCount !== txCountBeforeUpdate;
- tryCount++;
- } while (!listUpdated && tryCount < maxTries);
+ // Composite Bridge UI + Metamask Actions
+ doTokenApprovalIfNeeded: async ({ page, metamask, waitForTransactionToConfirm }, use) => {
+ await use(async () => {
+ // Check if approval required
+ const approvalButton = page.getByRole("button", { name: "Approve Token", exact: true });
+ if ((await approvalButton.count()) === 0) return;
+ await approvalButton.click();
- return listUpdated;
+ // Handle Metamask approval UI
+ await metamask.approveTokenPermission();
+ await waitForTransactionToConfirm();
+
+ // Close 'Transaction successful' modal
+ await page.bringToFront();
+ const closeModalBtn = page.getByRole("button", { name: "Bridge your token", exact: true });
+ await closeModalBtn.click();
});
},
- selectToken: async ({ page }, use) => {
- await use(async (tokenName: string) => {
- const tokenETHBtn = await page.waitForSelector("#token-select-btn");
- await tokenETHBtn.click();
- const tokenUSDCBtn = await page.waitForSelector(`#token-details-${tokenName}-btn`);
- await tokenUSDCBtn.click();
+ doInitiateBridgeTransaction: async ({ page, confirmTransactionAndWaitForInclusion }, use) => {
+ await use(async () => {
+ // Click "Bridge" button
+ const bridgeButton = page.getByRole("button", { name: "Bridge", exact: true });
+ await bridgeButton.waitFor();
+ await bridgeButton.click();
+
+ // Click "Confirm and Bridge" button
+ const confirmAndBridgeButton = page.getByTestId("confirm-and-bridge-btn");
+ await expect(confirmAndBridgeButton).toBeVisible();
+ await expect(confirmAndBridgeButton).toBeEnabled();
+ await confirmAndBridgeButton.click();
+
+ // Confirm Metamask Tx and wait for blockchain inclusion
+ // Should be ok to reuse this fixture function because it doesn't do much on the `Page` object
+ await confirmTransactionAndWaitForInclusion();
+
+ // Click on 'View transactions' button on the 'Transaction confirmed' modal
+ const viewTxButton = page.getByRole("button", { name: "View transactions", exact: true });
+ await viewTxButton.click();
+ });
+ },
+ doClaimTransaction: async ({ page, confirmTransactionAndWaitForInclusion }, use) => {
+ await use(async () => {
+ // Click on 'Claim' button
+ const claimButton = page.getByRole("button", { name: "Claim", exact: true });
+ await expect(claimButton).toBeVisible();
+ await expect(claimButton).toBeEnabled();
+ await claimButton.click();
+
+ // Confirm Metamask Tx and wait for blockchain inclusion
+ // Should be ok to reuse this fixture function because it doesn't do much on the `Page` object
+ await confirmTransactionAndWaitForInclusion();
+
+ // Should finish on tx history page
});
},
});
diff --git a/bridge-ui/test/constants.ts b/bridge-ui/test/constants.ts
index cd85be7a..4f23041e 100644
--- a/bridge-ui/test/constants.ts
+++ b/bridge-ui/test/constants.ts
@@ -1,4 +1,3 @@
-import "dotenv/config";
import { formatEther, formatUnits } from "viem";
export const METAMASK_SEED_PHRASE = process.env.E2E_TEST_SEED_PHRASE;
@@ -15,6 +14,10 @@ export const LINEA_SEPOLIA_NETWORK = {
};
export const TEST_URL = "http://localhost:3000/";
-export const SEPOLIA_NETWORK_NAME = "Sepolia";
-export const WEI_AMOUNT = formatEther(BigInt(1)).toString();
-export const USDC_AMOUNT = formatUnits(BigInt(1), 6).toString();
+export const WEI_AMOUNT = formatEther(1n).toString();
+// Must be > minimum CCTP fee
+export const USDC_AMOUNT = formatUnits(10n, 6).toString();
+export const ETH_SYMBOL = "ETH";
+export const USDC_SYMBOL = "USDC";
+
+export const POLLING_INTERVAL = 250;
diff --git a/bridge-ui/test/e2e/bridge-l1-l2.spec.ts b/bridge-ui/test/e2e/bridge-l1-l2.spec.ts
index 46e52ad9..ba3333f4 100644
--- a/bridge-ui/test/e2e/bridge-l1-l2.spec.ts
+++ b/bridge-ui/test/e2e/bridge-l1-l2.spec.ts
@@ -1,189 +1,164 @@
import { testWithSynpress } from "@synthetixio/synpress";
import { test as advancedFixtures } from "../advancedFixtures";
-import { SEPOLIA_NETWORK_NAME, TEST_URL, USDC_AMOUNT, WEI_AMOUNT } from "../constants";
+import { TEST_URL, USDC_SYMBOL, USDC_AMOUNT, WEI_AMOUNT, ETH_SYMBOL } from "../constants";
const test = testWithSynpress(advancedFixtures);
const { expect, describe } = test;
-describe("Bridge L1 > L2", () => {
- test.skip("should set up the UI and metamask correctly", async ({ page, metamask, initUI }) => {
- await initUI(true);
-
- await page.locator("#wallet-connect-btn").click();
- await page.locator("wui-list-wallet", { hasText: "MetaMask" }).nth(1).click();
-
- await metamask.connectToDapp();
-
- await page.bringToFront();
- });
-
+// To consider in a later ticket - Bridge ERC20 tokens case when ERC20 token is available in Sepolia token list
+describe("L1 > L2 via Native Bridge", () => {
test("should successfully go to the bridge UI page", async ({ page }) => {
const pageUrl = page.url();
expect(pageUrl).toEqual(TEST_URL);
});
- test.skip("should successfully display the correct heading", async ({ page, initUI }) => {
- await initUI(true);
-
- const header = "Bridge";
- await page.locator("h2", { hasText: header }).waitFor({ state: "visible" });
+ test("should have 'Native Bridge' button link on homepage", async ({ clickNativeBridgeButton }) => {
+ const nativeBridgeBtn = await clickNativeBridgeButton();
+ await expect(nativeBridgeBtn).toBeVisible();
});
- test.skip("metamask should be connected to the right network", async ({ page, metamask, initUI }) => {
- await initUI(true);
-
- await page.locator("#wallet-connect-btn").click();
- await page.locator("wui-list-wallet", { hasText: "MetaMask" }).nth(1).click();
-
- await metamask.connectToDapp();
-
- await page.bringToFront();
- await page
- .locator("#active-chain-name", {
- hasText: SEPOLIA_NETWORK_NAME,
- })
- .waitFor();
+ test("should connect MetaMask to dapp correctly", async ({ connectMetamaskToDapp, clickNativeBridgeButton }) => {
+ await clickNativeBridgeButton();
+ await connectMetamaskToDapp();
});
- test.skip("should be able to reload the transaction history", async ({ page, metamask, initUI }) => {
- await initUI(true);
- await page.locator("#wallet-connect-btn").click();
- await page.locator("wui-list-wallet", { hasText: "MetaMask" }).nth(1).click();
-
- await metamask.connectToDapp();
-
- const reloadHistoryBtn = await page.waitForSelector("#reload-history-btn");
- await reloadHistoryBtn.click();
-
- const reloadConfirmBtn = await page.waitForSelector("#reload-history-confirm-btn");
- await reloadConfirmBtn.click();
-
- await page.locator("#transactions-list").locator("ul").nth(1).waitFor({ timeout: 10_000 });
- });
-
- test.skip("should be able to switch network", async ({ page, metamask, initUI }) => {
- await initUI(true);
- await page.locator("#wallet-connect-btn").click();
- await page.locator("wui-list-wallet", { hasText: "MetaMask" }).nth(1).click();
-
- await metamask.connectToDapp();
-
- await page.locator("#chain-select").click();
- await page.locator("#switch-alternative-chain-btn").click();
-
- await metamask.approveSwitchNetwork();
-
- await page.bringToFront();
-
- await page.locator("#active-chain-name").getByText("Linea Sepolia Testnet").waitFor();
- });
-
- test.skip("should be able to claim funds if available", async ({
+ test("should be able to load the transaction history", async ({
page,
- metamask,
- initUI,
- waitForTransactionToConfirm,
+ connectMetamaskToDapp,
+ clickNativeBridgeButton,
+ openNativeBridgeTransactionHistory,
}) => {
- await initUI(true);
- await page.locator("#wallet-connect-btn").click();
- await page.locator("wui-list-wallet", { hasText: "MetaMask" }).nth(1).click();
+ await connectMetamaskToDapp();
+ await clickNativeBridgeButton();
+ await openNativeBridgeTransactionHistory();
- await metamask.connectToDapp();
-
- // Check if there is a claim button available
- const checkClaimBtn = await page.locator("#claim-funds-btn").all();
- if (checkClaimBtn.length > 0) {
- const claimBtn = page.locator("#claim-funds-btn").nth(1);
- await claimBtn.click();
-
- await metamask.confirmTransaction();
-
- await waitForTransactionToConfirm();
- } else {
- console.warn("Claim funds could not be tested since no funds are waiting to be claimed");
- }
+ const txHistoryHeading = page.getByRole("heading").filter({ hasText: "Transaction History" });
+ await expect(txHistoryHeading).toBeVisible();
});
- test.skip("should be able to bridge ETH from L1 to L2", async ({
+ test("should be able to switch to test networks", async ({
page,
- metamask,
- getBridgeTransactionsCount,
- sendTokens,
- waitForTransactionToConfirm,
- waitForTransactionListUpdate,
+ connectMetamaskToDapp,
+ clickNativeBridgeButton,
+ openNativeBridgeFormSettings,
+ toggleShowTestNetworksInNativeBridgeForm,
}) => {
- await page.bringToFront();
- const txnsLengthBefore = await getBridgeTransactionsCount();
+ await connectMetamaskToDapp();
+ await clickNativeBridgeButton();
+ await openNativeBridgeFormSettings();
+ await toggleShowTestNetworksInNativeBridgeForm();
- await sendTokens(WEI_AMOUNT, true);
- await metamask.confirmTransaction();
-
- // Wait for transaction to finish
- await waitForTransactionToConfirm();
-
- await page.bringToFront();
- // We check at the end that the transacton list is updated on the bridge UI
- const listUpdated = await waitForTransactionListUpdate(txnsLengthBefore);
-
- // Check that that new transaction is added to the list on the UI
- expect(listUpdated).toBeTruthy();
+ // Should have Sepolia text visible
+ const sepoliaText = page.getByText("Sepolia").first();
+ await expect(sepoliaText).toBeVisible();
});
- test.skip("should be able to bridge USDC from L1 to L2", async ({
- page,
- metamask,
- getBridgeTransactionsCount,
- selectToken,
- sendTokens,
- waitForTransactionToConfirm,
- waitForTransactionListUpdate,
+ test("should be able to initiate bridging ETH from L1 to L2 in testnet", async ({
+ getNativeBridgeTransactionsCount,
+ waitForNewTxAdditionToTxList,
+ connectMetamaskToDapp,
+ clickNativeBridgeButton,
+ openNativeBridgeFormSettings,
+ toggleShowTestNetworksInNativeBridgeForm,
+ selectTokenAndInputAmount,
+ doInitiateBridgeTransaction,
+ openNativeBridgeTransactionHistory,
+ closeNativeBridgeTransactionHistory,
}) => {
- const txnsLengthBefore = await getBridgeTransactionsCount();
+ // Code smell that we may need to refactor E2E tests with blockchain tx into another describe block with a separate timeout
+ test.setTimeout(90_000);
- // Select USDC in the token list
- await selectToken("USDC");
+ // Setup testnet UI
+ await connectMetamaskToDapp();
+ await clickNativeBridgeButton();
+ await openNativeBridgeFormSettings();
+ await toggleShowTestNetworksInNativeBridgeForm();
- await sendTokens(USDC_AMOUNT);
+ // Get # of txs in txHistory before doing bridge tx, so that we can later confirm that our bridge tx shows up in the txHistory.
+ await openNativeBridgeTransactionHistory();
+ const txnsLengthBefore = await getNativeBridgeTransactionsCount();
+ await closeNativeBridgeTransactionHistory();
- await metamask.confirmTransaction();
+ // // Actual bridging actions
+ await selectTokenAndInputAmount(ETH_SYMBOL, WEI_AMOUNT);
+ await doInitiateBridgeTransaction();
- // Wait for transaction to finish
- await waitForTransactionToConfirm();
-
- await page.bringToFront();
- // We check at the end that the transacton list is updated on the bridge UI
- const listUpdated = await waitForTransactionListUpdate(txnsLengthBefore);
-
- // Check that that new transaction is added to the list on the UI
- expect(listUpdated).toBeTruthy();
+ // Check that our bridge tx shows up in the tx history
+ await waitForNewTxAdditionToTxList(txnsLengthBefore);
});
- test.skip("should be able to bridge ERC20 tokens from L1 to L2", async ({
- page,
- metamask,
- getBridgeTransactionsCount,
- selectToken,
- sendTokens,
- waitForTransactionListUpdate,
- waitForTransactionToConfirm,
+ test("should be able to initiate bridging USDC from L1 to L2 in testnet", async ({
+ getNativeBridgeTransactionsCount,
+ waitForNewTxAdditionToTxList,
+ connectMetamaskToDapp,
+ clickNativeBridgeButton,
+ openNativeBridgeFormSettings,
+ toggleShowTestNetworksInNativeBridgeForm,
+ selectTokenAndInputAmount,
+ doInitiateBridgeTransaction,
+ openNativeBridgeTransactionHistory,
+ closeNativeBridgeTransactionHistory,
+ doTokenApprovalIfNeeded,
}) => {
- const txnsLengthBefore = await getBridgeTransactionsCount();
+ // At least 2 blockchain tx in this test
+ test.setTimeout(120_000);
- // Select WETH in the token list (Easiest to get)
- await selectToken("WETH");
+ // Setup testnet UI
+ await connectMetamaskToDapp();
+ await clickNativeBridgeButton();
+ await openNativeBridgeFormSettings();
+ await toggleShowTestNetworksInNativeBridgeForm();
- await sendTokens(WEI_AMOUNT);
- await metamask.confirmTransaction();
+ // Get # of txs in txHistory before doing bridge tx, so that we can later confirm that our bridge tx shows up in the txHistory.
+ await openNativeBridgeTransactionHistory();
+ const txnsLengthBefore = await getNativeBridgeTransactionsCount();
+ await closeNativeBridgeTransactionHistory();
- // Wait for transaction to finish
- await waitForTransactionToConfirm();
+ // Actual bridging actions
+ await selectTokenAndInputAmount(USDC_SYMBOL, USDC_AMOUNT);
+ await doTokenApprovalIfNeeded();
+ await doInitiateBridgeTransaction();
- await page.bringToFront();
- // We check at the end that the transacton list is updated on the bridge UI
- const listUpdated = await waitForTransactionListUpdate(txnsLengthBefore);
+ // Check that our bridge tx shows up in the tx history
+ await waitForNewTxAdditionToTxList(txnsLengthBefore);
+ });
- // Check that that new transaction is added to the list on the UI
- expect(listUpdated).toBeTruthy();
+ test("should be able to claim if available READY_TO_CLAIM transactions", async ({
+ page,
+ connectMetamaskToDapp,
+ clickNativeBridgeButton,
+ openNativeBridgeFormSettings,
+ toggleShowTestNetworksInNativeBridgeForm,
+ openNativeBridgeTransactionHistory,
+ getNativeBridgeTransactionsCount,
+ switchToLineaSepolia,
+ doClaimTransaction,
+ waitForTxListUpdateForClaimTx,
+ }) => {
+ test.setTimeout(90_000);
+
+ await connectMetamaskToDapp();
+ await clickNativeBridgeButton();
+ await openNativeBridgeFormSettings();
+ await toggleShowTestNetworksInNativeBridgeForm();
+
+ // Switch to L2 network
+ await switchToLineaSepolia();
+
+ // Load tx history
+ await openNativeBridgeTransactionHistory();
+ await getNativeBridgeTransactionsCount();
+
+ // Find and click READY_TO_CLAIM TX
+ const readyToClaimTx = page.getByRole("listitem").filter({ hasText: "Ready to claim" });
+ const readyToClaimCount = await readyToClaimTx.count();
+ if (readyToClaimCount === 0) return;
+ await readyToClaimTx.first().click();
+
+ await doClaimTransaction();
+
+ // Check that tx history has updated accordingly
+ await waitForTxListUpdateForClaimTx(readyToClaimCount);
});
});
diff --git a/bridge-ui/test/utils/getNativeBridgeTransactionsCountImpl.ts b/bridge-ui/test/utils/getNativeBridgeTransactionsCountImpl.ts
new file mode 100644
index 00000000..3f0de95b
--- /dev/null
+++ b/bridge-ui/test/utils/getNativeBridgeTransactionsCountImpl.ts
@@ -0,0 +1,15 @@
+import { Page } from "@playwright/test";
+
+export async function getNativeBridgeTransactionsCountImpl(page: Page): Promise {
+ const txList = page.getByTestId("native-bridge-transaction-history-list");
+ const noTransactionsYetText = page.getByText("No transactions yet");
+ // Either `txList` or `noTransactionsYetText` will appear. Should be mutually exclusive.
+ await Promise.race([txList.waitFor({ state: "visible" }), noTransactionsYetText.waitFor({ state: "visible" })]);
+
+ // Check which element is actually visible
+ const isTxListVisible = await txList.isVisible();
+ if (!isTxListVisible) return 0;
+ const txs = txList.getByRole("listitem");
+ const txCount = txs.count();
+ return txCount;
+}
diff --git a/bridge-ui/test/utils/index.ts b/bridge-ui/test/utils/index.ts
new file mode 100644
index 00000000..b1918216
--- /dev/null
+++ b/bridge-ui/test/utils/index.ts
@@ -0,0 +1,2 @@
+export { selectTokenAndWaitForBalance } from "./selectTokenAndWaitForBalance";
+export { getNativeBridgeTransactionsCountImpl } from "./getNativeBridgeTransactionsCountImpl";
diff --git a/bridge-ui/test/utils/selectTokenAndWaitForBalance.ts b/bridge-ui/test/utils/selectTokenAndWaitForBalance.ts
new file mode 100644
index 00000000..878c72b8
--- /dev/null
+++ b/bridge-ui/test/utils/selectTokenAndWaitForBalance.ts
@@ -0,0 +1,24 @@
+import { Page } from "@playwright/test";
+import { POLLING_INTERVAL } from "../constants";
+
+export async function selectTokenAndWaitForBalance(tokenSymbol: string, page: Page) {
+ const openModalBtn = page.getByTestId("native-bridge-open-token-list-modal");
+ await openModalBtn.click();
+ // Wait for API request to retrieve blockchain balance.
+ const tokenBalance = page.getByTestId(`token-details-${tokenSymbol.toLowerCase()}-amount`);
+ console.log(`Fetching token balance for ${tokenSymbol}`);
+
+ // Timeout implementation
+ const fetchTokenBalanceTimeout = 5000;
+ let fetchTokenTimeUsed = 0;
+ while ((await tokenBalance.textContent()) === `0 ${tokenSymbol}`) {
+ if (fetchTokenTimeUsed >= fetchTokenBalanceTimeout)
+ throw `Could not find any balance for ${tokenSymbol}, does the testing wallet have funds?`;
+ await page.waitForTimeout(POLLING_INTERVAL);
+ fetchTokenTimeUsed += POLLING_INTERVAL;
+ }
+ console.log(`Selected token balance: ${await tokenBalance.textContent()}`);
+
+ // Select token
+ await page.getByTestId(`token-details-${tokenSymbol.toLowerCase()}-btn`).click();
+}
diff --git a/bridge-ui/test/wallet-setup/metamask.setup.ts b/bridge-ui/test/wallet-setup/metamask.setup.ts
index 925acfe8..fbe86b5f 100644
--- a/bridge-ui/test/wallet-setup/metamask.setup.ts
+++ b/bridge-ui/test/wallet-setup/metamask.setup.ts
@@ -1,5 +1,6 @@
-import { MetaMask, defineWalletSetup, getExtensionId } from "@synthetixio/synpress";
import { LINEA_SEPOLIA_NETWORK, METAMASK_PASSWORD, METAMASK_SEED_PHRASE, TEST_PRIVATE_KEY } from "../constants";
+import { defineWalletSetup } from "@synthetixio/synpress";
+import { MetaMask, getExtensionId } from "@synthetixio/synpress/playwright";
export default defineWalletSetup(METAMASK_PASSWORD, async (context, walletPage) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -8,7 +9,6 @@ export default defineWalletSetup(METAMASK_PASSWORD, async (context, walletPage)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
const metamask = new MetaMask(context, walletPage, METAMASK_PASSWORD, extensionId);
-
await metamask.importWallet(METAMASK_SEED_PHRASE);
await metamask.importWalletFromPrivateKey(TEST_PRIVATE_KEY);
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ab34be53..9f2f399b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -88,10 +88,10 @@ importers:
version: 1.9.2
next:
specifier: 14.2.25
- version: 14.2.25(@babel/core@7.25.7)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.86.0)
+ version: 14.2.25(@babel/core@7.25.7)(@playwright/test@1.51.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.86.0)
next-seo:
specifier: 6.6.0
- version: 6.6.0(next@14.2.25(@babel/core@7.25.7)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.86.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ version: 6.6.0(next@14.2.25(@babel/core@7.25.7)(@playwright/test@1.51.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.86.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
pino-pretty:
specifier: 13.0.0
version: 13.0.0
@@ -124,14 +124,14 @@ importers:
version: 4.5.4(@types/react@18.3.11)(react@18.3.1)
devDependencies:
'@playwright/test':
- specifier: 1.45.3
- version: 1.45.3
+ specifier: 1.51.1
+ version: 1.51.1
'@svgr/webpack':
specifier: 8.1.0
version: 8.1.0(typescript@5.4.5)
'@synthetixio/synpress':
- specifier: 4.0.0-alpha.7
- version: 4.0.0-alpha.7(@playwright/test@1.45.3)(bufferutil@4.0.8)(playwright-core@1.45.3)(postcss@8.5.3)(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.4.5))(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.24.2)
+ specifier: 4.0.10
+ version: 4.0.10(@depay/solana-web3.js@1.27.0)(@depay/web3-blockchains@9.6.7)(@playwright/test@1.51.1)(bufferutil@4.0.8)(ethers@6.13.4(bufferutil@4.0.8)(utf-8-validate@5.0.10))(playwright-core@1.51.1)(postcss@8.5.3)(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.4.5))(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.24.2)
'@types/fs-extra':
specifier: 11.0.4
version: 11.0.4
@@ -150,6 +150,9 @@ importers:
eslint-config-next:
specifier: 14.2.15
version: 14.2.15(eslint@8.57.0)(typescript@5.4.5)
+ nock:
+ specifier: 14.0.1
+ version: 14.0.1
postcss:
specifier: 8.5.3
version: 8.5.3
@@ -248,8 +251,6 @@ importers:
specifier: 17.7.2
version: 17.7.2
- contracts/lib/forge-std: {}
-
e2e:
devDependencies:
'@jest/globals':
@@ -1290,8 +1291,20 @@ packages:
resolution: {integrity: sha512-RqVsm/aURJ2npRm0/0qr/GiMdBsGSbRA2GpzG75Vy7qODYScbYjA/CitMjhw9ktzGjiFN9oh/ooq9GBjPIhEdA==}
engines: {node: '>=10'}
- '@depay/web3-mock@14.17.0':
- resolution: {integrity: sha512-0WCIpHqGUTPmOb5l3iN+4wCY+P3nHnGWd3uyWB+Wrt5DygS6MWI2b50gwtSCgYUCfgmEv9KlRuCnHDC4TDKCeA==}
+ '@depay/web3-client@10.18.6':
+ resolution: {integrity: sha512-JeUAZ04/dsIra1ao3mvqAdVhRf4U1YxSoH0mE+XxhcXPgLC7KQTzH6oCLp07tAmoxholEL2cf5Oo20n6q1fZ/w==}
+ engines: {node: '>=16'}
+ peerDependencies:
+ '@depay/solana-web3.js': ^1.25.1
+ '@depay/web3-blockchains': ^9.3.6
+ ethers: ^5.7.1
+
+ '@depay/web3-mock-evm@14.19.1':
+ resolution: {integrity: sha512-Gx5n87gwya5dGv4JwDdlJFWshLbM9nDj6co8Z25FTf7/xKsTUD1en971B2QweXqZJxYadnumOPb+n19lgRofpQ==}
+ engines: {node: '>=16'}
+
+ '@depay/web3-mock@14.19.1':
+ resolution: {integrity: sha512-bBM1J0EWDWXJKVPtzo8YrX7fbGwUATYWN8kaJniQU2z5V+UK3kVhjQi+en0JMF9cCjinkERK7MqoZLaYR+cb+Q==}
engines: {node: '>=16'}
'@dynamic-labs/assert-package-version@4.9.5':
@@ -2283,6 +2296,10 @@ packages:
resolution: {integrity: sha512-z10PF9JV6SbjFq+/rYabM+8CVlMokgl8RFGvieSGNTmrkQanfHn+15XBrhG3BgUfvmTeSeyShfOHpG0i9zEdcg==}
deprecated: Motion One for Vue is deprecated. Use Oku Motion instead https://oku-ui.com/motion
+ '@mswjs/interceptors@0.37.6':
+ resolution: {integrity: sha512-wK+5pLK5XFmgtH3aQ2YVvA3HohS3xqV/OxuVOdNx9Wpnz7VE/fnC+e1A7ln6LFYeck7gOJ/dsZV6OLplOtAJ2w==}
+ engines: {node: '>=18'}
+
'@mui/core-downloads-tracker@6.4.8':
resolution: {integrity: sha512-vjP4+A1ybyCRhDZC7r5EPWu/gLseFZxaGyPdDl94vzVvk6Yj6gahdaqcjbhkaCrJjdZj90m3VioltWPAnWF/zw==}
@@ -2669,6 +2686,15 @@ packages:
peerDependencies:
'@oclif/core': '>= 3.0.0'
+ '@open-draft/deferred-promise@2.2.0':
+ resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==}
+
+ '@open-draft/logger@0.3.0':
+ resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==}
+
+ '@open-draft/until@2.1.0':
+ resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
+
'@openzeppelin/contracts-upgradeable@4.9.6':
resolution: {integrity: sha512-m4iHazOsOCv1DgM7eD7GupTJ+NFVujRZt1wzddDPSVGpWdKq1SKkla5htKG7+IS4d2XOCtzkUNwRZ7Vq5aEUMA==}
@@ -2799,8 +2825,8 @@ packages:
resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
- '@playwright/test@1.45.3':
- resolution: {integrity: sha512-UKF4XsBfy+u3MFWEH44hva1Q8Da28G6RFtR2+5saw+jgAFQV5yYnB1fu68Mz7fO+5GJF3wgwAIs0UelU8TxFrA==}
+ '@playwright/test@1.51.1':
+ resolution: {integrity: sha512-nM+kEaTSAoVlXmMPH10017vn3FSiFqr/bh4fKg9vmAdMfd9SDqRZNvPSiAHADc/itWak+qPvMPZQOPwCBW7k7Q==}
engines: {node: '>=18'}
hasBin: true
@@ -3340,32 +3366,32 @@ packages:
'@swc/helpers@0.5.5':
resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==}
- '@synthetixio/ethereum-wallet-mock@0.0.1-alpha.7':
- resolution: {integrity: sha512-afyAVC2b39OC/mxfVeISFQ7TVleS15GYeCPte6VrdFYWi/2SXPml6myAZx9FlRqRlyaPLhRrtRRjdno44S6/6Q==}
+ '@synthetixio/ethereum-wallet-mock@0.0.11':
+ resolution: {integrity: sha512-J/8hyHoh5lG5vIQ9iNVApyKKRCMRMqEmuv97s6zZVmqe/gJR0+cSWW+mnYHXcIiWZ82WLYnr4TK4OssqZ/4C3w==}
peerDependencies:
- '@playwright/test': 1.44.0
+ '@playwright/test': '*'
- '@synthetixio/synpress-cache@0.0.1-alpha.7':
- resolution: {integrity: sha512-vIR8f4XMJSKsSezmloT8A0yuEZON+Ovbkoy/5ELcrfLFORW1Ssu2pjOioWGgQEVZBCdGTQ4wVIWmo8MbvCrAqA==}
+ '@synthetixio/synpress-cache@0.0.11':
+ resolution: {integrity: sha512-5blQD9RQ/LF6buuNAhok/ouHm0Ch4sNas40J7rWVXrim6UA5sCAHPFfSVdavKpBURGN8t6Zce2+uiZs0srbtwA==}
hasBin: true
peerDependencies:
- playwright-core: 1.44.0
+ playwright-core: 1.48.2
- '@synthetixio/synpress-core@0.0.1-alpha.7':
- resolution: {integrity: sha512-El23vK/FlYiliT2JworRc8zXGVdUIvzxwzWtGMZwC/KOKy0RGgNA7rQfwc3VSPhp1ceUd4w3uIRtv6Ccjo8WbA==}
+ '@synthetixio/synpress-core@0.0.11':
+ resolution: {integrity: sha512-IZtA0KfG1qO0JJVzswTLT6VBmteut7NvqbUcVaEOL12FBPIaZCglyd7gdyTkUCWusp7sHuSA/Bysre0OaBnuQg==}
peerDependencies:
- '@playwright/test': 1.44.0
+ '@playwright/test': '*'
- '@synthetixio/synpress-metamask@0.0.1-alpha.7':
- resolution: {integrity: sha512-xzBsF5wnak25uw7ZO9RJEmCuMtT03W9pyDaEpO7T1VkDcZT8MBxKHs9vR4UKqHHd9IPKXc9EvsUJ/oSwW4saKQ==}
+ '@synthetixio/synpress-metamask@0.0.11':
+ resolution: {integrity: sha512-nYNtFYolyeus0WqTqEot6xikyHzSXb/bSK9NP4aQ5vhKFkT6KR+8EjC9G4QKZ/+lM0alBJfvu2BOLDnYtKEOAw==}
peerDependencies:
- '@playwright/test': 1.44.0
+ '@playwright/test': '*'
- '@synthetixio/synpress@4.0.0-alpha.7':
- resolution: {integrity: sha512-hdVV+LR/aCO+o7FyeJiFxJ+o797mDqNqkh+huz3Ic4LDbTW18HkLKwLwUzRKt8ngIOf60K/PFL9QyBLujTc76g==}
+ '@synthetixio/synpress@4.0.10':
+ resolution: {integrity: sha512-UXHGb9lylznEdXtDw5TYFR095XsMh5I2iqAHRUfaYgYRJlm+MQAlnFINqREt7g52VqFJ1EzyCeIgrs6Aufk+/g==}
hasBin: true
peerDependencies:
- '@playwright/test': 1.44.0
+ '@playwright/test': '*'
'@szmarczak/http-timer@4.0.6':
resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==}
@@ -6568,6 +6594,9 @@ packages:
resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==}
engines: {node: '>= 0.4'}
+ is-node-process@1.2.0:
+ resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==}
+
is-number-object@1.0.7:
resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==}
engines: {node: '>= 0.4'}
@@ -7576,6 +7605,10 @@ packages:
resolution: {integrity: sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw==}
engines: {node: '>=12.0.0'}
+ nock@14.0.1:
+ resolution: {integrity: sha512-IJN4O9pturuRdn60NjQ7YkFt6Rwei7ZKaOwb1tvUIIqTgeD0SDDAX3vrqZD4wcXczeEy/AsUXxpGpP/yHqV7xg==}
+ engines: {node: '>=18.20.0 <20 || >=20.12.1'}
+
node-abi@3.68.0:
resolution: {integrity: sha512-7vbj10trelExNjFSBm5kTvZXXa7pZyKWx9RCKIyqe6I9Ev3IzGpQoqBP3a+cOdxY+pWj6VkP28n/2wWysBHD/A==}
engines: {node: '>=10'}
@@ -7884,6 +7917,9 @@ packages:
resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
engines: {node: '>=0.10.0'}
+ outvariant@1.4.3:
+ resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==}
+
ox@0.6.7:
resolution: {integrity: sha512-17Gk/eFsFRAZ80p5eKqv89a57uXjd3NgIf1CaXojATPBuujVc/fQSVhBeAU9JCRB+k7J50WQAyWTxK19T9GgbA==}
peerDependencies:
@@ -8113,13 +8149,13 @@ packages:
pkg-types@1.2.1:
resolution: {integrity: sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==}
- playwright-core@1.45.3:
- resolution: {integrity: sha512-+ym0jNbcjikaOwwSZycFbwkWgfruWvYlJfThKYAlImbxUgdWFO2oW70ojPm4OpE4t6TAo2FY/smM+hpVTtkhDA==}
+ playwright-core@1.51.1:
+ resolution: {integrity: sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==}
engines: {node: '>=18'}
hasBin: true
- playwright@1.45.3:
- resolution: {integrity: sha512-QhVaS+lpluxCaioejDZ95l4Y4jSFCsBvl2UZkpeXlzxmqS+aABr5c82YmfMHrL6x27nvrvykJAFpkzT2eWdJww==}
+ playwright@1.51.1:
+ resolution: {integrity: sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==}
engines: {node: '>=18'}
hasBin: true
@@ -8258,6 +8294,10 @@ packages:
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
+ propagate@2.0.1:
+ resolution: {integrity: sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==}
+ engines: {node: '>= 8'}
+
proper-lockfile@4.1.2:
resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==}
@@ -9043,6 +9083,9 @@ packages:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'}
+ strict-event-emitter@0.5.1:
+ resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==}
+
strict-uri-encode@1.1.0:
resolution: {integrity: sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==}
engines: {node: '>=0.10.0'}
@@ -11497,7 +11540,21 @@ snapshots:
'@depay/web3-blockchains@9.6.7': {}
- '@depay/web3-mock@14.17.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)':
+ '@depay/web3-client@10.18.6(@depay/solana-web3.js@1.27.0)(@depay/web3-blockchains@9.6.7)(ethers@6.13.4(bufferutil@4.0.8)(utf-8-validate@5.0.10))':
+ dependencies:
+ '@depay/solana-web3.js': 1.27.0
+ '@depay/web3-blockchains': 9.6.7
+ ethers: 6.13.4(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+
+ '@depay/web3-mock-evm@14.19.1(bufferutil@4.0.8)(utf-8-validate@5.0.10)':
+ dependencies:
+ '@depay/web3-blockchains': 9.6.7
+ ethers: 5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+ transitivePeerDependencies:
+ - bufferutil
+ - utf-8-validate
+
+ '@depay/web3-mock@14.19.1(bufferutil@4.0.8)(utf-8-validate@5.0.10)':
dependencies:
'@depay/solana-web3.js': 1.27.0
'@depay/web3-blockchains': 9.6.7
@@ -13027,6 +13084,15 @@ snapshots:
'@motionone/dom': 10.18.0
tslib: 2.7.0
+ '@mswjs/interceptors@0.37.6':
+ dependencies:
+ '@open-draft/deferred-promise': 2.2.0
+ '@open-draft/logger': 0.3.0
+ '@open-draft/until': 2.1.0
+ is-node-process: 1.2.0
+ outvariant: 1.4.3
+ strict-event-emitter: 0.5.1
+
'@mui/core-downloads-tracker@6.4.8': {}
'@mui/icons-material@6.0.2(@mui/material@6.4.8(@emotion/react@11.14.0(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.11)(react@18.3.1)':
@@ -13408,6 +13474,15 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@open-draft/deferred-promise@2.2.0': {}
+
+ '@open-draft/logger@0.3.0':
+ dependencies:
+ is-node-process: 1.2.0
+ outvariant: 1.4.3
+
+ '@open-draft/until@2.1.0': {}
+
'@openzeppelin/contracts-upgradeable@4.9.6': {}
'@openzeppelin/contracts@4.9.6': {}
@@ -13558,9 +13633,9 @@ snapshots:
'@pkgr/core@0.1.1': {}
- '@playwright/test@1.45.3':
+ '@playwright/test@1.51.1':
dependencies:
- playwright: 1.45.3
+ playwright: 1.51.1
'@pnpm/config.env-replace@1.1.0': {}
@@ -14448,19 +14523,24 @@ snapshots:
'@swc/counter': 0.1.3
tslib: 2.7.0
- '@synthetixio/ethereum-wallet-mock@0.0.1-alpha.7(@playwright/test@1.45.3)(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.24.2)':
+ '@synthetixio/ethereum-wallet-mock@0.0.11(@depay/solana-web3.js@1.27.0)(@depay/web3-blockchains@9.6.7)(@playwright/test@1.51.1)(bufferutil@4.0.8)(ethers@6.13.4(bufferutil@4.0.8)(utf-8-validate@5.0.10))(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.24.2)':
dependencies:
- '@depay/web3-mock': 14.17.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)
- '@playwright/test': 1.45.3
- '@synthetixio/synpress-core': 0.0.1-alpha.7(@playwright/test@1.45.3)
+ '@depay/web3-client': 10.18.6(@depay/solana-web3.js@1.27.0)(@depay/web3-blockchains@9.6.7)(ethers@6.13.4(bufferutil@4.0.8)(utf-8-validate@5.0.10))
+ '@depay/web3-mock': 14.19.1(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+ '@depay/web3-mock-evm': 14.19.1(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+ '@playwright/test': 1.51.1
+ '@synthetixio/synpress-core': 0.0.11(@playwright/test@1.51.1)
viem: 2.9.9(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.24.2)
transitivePeerDependencies:
+ - '@depay/solana-web3.js'
+ - '@depay/web3-blockchains'
- bufferutil
+ - ethers
- typescript
- utf-8-validate
- zod
- '@synthetixio/synpress-cache@0.0.1-alpha.7(playwright-core@1.45.3)(postcss@8.5.3)(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.4.5))(typescript@5.4.5)':
+ '@synthetixio/synpress-cache@0.0.11(playwright-core@1.51.1)(postcss@8.5.3)(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.4.5))(typescript@5.4.5)':
dependencies:
axios: 1.6.7
chalk: 5.3.0
@@ -14469,7 +14549,7 @@ snapshots:
fs-extra: 11.2.0
glob: 10.3.10
gradient-string: 2.0.2
- playwright-core: 1.45.3
+ playwright-core: 1.51.1
progress: 2.0.3
tsup: 8.0.2(postcss@8.5.3)(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.4.5))(typescript@5.4.5)
unzipper: 0.10.14
@@ -14483,15 +14563,15 @@ snapshots:
- ts-node
- typescript
- '@synthetixio/synpress-core@0.0.1-alpha.7(@playwright/test@1.45.3)':
+ '@synthetixio/synpress-core@0.0.11(@playwright/test@1.51.1)':
dependencies:
- '@playwright/test': 1.45.3
+ '@playwright/test': 1.51.1
- '@synthetixio/synpress-metamask@0.0.1-alpha.7(@playwright/test@1.45.3)(bufferutil@4.0.8)(playwright-core@1.45.3)(postcss@8.5.3)(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.4.5))(typescript@5.4.5)(utf-8-validate@5.0.10)':
+ '@synthetixio/synpress-metamask@0.0.11(@playwright/test@1.51.1)(bufferutil@4.0.8)(playwright-core@1.51.1)(postcss@8.5.3)(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.4.5))(typescript@5.4.5)(utf-8-validate@5.0.10)':
dependencies:
- '@playwright/test': 1.45.3
- '@synthetixio/synpress-cache': 0.0.1-alpha.7(playwright-core@1.45.3)(postcss@8.5.3)(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.4.5))(typescript@5.4.5)
- '@synthetixio/synpress-core': 0.0.1-alpha.7(@playwright/test@1.45.3)
+ '@playwright/test': 1.51.1
+ '@synthetixio/synpress-cache': 0.0.11(playwright-core@1.51.1)(postcss@8.5.3)(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.4.5))(typescript@5.4.5)
+ '@synthetixio/synpress-core': 0.0.11(@playwright/test@1.51.1)
'@viem/anvil': 0.0.7(bufferutil@4.0.8)(utf-8-validate@5.0.10)
fs-extra: 11.2.0
zod: 3.22.4
@@ -14507,18 +14587,21 @@ snapshots:
- typescript
- utf-8-validate
- '@synthetixio/synpress@4.0.0-alpha.7(@playwright/test@1.45.3)(bufferutil@4.0.8)(playwright-core@1.45.3)(postcss@8.5.3)(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.4.5))(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.24.2)':
+ '@synthetixio/synpress@4.0.10(@depay/solana-web3.js@1.27.0)(@depay/web3-blockchains@9.6.7)(@playwright/test@1.51.1)(bufferutil@4.0.8)(ethers@6.13.4(bufferutil@4.0.8)(utf-8-validate@5.0.10))(playwright-core@1.51.1)(postcss@8.5.3)(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.4.5))(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.24.2)':
dependencies:
- '@playwright/test': 1.45.3
- '@synthetixio/ethereum-wallet-mock': 0.0.1-alpha.7(@playwright/test@1.45.3)(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.24.2)
- '@synthetixio/synpress-cache': 0.0.1-alpha.7(playwright-core@1.45.3)(postcss@8.5.3)(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.4.5))(typescript@5.4.5)
- '@synthetixio/synpress-core': 0.0.1-alpha.7(@playwright/test@1.45.3)
- '@synthetixio/synpress-metamask': 0.0.1-alpha.7(@playwright/test@1.45.3)(bufferutil@4.0.8)(playwright-core@1.45.3)(postcss@8.5.3)(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.4.5))(typescript@5.4.5)(utf-8-validate@5.0.10)
+ '@playwright/test': 1.51.1
+ '@synthetixio/ethereum-wallet-mock': 0.0.11(@depay/solana-web3.js@1.27.0)(@depay/web3-blockchains@9.6.7)(@playwright/test@1.51.1)(bufferutil@4.0.8)(ethers@6.13.4(bufferutil@4.0.8)(utf-8-validate@5.0.10))(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.24.2)
+ '@synthetixio/synpress-cache': 0.0.11(playwright-core@1.51.1)(postcss@8.5.3)(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.4.5))(typescript@5.4.5)
+ '@synthetixio/synpress-core': 0.0.11(@playwright/test@1.51.1)
+ '@synthetixio/synpress-metamask': 0.0.11(@playwright/test@1.51.1)(bufferutil@4.0.8)(playwright-core@1.51.1)(postcss@8.5.3)(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.4.5))(typescript@5.4.5)(utf-8-validate@5.0.10)
transitivePeerDependencies:
+ - '@depay/solana-web3.js'
+ - '@depay/web3-blockchains'
- '@microsoft/api-extractor'
- '@swc/core'
- bufferutil
- debug
+ - ethers
- playwright-core
- postcss
- supports-color
@@ -18810,6 +18893,8 @@ snapshots:
is-negative-zero@2.0.3: {}
+ is-node-process@1.2.0: {}
+
is-number-object@1.0.7:
dependencies:
has-tostringtag: 1.0.2
@@ -20193,15 +20278,15 @@ snapshots:
neo-async@2.6.2: {}
- next-seo@6.6.0(next@14.2.25(@babel/core@7.25.7)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.86.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ next-seo@6.6.0(next@14.2.25(@babel/core@7.25.7)(@playwright/test@1.51.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.86.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
- next: 14.2.25(@babel/core@7.25.7)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.86.0)
+ next: 14.2.25(@babel/core@7.25.7)(@playwright/test@1.51.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.86.0)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
next-tick@1.1.0: {}
- next@14.2.25(@babel/core@7.25.7)(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.86.0):
+ next@14.2.25(@babel/core@7.25.7)(@playwright/test@1.51.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.86.0):
dependencies:
'@next/env': 14.2.25
'@swc/helpers': 0.5.5
@@ -20222,7 +20307,7 @@ snapshots:
'@next/swc-win32-arm64-msvc': 14.2.25
'@next/swc-win32-ia32-msvc': 14.2.25
'@next/swc-win32-x64-msvc': 14.2.25
- '@playwright/test': 1.45.3
+ '@playwright/test': 1.51.1
sass: 1.86.0
transitivePeerDependencies:
- '@babel/core'
@@ -20235,6 +20320,12 @@ snapshots:
nocache@3.0.4: {}
+ nock@14.0.1:
+ dependencies:
+ '@mswjs/interceptors': 0.37.6
+ json-stringify-safe: 5.0.1
+ propagate: 2.0.1
+
node-abi@3.68.0:
dependencies:
semver: 7.6.3
@@ -20480,6 +20571,8 @@ snapshots:
os-tmpdir@1.0.2: {}
+ outvariant@1.4.3: {}
+
ox@0.6.7(typescript@5.4.5)(zod@3.24.2):
dependencies:
'@adraffy/ens-normalize': 1.11.0
@@ -20718,11 +20811,11 @@ snapshots:
mlly: 1.7.2
pathe: 1.1.2
- playwright-core@1.45.3: {}
+ playwright-core@1.51.1: {}
- playwright@1.45.3:
+ playwright@1.51.1:
dependencies:
- playwright-core: 1.45.3
+ playwright-core: 1.51.1
optionalDependencies:
fsevents: 2.3.2
@@ -20847,6 +20940,8 @@ snapshots:
object-assign: 4.1.1
react-is: 16.13.1
+ propagate@2.0.1: {}
+
proper-lockfile@4.1.2:
dependencies:
graceful-fs: 4.2.11
@@ -21834,6 +21929,8 @@ snapshots:
streamsearch@1.1.0: {}
+ strict-event-emitter@0.5.1: {}
+
strict-uri-encode@1.1.0: {}
strict-uri-encode@2.0.0: {}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 9d21baaa..a8b4de63 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -6,3 +6,4 @@ packages:
- 'operations/**'
- 'bridge-ui/**'
- 'ts-libs/**'
+ - '!contracts/lib/**'
\ No newline at end of file