diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index b06c9ab..da44278 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -5,10 +5,26 @@ on: - develop - main pull_request: +env: + PNPM_VERSION: 9.1.4 + NODE_VERSION: 20.x jobs: build: runs-on: ubuntu-latest + container: + image: mcr.microsoft.com/playwright:v1.48.2-jammy + env: + ARGENT_X_ENVIRONMENT: "hydrogen" + E2E_REPO: ${{ secrets.E2E_REPO }} + E2E_REPO_TOKEN: ${{ secrets.E2E_REPO_TOKEN }} + E2E_REPO_OWNER: ${{ secrets.E2E_REPO_OWNER }} + E2E_REPO_RELEASE_NAME: ${{ secrets.E2E_REPO_RELEASE_NAME }} + + WW_EMAIL: ${{ secrets.WW_EMAIL }} + WW_LOGIN_PASSWORD: ${{ secrets.WW_LOGIN_PASSWORD }} + EMAIL_PASSWORD: ${{ secrets.EMAIL_PASSWORD }} + steps: - name: Checkout code uses: actions/checkout@v4 @@ -18,8 +34,13 @@ jobs: - uses: pnpm/action-setup@v4 name: Install pnpm with: - # version: 9 run_install: false + version: ${{ env.PNPM_VERSION }} + + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: "pnpm" - name: Get pnpm store directory shell: bash @@ -36,5 +57,176 @@ jobs: - name: Install dependencies run: pnpm install + - name: Build demo-dapp-starknet run: pnpm run build + + - name: Use Cache + uses: actions/cache@v4 + with: + path: ./* + key: ${{ github.sha }} + + + test-webwallet: + runs-on: ubuntu-latest + container: + image: mcr.microsoft.com/playwright:v1.48.2-jammy + needs: [build] + env: + ARGENT_X_ENVIRONMENT: "hydrogen" + + WW_EMAIL: ${{ secrets.WW_EMAIL }} + WW_LOGIN_PASSWORD: ${{ secrets.WW_LOGIN_PASSWORD }} + EMAIL_PASSWORD: ${{ secrets.EMAIL_PASSWORD }} + + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + name: Install pnpm + id: pnpm-install + with: + version: ${{ env.PNPM_VERSION }} + run_install: false + + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: "pnpm" + + - name: Restore pnpm cache + uses: actions/cache/restore@v4 + with: + path: ~/.pnpm-store + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Restore cached build + uses: actions/cache/restore@v4 + with: + path: ./* + key: ${{ github.sha }} + + - name: Run e2e tests + run: | + pnpm run start & # Start the server in background + echo "Waiting for server to be ready..." + for i in {1..30}; do + if curl -s http://localhost:3000 > /dev/null; then + echo "Server is ready!" + break + fi + echo "Attempt $i: Server not ready yet..." + if [ $i -eq 30 ]; then + echo "Server failed to start" + exit 1 + fi + sleep 1 + done + xvfb-run --auto-servernum pnpm test:webwallet + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-artifacts + path: | + e2e/artifacts/playwright/ + !e2e/artifacts/playwright/*.webm + retention-days: 5 + + test-argentX: + runs-on: ubuntu-latest + container: + image: mcr.microsoft.com/playwright:v1.48.2-jammy + needs: [build] + env: + ARGENT_X_ENVIRONMENT: "hydrogen" + E2E_REPO: ${{ secrets.E2E_REPO }} + E2E_REPO_TOKEN: ${{ secrets.E2E_REPO_TOKEN }} + E2E_REPO_OWNER: ${{ secrets.E2E_REPO_OWNER }} + E2E_REPO_RELEASE_NAME: ${{ secrets.E2E_REPO_RELEASE_NAME }} + + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + name: Install pnpm + id: pnpm-install + with: + version: ${{ env.PNPM_VERSION }} + run_install: false + + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: "pnpm" + + - name: Restore pnpm cache + uses: actions/cache/restore@v4 + with: + path: ~/.pnpm-store + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Restore cached build + uses: actions/cache/restore@v4 + with: + path: ./* + key: ${{ github.sha }} + + - name: Install libarchive-tools + shell: bash + run: | + try_apt() { + rm -f /var/lib/apt/lists/lock + rm -f /var/cache/apt/archives/lock + rm -f /var/lib/dpkg/lock* + dpkg --configure -a + apt-get update && apt-get install -y libarchive-tools + } + + for i in {1..3}; do + echo "Attempt $i to install libarchive-tools" + if try_apt; then + echo "Successfully installed libarchive-tools" + exit 0 + fi + echo "Attempt $i failed, waiting 10 seconds..." + sleep 10 + done + + echo "Failed to install libarchive-tools after 3 attempts" + exit 1 + + - name: Run e2e tests + run: | + pnpm run start & # Start the server in background + echo "Waiting for server to be ready..." + for i in {1..30}; do + if curl -s http://localhost:3000 > /dev/null; then + echo "Server is ready!" + break + fi + echo "Attempt $i: Server not ready yet..." + if [ $i -eq 30 ]; then + echo "Server failed to start" + exit 1 + fi + sleep 1 + done + xvfb-run --auto-servernum pnpm test:argentx + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-artifacts + path: | + e2e/artifacts/playwright/ + !e2e/artifacts/playwright/*.webm + retention-days: 5 + \ No newline at end of file diff --git a/.gitignore b/.gitignore index b3b4187..dbeefdc 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,8 @@ next-env.d.ts .playwright-* playwright-report* -.eslintcache \ No newline at end of file +.eslintcache +artifacts +argent-x-dist + +/e2e/node_modules/ \ No newline at end of file diff --git a/e2e/config.ts b/e2e/config.ts new file mode 100644 index 0000000..07d4ba4 --- /dev/null +++ b/e2e/config.ts @@ -0,0 +1,86 @@ +import path from "path" +import dotenv from "dotenv" +import fs from "fs" + +const envPath = path.resolve(__dirname || "", ".env") +if (fs.existsSync(envPath)) { + dotenv.config({ path: envPath }) +} +const commonConfig = { + isProdTesting: process.env.ARGENT_X_ENVIRONMENT === "prod" ? true : false || "", + password: "MyP@ss3!", + //accounts used for setup + senderAddrs: process.env.E2E_SENDER_ADDRESSES?.split(",") || "", + senderKeys: process.env.E2E_SENDER_PRIVATEKEYS?.split(",") || "", + destinationAddress: process.env.E2E_SENDER_ADDRESSES?.split(",")[0] ||"", //used as transfers destination + // urls + rpcUrl: process.env.ARGENT_SEPOLIA_RPC_URL || "", + beAPIUrl: + process.env.ARGENT_X_ENVIRONMENT === "prod" + ? "" + : process.env.ARGENT_API_BASE_URL || "", + viewportSize: { width: 360, height: 800 }, + artifactsDir: path.resolve(__dirname, "./artifacts/playwright"), + isCI: Boolean(process.env.CI), + migDir: path.join(__dirname, "../../e2e/argent-x-dist/"), + distDir: path.join(__dirname, "../../extension/dist/"), + migVersionDir: path.join(__dirname || "", "../../e2e/argent-x-dist/dist"), + migRepo: process.env.E2E_REPO || "", + migRepoToken: process.env.E2E_REPO_TOKEN || "", + migRepoOwner: process.env.E2E_REPO_OWNER || "", + migReleaseName: process.env.E2E_REPO_RELEASE_NAME || "", +} + +const extensionHydrogenConfig = { + ...commonConfig || "", + testSeed1: process.env.E2E_TESTNET_SEED1 || "", //wallet with 33 regular deployed accounts and 1 multisig deployed account + testSeed3: process.env.E2E_TESTNET_SEED3 || "", //wallet with 1 deployed account|| "", and multisig with removed user + testSeed4: process.env.E2E_TESTNET_SEED4 || "", //wallet with non deployed account but with funds + senderSeed: process.env.E2E_SENDER_SEED || "", + account1Seed2: process.env.E2E_ACCOUNT_1_SEED2 || "", + spokCampaignName: process.env.E2E_SPOK_CAMPAIGN_NAME || "", + spokCampaignUrl: process.env.E2E_SPOK_CAMPAIGN_URL || "", + guardianEmail: process.env.E2E_GUARDIAN_EMAIL || "", + useStrkAsFeeToken: process.env.E2E_USE_STRK_AS_FEE_TOKEN || "", + skipTXTests: process.env.E2E_SKIP_TX_TESTS || "", + accountsToImport: process.env.E2E_ACCOUNTS_TO_IMPORT || "", + accountToImportAndTx: process.env.E2E_ACCOUNT_TO_IMPORT_AND_TX?.split(",") || "", + qaUtilsURL: process.env.E2E_QA_UTILS_URL || "", + qaUtilsAuthToken: process.env.E2E_QA_UTILS_AUTH_TOKEN || "", + initialBalanceMultiplier: process.env.INITIAL_BALANCE_MULTIPLIER || 1 || "", + migAccountAddress: process.env.E2E_MIG_ACCOUNT_ADDRESS || "", +} + +const extensionProdConfig = { + ...commonConfig, + testSeed1: process.env.E2E_MAINNET_SEED1 || "", + testSeed3: "", + testSeed4: "", + senderSeed: process.env.E2E_SENDER_SEED || "", + account1Seed2:"", + account1Seed3:"", + spokCampaignName:"", + spokCampaignUrl:"", + guardianEmail:"", + useStrkAsFeeToken: "false", + skipTXTests: "true", + accountsToImport:"", + accountToImportAndTx:"", + qaUtilsURL:"", + qaUtilsAuthToken:"", + initialBalanceMultiplier: 1, + migAccountAddress:"", + migVersions:"", +} + +const config = commonConfig.isProdTesting + ? extensionProdConfig + : extensionHydrogenConfig +// check that no value of config is undefined|| "", otherwise throw error +Object.entries(config).forEach(([key, value]) => { + if (value === undefined) { + throw new Error(`Missing ${key} config variable; check .env file`) + } +}) + +export default config diff --git a/e2e/package.json b/e2e/package.json new file mode 100644 index 0000000..f478608 --- /dev/null +++ b/e2e/package.json @@ -0,0 +1,41 @@ +{ + "name": "@demo-dapp-starket/e2e", + "private": true, + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "peerDependencies": { + "@scure/base": "^1.1.1", + "@scure/bip39": "^1.2.1", + "axios": "^1.7.7", + "fs-extra": "^11.2.0", + "lodash-es": "^4.17.21", + "object-hash": "^3.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "swr": "^1.3.0", + "zod": "^3.23.8" + }, + "devDependencies": { + "@playwright/test": "^1.48.1", + "@types/axios": "^0.14.0", + "@types/fs-extra": "^11.0.4", + "@types/imap-simple": "^4.2.9", + "@types/mailparser": "^3.4.5", + "@types/node": "^22.0.0", + "@types/nodemailer": "^6.4.17", + "@types/uuid": "^10.0.0", + "dotenv": "^16.3.1", + "starknet": "6.11.0", + "uuid": "^11.0.0" + }, + "scripts": { + "test:argentx": "pnpm playwright test --project=ArgentX", + "test:webwallet": "pnpm playwright test --project=WebWallet" + }, + "dependencies": { + "imap-simple": "^5.1.0", + "mailparser": "^3.7.1", + "nodemailer": "^6.9.16" + } +} \ No newline at end of file diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts new file mode 100644 index 0000000..182287e --- /dev/null +++ b/e2e/playwright.config.ts @@ -0,0 +1,50 @@ +import type { PlaywrightTestConfig } from "@playwright/test" +import config from "./config" + +const playwrightConfig: PlaywrightTestConfig = { + projects: [ + { + name: "ArgentX", + use: { + trace: "retain-on-failure", + actionTimeout: 120 * 1000, // 2 minute + permissions: ["clipboard-read", "clipboard-write"], + screenshot: "only-on-failure", + }, + timeout: config.isCI ? 5 * 60e3 : 1 * 60e3, + expect: { timeout: 2 * 60e3 }, // 2 minute + testDir: "./src/argent-x/specs", + testMatch: /\.spec.ts$/, + retries: config.isCI ? 1 : 0, + outputDir: config.artifactsDir, + }, + { + name: "WebWallet", + use: { + trace: "retain-on-failure", + actionTimeout: 120 * 1000, // 2 minute + permissions: ["clipboard-read", "clipboard-write"], + screenshot: "only-on-failure", + }, + timeout: config.isCI ? 5 * 60e3 : 1 * 60e3, + expect: { timeout: 2 * 60e3 }, // 2 minute + testDir: "./src/webwallet/specs", + testMatch: /\.spec.ts$/, + retries: config.isCI ? 1 : 0, + outputDir: config.artifactsDir, + }, + ], + workers: config.isCI ? 2 : 1, + fullyParallel: true, + reportSlowTests: { + threshold: 2 * 60e3, // 2 minutes + max: 5, + }, + reporter: config.isCI ? [["github"], ["blob"], ["list"]] : "list", + forbidOnly: config.isCI, + outputDir: config.artifactsDir, + preserveOutput: "failures-only", + globalTeardown: "./src/shared/cfg/global.teardown.ts", +} + +export default playwrightConfig diff --git a/e2e/src/argent-x/fixtures.ts b/e2e/src/argent-x/fixtures.ts new file mode 100644 index 0000000..24e3870 --- /dev/null +++ b/e2e/src/argent-x/fixtures.ts @@ -0,0 +1,11 @@ +import { ChromiumBrowserContext } from "@playwright/test" + +import type ExtensionPage from "./page-objects/ExtensionPage" + +export interface TestExtensions { + extension: ExtensionPage + secondExtension: ExtensionPage + thirdExtension: ExtensionPage + browserContext: ChromiumBrowserContext + upgradeExtension: ExtensionPage +} diff --git a/e2e/src/argent-x/languages/ILanguage.ts b/e2e/src/argent-x/languages/ILanguage.ts new file mode 100644 index 0000000..c96f1e4 --- /dev/null +++ b/e2e/src/argent-x/languages/ILanguage.ts @@ -0,0 +1,3 @@ +import texts from "./en" + +export type ILanguage = typeof texts diff --git a/e2e/src/argent-x/languages/en/index.ts b/e2e/src/argent-x/languages/en/index.ts new file mode 100644 index 0000000..7737812 --- /dev/null +++ b/e2e/src/argent-x/languages/en/index.ts @@ -0,0 +1,162 @@ +const texts = { + common: { + back: "Back", + close: "Close", + confirm: "Confirm", + done: "Done", + next: "Next", + continue: "Continue", + yes: "Yes", + no: "No", + unlock: "Unlock", + showSettings: "Show settings", + reset: "Reset", + confirmReset: "Reset", + save: "Save", + create: "Create", + cancel: "Cancel", + privacyStatement: + "GDPR statement for browser extension wallet: Argent takes the privacy and security of individuals very seriously and takes every reasonable measure and precaution to protect and secure the personal data that we process. The browser extension wallet does not collect any personal information nor does it correlate any of your personal information with anonymous data processed as part of its services. On top of this Argent has robust information security policies and procedures in place to make sure any processing complies with applicable laws. If you would like to know more or have any questions then please visit our website at https://www.argent.xyz/", + approve: "Approve", + addArgentShield: "Add Argent Shield", + changeAccountType: "Change", + accountUpgraded: "Account upgraded", + changedToStandardAccount: "Changed to Standard Account", + dismiss: "Dismiss", + reviewSend: "Review send", + hide: "Hide account", + copy: "Copy", + beforeYouContinue: "Before you continue...", + seedWarning: + "Please save your recovery phrase. This is the only way you will be able to recover your Argent X accounts", + revealSeedPhrase: "Click to reveal recovery phrase", + copied: "Copied", + confirmRecovery: + "I have saved my recovery phrase and understand I should never share it with anyone else", + remove: "Remove", + upgrade: "Upgrade", + }, + account: { + noAccounts: "You have no accounts on", + createAccount: "Create account", + fund: "Fund", + fundsFromStarkNet: "From another Starknet wallet", + fullAccountAddress: "Full account address", + send: "Send", + export: "Export", + accountRecovery: "Save your recovery phrase", + showAccountRecovery: "Show recovery phrase", + saveTheRecoveryPhrase: "Save the recovery phrase", + confirmTheSeedPhrase: + "I have saved my recovery phrase and understand I should never share it with anyone else", + pendingTransactions: "Pending", + recipientAddress: "Recipient's address", + saveAddress: "Save address", + deployFirst: + "You must deploy this account before upgrading to a Smart Account", + wrongPassword: "Incorrect password", + invalidStarkIdError: " not found", + shortAddressError: "Address must be 66 characters long", + invalidCheckSumError: "Invalid address (checksum error)", + invalidAddress: "Invalid address", + createMultisig: "Create multisig", + activateAccount: "Activate Account", + notEnoughFoundsFee: "Insufficient funds to pay fee", + newToken: "New token", + argentShield: { + wrongCode: "Looks like the wrong code. Please try again.", + failedCode: + "You have reached the maximum number of attempts. Please wait 30 minutes and request a new code.", + codeNotRequested: + "You have not requested a verification code. Please request a new one.", + emailInUse: + /This address is associated with accounts from another seedphrase[.,]?\s*Please enter another email address to continue[.,]?/, + }, + removedFromMultisig: "You were removed from this multisig", + copyAddress: "Copy address", + }, + wallet: { + //first screen + banner1: "Welcome to Argent X", + desc1: "Enjoy the security of Ethereum with the scale of Starknet", + createButton: "Create a new wallet", + restoreButton: "Restore an existing wallet", + //second screen + banner2: "Disclaimer", + desc2: + "Starknet is in Alpha and may experience technical issues or introduce breaking changes from time to time. Please accept this before continuing.", + lossOfFunds: + "I understand that Starknet will introduce changes (e.g. Cairo 1.0) that will affect my existing account(s) (e.g. rendering unusable) if I do not complete account upgrades.", + alphaVersion: + "I understand that Starknet may experience performance issues and my transactions may fail for various reasons.", + //third screen + banner3: "Create a password", + desc3: "This is used to protect and unlock your wallet", + password: "Password", + repeatPassword: "Repeat password", + createWallet: "Create wallet", + //fourth screen + banner4: /Your (smart )?account is ready!/, + download: "Download the mobile app", + twitter: "Follow us on X", + dapps: "Explore Starknet apps", + finish: "Finish", + }, + settings: { + account: { + manageOwners: { + manageOwners: "Manage owners", + removeOwner: "Remove owner", + replaceOwner: "Replace owner", + }, + setConfirmations: "Set confirmations", + viewOnStarkScan: "View on StarkScan", + viewOnVoyager: "View on Voyager", + hideAccount: "Hide account", + deployAccount: "Deploy account", + authorisedDapps: { + authorisedDapps: "Authorised dapps", + connect: "Connect", + reject: "Reject", + disconnectAll: "Disconnect all", + noAuthorisedDapps: "No authorised dapps", + }, + exportPrivateKey: "Export private key", + }, + preferences: { + preferences: "Preferences", + hideTokens: "Hidden and spam tokens", + hiddenAccounts: "Hidden accounts", + defaultBlockExplorer: "Default block explorer", + defaultNFTMarket: "Default NFT marketplace", + emailNotifications: "Email notifications", + }, + securityPrivacy: { + securityPrivacy: "Security & privacy", + autoLockTimer: "Auto lock timer", + recoveryPhase: "Recovery phrase", + automaticErrorReporting: "Automatic Error Reporting", + shareAnonymousData: "Share anonymous data", + }, + addressBook: { + addressBook: "Address book", + nameRequired: "Contact Name is required", + addressRequired: "Address is required", + removeAddress: "Remove from address book", + delete: "Delete", + }, + advancedSettings: { + advancedSettings: "Advanced settings", + manageNetworks: { + manageNetworks: "Manage networks", + restoreDefaultNetworks: "Restore default networks", + }, + smartContractDevelopment: "Smart Contract Development", + experimental: "Experimental", + }, + extendedView: "Extended view", + lockWallet: "Lock wallet", + }, +} as const + +export default texts diff --git a/e2e/src/argent-x/languages/index.ts b/e2e/src/argent-x/languages/index.ts new file mode 100644 index 0000000..183e1b9 --- /dev/null +++ b/e2e/src/argent-x/languages/index.ts @@ -0,0 +1,8 @@ +import path from "node:path" + +import type { ILanguage } from "./ILanguage" + +// eslint-disable-next-line @typescript-eslint/no-var-requires +export const lang: ILanguage = require( + path.join(__dirname, `${process.env.LANGUAGE ?? "en"}`), +).default diff --git a/e2e/src/argent-x/page-objects/Account.ts b/e2e/src/argent-x/page-objects/Account.ts new file mode 100644 index 0000000..80824db --- /dev/null +++ b/e2e/src/argent-x/page-objects/Account.ts @@ -0,0 +1,930 @@ +import { Page, expect } from "@playwright/test" + +import { lang } from "../languages" +import Activity from "./Activity" +import { FeeTokens, TokenSymbol, logInfo, sleep } from "../utils" +import config from "../../../config" + +export interface IAsset { + name: string + balance: number + unit: string +} + +export default class Account extends Activity { + upgradeTest: boolean + constructor(page: Page, upgradeTest: boolean = false) { + super(page) + this.upgradeTest = upgradeTest + } + accountName1 = "Account 1" + accountName2 = "Account 2" + accountName3 = "Account 3" + accountNameMulti1 = "Multisig 1" + accountNameMulti2 = "Multisig 2" + accountNameMulti3 = "Multisig 3" + accountNameMulti4 = "Multisig 4" + accountNameMulti5 = "Multisig 5" + accountNameMulti6 = "Multisig 6" + + importedAccountName1 = "Imported Account 1" + importedAccountName2 = "Imported Account 2" + get noAccountBanner() { + return this.page.locator(`div h4:has-text("${lang.account.noAccounts}")`) + } + + get createAccount() { + return this.page.locator('[data-testid="create-account-button"]') + } + + get fundMenu() { + return this.page.getByRole("button", { name: "Fund" }) + } + + get addFundsFromStartNet() { + return this.page.locator(`a :text-is("${lang.account.fundsFromStarkNet}")`) + } + + get accountAddress() { + return this.page.locator( + `[aria-label="${lang.account.fullAccountAddress}"]`, + ) + } + + get accountAddressFromAssetsView() { + return this.page.locator('[data-testid="address-copy-button"]').first() + } + + get send() { + return this.page.locator(`button:has-text("${lang.account.send}")`) + } + + get sendToHeader() { + return this.page.getByRole("heading", { name: "Send to" }) + } + + get deployAccount() { + return this.page.locator( + `button :text-is("${lang.settings.account.deployAccount}")`, + ) + } + + get selectTokenButton() { + return this.page.getByTestId("select-token-button") + } + + async accountNames() { + await expect( + this.page.locator('[data-testid="account-name"]').first(), + ).toBeVisible() + return await this.page + .locator('[data-testid="account-name"]') + .all() + .then( + async (els) => + await Promise.all(els.map(async (el) => await el.textContent())), + ) + } + + token(tkn: TokenSymbol) { + return this.page.locator(`[data-testid="${tkn}"]`) + } + + get accountListSelector() { + return this.page.locator(`[aria-label="Show account list"]`) + } + + get addANewAccountFromAccountList() { + return this.page.getByRole("button", { name: "Add account" }) + } + + get addStandardAccountFromNewAccountScreen() { + return this.page.locator('[aria-label="Standard Account"]') + } + + get importAccountFromNewAccountScreen() { + return this.page.locator('[aria-label="Import from private key"]') + } + + get importAccountAddressLoc() { + return this.page.locator('[name="address"]') + } + + get importPKLoc() { + return this.page.locator('[name="pk"]') + } + + get importSubmitLoc() { + return this.page.locator('button:text-is("Import")') + } + + get addMultisigAccountFromNewAccountScreen() { + return this.page.locator('[aria-label="Multisig Account"]') + } + + get createWithArgent() { + return this.page.locator('[aria-label="Create with Argent"]') + } + + get createNewMultisig() { + return this.page.locator('[aria-label="Create new multisig"]') + } + + get joinExistingMultisig() { + return this.page.locator('[aria-label="Join existing multisig"]') + } + + get joinWithArgent() { + return this.page.locator('[aria-label="Join with Argent"]') + } + + get assetsList() { + return this.page.locator('button[role="alert"] ~ button') + } + + get amount() { + return this.page.locator('[name="amount"]') + } + + get sendMax() { + return this.page.locator('button:text-is("Max")') + } + + get recipientAddressQuery() { + return this.page.locator('[data-testid="recipient-input"]') + } + + account(accountName: string) { + return this.page.locator(`button[aria-label^="Select ${accountName}"]`) + } + + accountNameBtnLoc(accountName: string) { + return this.page.locator(`button[aria-label="Select ${accountName}"]`) + } + + get balance() { + return this.page.locator('[data-testid="tokenBalance"]') + } + + currentBalance(tkn: TokenSymbol) { + return this.page.locator(`[data-testid="${tkn}-balance"]`) + } + + currentBalanceDevNet(tkn: "ETH") { + return this.page.locator(`//button//h6[contains(text(), '${tkn}')]`) + } + + get accountName() { + return this.page.locator('[data-testid="account-tokens"] h2') + } + + invalidStarkIdError(id: string) { + return this.page.locator( + `form label:has-text('${id}${lang.account.invalidStarkIdError}')`, + ) + } + + get shortAddressError() { + return this.page.locator( + `form label:has-text('${lang.account.shortAddressError}')`, + ) + } + + get invalidCheckSumError() { + return this.page.locator( + `form label:has-text('${lang.account.invalidCheckSumError}')`, + ) + } + + get invalidAddress() { + return this.page.locator( + `form label:has-text('${lang.account.invalidAddress}')`, + ) + } + + get failPredict() { + return this.page.locator('[data-testid="tx-error"]') + } + + accountGroup( + group: string = "my-accounts" || + "multisig - accounts" || + "imported-accounts", + ) { + return this.page.locator(`[data-testid="${group}"]`) + } + + async addAccountMainnet({ firstAccount = true }: { firstAccount?: boolean }) { + if (firstAccount) { + await this.createAccount.click() + } else { + await this.accountListSelector.click() + await this.addANewAccountFromAccountList.click() + } + await this.addStandardAccountFromNewAccountScreen.click() + await this.continueLocator.click() + await this.account("").last().click() + await expect(this.accountListSelector).toBeVisible() + } + + async dismissAccountRecoveryBanner() { + await this.showAccountRecovery.click() + await this.confirmTheSeedPhrase.click() + await this.doneLocator.click() + } + + async addAccount({ firstAccount = true }: { firstAccount?: boolean }) { + if (firstAccount) { + await this.createAccount.click() + } else { + await this.accountListSelector.click() + await this.page.getByRole("button", { name: "Add account" }).click() + } + await this.addStandardAccountFromNewAccountScreen.click() + await this.continueLocator.click() + await expect(this.account("").last()).toBeVisible() + const accountsName = await this.account("").allInnerTexts() + const accountLoc = this.page.locator( + `[data-testid="Account ${accountsName.length}"]`, + ) + await expect(accountLoc).toBeVisible() + await this.account(`Account ${accountsName.length}`).hover() + await expect( + accountLoc.locator('[data-testid="goto-settings"]'), + ).toBeVisible() + await accountLoc.click() + //todo check why this is needed, click twice + await sleep(1000) + if (await accountLoc.isVisible()) { + await accountLoc.click() + } + await expect(this.accountListSelector).toBeVisible() + await this.fundMenu.click() + await this.addFundsFromStartNet.click() + const accountAddress = await this.accountAddress + .textContent() + .then((v) => v?.replaceAll(" ", "")) + await this.closeLocator.last().click() + const accountName = await this.accountListSelector.textContent() + return { accountName, accountAddress } + } + + async selectAccount(accountName: string) { + await this.accountListSelector.click() + await this.account(accountName).click() + } + + async ensureSelectedAccount(accountName: string) { + const currentAccount = await this.accountListSelector.textContent() + if (currentAccount != accountName) { + await this.selectAccount(accountName) + } + await expect(this.accountListSelector).toContainText(accountName) + } + + async assets(accountName: string) { + await this.ensureSelectedAccount(accountName) + + const assetsList: IAsset[] = [] + for (const asset of await this.assetsList.all()) { + const row = (await asset.innerText()).split(/\r?\n| /) + assetsList.push({ + name: row[0], + balance: parseFloat(row[1]), + unit: row[2], + } as IAsset) + } + return assetsList + } + + async ensureAsset( + accountName: string, + name: TokenSymbol = "ETH", + value: string, + ) { + await this.ensureSelectedAccount(accountName) + await expect(this.currentBalance(name)).toContainText(value) + } + + async getTotalFeeValue() { + const fee = await this.page + .locator('[aria-label="Show Fee Estimate details"] p') + .first() + .textContent() + if (!fee) { + throw new Error("Error! Fee not available") + } + + return parseFloat(fee.split(" ")[0]) + } + async txValidations(feAmount: string) { + const trxAmountHeader = await this.page + .locator(`//*[starts-with(text(),'Send ')]`) + .textContent() + .then((v) => v?.split(" ")[1]) + + const sendAmountFEText = await this.page + .locator("[data-fe-value]") + .getAttribute("data-fe-value") + const sendAmountTXText = await this.page + .locator("[data-tx-value]") + .getAttribute("data-tx-value") + const sendAmountFE = sendAmountFEText!.split(" ")[0] + const sendAmountTX = parseInt(sendAmountTXText!) + logInfo({ sendAmountFE, sendAmountTX }) + expect(sendAmountFE).toBe(`${trxAmountHeader}`) + + if (feAmount != "MAX") { + expect(feAmount).toBe(trxAmountHeader) + } + return { sendAmountTX, sendAmountFE } + } + + async fillRecipientAddress({ + recipientAddress, + fillRecipientAddress = "paste", + validAddress = true, + }: { + recipientAddress: string + fillRecipientAddress?: "typing" | "paste" + validAddress?: boolean + }) { + if (fillRecipientAddress === "paste") { + await this.setClipboardText(recipientAddress) + await this.recipientAddressQuery.focus() + await this.paste() + } else { + await this.recipientAddressQuery.type(recipientAddress) + await this.page.keyboard.press("Enter") + } + if (validAddress) { + if (recipientAddress.endsWith("stark")) { + await this.page.click(`button:has-text("${recipientAddress}")`) + } + } + } + + async confirmTransaction() { + await Promise.race([ + expect(this.confirmLocator) + .toBeEnabled() + .then((_) => this.confirmLocator.click()), + expect(this.failPredict).toBeVisible(), + ]) + if (await this.failPredict.isVisible()) { + await this.failPredict.click() + console.error("failPredict", this.paste) + } + } + + async transfer({ + originAccountName, + recipientAddress, + token, + amount, + fillRecipientAddress = "paste", + submit = true, + feeToken = "ETH", + }: { + originAccountName: string + recipientAddress: string + token: TokenSymbol + amount: number | "MAX" + fillRecipientAddress?: "typing" | "paste" + submit?: boolean + feeToken?: FeeTokens + }) { + await this.ensureSelectedAccount(originAccountName) + await this.send.click() + await this.fillRecipientAddress({ recipientAddress, fillRecipientAddress }) + await this.selectTokenButton.click() + await this.token(token).click() + if (amount === "MAX") { + await expect(this.balance).toBeVisible() + await expect(this.sendMax).toBeVisible() + await this.sendMax.click() + } else { + await this.amount.fill(amount.toString()) + } + + await this.reviewSendLocator.click() + + if (submit) { + if (feeToken) { + await this.selectFeeToken(feeToken) + } + await this.confirmTransaction() + } + const { sendAmountFE, sendAmountTX } = await this.txValidations( + amount.toString(), + ) + try { + await expect(this.failPredict) + .toBeVisible({ timeout: 1000 * 3 }) + .then(async (_) => { + await this.failPredict.click() + await this.page.locator('[data-testid="copy-error"]').click() + await this.setClipboard() + console.error( + "Error message copied to clipboard", + await this.getClipboard(), + ) + throw new Error("Transaction failed") + }) + } catch { + /* empty */ + } + return { sendAmountTX, sendAmountFE } + } + + async ensureTokenBalance({ + accountName, + token, + balance, + }: { + accountName: string + token: TokenSymbol + balance: number + }) { + await this.ensureSelectedAccount(accountName) + await this.token(token).click() + await expect(this.page.locator('[data-testid="tokenBalance"]')).toHaveText( + balance.toString(), + ) + await this.backLocator.click() + } + + get password() { + return this.page.locator('input[name="password"]').first() + } + + get exportPrivateKey() { + return this.page.locator(`button:text-is("${lang.account.export}")`) + } + + get setUpAccountRecovery() { + return this.page.locator( + `button:text-is("${lang.account.accountRecovery}")`, + ) + } + + get showAccountRecovery() { + return this.page.locator( + `button:text-is("${lang.account.showAccountRecovery}")`, + ) + } + + get confirmTheSeedPhrase() { + return this.page.locator( + `p:text-is("${lang.account.confirmTheSeedPhrase}")`, + ) + } + + // account recovery modal + get saveTheRecoveryPhrase() { + return this.page.locator( + `//a//*[text()="${lang.account.saveTheRecoveryPhrase}"]`, + ) + } + + get recipientAddress() { + return this.page.locator('[data-testid="recipient-input"]') + } + + get saveAddress() { + return this.page.locator(`button:text-is("${lang.account.saveAddress}")`) + } + + get copyAddress() { + return this.page.locator('[data-testid="address-copy-button"]').first() + } + + get copyAddressFromFundMenu() { + return this.page.locator(`button:text-is("${lang.account.copyAddress}")`) + } + + contact(label: string) { + return this.page.locator(`div h5:text-is("${label}")`) + } + + get avnuBanner() { + return this.page.locator('p:text-is("Swap with AVNU")') + } + + get ekuboBanner() { + return this.page.locator('p:text-is("Provide liquidity on Ekubo")') + } + + get avnuBannerClose() { + return this.page.locator('[data-testid="close-banner"]') + } + + async saveRecoveryPhrase() { + const nextModal = await this.nextLocator.isVisible({ timeout: 60 }) + if (nextModal) { + await this.nextLocator.click() + } + await this.page + .locator(`span:has-text("${lang.common.revealSeedPhrase}")`) + .click() + const pos = Array.from({ length: 12 }, (_, i) => i + 1) + const seed = await Promise.all( + pos.map(async (index) => { + return this.page + .locator(`//*[normalize-space() = '${index}']/parent::*`) + .textContent() + .then((text) => text?.replace(/[0-9]/g, "")) + }), + ).then((result) => result.join(" ")) + + await Promise.all([ + this.page.locator(`button:has-text("${lang.common.copy}")`).click(), + expect( + this.page.locator(`button:has-text("${lang.common.copied}")`), + ).toBeVisible(), + ]) + await this.setClipboard() + const seedPhraseCopied = await this.getClipboard() + await expect(this.doneLocator).toBeDisabled() + await this.page + .locator(`p:has-text("${lang.common.confirmRecovery}")`) + .click() + await expect(this.page.getByTestId("recovery-phrase-checked")).toBeVisible() + await expect(this.doneLocator).toBeEnabled() + await this.doneLocator.click({ force: true }) + expect(seed).toBe(seedPhraseCopied) + return String(seedPhraseCopied) + } + + // Smart Account + get email() { + return this.page.locator('input[name="email"]') + } + + get pinLocator() { + return this.page.locator('[aria-label="Please enter your pin code"]') + } + + async fillPin(pin: string = "111111") { + //avoid BE error PIN not requested + await sleep(2000) + await expect(this.pinLocator).toHaveCount(6) + await this.pinLocator.first().click() + await this.pinLocator.first().fill(pin) + } + + async setupRecovery() { + //ensure modal is loaded + await expect( + this.page.locator('[data-testid="account-tokens"]'), + ).toBeVisible() + await expect( + this.page.locator('[data-testid="address-copy-button"]'), + ).toBeVisible() + if (config.isProdTesting) { + await this.showAccountRecovery.click() + } else { + await this.accountAddressFromAssetsView.click() + } + return this.saveRecoveryPhrase().then((adr) => String(adr)) + } + + get accountUpgraded() { + return this.page.getByRole("heading", { + name: lang.common.accountUpgraded, + }) + } + + get changedToStandardAccountLabel() { + return this.page.getByRole("heading", { + name: lang.common.changedToStandardAccount, + }) + } + + // Multisig + get deployNeededWarning() { + return this.page.locator(`p:has-text("${lang.account.deployFirst}")`) + } + + get increaseThreshold() { + return this.page.locator(`[data-testid="increase-threshold"]`) + } + + get decreaseThreshold() { + return this.page.locator(`[data-testid="decrease-threshold"]`) + } + + get setConfirmationsLocator() { + return this.page.locator(`button:has-text("Set confirmations")`) + } + + async addMultisigAccount({ + signers = [], + confirmations = 1, + }: { + signers?: string[] + confirmations?: number + }) { + await this.accountListSelector.click() + await this.addANewAccountFromAccountList.click() + await this.addMultisigAccountFromNewAccountScreen.click() + await this.continueLocator.click() + await this.createNewMultisig.click() + + const [pages] = await Promise.all([ + this.page.context().waitForEvent("page"), + this.createWithArgent.click(), + ]) + + const tabs = pages.context().pages() + await tabs[1].waitForLoadState("load") + await expect(tabs[1].locator('[name^="signerKeys.0.key"]')).toHaveCount(1) + + if (signers.length > 0) { + for (let index = 0; index < signers.length; index++) { + await tabs[1] + .locator(`[name="signerKeys\\.${index}\\.key"]`) + .isVisible() + .then(async (visible) => { + if (!visible) { + await tabs[1].locator('[data-testid="addOwnerButton"]').click() + } + }) + await tabs[1] + .locator(`[name="signerKeys.${index}.key"]`) + .fill(signers[index]) + } + } + //remove empty inputs + const locs = await tabs[1].locator('[data-testid^="signerContainer"]').all() + if (locs.length > signers.length) { + for (let index = locs.length; index > signers.length; index--) { + await tabs[1] + .locator(`[data-testid="closeButton.${index - 1}"]`) + .click() + } + } + + await tabs[1].locator('button:text-is("Next")').click() + const currentThreshold = await tabs[1] + .locator('[data-testid="threshold"]') + .innerText() + .then((v) => parseInt(v!)) + + //set confirmations + if (confirmations > currentThreshold) { + for (let i = currentThreshold; i < confirmations; i++) { + await tabs[1].locator('[data-testid="increase-threshold"]').click() + } + } + + await tabs[1] + .locator(`button:text-is("${lang.account.createMultisig}")`) + .click() + await tabs[1].locator(`button:text-is("${lang.wallet.finish}")`).click() + } + + async joinMultisig() { + await this.accountListSelector.click() + await this.addANewAccountFromAccountList.click() + await this.addMultisigAccountFromNewAccountScreen.click() + await this.continueLocator.click() + + await this.joinExistingMultisig.click() + await this.joinWithArgent.click() + await this.page.locator('[data-testid="copy-pubkey"]').click() + await this.setClipboard() + await this.page.locator('[data-testid="button-done"]').click() + return String(await this.getClipboard()) + } + + async addOwnerToMultisig({ + accountName, + pubKey, + confirmations = 1, + }: { + accountName: string + pubKey: string + confirmations?: number + }) { + await this.showSettingsLocator.click() + await this.account(accountName).click() + await this.manageOwners.click() + await this.page.locator('[data-testid="add-owners"]').click() + //hydrogen build will always have 2 inputs + const locs = await this.page.locator('[data-testid^="closeButton."]').all() + for (let index = 0; locs.length - 1 > index; index++) { + await this.page.locator(`[data-testid^="closeButton.${index}"]`).click() + } + await this.page.locator('[name^="signerKeys.0.key"]').fill(pubKey) + + await this.nextLocator.click() + + const currentThreshold = await this.page + .locator('[data-testid="threshold"]') + .innerText() + .then((v) => parseInt(v!)) + //set confirmations + if (confirmations > currentThreshold) { + for (let i = currentThreshold; i < confirmations; i++) { + await this.page.locator('[data-testid="increase-threshold"]').click() + } + } + await this.nextLocator.click() + await this.confirmLocator.click() + } + + ensureMultisigActivated() { + return Promise.all([ + expect(this.page.locator("label:has-text('Not activated')")).toBeHidden(), + expect( + this.page.locator('[data-testid="activating-multisig"]'), + ).toBeHidden(), + ]) + } + + accountListConfirmations(accountName: string) { + return this.page.locator( + `[aria-label="Select ${accountName}"] [data-testid="confirmations"]`, + ) + } + + get accountViewConfirmations() { + return this.page.locator('[data-testid="confirmations"]').first() + } + + async acceptTx(tx: string) { + await this.menuActivityLocator.click() + await this.page.locator(`[data-tx-hash="${tx}"]`).click() + await this.confirmTransaction() + } + + async setConfirmations(accountName: string, confirmations: number) { + await this.ensureSelectedAccount(accountName) + await this.showSettingsLocator.click() + await this.account(accountName).click() + await this.setConfirmationsLocator.click() + + const currentThreshold = await this.page + .locator('[data-testid="threshold"]') + .innerText() + .then((v) => parseInt(v!)) + if (confirmations > currentThreshold) { + for (let i = currentThreshold; i < confirmations; i++) { + await this.increaseThreshold.click() + } + } else if (confirmations < currentThreshold) { + for (let i = currentThreshold; i > confirmations; i--) { + await this.decreaseThreshold.click() + } + } + await this.page.locator('[data-testid="update-confirmations"]').click() + await this.confirmTransaction() + await Promise.all([ + expect(this.confirmLocator).toBeHidden(), + expect(this.menuActivityLocator).toBeVisible(), + ]) + } + + async ensureSmartAccountNotEnabled(accountName: string) { + await this.selectAccount(accountName) + await Promise.all([ + expect(this.menuPendingTransactionsIndicatorLocator).toBeHidden(), + expect( + this.page.locator('[data-testid="smart-account-on-account-view"]'), + ).toBeHidden(), + ]) + await this.showSettingsLocator.click() + await Promise.all([ + expect( + this.page.locator('[data-testid="smart-account-on-settings"]'), + ).toBeHidden(), + expect( + this.page.locator('[data-testid="smart-account-not-activated"]'), + ).toBeVisible(), + ]) + await this.account(accountName).click() + await expect( + this.page.locator( + '[data-testid="smart-account-button"]:has-text("Upgrade to Smart Account")', + ), + ).toBeVisible() + } + + editOwnerLocator(owner: string) { + return this.page.locator(`[data-testid="edit-${owner}"]`) + } + get manageOwners() { + return this.page.locator( + `//button//*[text()="${lang.settings.account.manageOwners.manageOwners}"]`, + ) + } + + get removeOwnerLocator() { + return this.page.locator( + `//button[text()="${lang.settings.account.manageOwners.removeOwner}"]`, + ) + } + + get removedFromMultisigLocator() { + return this.page.getByText(lang.account.removedFromMultisig) + } + + async removeMultiSigOwner(accountName: string, owner: string) { + await this.showSettingsLocator.click() + await this.account(accountName).click() + await this.manageOwners.click() + await this.editOwnerLocator(owner).click() + await this.removeOwnerLocator.click() + await this.removeLocator.click() + await this.nextLocator.click() + await this.confirmTransaction() + } + + //TX v3 + get feeTokenPickerLoc() { + return this.page.locator('[data-testid="fee-token-picker"]') + } + + feeTokenLoc(token: FeeTokens) { + return this.page.locator(`[data-testid="fee-token-${token}"]`) + } + + feeTokenBalanceLoc(token: FeeTokens) { + return this.page.locator(`[data-testid="fee-token-${token}-balance"]`) + } + + selectedFeeTokenLoc(token: FeeTokens) { + return this.feeTokenPickerLoc.locator(`img[alt=${token}]`) + } + + async selectFeeToken(token: FeeTokens) { + //wait for locator to be visible + await Promise.race([ + expect(this.selectedFeeTokenLoc("ETH")).toBeVisible(), + expect(this.selectedFeeTokenLoc("STRK")).toBeVisible(), + ]) + const tokenAlreadySelected = + await this.selectedFeeTokenLoc(token).isVisible() + if (!tokenAlreadySelected) { + await this.feeTokenPickerLoc.click() + await this.feeTokenLoc(token).click() + await expect(this.selectedFeeTokenLoc(token)).toBeVisible() + } + } + + async gotoSettingsFromAccountList(accountName: string) { + await expect(this.accountNameBtnLoc(accountName)).toBeVisible() + await this.accountNameBtnLoc(accountName).hover() + await expect( + this.accountNameBtnLoc(accountName).locator( + '[data-testid="token-value"]', + ), + ).toBeHidden() + await expect( + this.accountNameBtnLoc(accountName).locator( + '[data-testid="goto-settings"]', + ), + ).toBeVisible() + await expect( + this.accountNameBtnLoc(accountName).locator( + '[data-testid="goto-settings"]', + ), + ).toHaveCount(1) + //todo: remove sleep + await sleep(1000) + await this.accountNameBtnLoc(accountName) + .locator('[data-testid="goto-settings"]') + .click() + await expect( + this.page.locator( + `[data-testid="account-settings-${accountName.replaceAll(/ /g, "")}"]`, + ), + ).toBeVisible() + } + + async importAccount({ + address, + privateKey, + validPK = true, + }: { + address: string + privateKey: string + validPK?: boolean + }) { + await this.accountListSelector.click() + await this.addANewAccountFromAccountList.click() + await this.importAccountFromNewAccountScreen.click() + + await this.continueLocator.click() + await this.importAccountAddressLoc.fill(address) + await this.importPKLoc.fill(privateKey) + await this.importSubmitLoc.click() + if (!validPK) { + await Promise.all([ + expect(this.page.getByText("The private key is invalid")).toBeVisible(), + expect(this.page.getByRole("button", { name: "Ok" })).toBeVisible(), + ]) + } + } +} diff --git a/e2e/src/argent-x/page-objects/Activity.ts b/e2e/src/argent-x/page-objects/Activity.ts new file mode 100644 index 0000000..4ad4146 --- /dev/null +++ b/e2e/src/argent-x/page-objects/Activity.ts @@ -0,0 +1,74 @@ +import { Page, expect } from "@playwright/test" + +import { lang } from "../languages" +import Navigation from "./Navigation" + +export default class Activity extends Navigation { + constructor(page: Page) { + super(page) + } + + ensurePendingTransactions(nbr: number) { + return expect( + this.page.locator( + `//p[contains(text(),'Pending')]/following-sibling::div[contains(text(),'${nbr}')]`, + ), + ).toBeVisible() + } + + ensureNoPendingTransactions() { + return expect( + this.page.locator( + `h6 div:text-is("${lang.account.pendingTransactions}") >> div`, + ), + ).not.toBeVisible() + } + + activityByDestination(destination: string) { + return this.page.locator( + `//button//p[contains(text()[1], 'To: ') and contains(text()[2], '${destination}')]`, + ) + } + + checkActivity(nbr: number) { + return Promise.all([ + this.menuPendingTransactionsIndicatorLocator.click(), + this.ensurePendingTransactions(nbr), + ]) + } + + async activityTxHashs() { + await expect( + this.page.locator("button[data-tx-hash]").first(), + ).toBeVisible() + const loc = await this.page.locator("button[data-tx-hash]").all() + return Promise.all(loc.map((el) => el.getAttribute("data-tx-hash"))) + } + + async getLastTxHash() { + await this.menuActivityActiveLocator.isVisible().then(async (visible) => { + if (!visible) { + await this.menuActivityLocator.click() + } + }) + expect(this.historyButton) + .toBeVisible({ timeout: 1000 }) + .then(async () => { + await this.historyButton.click() + }) + .catch(async () => { + null + }) + + const txHashs = await this.activityTxHashs() + return txHashs[0] + } + + get historyButton() { + return this.page.locator("button:text-is('History')") + } + + get queueButton() { + return this.page.locator("button:text-is('Queue')") + } +} diff --git a/e2e/src/argent-x/page-objects/AddressBook.ts b/e2e/src/argent-x/page-objects/AddressBook.ts new file mode 100644 index 0000000..e98c399 --- /dev/null +++ b/e2e/src/argent-x/page-objects/AddressBook.ts @@ -0,0 +1,83 @@ +import type { Page } from "@playwright/test" + +import { lang } from "../languages" +import Navigation from "./Navigation" + +export default class AddressBook extends Navigation { + constructor(page: Page) { + super(page) + } + + get add() { + return this.page.locator('button[aria-label="add"]') + } + + get name() { + return this.page.locator('input[name="name"]') + } + + get address() { + return this.page.locator('textarea[name="address"]') + } + + get network() { + return this.page.locator('[aria-label="network-selector"]') + } + + get saveLocator() { + return this.page.locator(`button:text-is("${lang.common.save}")`) + } + + get cancelLocator() { + return this.page.locator(`button:text-is("${lang.common.cancel}")`) + } + + networkOption(name: "Localhost 5050" | "Sepolia" | "Mainnet") { + return this.page.locator(`button[role="menuitem"]:text-is("${name}")`) + } + + get nameRequired() { + return this.page.locator( + `//input[@name="name"]/following::label[contains(text(), '${lang.settings.addressBook.nameRequired}')]`, + ) + } + + get addressRequired() { + return this.page.locator( + `//textarea[@name="address"]/following::label[contains(text(), '${lang.settings.addressBook.addressRequired}')]`, + ) + } + + addressByName(name: string) { + return this.page.locator( + `//button/following::*[contains(text(),'${name}')]`, + ) + } + + get deleteAddress() { + return this.page.locator( + `button[aria-label="${lang.settings.addressBook.removeAddress}"]`, + ) + } + + get delete() { + return this.page.locator( + `button:text-is("${lang.settings.addressBook.delete}")`, + ) + } + + get addressBook() { + return this.page.locator( + `button:text-is("${lang.settings.addressBook.addressBook}")`, + ) + } + + async editAddress(name: string) { + await this.page.locator(`[data-testid="${name}"]`).first().click() + await this.page.locator(`[data-testid="${name}"]`).first().click() + await this.page.locator(`[data-testid="${name}"]`).first().hover() + await this.page + .locator(`[data-testid="${name}"] [data-testid^="edit-contact"]`) + .click() + } +} diff --git a/e2e/src/argent-x/page-objects/Dapps.ts b/e2e/src/argent-x/page-objects/Dapps.ts new file mode 100644 index 0000000..3ef8344 --- /dev/null +++ b/e2e/src/argent-x/page-objects/Dapps.ts @@ -0,0 +1,157 @@ +import { ChromiumBrowserContext, Page, expect } from "@playwright/test" + +import { lang } from "../languages" +import Navigation from "./Navigation" +import config from "../../../config" + +const dappUrl = "http://localhost:3000" +const dappName = 'localhost' +export default class Dapps extends Navigation { + constructor(page: Page) { + super(page) + } + + account(accountName: string) { + return this.page.locator(`[data-testid="${accountName}"]`).first() + } + + connectedDapps(accountName: string, nbrConnectedDapps: number) { + return nbrConnectedDapps > 1 + ? this.page.locator( + `[data-testid="${accountName}"]:has-text("${nbrConnectedDapps} dapps connected")`, + ) + : this.page.locator( + `[data-testid="${accountName}"]:has-text("${nbrConnectedDapps} dapp connected")`, + ) + } + + get noConnectedDapps() { + return this.page.locator( + `text=${lang.settings.account.authorisedDapps.noAuthorisedDapps}`, + ) + } + + connected() { + return this.page.locator(`//div/*[contains(text(),'${dappName}')]`) + } + + disconnect() { + return this.page.locator( + `//div/*[contains(text(),'${dappName}')]/following::button[1]`, + ) + } + + disconnectAll() { + return this.page.locator( + `p:text-is("${lang.settings.account.authorisedDapps.disconnectAll}")`, + ) + } + + get accept() { + return this.page.locator( + `button:text-is("${lang.settings.account.authorisedDapps.connect}")`, + ) + } + + get reject() { + return this.page.locator( + `button:text-is("${lang.settings.account.authorisedDapps.reject}")`, + ) + } + + get knownDappButton() { + return this.page.locator('[data-testid="KnownDappButton"]') + } + + async ensureKnowDappText() { + return Promise.all([ + expect(this.page.locator('h4:text-is("Known Dapp")')).toBeVisible(), + expect( + this.page.locator('p:text-is("This dapp is listed on Dappland")'), + ).toBeVisible(), + ]) + } + async requestConnectionFromDapp( + { browserContext, + useStarknetKitModal = false }: + { + browserContext: ChromiumBrowserContext, + useStarknetKitModal?: boolean + } + ) { + //open dapp page + const dapp = await browserContext.newPage() + await dapp.setViewportSize({ width: 1080, height: 720 }) + await dapp.goto("chrome://inspect/#extensions") + await dapp.waitForTimeout(1000) + await dapp.goto(dappUrl) + + await dapp.getByRole('button', { name: 'Connection' }).click() + if (useStarknetKitModal) { + await dapp.getByRole('button', { name: 'Starknetkit Modal' }).click() + await dapp.locator('#starknetkit-modal-container').getByRole('button', { name: 'Argent X Argent X' }).click() + } else { + await expect(dapp.locator('button :text-is("Argent X")')).toBeVisible() + } + await dapp.locator('button :text-is("Argent X")').click() + return dapp + } + + async claimSpok(browserContext: ChromiumBrowserContext) { + const spokCampaignUrl = config.spokCampaignUrl! || '' + //open dapp page + const dapp = await browserContext.newPage() + await dapp.setViewportSize({ width: 1080, height: 720 }) + await dapp.goto("chrome://inspect/#extensions") + await dapp.waitForTimeout(1000) + await dapp.goto(spokCampaignUrl) + await dapp.getByRole("button", { name: "Check eligibility" }).click() + await expect(dapp.locator("text=Argent X")).toBeVisible() + await dapp.locator("text=Argent X").click() + return dapp + } + + checkCriticalRiskConnectionScreen() { + return Promise.all([ + expect( + this.page.locator( + `//span[text()="Critical risk"]/following-sibling::label[text()="Use of a blacklisted domain"]`, + ), + ).toBeVisible(), + expect( + this.page.locator( + `//p[@data-testid="review-footer" and text()="Please review warnings before continuing"]`, + ), + ).toBeVisible(), + expect(this.page.getByRole("button", { name: "Connect" })).toBeDisabled(), + ]) + } + + async acceptCriticalRiskConnection() { + await this.page.getByRole("button", { name: "Review" }).click() + await Promise.all([ + expect( + this.page.locator(`//header[@title="1 risk identified"]`), + ).toBeVisible(), + expect( + this.page.locator( + '//label[text()="We strongly recommend you do not proceed with this transaction"]', + ), + ).toBeVisible(), + expect( + this.page.locator( + '//span[text()="Critical risk"]/following-sibling::span[text()="Use of a blacklisted domain"]/following-sibling::p[text()="You are currently on an unsafe domain. Be aware of the risks."]', + ), + ).toBeVisible(), + ]) + await this.page.getByRole("button", { name: "Accept risk" }).click() + } + + async connectedDappsTooltip(dappUrl: string) { + await this.showSettingsLocator.click() + await this.page.hover('[data-testid="connected-dapp"]') + await expect( + this.page.locator('[data-testid="connected-dapp"]'), + ).toHaveText(`Connected to ${dappUrl}`) + } +} diff --git a/e2e/src/argent-x/page-objects/DeveloperSettings.ts b/e2e/src/argent-x/page-objects/DeveloperSettings.ts new file mode 100644 index 0000000..fbed044 --- /dev/null +++ b/e2e/src/argent-x/page-objects/DeveloperSettings.ts @@ -0,0 +1,72 @@ +import type { Page } from "@playwright/test" + +import { lang } from "../languages" + +export default class DeveloperSettings { + constructor(private page: Page) { } + + get manageNetworks() { + return this.page.locator( + `//a//*[text()="${lang.settings.advancedSettings.manageNetworks.manageNetworks}"]`, + ) + } + + get blockExplorer() { + return this.page.locator( + `//a//*[text()="${lang.settings.preferences.defaultBlockExplorer}"]`, + ) + } + + get smartCOntractDevelopment() { + return this.page.locator( + `//a//*[text()="${lang.settings.advancedSettings.smartContractDevelopment}"]`, + ) + } + + get experimental() { + return this.page.locator( + `//a//*[text()="${lang.settings.advancedSettings.experimental}"]`, + ) + } + + // Manage networks + get addNetwork() { + return this.page.locator('button[aria-label="add"]') + } + + get networkName() { + return this.page.locator('[name="name"]') + } + + get chainId() { + return this.page.locator('[name="chainId"]') + } + + get sequencerUrl() { + return this.page.locator('[name="sequencerUrl"]') + } + + get rpcUrl() { + return this.page.locator('[name="rpcUrl"]') + } + + get create() { + return this.page.locator('button[type="submit"]') + } + + get restoreDefaultNetworks() { + return this.page.locator( + `button:has-text("${lang.settings.advancedSettings.manageNetworks.restoreDefaultNetworks}")`, + ) + } + + networkByName(name: string) { + return this.page.locator(`h5:has-text("${name}")`) + } + + deleteNetworkByName(name: string) { + return this.page.locator( + `//div/*[contains(text(),'${name}')]/following::button[1]`, + ) + } +} diff --git a/e2e/src/argent-x/page-objects/ExtensionPage.ts b/e2e/src/argent-x/page-objects/ExtensionPage.ts new file mode 100644 index 0000000..b134c37 --- /dev/null +++ b/e2e/src/argent-x/page-objects/ExtensionPage.ts @@ -0,0 +1,433 @@ +import { expect, type Page } from "@playwright/test" + +import Messages from "./Messages" +import Account from "./Account" +import Activity from "./Activity" +import AddressBook from "./AddressBook" +import Dapps from "./Dapps" +import DeveloperSettings from "./DeveloperSettings" +import Navigation from "./Navigation" +import Network from "./Network" +import Settings from "./Settings" +import Wallet from "./Wallet" +import config from "../../../config" +import Nfts from "./Nfts" +import Preferences from "./Preferences" +import Swap from "./Swap" +import TokenDetails from "./TokenDetails" + +import { + transferTokens, + AccountsToSetup, + validateTx, + isScientific, + convertScientificToDecimal, + FeeTokens, + logInfo, + Clipboard, +} from "../utils" + +export default class ExtensionPage { + page: Page + wallet: Wallet + network: Network + account: Account + messages: Messages + activity: Activity + settings: Settings + navigation: Navigation + developerSettings: DeveloperSettings + addressBook: AddressBook + dapps: Dapps + nfts: Nfts + preferences: Preferences + clipboard: Clipboard + swap: Swap + tokenDetails: TokenDetails + + upgradeTest: boolean = false + + constructor( + page: Page, + private extensionUrl: string, + upgradeTest: boolean = false, + ) { + this.page = page + this.wallet = new Wallet(page, upgradeTest) + this.network = new Network(page) + this.account = new Account(page, upgradeTest) + this.extensionUrl = extensionUrl + this.messages = new Messages(page) + this.activity = new Activity(page) + this.settings = new Settings(page) + this.navigation = new Navigation(page) + this.developerSettings = new DeveloperSettings(page) + this.addressBook = new AddressBook(page) + this.dapps = new Dapps(page) + this.nfts = new Nfts(page) + this.preferences = new Preferences(page) + this.clipboard = new Clipboard(page) + this.swap = new Swap(page) + this.tokenDetails = new TokenDetails(page) + this.upgradeTest = upgradeTest + } + + async open() { + await this.page.setViewportSize(config.viewportSize) + await this.page.goto(this.extensionUrl) + } + + async resetExtension() { + await this.navigation.showSettingsLocator.click() + await this.navigation.lockWalletLocator.click() + await this.navigation.resetLocator.click() + await this.page.locator('[name="validationString"]').fill("RESET WALLET") + await this.page.locator('label[type="checkbox"]').click({ force: true }) + await this.navigation.confirmResetLocator.click() + } + + async pasteSeed() { + await this.page.locator('[data-testid="seed-input-0"]').focus() + await this.clipboard.paste() + } + + async recoverWallet(seed: string, password?: string) { + await this.page.setViewportSize({ width: 1080, height: 720 }) + + await this.wallet.restoreExistingWallet.click() + await this.wallet.agreeLoc.click() + await this.clipboard.setClipboardText(seed) + await this.pasteSeed() + await this.navigation.continueLocator.click() + + await this.wallet.password.fill(password ?? config.password) + await this.wallet.repeatPassword.fill(password ?? config.password) + + await this.navigation.continueLocator.click() + await Promise.race([ + expect(this.wallet.finish).toBeVisible(), + expect(this.page.getByText("Your account is ready!")).toBeVisible(), + expect(this.page.getByText("Your smart account is ready!")).toBeVisible(), + ]) + + await this.open() + await expect(this.network.networkSelector).toBeVisible() + } + + async addAccount() { + await this.account.addAccount({ firstAccount: false }) + await this.account.copyAddress.click() + await this.clipboard.setClipboard() + const accountAddress = await this.clipboard.getClipboard() + expect(accountAddress).toMatch(/^0x0/) + return accountAddress + } + + async deployAccount(accountName: string, feeToken?: FeeTokens) { + if (accountName) { + await this.account.ensureSelectedAccount(accountName) + } + await this.navigation.showSettingsLocator.click() + await this.page.locator(`[data-testid="${accountName}"]`).click() + await this.settings.deployAccount.click() + if (feeToken) { + await this.account.selectFeeToken(feeToken) + } + await this.account.confirmTransaction() + await this.navigation.backLocator.click() + await this.navigation.closeLocator.click() + if (await this.page.getByRole("heading", { name: "Activity" }).isHidden()) { + await this.navigation.menuActivityLocator.click() + } + await expect( + this.page.getByText(/(Account created and transfer|Account activation)/), + ).toBeVisible() + + await this.navigation.showSettingsLocator.click() + await expect(this.page.getByText("Deploying")).toBeHidden() + await this.navigation.closeLocator.click() + await this.navigation.menuTokensLocator.click() + } + + async activateSmartAccount({ + accountName, + email, + pin = "111111", + validSession = false, + }: { + accountName: string + email?: string + pin?: string + validSession?: boolean + }) { + await this.account.ensureSelectedAccount(accountName) + await this.navigation.showSettingsLocator.click() + await this.settings.account(accountName).click() + await this.settings.smartAccountButton.click() + await this.navigation.nextLocator.click() + if (!validSession) { + await this.account.email.fill(email!) + await this.navigation.nextLocator.first().click() + await this.account.fillPin(pin) + } + await this.navigation.upgradeLocator.click() + await this.account.confirmTransaction() + await expect(this.account.accountUpgraded).toBeVisible() + await this.navigation.doneLocator.click() + await this.navigation.backLocator.click() + await this.navigation.closeLocator.click() + await Promise.all([ + expect( + this.activity.menuPendingTransactionsIndicatorLocator, + ).toBeHidden(), + expect( + this.page.locator('[data-testid="smart-account-on-account-view"]'), + ).toBeVisible(), + ]) + await this.navigation.showSettingsLocator.click() + await expect( + this.page.locator('[data-testid="smart-account-on-settings"]'), + ).toBeVisible() + await this.settings.account(accountName).click() + await expect( + this.page.locator( + '[data-testid="smart-account-button"]:has-text("Change to Standard Account")', + ), + ).toBeEnabled() + await this.navigation.backLocator.click() + await this.navigation.closeLocator.click() + } + + async changeToStandardAccount({ + accountName, + email, + pin = "111111", + validSession = false, + }: { + accountName: string + email: string + pin?: string + validSession?: boolean + }) { + await this.account.ensureSelectedAccount(accountName) + await this.navigation.showSettingsLocator.click() + await this.settings.account(accountName).click() + await this.settings.changeToStandardAccountButton.click() + //await this.navigation.nextLocator.click() + if (!validSession) { + await this.account.email.fill(email) + await this.navigation.nextLocator.first().click() + await this.account.fillPin(pin) + } + + await this.navigation.confirmChangeAccountTypeLocator.click() + await this.account.confirmTransaction() + await expect(this.account.changedToStandardAccountLabel).toBeVisible() + await this.navigation.doneLocator.click() + await this.navigation.backLocator.click() + await this.navigation.closeLocator.click() + await this.account.ensureSmartAccountNotEnabled(accountName) + } + + async fundAccount( + acc: AccountsToSetup, + accountAddress: string, + accIndex: number, + ) { + let expectedTokenValue + for (const [assetIndex, asset] of acc.assets.entries()) { + logInfo({ + op: "fundAccount", + assetIndex, + asset, + isProdTesting: config.isProdTesting, + }) + if (asset.balance > 0) { + await transferTokens( + asset.balance, + accountAddress, // receiver wallet address + asset.token, + ) + + if (isScientific(asset.balance)) { + expectedTokenValue = `${convertScientificToDecimal(asset.balance)}` + } else { + expectedTokenValue = `${asset.balance}` + } + if (!expectedTokenValue.includes(".")) { + expectedTokenValue += ".0" + } + expectedTokenValue += ` ${asset.token}` + await this.account.ensureAsset( + `Account ${accIndex + 1}`, + asset.token, + expectedTokenValue, + ) + } + } + + if (acc.deploy) { + await this.deployAccount(`Account ${accIndex + 1}`, acc.feeToken) + } + } + + async setupWallet({ + accountsToSetup, + email, + pin = "111111", + success = true, + }: { + accountsToSetup: AccountsToSetup[] + email?: string + success?: boolean + pin?: string + }) { + await this.wallet.newWalletOnboarding(email, pin, success) + if (!success) { + return { accountAddresses: [], seed: "" } + } + await this.open() + const seed = await this.account.setupRecovery() + //await this.network.selectDefaultNetwork() + const noAccount = await this.account.noAccountBanner.isVisible({ + timeout: 1000, + }) + const accountAddresses: string[] = [] + for (const [accIndex, acc] of accountsToSetup.entries()) { + if (noAccount) { + await this.account.addAccount({ firstAccount: true }) + } else if (accIndex !== 0) { + await this.account.addAccount({ firstAccount: false }) + } + await this.account.copyAddress.click() + await this.clipboard.setClipboard() + const accountAddress = await this.clipboard + .getClipboard() + .then((adr) => String(adr)) + expect(accountAddress).toMatch(/^0x0/) + accountAddresses.push(accountAddress) + if (acc.assets[0].balance > 0) { + await this.fundAccount(acc, accountAddress, accIndex) + } + } + logInfo({ + op: "setupWallet", + accountsNbr: accountAddresses.length, + accountAddresses, + seed, + }) + return { accountAddresses, seed } + } + + async validateTx({ + txHash, + receiver, + sendAmountFE, + sendAmountTX, + uniqLocator, + txType = "token", + }: { + txHash: string + receiver: string + sendAmountFE?: string + sendAmountTX?: number + uniqLocator?: boolean + txType?: "token" | "nft" + }) { + logInfo({ + op: "validateTx", + txHash, + receiver, + sendAmountFE, + sendAmountTX, + uniqLocator, + }) + await this.navigation.menuActivityActiveLocator + .isVisible() + .then(async (visible: boolean) => { + if (!visible) { + await this.navigation.menuActivityLocator.click() + } + }) + if (sendAmountFE) { + const activityAmountLocator = this.page.locator( + `button[data-tx-hash$="${txHash.substring(3)}"] [data-value]`, + ) + let activityAmountElement = activityAmountLocator + if (uniqLocator) { + activityAmountElement = activityAmountLocator.first() + } + expect(this.activity.historyButton) + .toBeVisible({ timeout: 1000 }) + .then(async () => { + await this.activity.historyButton.click() + }) + .catch(async () => { + return null + }) + const activityAmount = await activityAmountElement + .textContent() + .then((text) => text?.match(/[\d|.]+/)![0]) + if (sendAmountFE.toString().length > 6) { + expect(activityAmount).toBe( + parseFloat(sendAmountFE.toString()) + .toFixed(4) + .toString() + .match(/[\d\\.]+[^0]+/)?.[0], + ) + } else { + expect(activityAmount).toBe( + parseFloat(sendAmountFE.toString()).toString(), + ) + } + } + await this.activity.ensureNoPendingTransactions() + await validateTx({ txHash, receiver, amount: sendAmountTX, txType }) + } + + async fundMultisigAccount({ + accountName, + balance, + }: { + accountName: string + balance: number + }) { + await this.account.ensureSelectedAccount(accountName) + await this.account.copyAddress.click() + await this.clipboard.setClipboard() + const accountAddress = await this.clipboard + .getClipboard() + .then((adr) => String(adr)) + await transferTokens( + balance, + accountAddress, // receiver wallet address + ) + await this.account.ensureAsset(accountName, "ETH", `${balance} ETH`) + } + + async activateMultisig(accountName: string) { + await this.account.ensureSelectedAccount(accountName) + await expect( + this.page.locator("label:has-text('Add ETH or STRK and activate')"), + ).toBeVisible() + await this.page.locator('[data-testid="activate-multisig"]').click() + await this.account.confirmTransaction() + await expect( + this.page.locator('[data-testid="activating-multisig"]'), + ).toBeVisible() + await Promise.all([ + expect( + this.page.locator("label:has-text('Add ETH or STRK and activate')"), + ).toBeHidden(), + expect( + this.page.locator('[data-testid="activating-multisig"]'), + ).toBeHidden(), + ]) + } + + async removeMultisigOwner(accountName: string) { + await this.account.ensureSelectedAccount(accountName) + await this.navigation.showSettingsLocator.click() + await this.settings.account(accountName).click() + } +} diff --git a/e2e/src/argent-x/page-objects/Messages.ts b/e2e/src/argent-x/page-objects/Messages.ts new file mode 100644 index 0000000..9e940bc --- /dev/null +++ b/e2e/src/argent-x/page-objects/Messages.ts @@ -0,0 +1,17 @@ +import type { Page } from "@playwright/test" + +export default class Messages { + constructor(private page: Page) {} + + sendMessage = (message: any) => + this.page.evaluate(`window.sendMessage(${JSON.stringify(message)})`) + waitForMessage = (message: string) => + this.page.evaluate(`window.waitForMessage(${JSON.stringify(message)})`) + + resetExtension() { + return Promise.all([ + this.sendMessage({ type: "RESET_ALL" }), + this.waitForMessage("DISCONNECT_ACCOUNT"), + ]) + } +} diff --git a/e2e/src/argent-x/page-objects/Navigation.ts b/e2e/src/argent-x/page-objects/Navigation.ts new file mode 100644 index 0000000..3be34e1 --- /dev/null +++ b/e2e/src/argent-x/page-objects/Navigation.ts @@ -0,0 +1,140 @@ +import type { Page } from "@playwright/test" + +import { lang } from "../languages" +import Clipboard from "../utils/Clipboard" + +export default class Navigation extends Clipboard { + constructor(page: Page) { + super(page) + } + + get backLocator() { + return this.page.getByLabel(`${lang.common.back}`).first() + } + + get closeLocator() { + return this.page.locator(`[aria-label="${lang.common.close}"]`) + } + + get closeButtonLocator() { + return this.page.getByLabel("close") + } + + get closeButtonDappInfoLocator() { + return this.page.getByTestId("close-button") + } + + get confirmLocator() { + return this.page.locator(`button:text-is("${lang.common.confirm}")`) + } + + get nextLocator() { + return this.page.locator(`button:text-is("${lang.common.next}")`) + } + + get reviewSendLocator() { + return this.page.locator(`button:text-is("${lang.common.reviewSend}")`) + } + + get doneLocator() { + return this.page.locator(`button:text-is("${lang.common.done}")`) + } + + get continueLocator() { + return this.page + .locator(`button:text-is("${lang.common.continue}")`) + .first() + } + + get yesLocator() { + return this.page.locator(`button:text-is("${lang.common.yes}")`) + } + + get noLocator() { + return this.page.locator(`button:text-is("${lang.common.no}")`) + } + + get unlockLocator() { + return this.page.locator(`button:text-is("${lang.common.unlock}")`).first() + } + + get showSettingsLocator() { + return this.page.locator('[aria-label="Show settings"]') + } + + get lockWalletLocator() { + return this.page.locator( + `//button//*[text()="${lang.settings.lockWallet}"]`, + ) + } + + get resetLocator() { + return this.page.getByText("Reset").first() + } + + get confirmResetLocator() { + return this.page.locator(`button:text-is("${lang.common.confirmReset}")`) + } + + get menuPendingTransactionsIndicatorLocator() { + return this.page.locator('[aria-label="Pending transactions"]') + } + + get menuTokensLocator() { + return this.page.locator('[aria-label="Tokens"]') + } + + get menuNTFsLocator() { + return this.page.locator('[aria-label="NFTs"]') + } + + get menuSwapsLocator() { + return this.page.locator('[aria-label="Swap"]') + } + + get menuActivityLocator() { + return this.page.locator('[aria-label="Activity"]') + } + + get menuActivityActiveLocator() { + return this.page.locator('[aria-label="Activity"][class*="active"]') + } + + get saveLocator() { + return this.page.locator(`button:text-is("${lang.common.save}")`) + } + + get createLocator() { + return this.page.locator(`button:text-is("${lang.common.create}")`) + } + + get cancelLocator() { + return this.page.locator(`button:text-is("${lang.common.cancel}")`) + } + + get approveLocator() { + return this.page.locator(`button:text-is("${lang.common.approve}")`) + } + + get addArgentShieldLocator() { + return this.page.locator(`button:text-is("${lang.common.addArgentShield}")`) + } + + get confirmChangeAccountTypeLocator() { + return this.page.locator( + `button:text-is("${lang.common.changeAccountType}")`, + ) + } + + get dismissLocator() { + return this.page.locator(`button:text-is("${lang.common.dismiss}")`) + } + + get removeLocator() { + return this.page.locator(`button:text-is("${lang.common.remove}")`) + } + + get upgradeLocator() { + return this.page.locator(`button:text-is("${lang.common.upgrade}")`) + } +} diff --git a/e2e/src/argent-x/page-objects/Network.ts b/e2e/src/argent-x/page-objects/Network.ts new file mode 100644 index 0000000..a5ba2ce --- /dev/null +++ b/e2e/src/argent-x/page-objects/Network.ts @@ -0,0 +1,91 @@ +import { Page, expect } from "@playwright/test" +import Navigation from "./Navigation" + +type NetworkName = "Devnet" | "Sepolia" | "Mainnet" | "My Network" + +export function getDefaultNetwork() { + const argentXEnv = process.env.ARGENT_X_ENVIRONMENT + + if (!argentXEnv) { + throw new Error("ARGENT_X_ENVIRONMENT not set") + } + let defaultNetworkId: string + switch (argentXEnv.toLowerCase()) { + case "prod": + case "staging": + defaultNetworkId = "mainnet-alpha" + break + + case "hydrogen": + case "test": + defaultNetworkId = "sepolia-alpha" + break + + default: + throw new Error(`Unknown ARGENTX_ENVIRONMENT: ${argentXEnv}`) + } + + return defaultNetworkId +} +export default class Network extends Navigation { + // Change 'private' to 'protected' or 'public' to match the base class + constructor(page: Page) { + super(page) + } + + get networkSelector() { + return this.page.getByLabel("Show account list") + } + + networkOption(name: string) { + return this.page.locator(`button[role="menuitem"] span:text-is("${name}")`) + } + + async selectNetwork(networkName: NetworkName) { + await this.networkSelector.click() + await this.page.locator('[data-testid="network-switcher-button"]').click() + await this.networkOption(networkName).click() + } + + async selectDefaultNetwork() { + const networkName = this.getDefaultNetworkName() + await this.networkSelector.click() + await this.page.locator('[data-testid="network-switcher-button"]').click() + await this.networkOption(networkName).click() + const accounts = await this.page + .locator('[aria-label^="Select A"]') + .allInnerTexts() + if (accounts.length > 0) { + await this.page.locator('[aria-label^="Select A"]').first().click() + } else { + await this.closeButtonLocator.click() + } + } + + async ensureAvailableNetworks(networks: string[]) { + await this.networkSelector.click() + await this.page.locator('[data-testid="network-switcher-button"]').click() + const availableNetworks = await this.page + .locator('[role="menu"] button') + .allInnerTexts() + return expect(availableNetworks).toEqual(networks) + } + + getDefaultNetworkName() { + const defaultNetworkId = getDefaultNetwork() + switch (defaultNetworkId.toLowerCase()) { + case "mainnet-alpha": + return "Mainnet" + case "sepolia-alpha": + return "Sepolia" + case "goerli-alpha": + return "Goerli" + default: + throw new Error(`Unknown ARGENTX_Network: ${defaultNetworkId}`) + } + } + + ensureSelectedNetwork(networkName: NetworkName) { + return expect(this.networkSelector).toContainText(networkName) + } +} diff --git a/e2e/src/argent-x/page-objects/Nfts.ts b/e2e/src/argent-x/page-objects/Nfts.ts new file mode 100644 index 0000000..15a56cc --- /dev/null +++ b/e2e/src/argent-x/page-objects/Nfts.ts @@ -0,0 +1,21 @@ +import { Page } from "@playwright/test" + +import Navigation from "./Navigation" + +export default class Nfts extends Navigation { + constructor(page: Page) { + super(page) + } + + collection(name: string) { + return this.page.locator(`h5:text-is("${name}")`) + } + + ntf(name: string) { + return this.page.getByRole("group", { name }).getByRole("img") + } + + nftByPosition(position: number = 0) { + return this.page.locator('[data-testid="nft-item-name"]').nth(position) + } +} diff --git a/e2e/src/argent-x/page-objects/Preferences.ts b/e2e/src/argent-x/page-objects/Preferences.ts new file mode 100644 index 0000000..96330d2 --- /dev/null +++ b/e2e/src/argent-x/page-objects/Preferences.ts @@ -0,0 +1,40 @@ +import { Page } from "@playwright/test" + +import { lang } from "../languages" +import Navigation from "./Navigation" + +export default class Preferences extends Navigation { + constructor(page: Page) { + super(page) + } + + get hiddenAndSpamTokens() { + return this.page.locator( + `//p[contains(text(),'${lang.settings.preferences.hideTokens}')]`, + ) + } + + get hideTokensStatus() { + return this.page.locator( + `//p[contains(text(),'${lang.settings.preferences.hideTokens}')]/following::input`, + ) + } + + get defaultBlockExplorer() { + return this.page.locator( + `//p[contains(text(),'${lang.settings.preferences.defaultBlockExplorer}')]`, + ) + } + + get defaultNFTMarket() { + return this.page.locator( + `//p[contains(text(),'${lang.settings.preferences.defaultNFTMarket}')]`, + ) + } + + get emailNotifications() { + return this.page.locator( + `//p[contains(text(),'${lang.settings.preferences.emailNotifications}')]`, + ) + } +} diff --git a/e2e/src/argent-x/page-objects/Settings.ts b/e2e/src/argent-x/page-objects/Settings.ts new file mode 100644 index 0000000..eaa9ef5 --- /dev/null +++ b/e2e/src/argent-x/page-objects/Settings.ts @@ -0,0 +1,142 @@ +import { expect, type Page } from "@playwright/test" + +import { lang } from "../languages" +import { sleep } from "../utils" +import Navigation from "./Navigation" + +export default class Settings extends Navigation { + constructor(page: Page) { + super(page) + } + + get extendedView() { + return this.page.locator(`[aria-label="${lang.settings.extendedView}"]`) + } + + get addressBook() { + return this.page.locator( + `//a//*[text()="${lang.settings.addressBook.addressBook}"]`, + ) + } + + get authorizedDapps() { + return this.page.locator( + `//a//*[text()="${lang.settings.account.authorisedDapps.authorisedDapps}"]`, + ) + } + + get advancedSettings() { + return this.page.locator( + `//a//*[text()="${lang.settings.advancedSettings.advancedSettings}"]`, + ) + } + + get preferences() { + return this.page.locator( + `//a//*[text()="${lang.settings.preferences.preferences}"]`, + ) + } + + // account settings + get accountName() { + return this.page.locator('input[placeholder="Account name"]') + } + + get exportPrivateKey() { + return this.page.getByRole("button", { name: "Export private key" }) + } + + get deployAccount() { + return this.page.locator( + `//button//*[text()="${lang.settings.account.deployAccount}"]`, + ) + } + + get hideAccount() { + return this.page.getByRole("button", { name: "Hide account" }) + } + + account(accountName: string) { + return this.page.locator(`[aria-label="Select ${accountName}"]`) + } + + async setAccountName(newAccountName: string) { + await this.accountName.click() + await this.accountName.fill(newAccountName) + await this.page.locator("form button").click() + } + + get confirmHide() { + return this.page.locator(`button:text-is("${lang.common.hide}")`) + } + get hiddenAccounts() { + return this.page.locator( + `p:text-is("${lang.settings.preferences.hiddenAccounts}")`, + ) + } + + unhideAccount(accountName: string) { + return this.page.locator(`button :text-is("${accountName}")`) + } + + get smartAccountButton() { + return this.page.locator('[data-testid="smart-account-button"]') + } + + get changeToStandardAccountButton() { + return this.page.locator( + '[data-testid="smart-account-button"]:has-text("Change to Standard Account")', + ) + } + + get privateKey() { + return this.page.locator('[aria-label="Private key"]') + } + + get copy() { + return this.page.locator(`button:text-is("${lang.common.copy}")`) + } + + get help() { + return this.page.getByRole("link", { name: "Help" }) + } + + get discord() { + return this.page.getByRole("link", { name: "Discord" }) + } + + get github() { + return this.page.getByRole("link", { name: "GitHub" }) + } + + get viewOnStarkScanLocator() { + return this.page.getByRole("button", { + name: lang.settings.account.viewOnStarkScan, + }) + } + + get viewOnVoyagerLocator() { + return this.page.getByRole("button", { + name: lang.settings.account.viewOnVoyager, + }) + } + + get pinLocator() { + return this.page.locator('[aria-label="Please enter your pin code"]') + } + + async signIn(email: string, pin: string = "111111") { + await this.page.getByRole("button", { name: "Sign in to Argent" }).click() + await this.page.getByTestId("email-input").fill(email) + await this.nextLocator.click() + //avoid BE error PIN not requested + await sleep(2000) + await expect(this.pinLocator).toHaveCount(6) + await this.pinLocator.first().click() + await this.pinLocator.first().fill(pin) + await expect( + this.page.getByRole("button", { name: "Logout" }), + ).toBeVisible() + await this.closeLocator.click() + } +} diff --git a/e2e/src/argent-x/page-objects/Swap.ts b/e2e/src/argent-x/page-objects/Swap.ts new file mode 100644 index 0000000..7065f28 --- /dev/null +++ b/e2e/src/argent-x/page-objects/Swap.ts @@ -0,0 +1,95 @@ +import { Page, expect } from "@playwright/test" + +import Navigation from "./Navigation" +import { TokenSymbol } from "../utils" + +export default class Swap extends Navigation { + constructor(page: Page) { + super(page) + } + + get swapHeader() { + return this.page.getByRole("heading", { name: "Swap" }) + } + + get valueLoc() { + return this.page.locator('[data-testid="swap-input-pay-panel"]') + } + + get switchInOutLoc() { + return this.page.locator('[aria-label="Switch input and output"]') + } + + get maxLoc() { + return this.page.locator('label:has-text("Max")') + } + + get payTokenLoc() { + return this.page.locator('[data-testid="swap-token-button"]').nth(0) + } + + get receiveTokenLoc() { + return this.page.locator('[data-testid="swap-token-button"]').nth(1) + } + + get reviewSwapLoc() { + return this.page.locator('[data-testid="review-swap-button"]') + } + + get deployFeeLoc() { + return this.page.locator('[data-testid="deploy-fee"]') + } + + get useMaxLoc() { + return this.page.locator('[data-testid="use-max-button"]') + } + + async setPayToken(token: string) { + await this.payTokenLoc.click() + await this.page.locator(`p:text-is("${token}")`).click() + } + + async setReceiveToken(token: string) { + await this.receiveTokenLoc.click() + await this.page.locator(`p:text-is("${token}")`).click() + } + + async swapTokens({ + payToken, + receiveToken, + amount, + alreadyDeployed = true, + }: { + payToken: TokenSymbol + receiveToken: TokenSymbol + amount: number | "MAX" + alreadyDeployed: boolean + }) { + await this.setPayToken(payToken) + await this.setReceiveToken(receiveToken) + if (amount === "MAX") { + await this.maxLoc.click() + await this.useMaxLoc.click() + } else { + await this.valueLoc.fill(amount.toString()) + } + await this.reviewSwapLoc.click() + //raise an error if Transaction fail predict, to avoid waiting test timeout + const failPredict = this.page.getByText("Transaction fail") + await expect(failPredict) + .toBeVisible({ timeout: 1000 * 5 }) + .then(async (_) => { + throw new Error("Transaction failure predicted") + }) + .catch((_) => null) + if (!alreadyDeployed) { + await expect(this.deployFeeLoc).toBeVisible() + } + const sendAmountFEText = await this.page + .locator("[data-fe-value]") + .nth(1) + .getAttribute("data-fe-value") + await this.confirmLocator.click() + return sendAmountFEText + } +} diff --git a/e2e/src/argent-x/page-objects/TokenDetails.ts b/e2e/src/argent-x/page-objects/TokenDetails.ts new file mode 100644 index 0000000..8b96aff --- /dev/null +++ b/e2e/src/argent-x/page-objects/TokenDetails.ts @@ -0,0 +1,94 @@ +import { expect, Page } from "@playwright/test" + +import Navigation from "./Navigation" + +export default class TokenDetails extends Navigation { + constructor(page: Page) { + super(page) + } + + openTokenDetails(token: string) { + return this.page.getByTestId(`${token}-balance`) + } + + get swapButtonLoc() { + return this.page.locator('button[aria-label="Swap"]') + } + + get buyButtonLoc() { + return this.page.locator('button[aria-label="Buy"]') + } + + get sendButtonLoc() { + return this.page.locator('button[aria-label="Send"]') + } + + graphTimeFrameLoc(frame: "1D" | "1W" | "1M" | "1Y" | "All") { + return this.page.locator(`button:text-is('${frame}')`) + } + + get activityButtonLoc() { + return this.page.locator(`button:text-is('Activity')`) + } + + get aboutButtonLoc() { + return this.page.locator(`button:text-is('About')`) + } + + get menuButtonLoc() { + return this.page.locator('[id^="menu-button"]') + } + + get menuCopyTokenAddressLoc() { + return this.page.getByText("Copy token address") + } + + get menuViewOnVoyagerLoc() { + return this.page.getByRole("menuitem", { name: "View on Voyager" }) + } + + get newTokenButtonLoc() { + return this.page.getByText("New token") + } + + get addTokenButtonLoc() { + return this.page.getByRole("button", { name: "Add token" }) + } + + fillTokenAddress(tokenAddress: string) { + return this.page.locator("[name='address']").fill(tokenAddress) + } + + async addNewToken(tokenAddress: string, tokenSymbol: string) { + await this.newTokenButtonLoc.click() + await this.fillTokenAddress(tokenAddress) + await expect(this.page.locator('[name="symbol"]')).toHaveValue(tokenSymbol) + await Promise.race([ + this.addTokenButtonLoc.click(), + this.addThisToken.click(), + ]) + } + + token(tokenName: string) { + return this.page.locator(`h5:text-is('${tokenName}')`) + } + + showToken(tokenSymbol: string) { + return this.page.locator(`[data-testid="show-token-button-${tokenSymbol}"]`) + } + + hideToken(tokenSymbol: string) { + return this.page.locator(`[data-testid="hide-token-button-${tokenSymbol}"]`) + } + + get spamTokensList() { + return this.page.getByRole("button", { name: "Spam" }) + } + + get tokensList() { + return this.page.getByRole("button", { name: "Tokens" }) + } + get addThisToken() { + return this.page.getByRole("button", { name: "Add this token" }) + } +} diff --git a/e2e/src/argent-x/page-objects/Wallet.ts b/e2e/src/argent-x/page-objects/Wallet.ts new file mode 100644 index 0000000..d7ddb6f --- /dev/null +++ b/e2e/src/argent-x/page-objects/Wallet.ts @@ -0,0 +1,152 @@ +import { Page, expect } from "@playwright/test" + +import config from "../../../config" +import { lang } from "../languages" +import Navigation from "./Navigation" +import { sleep } from "../utils" + +export default class Wallet extends Navigation { + upgradeTest: boolean + constructor(page: Page, upgradeTest: boolean = false) { + super(page) + this.upgradeTest = upgradeTest + } + get banner() { + return this.page.locator(`div h1:text-is("${lang.wallet.banner1}")`) + } + get description() { + return this.page.locator(`div p:text-is("${lang.wallet.desc1}")`) + } + get createNewWallet() { + return this.page.locator(`button:text-is("${lang.wallet.createButton}")`) + } + get restoreExistingWallet() { + return this.page.locator(`button:text-is("${lang.wallet.restoreButton}")`) + } + + //second screen + get banner2() { + return this.page.locator(`div h1:text-is("${lang.wallet.banner2}")`) + } + get description2() { + return this.page.locator(`div p:text-is("${lang.wallet.desc2}")`) + } + + get disclaimerLostOfFunds() { + return this.page.locator( + `//input[@value="lossOfFunds"]/following::p[contains(text(),'${lang.wallet.lossOfFunds}')]`, + ) + } + get disclaimerAlphaVersion() { + return this.page.locator( + `//input[@value="alphaVersion"]/following::p[contains(text(),'${lang.wallet.alphaVersion}')]`, + ) + } + + get privacyPolicyLink() { + return this.page.getByRole("link", { name: "Privacy Policy" }) + } + + //third screen + get banner3() { + return this.page.locator(`div h1:text-is("${lang.wallet.banner3}")`) + } + get description3() { + return this.page.locator(`div p:text-is("${lang.wallet.desc3}")`) + } + get password() { + return this.page.locator( + `input[name="password"][placeholder="${lang.wallet.password}"]`, + ) + } + get repeatPassword() { + return this.page.locator( + `input[name="repeatPassword"][placeholder="${lang.wallet.repeatPassword}"]`, + ) + } + get createWallet() { + return this.page.locator(`button:text-is("${lang.wallet.createWallet}")`) + } + + //fourth screen + get banner4() { + return this.page.locator("div h1", { + hasText: lang.wallet.banner4, + }) + } + + get download() { + return this.page.locator(`a:has-text("${lang.wallet.download}")`) + } + + get twitter() { + return this.page.locator(`a:has-text("${lang.wallet.twitter}")`) + } + + get dapps() { + return this.page.locator(`a:has-text("${lang.wallet.dapps}")`) + } + + get finish() { + return this.page.locator(`button:text-is("${lang.wallet.finish}")`) + } + + get agreeLoc() { + return this.page.locator('[data-testid="agree-button"]') + } + + get addStandardAccountFromNewAccountScreen() { + return this.page.locator('[aria-label="Standard Account"]') + } + + get addSmartAccountFromNewAccountScreen() { + return this.page.locator('[aria-label="Smart Account"]') + } + + get pinLocator() { + return this.page.locator('[aria-label="Please enter your pin code"]') + } + + fillEmail(email: string) { + return this.page.locator('[data-testid="email-input"]').fill(email) + } + + async fillPin(pin: string) { + //avoid BE error PIN not requested + await sleep(2000) + await expect(this.pinLocator).toHaveCount(6) + await this.pinLocator.first().click() + await this.pinLocator.first().fill(pin) + } + async newWalletOnboarding( + email?: string, + pin: string = "111111", + success: boolean = true, + ) { + await this.createNewWallet.click() + await this.agreeLoc.click() + await this.password.fill(config.password) + await this.repeatPassword.fill(config.password) + await this.continueLocator.click() + if (!email) { + await this.addStandardAccountFromNewAccountScreen.click() + await this.continueLocator.click() + } else { + await this.addSmartAccountFromNewAccountScreen.click() + await this.continueLocator.click() + await this.fillEmail(email) + await this.continueLocator.click() + await this.fillPin(pin) + if (!success) { + await expect( + this.page.getByText(lang.account.argentShield.emailInUse), + ).toBeVisible() + } + } + if (success) { + await expect( + this.page.getByRole("heading", { name: "Your wallet is ready!" }), + ).toBeVisible() + } + } +} diff --git a/e2e/src/argent-x/specs/dapps.spec.ts b/e2e/src/argent-x/specs/dapps.spec.ts new file mode 100644 index 0000000..e9de1dd --- /dev/null +++ b/e2e/src/argent-x/specs/dapps.spec.ts @@ -0,0 +1,46 @@ +import { expect } from "@playwright/test" + +import test from "../test" +import { downloadGitHubRelease, unzip } from "../utils" +import config from "../../../config" + +test.describe("Dapps", () => { + test.beforeAll(async ({ }) => { + const version = await downloadGitHubRelease() + const currentVersionDir = await unzip(version) + config.distDir = currentVersionDir + }) + for (const useStarknetKitModal of [true, false] as const) { + + test(`connect from testDapp using starknetKitModal ${useStarknetKitModal}`, async ({ extension, browserContext }) => { + + //setup wallet + await extension.wallet.newWalletOnboarding() + await extension.open() + await extension.dapps.requestConnectionFromDapp( + { + browserContext, + useStarknetKitModal + } + ) + //accept connection from Argent X + await extension.dapps.accept.click() + //check connect dapps + await extension.navigation.showSettingsLocator.click() + await extension.settings.account(extension.account.accountName1).click() + await extension.page.getByRole('button', { name: 'Connected dapps' }).click() + await expect( + extension.dapps.connected(), + ).toBeVisible() + //disconnect dapp from Argent X + await extension.dapps + .disconnect() + .click() + await expect( + extension.dapps.connected(), + ).toBeHidden() + await extension.page.getByRole('button', { name: 'Connected dapps' }).click() + await expect(extension.page.getByRole('heading', { name: 'No connected dapps' })).toBeVisible() + }) + } +}) diff --git a/e2e/src/argent-x/test.ts b/e2e/src/argent-x/test.ts new file mode 100644 index 0000000..d4f2c83 --- /dev/null +++ b/e2e/src/argent-x/test.ts @@ -0,0 +1,183 @@ +import { + ChromiumBrowserContext, + Page, + TestInfo, + chromium, + test as testBase, +} from "@playwright/test" +import { v4 as uuid } from "uuid" +import type { TestExtensions } from "./fixtures" +import ExtensionPage from "./page-objects/ExtensionPage" +import config from "../../config" +import { logInfo } from "./utils" +import path from "path" +import fs from "fs-extra" + +declare global { + interface Window { + PLAYWRIGHT?: boolean + } +} +const outputFolder = (testInfo: TestInfo) => + testInfo.title.replace(/\s+/g, "_").replace(/\W/g, "") +const artifactFilename = (testInfo: TestInfo, label: string) => + `${testInfo.retry}-${testInfo.status}-${label}-${testInfo.workerIndex}` +const isKeepArtifacts = (testInfo: TestInfo) => + testInfo.config.preserveOutput === "always" || + (testInfo.config.preserveOutput === "failures-only" && + testInfo.status === "failed") || + testInfo.status === "timedOut" + +const artifactSetup = async (testInfo: TestInfo, label: string) => { + await fs.promises + .mkdir(path.resolve(config.artifactsDir, outputFolder(testInfo)), { + recursive: true, + }) + .catch((error) => { + console.error({ op: "artifactSetup", error }) + }) + return artifactFilename(testInfo, label) +} + +const saveHtml = async (testInfo: TestInfo, page: Page, label: string) => { + logInfo({ + op: "saveHtml", + label, + }) + const fileName = await artifactSetup(testInfo, label) + const htmlContent = await page.content() + await fs.promises + .writeFile( + path.resolve( + config.artifactsDir, + outputFolder(testInfo), + `${fileName}.html`, + ), + htmlContent, + ) + .catch((error) => { + console.error({ op: "saveHtml", error }) + }) +} + +const keepVideos = async (testInfo: TestInfo, page: Page, label: string) => { + logInfo({ + op: "keepVideos", + label, + }) + const fileName = await artifactSetup(testInfo, label) + await page + .video() + ?.saveAs( + path.resolve( + config.artifactsDir, + outputFolder(testInfo), + `${fileName}.webm`, + ), + ) + .catch((error) => { + console.error({ op: "keepVideos", error }) + }) +} + +const isExtensionURL = (url: string) => url.startsWith("chrome-extension://") +let browserCtx: ChromiumBrowserContext +const closePages = async (browserContext: ChromiumBrowserContext) => { + const pages = browserContext?.pages() || [] + for (const page of pages) { + if (!isExtensionURL(page.url())) { + await page.close() + } + } +} + +const createBrowserContext = async (userDataDir: string, buildDir: string) => { + const context = await chromium.launchPersistentContext(userDataDir, { + headless: false, + args: [ + "--disable-dev-shm-usage", + "--ipc=host", + `--disable-extensions-except=${buildDir}`, + `--load-extension=${buildDir}`, + ], + viewport: config.viewportSize, + ignoreDefaultArgs: ["--disable-component-extensions-with-background-pages"], + recordVideo: { + dir: config.artifactsDir, + size: config.viewportSize, + }, + }) + await context.addInitScript(() => { + window.PLAYWRIGHT = true + window.localStorage.setItem( + "seenNetworkStatusState", + JSON.stringify({ state: { lastSeen: Date.now() }, version: 0 }), + ) + window.localStorage.setItem("onboardingExperiment", "E1A1") + }) + return context +} + +const initBrowserWithExtension = async ( + userDataDir: string, + buildDir: string, +) => { + const browserContext = await createBrowserContext(userDataDir, buildDir) + const page = await browserContext.newPage() + + await page.bringToFront() + await page.goto("chrome://extensions") + await page.locator('[id="devMode"]').click() + const extensionId = await page + .locator('[id="extension-id"]') + .first() + .textContent() + .then((text) => text?.replace("ID: ", "")) + + const extensionURL = `chrome-extension://${extensionId}/index.html` + await page.goto(extensionURL) + await page.waitForTimeout(500) + + await page.emulateMedia({ reducedMotion: "reduce" }) + return { browserContext, extensionURL, page } +} + +function createExtension(label: string, upgrade: boolean = false) { + return async ({ }, use: any, testInfo: TestInfo) => { + const userDataDir = `/tmp/test-user-data-${uuid()}` + let buildDir = config.distDir + if (upgrade) { + fs.copy(buildDir, config.migVersionDir) + buildDir = config.migVersionDir + } + const { browserContext, page, extensionURL } = + await initBrowserWithExtension(userDataDir, buildDir) + process.env.workerIndex = testInfo.workerIndex.toString() + const extension = new ExtensionPage(page, extensionURL, upgrade) + await closePages(browserContext) + browserCtx = browserContext + await use(extension) + + if (isKeepArtifacts(testInfo)) { + await saveHtml(testInfo, page, label) + await keepVideos(testInfo, page, label) + } + await browserContext.close() + } +} + +function getContext() { + return async ({ }, use: any, _testInfo: TestInfo) => { + await use(browserCtx) + } +} + +const test = testBase.extend({ + extension: createExtension("extension"), + secondExtension: createExtension("secondExtension"), + thirdExtension: createExtension("thirdExtension"), + browserContext: getContext(), + upgradeExtension: createExtension("upgradeExtension", true), +}) + +export default test diff --git a/e2e/src/argent-x/utils/Clipboard.ts b/e2e/src/argent-x/utils/Clipboard.ts new file mode 100644 index 0000000..e85290e --- /dev/null +++ b/e2e/src/argent-x/utils/Clipboard.ts @@ -0,0 +1,46 @@ +import type { Page } from "@playwright/test" + +export default class Clipboard { + page: Page + private static clipboards: Map = new Map() + private readonly workerIndex: number + + constructor(page: Page) { + this.page = page + this.workerIndex = Number(process.env.workerIndex) + } + + async setClipboard(): Promise { + const text = String( + await this.page.evaluate(`navigator.clipboard.readText()`), + ) + Clipboard.clipboards.set(this.workerIndex, text) + } + + async setClipboardText(text: string): Promise { + Clipboard.clipboards.set(this.workerIndex, text) + } + + async getClipboard(): Promise { + return Clipboard.clipboards.get(this.workerIndex) || "" + } + + async paste(): Promise { + const content = Clipboard.clipboards.get(this.workerIndex) || "" + await this.page.evaluate( + (text) => navigator.clipboard.writeText(text), + content, + ) + const key = process.platform === "darwin" ? "Meta" : "Control" + await this.page.keyboard.press(`${key}+v`) + } + + async clear(): Promise { + Clipboard.clipboards.delete(this.workerIndex) + } + + // Optional: method to clear all clipboards + static clearAll(): void { + Clipboard.clipboards.clear() + } +} diff --git a/e2e/src/argent-x/utils/downloadGitHubRelease.ts b/e2e/src/argent-x/utils/downloadGitHubRelease.ts new file mode 100644 index 0000000..41cdf41 --- /dev/null +++ b/e2e/src/argent-x/utils/downloadGitHubRelease.ts @@ -0,0 +1,102 @@ +import axios from "axios" +import * as fs from "fs" +import * as path from "path" +import { pipeline } from "stream" +import { promisify } from "util" +import config from "../../../config" + +const owner = config.migRepoOwner +const repo = config.migRepo +const streamPipeline = promisify(pipeline) + +async function getLatestReleaseTag(): Promise { + console.log(`https://api.github.com/repos/${owner}/${repo}/releases/latest`) + + try { + // First try to get the latest release + const response = await axios.get( + `https://api.github.com/repos/${owner}/${repo}/releases/latest`, + { + headers: { + 'User-Agent': 'GitHub Release Checker' + } + } + ); + return response.data.tag_name; + } catch (error: any) { + if (error.response && error.response.status === 404) { + // If no releases found, try getting tags instead + const tagsResponse = await axios.get( + `https://api.github.com/repos/${owner}/${repo}/tags`, + { + headers: { + 'User-Agent': 'GitHub Release Checker' + } + } + ); + + if (tagsResponse.data && tagsResponse.data.length > 0) { + return tagsResponse.data[0].name; + } + throw new Error('No releases or tags found for this repository'); + } + throw error; + } +} + + +export async function downloadGitHubRelease(): Promise { + const tag = await getLatestReleaseTag(); + const version = tag.replace("v", "") + const assetName = config.migReleaseName + const token = config.migRepoToken + const outputPath = `${config.migDir}${version}.zip` + try { + // Get release by tag name + const releaseResponse = await axios.get( + `https://api.github.com/repos/${owner}/${repo}/releases/tags/${tag}`, + { + headers: { + Authorization: `token ${token}`, + Accept: "application/vnd.github.v3+json", + "User-Agent": "Node.js", + }, + }, + ) + + const releaseData = releaseResponse.data + + // Find the asset by name + const asset = releaseData.assets.find((a: any) => a.name === assetName) + + if (!asset) { + throw new Error(`Asset ${assetName} not found in release ${tag}`) + } + + const assetUrl = asset.url + + // Download the asset + const assetResponse = await axios.get(assetUrl, { + headers: { + Authorization: `token ${token}`, + Accept: "application/octet-stream", + "User-Agent": "Node.js", + }, + responseType: "stream", // Important for streaming the response + }) + + // Ensure the output directory exists + const dir = path.dirname(outputPath) + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }) + } + + // Write the file + await streamPipeline(assetResponse.data, fs.createWriteStream(outputPath)) + + console.log(`Asset downloaded to ${outputPath}`) + } catch (error: any) { + console.error(`Error: ${error.message}`) + } + return version +} diff --git a/e2e/src/argent-x/utils/getBranchVersion.sh b/e2e/src/argent-x/utils/getBranchVersion.sh new file mode 100755 index 0000000..1bc0956 --- /dev/null +++ b/e2e/src/argent-x/utils/getBranchVersion.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Enable command printing +#set -x + +# Function to run a command and check its exit status +run_command() { + output=$("$@") + local status=$? + if [ $status -ne 0 ]; then + echo "Error: Command '$*' failed with exit status $status" >&2 + exit 1 + fi + echo "$output" +} + +# Extract the version +VERSION=$(run_command grep -m1 '"version":' ../extension/dist/manifest.json | awk -F: '{ print $2 }' | sed 's/[", ]//g') + +# Print the version +echo "$VERSION" + +# Return the version as the script's output +exit 0 \ No newline at end of file diff --git a/e2e/src/argent-x/utils/getBranchVersion.ts b/e2e/src/argent-x/utils/getBranchVersion.ts new file mode 100644 index 0000000..32a47a5 --- /dev/null +++ b/e2e/src/argent-x/utils/getBranchVersion.ts @@ -0,0 +1,22 @@ +import { execSync } from "child_process" +import * as path from "path" +import * as fs from "fs" + +export const getBranchVersion = (): string => { + const scriptPath = path.join(__dirname, "getBranchVersion.sh") + + try { + // Make the script executable + fs.chmodSync(scriptPath, "755") + + // Execute the script synchronously + const stdout = execSync(`bash ${scriptPath}`, { encoding: "utf8" }) + + console.log(`Version:${stdout}`) + + return stdout.trim() + } catch (error) { + console.error(`getVersion Error: ${error}`) + throw error + } +} diff --git a/e2e/src/argent-x/utils/index.ts b/e2e/src/argent-x/utils/index.ts new file mode 100644 index 0000000..dbb5229 --- /dev/null +++ b/e2e/src/argent-x/utils/index.ts @@ -0,0 +1,19 @@ +export { sleep, expireBESession, logInfo, generateEmail } from "../../shared/src/common" +export { default as Clipboard } from "./Clipboard" + +export { + TokenSymbol, + TokenName, + FeeTokens, + AccountsToSetup, + transferTokens, + getTokenInfo, + validateTx, + isScientific, + convertScientificToDecimal, + getBalance, +} from "../../shared/src/assets" + +export { unzip } from "./unzip" + +export { downloadGitHubRelease } from "./downloadGitHubRelease" diff --git a/e2e/src/argent-x/utils/unzip.sh b/e2e/src/argent-x/utils/unzip.sh new file mode 100755 index 0000000..58881fe --- /dev/null +++ b/e2e/src/argent-x/utils/unzip.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +# Enable command printing +#set -x + +# Function to run a command and check its exit status +run_command() { + "$@" + local status=$? + if [ $status -ne 0 ]; then + echo "Error: Command '$*' failed with exit status $status" + exit 1 + fi + return $status +} + +# Check if the correct number of arguments are provided +if [ $# -ne 2 ]; then + echo "Usage: $0 " + exit 1 +fi + +ZIP_FILE="$1" +OUTPUT_DIR="$2" + +# Check if ZIP_FILE exists +if [ ! -f "$ZIP_FILE" ]; then + echo "Error: ZIP file $ZIP_FILE does not exist" + exit 1 +fi + +BASE_NAME=$(basename "$ZIP_FILE" .zip) +echo "Removing directory: $OUTPUT_DIR/$BASE_NAME" +run_command rm -rf "$OUTPUT_DIR/$BASE_NAME" +run_command rm -rf "$OUTPUT_DIR/__MACOSX" + +# Create the output directory if it doesn't exist +run_command mkdir -p "$OUTPUT_DIR" + +# Check if bsdtar is installed, if not, install it +if ! command -v bsdtar &> /dev/null; then + echo "bsdtar not found. Installing libarchive-tools..." + run_command apt-get update + run_command apt-get install -y libarchive-tools +fi + +# Extract the zip file using bsdtar with verbose output +echo "Extracting $ZIP_FILE to $OUTPUT_DIR" +run_command bsdtar --no-xattrs -xf "$ZIP_FILE" -C "$OUTPUT_DIR" + +echo "Extraction completed" + +# Print the contents of the output directory for verification +#echo "Contents of $OUTPUT_DIR:" +#run_command ls -R "$OUTPUT_DIR" + +# Disable command printing +set +x \ No newline at end of file diff --git a/e2e/src/argent-x/utils/unzip.ts b/e2e/src/argent-x/utils/unzip.ts new file mode 100644 index 0000000..2ee0628 --- /dev/null +++ b/e2e/src/argent-x/utils/unzip.ts @@ -0,0 +1,36 @@ +import { exec } from "child_process" +import * as path from "path" +import { promisify } from "util" +import config from "../../../config" + +const execAsync = promisify(exec) + +export const unzip = async (version: string): Promise => { + const zipFilePath = path.join(config.migDir, `${version}.zip`) + const outputDir = path.join(config.migDir, version) + const scriptPath = path.join(__dirname, "unzip.sh") + + try { + console.log(`###### Unzipping ${version}.zip`) + + // Ensure the script is executable + await execAsync(`chmod +x ${scriptPath}`) + + // Execute the unzip script + const { stdout, stderr } = await execAsync( + `bash "${scriptPath}" "${zipFilePath}" "${outputDir}"`, + { maxBuffer: 1024 * 1024 * 10 }, // Increase buffer size to 10MB + ) + + console.log(`Unzip Output:\n${stdout}`) + + if (stderr) { + console.warn(`Unzip Warnings:\n${stderr}`) + } + } catch (error) { + console.error(`Error during unzip: ${error}`) + throw error + } + + return `${outputDir}` +} diff --git a/e2e/src/shared/cfg/global.teardown.ts b/e2e/src/shared/cfg/global.teardown.ts new file mode 100644 index 0000000..2153e09 --- /dev/null +++ b/e2e/src/shared/cfg/global.teardown.ts @@ -0,0 +1,16 @@ +import { artifactsDir } from "./test" +import * as fs from "fs" + +export default function cleanArtifactDir() { + console.time("cleanArtifactDir") + try { + fs.readdirSync(artifactsDir) + .filter((f) => f.endsWith("webm")) + .forEach((fileToDelete) => { + fs.rmSync(`${artifactsDir}/${fileToDelete}`) + }) + } catch (error) { + console.error({ op: "cleanArtifactDir", error }) + } + console.timeEnd("cleanArtifactDir") +} diff --git a/e2e/src/shared/cfg/test.ts b/e2e/src/shared/cfg/test.ts new file mode 100644 index 0000000..17a7461 --- /dev/null +++ b/e2e/src/shared/cfg/test.ts @@ -0,0 +1,75 @@ +import dotenv from "dotenv" +dotenv.config() + +import * as fs from "fs" +import path from "path" + +import { Page, TestInfo } from "@playwright/test" +import { logInfo } from "../src/common" +export const artifactsDir = path.resolve( + __dirname, + "../../../artifacts/playwright", +) +export const reportsDir = path.resolve(__dirname, "../../artifacts/reports") +export const isCI = Boolean(process.env.CI) +export const outputFolder = (testInfo: TestInfo) => + testInfo.title.replace(/\s+/g, "_").replace(/\W/g, "") +export const artifactFilename = (testInfo: TestInfo, label: string) => + `${testInfo.retry}-${testInfo.status}-${label}-${testInfo.workerIndex}` +export const isKeepArtifacts = (testInfo: TestInfo) => + testInfo.config.preserveOutput === "always" || + (testInfo.config.preserveOutput === "failures-only" && + testInfo.status === "failed") || + testInfo.status === "timedOut" + +export const artifactSetup = async (testInfo: TestInfo, label: string) => { + await fs.promises + .mkdir(path.resolve(artifactsDir, outputFolder(testInfo)), { + recursive: true, + }) + .catch((error) => { + console.error({ op: "artifactSetup", error }) + }) + return artifactFilename(testInfo, label) +} + +export const saveHtml = async ( + testInfo: TestInfo, + page: Page, + label: string, +) => { + logInfo({ + op: "saveHtml", + label, + }) + const fileName = await artifactSetup(testInfo, label) + const htmlContent = await page.content() + await fs.promises + .writeFile( + path.resolve(artifactsDir, outputFolder(testInfo), `${fileName}.html`), + htmlContent, + ) + .catch((error) => { + console.error({ op: "saveHtml", error }) + }) +} + +export const keepVideos = async ( + testInfo: TestInfo, + page: Page, + label: string, +) => { + logInfo({ + op: "keepVideos", + label, + }) + const fileName = await artifactSetup(testInfo, label) + await page + .video() + ?.saveAs( + path.resolve(artifactsDir, outputFolder(testInfo), `${fileName}.webm`), + ) + .catch((error) => { + console.error({ op: "keepVideos", error }) + }) +} diff --git a/e2e/src/shared/config.ts b/e2e/src/shared/config.ts new file mode 100644 index 0000000..d296860 --- /dev/null +++ b/e2e/src/shared/config.ts @@ -0,0 +1,27 @@ +import path from "path" +import dotenv from "dotenv" +import fs from "fs" + +const envPath = path.resolve(__dirname, "../../.env") +if (fs.existsSync(envPath)) { + dotenv.config({ path: envPath }) +} + +const commonConfig = { + isProdTesting: process.env.ARGENT_X_ENVIRONMENT === "prod" ? true : false, + //accounts used for setup + senderAddrs: process.env.E2E_SENDER_ADDRESSES?.split(",") || [], + senderKeys: process.env.E2E_SENDER_PRIVATEKEYS?.split(",") || [], + destinationAddress: process.env.E2E_SENDER_ADDRESSES?.split(",")[0] || '', //used as transfers destination + // urls + rpcUrl: process.env.ARGENT_SEPOLIA_RPC_URL || '', +} + +// check that no value of config is undefined, otherwise throw error +Object.entries(commonConfig).forEach(([key, value]) => { + if (value === undefined) { + throw new Error(`Missing ${key} config variable; check .env file`) + } +}) + +export default commonConfig diff --git a/e2e/src/shared/src/SapoEmailClient.ts b/e2e/src/shared/src/SapoEmailClient.ts new file mode 100644 index 0000000..f132670 --- /dev/null +++ b/e2e/src/shared/src/SapoEmailClient.ts @@ -0,0 +1,85 @@ +import * as ImapClient from 'imap-simple'; +import { simpleParser } from 'mailparser'; + +export default class SapoEmailClient { + private config: any; + + constructor(email: string, password: string) { + this.config = { + imap: { + user: email, + password: password, + host: 'imap.sapo.pt', + port: 993, + tls: true, + tlsOptions: { rejectUnauthorized: false }, + authTimeout: 3000 + } + }; + } + + private async getConnection() { + return await ImapClient.connect(this.config); + } + + private async moveToTrash(connection: any, messageId: number) { + await connection.moveMessage(messageId, 'Lixo'); + } + + async waitForEmail(timeout: number = 30000): Promise { + const startTime = Date.now(); + console.log('Waiting for verification email...'); + + while (Date.now() - startTime < timeout) { + try { + const connection = await this.getConnection(); + await connection.openBox('INBOX'); + + const messages = await connection.search(['UNSEEN'], { + bodies: ['HEADER', ''], // Include headers for date checking + markSeen: true + }); + + if (messages.length > 0) { + // Filter messages by received date + for (let i = messages.length - 1; i >= 0; i--) { + const message = messages[i]; + const body = message.parts.find(part => part.which === ''); + + if (body) { + const parsed = await simpleParser(body.body); + const messageDate = parsed.date || new Date(); + + // Check if message is less than 10 seconds old + if (messageDate > new Date(Date.now() - 10000)) { + const pin = parsed.subject?.match(/\d{6}/)?.[0]; + if (pin) { + await this.moveToTrash(connection, message.attributes.uid); + await connection.end(); + return pin; + } + } + } + } + } + + await connection.end(); + await new Promise(resolve => setTimeout(resolve, 2000)); + + } catch (error) { + console.error('Error checking email:', error); + await new Promise(resolve => setTimeout(resolve, 2000)); + } + } + + throw new Error(`No verification code found within ${timeout}ms`); + } + + async getPin(): Promise { + const pin = await this.waitForEmail(); + if (!pin) { + throw new Error('No verification code found in email'); + } + return pin; + } +} \ No newline at end of file diff --git a/e2e/src/shared/src/Utils.ts b/e2e/src/shared/src/Utils.ts new file mode 100644 index 0000000..e5050f7 --- /dev/null +++ b/e2e/src/shared/src/Utils.ts @@ -0,0 +1,21 @@ +import type { Page } from "@playwright/test" + +export default class Utils { + page: Page + constructor(page: Page) { + this.page = page + } + + async setClipBoardContent(text: string) { + await this.page.evaluate(`navigator.clipboard.writeText('${text}')`) + } + + async getClipboard() { + return String(await this.page.evaluate(`navigator.clipboard.readText()`)) + } + + async paste() { + const key = process.env.CI ? "Control" : "Meta" + await this.page.keyboard.press(`${key}+KeyV`) + } +} diff --git a/e2e/src/shared/src/assets.ts b/e2e/src/shared/src/assets.ts new file mode 100644 index 0000000..a30e8b3 --- /dev/null +++ b/e2e/src/shared/src/assets.ts @@ -0,0 +1,333 @@ +import { + Account, + uint256, + TransactionExecutionStatus, + RpcProvider, + constants, + TransactionFinalityStatus, + num, +} from "starknet" +import commonConfig from "../../../config" +import { expect } from "@playwright/test" +import { logInfo, sleep } from "./common" + +const isEqualAddress = (a?: string, b?: string) => { + try { + if (!a || !b) { + return false + } + return num.hexToDecimalString(a) === num.hexToDecimalString(b) + } catch { + // ignore parsing error + } + return false +} + +export type TokenSymbol = + | "ETH" + | "WBTC" + | "STRK" + | "SWAY" + | "USDC" + | "DAI" + | "ádfas" +export type TokenName = + | "Ethereum" + | "Wrapped BTC" + | "Starknet" + | "Standard Weighted Adalian Yield" + | "DAI" + | "USD Coin (Fake)" +export type FeeTokens = "ETH" | "STRK" +export interface AccountsToSetup { + assets: { + token: TokenSymbol + balance: number + }[] + deploy?: boolean + feeToken?: FeeTokens +} +const rpcUrl = commonConfig.rpcUrl +logInfo({ op: "Creating RPC provider with url", rpcUrl }) + +const provider = new RpcProvider({ + nodeUrl: rpcUrl, + chainId: constants.StarknetChainId.SN_SEPOLIA, + headers: { + "argent-version": process.env.VERSION || "Unknown version", + "argent-client": "argent-x", + }, +}) + +interface TokenInfo { + name: string + address: string + decimals: number +} +const tokenAddresses = new Map() +tokenAddresses.set("ETH", { + name: "Ethereum", + address: "0x049D36570D4e46f48e99674bd3fcc84644DdD6b96F7C741B1562B82f9e004dC7", + decimals: 18, +}) +tokenAddresses.set("WBTC", { + name: "Wrapped BTC", + address: "0x00c6164dA852d230360333D6adE3551eE3e48124C815704f51fA7F12D8287Dcc", + decimals: 8, +}) +tokenAddresses.set("STRK", { + name: "Starknet Token", + address: "0x04718f5a0Fc34cC1AF16A1cdee98fFB20C31f5cD61D6Ab07201858f4287c938D", + decimals: 18, +}) +tokenAddresses.set("SWAY", { + name: "Standard Weighted Adalian Yield", + address: "0x0030058F19Ed447208015F6430F0102e8aB82D6c291566D7E73fE8e613c3D2ed", + decimals: 18, +}) +tokenAddresses.set("USDC", { + name: "USD Coin (Fake)", + address: "0x07ab0b8855a61f480b4423c46c32fa7c553f0aac3531bbddaa282d86244f7a23", + decimals: 6, +}) +export const getTokenInfo = (tkn: string) => { + const tokenInfo = tokenAddresses.get(tkn) + if (!tokenInfo) { + throw new Error(`Invalid token: ${tkn}`) + } + return tokenInfo +} + +const maxRetries = 4 + +const formatAmount = (amount: string) => { + return parseInt(amount, 16) +} + +export const formatAmountBase18 = (amount: number) => { + return amount * Math.pow(10, 18) +} + +const getAccount = async (amount: string, token: TokenSymbol) => { + const log: string[] = [] + const maxAttempts = 5 + let i = 0 + while (i < maxAttempts) { + i++ + const randomAccountPosition = Math.floor( + Math.random() * commonConfig.senderKeys!.length, + ) + const acc = new Account( + provider, + commonConfig.senderAddrs![randomAccountPosition], + commonConfig.senderKeys![randomAccountPosition], + "1", + ) + const initialBalance = await getBalance(acc.address, token) + const initialBalanceFormatted = + parseFloat(initialBalance) * Math.pow(10, 18) + if (initialBalanceFormatted < parseInt(amount)) { + log.push( + `${commonConfig.senderAddrs![randomAccountPosition] + } Not enough balance ${initialBalanceFormatted} ${token} < ${amount}`, + ) + } else { + logInfo({ + op: "getAccount", + randomAccountPosition, + address: acc.address, + balance: `initialBalance ${initialBalanceFormatted} ${token}`, + }) + return acc + } + } + console.error(log.join("\n")) + throw new Error("No account with enough balance") +} + +const isTXProcessed = async (txHash: string) => { + let txProcessed = false + let txAcceptedRetries = 10 + let txStatusResponse + while (!txProcessed && txAcceptedRetries > 0) { + txAcceptedRetries-- + txStatusResponse = await provider.getTransactionStatus(txHash) + if ( + txStatusResponse.finality_status === + TransactionFinalityStatus.ACCEPTED_ON_L2 || + txStatusResponse.finality_status === + TransactionFinalityStatus.ACCEPTED_ON_L1 + ) { + txProcessed = true + } else { + await sleep(2 * 1000) + } + } + if (!txProcessed) { + console.error("txStatusResponse", txStatusResponse) + } + return { txProcessed, txStatusResponse } +} + +const getTXData = async (txHash: string) => { + const isProcessed = await isTXProcessed(txHash) + if (!isProcessed) { + throw new Error(`Transaction not processed: ${txHash}`) + } + let nodeUpdated = false + let txAcceptedRetries = 10 + let txData + while (!nodeUpdated && txAcceptedRetries > 0) { + txAcceptedRetries-- + txData = await provider.getTransactionByHash(txHash) + if (txData.type) { + nodeUpdated = true + } else { + await sleep(2 * 1000) + } + } + if (!nodeUpdated) { + console.error("txData", txData) + } + return { nodeUpdated, txData } +} + +export async function transferTokens( + amount: number, + to: string, + token: TokenSymbol = "ETH", +) { + const tokenInfo = getTokenInfo(token) + const amountToTransfer = `${amount * Math.pow(10, tokenInfo.decimals)}` + logInfo({ op: "transferTokens", amount, amountToTransfer, to, token }) + + const { low, high } = uint256.bnToUint256(amountToTransfer) + let placeTXAttempt = 0 + let txHash: string | null = null + let account + while (placeTXAttempt < maxRetries) { + account = await getAccount(amountToTransfer, token) + /** timeout if we don't receive a valid execution response */ + const placeTXTimeout = setTimeout(() => { + throw new Error(`Place tx timed out: ${txHash}`) + }, 60 * 1000) /** 60 seconds */ + try { + placeTXAttempt++ + const tx = await account.execute({ + contractAddress: tokenInfo.address, + entrypoint: "transfer", + calldata: [to, low, high], + }) + txHash = tx.transaction_hash + const { txProcessed, txStatusResponse } = await isTXProcessed( + tx.transaction_hash, + ) + if (txProcessed) { + logInfo({ + TxStatus: TransactionExecutionStatus.SUCCEEDED, + transaction_hash: tx.transaction_hash, + }) + return tx.transaction_hash + } + + console.error( + `[Failed to place TX] ${tx.transaction_hash} ${JSON.stringify(txStatusResponse)}`, + ) + } catch (e) { + if (e instanceof Error) { + //for debug only + console.error( + `placeTXAttempt: ${placeTXAttempt}, Exception: ${txHash}`, + e, + ) + } + } finally { + clearTimeout(placeTXTimeout) + } + console.warn("Transfer failed, going to try again ") + } + return null +} + +export async function getBalance( + accountAddress: string, + token: TokenSymbol = "ETH", +) { + const tokenInfo = getTokenInfo(token) + logInfo({ op: "getBalance", accountAddress, token, tokenInfo }) + const balanceOfCall = { + contractAddress: tokenInfo.address, + entrypoint: "balanceOf", + calldata: [accountAddress], + } + const [low] = await provider.callContract(balanceOfCall) + const balance = ( + parseInt(low, 16) / Math.pow(10, tokenInfo.decimals) + ).toFixed(4) + + logInfo({ + op: "getBalance", + balance, + formattedBalance: balance, + }) + return balance +} + +export async function validateTx({ + txHash, + receiver, + amount, + txType = "token", +}: { + txHash: string + receiver: string + amount?: number + txType?: "token" | "nft" +}) { + const log: string[] = [] + logInfo({ + op: "validateTx", + txHash, + receiver, + amount, + }) + const processed = await isTXProcessed(txHash) + if (!processed) { + throw new Error(`Transaction not processed: ${txHash}`) + } + const { nodeUpdated, txData } = await getTXData(txHash) + if (!nodeUpdated) { + console.error(log.join("\n")) + throw new Error(`Transaction data not found: ${txHash}`) + } + log.push("txData", JSON.stringify(txData)) + if (!("calldata" in txData!)) { + console.error(log.join("\n")) + throw new Error( + `Invalid transaction data: ${txHash}, ${JSON.stringify(txData)}`, + ) + } + logInfo(log) + let accAdd + txType === "token" + ? (accAdd = txData.calldata[4].toString()) + : (accAdd = txData.calldata[5].toString()) + + if (accAdd.length === 65) { + accAdd = accAdd.replace("0x", "0x0") + } + expect(isEqualAddress(accAdd, receiver)).toBe(true) + if (amount) { + expect(formatAmount(txData.calldata[5].toString())).toBe(amount) + } +} + +export function isScientific(num: number) { + const scientificPattern = /(.*)([eE])(.*)$/ + return scientificPattern.test(String(num)) +} + +export function convertScientificToDecimal(num: number) { + const exponent = String(num).split("e")[1] + return Number(num).toFixed(Math.abs(Number(exponent))) +} diff --git a/e2e/src/shared/src/common.ts b/e2e/src/shared/src/common.ts new file mode 100644 index 0000000..0364a50 --- /dev/null +++ b/e2e/src/shared/src/common.ts @@ -0,0 +1,29 @@ +import config from "../../../config" +import { v4 as uuid } from "uuid" + +export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)) +const app = "argentx" +export const expireBESession = async (email: string) => { + const requestOptions = { + method: "GET", + } + const request = `${config.beAPIUrl + }/debug/expireCredentials?application=${app}&email=${encodeURIComponent( + email, + )}` + const response = await fetch(request, requestOptions) + if (response.status != 200) { + console.error(response.body) + throw new Error(`Error expiring session: ${request}`) + } + return response.status +} + +export const logInfo = (message: string | object) => { + const canLogInfo = process.env.E2E_LOG_INFO || false + if (canLogInfo) { + console.log(message) + } +} + +export const generateEmail = () => `e2e_2fa_${uuid()}@mail.com` diff --git a/e2e/src/webwallet/config.ts b/e2e/src/webwallet/config.ts new file mode 100644 index 0000000..0781b2e --- /dev/null +++ b/e2e/src/webwallet/config.ts @@ -0,0 +1,30 @@ +import path from "path" +import dotenv from "dotenv" +import fs from "fs" +import commonConfig from "../shared/config" + +const envPath = path.resolve(__dirname, ".env") +if (fs.existsSync(envPath)) { + dotenv.config({ path: envPath }) +} +const config = { + validLogin: { + email: process.env.WW_EMAIL!, + pin: process.env.WW_PIN!, + password: process.env.WW_LOGIN_PASSWORD!, + }, + emailPassword: process.env.EMAIL_PASSWORD!, + acc_destination: commonConfig.destinationAddress! || '', + vw_acc_addr: process.env.VW_ACC_ADDR! || '', + url: "https://web.argent.xyz", + ...commonConfig, +} + +// check that no value of config is undefined, otherwise throw error +Object.entries(config).forEach(([key, value]) => { + if (value === undefined) { + throw new Error(`Missing ${key} config variable; check .env file`) + } +}) + +export default config diff --git a/e2e/src/webwallet/fixtures.ts b/e2e/src/webwallet/fixtures.ts new file mode 100644 index 0000000..f9bf060 --- /dev/null +++ b/e2e/src/webwallet/fixtures.ts @@ -0,0 +1,8 @@ +import { BrowserContext, Page } from "@playwright/test" +import type WebWalletPage from "./page-objects/WebWalletPage" + +export interface TestPages { + webWallet: WebWalletPage + dApp: Page + browserContext: BrowserContext +} diff --git a/e2e/src/webwallet/page-objects/Dapps.ts b/e2e/src/webwallet/page-objects/Dapps.ts new file mode 100644 index 0000000..af17222 --- /dev/null +++ b/e2e/src/webwallet/page-objects/Dapps.ts @@ -0,0 +1,83 @@ +import { Page, expect } from "@playwright/test" +import { ICredentials } from "./Login" +import Navigation from "./Navigation" +import { artifactsDir } from "../../shared/cfg/test" +import { randomUUID } from "crypto" +import SapoEmailClient from "../../shared/src/SapoEmailClient" +import config from "../config" +const dappUrl = 'http://localhost:3000/' +let mailClient: SapoEmailClient + +export default class Dapps extends Navigation { + constructor(page: Page) { + super(page) + mailClient = new SapoEmailClient(config.validLogin.email, config.emailPassword); + } + + async requestConnectionFromDapp({ + dApp, + credentials, + newAccount = false, + useStarknetKitModal = false + }: { + dApp: Page + credentials: ICredentials + newAccount: boolean + useStarknetKitModal?: boolean + }) { + + await dApp.setViewportSize({ width: 1080, height: 720 }) + await dApp.goto(dappUrl) + + await dApp.getByRole('button', { name: 'Connection' }).click() + if (useStarknetKitModal) { + await dApp.getByRole('button', { name: 'Starknetkit Modal' }).click() + const popup = await this.handlePopup(dApp, credentials, newAccount) + await this.verifyEmailInPopup(popup, credentials.email) + await popup.locator('button[type="submit"]').click() + } else { + const pagePromise = dApp.context().waitForEvent('page'); + await dApp.locator('button :text-is("Argent Web Wallet")').click(); + const newPage = await pagePromise; + await this.fillCredentials(newPage, credentials, newAccount) + } + return dApp + } + + private async fillCredentials(page: Page, credentials: ICredentials, newAccount: boolean) { + await page.locator("[name=email]").fill(credentials.email) + await page.locator('button[type="submit"]').click() + const pin = await mailClient.getPin() + console.log("PIN:", pin) + await page.locator('[id^="pin-input"]').first().click() + await page.locator('[id^="pin-input"]').first().fill(pin!) + if (newAccount) { + await page.locator("[name=password]").fill(credentials.password) + await page.locator("[name=repeatPassword]").fill(credentials.password) + } else { + await page.locator("[name=password]").fill(credentials.password) + } + await page.locator('button[type="submit"]').click() + await page.waitForLoadState() + return page + } + + private async handlePopup(dApp: Page, credentials: ICredentials, newAccount: boolean) { + const popupPromise = dApp.waitForEvent("popup") + await expect(dApp.locator("p:text-is('Email')")).toBeVisible() + await dApp.locator("p:text-is('Email')").click() + const popup = await popupPromise + // Wait for the popup to load. + await popup.waitForLoadState() + return this.fillCredentials(popup, credentials, newAccount) + } + + private async verifyEmailInPopup(popup: Page, email: string) { + await expect(popup.locator(`text="${email}"`)) + .toBeVisible() + .catch(async () => { + await popup.screenshot({ path: `${artifactsDir}/${randomUUID()}.png` }) + throw new Error("Email not visible") + }) + } +} diff --git a/e2e/src/webwallet/page-objects/Login.ts b/e2e/src/webwallet/page-objects/Login.ts new file mode 100644 index 0000000..27f4071 --- /dev/null +++ b/e2e/src/webwallet/page-objects/Login.ts @@ -0,0 +1,88 @@ +import { Page, expect } from "@playwright/test" + +import config from "../config" +import Navigation from "./Navigation" + +export interface ICredentials { + email: string + pin: string + password: string +} + +export default class Login extends Navigation { + constructor(page: Page) { + super(page) + } + + get email() { + return this.page.locator("input[name=email]") + } + + get pinInput() { + return this.page.locator('[id^="pin-input"]') + } + + get password() { + return this.page.locator("input[name=password]") + } + get repeatPassword() { + return this.page.locator("input[name=repeatPassword]") + } + get wrongPassword() { + return this.page.locator( + '//input[@name="password"][@aria-invalid="true"]/following::label[contains(text(), "Wrong password")]', + ) + } + + get resendPin() { + return this.page.getByText("Not received the email?") + } + + get pinResendConfirmation() { + return this.page.getByText("Email sent!") + } + + get forgetPassword() { + return this.page.getByText("Forgotten your password?") + } + + get recoveryOptionsTitle() { + return this.page.getByText("Recovery options") + } + + get differentAccount() { + return this.page.locator('p:text-is("Use a different account")') + } + + async fillPin(pin: string) { + await this.continue.click() + await this.pinInput.first().click() + await this.pinInput.first().fill(pin) + } + + async success(credentials: ICredentials = config.validLogin) { + await this.email.fill(credentials.email) + await this.fillPin(credentials.pin) + await this.password.fill(credentials.password) + await expect(this.forgetPassword).toBeVisible() + await expect(this.differentAccount).toBeVisible() + await Promise.all([ + this.page.waitForURL(`${config.url}/settings`), + this.continue.click(), + ]) + await expect(this.lock).toBeVisible() + } + + async createWallet(credentials: ICredentials) { + await this.email.fill(credentials.email) + //await this.continue.click() + await this.fillPin(credentials.pin) + await this.password.fill(credentials.password) + await this.repeatPassword.fill(credentials.password) + await Promise.all([ + this.page.waitForURL(`${config.url}/settings`), + this.continue.click(), + ]) + await expect(this.lock).toBeVisible() + } +} diff --git a/e2e/src/webwallet/page-objects/Navigation.ts b/e2e/src/webwallet/page-objects/Navigation.ts new file mode 100644 index 0000000..92f4c86 --- /dev/null +++ b/e2e/src/webwallet/page-objects/Navigation.ts @@ -0,0 +1,44 @@ +import type { Page } from "@playwright/test" + +export default class Navigation { + page: Page + constructor(page: Page) { + this.page = page + } + + get viewYourAccountTitle() { + return this.page.locator("text=View your smart account") + } + + get viewYourAccountDescription() { + return this.page.locator("text=See your smart account on Argent Web.") + } + + get continue() { + return this.page.locator(`button:text-is("Continue")`) + } + + get addFunds() { + return this.page.getByRole("link", { name: "Add funds" }) + } + + get send() { + return this.page.getByRole("link", { name: "Send" }) + } + + get authorizedDapps() { + return this.page.getByRole("link", { name: "Authorized dapps" }) + } + + get changePassword() { + return this.page.getByRole("link", { name: "Change password" }) + } + + get lock() { + return this.page.getByRole("button", { name: "Lock" }) + } + + get switchTheme() { + return this.page.getByRole("button", { name: "Switch theme" }) + } +} diff --git a/e2e/src/webwallet/page-objects/WalletHome.ts b/e2e/src/webwallet/page-objects/WalletHome.ts new file mode 100644 index 0000000..376ab96 --- /dev/null +++ b/e2e/src/webwallet/page-objects/WalletHome.ts @@ -0,0 +1,90 @@ +import type { Page } from "@playwright/test" +import email from '../config'; + +export default class WalletHome { + page: Page + constructor(page: Page) { + this.page = page + } + + get webWalletTitle() { + return this.page.locator(`p:text-is("${email}")`) + } + + get viewInPortfolio() { + return this.page.locator('p:text-is("View your smart account")') + } + + get viewInPortfolioParagraph() { + return this.page.locator('p:text-is("See your smart account on Argent Web.")') + } + + get authorizedDapps() { + return this.page.locator('p:text-is("Authorized dapps")') + } + + get manageDappsParagraph() { + return this.page.locator('p:text-is("Manage dapps connected with your account.")') + } + + get passwordInput() { + return this.page.locator('input[type="password"]') + } + + get changePasswordParagraph() { + return this.page.locator('p:text-is("Change password for your smart account.")') + } + + get downloadPrivateKeyButton() { + return this.page.locator('button:text-is("Download private key")') + } + + get downloadButton() { + return this.page.locator('button:text-is("Download")') + } + + get lockAccountParagraph() { + return this.page.locator('p:text-is("Lock your account.")') + } + + get lockButton() { + return this.page.locator('button:text-is("Lock")') + } + + get termsOfServiceParagraph() { + return this.page.locator('p:text-is("Terms of service")') + } + + get privacyPolicyParagraph() { + return this.page.locator('p:text-is("Privacy policy")') + } + + get versionParagraph() { + return this.page.locator('p:text-is("Version")') + } + + + async verifyLayout() { + const elements = [ + this.webWalletTitle, + this.viewInPortfolio, + this.viewInPortfolioParagraph, + this.authorizedDapps, + this.manageDappsParagraph, + this.passwordInput, + this.changePasswordParagraph, + this.downloadPrivateKeyButton, + this.downloadButton, + this.lockAccountParagraph, + this.lockButton, + this.termsOfServiceParagraph, + this.privacyPolicyParagraph + ]; + + for (const element of elements) { + await element.isVisible(); + } + } + + +} diff --git a/e2e/src/webwallet/page-objects/WalletHomeSubpages.ts b/e2e/src/webwallet/page-objects/WalletHomeSubpages.ts new file mode 100644 index 0000000..a52547d --- /dev/null +++ b/e2e/src/webwallet/page-objects/WalletHomeSubpages.ts @@ -0,0 +1,124 @@ +import type { Page } from "@playwright/test" +import email from '../config'; + +export default class WalletHomeSubpages { + page: Page + constructor(page: Page) { + this.page = page + } + + // Define the locators for the elements on the Wallet subpage: Portfolio + get portfolioHeading() { + return this.page.locator('text-is("Portfolio")'); + } + + get portfolioSubTitle() { + return this.page.locator('text="Theh best place to track your portfolio on Starknet"'); + } + + // Define the locators for the elements on the Wallet subpage: Authorized dapps + get authorizedDappsHeading() { + return this.page.locator('h1:text-is("Authorized dapps")') + } + get goBackButton() { + return this.page.locator(`button[aria-label="Go back"]`); + } + + get connectedDappsTab() { + return this.page.locator('tab:text-is("Connected dapps")'); + } + + get noConnectedDappsMessage() { + return this.page.locator('text="No connected dapps"'); + } + + get activeSessionsTab() { + return this.page.locator('text="Active sessions"'); + } + + get noActiveSessionsMessage() { + return this.page.locator('text="No active sessions"'); + } + + //Define the locators for the elements on the Wallet subpage: Change password + + get changePasswordHeading() { + return this.page.locator('h1:text-is("Change password")') + } + + get enterCodeMessage() { + return this.page.locator(`text="Enter the code we sent to" ${email}`); + } + + get changePasswordDescription() { + return this.page.locator(`text="We've sent you an email with a code. Enter it below so you can change your password."`) + } + + get enterNewPasswordParagraph() { + return this.page.locator('text="Enter new password"'); + } + + get passwordInput() { + return this.page.locator('input[name="password"]'); + } + + get repeatNewPasswordParagraph() { + return this.page.locator('text="Repeat new password"'); + } + + get repeatPasswordInput() { + return this.page.locator('input[name="repeatPassword"]'); + } + + get continueButton() { + return this.page.locator('button:text-is("Continue")'); + } + + get passwordChangedHeading() { + return this.page.locator('element[name="Password successfully changed."]'); + } + + get goBackToSettingsButton() { + return this.page.locator('button[name="Go back to settings"]'); + } + + // Define the locators for the elements on the Wallet subpage: Download private key + get securityCompromisedHeading() { + return this.page.locator('h1:text-is("Security of your wallet might get compromised")') + } + + get securityCompromisedParagraph() { + return this.page.locator('p:text-is("We strongly recommend to not download your private key if you\'re not sure what it means")') + } + + get downloadKeyButton() { + return this.page.locator('button:text-is("Download private key")') + } + + get closeButton() { + return this.page.locator('button:text-is("Close")') + } + + // Define the locators for the elements on the Wallet subpage: Lock account + get welcomeBackHeading() { + return this.page.locator('h1:text-is("Welcome Back")'); + } + + get emailAddressHeading() { + return this.page.locator(`text=${email}`); + } + + get enterYourPasswordHeading() { + return this.page.locator('h1:text-is("Enter your password")'); + } + + // Define the locators for the elements on Terms of service and Privacy policy Pages (outside WebWallet) + get argentAppTermsOfServiceHeading() { + return this.page.locator('h1:text-is("Argent App - Terms of Service")') + } + + get argentAppPrivacyPolicyHeading() { + return this.page.locator('h1:text-is("Argent App - Privacy Policy")') + } + +} diff --git a/e2e/src/webwallet/page-objects/WebWalletPage.ts b/e2e/src/webwallet/page-objects/WebWalletPage.ts new file mode 100644 index 0000000..3d2f46d --- /dev/null +++ b/e2e/src/webwallet/page-objects/WebWalletPage.ts @@ -0,0 +1,35 @@ +import type { Page } from "@playwright/test" +import { v4 as uuid } from "uuid" + +import config from "../config" +import Login from "./Login" +import Navigation from "./Navigation" + +export const generateEmail = () => `newWallet_${uuid()}@mail.com` + +import Dapps from "./Dapps" +import WalletHome from "./WalletHome" +import WalletHomeSubpages from "./WalletHomeSubpages" +export default class WebWalletPage { + page: Page + login: Login + navigation: Navigation + dapps: Dapps + wallethome: WalletHome + walletHomeSubpages: WalletHomeSubpages + + constructor(page: Page) { + this.page = page + this.login = new Login(page) + this.navigation = new Navigation(page) + this.dapps = new Dapps(page) + this.wallethome = new WalletHome(page) + this.walletHomeSubpages = new WalletHomeSubpages(page) + } + + open() { + return this.page.goto(config.url) + } + + generateEmail = () => `e2e_webwallet_${uuid()}@mail.com` +} diff --git a/e2e/src/webwallet/specs/dapps.spec.ts b/e2e/src/webwallet/specs/dapps.spec.ts new file mode 100644 index 0000000..faa5b74 --- /dev/null +++ b/e2e/src/webwallet/specs/dapps.spec.ts @@ -0,0 +1,15 @@ +import test from "../test" +import config from "../config" + +test.describe(`Dapps`, () => { + for (const useStarknetKitModal of [true, false] as const) { + test(`connect from testDapp using starknetKitModal ${useStarknetKitModal}`, async ({ webWallet, dApp }) => { + await webWallet.dapps.requestConnectionFromDapp({ + dApp, + credentials: config.validLogin, + newAccount: false, + useStarknetKitModal + }) + }) + } +}) diff --git a/e2e/src/webwallet/test.ts b/e2e/src/webwallet/test.ts new file mode 100644 index 0000000..02a2fa2 --- /dev/null +++ b/e2e/src/webwallet/test.ts @@ -0,0 +1,94 @@ +import { + artifactsDir, + isKeepArtifacts, + keepVideos, + saveHtml, +} from "../shared/cfg/test" + +import { + BrowserContext, + Browser, + TestInfo, + test as testBase, +} from "@playwright/test" + +import config from "./config" +import { TestPages } from "./fixtures" +import WebWalletPage from "./page-objects/WebWalletPage" + +let browserCtx: BrowserContext + +async function createContext({ + browser, + baseURL, +}: { + browser: Browser + baseURL: string + name: string + testInfo: TestInfo +}) { + const context = await browser.newContext({ + ignoreHTTPSErrors: true, + acceptDownloads: true, + recordVideo: { + dir: artifactsDir, + size: { + width: 1366, + height: 768, + }, + }, + baseURL, + viewport: { width: 1196, height: 724 }, + }) + + await context.addInitScript("window.PLAYWRIGHT = true;") + return context +} + +function createPage(pageType: "WebWallet" | "DApp" = "WebWallet") { + return async ( + { browser }: { browser: Browser }, + use: any, + testInfo: TestInfo, + ) => { + const url = config.url + + const context = await createContext({ + browser, + testInfo, + name: pageType, + baseURL: url, + }) + const page = await context.newPage() + browserCtx = context + if (pageType === "WebWallet") { + const webWalletPage = new WebWalletPage(page) + await webWalletPage.open() + await use(webWalletPage) + } else { + await use(page) + } + + const keepArtifacts = isKeepArtifacts(testInfo) + if (keepArtifacts) { + await saveHtml(testInfo, page, pageType) + await context.close() + await keepVideos(testInfo, page, pageType) + } else { + await context.close() + } + } +} +function getContext() { + return async ({ }, use: any, _testInfo: TestInfo) => { + await use(browserCtx) + } +} + +const test = testBase.extend({ + webWallet: createPage(), + browserContext: getContext(), + dApp: createPage("DApp"), +}) + +export default test diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json new file mode 100644 index 0000000..2571efc --- /dev/null +++ b/e2e/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "Esnext", + "module": "ESNext", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "strict": true, + "noImplicitAny": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "resolveJsonModule": true, + "inlineSources": true, + "inlineSourceMap": true, + "composite": true, + "types": [ + "node" + ], + "noEmit": true, + "skipLibCheck": true + }, + "include": [ + "**/src", + "**/shared", + "config.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/e2e/until-failure b/e2e/until-failure new file mode 100755 index 0000000..d0bc639 --- /dev/null +++ b/e2e/until-failure @@ -0,0 +1,9 @@ +#!/bin/bash +# +# Example usage: +# +# # Repeats test until it fails: +# ./until-failure pnpm playwright test --config=./extension src/specs/accountSettings.spec.ts:95 +# + +while "$@"; do :; done diff --git a/package.json b/package.json index 74f2494..b247fd4 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "demo-dapp-starknet", "version": "0.1.0", "private": true, - "packageManager": "pnpm@9.1.0", + "packageManager": "pnpm@9.1.4", "engines": { "node": "20.x" }, @@ -11,7 +11,8 @@ "build": "next build", "start": "next start", "lint": "next lint", - "test": "playwright test", + "test:argentx": "pnpm run --filter @demo-dapp-starket/e2e test:argentx", + "test:webwallet": "pnpm run --filter @demo-dapp-starket/e2e test:webwallet", "test:headed": "playwright test --headed", "test:ui": "playwright test --ui", "prepare": "husky" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b496aab..a9428c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,6 +82,82 @@ importers: specifier: ^5 version: 5.6.3 + e2e: + dependencies: + '@scure/base': + specifier: ^1.1.1 + version: 1.1.9 + '@scure/bip39': + specifier: ^1.2.1 + version: 1.4.0 + axios: + specifier: ^1.7.7 + version: 1.7.7 + fs-extra: + specifier: ^11.2.0 + version: 11.2.0 + imap-simple: + specifier: ^5.1.0 + version: 5.1.0 + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 + mailparser: + specifier: ^3.7.1 + version: 3.7.1 + nodemailer: + specifier: ^6.9.16 + version: 6.9.16 + object-hash: + specifier: ^3.0.0 + version: 3.0.0 + react: + specifier: ^18.0.0 + version: 18.3.1 + react-dom: + specifier: ^18.0.0 + version: 18.3.1(react@18.3.1) + swr: + specifier: ^1.3.0 + version: 1.3.0(react@18.3.1) + zod: + specifier: ^3.23.8 + version: 3.23.8 + devDependencies: + '@playwright/test': + specifier: ^1.48.1 + version: 1.48.2 + '@types/axios': + specifier: ^0.14.0 + version: 0.14.4 + '@types/fs-extra': + specifier: ^11.0.4 + version: 11.0.4 + '@types/imap-simple': + specifier: ^4.2.9 + version: 4.2.9 + '@types/mailparser': + specifier: ^3.4.5 + version: 3.4.5 + '@types/node': + specifier: ^22.0.0 + version: 22.9.3 + '@types/nodemailer': + specifier: ^6.4.17 + version: 6.4.17 + '@types/uuid': + specifier: ^10.0.0 + version: 10.0.0 + dotenv: + specifier: ^16.3.1 + version: 16.4.5 + starknet: + specifier: 6.11.0 + version: 6.11.0 + uuid: + specifier: ^11.0.0 + version: 11.0.3 + packages: '@adraffy/ens-normalize@1.11.0': @@ -478,92 +554,86 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} - '@parcel/watcher-android-arm64@2.5.0': - resolution: {integrity: sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==} + '@parcel/watcher-android-arm64@2.4.1': + resolution: {integrity: sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [android] - '@parcel/watcher-darwin-arm64@2.5.0': - resolution: {integrity: sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==} + '@parcel/watcher-darwin-arm64@2.4.1': + resolution: {integrity: sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [darwin] - '@parcel/watcher-darwin-x64@2.5.0': - resolution: {integrity: sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==} + '@parcel/watcher-darwin-x64@2.4.1': + resolution: {integrity: sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [darwin] - '@parcel/watcher-freebsd-x64@2.5.0': - resolution: {integrity: sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==} + '@parcel/watcher-freebsd-x64@2.4.1': + resolution: {integrity: sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [freebsd] - '@parcel/watcher-linux-arm-glibc@2.5.0': - resolution: {integrity: sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==} - engines: {node: '>= 10.0.0'} - cpu: [arm] - os: [linux] - - '@parcel/watcher-linux-arm-musl@2.5.0': - resolution: {integrity: sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==} + '@parcel/watcher-linux-arm-glibc@2.4.1': + resolution: {integrity: sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - '@parcel/watcher-linux-arm64-glibc@2.5.0': - resolution: {integrity: sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==} + '@parcel/watcher-linux-arm64-glibc@2.4.1': + resolution: {integrity: sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - '@parcel/watcher-linux-arm64-musl@2.5.0': - resolution: {integrity: sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==} + '@parcel/watcher-linux-arm64-musl@2.4.1': + resolution: {integrity: sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - '@parcel/watcher-linux-x64-glibc@2.5.0': - resolution: {integrity: sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==} + '@parcel/watcher-linux-x64-glibc@2.4.1': + resolution: {integrity: sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - '@parcel/watcher-linux-x64-musl@2.5.0': - resolution: {integrity: sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==} + '@parcel/watcher-linux-x64-musl@2.4.1': + resolution: {integrity: sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - '@parcel/watcher-wasm@2.5.0': - resolution: {integrity: sha512-Z4ouuR8Pfggk1EYYbTaIoxc+Yv4o7cGQnH0Xy8+pQ+HbiW+ZnwhcD2LPf/prfq1nIWpAxjOkQ8uSMFWMtBLiVQ==} + '@parcel/watcher-wasm@2.4.1': + resolution: {integrity: sha512-/ZR0RxqxU/xxDGzbzosMjh4W6NdYFMqq2nvo2b8SLi7rsl/4jkL8S5stIikorNkdR50oVDvqb/3JT05WM+CRRA==} engines: {node: '>= 10.0.0'} bundledDependencies: - napi-wasm - '@parcel/watcher-win32-arm64@2.5.0': - resolution: {integrity: sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==} + '@parcel/watcher-win32-arm64@2.4.1': + resolution: {integrity: sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [win32] - '@parcel/watcher-win32-ia32@2.5.0': - resolution: {integrity: sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==} + '@parcel/watcher-win32-ia32@2.4.1': + resolution: {integrity: sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==} engines: {node: '>= 10.0.0'} cpu: [ia32] os: [win32] - '@parcel/watcher-win32-x64@2.5.0': - resolution: {integrity: sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==} + '@parcel/watcher-win32-x64@2.4.1': + resolution: {integrity: sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [win32] - '@parcel/watcher@2.5.0': - resolution: {integrity: sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==} + '@parcel/watcher@2.4.1': + resolution: {integrity: sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==} engines: {node: '>= 10.0.0'} '@pkgjs/parseargs@0.11.0': @@ -593,6 +663,9 @@ packages: '@scure/starknet@1.0.0': resolution: {integrity: sha512-o5J57zY0f+2IL/mq8+AYJJ4Xpc1fOtDhr+mFQKbHnYFmm3WQrC+8zj2HEgxak1a+x86mhmBC1Kq305KUpVf0wg==} + '@selderee/plugin-htmlparser2@0.11.0': + resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + '@stablelib/aead@1.0.1': resolution: {integrity: sha512-q39ik6sxGHewqtO0nP4BuSe3db5G1fEJE8ukvngS2gLkBXyy6E7pLubhbYgnkDFv6V8cWaxcE4Xn0t6LWcJkyg==} @@ -688,15 +761,40 @@ packages: '@trpc/server@10.45.2': resolution: {integrity: sha512-wOrSThNNE4HUnuhJG6PfDRp4L2009KDVxsd+2VYH8ro6o/7/jwYZ8Uu5j+VaW+mOmc8EHerHzGcdbGNQSAUPgg==} + '@types/axios@0.14.4': + resolution: {integrity: sha512-9JgOaunvQdsQ/qW2OPmE5+hCeUB52lQSolecrFrthct55QekhmXEwT203s20RL+UHtCQc15y3VXpby9E7Kkh/g==} + deprecated: This is a stub types definition. axios provides its own type definitions, so you do not need this installed. + '@types/conventional-commits-parser@5.0.0': resolution: {integrity: sha512-loB369iXNmAZglwWATL+WRe+CRMmmBPtpolYzIebFaX4YA3x+BEfLqhUAV9WanycKI3TG1IMr5bMJDajDKLlUQ==} + '@types/fs-extra@11.0.4': + resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} + + '@types/imap-simple@4.2.9': + resolution: {integrity: sha512-qROCP+BJfSpelnlhL48QGNU3bY1GWZpOgubiWnbs7HkIiLxmpP6TPE9Q/ROPjVzL9L89kB+vtBbQNNQTYNmuew==} + + '@types/imap@0.8.42': + resolution: {integrity: sha512-FusePG9Cp2GYN6OLow9xBCkjznFkAR7WCz0Fm+j1p/ER6C8V8P71DtjpSmwrZsS7zekCeqdTPHEk9N5OgPwcsg==} + '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/jsonfile@6.1.4': + resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} + + '@types/mailparser@3.4.5': + resolution: {integrity: sha512-EPERBp7fLeFZh7tS2X36MF7jawUx3Y6/0rXciZah3CTYgwLi3e0kpGUJ6FOmUabgzis/U1g+3/JzrVWbWIOGjg==} + '@types/node@20.17.4': resolution: {integrity: sha512-Fi1Bj8qTJr4f1FDdHFR7oMlOawEYSzkHNdBJK+aRjcDDNHwEV3jPPjuZP2Lh2QNgXeqzM8Y+U6b6urKAog2rZw==} + '@types/node@22.9.3': + resolution: {integrity: sha512-F3u1fs/fce3FFk+DAxbxc78DF8x0cY09RRL8GnXLmkJ1jvx3TtPdWoTT5/NiYfI5ASqXBmfqJi9dZ3gxMx4lzw==} + + '@types/nodemailer@6.4.17': + resolution: {integrity: sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==} + '@types/prop-types@15.7.13': resolution: {integrity: sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==} @@ -706,6 +804,9 @@ packages: '@types/react@18.3.12': resolution: {integrity: sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==} + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + '@typescript-eslint/eslint-plugin@8.12.2': resolution: {integrity: sha512-gQxbxM8mcxBwaEmWdtLCIGLfixBMHhQjBqR8sVWNTPpcj45WlYL2IObS/DNMLH1DBP0n8qz+aiiLTGfopPEebw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -766,8 +867,8 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - '@walletconnect/core@2.17.2': - resolution: {integrity: sha512-O9VUsFg78CbvIaxfQuZMsHcJ4a2Z16DRz/O4S+uOAcGKhH/i/ln8hp864Tb+xRvifWSzaZ6CeAVxk657F+pscA==} + '@walletconnect/core@2.17.1': + resolution: {integrity: sha512-SMgJR5hEyEE/tENIuvlEb4aB9tmMXPzQ38Y61VgYBmwAFEhOHtpt8EDfnfRWqEhMyXuBXG4K70Yh8c67Yry+Xw==} engines: {node: '>=18'} '@walletconnect/environment@1.0.1': @@ -811,17 +912,17 @@ packages: '@walletconnect/safe-json@1.0.2': resolution: {integrity: sha512-Ogb7I27kZ3LPC3ibn8ldyUr5544t3/STow9+lzz7Sfo808YD7SBWk7SAsdBFlYgP2zDRy2hS3sKRcuSRM0OTmA==} - '@walletconnect/sign-client@2.17.2': - resolution: {integrity: sha512-/wigdCIQjlBXSWY43Id0IPvZ5biq4HiiQZti8Ljvx408UYjmqcxcBitbj2UJXMYkid7704JWAB2mw32I1HgshQ==} + '@walletconnect/sign-client@2.17.1': + resolution: {integrity: sha512-6rLw6YNy0smslH9wrFTbNiYrGsL3DrOsS5FcuU4gIN6oh8pGYOFZ5FiSyTTroc5tngOk3/Sd7dlGY9S7O4nveg==} '@walletconnect/time@1.0.2': resolution: {integrity: sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g==} - '@walletconnect/types@2.17.2': - resolution: {integrity: sha512-j/+0WuO00lR8ntu7b1+MKe/r59hNwYLFzW0tTmozzhfAlDL+dYwWasDBNq4AH8NbVd7vlPCQWmncH7/6FVtOfQ==} + '@walletconnect/types@2.17.1': + resolution: {integrity: sha512-aiUeBE3EZZTsZBv5Cju3D0PWAsZCMks1g3hzQs9oNtrbuLL6pKKU0/zpKwk4vGywszxPvC3U0tBCku9LLsH/0A==} - '@walletconnect/utils@2.17.2': - resolution: {integrity: sha512-T7eLRiuw96fgwUy2A5NZB5Eu87ukX8RCVoO9lji34RFV4o2IGU9FhTEWyd4QQKI8OuQRjSknhbJs0tU0r0faPw==} + '@walletconnect/utils@2.17.1': + resolution: {integrity: sha512-KL7pPwq7qUC+zcTmvxGqIyYanfHgBQ+PFd0TEblg88jM7EjuDLhjyyjtkhyE/2q7QgR7OanIK7pCpilhWvBsBQ==} '@walletconnect/window-getters@1.0.1': resolution: {integrity: sha512-vHp+HqzGxORPAN8gY03qnbTMnhqIwjeRJNOMOAzePRg4xVEEE2WvYsI9G2NMjOknA8hnuYbU3/hwLcKbjhc8+Q==} @@ -945,6 +1046,9 @@ packages: async-mutex@0.5.0: resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + atomic-sleep@1.0.0: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} @@ -964,6 +1068,9 @@ packages: resolution: {integrity: sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==} engines: {node: '>=4'} + axios@1.7.7: + resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -975,8 +1082,8 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - bn.js@4.12.1: - resolution: {integrity: sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==} + bn.js@4.12.0: + resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==} bn.js@5.2.1: resolution: {integrity: sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==} @@ -1076,6 +1183,10 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} @@ -1113,6 +1224,9 @@ packages: cookie-es@1.2.2: resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cosmiconfig-typescript-loader@5.1.0: resolution: {integrity: sha512-7PtBB+6FdsOvZyJtlF3hEPpACq7RQX6BVGsgC7/lfVXnKMvNCu/XY3ykreqG5w/rBNdu2z8LCIKoF3kpHHdHlA==} engines: {node: '>=v16'} @@ -1134,6 +1248,10 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + crossws@0.3.1: resolution: {integrity: sha512-HsZgeVYaG+b5zA+9PbIPGq4+J/CJynJuearykPsXx4V/eMhyQ5EDVg3Ak2FBZtVXCiOLu/U7IiwDHTr9MA+IKw==} @@ -1188,6 +1306,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -1199,6 +1321,10 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + destr@2.0.3: resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==} @@ -1228,10 +1354,27 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.1.0: + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + dot-prop@5.3.0: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} engines: {node: '>=8'} + dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + duplexify@4.1.3: resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} @@ -1244,8 +1387,8 @@ packages: elliptic@6.5.4: resolution: {integrity: sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==} - elliptic@6.6.0: - resolution: {integrity: sha512-dpwoQcLc/2WLQvJvLRHKZ+f9FgOdjnq11rurqwekGQygGPsYSK29OMMD2WalatiqQ+XGFDglTNixpPfI+lpaAA==} + elliptic@6.5.7: + resolution: {integrity: sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==} emoji-regex@10.4.0: resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} @@ -1256,6 +1399,14 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + encoding-japanese@2.0.0: + resolution: {integrity: sha512-++P0RhebUC8MJAwJOsT93dT+5oc5oPImp1HubZpAuCZ5kTLnhuuBhKHj2jJeO/Gj93idPBWmIuQ9QWMe5rX3pQ==} + engines: {node: '>=8.10.0'} + + encoding-japanese@2.1.0: + resolution: {integrity: sha512-58XySVxUgVlBikBTbQ8WdDxBDHIdXucB16LO5PBHR8t75D54wQrNo4cg+58+R1CtJfKnsVsvt9XlteRaR8xw1w==} + engines: {node: '>=8.10.0'} + end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} @@ -1263,6 +1414,10 @@ packages: resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==} engines: {node: '>=10.13.0'} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -1494,6 +1649,15 @@ packages: flatted@3.3.1: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} @@ -1501,6 +1665,10 @@ packages: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} + form-data@4.0.1: + resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} + engines: {node: '>= 6'} + fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} @@ -1508,6 +1676,10 @@ packages: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} + fs-extra@11.2.0: + resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} + engines: {node: '>=14.14'} + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -1634,9 +1806,20 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + hmac-drbg@1.0.1: resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} + html-to-text@9.0.5: + resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} + engines: {node: '>=14'} + + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + http-shutdown@1.2.2: resolution: {integrity: sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} @@ -1650,6 +1833,14 @@ packages: engines: {node: '>=18'} hasBin: true + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + idb-keyval@6.2.1: resolution: {integrity: sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==} @@ -1657,6 +1848,14 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + imap-simple@5.1.0: + resolution: {integrity: sha512-FLZm1v38C5ekN46l/9X5gBRNMQNVc5TSLYQ3Hsq3xBLvKwt1i5fcuShyth8MYMPuvId1R46oaPNrH92hFGHr/g==} + engines: {node: '>=6'} + + imap@0.8.19: + resolution: {integrity: sha512-z5DxEA1uRnZG73UcPA4ES5NSCGnPuuouUx43OPX7KZx1yzq3N8/vx2mtXEShT5inxB3pRgnfG1hijfu7XN2YMw==} + engines: {node: '>=0.8.0'} + import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} @@ -1791,6 +1990,9 @@ packages: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} + is-promise@1.0.1: + resolution: {integrity: sha512-mjWH5XxnhMA8cFnDchr6qRP9S/kLntKuEfIYku+PaN1CnS8v+OG9O/BKpRCVRJvpIkgAZm0Pf5Is3iSSOILlcg==} + is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} @@ -1845,6 +2047,9 @@ packages: resolution: {integrity: sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw==} engines: {node: '>=18'} + isarray@0.0.1: + resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} + isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -1870,8 +2075,8 @@ packages: resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} hasBin: true - jiti@2.4.0: - resolution: {integrity: sha512-H5UpaUI+aHOqZXlYOaFP/8AzKsg+guWu+Pr3Y8i7+Y3zr1aXAvCvTAQ1RxSc6oVD8R8c7brgNtTVP91E7upH/g==} + jiti@2.3.3: + resolution: {integrity: sha512-EX4oNDwcXSivPrw2qKH2LB5PoFxEvgtv2JgwW0bU858HoLQ+kutSvjLMUqBd0PeJYEinLWhoI9Ol0eYMqj/wNQ==} hasBin: true js-sha3@0.8.0: @@ -1927,10 +2132,31 @@ packages: resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} engines: {node: '>=0.10'} + leac@0.6.0: + resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + libbase64@1.2.1: + resolution: {integrity: sha512-l+nePcPbIG1fNlqMzrh68MLkX/gTxk/+vdvAb388Ssi7UuUN31MI44w4Yf33mM3Cm4xDfw48mdf3rkdHszLNew==} + + libbase64@1.3.0: + resolution: {integrity: sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==} + + libmime@5.2.0: + resolution: {integrity: sha512-X2U5Wx0YmK0rXFbk67ASMeqYIkZ6E5vY7pNWRKtnNzqjvdYYG8xtPDpCnuUEnPU9vlgNev+JoSrcaKSUaNvfsw==} + + libmime@5.3.5: + resolution: {integrity: sha512-nSlR1yRZ43L3cZCiWEw7ali3jY29Hz9CQQ96Oy+sSspYnIP5N54ucOPHqooBsXzwrX1pwn13VUE05q4WmzfaLg==} + + libqp@2.0.1: + resolution: {integrity: sha512-Ka0eC5LkF3IPNQHJmYBWljJsw0UvM6j+QdKRbWyCdTmYwvIDE6a7bCm0UkTAL/K+3KXK5qXT/ClcInU01OpdLg==} + + libqp@2.1.0: + resolution: {integrity: sha512-O6O6/fsG5jiUVbvdgT7YX3xY3uIadR6wEZ7+vy9u7PKHAlSEB6blvC1o5pHBjgsi95Uo0aiBBdkyFecj6jtb7A==} + lilconfig@2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} @@ -1942,6 +2168,9 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + lint-staged@15.2.10: resolution: {integrity: sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg==} engines: {node: '>=18.12.0'} @@ -2010,6 +2239,12 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + mailparser@3.7.1: + resolution: {integrity: sha512-RCnBhy5q8XtB3mXzxcAfT1huNqN93HTYYyL6XawlIKycfxM/rXPg9tXoZ7D46+SgCS1zxKzw+BayDQSvncSTTw==} + + mailsplit@5.4.0: + resolution: {integrity: sha512-wnYxX5D5qymGIPYLwnp6h8n1+6P6vz/MJn5AzGjZ8pwICWssL+CCQjWBIToOVHASmATot4ktvlLo6CyLfOXWYA==} + meow@12.1.1: resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} engines: {node: '>=16.10'} @@ -2025,6 +2260,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + mime@3.0.0: resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} engines: {node: '>=10.0.0'} @@ -2058,8 +2301,12 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} - mlly@1.7.3: - resolution: {integrity: sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==} + mlly@1.7.2: + resolution: {integrity: sha512-tN3dvVHYVz4DhSXinXIk7u9syPYaJvio118uomkovAtWBT+RdbP6Lfh/5Lvo519YMmwBafwlh20IPTXIStscpA==} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2121,6 +2368,17 @@ packages: node-releases@2.0.18: resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + nodeify@1.0.1: + resolution: {integrity: sha512-n7C2NyEze8GCo/z73KdbjRsBiLbv6eBn1FxwYKQ23IqGo7pQY3mhQan61Sv7eEDJCiyUjTVrVkXTzJCo1dW7Aw==} + + nodemailer@6.9.13: + resolution: {integrity: sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==} + engines: {node: '>=6.0.0'} + + nodemailer@6.9.16: + resolution: {integrity: sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==} + engines: {node: '>=6.0.0'} + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -2223,6 +2481,9 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parseley@0.12.1: + resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -2253,6 +2514,9 @@ packages: pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + peberminta@0.9.0: + resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2357,12 +2621,22 @@ packages: process-warning@1.0.0: resolution: {integrity: sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==} + promise@1.3.0: + resolution: {integrity: sha512-R9WrbTF3EPkVtWjp7B7umQGVndpsi+rsDAfrR4xAALQpFLa/+2OriecLhawxzvii2gd9+DZFwROWDuUUaqS5yA==} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + psl@1.9.0: resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2380,9 +2654,18 @@ packages: quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + quoted-printable@1.0.1: + resolution: {integrity: sha512-cihC68OcGiQOjGiXuo5Jk6XHANTHl1K4JLk/xlEJRTIXfy19Sg6XzB95XonYgr+1rB88bCpr7WZE7D7AlZow4g==} + hasBin: true + radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + react-dom@19.0.0-rc-02c0e824-20241028: resolution: {integrity: sha512-LrZf3DfHL6Fs07wwlUCHrzFTCMM19yA99MvJpfLokN4I2nBAZvREGZjZAn8VPiSfN72+i9j1eL4wB8gC695F3Q==} peerDependencies: @@ -2391,6 +2674,10 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + react@19.0.0-rc-02c0e824-20241028: resolution: {integrity: sha512-GbZ7hpPHQMiEu53BqEaPQVM/4GG4hARo+mqEEnx4rYporDvNvUjutiAFxYFSbu6sgHwcr7LeFv8htEOwALVA2A==} engines: {node: '>=0.10.0'} @@ -2398,6 +2685,9 @@ packages: read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + readable-stream@1.1.14: + resolution: {integrity: sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -2485,9 +2775,22 @@ packages: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + scheduler@0.25.0-rc-02c0e824-20241028: resolution: {integrity: sha512-GysnKjmMSaWcwsKTLzeJO0IhU3EyIiC0ivJKE6yDNLqt3IMxDByx8b6lSNXRNdN+ULUY0WLLjSPaZ0LuU/GnTg==} + selderee@0.11.0: + resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} + + semver@5.3.0: + resolution: {integrity: sha512-mfmm3/H9+67MCVix1h+IXTpDwL6710LyHuk7+cWC9T1mE0qz4iHhh6r4hU2wrIT9iTsAAC2XQRvfblL028cpLw==} + hasBin: true + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -2562,8 +2865,8 @@ packages: peerDependencies: starknet: ^6.9.0 - std-env@3.8.0: - resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} + std-env@3.7.0: + resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} stream-shift@1.0.3: resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} @@ -2614,6 +2917,9 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + string_decoder@0.10.31: + resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -2666,6 +2972,11 @@ packages: svelte-forms@2.3.1: resolution: {integrity: sha512-ExX9PM0JgvdOWlHl2ztD7XzLNPOPt9U5hBKV8sUAisMfcYWpPRnyz+6EFmh35BOBGJJmuhTDBGm5/7seLjOTIA==} + swr@1.3.0: + resolution: {integrity: sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 + system-architecture@0.1.0: resolution: {integrity: sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==} engines: {node: '>=18'} @@ -2702,6 +3013,10 @@ packages: tinyexec@0.3.1: resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} + tlds@1.252.0: + resolution: {integrity: sha512-GA16+8HXvqtfEnw/DTcwB0UU354QE1n3+wh08oFjr6Znl7ZLAeUgYzCcK+/CCrOyE0vnHR8/pu3XXG3vDijXpQ==} + hasBin: true + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -2769,6 +3084,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + ufo@1.5.4: resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} @@ -2799,19 +3117,19 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} - unstorage@1.13.1: - resolution: {integrity: sha512-ELexQHUrG05QVIM/iUeQNdl9FXDZhqLJ4yP59fnmn2jGUh0TEulwOgov1ubOb3Gt2ZGK/VMchJwPDNVEGWQpRg==} + unstorage@1.12.0: + resolution: {integrity: sha512-ARZYTXiC+e8z3lRM7/qY9oyaOkaozCeNd2xoz7sYK9fv7OLGhVsf+BZbmASqiK/HTZ7T6eAlnVq9JynZppyk3w==} peerDependencies: '@azure/app-configuration': ^1.7.0 '@azure/cosmos': ^4.1.1 '@azure/data-tables': ^13.2.2 - '@azure/identity': ^4.5.0 - '@azure/keyvault-secrets': ^4.9.0 - '@azure/storage-blob': ^12.25.0 + '@azure/identity': ^4.4.1 + '@azure/keyvault-secrets': ^4.8.0 + '@azure/storage-blob': ^12.24.0 '@capacitor/preferences': ^6.0.2 - '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 + '@netlify/blobs': ^6.5.0 || ^7.0.0 '@planetscale/database': ^1.19.0 - '@upstash/redis': ^1.34.3 + '@upstash/redis': ^1.34.0 '@vercel/kv': ^1.0.1 idb-keyval: ^6.2.1 ioredis: ^5.4.1 @@ -2865,9 +3183,22 @@ packages: url-parse@1.5.10: resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + utf7@1.0.2: + resolution: {integrity: sha512-qQrPtYLLLl12NF4DrM9CvfkxkYI97xOb5dsnGZHE3teFr0tWiEZ9UdgMPczv24vl708cYMpe6mGXGHrotIp3Bw==} + + utf8@2.1.2: + resolution: {integrity: sha512-QXo+O/QkLP/x1nyi54uQiG0XrODxdysuQvE5dtVqv7F5K2Qb6FsN+qbr6KhF5wQ20tfcV3VQp0/2x1e1MRSPWg==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuencode@0.0.4: + resolution: {integrity: sha512-yEEhCuCi5wRV7Z5ZVf9iV2gWMvUZqKJhAs1ecFdKJ0qzbyaVelmsE3QjYAamehfp9FKLiZbKldd+jklG3O0LfA==} + + uuid@11.0.3: + resolution: {integrity: sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==} + hasBin: true + viem@2.21.37: resolution: {integrity: sha512-JupwyttT4aJNnP9+kD7E8jorMS5VmgpC3hm3rl5zXsO8WNBTsP3JJqZUSg4AG6s2lTrmmpzS/qpmXMZu5gJw5Q==} peerDependencies: @@ -3423,70 +3754,66 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} - '@parcel/watcher-android-arm64@2.5.0': + '@parcel/watcher-android-arm64@2.4.1': optional: true - '@parcel/watcher-darwin-arm64@2.5.0': + '@parcel/watcher-darwin-arm64@2.4.1': optional: true - '@parcel/watcher-darwin-x64@2.5.0': + '@parcel/watcher-darwin-x64@2.4.1': optional: true - '@parcel/watcher-freebsd-x64@2.5.0': + '@parcel/watcher-freebsd-x64@2.4.1': optional: true - '@parcel/watcher-linux-arm-glibc@2.5.0': + '@parcel/watcher-linux-arm-glibc@2.4.1': optional: true - '@parcel/watcher-linux-arm-musl@2.5.0': + '@parcel/watcher-linux-arm64-glibc@2.4.1': optional: true - '@parcel/watcher-linux-arm64-glibc@2.5.0': + '@parcel/watcher-linux-arm64-musl@2.4.1': optional: true - '@parcel/watcher-linux-arm64-musl@2.5.0': + '@parcel/watcher-linux-x64-glibc@2.4.1': optional: true - '@parcel/watcher-linux-x64-glibc@2.5.0': + '@parcel/watcher-linux-x64-musl@2.4.1': optional: true - '@parcel/watcher-linux-x64-musl@2.5.0': - optional: true - - '@parcel/watcher-wasm@2.5.0': + '@parcel/watcher-wasm@2.4.1': dependencies: is-glob: 4.0.3 micromatch: 4.0.8 - '@parcel/watcher-win32-arm64@2.5.0': + '@parcel/watcher-win32-arm64@2.4.1': optional: true - '@parcel/watcher-win32-ia32@2.5.0': + '@parcel/watcher-win32-ia32@2.4.1': optional: true - '@parcel/watcher-win32-x64@2.5.0': + '@parcel/watcher-win32-x64@2.4.1': optional: true - '@parcel/watcher@2.5.0': + '@parcel/watcher@2.4.1': dependencies: detect-libc: 1.0.3 is-glob: 4.0.3 micromatch: 4.0.8 node-addon-api: 7.1.1 optionalDependencies: - '@parcel/watcher-android-arm64': 2.5.0 - '@parcel/watcher-darwin-arm64': 2.5.0 - '@parcel/watcher-darwin-x64': 2.5.0 - '@parcel/watcher-freebsd-x64': 2.5.0 - '@parcel/watcher-linux-arm-glibc': 2.5.0 - '@parcel/watcher-linux-arm-musl': 2.5.0 - '@parcel/watcher-linux-arm64-glibc': 2.5.0 - '@parcel/watcher-linux-arm64-musl': 2.5.0 - '@parcel/watcher-linux-x64-glibc': 2.5.0 - '@parcel/watcher-linux-x64-musl': 2.5.0 - '@parcel/watcher-win32-arm64': 2.5.0 - '@parcel/watcher-win32-ia32': 2.5.0 - '@parcel/watcher-win32-x64': 2.5.0 + '@parcel/watcher-android-arm64': 2.4.1 + '@parcel/watcher-darwin-arm64': 2.4.1 + '@parcel/watcher-darwin-x64': 2.4.1 + '@parcel/watcher-freebsd-x64': 2.4.1 + '@parcel/watcher-linux-arm-glibc': 2.4.1 + '@parcel/watcher-linux-arm64-glibc': 2.4.1 + '@parcel/watcher-linux-arm64-musl': 2.4.1 + '@parcel/watcher-linux-x64-glibc': 2.4.1 + '@parcel/watcher-linux-x64-musl': 2.4.1 + '@parcel/watcher-win32-arm64': 2.4.1 + '@parcel/watcher-win32-ia32': 2.4.1 + '@parcel/watcher-win32-x64': 2.4.1 '@pkgjs/parseargs@0.11.0': optional: true @@ -3517,6 +3844,11 @@ snapshots: '@noble/curves': 1.3.0 '@noble/hashes': 1.3.3 + '@selderee/plugin-htmlparser2@0.11.0': + dependencies: + domhandler: 5.0.3 + selderee: 0.11.0 + '@stablelib/aead@1.0.1': {} '@stablelib/binary@1.0.1': @@ -3647,16 +3979,53 @@ snapshots: '@trpc/server@10.45.2': {} + '@types/axios@0.14.4': + dependencies: + axios: 1.7.7 + transitivePeerDependencies: + - debug + '@types/conventional-commits-parser@5.0.0': dependencies: '@types/node': 20.17.4 + '@types/fs-extra@11.0.4': + dependencies: + '@types/jsonfile': 6.1.4 + '@types/node': 22.9.3 + + '@types/imap-simple@4.2.9': + dependencies: + '@types/imap': 0.8.42 + '@types/node': 22.9.3 + + '@types/imap@0.8.42': + dependencies: + '@types/node': 22.9.3 + '@types/json5@0.0.29': {} + '@types/jsonfile@6.1.4': + dependencies: + '@types/node': 22.9.3 + + '@types/mailparser@3.4.5': + dependencies: + '@types/node': 22.9.3 + iconv-lite: 0.6.3 + '@types/node@20.17.4': dependencies: undici-types: 6.19.8 + '@types/node@22.9.3': + dependencies: + undici-types: 6.19.8 + + '@types/nodemailer@6.4.17': + dependencies: + '@types/node': 22.9.3 + '@types/prop-types@15.7.13': {} '@types/react-dom@18.3.1': @@ -3668,6 +4037,8 @@ snapshots: '@types/prop-types': 15.7.13 csstype: 3.1.3 + '@types/uuid@10.0.0': {} + '@typescript-eslint/eslint-plugin@8.12.2(@typescript-eslint/parser@8.12.2(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -3751,7 +4122,7 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@walletconnect/core@2.17.2': + '@walletconnect/core@2.17.1': dependencies: '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-provider': 1.0.14 @@ -3764,8 +4135,8 @@ snapshots: '@walletconnect/relay-auth': 1.0.4 '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.17.2 - '@walletconnect/utils': 2.17.2 + '@walletconnect/types': 2.17.1 + '@walletconnect/utils': 2.17.1 '@walletconnect/window-getters': 1.0.1 events: 3.3.0 lodash.isequal: 4.5.0 @@ -3833,7 +4204,7 @@ snapshots: dependencies: '@walletconnect/safe-json': 1.0.2 idb-keyval: 6.2.1 - unstorage: 1.13.1(idb-keyval@6.2.1) + unstorage: 1.12.0(idb-keyval@6.2.1) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -3870,16 +4241,16 @@ snapshots: dependencies: tslib: 1.14.1 - '@walletconnect/sign-client@2.17.2': + '@walletconnect/sign-client@2.17.1': dependencies: - '@walletconnect/core': 2.17.2 + '@walletconnect/core': 2.17.1 '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/logger': 2.1.2 '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.17.2 - '@walletconnect/utils': 2.17.2 + '@walletconnect/types': 2.17.1 + '@walletconnect/utils': 2.17.1 events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -3902,7 +4273,7 @@ snapshots: dependencies: tslib: 1.14.1 - '@walletconnect/types@2.17.2': + '@walletconnect/types@2.17.1': dependencies: '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 @@ -3925,7 +4296,7 @@ snapshots: - '@vercel/kv' - ioredis - '@walletconnect/utils@2.17.2': + '@walletconnect/utils@2.17.1': dependencies: '@ethersproject/hash': 5.7.0 '@ethersproject/transactions': 5.7.0 @@ -3940,11 +4311,11 @@ snapshots: '@walletconnect/relay-auth': 1.0.4 '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.17.2 + '@walletconnect/types': 2.17.1 '@walletconnect/window-getters': 1.0.1 '@walletconnect/window-metadata': 1.0.1 detect-browser: 5.3.0 - elliptic: 6.6.0 + elliptic: 6.5.7 query-string: 7.1.3 uint8arrays: 3.1.0 transitivePeerDependencies: @@ -4110,6 +4481,8 @@ snapshots: dependencies: tslib: 2.8.0 + asynckit@0.4.0: {} + atomic-sleep@1.0.0: {} autoprefixer@10.4.20(postcss@8.4.49): @@ -4128,13 +4501,21 @@ snapshots: axe-core@4.10.2: {} + axios@1.7.7: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.1 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axobject-query@4.1.0: {} balanced-match@1.0.2: {} binary-extensions@2.3.0: {} - bn.js@4.12.1: {} + bn.js@4.12.0: {} bn.js@5.2.1: {} @@ -4251,6 +4632,10 @@ snapshots: colorette@2.0.20: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@12.1.0: {} commander@4.1.1: {} @@ -4283,6 +4668,8 @@ snapshots: cookie-es@1.2.2: {} + core-util-is@1.0.3: {} + cosmiconfig-typescript-loader@5.1.0(@types/node@20.17.4)(cosmiconfig@9.0.0(typescript@5.6.3))(typescript@5.6.3): dependencies: '@types/node': 20.17.4 @@ -4305,6 +4692,12 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + crossws@0.3.1: dependencies: uncrypto: 0.1.3 @@ -4347,6 +4740,8 @@ snapshots: deep-is@0.1.4: {} + deepmerge@4.3.1: {} + define-data-property@1.1.4: dependencies: es-define-property: 1.0.0 @@ -4361,6 +4756,8 @@ snapshots: defu@6.1.4: {} + delayed-stream@1.0.0: {} + destr@2.0.3: {} detect-browser@5.3.0: {} @@ -4382,10 +4779,30 @@ snapshots: dependencies: esutils: 2.0.3 + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.1.0: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dot-prop@5.3.0: dependencies: is-obj: 2.0.0 + dotenv@16.4.5: {} + duplexify@4.1.3: dependencies: end-of-stream: 1.4.4 @@ -4399,7 +4816,7 @@ snapshots: elliptic@6.5.4: dependencies: - bn.js: 4.12.1 + bn.js: 4.12.0 brorand: 1.1.0 hash.js: 1.1.7 hmac-drbg: 1.0.1 @@ -4407,9 +4824,9 @@ snapshots: minimalistic-assert: 1.0.1 minimalistic-crypto-utils: 1.0.1 - elliptic@6.6.0: + elliptic@6.5.7: dependencies: - bn.js: 4.12.1 + bn.js: 4.12.0 brorand: 1.1.0 hash.js: 1.1.7 hmac-drbg: 1.0.1 @@ -4423,6 +4840,10 @@ snapshots: emoji-regex@9.2.2: {} + encoding-japanese@2.0.0: {} + + encoding-japanese@2.1.0: {} + end-of-stream@1.4.4: dependencies: once: 1.4.0 @@ -4432,6 +4853,8 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.2.1 + entities@4.5.0: {} + env-paths@2.2.1: {} environment@1.1.0: {} @@ -4687,7 +5110,7 @@ snapshots: '@ungap/structured-clone': 1.2.0 ajv: 6.12.6 chalk: 4.1.2 - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 debug: 4.3.7 doctrine: 3.0.0 escape-string-regexp: 4.0.0 @@ -4818,6 +5241,8 @@ snapshots: flatted@3.3.1: {} + follow-redirects@1.15.9: {} + for-each@0.3.3: dependencies: is-callable: 1.2.7 @@ -4827,6 +5252,12 @@ snapshots: cross-spawn: 7.0.3 signal-exit: 4.1.0 + form-data@4.0.1: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + fraction.js@4.3.7: {} fs-extra@10.1.0: @@ -4835,6 +5266,12 @@ snapshots: jsonfile: 6.1.0 universalify: 2.0.1 + fs-extra@11.2.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + fs.realpath@1.0.0: {} fsevents@2.3.2: @@ -4975,22 +5412,61 @@ snapshots: dependencies: function-bind: 1.1.2 + he@1.2.0: {} + hmac-drbg@1.0.1: dependencies: hash.js: 1.1.7 minimalistic-assert: 1.0.1 minimalistic-crypto-utils: 1.0.1 + html-to-text@9.0.5: + dependencies: + '@selderee/plugin-htmlparser2': 0.11.0 + deepmerge: 4.3.1 + dom-serializer: 2.0.0 + htmlparser2: 8.0.2 + selderee: 0.11.0 + + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + entities: 4.5.0 + http-shutdown@1.2.2: {} human-signals@5.0.0: {} husky@9.1.6: {} + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + idb-keyval@6.2.1: {} ignore@5.3.2: {} + imap-simple@5.1.0: + dependencies: + iconv-lite: 0.4.24 + imap: 0.8.19 + nodeify: 1.0.1 + quoted-printable: 1.0.1 + utf8: 2.1.2 + uuencode: 0.0.4 + + imap@0.8.19: + dependencies: + readable-stream: 1.1.14 + utf7: 1.0.2 + import-fresh@3.3.0: dependencies: parent-module: 1.0.1 @@ -5104,6 +5580,8 @@ snapshots: is-path-inside@3.0.3: {} + is-promise@1.0.1: {} + is-promise@4.0.0: {} is-regex@1.1.4: @@ -5154,6 +5632,8 @@ snapshots: dependencies: system-architecture: 0.1.0 + isarray@0.0.1: {} + isarray@2.0.5: {} isexe@2.0.0: {} @@ -5185,7 +5665,7 @@ snapshots: jiti@1.21.6: {} - jiti@2.4.0: {} + jiti@2.3.3: {} js-sha3@0.8.0: {} @@ -5236,17 +5716,45 @@ snapshots: dependencies: language-subtag-registry: 0.3.23 + leac@0.6.0: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 type-check: 0.4.0 + libbase64@1.2.1: {} + + libbase64@1.3.0: {} + + libmime@5.2.0: + dependencies: + encoding-japanese: 2.0.0 + iconv-lite: 0.6.3 + libbase64: 1.2.1 + libqp: 2.0.1 + + libmime@5.3.5: + dependencies: + encoding-japanese: 2.1.0 + iconv-lite: 0.6.3 + libbase64: 1.3.0 + libqp: 2.1.0 + + libqp@2.0.1: {} + + libqp@2.1.0: {} + lilconfig@2.1.0: {} lilconfig@3.1.2: {} lines-and-columns@1.2.4: {} + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + lint-staged@15.2.10: dependencies: chalk: 5.3.0 @@ -5264,8 +5772,8 @@ snapshots: listhen@1.9.0: dependencies: - '@parcel/watcher': 2.5.0 - '@parcel/watcher-wasm': 2.5.0 + '@parcel/watcher': 2.4.1 + '@parcel/watcher-wasm': 2.4.1 citty: 0.1.6 clipboardy: 4.0.0 consola: 3.2.3 @@ -5274,11 +5782,11 @@ snapshots: get-port-please: 3.1.2 h3: 1.13.0 http-shutdown: 1.2.2 - jiti: 2.4.0 - mlly: 1.7.3 + jiti: 2.3.3 + mlly: 1.7.2 node-forge: 1.3.1 pathe: 1.1.2 - std-env: 3.8.0 + std-env: 3.7.0 ufo: 1.5.4 untun: 0.1.3 uqr: 0.1.2 @@ -5338,6 +5846,25 @@ snapshots: lru-cache@10.4.3: {} + mailparser@3.7.1: + dependencies: + encoding-japanese: 2.1.0 + he: 1.2.0 + html-to-text: 9.0.5 + iconv-lite: 0.6.3 + libmime: 5.3.5 + linkify-it: 5.0.0 + mailsplit: 5.4.0 + nodemailer: 6.9.13 + punycode.js: 2.3.1 + tlds: 1.252.0 + + mailsplit@5.4.0: + dependencies: + libbase64: 1.2.1 + libmime: 5.2.0 + libqp: 2.0.1 + meow@12.1.1: {} merge-stream@2.0.0: {} @@ -5349,6 +5876,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + mime@3.0.0: {} mimic-fn@4.0.0: {} @@ -5371,13 +5904,15 @@ snapshots: minipass@7.1.2: {} - mlly@1.7.3: + mlly@1.7.2: dependencies: acorn: 8.14.0 pathe: 1.1.2 pkg-types: 1.2.1 ufo: 1.5.4 + mri@1.2.0: {} + ms@2.1.3: {} multiformats@9.9.0: {} @@ -5430,6 +5965,15 @@ snapshots: node-releases@2.0.18: {} + nodeify@1.0.1: + dependencies: + is-promise: 1.0.1 + promise: 1.3.0 + + nodemailer@6.9.13: {} + + nodemailer@6.9.16: {} + normalize-path@3.0.0: {} normalize-range@0.1.2: {} @@ -5540,6 +6084,11 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parseley@0.12.1: + dependencies: + leac: 0.6.0 + peberminta: 0.9.0 + path-exists@4.0.0: {} path-exists@5.0.0: {} @@ -5559,6 +6108,8 @@ snapshots: pathe@1.1.2: {} + peberminta@0.9.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -5593,7 +6144,7 @@ snapshots: pkg-types@1.2.1: dependencies: confbox: 0.1.8 - mlly: 1.7.3 + mlly: 1.7.2 pathe: 1.1.2 playwright-core@1.48.2: {} @@ -5655,14 +6206,22 @@ snapshots: process-warning@1.0.0: {} + promise@1.3.0: + dependencies: + is-promise: 1.0.1 + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 object-assign: 4.1.1 react-is: 16.13.1 + proxy-from-env@1.1.0: {} + psl@1.9.0: {} + punycode.js@2.3.1: {} + punycode@2.3.1: {} query-string@7.1.3: @@ -5678,8 +6237,18 @@ snapshots: quick-format-unescaped@4.0.4: {} + quoted-printable@1.0.1: + dependencies: + utf8: 2.1.2 + radix3@1.1.2: {} + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + react-dom@19.0.0-rc-02c0e824-20241028(react@19.0.0-rc-02c0e824-20241028): dependencies: react: 19.0.0-rc-02c0e824-20241028 @@ -5687,12 +6256,23 @@ snapshots: react-is@16.13.1: {} + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + react@19.0.0-rc-02c0e824-20241028: {} read-cache@1.0.0: dependencies: pify: 2.3.0 + readable-stream@1.1.14: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 0.0.1 + string_decoder: 0.10.31 + readable-stream@3.6.2: dependencies: inherits: 2.0.4 @@ -5784,8 +6364,20 @@ snapshots: safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + scheduler@0.25.0-rc-02c0e824-20241028: {} + selderee@0.11.0: + dependencies: + parseley: 0.12.1 + + semver@5.3.0: {} + semver@6.3.1: {} semver@7.6.3: {} @@ -5900,7 +6492,7 @@ snapshots: '@starknet-io/types-js': 0.7.7 '@trpc/client': 10.45.2(@trpc/server@10.45.2) '@trpc/server': 10.45.2 - '@walletconnect/sign-client': 2.17.2 + '@walletconnect/sign-client': 2.17.1 bowser: 2.11.0 detect-browser: 5.3.0 eventemitter3: 5.0.1 @@ -5926,7 +6518,7 @@ snapshots: - ioredis - utf-8-validate - std-env@3.8.0: {} + std-env@3.7.0: {} stream-shift@1.0.3: {} @@ -5999,6 +6591,8 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.0.0 + string_decoder@0.10.31: {} + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -6042,6 +6636,10 @@ snapshots: dependencies: is-promise: 4.0.0 + swr@1.3.0(react@18.3.1): + dependencies: + react: 18.3.1 + system-architecture@0.1.0: {} tailwindcss@3.4.15: @@ -6093,6 +6691,8 @@ snapshots: tinyexec@0.3.1: {} + tlds@1.252.0: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -6170,6 +6770,8 @@ snapshots: typescript@5.6.3: {} + uc.micro@2.1.0: {} + ufo@1.5.4: {} uint8arrays@3.1.0: @@ -6201,15 +6803,15 @@ snapshots: universalify@2.0.1: {} - unstorage@1.13.1(idb-keyval@6.2.1): + unstorage@1.12.0(idb-keyval@6.2.1): dependencies: anymatch: 3.1.3 chokidar: 3.6.0 - citty: 0.1.6 destr: 2.0.3 h3: 1.13.0 listhen: 1.9.0 lru-cache: 10.4.3 + mri: 1.2.0 node-fetch-native: 1.6.4 ofetch: 1.4.1 ufo: 1.5.4 @@ -6241,8 +6843,18 @@ snapshots: querystringify: 2.2.0 requires-port: 1.0.0 + utf7@1.0.2: + dependencies: + semver: 5.3.0 + + utf8@2.1.2: {} + util-deprecate@1.0.2: {} + uuencode@0.0.4: {} + + uuid@11.0.3: {} + viem@2.21.37(typescript@5.6.3)(zod@3.23.8): dependencies: '@adraffy/ens-normalize': 1.11.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..a395802 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - "src" + - "e2e" diff --git a/test-results/.last-run.json b/test-results/.last-run.json new file mode 100644 index 0000000..0bf087c --- /dev/null +++ b/test-results/.last-run.json @@ -0,0 +1,6 @@ +{ + "status": "failed", + "failedTests": [ + "a58aee9b88d8075424d9-b89a730b188635a04182" + ] +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 9194388..36c5d3b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,5 +23,5 @@ } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "exclude": ["node_modules", "e2e"] }