diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index b06c9ab..f16150f 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,175 @@ 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 $(seq 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 }} + E2E_TESTNET_SEED3: ${{ secrets.E2E_TESTNET_SEED3 }} + + 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 $(seq 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 $(seq 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 diff --git a/.gitignore b/.gitignore index b3b4187..00bf8e4 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,10 @@ next-env.d.ts .playwright-* playwright-report* -.eslintcache \ No newline at end of file +.eslintcache +artifacts +argent-x-dist + +/e2e/node_modules/ + +settings.json \ No newline at end of file diff --git a/README.md b/README.md index b36348a..addd27e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # demo-dapp-starknet +This dapp is used as an example for integrating [Starknetkit](https://github.com/argentlabs/starknetkit) with [Starknet-react][https://github.com/apibara/starknet-react]. + This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). ## Getting Started 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..4a8b657 --- /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": "DONWNLOAD_ARGENTX_BUILD=1 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..f0c13ab --- /dev/null +++ b/e2e/playwright.config.ts @@ -0,0 +1,51 @@ +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: 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", + globalSetup: "./src/shared/cfg/global.setup.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..a7c6ecc --- /dev/null +++ b/e2e/src/argent-x/languages/en/index.ts @@ -0,0 +1,174 @@ +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", + }, + sign: { + accept: "Sign", + reject: "Reject", + }, + transaction: { + accept: "Confirm", + reject: "Reject", + }, + network: { + addNetwork: "Add Network", + switchNetwork: "Switch network", + }, +} 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..7160414 --- /dev/null +++ b/e2e/src/argent-x/page-objects/Dapps.ts @@ -0,0 +1,185 @@ +import { ChromiumBrowserContext, Page, expect } from "@playwright/test" + +import { lang } from "../languages" +import Navigation from "./Navigation" + +const dappUrl = "http://localhost:3000" +const dappName = "localhost" +export default class Dapps extends Navigation { + private dApp: Page + constructor(page: Page) { + super(page) + this.dApp = 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}")`, + ) + } + + async requestConnectionFromDapp({ + browserContext, + useStarknetKitModal = false, + }: { + browserContext: ChromiumBrowserContext + useStarknetKitModal?: boolean + }) { + //open dapp page + this.dApp = await browserContext.newPage() + await this.dApp.setViewportSize({ width: 1080, height: 720 }) + await this.dApp.goto("chrome://inspect/#extensions") + await this.dApp.waitForTimeout(1000) + await this.dApp.goto(dappUrl) + + await this.dApp.getByRole("button", { name: "Connection" }).click() + if (useStarknetKitModal) { + await this.dApp.getByRole("button", { name: "Starknetkit Modal" }).click() + await this.dApp + .locator("#starknetkit-modal-container") + .getByRole("button", { name: "Argent X" }) + .click() + } else { + await expect( + this.dApp.locator('button :text-is("Argent X")'), + ).toBeVisible() + } + + await this.dApp.locator('button :text-is("Argent X")').click() + } + + async sendERC20transaction({ + browserContext, + type, + }: { + browserContext: ChromiumBrowserContext + type: "ERC20" | "Multicall" + }) { + const [extension, dappPage] = browserContext.pages() + const dialogPromise = this.dApp.waitForEvent("dialog") + + dappPage.bringToFront() + + // avoid too many requests in a short time, causing user to reject + await this.dApp.waitForTimeout(2500) + await this.dApp.locator('button :text-is("Transactions")').click() + await this.dApp.waitForTimeout(2500) + await this.dApp.locator(`button :text-is("Send ${type}")`).click() + + await expect(extension.getByText("Review transaction")).toBeVisible() + await expect(extension.getByText("Confirm")).toBeVisible() + const [, dialog] = await Promise.all([ + extension.getByText("Confirm").click(), + dialogPromise, + ]) + + expect(dialog.message()).toContain("Transaction sent") + await dialog.accept() + } + + async signMessage({ + browserContext, + }: { + browserContext: ChromiumBrowserContext + }) { + const [extension, dappPage] = browserContext.pages() + + dappPage.bringToFront() + + await this.dApp.locator('button :text-is("Signing")').click() + await this.dApp.locator("[name=short-text]").fill("some message to sign") + await this.dApp.locator('button[type="submit"]').click() + + extension.bringToFront() + await this.page.locator(`button:text-is("${lang.sign.accept}")`).click() + dappPage.bringToFront() + + await Promise.all([ + expect(this.dApp.getByText("Signer", { exact: true })).toBeVisible(), + expect(this.dApp.locator("[name=signer_r]")).toBeVisible(), + expect(this.dApp.locator("[name=signer_s]")).toBeVisible(), + ]) + } + + async network({ + browserContext, + type, + }: { + browserContext: ChromiumBrowserContext + type: "Add" | "Change" + }) { + const [extension, dappPage] = browserContext.pages() + dappPage.bringToFront() + await this.dApp.locator('button :text-is("Network")').click() + await this.dApp.locator(`button :text-is("${type} Network")`).click() + + extension.bringToFront() + await this.dApp.waitForTimeout(1000) + await this.page + .locator( + `button:text-is("${type === "Add" ? lang.network.addNetwork : lang.network.switchNetwork}")`, + ) + .click() + + if (type === "Change") { + await this.accept.click() + } + } + + async addToken({ + browserContext, + }: { + browserContext: ChromiumBrowserContext + }) { + const [extension, dappPage] = browserContext.pages() + dappPage.bringToFront() + await this.dApp.locator('button :text-is("ERC20")').click() + await this.dApp.locator(`button :text-is("Add Token")`).click() + + extension.bringToFront() + await this.dApp.waitForTimeout(1000) + await this.page.locator(`button:text-is("Add token")`).click() + } +} 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..955defa --- /dev/null +++ b/e2e/src/argent-x/page-objects/Network.ts @@ -0,0 +1,96 @@ +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.locator(`[aria-label="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 openNetworkSelector() { + await this.networkSelector.click() + await this.page.locator('[data-testid="network-switcher-button"]').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/connect.spec.ts b/e2e/src/argent-x/specs/connect.spec.ts new file mode 100644 index 0000000..4865838 --- /dev/null +++ b/e2e/src/argent-x/specs/connect.spec.ts @@ -0,0 +1,45 @@ +import { expect } from "@playwright/test" + +import test from "../test" +import config from "../../../config" + +test.describe("Connect", () => { + + for (const useStarknetKitModal of [true, false] as const) { + test(`connect from testDapp using starknetKitModal ${useStarknetKitModal}`, async ({ + extension, + browserContext, + }) => { + //setup wallet + await extension.open() + await extension.recoverWallet(config.testSeed3!) + await expect(extension.network.networkSelector).toBeVisible() + await extension.network.selectDefaultNetwork() + + 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 authorised dapps" }), + ).toBeVisible() + }) + } +}) diff --git a/e2e/src/argent-x/specs/network.spec.ts b/e2e/src/argent-x/specs/network.spec.ts new file mode 100644 index 0000000..a5c726a --- /dev/null +++ b/e2e/src/argent-x/specs/network.spec.ts @@ -0,0 +1,62 @@ +import test from "../test" +import config from "../../../config" +import { expect } from "@playwright/test" + +test.describe(`Network`, () => { + + test(`add a new network`, async ({ extension, browserContext }) => { + await extension.open() + await extension.recoverWallet(config.testSeed3!) + await expect(extension.network.networkSelector).toBeVisible() + await extension.network.selectDefaultNetwork() + + await extension.dapps.requestConnectionFromDapp({ + browserContext, + useStarknetKitModal: true, + }) + //accept connection from Argent X + await extension.dapps.accept.click() + + await extension.dapps.network({ + browserContext, + type: "Add", + }) + + await expect(extension.network.networkSelector).toBeVisible() + extension.network.openNetworkSelector() + + await expect( + extension.network.page.locator( + `button[role="menuitem"] span:text-is("ZORG")`, + ), + ).toBeVisible() + }) + + test(`switch network`, async ({ extension, browserContext }) => { + await extension.open() + await extension.recoverWallet(config.testSeed3!) + await expect(extension.network.networkSelector).toBeVisible() + await extension.network.selectDefaultNetwork() + + await extension.dapps.requestConnectionFromDapp({ + browserContext, + useStarknetKitModal: true, + }) + //accept connection from Argent X + await extension.dapps.accept.click() + + await extension.dapps.network({ + browserContext, + type: "Change", + }) + + await expect(extension.network.networkSelector).toBeVisible() + + const element = extension.network.page.locator( + '[aria-label="Show account list"]', + ) + + const innerText = await element.evaluate((el) => el.textContent) + expect(innerText).toContain("Mainnet") + }) +}) diff --git a/e2e/src/argent-x/specs/signMessage.spec.ts b/e2e/src/argent-x/specs/signMessage.spec.ts new file mode 100644 index 0000000..6b5fee5 --- /dev/null +++ b/e2e/src/argent-x/specs/signMessage.spec.ts @@ -0,0 +1,29 @@ +import { expect } from "@playwright/test" + +import test from "../test" +import config from "../../../config" + +test.describe("Sign message", () => { + + test(`sign a message from testDapp`, async ({ + extension, + browserContext, + }) => { + //setup wallet + await extension.open() + await extension.recoverWallet(config.testSeed3!) + await expect(extension.network.networkSelector).toBeVisible() + await extension.network.selectDefaultNetwork() + + await extension.dapps.requestConnectionFromDapp({ + browserContext, + useStarknetKitModal: true, + }) + //accept connection from Argent X + await extension.dapps.accept.click() + + await extension.dapps.signMessage({ + browserContext, + }) + }) +}) diff --git a/e2e/src/argent-x/specs/token.spec.ts b/e2e/src/argent-x/specs/token.spec.ts new file mode 100644 index 0000000..981524f --- /dev/null +++ b/e2e/src/argent-x/specs/token.spec.ts @@ -0,0 +1,29 @@ +import test from "../test" +import config from "../../../config" +import { expect } from "@playwright/test" + +test.describe(`Token`, () => { + + test(`add a new token`, async ({ extension, browserContext }) => { + await extension.open() + await extension.recoverWallet(config.testSeed3!) + await expect(extension.network.networkSelector).toBeVisible() + await extension.network.selectDefaultNetwork() + + await extension.dapps.requestConnectionFromDapp({ + browserContext, + useStarknetKitModal: true, + }) + //accept connection from Argent X + await extension.dapps.accept.click() + + await extension.dapps.addToken({ + browserContext, + }) + + await expect(extension.network.networkSelector).toBeVisible() + await expect( + extension.dapps.page.locator('[aria-label="Show account list"]'), + ).toBeVisible() + }) +}) diff --git a/e2e/src/argent-x/specs/transactions.spec.ts b/e2e/src/argent-x/specs/transactions.spec.ts new file mode 100644 index 0000000..2f26ba6 --- /dev/null +++ b/e2e/src/argent-x/specs/transactions.spec.ts @@ -0,0 +1,51 @@ +import { expect } from "@playwright/test" + +import test from "../test" +import config from "../../../config" + +test.describe(`Transactions`, () => { + test(`send an ERC20 from testDapp`, async ({ extension, browserContext }) => { + //setup wallet + await extension.open() + await extension.recoverWallet(config.testSeed3!) + await expect(extension.network.networkSelector).toBeVisible() + await extension.network.selectDefaultNetwork() + + await extension.dapps.requestConnectionFromDapp({ + browserContext, + useStarknetKitModal: true, + }) + //accept connection from Argent X + await extension.dapps.accept.click() + + await extension.dapps.sendERC20transaction({ + browserContext, + type: "ERC20", + }) + }) + + test(`send an Multicall from testDapp`, async ({ + extension, + browserContext, + }) => { + //setup wallet + await extension.open() + await extension.recoverWallet(config.testSeed3!) + await expect(extension.network.networkSelector).toBeVisible() + await extension.network.selectDefaultNetwork() + + console.log(extension.account.accountAddress) + + await extension.dapps.requestConnectionFromDapp({ + browserContext, + useStarknetKitModal: true, + }) + //accept connection from Argent X + await extension.dapps.accept.click() + + await extension.dapps.sendERC20transaction({ + browserContext, + type: "Multicall", + }) + }) +}) diff --git a/e2e/src/argent-x/test.ts b/e2e/src/argent-x/test.ts new file mode 100644 index 0000000..e0101bc --- /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 = process.env.ARGENTX_DIST_DIR! + 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.setup.ts b/e2e/src/shared/cfg/global.setup.ts new file mode 100644 index 0000000..883c313 --- /dev/null +++ b/e2e/src/shared/cfg/global.setup.ts @@ -0,0 +1,13 @@ +import { downloadGitHubRelease, unzip } from "../../argent-x/utils" + +export default async function downloadArgentXBuild() { + if (process.env.DONWNLOAD_ARGENTX_BUILD) { + console.log("Downloading ArgentX build") + const version = await downloadGitHubRelease() + const currentVersionDir = await unzip(version) + process.env.ARGENTX_DIST_DIR = currentVersionDir + console.log("ArgentX build downloaded:", version) + } else { + console.log("ArgentX build download skipped") + } +} \ No newline at end of file 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..c3e6b43 --- /dev/null +++ b/e2e/src/shared/src/SapoEmailClient.ts @@ -0,0 +1,128 @@ +import * as ImapClient from 'imap-simple'; +import { simpleParser } from 'mailparser'; + +// Define Connection type based on what imap-simple returns +type Connection = ReturnType extends Promise ? T : never; + +interface EmailConfig { + imap: { + user: string; + password: string; + host: string; + port: number; + tls: boolean; + tlsOptions: { rejectUnauthorized: boolean }; + authTimeout: number; + }; +} + +export default class SapoEmailClient { + private readonly config: EmailConfig; + private static readonly TRASH_FOLDER = 'Lixo'; + private static readonly CHECK_INTERVAL = 3000; + private static readonly EMAIL_AGE_THRESHOLD = 20000; + + 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(): Promise { + try { + return await ImapClient.connect(this.config); + } catch (error) { + throw new Error(`Failed to connect to IMAP server: ${error.message}`); + } + } + + private async moveToTrash(connection: Connection, uid: number): Promise { + try { + await connection.moveMessage(uid, SapoEmailClient.TRASH_FOLDER); + console.log(`Successfully moved message ${uid} to trash`); + } catch (error) { + console.error(`Failed to move message ${uid} to trash:`, error); + // Attempt to copy and then delete as a fallback + try { + // await connection.copy(uid, SapoEmailClient.TRASH_FOLDER); + await connection.addFlags(uid, '\\Deleted'); + await connection.deleteMessage(uid); + console.log(`Successfully copied and deleted message ${uid} as fallback`); + } catch (fallbackError) { + throw new Error(`Failed to move message to trash (both methods): ${fallbackError.message}`); + } + } + } + + private async processMessage(message: any): Promise { + const body = message.parts.find(part => part.which === ''); + if (!body) return null; + + const parsed = await simpleParser(body.body); + const messageDate = parsed.date || new Date(); + + if (messageDate > new Date(Date.now() - SapoEmailClient.EMAIL_AGE_THRESHOLD)) { + return parsed.subject?.match(/\d{6}/)?.[0] || null; + } + return null; + } + + async waitForEmail(timeout: number = 40000): Promise { + const startTime = Date.now(); + console.log('Waiting for verification email...'); + const connection = await this.getConnection(); + try { + let i = 0 + while (Date.now() - startTime < timeout) { + + await connection.openBox('INBOX'); + console.log('Checking for new messages...', i++); + try { + const messages = await connection.search(['UNSEEN'], { + bodies: ['HEADER', ''], + markSeen: true + }); + + for (const message of messages.reverse()) { + const pin = await this.processMessage(message); + + if (pin) { + await connection.addFlags(message.attributes.uid, '\\Seen'); + await this.moveToTrash(connection, message.attributes.uid); + return pin; + } else { + // Move old messages to trash + await this.moveToTrash(connection, message.attributes.uid); + } + } + + await new Promise(resolve => setTimeout(resolve, SapoEmailClient.CHECK_INTERVAL)); + } catch (error) { + console.error('Error processing messages:', error); + await new Promise(resolve => setTimeout(resolve, SapoEmailClient.CHECK_INTERVAL)); + } + } + + throw new Error(`No verification code found within ${timeout}ms`); + } finally { + await connection.end(); + } + } + + async getPin(): Promise { + const pin = await this.waitForEmail(); + if (!pin) { + throw new Error('No verification code found in email'); + } + console.log(`Found verification code: ${pin}`); + 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..0008ceb --- /dev/null +++ b/e2e/src/webwallet/config.ts @@ -0,0 +1,34 @@ +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://sepolia-web.argent.xyz/", + /* + TODO: wait for sepolia in prod + process.env.ARGENT_X_ENVIRONMENT === "prod" + ? "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..64d4bff --- /dev/null +++ b/e2e/src/webwallet/page-objects/Dapps.ts @@ -0,0 +1,207 @@ +import { Page, expect } from "@playwright/test" +import { ICredentials } from "./Login" +import Navigation from "./Navigation" +import SapoEmailClient from "../../shared/src/SapoEmailClient" +import config from "../config" +const dappUrl = "http://localhost:3000/" +let mailClient: SapoEmailClient + +export default class Dapps extends Navigation { + private dApp: Page + constructor(page: Page) { + super(page) + this.dApp = 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 + }) { + this.dApp = dApp + await dApp.setViewportSize({ width: 1080, height: 720 }) + await dApp.goto(dappUrl) + + await dApp.getByRole("button", { name: "Connection" }).click() + if (useStarknetKitModal) { + await Promise.all([ + dApp.getByRole("button", { name: "Starknetkit Modal" }).click(), + this.handlePopup(credentials, newAccount), + ]) + } 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) + } + + this.dApp = dApp + } + + private async fillCredentials( + popup: Page, + credentials: ICredentials, + newAccount: boolean, + ) { + await popup.locator("[name=email]").fill(credentials.email) + await popup.locator('button[type="submit"]').click() + const pin = await mailClient.getPin() + await popup.locator('[id^="pin-input"]').first().click() + await popup.locator('[id^="pin-input"]').first().fill(pin!) + if (newAccount) { + await popup.locator("[name=password]").fill(credentials.password) + await popup.locator("[name=repeatPassword]").fill(credentials.password) + } else { + await popup.locator("[name=password]").fill(credentials.password) + } + + // password submit + await popup.locator('button[type="submit"]').click() + + await Promise.all([ + popup.waitForURL("**/connect?**"), + popup.waitForTimeout(5000), // additional safety delay if needed + ]) + + const allButtons = popup.locator("button") + const count = await allButtons.count() + + // check if connect page is showed by checking buttons + if (count > 0) { + await popup.locator('button[type="submit"]').click() + } + + return popup + } + + private async handlePopup(credentials: ICredentials, newAccount: boolean) { + const popupPromise = this.dApp.waitForEvent("popup") + await expect(this.dApp.locator("p:text-is('Email')")).toBeVisible() + await this.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) + } + + async sendERC20transaction({ type }: { type: "ERC20" | "Multicall" }) { + const popupPromise = this.dApp.waitForEvent("popup") + const dialogPromise = this.dApp.waitForEvent("dialog") + await this.dApp.locator('button :text-is("Transactions")').click() + const [, popup] = await Promise.all([ + this.dApp.locator(`button :text-is("Send ${type}")`).click(), + popupPromise, + ]) + + await expect(popup.getByText("Review transaction")).toBeVisible() + await expect(popup.getByText("Confirm")).toBeVisible() + const [, dialog] = await Promise.all([ + popup.getByText("Confirm").click(), + dialogPromise, + ]) + + expect(dialog.message()).toContain("Transaction sent") + await dialog.accept() + } + + async signMessage() { + const popupPromise = this.dApp.waitForEvent("popup") + await this.dApp.locator('button :text-is("Signing")').click() + await this.dApp.locator("[name=short-text]").fill("some message to sign") + const [, popup] = await Promise.all([ + this.dApp.locator('button[type="submit"]').click(), + popupPromise, + ]) + + await expect(popup.getByText("Sign Message")).toBeVisible() + await expect(popup.getByText("Confirm")).toBeVisible() + await popup.getByText("Confirm").click({ timeout: 3000, force: true }) + + await Promise.all([ + expect(this.dApp.getByText("Signer", { exact: true })).toBeVisible(), + expect(this.dApp.getByText("Cosigner", { exact: true })).toBeVisible(), + expect(this.dApp.locator("[name=signer_r]")).toBeVisible(), + expect(this.dApp.locator("[name=signer_s]")).toBeVisible(), + expect(this.dApp.locator("[name=cosigner_r]")).toBeVisible(), + expect(this.dApp.locator("[name=cosigner_s]")).toBeVisible(), + ]) + } + + async network({ type }: { type: "Add" | "Change" }) { + const dialogPromise = this.dApp.waitForEvent("dialog") + await this.dApp.locator('button :text-is("Network")').click() + await this.dApp.locator(`button :text-is("${type} Network")`).click() + const dialog = await dialogPromise + expect(dialog.message()).toContain("Not implemented") + await dialog.accept() + } + + async addToken() { + const dialogPromise = this.dApp.waitForEvent("dialog") + await this.dApp.locator('button :text-is("ERC20")').click() + await this.dApp.locator(`button :text-is("Add Token")`).click() + const dialog = await dialogPromise + expect(dialog.message()).toContain("Not implemented") + await dialog.accept() + } + + async sessionKeys() { + const popupPromise = this.dApp.waitForEvent("popup") + + await this.dApp.locator('button :text-is("Session Keys")').click() + await expect( + this.dApp.locator(`button :text-is("Create session")`), + ).toBeVisible() + await this.dApp.waitForTimeout(100) + const [, popup] = await Promise.all([ + this.dApp.locator(`button :text-is("Create session")`).click(), + popupPromise, + ]) + + await popup + .getByRole("button") + .and(popup.getByText("Start session")) + .click() + await this.dApp.waitForTimeout(1000) + + const dialogPromise = this.dApp.waitForEvent("dialog") + const [, dialog] = await Promise.all([ + this.dApp.getByText("Submit session tx").click(), + dialogPromise, + ]) + + expect(dialog.message()).toContain("Transaction sent") + await dialog.accept() + + await this.dApp.waitForTimeout(500) + await this.dApp.getByText("Submit EFO call").click() + const dialogPromiseEFO = this.dApp.waitForEvent("dialog") + await this.dApp.waitForTimeout(100) + this.dApp.getByText("Copy EFO call").click() + const dialogEFO = await dialogPromiseEFO + await this.dApp.waitForTimeout(500) + expect(dialogEFO.message()).toContain("Data copied in your clipboard") + await dialogEFO.accept() + + await this.dApp.getByText("Submit EFO TypedData").click() + const dialogPromiseEFOTypedData = this.dApp.waitForEvent("dialog") + await this.dApp.waitForTimeout(100) + this.dApp.getByText("Copy EFO TypedData").click() + const dialogEFOTypedData = await dialogPromiseEFOTypedData + await this.dApp.waitForTimeout(500) + expect(dialogEFOTypedData.message()).toContain( + "Data copied in your clipboard", + ) + await dialogEFOTypedData.accept() + } +} 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/connect.spec.ts b/e2e/src/webwallet/specs/connect.spec.ts new file mode 100644 index 0000000..1a0fdcd --- /dev/null +++ b/e2e/src/webwallet/specs/connect.spec.ts @@ -0,0 +1,28 @@ +import test from "../test" +import config from "../config" + +test.describe(`Connect`, () => { + test(`connect from testDapp using starknetKitModal`, async ({ + webWallet, + dApp, + }) => { + await webWallet.dapps.requestConnectionFromDapp({ + dApp, + credentials: config.validLogin, + newAccount: false, + useStarknetKitModal: true, + }) + }) + + test(`connect from testDapp using webwallet connector`, async ({ + webWallet, + dApp, + }) => { + await webWallet.dapps.requestConnectionFromDapp({ + dApp, + credentials: config.validLogin, + newAccount: false, + useStarknetKitModal: false, + }) + }) +}) diff --git a/e2e/src/webwallet/specs/network.spec.ts b/e2e/src/webwallet/specs/network.spec.ts new file mode 100644 index 0000000..b6711ea --- /dev/null +++ b/e2e/src/webwallet/specs/network.spec.ts @@ -0,0 +1,36 @@ +import test from "../test" +import config from "../config" + +test.describe(`Network`, () => { + test(`not implemented while calling Add Network`, async ({ + webWallet, + dApp, + }) => { + await webWallet.dapps.requestConnectionFromDapp({ + dApp, + credentials: config.validLogin, + newAccount: false, + useStarknetKitModal: true, + }) + + await webWallet.dapps.network({ + type: "Add", + }) + }) + + test(`not implemented while calling Change Network`, async ({ + webWallet, + dApp, + }) => { + await webWallet.dapps.requestConnectionFromDapp({ + dApp, + credentials: config.validLogin, + newAccount: false, + useStarknetKitModal: false, + }) + + await webWallet.dapps.network({ + type: "Change", + }) + }) +}) diff --git a/e2e/src/webwallet/specs/sessionKeys.spec.ts b/e2e/src/webwallet/specs/sessionKeys.spec.ts new file mode 100644 index 0000000..5a6672d --- /dev/null +++ b/e2e/src/webwallet/specs/sessionKeys.spec.ts @@ -0,0 +1,18 @@ +import test from "../test" +import config from "../config" + +test.describe(`Session Keys`, () => { + test(`create a sessions, send a transaction a get EFO data`, async ({ + webWallet, + dApp, + }) => { + await webWallet.dapps.requestConnectionFromDapp({ + dApp, + credentials: config.validLogin, + newAccount: false, + useStarknetKitModal: true, + }) + + await webWallet.dapps.sessionKeys() + }) +}) diff --git a/e2e/src/webwallet/specs/signMessage.spec.ts b/e2e/src/webwallet/specs/signMessage.spec.ts new file mode 100644 index 0000000..c68d63f --- /dev/null +++ b/e2e/src/webwallet/specs/signMessage.spec.ts @@ -0,0 +1,15 @@ +import test from "../test" +import config from "../config" + +test.describe(`Sign message`, () => { + test(`sign a message from testDapp`, async ({ webWallet, dApp }) => { + await webWallet.dapps.requestConnectionFromDapp({ + dApp, + credentials: config.validLogin, + newAccount: false, + useStarknetKitModal: true, + }) + + await webWallet.dapps.signMessage() + }) +}) diff --git a/e2e/src/webwallet/specs/token.spec.ts b/e2e/src/webwallet/specs/token.spec.ts new file mode 100644 index 0000000..f18e3ca --- /dev/null +++ b/e2e/src/webwallet/specs/token.spec.ts @@ -0,0 +1,18 @@ +import test from "../test" +import config from "../config" + +test.describe(`Token`, () => { + test(`not implemented while calling Add Token`, async ({ + webWallet, + dApp, + }) => { + await webWallet.dapps.requestConnectionFromDapp({ + dApp, + credentials: config.validLogin, + newAccount: false, + useStarknetKitModal: true, + }) + + await webWallet.dapps.addToken() + }) +}) diff --git a/e2e/src/webwallet/specs/transactions.spec.ts b/e2e/src/webwallet/specs/transactions.spec.ts new file mode 100644 index 0000000..3f324e6 --- /dev/null +++ b/e2e/src/webwallet/specs/transactions.spec.ts @@ -0,0 +1,30 @@ +import test from "../test" +import config from "../config" + +test.describe(`Transactions`, () => { + test(`send an ERC20 from testDapp`, async ({ webWallet, dApp }) => { + await webWallet.dapps.requestConnectionFromDapp({ + dApp, + credentials: config.validLogin, + newAccount: false, + useStarknetKitModal: true, + }) + + await webWallet.dapps.sendERC20transaction({ + type: "ERC20", + }) + }) + + test(`send an Multicall from testDapp`, async ({ webWallet, dApp }) => { + await webWallet.dapps.requestConnectionFromDapp({ + dApp, + credentials: config.validLogin, + newAccount: false, + useStarknetKitModal: true, + }) + + await webWallet.dapps.sendERC20transaction({ + type: "Multicall", + }) + }) +}) 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 fd74b82..eb23508 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,19 +11,22 @@ "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" }, "dependencies": { + "@argent/x-sessions": "7.0.0", + "@starknet-io/get-starknet-core": "4.0.4", "@starknet-react/chains": "^3.1.0", "@starknet-react/core": "^3.5.0", "next": "15.0.2", "react": "19.0.0-rc-02c0e824-20241028", "react-dom": "19.0.0-rc-02c0e824-20241028", "starknet": "^6.11.0", - "starknetkit": "^2.6.0" + "starknetkit": "2.6.2" }, "devDependencies": { "@commitlint/cli": "^19.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a77bd4..ec59cc6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,12 @@ importers: .: dependencies: + '@argent/x-sessions': + specifier: 7.0.0 + version: 7.0.0(starknet@6.11.0) + '@starknet-io/get-starknet-core': + specifier: 4.0.4 + version: 4.0.4 '@starknet-react/chains': specifier: ^3.1.0 version: 3.1.0 @@ -27,8 +33,8 @@ importers: specifier: ^6.11.0 version: 6.11.0 starknetkit: - specifier: ^2.6.0 - version: 2.6.0(starknet@6.11.0) + specifier: 2.6.2 + version: 2.6.2(starknet@6.11.0) devDependencies: '@commitlint/cli': specifier: ^19.5.0 @@ -79,6 +85,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': @@ -88,6 +170,11 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@argent/x-sessions@7.0.0': + resolution: {integrity: sha512-HVbwOALYUZaJ+alLfBdhiqOcEDQsq0/xFk0/H23aho2LG++RWsQnmSLxhMzcjQYx9GRbt7H8oK2HL2YKfrFPCw==} + peerDependencies: + starknet: 6.11.0 + '@babel/code-frame@7.26.2': resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} @@ -475,92 +562,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==} + '@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-arm-musl@2.5.0': - resolution: {integrity: sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==} - 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': @@ -590,6 +671,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==} @@ -644,11 +728,11 @@ packages: '@stablelib/x25519@1.0.3': resolution: {integrity: sha512-KnTbKmUhPhHavzobclVJQG5kuivH+qDLpe84iRqX3CLrKp881cF160JvXJ+hjn1aMyCwYOKeIZefIH/P5cJoRw==} - '@starknet-io/get-starknet-core@4.0.3': - resolution: {integrity: sha512-yE9P3i1+QE1R0KwC1frruh28iLfDfTzRei46EP2Hzqg4m08suV/tFrcfj7iKsymmvbE7R2eGIXy4wV5roSwb4g==} + '@starknet-io/get-starknet-core@4.0.4': + resolution: {integrity: sha512-XSypDxLUE1WDDD/yh8ik+tEAqE+MZZMa4CJ/ocn7hbrKvHOF08/oT3npddObhuu4/IjklZaRzIvdfv0nT+QduA==} - '@starknet-io/get-starknet@4.0.3': - resolution: {integrity: sha512-lZqlYYe1HnaX/j3mpm/DdC0Vi6g0dUKVLiHX7/Rc7Bf5CG7G5bAQHWDzHvACw+7pOAZT3/DlvfsZet9cBmX10Q==} + '@starknet-io/get-starknet@4.0.4': + resolution: {integrity: sha512-3qPn8l7khUef1710j/qp80T9iP+QKRYzaHAMae6ZJtGLKxDNqGdRMb6Efi0XARIHXG3xbg26Fh32FesEflV/Og==} '@starknet-io/types-js@0.7.7': resolution: {integrity: sha512-WLrpK7LIaIb8Ymxu6KF/6JkGW1sso988DweWu7p5QY/3y7waBIiPvzh27D9bX5KIJNRDyOoOVoHVEKYUYWZ/RQ==} @@ -685,15 +769,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==} @@ -703,6 +812,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} @@ -763,8 +875,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': @@ -808,17 +920,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==} @@ -939,6 +1051,12 @@ packages: ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + 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'} @@ -958,6 +1076,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'} @@ -969,8 +1090,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==} @@ -1070,6 +1191,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'} @@ -1107,6 +1232,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'} @@ -1128,6 +1256,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==} @@ -1182,6 +1314,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'} @@ -1193,6 +1329,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==} @@ -1222,10 +1362,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==} @@ -1238,8 +1395,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==} @@ -1250,6 +1407,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==} @@ -1257,6 +1422,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'} @@ -1488,6 +1657,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==} @@ -1495,6 +1673,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==} @@ -1502,6 +1684,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==} @@ -1628,9 +1814,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'} @@ -1644,6 +1841,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==} @@ -1651,6 +1856,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'} @@ -1785,6 +1998,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==} @@ -1839,6 +2055,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==} @@ -1864,8 +2083,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: @@ -1921,10 +2140,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'} @@ -1936,6 +2176,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'} @@ -2004,6 +2247,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'} @@ -2019,6 +2268,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'} @@ -2052,8 +2309,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==} @@ -2115,6 +2376,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'} @@ -2217,6 +2489,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'} @@ -2247,6 +2522,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==} @@ -2351,12 +2629,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'} @@ -2374,9 +2662,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: @@ -2385,6 +2682,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'} @@ -2392,6 +2693,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'} @@ -2479,9 +2783,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 @@ -2551,13 +2868,13 @@ packages: starknet@6.11.0: resolution: {integrity: sha512-u50KrGDi9fbu1Ogu7ynwF/tSeFlp3mzOg1/Y5x50tYFICImo3OfY4lOz9OtYDk404HK4eUujKkhov9tG7GAKlg==} - starknetkit@2.6.0: - resolution: {integrity: sha512-OXIw7JTEsyYDUn4bOwDc/5b3RvVwwVU8ky3VdpCgzdJCicm5iuCfVaD8hiBH/YmU46C/LBrs2QOMCW5cstszfQ==} + starknetkit@2.6.2: + resolution: {integrity: sha512-rGEzyDtB6oC+LuyjFg3TCFJOkdNiB7D9FXirZw2VjtBiAltrB+6MVDPikZ0Ru30MqcQ4Kc+CbgT9JsAFsk15MQ==} 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==} @@ -2608,6 +2925,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==} @@ -2660,6 +2980,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'} @@ -2696,6 +3021,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'} @@ -2763,6 +3092,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==} @@ -2793,19 +3125,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 @@ -2859,9 +3191,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: @@ -2979,6 +3324,11 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@argent/x-sessions@7.0.0(starknet@6.11.0)': + dependencies: + minimalistic-assert: 1.0.1 + starknet: 6.11.0 + '@babel/code-frame@7.26.2': dependencies: '@babel/helper-validator-identifier': 7.25.9 @@ -3417,70 +3767,66 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} - '@parcel/watcher-android-arm64@2.5.0': - optional: true - - '@parcel/watcher-darwin-arm64@2.5.0': + '@parcel/watcher-android-arm64@2.4.1': optional: true - '@parcel/watcher-darwin-x64@2.5.0': + '@parcel/watcher-darwin-arm64@2.4.1': optional: true - '@parcel/watcher-freebsd-x64@2.5.0': + '@parcel/watcher-darwin-x64@2.4.1': optional: true - '@parcel/watcher-linux-arm-glibc@2.5.0': + '@parcel/watcher-freebsd-x64@2.4.1': optional: true - '@parcel/watcher-linux-arm-musl@2.5.0': + '@parcel/watcher-linux-arm-glibc@2.4.1': optional: true - '@parcel/watcher-linux-arm64-glibc@2.5.0': + '@parcel/watcher-linux-arm64-glibc@2.4.1': optional: true - '@parcel/watcher-linux-arm64-musl@2.5.0': + '@parcel/watcher-linux-arm64-musl@2.4.1': optional: true - '@parcel/watcher-linux-x64-glibc@2.5.0': + '@parcel/watcher-linux-x64-glibc@2.4.1': optional: true - '@parcel/watcher-linux-x64-musl@2.5.0': + '@parcel/watcher-linux-x64-musl@2.4.1': 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 @@ -3511,6 +3857,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': @@ -3591,14 +3942,15 @@ snapshots: '@stablelib/random': 1.0.2 '@stablelib/wipe': 1.0.1 - '@starknet-io/get-starknet-core@4.0.3': + '@starknet-io/get-starknet-core@4.0.4': dependencies: '@module-federation/runtime': 0.1.21 '@starknet-io/types-js': 0.7.7 + async-mutex: 0.5.0 - '@starknet-io/get-starknet@4.0.3': + '@starknet-io/get-starknet@4.0.4': dependencies: - '@starknet-io/get-starknet-core': 4.0.3 + '@starknet-io/get-starknet-core': 4.0.4 bowser: 2.11.0 '@starknet-io/types-js@0.7.7': {} @@ -3640,16 +3992,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': @@ -3661,6 +4050,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 @@ -3744,7 +4135,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 @@ -3757,8 +4148,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 @@ -3826,7 +4217,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' @@ -3863,16 +4254,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' @@ -3895,7 +4286,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 @@ -3918,7 +4309,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 @@ -3933,11 +4324,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: @@ -4099,6 +4490,12 @@ snapshots: ast-types-flow@0.0.8: {} + async-mutex@0.5.0: + dependencies: + tslib: 2.8.0 + + asynckit@0.4.0: {} + atomic-sleep@1.0.0: {} autoprefixer@10.4.20(postcss@8.4.49): @@ -4117,13 +4514,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: {} @@ -4240,6 +4645,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: {} @@ -4272,6 +4681,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 @@ -4294,6 +4705,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 @@ -4336,6 +4753,8 @@ snapshots: deep-is@0.1.4: {} + deepmerge@4.3.1: {} + define-data-property@1.1.4: dependencies: es-define-property: 1.0.0 @@ -4350,6 +4769,8 @@ snapshots: defu@6.1.4: {} + delayed-stream@1.0.0: {} + destr@2.0.3: {} detect-browser@5.3.0: {} @@ -4371,10 +4792,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 @@ -4388,7 +4829,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 @@ -4396,9 +4837,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 @@ -4412,6 +4853,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 @@ -4421,6 +4866,8 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.2.1 + entities@4.5.0: {} + env-paths@2.2.1: {} environment@1.1.0: {} @@ -4676,7 +5123,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 @@ -4807,6 +5254,8 @@ snapshots: flatted@3.3.1: {} + follow-redirects@1.15.9: {} + for-each@0.3.3: dependencies: is-callable: 1.2.7 @@ -4816,6 +5265,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: @@ -4824,6 +5279,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: @@ -4964,22 +5425,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 @@ -5093,6 +5593,8 @@ snapshots: is-path-inside@3.0.3: {} + is-promise@1.0.1: {} + is-promise@4.0.0: {} is-regex@1.1.4: @@ -5143,6 +5645,8 @@ snapshots: dependencies: system-architecture: 0.1.0 + isarray@0.0.1: {} + isarray@2.0.5: {} isexe@2.0.0: {} @@ -5174,7 +5678,7 @@ snapshots: jiti@1.21.6: {} - jiti@2.4.0: {} + jiti@2.3.3: {} js-sha3@0.8.0: {} @@ -5225,17 +5729,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 @@ -5253,8 +5785,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 @@ -5263,11 +5795,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 @@ -5327,6 +5859,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: {} @@ -5338,6 +5889,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: {} @@ -5360,13 +5917,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: {} @@ -5419,6 +5978,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: {} @@ -5529,6 +6097,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: {} @@ -5548,6 +6121,8 @@ snapshots: pathe@1.1.2: {} + peberminta@0.9.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -5582,7 +6157,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: {} @@ -5644,14 +6219,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: @@ -5667,8 +6250,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 @@ -5676,12 +6269,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 @@ -5773,8 +6377,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: {} @@ -5882,14 +6498,14 @@ snapshots: transitivePeerDependencies: - encoding - starknetkit@2.6.0(starknet@6.11.0): + starknetkit@2.6.2(starknet@6.11.0): dependencies: - '@starknet-io/get-starknet': 4.0.3 - '@starknet-io/get-starknet-core': 4.0.3 + '@starknet-io/get-starknet': 4.0.4 + '@starknet-io/get-starknet-core': 4.0.4 '@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 @@ -5915,7 +6531,7 @@ snapshots: - ioredis - utf-8-validate - std-env@3.8.0: {} + std-env@3.7.0: {} stream-shift@1.0.3: {} @@ -5988,6 +6604,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 @@ -6031,6 +6649,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: @@ -6082,6 +6704,8 @@ snapshots: tinyexec@0.3.1: {} + tlds@1.252.0: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -6159,6 +6783,8 @@ snapshots: typescript@5.6.3: {} + uc.micro@2.1.0: {} + ufo@1.5.4: {} uint8arrays@3.1.0: @@ -6190,15 +6816,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 @@ -6230,8 +6856,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/src/abi/DummyContract.json b/src/abi/DummyContract.json deleted file mode 100644 index d1707c1..0000000 --- a/src/abi/DummyContract.json +++ /dev/null @@ -1,99 +0,0 @@ -[ - { - "type": "impl", - "name": "MockDappImpl", - "interface_name": "argent::mocks::mock_dapp::IMockDapp" - }, - { - "type": "interface", - "name": "argent::mocks::mock_dapp::IMockDapp", - "items": [ - { - "type": "function", - "name": "set_number", - "inputs": [ - { - "name": "number", - "type": "core::felt252" - } - ], - "outputs": [], - "state_mutability": "external" - }, - { - "type": "function", - "name": "set_number_double", - "inputs": [ - { - "name": "number", - "type": "core::felt252" - } - ], - "outputs": [], - "state_mutability": "external" - }, - { - "type": "function", - "name": "set_number_times3", - "inputs": [ - { - "name": "number", - "type": "core::felt252" - } - ], - "outputs": [], - "state_mutability": "external" - }, - { - "type": "function", - "name": "increase_number", - "inputs": [ - { - "name": "number", - "type": "core::felt252" - } - ], - "outputs": [ - { - "type": "core::felt252" - } - ], - "state_mutability": "external" - }, - { - "type": "function", - "name": "throw_error", - "inputs": [ - { - "name": "number", - "type": "core::felt252" - } - ], - "outputs": [], - "state_mutability": "external" - }, - { - "type": "function", - "name": "get_number", - "inputs": [ - { - "name": "user", - "type": "core::starknet::contract_address::ContractAddress" - } - ], - "outputs": [ - { - "type": "core::felt252" - } - ], - "state_mutability": "view" - } - ] - }, - { - "type": "event", - "name": "argent::mocks::mock_dapp::MockDapp::Event", - "kind": "enum", - "variants": [] - } -] diff --git a/src/abi/ERC20.json b/src/abi/ERC20.json deleted file mode 100644 index 8203392..0000000 --- a/src/abi/ERC20.json +++ /dev/null @@ -1,258 +0,0 @@ -[ - { - "members": [ - { - "name": "low", - "offset": 0, - "type": "felt" - }, - { - "name": "high", - "offset": 1, - "type": "felt" - } - ], - "name": "Uint256", - "size": 2, - "type": "struct" - }, - { - "inputs": [ - { - "name": "name", - "type": "felt" - }, - { - "name": "symbol", - "type": "felt" - }, - { - "name": "recipient", - "type": "felt" - } - ], - "name": "constructor", - "outputs": [], - "type": "constructor" - }, - { - "inputs": [], - "name": "name", - "outputs": [ - { - "name": "name", - "type": "felt" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "symbol", - "outputs": [ - { - "name": "symbol", - "type": "felt" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "totalSupply", - "outputs": [ - { - "name": "totalSupply", - "type": "Uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "decimals", - "outputs": [ - { - "name": "decimals", - "type": "felt" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "name": "account", - "type": "felt" - } - ], - "name": "balanceOf", - "outputs": [ - { - "name": "balance", - "type": "Uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "name": "owner", - "type": "felt" - }, - { - "name": "spender", - "type": "felt" - } - ], - "name": "allowance", - "outputs": [ - { - "name": "remaining", - "type": "Uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "name": "recipient", - "type": "felt" - }, - { - "name": "amount", - "type": "Uint256" - } - ], - "name": "transfer", - "outputs": [ - { - "name": "success", - "type": "felt" - } - ], - "type": "function" - }, - { - "inputs": [ - { - "name": "sender", - "type": "felt" - }, - { - "name": "recipient", - "type": "felt" - }, - { - "name": "amount", - "type": "Uint256" - } - ], - "name": "transferFrom", - "outputs": [ - { - "name": "success", - "type": "felt" - } - ], - "type": "function" - }, - { - "inputs": [ - { - "name": "spender", - "type": "felt" - }, - { - "name": "amount", - "type": "Uint256" - } - ], - "name": "approve", - "outputs": [ - { - "name": "success", - "type": "felt" - } - ], - "type": "function" - }, - { - "inputs": [ - { - "name": "spender", - "type": "felt" - }, - { - "name": "added_value", - "type": "Uint256" - } - ], - "name": "increaseAllowance", - "outputs": [ - { - "name": "success", - "type": "felt" - } - ], - "type": "function" - }, - { - "inputs": [ - { - "name": "spender", - "type": "felt" - }, - { - "name": "subtracted_value", - "type": "Uint256" - } - ], - "name": "decreaseAllowance", - "outputs": [ - { - "name": "success", - "type": "felt" - } - ], - "type": "function" - }, - { - "inputs": [ - { - "name": "recipient", - "type": "felt" - }, - { - "name": "amount", - "type": "Uint256" - } - ], - "name": "mint", - "outputs": [], - "type": "function" - }, - { - "inputs": [ - { - "name": "user", - "type": "felt" - }, - { - "name": "amount", - "type": "Uint256" - } - ], - "name": "burn", - "outputs": [], - "type": "function" - } -] diff --git a/src/abi/ERC20TransferAbi.json b/src/abi/ERC20TransferAbi.json deleted file mode 100644 index c6ca556..0000000 --- a/src/abi/ERC20TransferAbi.json +++ /dev/null @@ -1,18 +0,0 @@ -[ - { - "type": "function", - "name": "transfer", - "state_mutability": "external", - "inputs": [ - { - "name": "recipient", - "type": "core::starknet::contract_address::ContractAddress" - }, - { - "name": "amount", - "type": "core::integer::u256" - } - ], - "outputs": [] - } -] diff --git a/src/abi/dummyContractAbi.ts b/src/abi/dummyContractAbi.ts new file mode 100644 index 0000000..0b22c19 --- /dev/null +++ b/src/abi/dummyContractAbi.ts @@ -0,0 +1,18 @@ +import { Abi } from "@starknet-react/core" + +const dummyContractAbi = [ + { + type: "function", + name: "set_number", + state_mutability: "external", + inputs: [ + { + name: "number", + type: "core::felt252", + }, + ], + outputs: [], + }, +] as const satisfies Abi + +export { dummyContractAbi } diff --git a/src/components/sections/Transactions/abi.ts b/src/abi/erc20TransferAbi.ts similarity index 87% rename from src/components/sections/Transactions/abi.ts rename to src/abi/erc20TransferAbi.ts index 928d0de..f8de4a2 100644 --- a/src/components/sections/Transactions/abi.ts +++ b/src/abi/erc20TransferAbi.ts @@ -1,6 +1,6 @@ import { Abi } from "@starknet-react/core" -const abi = [ +const erco20TransferAbi = [ { type: "function", name: "transfer", @@ -19,4 +19,4 @@ const abi = [ }, ] as const satisfies Abi -export { abi } +export { erco20TransferAbi } diff --git a/src/app/globals.css b/src/app/globals.css index 8f77465..7479965 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -97,8 +97,6 @@ textarea { } .get-started-title { - font-size: 40px; - line-height: 42px; color: #6f727c; } diff --git a/src/components/HeaderConnectButton.tsx b/src/components/HeaderConnectButton.tsx index 847a7ee..fb5d83a 100644 --- a/src/components/HeaderConnectButton.tsx +++ b/src/components/HeaderConnectButton.tsx @@ -6,12 +6,13 @@ const HeaderConnectButton = () => { const { starknetkitConnectModal } = useStarknetkitConnectModal({ connectors: connectors as StarknetkitConnector[], + modalTheme: "dark", }) return ( <> diff --git a/src/components/sections/Network/AddNetwork.tsx b/src/components/sections/Network/AddNetwork.tsx index 1b59973..08768b7 100644 --- a/src/components/sections/Network/AddNetwork.tsx +++ b/src/components/sections/Network/AddNetwork.tsx @@ -23,15 +23,17 @@ const AddNetwork = () => { }, }) + const handleAddNetwork = async () => { + try { + await walletRequest.requestAsync() + } catch { + alert("Not implemented") + } + } + return (
-
diff --git a/src/components/sections/Network/ChangeNetwork.tsx b/src/components/sections/Network/ChangeNetwork.tsx index 028a034..3b24b5b 100644 --- a/src/components/sections/Network/ChangeNetwork.tsx +++ b/src/components/sections/Network/ChangeNetwork.tsx @@ -15,15 +15,17 @@ const ChangeNetwork = () => { }, }) + const handleChangeNetwork = async () => { + try { + await walletRequest.requestAsync() + } catch { + alert("Not implemented") + } + } + return (
-
diff --git a/src/components/sections/SectionButton.tsx b/src/components/sections/SectionButton.tsx index 5f5e132..a2f67d8 100644 --- a/src/components/sections/SectionButton.tsx +++ b/src/components/sections/SectionButton.tsx @@ -5,6 +5,7 @@ import { Section } from "./types" interface SectionButtonProps { disabled?: boolean section: Section + label?: string selected: boolean className?: string setSection: ( @@ -17,11 +18,13 @@ interface SectionButtonProps { const SectionButton: FC = ({ disabled, section, + label, selected, setSection, className, }) => ( ) diff --git a/src/components/sections/SessionKeys/SessionKeysEFOLayout.tsx b/src/components/sections/SessionKeys/SessionKeysEFOLayout.tsx new file mode 100644 index 0000000..096fe5e --- /dev/null +++ b/src/components/sections/SessionKeys/SessionKeysEFOLayout.tsx @@ -0,0 +1,53 @@ +import { Button } from "@/components/ui/Button" +import { FC } from "react" + +interface SessionKeysEFOLayoutProps { + handleSubmit: () => Promise + copyData: string + submitDisabled: boolean + copyDataDisabled: boolean + title: string + submitText: string + copyText: string +} + +const SessionKeysEFOLayout: FC = ({ + handleSubmit, + copyData, + submitDisabled, + copyDataDisabled, + title, + submitText, + copyText, +}) => { + const copy = async () => { + await navigator.clipboard.writeText(JSON.stringify(copyData)) + alert(`Data copied in your clipboard`) + } + + return ( +
+

{title}

+
+ + +
+
+ ) +} + +export { SessionKeysEFOLayout } diff --git a/src/components/sections/SessionKeys/SessionKeysExecute.tsx b/src/components/sections/SessionKeys/SessionKeysExecute.tsx new file mode 100644 index 0000000..d5fb2ef --- /dev/null +++ b/src/components/sections/SessionKeys/SessionKeysExecute.tsx @@ -0,0 +1,78 @@ +import { dummyContractAbi } from "@/abi/dummyContractAbi" +import { Button } from "@/components/ui/Button" +import { Spinner } from "@/components/ui/Spinner" +import { ARGENT_DUMMY_CONTRACT_ADDRESS } from "@/constants" +import { useAccount, useContract } from "@starknet-react/core" +import { FC, useState } from "react" +import { WithSessionAccount } from "./types" + +const SessionKeysExecute: FC = ({ sessionAccount }) => { + const { address } = useAccount() + const [isSubmitting, setIsSubmitting] = useState(false) + + const { contract } = useContract({ + abi: dummyContractAbi, + address: ARGENT_DUMMY_CONTRACT_ADDRESS, + provider: sessionAccount, + }) + + const handleSessionExecute = async () => { + try { + setIsSubmitting(true) + + if (!address) { + throw new Error("No address") + } + + if (!sessionAccount) { + throw new Error("No session account") + } + + const transferCallData = contract.populate("set_number", { + number: 1, + }) + + // https://www.starknetjs.com/docs/guides/estimate_fees/#estimateinvokefee + const { suggestedMaxFee } = await sessionAccount.estimateInvokeFee({ + contractAddress: ARGENT_DUMMY_CONTRACT_ADDRESS, + entrypoint: "set_number", + calldata: transferCallData.calldata, + }) + + // https://www.starknetjs.com/docs/guides/estimate_fees/#fee-limitation + const maxFee = (suggestedMaxFee * BigInt(15)) / BigInt(10) + // send to same account + const { transaction_hash } = await contract.set_number( + transferCallData.calldata, + { + maxFee, + }, + ) + setTimeout(() => { + alert(`Transaction sent: ${transaction_hash}`) + }) + + setIsSubmitting(true) + } catch (error) { + console.error(error) + } finally { + setIsSubmitting(false) + } + } + + return ( + <> +

Execute session transaction

+ + + ) +} + +export { SessionKeysExecute } diff --git a/src/components/sections/SessionKeys/SessionKeysExecuteOutside.tsx b/src/components/sections/SessionKeys/SessionKeysExecuteOutside.tsx new file mode 100644 index 0000000..24d8bfa --- /dev/null +++ b/src/components/sections/SessionKeys/SessionKeysExecuteOutside.tsx @@ -0,0 +1,72 @@ +import { dummyContractAbi } from "@/abi/dummyContractAbi" +import { + ARGENT_DUMMY_CONTRACT_ADDRESS, + ARGENT_SESSION_SERVICE_BASE_URL, + CHAIN_ID, +} from "@/constants" +import { sessionKey } from "@/helpers/sessionKeys" +import { createOutsideExecutionCall } from "@argent/x-sessions" +import { useContract } from "@starknet-react/core" +import { FC, useState } from "react" +import { constants } from "starknet" +import { SessionKeysEFOLayout } from "./SessionKeysEFOLayout" +import { WithSession } from "./types" + +const SessionKeysExecuteOutside: FC = ({ + session, + sessionAccount, +}) => { + const { contract } = useContract({ + abi: dummyContractAbi, + address: ARGENT_DUMMY_CONTRACT_ADDRESS, + provider: sessionAccount, + }) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const [outsideExecution, setOutsideExecution] = useState() + + const handleSubmitEFO = async () => { + try { + if (!session || !sessionAccount) { + throw new Error("No open session") + } + // https://www.starknetjs.com/docs/guides/use_erc20/#interact-with-an-erc20 + // check .populate + const transferCallData = contract.populate("set_number", { + number: 1, + }) + + const efoExecutionCall = await createOutsideExecutionCall({ + session, + sessionKey, + calls: [transferCallData], + argentSessionServiceUrl: ARGENT_SESSION_SERVICE_BASE_URL, + network: + CHAIN_ID === constants.NetworkName.SN_SEPOLIA ? "sepolia" : "mainnet", + }) + + console.log( + "execute from outside response", + JSON.stringify(efoExecutionCall), + ) + + setOutsideExecution(efoExecutionCall) + } catch (error) { + console.error(error) + } + } + + return ( + + ) +} + +export { SessionKeysExecuteOutside } diff --git a/src/components/sections/SessionKeys/SessionKeysSign.tsx b/src/components/sections/SessionKeys/SessionKeysSign.tsx new file mode 100644 index 0000000..92e0ca8 --- /dev/null +++ b/src/components/sections/SessionKeys/SessionKeysSign.tsx @@ -0,0 +1,97 @@ +import { Button } from "@/components/ui/Button" +import { toHexChainid } from "@/helpers/chainId" +import { + allowedMethods, + expiry, + metaData, + sessionKey, +} from "@/helpers/sessionKeys" +import { + buildSessionAccount, + createSession, + CreateSessionParams, + createSessionRequest, + Session, +} from "@argent/x-sessions" +import { useAccount, useSignTypedData } from "@starknet-react/core" +import { useState } from "react" +import { Account, AccountInterface, constants } from "starknet" +import { SectionLayout } from "../SectionLayout" +import { ARGENT_SESSION_SERVICE_BASE_URL, provider } from "@/constants" +import { SessionKeysExecute } from "./SessionKeysExecute" +import { SessionKeysExecuteOutside } from "./SessionKeysExecuteOutside" +import { SessionKeysTypedDataOutside } from "./SessionKeysTypedDataOutside" +import { SessionKeysIcon } from "@/components/icons/SessionKesIcon" + +const SessionKeysSign = () => { + const { address, chainId } = useAccount() + const [session, setSession] = useState() + const [sessionAccount, setSessionAccount] = useState< + Account | AccountInterface | undefined + >() + + const sessionParams: CreateSessionParams = { + allowedMethods, + expiry, + metaData: metaData(false), + sessionKey, + } + + const hexChainId = toHexChainid(chainId) + + const sessionRequest = createSessionRequest({ + sessionParams, + chainId: hexChainId as constants.StarknetChainId, + }) + + const { signTypedDataAsync } = useSignTypedData({ + params: sessionRequest.sessionTypedData, + }) + + const handleSignSessionKeys = async () => { + if (!address || !chainId) { + throw new Error("No address or chainId") + } + + try { + const authorisationSignature = await signTypedDataAsync() + const sessionObj = await createSession({ + address: address, + chainId: hexChainId as constants.StarknetChainId, + authorisationSignature, + sessionRequest, + }) + + const sessionAccount = await buildSessionAccount({ + session: sessionObj, + sessionKey, + provider, + argentSessionServiceBaseUrl: ARGENT_SESSION_SERVICE_BASE_URL, + }) + + setSession(sessionObj) + setSessionAccount(sessionAccount) + } catch (e) { + console.error(e) + } + } + + return ( + }> + + + + + + ) +} + +export { SessionKeysSign } diff --git a/src/components/sections/SessionKeys/SessionKeysTypedDataOutside.tsx b/src/components/sections/SessionKeys/SessionKeysTypedDataOutside.tsx new file mode 100644 index 0000000..0bc43b5 --- /dev/null +++ b/src/components/sections/SessionKeys/SessionKeysTypedDataOutside.tsx @@ -0,0 +1,72 @@ +import { dummyContractAbi } from "@/abi/dummyContractAbi" +import { + ARGENT_DUMMY_CONTRACT_ADDRESS, + ARGENT_SESSION_SERVICE_BASE_URL, + CHAIN_ID, +} from "@/constants" +import { sessionKey } from "@/helpers/sessionKeys" +import { createOutsideExecutionTypedData } from "@argent/x-sessions" +import { useContract } from "@starknet-react/core" +import { FC, useState } from "react" +import { constants } from "starknet" +import { SessionKeysEFOLayout } from "./SessionKeysEFOLayout" +import { WithSession } from "./types" + +const SessionKeysTypedDataOutside: FC = ({ + session, + sessionAccount, +}) => { + const { contract } = useContract({ + abi: dummyContractAbi, + address: ARGENT_DUMMY_CONTRACT_ADDRESS, + provider: sessionAccount, + }) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const [outsideTypedData, setOutsideTypedData] = useState() + + const handleSubmitEFO = async () => { + try { + if (!session || !sessionAccount) { + throw new Error("No open session") + } + // https://www.starknetjs.com/docs/guides/use_erc20/#interact-with-an-erc20 + // check .populate + const transferCallData = contract.populate("set_number", { + number: 1, + }) + + const efoTypedData = await createOutsideExecutionTypedData({ + session, + sessionKey, + calls: [transferCallData], + argentSessionServiceUrl: ARGENT_SESSION_SERVICE_BASE_URL, + network: + CHAIN_ID === constants.NetworkName.SN_SEPOLIA ? "sepolia" : "mainnet", + }) + + console.log( + "execute from outside typed data response", + JSON.stringify(efoTypedData), + ) + + setOutsideTypedData(efoTypedData) + } catch (error) { + console.error(error) + } + } + + return ( + + ) +} + +export { SessionKeysTypedDataOutside } diff --git a/src/components/sections/SessionKeys/types.ts b/src/components/sections/SessionKeys/types.ts new file mode 100644 index 0000000..8207ab6 --- /dev/null +++ b/src/components/sections/SessionKeys/types.ts @@ -0,0 +1,10 @@ +import { Session } from "@argent/x-sessions" +import { Account, AccountInterface } from "starknet" + +export interface WithSessionAccount { + sessionAccount?: Account | AccountInterface +} + +export interface WithSession extends WithSessionAccount { + session: Session | undefined +} diff --git a/src/components/sections/SignMessage.tsx b/src/components/sections/SignMessage.tsx index a41feb8..f6ea2bd 100644 --- a/src/components/sections/SignMessage.tsx +++ b/src/components/sections/SignMessage.tsx @@ -129,8 +129,8 @@ const SignMessage = () => {

r