[Feat] Bridge UI Test Workflow (#814)

* add first test

* create connectMetamaskToDapp fixture

* bump testing framework version

* correctly working connectMetamaskToDapp function

* working metamask e2e test

* ci fix

* fix

* some changes

* working transaction history list test

* try version bump

* try version bump

* work on toggleShowTestNetworksInNativeBridgeForm

* try playwright 1.51.1

* ci file change

* test fixture changes

* try

* try

* try

* try

* test

* Revert "test"

This reverts commit 2592b6835530feb912d537a29eefaba37f74560e.

* try fix

* try fix

* test

* test

* Revert "test"

This reverts commit a21634ca9224b3d025290e215ba61917bec043ff.

* working toggle network e2e test

* working confirm and bridge flow on e2e test

* refactor metamask fixtures

* successful e2e case for bridge eth

* did usdc bridge e2e test

* fix pnpm lock

* refactor

* refactor

* lint and try single text

* try

* test

* test ci change

* test

* test ci change

* run once to get cache

* first unit test for bridge-ui

* redo test

* more test logs

* test

* test

* test

* removed console logs

* address one more edge case

* activate all tests

* try

* more comments

* adjust fixture

* did some unit tests

* add more unit tests

* disable unit tests in ci

* new e2e test

* new comment

* new things

* fix

* comment out blockchain tx tests

* add reporter for playwright

* removed pause

* clean-up

* test

* change get token balance mechanic

* fix import

* fixes

* clean up

* test

* try headless

* redo headful
This commit is contained in:
kyzooghost
2025-03-28 22:35:59 +11:00
committed by GitHub
parent 60101122fb
commit 4f346b558d
25 changed files with 772 additions and 321 deletions

View File

@@ -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

View File

@@ -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"

View File

@@ -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"
}
}

View File

@@ -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",

View File

@@ -42,7 +42,7 @@ export default function ReceivedAmount() {
return (
<div className={styles.value}>
<p className={styles.crypto}>
<p className={styles.crypto} data-testid="received-amount-text">
{formatBalance(receivedAmount, 6)} {token.symbol}
</p>
{tokenPrices?.[token[fromChain.layer].toLowerCase()] &&

View File

@@ -58,10 +58,11 @@ export default function BridgeForm() {
setIsBridgeOpen(false);
setIsTransactionHistoryOpen(true);
}}
data-testid="native-bridge-transaction-history-icon"
>
<TransactionPaperIcon className={styles["transaction-icon"]} />
</Button>
<Setting />
<Setting data-testid="native-bridge-form-settings-icon" />
</div>
</div>
<div className={styles["content"]}>

View File

@@ -32,7 +32,7 @@ export default function ConfirmDestinationAddress({ isModalOpen, recipient, onCl
{formatAddress(recipient)}
<UnionIcon />
</Link>
<Button fullWidth onClick={onConfirm}>
<Button fullWidth onClick={onConfirm} data-testid="confirm-and-bridge-btn">
Confirm and bridge
</Button>
</div>

View File

@@ -46,6 +46,7 @@ export default function TokenDetails({ isConnected, token, onTokenClick, tokenPr
return (
<button
id={`token-details-${token.symbol}-btn`}
data-testid={`token-details-${token.symbol.toLowerCase()}-btn`}
className={styles["token-wrapper"]}
type="button"
disabled={tokenNotFromCurrentLayer}
@@ -60,7 +61,7 @@ export default function TokenDetails({ isConnected, token, onTokenClick, tokenPr
</div>
{isConnected && !tokenNotFromCurrentLayer && (
<div className={styles.rìght}>
<p className={styles["balance"]}>
<p className={styles["balance"]} data-testid={`token-details-${token.symbol.toLowerCase()}-amount`}>
{formatBalance(formattedBalance, 8)} {token.symbol}
</p>
{totalValue !== undefined && (

View File

@@ -16,7 +16,11 @@ export default function TokenList() {
return (
<div className={styles["wrapper"]}>
{token && (
<Button className={styles["token-select-btn"]} onClick={openModal}>
<Button
className={styles["token-select-btn"]}
onClick={openModal}
data-testid="native-bridge-open-token-list-modal"
>
<Image src={token.image} alt={token.name} width={24} height={24} />
{token.symbol}
<CaretDownIcon className={styles["arrow-down-icon"]} />

View File

@@ -29,6 +29,7 @@ export default function TransactionHistory() {
setIsTransactionHistoryOpen(false);
setIsBridgeOpen(true);
}}
data-testid="transaction-history-close-btn"
>
<ArrowLeftIcon className={styles["go-back-icon"]} />
</Button>
@@ -51,6 +52,7 @@ export default function TransactionHistory() {
setIsTransactionHistoryOpen(false);
setIsBridgeOpen(true);
}}
data-testid="transaction-history-close-btn"
>
<ArrowLeftIcon className={styles["go-back-icon"]} />
</Button>

View File

@@ -21,7 +21,7 @@ export default function ListTransaction({ transactions }: Props) {
};
return (
<>
<ul className={styles["list"]}>
<ul className={styles["list"]} data-testid="native-bridge-transaction-history-list">
{transactions.map((item, index) => (
<Transaction key={`transaction-${item.bridgingTx}-${index}`} onClick={handleClickTransaction} {...item} />
))}

View File

@@ -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<HTMLDivElement> {
"data-testid": string;
}
export default function Setting(props: SettingProps) {
const dropdownRef = useRef<HTMLDivElement | null>(null);
const [isDropdownVisible, setDropdownVisible] = useState<boolean>(false);
const setShowTestnet = useConfigStore.useSetShowTestnet();
@@ -56,6 +60,7 @@ export default function Setting() {
[styles["visible"]]: isDropdownVisible,
})}
onClick={toggleDropdown}
data-testid={props["data-testid"]}
>
<SettingIcon />
</div>
@@ -76,6 +81,7 @@ export default function Setting() {
onChange={(checked) => {
setShowTestnet(checked);
}}
data-testid="native-bridge-test-network-toggle"
/>
</li>
</ul>

View File

@@ -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<boolean>(checked);
useEffect(() => {
@@ -32,7 +32,7 @@ export default function ToggleSwitch({ checked = false, onChange, disabled }: Pr
})}
>
<input type="checkbox" checked={isChecked} onChange={handleChange} disabled={disabled} />
<span className={styles["slider"]} />
<span className={styles["slider"]} {...rest} />
</label>
);
}

View File

@@ -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;

View File

@@ -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);
});
});

View File

@@ -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()

View File

@@ -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<void>;
waitForTransactionToConfirm: () => Promise<void>;
getBridgeTransactionsCount: () => Promise<number>;
sendTokens: (amount: string, isETH?: boolean) => Promise<void>;
waitForTransactionListUpdate: (txCountBeforeUpdate: number) => Promise<boolean>;
selectToken: (tokenName: string) => Promise<void>;
}>({
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<Locator>;
openNativeBridgeTransactionHistory: () => Promise<void>;
closeNativeBridgeTransactionHistory: () => Promise<void>;
openNativeBridgeFormSettings: () => Promise<void>;
toggleShowTestNetworksInNativeBridgeForm: () => Promise<void>;
getNativeBridgeTransactionsCount: () => Promise<number>;
selectTokenAndInputAmount: (tokenSymbol: string, amount: string) => Promise<void>;
waitForNewTxAdditionToTxList: (txCountBeforeUpdate: number) => Promise<void>;
waitForTxListUpdateForClaimTx: (claimTxCountBeforeUpdate: number) => Promise<void>;
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<void>;
waitForTransactionToConfirm: () => Promise<void>;
confirmTransactionAndWaitForInclusion: () => Promise<void>;
switchToLineaSepolia: () => Promise<void>;
// Composite Bridge UI + Metamask Actions
doTokenApprovalIfNeeded: () => Promise<void>;
doInitiateBridgeTransaction: () => Promise<void>;
doClaimTransaction: () => Promise<void>;
}>({
// 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;
});
},
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 metamask.confirmTransaction();
await waitForTransactionToConfirm();
await page.bringToFront();
}
const submitBtn = await page.waitForSelector(`#submit-${tokenType}-btn`);
await submitBtn.click();
});
},
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);
return listUpdated;
switchToLineaSepolia: async ({ metamask }, use) => {
await use(async () => {
await metamask.switchNetwork(LINEA_SEPOLIA_NETWORK.name, true);
});
},
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();
// 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();
// 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();
});
},
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
});
},
});

View File

@@ -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;

View File

@@ -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);
});
});

View File

@@ -0,0 +1,15 @@
import { Page } from "@playwright/test";
export async function getNativeBridgeTransactionsCountImpl(page: Page): Promise<number> {
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;
}

View File

@@ -0,0 +1,2 @@
export { selectTokenAndWaitForBalance } from "./selectTokenAndWaitForBalance";
export { getNativeBridgeTransactionsCountImpl } from "./getNativeBridgeTransactionsCountImpl";

View File

@@ -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();
}

View File

@@ -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);

215
pnpm-lock.yaml generated
View File

@@ -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: {}

View File

@@ -6,3 +6,4 @@ packages:
- 'operations/**'
- 'bridge-ui/**'
- 'ts-libs/**'
- '!contracts/lib/**'