diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
index 3cc258ca1..893c67e74 100644
--- a/.github/workflows/pull-request.yml
+++ b/.github/workflows/pull-request.yml
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- env: [prod, hydrogen]
+ env: [prod, staging, hydrogen]
extension_type: [chrome, firefox]
environment: ${{ matrix.env }}
@@ -29,8 +29,9 @@ jobs:
# API URLs
ARGENT_API_BASE_URL: ${{ vars.ARGENT_API_BASE_URL }}
ARGENT_X_STATUS_URL: ${{ vars.ARGENT_X_STATUS_URL }}
+ ARGENT_X_NEWS_URL: ${{ vars.ARGENT_X_NEWS_URL }}
ARGENT_TESTNET_RPC_URL: ${{ vars.ARGENT_TESTNET_RPC_URL }}
-
+ ARGENT_HEALTHCHECK_BASE_URL: ${{ vars.ARGENT_HEALTHCHECK_BASE_URL }}
# API ENVIRONMENT
ARGENT_X_ENVIRONMENT: ${{ matrix.env }}
@@ -44,6 +45,7 @@ jobs:
RAMP_API_KEY: ${{ secrets.RAMP_API_KEY }}
SAFE_ENV_VARS: false
MULTICALL_MAX_BATCH_SIZE: 20
+ NEW_CAIRO_0_ENABLED: false
# Refresh intervals
FAST: 20 # 20s
@@ -111,7 +113,7 @@ jobs:
run: (cd ./packages/extension/dist && zip -r "../../../${{ env.FILENAME_PREFIX }}-${{ matrix.extension_type }}" .)
- name: Upload ${{ matrix.extension_type }} extension
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: ${{ env.FILENAME_PREFIX }}-${{ matrix.extension_type }}
path: "*-${{ matrix.extension_type }}.zip"
@@ -125,7 +127,9 @@ jobs:
env:
ARGENT_API_BASE_URL: ${{ vars.ARGENT_API_BASE_URL }}
ARGENT_TESTNET_RPC_URL: ${{ vars.ARGENT_TESTNET_RPC_URL }}
+ ARGENT_HEALTHCHECK_BASE_URL: ${{ vars.ARGENT_HEALTHCHECK_BASE_URL }}
ARGENT_X_STATUS_URL: ${{ vars.ARGENT_X_STATUS_URL }}
+ ARGENT_X_NEWS_URL: ${{ vars.ARGENT_X_NEWS_URL }}
ARGENT_X_ENVIRONMENT: "hydrogen"
services:
@@ -174,7 +178,7 @@ jobs:
run: pnpm run test:ci
- name: SonarCloud Scan
# TODO replace with master as soon as sonarcloud fixes the issue with action https://community.sonarsource.com/t/sonarsource-sonarcloud-github-action-failing-with-node-js-12-error/89664/2
- uses: SonarSource/sonarcloud-github-action@v1.9
+ uses: SonarSource/sonarcloud-github-action@v2.1.1
with:
projectBaseDir: ./packages/extension
env:
@@ -198,12 +202,20 @@ jobs:
E2E_ACCOUNT_1_SEED2: ${{ secrets.E2E_ACCOUNT_1_SEED2 }}
E2E_ACCOUNT_1_SEED3: ${{ secrets.E2E_ACCOUNT_1_SEED3 }}
## BANK ACCOUNT, USED FOR FUND OTHER ACCOUNTS
- E2E_SENDER_ADDRESS: ${{ secrets.E2E_SENDER_ADDRESS }}
- E2E_SENDER_PRIVATEKEY: ${{ secrets.E2E_SENDER_PRIVATEKEY }}
+ E2E_SENDER_ADDRESSES: ${{ secrets.E2E_SENDER_ADDRESSES }}
+ E2E_SENDER_PRIVATEKEYS: ${{ secrets.E2E_SENDER_PRIVATEKEYS }}
E2E_SENDER_SEED: ${{ secrets.E2E_SENDER_SEED }}
STARKNET_TESTNET_URL: ${{ secrets.STARKNET_TESTNET_URL }}
STARKSCAN_TESTNET_URL: ${{ secrets.STARKSCAN_TESTNET_URL }}
ARGENT_TESTNET_RPC_URL: ${{ secrets.ARGENT_TESTNET_RPC_URL }}
+ ARGENT_HEALTHCHECK_BASE_URL: ${{ secrets.ARGENT_HEALTHCHECK_BASE_URL }}
+ E2E_SPOK_CAMPAIGN_URL: ${{ secrets.E2E_SPOK_CAMPAIGN_URL }}
+ E2E_SPOK_CAMPAIGN_NAME: ${{ secrets.E2E_SPOK_CAMPAIGN_NAME }}
+ # Refresh intervals
+ REFRESH_INTERVAL_FAST: 1 # 1s
+ REFRESH_INTERVAL_MEDIUM: 5 # 5s
+ REFRESH_INTERVAL_SLOW: 20 # 20s
+ REFRESH_INTERVAL_VERY_SLOW: 60 * 10 # 10m
steps:
- uses: actions/checkout@v4
@@ -241,19 +253,20 @@ jobs:
run: xvfb-run --auto-servernum pnpm test:e2e:extension --project=${{ matrix.project }} --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
- name: Upload artifacts
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
if: always()
with:
- name: test-artifacts
+ name: test-artifacts-${{ matrix.shardIndex }}
path: |
packages/e2e/artifacts/playwright/
+ !packages/e2e/artifacts/playwright/*.webm
retention-days: 5
- name: Upload blob report to GitHub Actions Artifacts
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
- name: all-blob-reports
+ name: all-blob-reports-${{ matrix.shardIndex }}
path: packages/e2e/blob-report/
retention-days: 5
@@ -286,19 +299,20 @@ jobs:
${{ runner.os }}-pnpm-store-
- name: Download blob reports from GitHub Actions Artifacts
- uses: actions/download-artifact@v3
+ uses: actions/download-artifact@v4
with:
- name: all-blob-reports
path: all-blob-reports
+ pattern: all-blob-reports-*
+ merge-multiple: true
- name: Merge into HTML Report
- run: npx playwright merge-reports --reporter html ./all-blob-reports
+ run: npx playwright merge-reports -c ./packages/e2e/merge-reports.config.js ./all-blob-reports
- name: Upload HTML report
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: html-report--attempt-${{ github.run_attempt }}
- path: playwright-report
+ path: packages/e2e/playwright-report
retention-days: 14
add_pr_comments:
@@ -408,6 +422,23 @@ jobs:
NEXT_PUBLIC_ARGENT_API_BASE_URL: ${{ vars.ARGENT_API_BASE_URL }}
NEXT_PUBLIC_ARGENT_TESTNET_RPC_URL: ${{ vars.ARGENT_TESTNET_RPC_URL }}
+ ARGENT_API_BASE_URL: ${{ secrets.ARGENT_API_BASE_URL }}
+ E2E_TESTNET_SEED1: ${{ secrets.E2E_TESTNET_SEED1 }}
+ E2E_TESTNET_SEED2: ${{ secrets.E2E_TESTNET_SEED2 }}
+ E2E_TESTNET_SEED3: ${{ secrets.E2E_TESTNET_SEED3 }}
+ E2E_ACCOUNT_1_SEED2: ${{ secrets.E2E_ACCOUNT_1_SEED2 }}
+ E2E_ACCOUNT_1_SEED3: ${{ secrets.E2E_ACCOUNT_1_SEED3 }}
+ ## BANK ACCOUNT, USED FOR FUND OTHER ACCOUNTS
+ E2E_SENDER_ADDRESSES: ${{ secrets.E2E_SENDER_ADDRESSES }}
+ E2E_SENDER_PRIVATEKEYS: ${{ secrets.E2E_SENDER_PRIVATEKEYS }}
+ E2E_SENDER_SEED: ${{ secrets.E2E_SENDER_SEED }}
+ STARKNET_TESTNET_URL: ${{ secrets.STARKNET_TESTNET_URL }}
+ STARKSCAN_TESTNET_URL: ${{ secrets.STARKSCAN_TESTNET_URL }}
+ ARGENT_TESTNET_RPC_URL: ${{ secrets.ARGENT_TESTNET_RPC_URL }}
+ ARGENT_HEALTHCHECK_BASE_URL: ${{ secrets.ARGENT_HEALTHCHECK_BASE_URL }}
+ E2E_SPOK_CAMPAIGN_URL: ${{ secrets.E2E_SPOK_CAMPAIGN_URL }}
+ E2E_SPOK_CAMPAIGN_NAME: ${{ secrets.E2E_SPOK_CAMPAIGN_NAME }}
+
steps:
- uses: actions/checkout@v4
@@ -443,19 +474,19 @@ jobs:
pnpm run test:e2e:webwallet
- name: Upload artifacts
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
if: always()
with:
- name: test-artifacts
+ name: test-artifacts-${{ matrix.shardIndex }}
path: |
packages/e2e/artifacts/playwright/
retention-days: 5
- name: Upload blob report to GitHub Actions Artifacts
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
- name: all-blob-reports-webwallet
+ name: all-blob-reports-webwallet-${{ matrix.shardIndex }}
path: packages/e2e/blob-report/
retention-days: 5
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index f5dd457f3..cd7fcf1b7 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -30,13 +30,16 @@ jobs:
SAFE_ENV_VARS: true
ARGENT_API_BASE_URL: ${{ vars.ARGENT_API_BASE_URL }}
ARGENT_TESTNET_RPC_URL: ${{ vars.ARGENT_TESTNET_RPC_URL }}
+ ARGENT_HEALTHCHECK_BASE_URL: ${{ vars.ARGENT_HEALTHCHECK_BASE_URL }}
ARGENT_X_STATUS_URL: ${{ vars.ARGENT_X_STATUS_URL }}
+ ARGENT_X_NEWS_URL: ${{ vars.ARGENT_X_NEWS_URL }}
ARGENT_X_ENVIRONMENT: "prod"
MULTICALL_MAX_BATCH_SIZE: 20
FAST: 20 # 20s
MEDIUM: 60 # 60s
SLOW: 60 * 5 # 5m
VERY_SLOW: 24 * 60 * 60 # 1d
+ NEW_CAIRO_0_ENABLED: false
if: ${{ !startsWith(github.ref, 'refs/tags/') || startsWith(github.ref, 'refs/tags/') && contains(github.ref, 'extension') }}
runs-on: ubuntu-latest
@@ -82,7 +85,7 @@ jobs:
run: pnpm bundlewatch
- name: Upload artifacts for chrome
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: chrome
path: "*-chrome.zip"
@@ -90,7 +93,7 @@ jobs:
if-no-files-found: error
- name: Upload artifacts for firefox
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: firefox
path: "*-firefox.zip"
@@ -132,10 +135,6 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
run: |
npm config set "//registry.npmjs.org/:_authToken" "$NPM_ACCESS_TOKEN"
- cp Readme.md ./packages/get-starknet/README.md
- pnpm --filter @argent/get-starknet publish --no-git-checks --access public || exit 0
- pnpm --filter @argent/web-sdk publish --no-git-checks --access public || exit 0
- pnpm --filter @argent/starknet-react-webwallet-connector publish --no-git-checks --access public || exit 0
pnpm --filter @argent/x-sessions publish --no-git-checks --access public || exit 0
- name: Get product version
diff --git a/.nvmrc b/.nvmrc
index d5a159609..8b0beab16 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-20.10.0
+20.11.0
diff --git a/Readme.md b/Readme.md
index 2806f8d11..a903ea175 100644
--- a/Readme.md
+++ b/Readme.md
@@ -5,7 +5,7 @@
---
-
⬇️ Get Argent X for StarkNet today:
+⬇️ Get Argent X for Starknet today:
@@ -44,7 +44,7 @@ The example dapp is also contained in this repository.
## 🌐 Usage with your dapp
-If you want to use this StarkNet Wallet extension with your dapp, the easiest way is to checkout the [starknetkit](https://github.com/argentlabs/starknetkit) package
+If you want to use this Starknet Wallet extension with your dapp, the easiest way is to checkout the [starknetkit](https://github.com/argentlabs/starknetkit) package
```bash
# starknet.js is a peer dependency
diff --git a/docs/Upgrade_v3.md b/docs/Upgrade_v3.md
index 4c520da6c..78eecb5f2 100644
--- a/docs/Upgrade_v3.md
+++ b/docs/Upgrade_v3.md
@@ -35,9 +35,9 @@ As a user of Argent X 2.x, after upgrading to version 3.0.0, your extension will
4. Disable Argent X 3.0.0 by switching off the toggle in the lower right corner of the Argent X panel in the Chrome extensions page.
-5. Open the extension v2.2.3 and restore from your backup file, the one you downloaded in step 1. Now you can transfer all your tokens to your new address, the one you copied at step 2. For ERC721 assets, you'll have to do it in [Voyager](https://voyager.online/), the StarkNet block explorer, by connecting your wallet and transferring manually.
+5. Open the extension v2.2.3 and restore from your backup file, the one you downloaded in step 1. Now you can transfer all your tokens to your new address, the one you copied at step 2. For ERC721 assets, you'll have to do it in [Voyager](https://voyager.online/), the Starknet block explorer, by connecting your wallet and transferring manually.
-## What does it mean as a StarkNet dapp developer?
+## What does it mean as a Starknet dapp developer?
The `starknet` object (returned by `get-starknet`) will now expose an `account` object instead of a `signer` object. This `account` object implements the [AccountInterface](https://github.com/0xs34n/starknet.js/blob/develop/src/account/interface.ts), specifically it exposes the methods `execute()` and `signMessage()`:
@@ -51,9 +51,9 @@ where `Call` is defined by:
```typescript
interface Call {
- contractAddress: string;
- entrypoint: string;
- calldata?: BigNumberish[];
+ contractAddress: string
+ entrypoint: string
+ calldata?: BigNumberish[]
}
```
diff --git a/package.json b/package.json
index e5928a8e4..ecf4144f8 100644
--- a/package.json
+++ b/package.json
@@ -25,21 +25,21 @@
"scripts": {
"format": "prettier --loglevel warn --write \"**/*.{js,jsx,ts,tsx,css,md,yml,json}\"",
"dev": "NODE_ENV=development pnpm run -r --stream --parallel dev",
- "dev:ui": "NODE_ENV=development pnpm --parallel run dev:ui ",
+ "dev:ui": "NODE_ENV=development pnpm --parallel run dev:ui",
"dev:extension": "NODE_ENV=development pnpm run --filter @argent-x/extension -r --stream --parallel dev",
"build-storybook": "pnpm run --filter @argent-x/storybook build-storybook",
- "clean": "rm -rf packages/extension/dist packages/get-starket/dist",
+ "clean": "rm -rf packages/extension/dist",
"build": "pnpm run -r --parallel --stream build",
"build:extension": "pnpm run --filter @argent-x/extension build",
"build:web": "pnpm run --filter @argent/web build",
"build:sourcemaps": "GEN_SOURCE_MAPS=true pnpm run build",
- "lint": "pnpm run -r --parallel lint ",
+ "lint": "pnpm run -r --parallel lint",
"test": "pnpm run -r --parallel --stream test",
- "test:watch": "pnpm run -r --paralle; --stream test:watch ",
+ "test:watch": "pnpm run -r --parallel; --stream test:watch",
"test:e2e:extension": "pnpm run --filter @argent-x/e2e test:extension",
"test:e2e:webwallet": "pnpm run --filter @argent-x/e2e test:webwallet",
"setup": "pnpm install --frozen-lockfile && pnpm allow-scripts && husky install && patch-package && pnpm run -r --stream setup",
- "test:ci": "pnpm run --stream --parallel test:ci ",
+ "test:ci": "pnpm run --stream --parallel test:ci",
"storybook": "cd packages/storybook && pnpm run storybook",
"devnet:upgrade-helper": "NODE_NO_WARNINGS=1 ts-node ./scripts/devnet-upgrade-helper.ts",
"devnet:setup-contracts": "NODE_NO_WARNINGS=1 ts-node ./scripts/devnet-setup-contracts.ts",
diff --git a/packages/dapp/package.json b/packages/dapp/package.json
index 2ffb47161..7b73d2c99 100644
--- a/packages/dapp/package.json
+++ b/packages/dapp/package.json
@@ -14,17 +14,17 @@
"@argent/ui": "^6.3.1",
"@argent/x-sessions": "^6.3.1",
"@chakra-ui/react": "^2.6.1",
- "@starknet-react/chains": "0.1.0",
- "@starknet-react/core": "2.1.1",
+ "@starknet-react/chains": "0.1.5",
+ "@starknet-react/core": "2.2.2",
"micro-starknet": "^0.2.3",
"next": "^13.4.6",
"react": "^18.0.0",
"react-dom": "^18.0.0",
- "starknet": "5.24.3",
- "starknetkit": "^1.0.22"
+ "starknet": "5.25.0",
+ "starknetkit": "^1.1.0"
},
"devDependencies": {
- "@types/node": "20.10.4",
+ "@types/node": "20.11.0",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"eslint": "8",
diff --git a/packages/dapp/src/components/AddNetwork.tsx b/packages/dapp/src/components/AddNetwork.tsx
index 397ef6573..03416dc0e 100644
--- a/packages/dapp/src/components/AddNetwork.tsx
+++ b/packages/dapp/src/components/AddNetwork.tsx
@@ -13,6 +13,7 @@ const AddNetwork = () => {
chainId: "SN_DAPP_TEST",
chainName: "Test chain name",
baseUrl: "http://localhost:5050",
+ rpcUrls: ["http://localhost:5050/rpc"],
})
setAddNetworkError("")
} catch (error) {
diff --git a/packages/dapp/src/components/InfoRow.tsx b/packages/dapp/src/components/InfoRow.tsx
index b7585a3fa..bc9e77200 100644
--- a/packages/dapp/src/components/InfoRow.tsx
+++ b/packages/dapp/src/components/InfoRow.tsx
@@ -1,10 +1,10 @@
import { H4 } from "@argent/ui"
import { Code, Flex } from "@chakra-ui/react"
-import { FC } from "react"
+import { FC, ReactNode } from "react"
const InfoRow: FC<{
title: string
- content?: string
+ content?: ReactNode
copyContent?: string
}> = ({ title, content, copyContent }) => {
return (
diff --git a/packages/dapp/src/pages/index.tsx b/packages/dapp/src/pages/index.tsx
index 0496dc1c0..ea3dd1343 100644
--- a/packages/dapp/src/pages/index.tsx
+++ b/packages/dapp/src/pages/index.tsx
@@ -1,36 +1,53 @@
import { supportsSessions } from "@argent/x-sessions"
import type { StarknetWindowObject } from "get-starknet-core"
-import { useCallback, useEffect, useState } from "react"
+import { useCallback, useEffect, useMemo, useState } from "react"
import { AccountInterface } from "starknet"
import { Header } from "../components/Header"
+import {
+ Button,
+ Code,
+ Flex,
+ Menu,
+ MenuButton,
+ MenuItem,
+ MenuList,
+ Text,
+} from "@chakra-ui/react"
+import { ChevronDownIcon, CheckIcon } from "@chakra-ui/icons"
-import { Button } from "@argent/ui"
import { InfoRow } from "../components/InfoRow"
import { truncateAddress } from "../services/address.service"
import {
- addWalletChangeListener,
+ addWalletAccountsChangedListener,
+ addWalletNetworkChangedListener,
connectWallet,
disconnectWallet,
getChainId,
- removeWalletChangeListener,
+ removeWalletAccountsChangedListener,
+ removeWalletNetworkChangedListener,
silentConnectWallet,
+ switchNetwork,
} from "../services/wallet.service"
import { TokenDapp } from "../components/TokenDapp"
-import { Flex } from "@chakra-ui/react"
+
+const chainIds = ["SN_MAIN", "SN_GOERLI", "SN_SEPOLIA"]
const StarknetKitDapp = () => {
const [supportSessions, setSupportsSessions] = useState(null)
- const [chain, setChain] = useState(undefined)
+ const [chainId, setChainId] = useState(undefined)
const [isConnected, setConnected] = useState(false)
const [account, setAccount] = useState(null)
const [isSilentConnect, setIsSilentConnect] = useState(true)
useEffect(() => {
- const handler = async () => {
+ const onAccountsChanged = async () => {
try {
const wallet = await silentConnectWallet()
const chainId = await getChainId(wallet?.provider as any)
- setChain(chainId)
+ if (chainId && !chainIds.includes(chainId)) {
+ chainIds.push(chainId)
+ }
+ setChainId(chainId)
setConnected(!!wallet?.isConnected)
if (wallet?.account) {
setAccount(wallet.account as any)
@@ -54,13 +71,27 @@ const StarknetKitDapp = () => {
}
}
+ const onNetworkChanged = (chainId?: any) => {
+ setChainId(chainId)
+ }
+
;(async () => {
- await handler()
- addWalletChangeListener(handler)
+ await onAccountsChanged()
+ addWalletAccountsChangedListener(onAccountsChanged)
+ addWalletNetworkChangedListener(onNetworkChanged)
})()
return () => {
- removeWalletChangeListener(handler)
+ removeWalletAccountsChangedListener(onAccountsChanged)
+ removeWalletNetworkChangedListener(onNetworkChanged)
+ }
+ }, [])
+
+ const handleNetworkClick = useCallback(async (chainId: string) => {
+ try {
+ await switchNetwork(chainId)
+ } catch (e) {
+ console.error(e)
}
}, [])
@@ -74,7 +105,7 @@ const StarknetKitDapp = () => {
async () => {
const wallet = await connectWallet(enableWebWallet)
const chainId = await getChainId(wallet?.provider as any)
- setChain(chainId)
+ setChainId(chainId)
setConnected(!!wallet?.isConnected)
if (wallet?.account) {
setAccount(wallet.account as any)
@@ -99,7 +130,7 @@ const StarknetKitDapp = () => {
() => async () => {
try {
await disconnectWallet()
- setChain(undefined)
+ setChainId(undefined)
setConnected(false)
setAccount(null)
setSupportsSessions(null)
@@ -110,6 +141,32 @@ const StarknetKitDapp = () => {
[],
)
+ const chainMenu = useMemo(() => {
+ return (
+
+ )
+ }, [chainId, handleNetworkClick])
+
if (isSilentConnect) {
return (
@@ -140,6 +197,7 @@ const StarknetKitDapp = () => {
{isConnected ? (
<>
+
{
title="Supports sessions:"
content={`${supportSessions}`}
/>
-
{account && (
)}
diff --git a/packages/dapp/src/pages/starknetReactDapp.tsx b/packages/dapp/src/pages/starknetReactDapp.tsx
index f25dcf1a9..e26549725 100644
--- a/packages/dapp/src/pages/starknetReactDapp.tsx
+++ b/packages/dapp/src/pages/starknetReactDapp.tsx
@@ -16,8 +16,10 @@ import { InjectedConnector } from "starknetkit/injected"
import { WebWalletConnector } from "starknetkit/webwallet"
import { Header } from "../components/Header"
+import { H2 } from "@argent/ui"
import { Flex, Image } from "@chakra-ui/react"
import React, { useEffect, useState } from "react"
+import { useStarknetkitConnectModal } from "starknetkit"
import { InfoRow } from "../components/InfoRow"
import { TokenDapp } from "../components/TokenDapp"
import { truncateAddress } from "../services/address.service"
@@ -39,10 +41,14 @@ const StarknetReactDappContent = () => {
const chains = [goerli]
const { account, status } = useAccount()
- const { connect, connectors } = useConnect()
+ const { connectAsync, connectors } = useConnect()
const { disconnect } = useDisconnect()
const [isClient, setIsClient] = useState(false)
+ const { starknetkitConnectModal } = useStarknetkitConnectModal({
+ connectors: availableConnectors,
+ })
+
/* https://nextjs.org/docs/messages/react-hydration-error#solution-1-using-useeffect-to-run-on-the-client-only
starknet react had an issue with the `available` method
need to check their code, probably is executed only on client causing an hydration issue
@@ -79,6 +85,7 @@ const StarknetReactDappContent = () => {
) : (
<>
+
{connectors.filter(inAppBrowserFilter).map((connector) => {
if (!connector.available()) {
@@ -92,7 +99,7 @@ const StarknetReactDappContent = () => {
as="button"
key={connector.id}
borderRadius="full"
- onClick={() => connect({ connector })}
+ onClick={async () => connectAsync({ connector })}
alignItems="center"
background="neutrals.700"
_hover={{
@@ -119,6 +126,30 @@ const StarknetReactDappContent = () => {
)
})}
+
+ Starknetkit modal + starknet-react
+ {
+ const { connector } = await starknetkitConnectModal()
+ if (!connector) return // or throw error
+ await connectAsync({ connector })
+ }}
+ alignItems="center"
+ background="neutrals.700"
+ _hover={{
+ background: "neutrals.600",
+ }}
+ cursor="pointer"
+ maxW="350px"
+ gap="2"
+ py="2"
+ px="4"
+ mt="2"
+ >
+ Starknetkit modal with starknet-react
+
>
)}
>
diff --git a/packages/dapp/src/services/wallet.service.ts b/packages/dapp/src/services/wallet.service.ts
index dc5d51879..f081a5c2a 100644
--- a/packages/dapp/src/services/wallet.service.ts
+++ b/packages/dapp/src/services/wallet.service.ts
@@ -29,7 +29,7 @@ export let windowStarknet: StarknetWindowObjectV5 | null = null
export const starknetVersion = "v5"
export const silentConnectWallet = async () => {
- const _windowStarknet = await connect({
+ const { wallet } = await connect({
modalMode: "neverAsk",
webWalletUrl,
argentMobileOptions: {
@@ -40,12 +40,12 @@ export const silentConnectWallet = async () => {
// comment this when using webwallet -- enable is already done by @argent/get-starknet and webwallet is currently using only v4
// to remove when @argent/get-starknet will support both v4 and v5
//await _windowStarknet?.enable({ starknetVersion })
- windowStarknet = _windowStarknet as StarknetWindowObjectV5 | null
+ windowStarknet = wallet as StarknetWindowObjectV5 | null
return windowStarknet ?? undefined
}
export const connectWallet = async () => {
- const _windowStarknet = await connect({
+ const { wallet } = await connect({
webWalletUrl, // TODO: remove hardcoding
argentMobileOptions: {
dappName: "Example dapp",
@@ -56,7 +56,7 @@ export const connectWallet = async () => {
// comment this when using webwallet -- enable is already done by @argent/get-starknet and webwallet is currently using only v4
// to remove when @argent/get-starknet will support both v4 and v5
//await _windowStarknet?.enable({ starknetVersion })
- windowStarknet = _windowStarknet as StarknetWindowObjectV5 | null
+ windowStarknet = wallet as StarknetWindowObjectV5 | null
return windowStarknet ?? undefined
}
@@ -168,7 +168,7 @@ export const waitForTransaction = async (hash: string) => {
return windowStarknet.provider.waitForTransaction(hash)
}
-export const addWalletChangeListener = async (
+export const addWalletAccountsChangedListener = async (
handleEvent: (accounts: string[]) => void,
) => {
if (!windowStarknet?.isConnected) {
@@ -177,7 +177,7 @@ export const addWalletChangeListener = async (
windowStarknet.on("accountsChanged", handleEvent)
}
-export const removeWalletChangeListener = async (
+export const removeWalletAccountsChangedListener = async (
handleEvent: (accounts: string[]) => void,
) => {
if (!windowStarknet?.isConnected) {
@@ -186,6 +186,24 @@ export const removeWalletChangeListener = async (
windowStarknet.off("accountsChanged", handleEvent)
}
+export const addWalletNetworkChangedListener = async (
+ handleEvent: (network?: string) => void,
+) => {
+ if (!windowStarknet?.isConnected) {
+ return
+ }
+ windowStarknet.on("networkChanged", handleEvent)
+}
+
+export const removeWalletNetworkChangedListener = async (
+ handleEvent: (network?: string) => void,
+) => {
+ if (!windowStarknet?.isConnected) {
+ return
+ }
+ windowStarknet.off("networkChanged", handleEvent)
+}
+
export const declare = async (
payload: DeclareContractPayload,
transactionsDetail?: InvocationsDetails,
@@ -228,3 +246,15 @@ export const addNetwork = async (params: AddStarknetChainParameters) => {
params,
})
}
+
+export const switchNetwork = async (chainId: string) => {
+ if (!windowStarknet?.isConnected) {
+ throw Error("starknet wallet not connected")
+ }
+ await windowStarknet.request({
+ type: "wallet_switchStarknetChain",
+ params: {
+ chainId,
+ },
+ })
+}
diff --git a/packages/e2e/extension/network-setup/Dockerfile b/packages/e2e/extension/network-setup/Dockerfile
deleted file mode 100644
index 728e9bee5..000000000
--- a/packages/e2e/extension/network-setup/Dockerfile
+++ /dev/null
@@ -1,9 +0,0 @@
-
-FROM shardlabs/starknet-devnet:0.6.3
-RUN addgroup -S localuser \
- && adduser -S localuser -G localuser
-
-USER localuser
-
-COPY ./dump.pkl ./dump.pkl
-ENTRYPOINT [ "starknet-devnet", "--host", "0.0.0.0", "--port", "5050", "--seed", "0", "--lite-mode" ]
\ No newline at end of file
diff --git a/packages/e2e/extension/network-setup/build_and_push.sh b/packages/e2e/extension/network-setup/build_and_push.sh
deleted file mode 100755
index da2fd886d..000000000
--- a/packages/e2e/extension/network-setup/build_and_push.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/bin/sh
-
-docker build . --tag e2e-starknet-devnet:latest
-
-docker login argentlabs-argent-x.jfrog.io
-docker tag e2e-starknet-devnet argentlabs-argent-x.jfrog.io/e2e-starknet-devnet:latest
-docker push argentlabs-argent-x.jfrog.io/e2e-starknet-devnet:latest
diff --git a/packages/e2e/extension/network-setup/dump.pkl b/packages/e2e/extension/network-setup/dump.pkl
deleted file mode 100644
index cecd13391..000000000
Binary files a/packages/e2e/extension/network-setup/dump.pkl and /dev/null differ
diff --git a/packages/e2e/extension/playwright.config.ts b/packages/e2e/extension/playwright.config.ts
index 360c9bb54..dd3e9c23a 100644
--- a/packages/e2e/extension/playwright.config.ts
+++ b/packages/e2e/extension/playwright.config.ts
@@ -1,7 +1,5 @@
import type { PlaywrightTestConfig } from "@playwright/test"
-import config from "./src/config"
-
-const isCI = Boolean(process.env.CI)
+import { isCI, artifactsDir } from "../shared/cfg/test"
const playwrightConfig: PlaywrightTestConfig = {
projects: [
@@ -9,27 +7,29 @@ const playwrightConfig: PlaywrightTestConfig = {
name: "ArgentX",
use: {
trace: "on-first-retry",
- viewport: { width: 360, height: 600 },
- actionTimeout: 60 * 1000, // 1 minute
+ viewport: { width: 360, height: 800 },
+ actionTimeout: 120 * 1000, // 2 minute
permissions: ["clipboard-read", "clipboard-write"],
},
timeout: 5 * 60e3, // 5 minutes
- expect: { timeout: 30 * 1000 }, // 30 seconds
+ expect: { timeout: 120 * 1000 }, // 2 minute
testDir: "./src/specs",
testMatch: /\.spec.ts$/,
retries: isCI ? 1 : 0,
- outputDir: config.artifactsDir,
+ outputDir: artifactsDir,
},
],
- workers: 1,
+ workers: isCI ? 2 : 1,
+ fullyParallel: true,
reportSlowTests: {
- threshold: 1 * 60e3, // 1 minute
+ threshold: 2 * 60e3, // 2 minutes
max: 5,
},
- reporter: isCI ? [["github"], ["blob"]] : "list",
+ reporter: isCI ? [["github"], ["blob"], ["list"]] : "list",
forbidOnly: isCI,
- outputDir: config.artifactsDir,
+ outputDir: artifactsDir,
preserveOutput: isCI ? "failures-only" : "never",
+ globalTeardown: "../shared/cfg/global.teardown.ts",
}
export default playwrightConfig
diff --git a/packages/e2e/extension/src/languages/ILanguage.ts b/packages/e2e/extension/src/languages/ILanguage.ts
index 249930d9c..f29c87f63 100644
--- a/packages/e2e/extension/src/languages/ILanguage.ts
+++ b/packages/e2e/extension/src/languages/ILanguage.ts
@@ -10,7 +10,6 @@ export interface ILanguage {
no: string
unlock: string
showSettings: string
- lockWallet: string
reset: string
confirmReset: string
save: string
@@ -19,8 +18,20 @@ export interface ILanguage {
privacyStatement: string
approve: string
addArgentShield: string
+ removeArgentShield: string
+ argentShieldAdded: string
+ argentShieldRemoved: string
dismiss: string
reviewSend: string
+ hide: string
+ hiddenAccounts: string
+ copy: string
+ beforeYouContinue: string
+ seedWarning: string
+ revealSeedPhrase: string
+ copied: string
+ confirmRecovery: string
+ remove: string
}
account: {
noAccounts: string
@@ -44,6 +55,16 @@ export interface ILanguage {
invalidCheckSumError: string
invalidAddress: string
createMultisig: string
+ activateAccount: string
+ notEnoughFoundsFee: string
+ newToken: string
+ argentShield: {
+ wrong2faCode: string
+ failed2faCode: string
+ codeNotRequested2fa: string
+ emailInUse: string
+ }
+ removedFromMultisig: string
}
wallet: {
//first screen
@@ -70,44 +91,57 @@ export interface ILanguage {
finish: string
}
settings: {
- addresBook: string
- connectedDapps: string
- showRecoveryPhase: string
- developerSettings: string
- privacy: string
- hideAccount: string
- deleteAccount: string //only available for local network
- exportPrivateKey: string
+ account: {
+ manageOwners: {
+ manageOwners: string
+ removeOwner: string
+ replaceOwner: string
+ }
+ setConfirmations: string
+ viewOnStarkScan: string
+ viewOnVoyager: string
+ hideAccount: string
+ deployAccount: string
+ connectedDapps: {
+ connectedDapps: string
+ connect: string
+ reject: string
+ disconnectAll: string
+ noConnectedDapps: string
+ }
+ exportPrivateKey: string
+ }
+ preferences: {
+ preferences: string
+ hideTokens: string
+ defaultBlockExplorer: string
+ defaultNFTMarket: string
+ emailNotifications: string
+ }
+ securityPrivacy: {
+ securityPrivacy: string
+ autoLockTimer: string
+ recoveryPhase: string
+ automaticErrorReporting: string
+ shareAnonymousData: string
+ }
+ addressBook: {
+ addressBook: string
+ nameRequired: string
+ addressRequired: string
+ removeAddress: string
+ delete: string
+ }
+ developerSettings: {
+ developerSettings: string
+ manageNetworks: {
+ manageNetworks: string
+ restoreDefaultNetworks: string
+ }
+ smartContractDevelopment: string
+ experimental: string
+ }
extendedView: string
- hide: string
- hiddenAccounts: string
- delete: string
- copy: string
- copied: string
- confirmRecovery: string
- revealSeedPhrase: string
- beforeYouContinue: string
- seedWarning: string
- deployAccount: string
- }
- developerSettings: {
- manageNetworks: string
- blockExplorer: string
- smartContractDevelopment: string
- experimental: string
- restoreDefaultNetworks: string
- }
- address: {
- nameRequired: string
- addressRequired: string
- removeAddress: string
- delete: string
- addressBook: string
- }
- dapps: {
- connect: string
- reject: string
- disconnectAll: string
- noConnectedDapps: string
+ lockWallet: string
}
}
diff --git a/packages/e2e/extension/src/languages/en/index.ts b/packages/e2e/extension/src/languages/en/index.ts
index 5334efad7..f5927ac21 100644
--- a/packages/e2e/extension/src/languages/en/index.ts
+++ b/packages/e2e/extension/src/languages/en/index.ts
@@ -4,13 +4,12 @@ const texts = {
close: "Close",
confirm: "Confirm",
done: "Done",
- continue: "Continue",
next: "Next",
+ continue: "Continue",
yes: "Yes",
no: "No",
unlock: "Unlock",
showSettings: "Show settings",
- lockWallet: "Lock wallet",
reset: "Reset",
confirmReset: "RESET",
save: "Save",
@@ -20,11 +19,25 @@ const texts = {
"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",
+ removeArgentShield: "Remove Argent Shield",
+ argentShieldAdded: "Argent Shield Added",
+ argentShieldRemoved: "Argent Shield Removed",
dismiss: "Dismiss",
reviewSend: "Review send",
+ hide: "Hide",
+ hiddenAccounts: "Hidden accounts",
+ 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",
},
account: {
- noAccounts: "You have no accounts on ",
+ noAccounts: "You have no accounts on",
createAccount: "Create account",
addFunds: "Add funds",
fundsFromStarkNet: "From another Starknet wallet",
@@ -47,21 +60,34 @@ const texts = {
invalidCheckSumError: "Invalid address (checksum error)",
invalidAddress: "Invalid address",
createMultisig: "Create multisig",
+ activateAccount: "Activate Account",
+ notEnoughFoundsFee: "Insufficient funds to pay fee",
+ newToken: "New token",
+ argentShield: {
+ wrong2faCode: "Looks like the wrong code. Please try again.",
+ failed2faCode:
+ "You have reached the maximum number of attempts. Please wait 30 minutes and request a new code.",
+ codeNotRequested2fa:
+ "You have not requested a verification code. Please request a new one.",
+ emailInUse:
+ "This address is associated with accounts from another seedphrase.Please enter another email address to continue.",
+ },
+ removedFromMultisig: "You were removed from this multisig",
},
wallet: {
//first screen
banner1: "Welcome to Argent X",
- desc1: "Enjoy the security of Ethereum with the scale of StarkNet",
+ 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.",
+ "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.",
+ "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.",
+ "I understand that Starknet may experience performance issues and my transactions may fail for various reasons.",
//third screen
banner3: "New wallet",
desc3: "Enter a password to protect your wallet",
@@ -76,47 +102,58 @@ const texts = {
finish: "Finish",
},
settings: {
- addresBook: "Address book",
- connectedDapps: "Connected dapps",
- showRecoveryPhase: "Recovery phrase",
- developerSettings: "Developer settings",
- privacy: "Privacy",
- hideAccount: "Hide account",
- deleteAccount: "Delete account", //only available for local network
- exportPrivateKey: "Export private key",
+ 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",
+ connectedDapps: {
+ connectedDapps: "Connected dapps",
+ connect: "Connect",
+ reject: "Reject",
+ disconnectAll: "Disconnect all",
+ noConnectedDapps: "No connected dapps",
+ },
+ exportPrivateKey: "Export private key",
+ },
+ preferences: {
+ preferences: "Preferences",
+ hideTokens: "Hide tokens with no balance",
+ 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",
+ },
+ developerSettings: {
+ developerSettings: "Developer settings",
+ manageNetworks: {
+ manageNetworks: "Manage networks",
+ restoreDefaultNetworks: "Restore default networks",
+ },
+ smartContractDevelopment: "Smart Contract Development",
+ experimental: "Experimental",
+ },
extendedView: "Extended view",
- hide: "Hide",
- hiddenAccounts: "Hidden accounts",
- delete: "Delete",
- copy: "Copy",
- copied: "Copied",
- confirmRecovery:
- "I have saved my recovery phrase and understand I should never share it with anyone else",
- revealSeedPhrase: "Click to reveal recovery phrase",
- 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",
- deployAccount: "Deploy account",
- },
- developerSettings: {
- manageNetworks: "Manage networks",
- blockExplorer: "Block explorer",
- smartContractDevelopment: "Smart Contract Development",
- experimental: "Experimental",
- restoreDefaultNetworks: "Restore default networks",
- },
- address: {
- nameRequired: "Contact Name is required",
- addressRequired: "Address is required",
- removeAddress: "Remove from address book",
- delete: "Delete",
- addressBook: "Address book",
- },
- dapps: {
- connect: "Connect",
- reject: "Reject",
- disconnectAll: "Disconnect all",
- noConnectedDapps: "No connected dapps",
+ lockWallet: "Lock wallet",
},
}
diff --git a/packages/e2e/extension/src/page-objects/Account.ts b/packages/e2e/extension/src/page-objects/Account.ts
index 81139e0d1..f47c077ba 100644
--- a/packages/e2e/extension/src/page-objects/Account.ts
+++ b/packages/e2e/extension/src/page-objects/Account.ts
@@ -2,8 +2,12 @@ import { Page, expect } from "@playwright/test"
import { lang } from "../languages"
import Activity from "./Activity"
+import {
+ FeeTokens,
+ TokenSymbol,
+ getTokenInfo,
+} from "../../../shared/src/assets"
-type TokenName = "Ethereum"
export interface IAsset {
name: string
balance: number
@@ -46,24 +50,28 @@ export default class Account extends Activity {
}
get send() {
- return this.page.locator(`button:text-is("${lang.account.send}")`)
+ return this.page.locator(`button:has-text("${lang.account.send}")`)
}
get deployAccount() {
return this.page.locator(
- `button :text-is("${lang.settings.deployAccount}")`,
+ `button :text-is("${lang.settings.account.deployAccount}")`,
)
}
- token(tkn: TokenName) {
- return this.page.locator(`button :text-is('${tkn}')`)
+ token(tkn: TokenSymbol) {
+ const tokenInfo = getTokenInfo(tkn)
+ if (!tokenInfo) {
+ throw new Error(`Invalid token: ${tkn}`)
+ }
+ return this.page.locator(`button :text-is('${tokenInfo.name}')`)
}
get accountListSelector() {
return this.page.locator(`[aria-label="Show account list"]`)
}
- get addANewccountFromAccountList() {
+ get addANewAccountFromAccountList() {
return this.page.locator('[aria-label="Create new wallet"]')
}
@@ -107,10 +115,8 @@ export default class Account extends Activity {
return this.page.locator('[data-testid="tokenBalance"]')
}
- currentBalance(tkn: "Ethereum") {
- return this.page.locator(
- ` //button//h6[contains(text(), '${tkn}')]/following::p`,
- )
+ currentBalance(tkn: TokenSymbol) {
+ return this.page.locator(`[data-testid="${tkn}-balance"]`)
}
currentBalanceDevNet(tkn: "ETH") {
@@ -150,7 +156,7 @@ export default class Account extends Activity {
await this.createAccount.click()
} else {
await this.accountListSelector.click()
- await this.addANewccountFromAccountList.click()
+ await this.addANewAccountFromAccountList.click()
}
await this.addStandardAccountFromNewAccountScreen.click()
@@ -163,7 +169,7 @@ export default class Account extends Activity {
await this.createAccount.click()
} else {
await this.accountListSelector.click()
- await this.addANewccountFromAccountList.click()
+ await this.addANewAccountFromAccountList.click()
}
await this.addStandardAccountFromNewAccountScreen.click()
@@ -174,7 +180,7 @@ export default class Account extends Activity {
const accountAddress = await this.accountAddress
.textContent()
.then((v) => v?.replaceAll(" ", ""))
- await this.close.last().click()
+ await this.closeLocator.last().click()
const accountName = await this.accountListSelector.textContent()
return [accountName, accountAddress]
}
@@ -207,13 +213,13 @@ export default class Account extends Activity {
return assetsList
}
- async ensureAsset(accountName: string, name: "Ethereum", value: string) {
+ async ensureAsset(
+ accountName: string,
+ name: TokenSymbol = "ETH",
+ value: string,
+ ) {
await this.ensureSelectedAccount(accountName)
- await expect(
- this.page.locator(
- `//*[text() = '${name}']/following-sibling::div/p[text() = '${value}']`,
- ),
- ).toBeVisible({ timeout: 1000 * 60 * 4 })
+ await expect(this.currentBalance(name)).toContainText(value)
}
async getTotalFeeValue() {
@@ -227,27 +233,27 @@ export default class Account extends Activity {
return parseFloat(fee.split(" ")[0])
}
- async txValidations(txAmount: string) {
+ async txValidations(feAmount: string) {
const trxAmountHeader = await this.page
.locator(`//*[starts-with(text(),'Send ')]`)
.textContent()
.then((v) => v?.split(" ")[1])
- const amountLocator = this.page.locator(
- `//div//label[text()='Send']/following-sibling::div[1]//*[@data-testid]`,
- )
- const sendAmount = await amountLocator
- .textContent()
- .then((v) => v?.split(" ")[0])
-
- expect(sendAmount!.substring(1)).toBe(`${trxAmountHeader}`)
- if (txAmount != "MAX") {
- expect(txAmount.toString()).toBe(trxAmountHeader)
+ 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!)
+ console.log({ sendAmountFE, sendAmountTX })
+ expect(sendAmountFE).toBe(`${trxAmountHeader}`)
+
+ if (feAmount != "MAX") {
+ expect(feAmount).toBe(trxAmountHeader)
}
- const amount = await amountLocator
- .getAttribute("data-testid")
- .then((value) => parseInt(value!) / Math.pow(10, 18))
- return amount
+ return { sendAmountTX, sendAmountFE }
}
async fillRecipientAddress({
@@ -271,23 +277,34 @@ export default class Account extends Activity {
}
}
}
+
+ async confirmTransaction() {
+ const failPredict = this.page.getByText("Transaction fail")
+ await expect(failPredict)
+ .toBeVisible({ timeout: 1000 * 5 })
+ .then(async (_) => await failPredict.click())
+ .catch(async (_) => await this.confirmLocator.click())
+ }
+
async transfer({
originAccountName,
recipientAddress,
- tokenName,
+ token,
amount,
fillRecipientAddress = "paste",
submit = true,
+ feeToken = "ETH",
}: {
originAccountName: string
recipientAddress: string
- tokenName: TokenName
+ token: TokenSymbol
amount: number | "MAX"
fillRecipientAddress?: "typing" | "paste"
submit?: boolean
+ feeToken?: FeeTokens
}) {
await this.ensureSelectedAccount(originAccountName)
- await this.token(tokenName).click()
+ await this.token(token).click()
await this.fillRecipientAddress({ recipientAddress, fillRecipientAddress })
if (amount === "MAX") {
await expect(this.balance).toBeVisible()
@@ -297,12 +314,17 @@ export default class Account extends Activity {
await this.amount.fill(amount.toString())
}
- await this.reviewSend.click()
- const trxAmount = await this.txValidations(amount.toString())
+ await this.reviewSendLocator.click()
if (submit) {
- await this.confirm.click()
+ if (feeToken) {
+ await this.selectFeeToken(feeToken)
+ }
+ await this.confirmTransaction()
}
- return trxAmount
+ const { sendAmountFE, sendAmountTX } = await this.txValidations(
+ amount.toString(),
+ )
+ return { sendAmountTX, sendAmountFE }
}
async ensureTokenBalance({
@@ -311,7 +333,7 @@ export default class Account extends Activity {
balance,
}: {
accountName: string
- token: TokenName
+ token: TokenSymbol
balance: number
}) {
await this.ensureSelectedAccount(accountName)
@@ -319,7 +341,7 @@ export default class Account extends Activity {
await expect(this.page.locator('[data-testid="tokenBalance"]')).toHaveText(
balance.toString(),
)
- await this.back.click()
+ await this.backLocator.click()
}
get password() {
@@ -356,9 +378,7 @@ export default class Account extends Activity {
}
get recipientAddress() {
- return this.page.locator(
- `//textarea[@placeholder="${lang.account.recipientAddress}"]/following::button[1]`,
- )
+ return this.page.locator('[data-testid="recipient-input"]')
}
get saveAddress() {
@@ -386,22 +406,20 @@ export default class Account extends Activity {
}
async saveRecoveryPhrase() {
- const nextModal = await this.next.isVisible({ timeout: 60 })
+ const nextModal = await this.nextLocator.isVisible({ timeout: 60 })
if (nextModal) {
await Promise.all([
expect(
- this.page.locator(
- `h3:has-text("${lang.settings.beforeYouContinue}")`,
- ),
+ this.page.locator(`h3:has-text("${lang.common.beforeYouContinue}")`),
).toBeVisible(),
expect(
- this.page.locator(`p:has-text("${lang.settings.seedWarning}")`),
+ this.page.locator(`p:has-text("${lang.common.seedWarning}")`),
).toBeVisible(),
])
- await this.next.click()
+ await this.nextLocator.click()
}
await this.page
- .locator(`span:has-text("${lang.settings.revealSeedPhrase}")`)
+ .locator(`span:has-text("${lang.common.revealSeedPhrase}")`)
.click()
const pos = Array.from({ length: 12 }, (_, i) => i + 1)
const seed = await Promise.all(
@@ -414,15 +432,15 @@ export default class Account extends Activity {
).then((result) => result.join(" "))
await Promise.all([
- this.page.locator(`button:has-text("${lang.settings.copy}")`).click(),
+ this.page.locator(`button:has-text("${lang.common.copy}")`).click(),
expect(
- this.page.locator(`button:has-text("${lang.settings.copied}")`),
+ this.page.locator(`button:has-text("${lang.common.copied}")`),
).toBeVisible(),
])
await this.page
- .locator(`p:has-text("${lang.settings.confirmRecovery}")`)
+ .locator(`p:has-text("${lang.common.confirmRecovery}")`)
.click()
- await this.done.click()
+ await this.doneLocator.click()
const seedPhraseCopied = await this.page.evaluate(
`navigator.clipboard.readText();`,
)
@@ -449,6 +467,18 @@ export default class Account extends Activity {
return this.saveRecoveryPhrase().then((adr) => String(adr))
}
+ get addedArgentShieldLocator() {
+ return this.page.getByRole("heading", {
+ name: lang.common.argentShieldAdded,
+ })
+ }
+
+ get removedArgentShieldLocator() {
+ return this.page.getByRole("heading", {
+ name: lang.common.argentShieldRemoved,
+ })
+ }
+
// Multisig
get deployNeededWarning() {
return this.page.locator(`p:has-text("${lang.account.deployFirst}")`)
@@ -462,10 +492,6 @@ export default class Account extends Activity {
return this.page.locator(`[data-testid="decrease-threshold"]`)
}
- get manageOwners() {
- return this.page.locator(`button:text-is("Manage owners")`)
- }
-
get setConfirmationsLocator() {
return this.page.locator(`button:has-text("Set confirmations")`)
}
@@ -478,7 +504,7 @@ export default class Account extends Activity {
confirmations?: number
}) {
await this.accountListSelector.click()
- await this.addANewccountFromAccountList.click()
+ await this.addANewAccountFromAccountList.click()
await this.addMultisigAccountFromNewAccountScreen.click()
const [pages] = await Promise.all([
@@ -488,6 +514,7 @@ export default class Account extends Activity {
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]
@@ -515,14 +542,14 @@ export default class Account extends Activity {
}
await tabs[1].locator('button:text-is("Next")').click()
- const currentTheshold = await tabs[1]
+ const currentThreshold = await tabs[1]
.locator('[data-testid="threshold"]')
.innerText()
.then((v) => parseInt(v!))
//set confirmations
- if (confirmations > currentTheshold) {
- for (let i = currentTheshold; i < confirmations; i++) {
+ if (confirmations > currentThreshold) {
+ for (let i = currentThreshold; i < confirmations; i++) {
await tabs[1].locator('[data-testid="increase-threshold"]').click()
}
}
@@ -535,7 +562,7 @@ export default class Account extends Activity {
async joinMultisig() {
await this.accountListSelector.click()
- await this.addANewccountFromAccountList.click()
+ await this.addANewAccountFromAccountList.click()
await this.addMultisigAccountFromNewAccountScreen.click()
await this.joinExistingMultisig.click()
@@ -544,14 +571,48 @@ export default class Account extends Activity {
return String(await this.page.evaluate(`navigator.clipboard.readText()`))
}
+ 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({
- timeout: 60000,
- }),
+ expect(this.page.locator("label:has-text('Not activated')")).toBeHidden(),
expect(
this.page.locator('[data-testid="activating-multisig"]'),
- ).toBeHidden({ timeout: 60000 }),
+ ).toBeHidden(),
])
}
@@ -566,35 +627,121 @@ export default class Account extends Activity {
}
async acceptTx(tx: string) {
- await this.menuActivity.click()
+ await this.menuActivityLocator.click()
await this.page.locator(`[data-tx-hash="${tx}"]`).click()
- await this.confirm.click()
+ await this.confirmTransaction()
}
async setConfirmations(accountName: string, confirmations: number) {
await this.ensureSelectedAccount(accountName)
- await this.showSettings.click()
+ await this.showSettingsLocator.click()
await this.account(accountName).click()
await this.setConfirmationsLocator.click()
- const currentTheshold = await this.page
+ const currentThreshold = await this.page
.locator('[data-testid="threshold"]')
.innerText()
.then((v) => parseInt(v!))
- if (confirmations > currentTheshold) {
- for (let i = currentTheshold; i < confirmations; i++) {
+ if (confirmations > currentThreshold) {
+ for (let i = currentThreshold; i < confirmations; i++) {
await this.increaseThreshold.click()
}
- } else if (confirmations < currentTheshold) {
- for (let i = currentTheshold; i > confirmations; i--) {
+ } 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.confirm.click()
+ await this.confirmTransaction()
await Promise.all([
- expect(this.confirm).toBeHidden(),
- expect(this.menuActivity).toBeVisible(),
+ expect(this.confirmLocator).toBeHidden(),
+ expect(this.menuActivityLocator).toBeVisible(),
])
}
+
+ async ensure2FANotEnabled(accountName: string) {
+ await this.selectAccount(accountName)
+ await Promise.all([
+ expect(this.menuPendingTransactionsIndicatorLocator).toBeHidden(),
+ expect(
+ this.page.locator('[data-testid="shield-on-account-view"]'),
+ ).toBeHidden(),
+ ])
+ await this.showSettingsLocator.click()
+ await Promise.all([
+ expect(
+ this.page.locator('[data-testid="shield-on-settings"]'),
+ ).toBeHidden(),
+ expect(
+ this.page.locator('[data-testid="shield-not-activated"]'),
+ ).toBeVisible(),
+ ])
+ await this.account(accountName).click()
+ await expect(
+ this.page.locator('[data-testid="shield-switch"]'),
+ ).not.toBeChecked()
+ }
+
+ 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()
+ }
+ }
}
diff --git a/packages/e2e/extension/src/page-objects/Activity.ts b/packages/e2e/extension/src/page-objects/Activity.ts
index 2ebeaaac2..bdb833b70 100644
--- a/packages/e2e/extension/src/page-objects/Activity.ts
+++ b/packages/e2e/extension/src/page-objects/Activity.ts
@@ -21,7 +21,7 @@ export default class Activity extends Navigation {
this.page.locator(
`h6 div:text-is("${lang.account.pendingTransactions}") >> div`,
),
- ).not.toBeVisible({ timeout: 60000 })
+ ).not.toBeVisible()
}
activityByDestination(destination: string) {
@@ -32,7 +32,7 @@ export default class Activity extends Navigation {
checkActivity(nbr: number) {
return Promise.all([
- this.menuPendingTransactionsIndicator.click(),
+ this.menuPendingTransactionsIndicatorLocator.click(),
this.ensurePendingTransactions(nbr),
])
}
@@ -46,9 +46,9 @@ export default class Activity extends Navigation {
}
async getLastTxHash() {
- await this.menuActivityActive.isVisible().then(async (visible) => {
+ await this.menuActivityActiveLocator.isVisible().then(async (visible) => {
if (!visible) {
- await this.menuActivity.click()
+ await this.menuActivityLocator.click()
}
})
const txHashs = await this.activityTxHashs()
diff --git a/packages/e2e/extension/src/page-objects/AddressBook.ts b/packages/e2e/extension/src/page-objects/AddressBook.ts
index 999968ad8..e702d2ba8 100644
--- a/packages/e2e/extension/src/page-objects/AddressBook.ts
+++ b/packages/e2e/extension/src/page-objects/AddressBook.ts
@@ -24,27 +24,27 @@ export default class AddressBook extends Navigation {
return this.page.locator('[aria-label="network-selector"]')
}
- get save() {
+ get saveLocator() {
return this.page.locator(`button:text-is("${lang.common.save}")`)
}
- get cancel() {
+ get cancelLocator() {
return this.page.locator(`button:text-is("${lang.common.cancel}")`)
}
- networkOption(name: "Localhost 5050" | "Testnet" | "Mainnet") {
+ networkOption(name: "Localhost 5050" | "Goerli" | "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.address.nameRequired}')]`,
+ `//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.address.addressRequired}')]`,
+ `//textarea[@name="address"]/following::label[contains(text(), '${lang.settings.addressBook.addressRequired}')]`,
)
}
@@ -56,15 +56,19 @@ export default class AddressBook extends Navigation {
get deleteAddress() {
return this.page.locator(
- `button[aria-label="${lang.address.removeAddress}"]`,
+ `button[aria-label="${lang.settings.addressBook.removeAddress}"]`,
)
}
get delete() {
- return this.page.locator(`button:text-is("${lang.address.delete}")`)
+ return this.page.locator(
+ `button:text-is("${lang.settings.addressBook.delete}")`,
+ )
}
get addressBook() {
- return this.page.locator(`button:text-is("${lang.address.addressBook}")`)
+ return this.page.locator(
+ `button:text-is("${lang.settings.addressBook.addressBook}")`,
+ )
}
}
diff --git a/packages/e2e/extension/src/page-objects/Dapps.ts b/packages/e2e/extension/src/page-objects/Dapps.ts
index e7a5f612a..4aba31f2a 100644
--- a/packages/e2e/extension/src/page-objects/Dapps.ts
+++ b/packages/e2e/extension/src/page-objects/Dapps.ts
@@ -2,7 +2,12 @@ import { ChromiumBrowserContext, Page, expect } from "@playwright/test"
import { lang } from "../languages"
import Navigation from "./Navigation"
+import config from "../../../shared/config"
+type DappUrl =
+ | "https://goerli.app.starknet.id"
+ | "https://dapp-argentlabs.vercel.app"
+ | "https://starknetkit-blacked-listed.vercel.app"
export default class Dapps extends Navigation {
constructor(page: Page) {
super(page)
@@ -23,50 +28,129 @@ export default class Dapps extends Navigation {
}
get noConnectedDapps() {
- return this.page.locator(`text=${lang.dapps.noConnectedDapps}`)
+ return this.page.locator(
+ `text=${lang.settings.account.connectedDapps.noConnectedDapps}`,
+ )
}
- connected(url: string) {
+ connected(url: DappUrl) {
return this.page.locator(`//div/*[contains(text(),'${url.slice(8, 30)}')]`)
}
- disconnect(url: string) {
+ disconnect(url: DappUrl) {
return this.page.locator(
`//div/*[contains(text(),'${url.slice(8, 30)}')]/following::button[1]`,
)
}
disconnectAll() {
- return this.page.locator(`p:text-is("${lang.dapps.disconnectAll}")`)
+ return this.page.locator(
+ `p:text-is("${lang.settings.account.connectedDapps.disconnectAll}")`,
+ )
}
get accept() {
- return this.page.locator(`button:text-is("${lang.dapps.connect}")`)
+ return this.page.locator(
+ `button:text-is("${lang.settings.account.connectedDapps.connect}")`,
+ )
}
get reject() {
- return this.page.locator(`button:text-is("${lang.dapps.reject}")`)
+ return this.page.locator(
+ `button:text-is("${lang.settings.account.connectedDapps.reject}")`,
+ )
+ }
+
+ get knownDappButton() {
+ return this.page.locator('[data-testid="KnownDappButton"]')
}
+ async ensureKnowDappText() {
+ return Promise.all([
+ expect(this.page.locator('h5:text-is("Known Dapp")')).toBeVisible(),
+ expect(
+ this.page.locator('p:text-is("This dapp is listed on Dappland")'),
+ ).toBeVisible(),
+ ])
+ }
async requestConnectionFromDapp(
browserContext: ChromiumBrowserContext,
- url: string,
+ dappUrl: DappUrl,
) {
//open dapp page
const dapp = await browserContext.newPage()
await dapp.setViewportSize({ width: 1080, height: 720 })
await dapp.goto("chrome://inspect/#extensions")
await dapp.waitForTimeout(5000)
- await dapp.goto(url)
- const warningLoc = dapp.locator("text=enter anyway")
- if (await warningLoc.isVisible()) {
- await warningLoc.click()
+ await dapp.goto(dappUrl)
+
+ if (
+ dappUrl === "https://dapp-argentlabs.vercel.app" ||
+ dappUrl === "https://starknetkit-blacked-listed.vercel.app"
+ ) {
+ await expect(dapp.locator('button:has-text("Connect")')).toHaveCount(1)
+ await dapp.locator('button:has-text("Connect")').first().click()
+ await expect(dapp.locator("text=Argent X")).toBeVisible()
+ await dapp.locator("text=Argent X").click()
+ } else {
+ await expect(dapp.getByRole("button", { name: "Argent X" })).toBeVisible()
+ await dapp.getByRole("button", { name: "Argent X" }).click()
}
- await dapp
- .locator('div :text-matches("Connect Wallet", "i")')
- .first()
- .click()
+ return dapp
+ }
+
+ async claimSpok(browserContext: ChromiumBrowserContext) {
+ const spokCampaignUrl = config.spokCampaignUrl!
+ //open dapp page
+ const dapp = await browserContext.newPage()
+ await dapp.setViewportSize({ width: 1080, height: 720 })
+ await dapp.goto("chrome://inspect/#extensions")
+ await dapp.waitForTimeout(5000)
+ await dapp.goto(spokCampaignUrl)
+ await dapp.getByRole("button", { name: "Check eligibility" }).click()
await expect(dapp.locator("text=Argent X")).toBeVisible()
await dapp.locator("text=Argent X").click()
+ return dapp
+ }
+
+ checkCriticalRiskConnectionScreen() {
+ return Promise.all([
+ expect(
+ this.page.locator(
+ `//span[text()="Critical risk"]/following-sibling::label[text()="Use of a blacklisted domain"]`,
+ ),
+ ).toBeVisible(),
+ expect(
+ this.page.locator(
+ `//p[@data-testid="review-footer" and text()="Please review warnings before continuing"]`,
+ ),
+ ).toBeVisible(),
+ expect(this.page.getByRole("button", { name: "Connect" })).toBeDisabled(),
+ ])
+ }
+
+ async acceptCriticalRiskConnection() {
+ await this.page.getByRole("button", { name: "Review" }).click()
+ await Promise.all([
+ expect(
+ this.page.locator(
+ `//header[@title="1 risk identified"]//label[text()="We strongly recommend you do not proceed with this transaction"]`,
+ ),
+ ).toBeVisible(),
+ expect(
+ this.page.locator(
+ '//span[text()="Critical risk"]/following-sibling::span[text()="Use of a blacklisted domain"]/following-sibling::p[text()="You are currently on an unsafe domain. Be aware of the risks."]',
+ ),
+ ).toBeVisible(),
+ ])
+ await this.page.getByRole("button", { name: "Accept risk" }).click()
+ }
+
+ async connectedDappsTooltip(dappUrl: string) {
+ await this.showSettingsLocator.click()
+ await this.page.hover('[data-testid="connected-dapp"]')
+ await expect(
+ this.page.locator('[data-testid="connected-dapp"]'),
+ ).toHaveText(`Connected to ${dappUrl}`)
}
}
diff --git a/packages/e2e/extension/src/page-objects/DeveloperSettings.ts b/packages/e2e/extension/src/page-objects/DeveloperSettings.ts
index 5933a1928..6c9c5ba0d 100644
--- a/packages/e2e/extension/src/page-objects/DeveloperSettings.ts
+++ b/packages/e2e/extension/src/page-objects/DeveloperSettings.ts
@@ -7,25 +7,25 @@ export default class DeveloperSettings {
get manageNetworks() {
return this.page.locator(
- `//a//*[text()="${lang.developerSettings.manageNetworks}"]`,
+ `//a//*[text()="${lang.settings.developerSettings.manageNetworks.manageNetworks}"]`,
)
}
get blockExplorer() {
return this.page.locator(
- `//a//*[text()="${lang.developerSettings.blockExplorer}"]`,
+ `//a//*[text()="${lang.settings.preferences.defaultBlockExplorer}"]`,
)
}
get smartCOntractDevelopment() {
return this.page.locator(
- `//a//*[text()="${lang.developerSettings.smartContractDevelopment}"]`,
+ `//a//*[text()="${lang.settings.developerSettings.smartContractDevelopment}"]`,
)
}
get experimental() {
return this.page.locator(
- `//a//*[text()="${lang.developerSettings.experimental}"]`,
+ `//a//*[text()="${lang.settings.developerSettings.experimental}"]`,
)
}
@@ -56,7 +56,7 @@ export default class DeveloperSettings {
get restoreDefaultNetworks() {
return this.page.locator(
- `button:has-text("${lang.developerSettings.restoreDefaultNetworks}")`,
+ `button:has-text("${lang.settings.developerSettings.manageNetworks.restoreDefaultNetworks}")`,
)
}
diff --git a/packages/e2e/extension/src/page-objects/ExtensionPage.ts b/packages/e2e/extension/src/page-objects/ExtensionPage.ts
index f104b3568..e0bc7feeb 100644
--- a/packages/e2e/extension/src/page-objects/ExtensionPage.ts
+++ b/packages/e2e/extension/src/page-objects/ExtensionPage.ts
@@ -1,6 +1,6 @@
import { expect, type Page } from "@playwright/test"
-import Messages from "../utils/Messages"
+import Messages from "./Messages"
import Account from "./Account"
import Activity from "./Activity"
import AddressBook from "./AddressBook"
@@ -10,8 +10,17 @@ import Navigation from "./Navigation"
import Network from "./Network"
import Settings from "./Settings"
import Wallet from "./Wallet"
-import config from "../config"
-import { transferEth, AccountsToSetup, validateTx } from "../utils/account"
+import config from "../../../shared/config"
+import Nfts from "./Nfts"
+import Preferences from "./Preferences"
+import {
+ transferTokens,
+ AccountsToSetup,
+ validateTx,
+ isScientific,
+ convertScientificToDecimal,
+ FeeTokens,
+} from "../../../shared/src/assets"
export default class ExtensionPage {
page: Page
@@ -25,6 +34,8 @@ export default class ExtensionPage {
developerSettings: DeveloperSettings
addressBook: AddressBook
dapps: Dapps
+ nfts: Nfts
+ preferences: Preferences
constructor(page: Page, private extensionUrl: string) {
this.page = page
this.wallet = new Wallet(page)
@@ -38,6 +49,8 @@ export default class ExtensionPage {
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)
}
async open() {
@@ -45,10 +58,10 @@ export default class ExtensionPage {
}
async resetExtension() {
- await this.navigation.showSettings.click()
- await this.navigation.lockWallet.click()
- await this.navigation.reset.click()
- await this.navigation.confirmReset.click()
+ await this.navigation.showSettingsLocator.click()
+ await this.navigation.lockWalletLocator.click()
+ await this.navigation.resetLocator.click()
+ await this.navigation.confirmResetLocator.click()
}
async paste() {
@@ -69,15 +82,13 @@ export default class ExtensionPage {
await this.wallet.restoreExistingWallet.click()
await this.setClipBoardContent(seed)
await this.pasteSeed()
- await this.navigation.continue.click()
+ await this.navigation.continueLocator.click()
await this.wallet.password.fill(password ?? config.password)
await this.wallet.repeatPassword.fill(password ?? config.password)
- await this.navigation.continue.click()
- await expect(this.wallet.finish.first()).toBeVisible({
- timeout: 180000,
- })
+ await this.navigation.continueLocator.click()
+ await expect(this.wallet.finish.first()).toBeVisible()
await this.open()
await expect(this.network.networkSelector).toBeVisible()
@@ -95,51 +106,67 @@ export default class ExtensionPage {
return accountAddress
}
- async deployAccount(accountName: string) {
+ async deployAccount(accountName: string, feeToken?: FeeTokens) {
if (accountName) {
await this.account.ensureSelectedAccount(accountName)
}
- await this.navigation.showSettings.click()
- await this.page.locator(`text=${accountName}`).click()
+ await this.navigation.showSettingsLocator.click()
+ await this.page.locator(`[data-testid="${accountName}"]`).click()
await this.settings.deployAccount.click()
- await this.navigation.confirm.click()
- await this.navigation.back.click()
- await this.navigation.close.click()
- await this.navigation.menuActivity.click()
+ if (feeToken) {
+ await this.account.selectFeeToken(feeToken)
+ }
+ await this.account.confirmTransaction()
+ await this.navigation.backLocator.click()
+ await this.navigation.closeLocator.click()
+ await this.navigation.menuActivityLocator.click()
await expect(
this.page.getByText(
/(Account created and transfer|Contract interaction)/,
),
- ).toBeVisible({ timeout: 120000 })
- await this.navigation.showSettings.click()
- await expect(this.page.getByText("Deploying")).toBeHidden({
- timeout: 90000,
- })
- await this.navigation.close.click()
- await this.navigation.menuTokens.click()
+ ).toBeVisible()
+ await this.navigation.showSettingsLocator.click()
+ await expect(this.page.getByText("Deploying")).toBeHidden()
+ await this.navigation.closeLocator.click()
+ await this.navigation.menuTokensLocator.click()
}
- async activate2fa(accountName: string, email: string, pin = "111111") {
+ async activate2fa({
+ accountName,
+ email,
+ pin = "111111",
+ validSession = false,
+ }: {
+ accountName: string
+ email: string
+ pin?: string
+ validSession?: boolean
+ }) {
await this.account.ensureSelectedAccount(accountName)
- await this.navigation.showSettings.click()
+ await this.navigation.showSettingsLocator.click()
await this.settings.account(accountName).click()
await this.settings.argentShield().click()
- await this.navigation.next.click()
- await this.account.email.fill(email)
- await this.navigation.next.first().click()
- await this.account.fillPin(pin)
- await this.navigation.addArgentShield.click()
- await this.navigation.confirm.click()
- await this.navigation.dismiss.click()
- await this.navigation.back.click()
- await this.navigation.close.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.addArgentShieldLocator.click()
+ await this.account.confirmTransaction()
+ await expect(this.account.addedArgentShieldLocator).toBeVisible()
+ await this.navigation.doneLocator.click()
+ await this.navigation.backLocator.click()
+ await this.navigation.closeLocator.click()
await Promise.all([
- expect(this.activity.menuPendingTransactionsIndicator).toBeHidden(),
+ expect(
+ this.activity.menuPendingTransactionsIndicatorLocator,
+ ).toBeHidden(),
expect(
this.page.locator('[data-testid="shield-on-account-view"]'),
).toBeVisible(),
])
- await this.navigation.showSettings.click()
+ await this.navigation.showSettingsLocator.click()
await expect(
this.page.locator('[data-testid="shield-on-settings"]'),
).toBeVisible()
@@ -147,8 +174,71 @@ export default class ExtensionPage {
await expect(
this.page.locator('[data-testid="shield-switch"]'),
).toBeEnabled()
- await this.navigation.back.click()
- await this.navigation.close.click()
+ await this.navigation.backLocator.click()
+ await this.navigation.closeLocator.click()
+ }
+
+ async disable2fa({
+ 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.argentShield().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.removeArgentShieldLocator.click()
+ await this.account.confirmTransaction()
+ await expect(this.account.removedArgentShieldLocator).toBeVisible()
+ await this.navigation.doneLocator.click()
+ await this.navigation.backLocator.click()
+ await this.navigation.closeLocator.click()
+ await this.account.ensure2FANotEnabled(accountName)
+ }
+
+ async fundAccount(
+ acc: AccountsToSetup,
+ accountAddress: string,
+ accIndex: number,
+ ) {
+ let expectedTokenValue
+ for (const [assetIndex, asset] of acc.assets.entries()) {
+ console.log({ op: "fundAccount", assetIndex, asset })
+ if (asset.balance > 0) {
+ await transferTokens(
+ asset.balance,
+ accountAddress, // receiver wallet address
+ asset.token,
+ )
+ expectedTokenValue = `${asset.balance} ${asset.token}`
+ if (isScientific(asset.balance)) {
+ expectedTokenValue = `${convertScientificToDecimal(asset.balance)} ${
+ 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({
@@ -162,7 +252,6 @@ export default class ExtensionPage {
const accountAddresses: string[] = []
for (const [accIndex, acc] of accountsToSetup.entries()) {
- console.log(accIndex, acc)
if (accIndex !== 0) {
await this.account.addAccount({ firstAccount: false })
}
@@ -172,40 +261,48 @@ export default class ExtensionPage {
)
expect(accountAddress).toMatch(/^0x0/)
accountAddresses.push(accountAddress)
-
- if (acc.initialBalance > 0) {
- await transferEth(
- `${acc.initialBalance * Math.pow(10, 18)}`, // amount Ethereum has 18 decimals
- accountAddress, // reciever wallet address
- )
- await this.account.ensureAsset(
- `Account ${accIndex + 1}`,
- "Ethereum",
- `${acc.initialBalance} ETH`,
- )
- if (acc.deploy) {
- await this.deployAccount(`Account ${accIndex + 1}`)
- }
+ if (acc.assets[0].balance > 0) {
+ await this.fundAccount(acc, accountAddress, accIndex)
}
}
- console.log(accountAddresses.length, accountAddresses, seed)
+ console.log({
+ op: "setupWallet",
+ accountsNbr: accountAddresses.length,
+ accountAddresses,
+ seed,
+ })
return { accountAddresses, seed }
}
- async validateTx(
- txHash: string,
- reciever: string,
- amount?: number,
- uniqLocator?: boolean,
- ) {
- await this.navigation.menuActivityActive
+ async validateTx({
+ txHash,
+ receiver,
+ sendAmountFE,
+ sendAmountTX,
+ uniqLocator,
+ }: {
+ txHash: string
+ receiver: string
+ sendAmountFE?: string
+ sendAmountTX?: number
+ uniqLocator?: boolean
+ }) {
+ console.log({
+ op: "validateTx",
+ txHash,
+ receiver,
+ sendAmountFE,
+ sendAmountTX,
+ uniqLocator,
+ })
+ await this.navigation.menuActivityActiveLocator
.isVisible()
.then(async (visible) => {
if (!visible) {
- await this.navigation.menuActivity.click()
+ await this.navigation.menuActivityLocator.click()
}
})
- if (amount) {
+ if (sendAmountFE) {
const activityAmountLocator = this.page.locator(
`button[data-tx-hash$="${txHash.substring(3)}"] [data-value]`,
)
@@ -216,19 +313,21 @@ export default class ExtensionPage {
const activityAmount = await activityAmountElement
.textContent()
.then((text) => text?.match(/[\d|.]+/)![0])
- if (amount.toString().length > 6) {
+ if (sendAmountFE.toString().length > 6) {
expect(activityAmount).toBe(
- parseFloat(amount.toString())
+ parseFloat(sendAmountFE.toString())
.toFixed(4)
.toString()
.match(/[\d\\.]+[^0]+/)?.[0],
)
} else {
- expect(activityAmount).toBe(parseFloat(amount.toString()).toString())
+ expect(activityAmount).toBe(
+ parseFloat(sendAmountFE.toString()).toString(),
+ )
}
}
await this.activity.ensureNoPendingTransactions()
- await validateTx(txHash, reciever, amount)
+ await validateTx(txHash, receiver, sendAmountTX)
}
async fundMultisigAccount({
@@ -241,11 +340,11 @@ export default class ExtensionPage {
await this.account.ensureSelectedAccount(accountName)
await this.account.copyAddress.click()
const accountAddress = await this.getClipboard().then((adr) => String(adr))
- await transferEth(
- `${amount * Math.pow(10, 18)}`, // amount Ethereum has 18 decimals
- accountAddress, // reciever wallet address
+ await transferTokens(
+ amount,
+ accountAddress, // receiver wallet address
)
- await this.account.ensureAsset(accountName, "Ethereum", `${amount} ETH`)
+ await this.account.ensureAsset(accountName, "ETH", `${amount} ETH`)
}
async activateMultisig(accountName: string) {
@@ -254,17 +353,21 @@ export default class ExtensionPage {
this.page.locator("label:has-text('Not activated')"),
).toBeVisible()
await this.page.locator('[data-testid="activate-multisig"]').click()
- await this.navigation.confirm.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('Not activated')")).toBeHidden({
- timeout: 60000,
- }),
+ expect(this.page.locator("label:has-text('Not activated')")).toBeHidden(),
expect(
this.page.locator('[data-testid="activating-multisig"]'),
- ).toBeHidden({ timeout: 60000 }),
+ ).toBeHidden(),
])
}
+
+ async removeMultisigOwner(accountName: string) {
+ await this.account.ensureSelectedAccount(accountName)
+ await this.navigation.showSettingsLocator.click()
+ await this.settings.account(accountName).click()
+ }
}
diff --git a/packages/e2e/extension/src/utils/Messages.ts b/packages/e2e/extension/src/page-objects/Messages.ts
similarity index 100%
rename from packages/e2e/extension/src/utils/Messages.ts
rename to packages/e2e/extension/src/page-objects/Messages.ts
diff --git a/packages/e2e/extension/src/page-objects/Navigation.ts b/packages/e2e/extension/src/page-objects/Navigation.ts
index 650fa7dd6..8a959d08a 100644
--- a/packages/e2e/extension/src/page-objects/Navigation.ts
+++ b/packages/e2e/extension/src/page-objects/Navigation.ts
@@ -8,109 +8,123 @@ export default class Navigation {
this.page = page
}
- get back() {
+ get backLocator() {
return this.page.locator(`[aria-label="${lang.common.back}"]`).first()
}
- get close() {
+ get closeLocator() {
return this.page.locator(`[aria-label="${lang.common.close}"]`)
}
-
- get confirm() {
+ get closeButtonLocator() {
+ return this.page.locator('[data-testid="close-button"]')
+ }
+ get confirmLocator() {
return this.page.locator(`button:text-is("${lang.common.confirm}")`)
}
- get next() {
+ get nextLocator() {
return this.page.locator(`button:text-is("${lang.common.next}")`)
}
- get reviewSend() {
+ get reviewSendLocator() {
return this.page.locator(`button:text-is("${lang.common.reviewSend}")`)
}
- get done() {
+ get doneLocator() {
return this.page.locator(`button:text-is("${lang.common.done}")`)
}
- get continue() {
+ get continueLocator() {
return this.page
.locator(`button:text-is("${lang.common.continue}")`)
.first()
}
- get yes() {
+ get yesLocator() {
return this.page.locator(`button:text-is("${lang.common.yes}")`)
}
- get no() {
+ get noLocator() {
return this.page.locator(`button:text-is("${lang.common.no}")`)
}
- get unlock() {
+ get unlockLocator() {
return this.page.locator(`button:text-is("${lang.common.unlock}")`)
}
- get showSettings() {
+ get showSettingsLocator() {
return this.page.locator('[aria-label="Show settings"]')
}
- get lockWallet() {
- return this.page.locator(`//button//*[text()="${lang.common.lockWallet}"]`)
+ get lockWalletLocator() {
+ return this.page.locator(
+ `//button//*[text()="${lang.settings.lockWallet}"]`,
+ )
}
- get reset() {
+ get resetLocator() {
return this.page.locator(`a:text-is("${lang.common.reset}")`)
}
- get confirmReset() {
+ get confirmResetLocator() {
return this.page.locator(`button:text-is("${lang.common.confirmReset}")`)
}
- get menuPendingTransactionsIndicator() {
+ get menuPendingTransactionsIndicatorLocator() {
return this.page.locator('[aria-label="Pending transactions"]')
}
- get menuTokens() {
+ get menuTokensLocator() {
return this.page.locator('[aria-label="Tokens"]')
}
- get menuNTFs() {
+ get menuNTFsLocator() {
return this.page.locator('[aria-label="NFTs"]')
}
- get menuSwaps() {
+ get menuSwapsLocator() {
return this.page.locator('[aria-label="Swap"]')
}
- get menuActivity() {
+ get menuActivityLocator() {
return this.page.locator('[aria-label="Activity"]')
}
- get menuActivityActive() {
+ get menuActivityActiveLocator() {
return this.page.locator('[aria-label="Activity"][class*="active"]')
}
- get save() {
+ get saveLocator() {
return this.page.locator(`button:text-is("${lang.common.save}")`)
}
- get create() {
+ get createLocator() {
return this.page.locator(`button:text-is("${lang.common.create}")`)
}
- get cancel() {
+ get cancelLocator() {
return this.page.locator(`button:text-is("${lang.common.cancel}")`)
}
- get approve() {
+ get approveLocator() {
return this.page.locator(`button:text-is("${lang.common.approve}")`)
}
- get addArgentShield() {
+ get addArgentShieldLocator() {
return this.page.locator(`button:text-is("${lang.common.addArgentShield}")`)
}
- get dismiss() {
+ get removeArgentShieldLocator() {
+ return this.page.locator(
+ `button:text-is("${lang.common.removeArgentShield}")`,
+ )
+ }
+
+ get dismissLocator() {
return this.page.locator(`button:text-is("${lang.common.dismiss}")`)
}
+
+ get removeLocator() {
+ return this.page.locator(`button:text-is("${lang.common.remove}")`)
+ }
}
diff --git a/packages/e2e/extension/src/page-objects/Network.ts b/packages/e2e/extension/src/page-objects/Network.ts
index fb52427c6..a9f22d9e0 100644
--- a/packages/e2e/extension/src/page-objects/Network.ts
+++ b/packages/e2e/extension/src/page-objects/Network.ts
@@ -1,5 +1,7 @@
import { Page, expect } from "@playwright/test"
-type NetworkName = "Localhost 5050" | "Testnet" | "Mainnet" | "My Network"
+
+type NetworkName = "Devnet" | "Goerli" | "Mainnet" | "My Network"
+
export function getDefaultNetwork() {
const argentXEnv = process.env.ARGENT_X_ENVIRONMENT
@@ -39,6 +41,12 @@ export default class Network {
await this.networkOption(networkName).click()
}
+ async selectDefaultNetwork() {
+ const networkName = this.getDefaultNetworkName()
+ await this.networkSelector.click()
+ await this.networkOption(networkName).click()
+ }
+
async ensureAvailableNetworks(networks: string[]) {
await this.networkSelector.click()
const availableNetworks = await this.page
@@ -52,8 +60,10 @@ export default class Network {
switch (defaultNetworkId.toLowerCase()) {
case "mainnet-alpha":
return "Mainnet"
+ case "sepolia-alpha":
+ return "Sepolia"
case "goerli-alpha":
- return "Testnet"
+ return "Goerli"
default:
throw new Error(`Unknown ARGENTX_Network: ${defaultNetworkId}`)
}
diff --git a/packages/e2e/extension/src/page-objects/Nfts.ts b/packages/e2e/extension/src/page-objects/Nfts.ts
new file mode 100644
index 000000000..4db6bbff4
--- /dev/null
+++ b/packages/e2e/extension/src/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(`h6: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/packages/e2e/extension/src/page-objects/Preferences.ts b/packages/e2e/extension/src/page-objects/Preferences.ts
new file mode 100644
index 000000000..7c15b11da
--- /dev/null
+++ b/packages/e2e/extension/src/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 hideTokens() {
+ 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/packages/e2e/extension/src/page-objects/Settings.ts b/packages/e2e/extension/src/page-objects/Settings.ts
index fdf42062d..8381825e0 100644
--- a/packages/e2e/extension/src/page-objects/Settings.ts
+++ b/packages/e2e/extension/src/page-objects/Settings.ts
@@ -10,22 +10,26 @@ export default class Settings {
}
get addressBook() {
- return this.page.locator(`//a//*[text()="${lang.settings.addresBook}"]`)
+ return this.page.locator(
+ `//a//*[text()="${lang.settings.addressBook.addressBook}"]`,
+ )
}
get connectedDapps() {
- return this.page.locator(`//a//*[text()="${lang.settings.connectedDapps}"]`)
+ return this.page.locator(
+ `//a//*[text()="${lang.settings.account.connectedDapps.connectedDapps}"]`,
+ )
}
- get showRecoveryPhase() {
+ get developerSettings() {
return this.page.locator(
- `//a//*[text()="${lang.settings.showRecoveryPhase}"]`,
+ `//a//*[text()="${lang.settings.developerSettings.developerSettings}"]`,
)
}
- get developerSettings() {
+ get preferences() {
return this.page.locator(
- `//a//*[text()="${lang.settings.developerSettings}"]`,
+ `//a//*[text()="${lang.settings.preferences.preferences}"]`,
)
}
@@ -36,19 +40,19 @@ export default class Settings {
get exportPrivateKey() {
return this.page.locator(
- `//button//*[text()="${lang.settings.exportPrivateKey}"]`,
+ `//button//*[text()="${lang.settings.account.exportPrivateKey}"]`,
)
}
get deployAccount() {
return this.page.locator(
- `//button//*[text()="${lang.settings.deployAccount}"]`,
+ `//button//*[text()="${lang.settings.account.deployAccount}"]`,
)
}
get hideAccount() {
return this.page.locator(
- `//button//*[text()="${lang.settings.hideAccount}"]`,
+ `//button//*[text()="${lang.settings.account.hideAccount}"]`,
)
}
@@ -63,12 +67,10 @@ export default class Settings {
}
get confirmHide() {
- return this.page.locator(`button:text-is("${lang.settings.hide}")`)
+ return this.page.locator(`button:text-is("${lang.common.hide}")`)
}
get hiddenAccounts() {
- return this.page.locator(
- `button:text-is("${lang.settings.hiddenAccounts}")`,
- )
+ return this.page.locator(`button:text-is("${lang.common.hiddenAccounts}")`)
}
unhideAccount(accountName: string) {
@@ -80,11 +82,11 @@ export default class Settings {
}
get privateKey() {
- return this.page.locator('[data-testid="privateKey"]')
+ return this.page.locator('[aria-label="Private key"]')
}
get copy() {
- return this.page.locator(`button:text-is("${lang.settings.copy}")`)
+ return this.page.locator(`button:text-is("${lang.common.copy}")`)
}
get help() {
@@ -99,11 +101,9 @@ export default class Settings {
return this.page.getByRole("link", { name: "Github" })
}
- get privacyStatement() {
- return this.page.getByRole("link", { name: "Privacy statement" })
- }
-
- get privacyStatementText() {
- return this.page.locator('[aria-label="privacyStatementText"]')
+ get viewOnStarkScanLocator() {
+ return this.page.getByRole("button", {
+ name: lang.settings.account.viewOnStarkScan,
+ })
}
}
diff --git a/packages/e2e/extension/src/page-objects/Wallet.ts b/packages/e2e/extension/src/page-objects/Wallet.ts
index 22cc24c54..bc523eb28 100644
--- a/packages/e2e/extension/src/page-objects/Wallet.ts
+++ b/packages/e2e/extension/src/page-objects/Wallet.ts
@@ -1,6 +1,6 @@
import { Page, expect } from "@playwright/test"
-import config from "../config"
+import config from "../../../shared/config"
import { lang } from "../languages"
import Navigation from "./Navigation"
@@ -40,12 +40,8 @@ export default class Wallet extends Navigation {
)
}
- get privacyStatement() {
- return this.page.getByRole("link", { name: "Privacy statement" })
- }
-
- get privacyStatementText() {
- return this.page.locator('[aria-label="privacyStatementText"]')
+ get privacyPolicyLink() {
+ return this.page.getByRole("link", { name: "Privacy Policy" })
}
//third screen
@@ -94,21 +90,6 @@ export default class Wallet extends Navigation {
])
await this.createNewWallet.click()
- await Promise.all([
- expect(this.banner2).toBeVisible(),
- expect(this.description2).toBeVisible(),
- ])
- await expect(this.privacyStatement).toBeVisible()
- await this.privacyStatement.click()
- await expect(this.privacyStatementText).toHaveText(
- lang.common.privacyStatement,
- )
-
- await this.page.locator('button:text-is("Back")').click()
- await this.disclaimerLostOfFunds.click()
- await this.disclaimerAlphaVersion.click()
- await this.continue.click()
-
await Promise.all([
expect(this.banner3).toBeVisible(),
expect(this.description3).toBeVisible(),
diff --git a/packages/e2e/extension/src/specs/2FA.spec.ts b/packages/e2e/extension/src/specs/2FA.spec.ts
index 773cfda9d..136cf9df9 100644
--- a/packages/e2e/extension/src/specs/2FA.spec.ts
+++ b/packages/e2e/extension/src/specs/2FA.spec.ts
@@ -1,18 +1,22 @@
import { expect } from "@playwright/test"
import test from "../test"
-import { expireBESession } from "../utils/common"
+import { expireBESession } from "../../../shared/src/common"
import { v4 as uuid } from "uuid"
-import config from "../config"
+import config from "../../../shared/config"
+import { lang } from "../languages"
const generateEmail = () => `e2e_2fa_${uuid()}@mail.com`
test.describe("2FA", () => {
+ test.slow()
test("User should not be able to enable 2FA for a non deployed account", async ({
extension,
}) => {
- await extension.setupWallet({ accountsToSetup: [{ initialBalance: 0 }] })
- await extension.navigation.showSettings.click()
+ await extension.setupWallet({
+ accountsToSetup: [{ assets: [{ token: "ETH", balance: 0 }] }],
+ })
+ await extension.navigation.showSettingsLocator.click()
await extension.settings.account(extension.account.accountName1).click()
await extension.settings.argentShield().first().click()
await expect(extension.account.deployNeededWarning).toBeVisible()
@@ -23,16 +27,61 @@ test.describe("2FA", () => {
}) => {
const email = generateEmail()
await extension.setupWallet({
- accountsToSetup: [{ initialBalance: 0.002, deploy: true }],
+ accountsToSetup: [
+ { assets: [{ token: "ETH", balance: 0.002 }], deploy: true },
+ { assets: [{ token: "ETH", balance: 0 }] },
+ ],
+ })
+ await extension.activate2fa({
+ accountName: extension.account.accountName1,
+ email,
})
- await extension.activate2fa(extension.account.accountName1, email)
await extension.account.transfer({
originAccountName: extension.account.accountName1,
- recipientAddress: config.senderAddr!,
- tokenName: "Ethereum",
+ recipientAddress: config.destinationAddress!,
+ token: "ETH",
amount: "MAX",
})
await extension.activity.checkActivity(1)
+ await extension.activity.ensureNoPendingTransactions()
+ await extension.navigation.menuTokensLocator.click()
+
+ //other accounts should have independent Argent Shield
+ await extension.account.ensure2FANotEnabled(extension.account.accountName2)
+ })
+
+ test("User should be able to enable/disable 2FA for all accounts", async ({
+ extension,
+ }) => {
+ const email = generateEmail()
+ await extension.setupWallet({
+ accountsToSetup: [
+ { assets: [{ token: "ETH", balance: 0.001 }], deploy: true },
+ { assets: [{ token: "ETH", balance: 0.001 }], deploy: true },
+ ],
+ })
+ await extension.activate2fa({
+ accountName: extension.account.accountName1,
+ email,
+ })
+ await extension.activate2fa({
+ accountName: extension.account.accountName2,
+ email,
+ validSession: true,
+ })
+
+ await extension.disable2fa({
+ accountName: extension.account.accountName1,
+ email,
+ validSession: true,
+ })
+ await extension.navigation.backLocator.click()
+ await extension.navigation.closeLocator.click()
+ await extension.disable2fa({
+ accountName: extension.account.accountName2,
+ email,
+ validSession: true,
+ })
})
test("Recover wallet with 2FA, authentication needed before create a TX", async ({
@@ -40,27 +89,33 @@ test.describe("2FA", () => {
}) => {
const email = generateEmail()
const { seed } = await extension.setupWallet({
- accountsToSetup: [{ initialBalance: 0.002, deploy: true }],
+ accountsToSetup: [
+ { assets: [{ token: "ETH", balance: 0.002 }], deploy: true },
+ ],
+ })
+
+ await extension.activate2fa({
+ accountName: extension.account.accountName1,
+ email,
})
- await extension.activate2fa(extension.account.accountName1, email)
await extension.resetExtension()
await extension.recoverWallet(seed)
- await extension.account.token("Ethereum").click()
+ await extension.account.token("ETH").click()
await extension.account.fillRecipientAddress({
- recipientAddress: config.senderAddr!,
+ recipientAddress: config.destinationAddress!,
})
await extension.account.email.fill(email)
- await extension.navigation.next.first().click()
+ await extension.navigation.nextLocator.first().click()
await extension.account.fillPin("111111")
await Promise.all([
expect(extension.account.balance).toBeVisible(),
expect(extension.account.sendMax).toBeVisible(),
])
await extension.account.sendMax.click()
- await extension.account.reviewSend.click()
- await extension.account.confirm.click()
+ await extension.account.reviewSendLocator.click()
+ await extension.account.confirmTransaction()
await extension.activity.checkActivity(1)
})
@@ -69,13 +124,19 @@ test.describe("2FA", () => {
}) => {
const email = generateEmail()
await extension.setupWallet({
- accountsToSetup: [{ initialBalance: 0.002, deploy: true }],
+ accountsToSetup: [
+ { assets: [{ token: "ETH", balance: 0.002 }], deploy: true },
+ ],
})
- await extension.activate2fa(extension.account.accountName1, email)
- await expireBESession(email)
- await extension.account.token("Ethereum").click()
+
+ await extension.activate2fa({
+ accountName: extension.account.accountName1,
+ email,
+ })
+ await expireBESession(email, "argentx")
+ await extension.account.token("ETH").click()
await extension.account.fillRecipientAddress({
- recipientAddress: config.senderAddr!,
+ recipientAddress: config.destinationAddress!,
})
await extension.account.fillPin("111111")
await Promise.all([
@@ -83,8 +144,64 @@ test.describe("2FA", () => {
expect(extension.account.sendMax).toBeVisible(),
])
await extension.account.sendMax.click()
- await extension.account.reviewSend.click()
- await extension.account.confirm.click()
+ await extension.account.reviewSendLocator.click()
+ await extension.account.confirmTransaction()
await extension.activity.checkActivity(1)
})
+
+ test("Try to activate 2FA with an email already in use", async ({
+ extension,
+ }) => {
+ await extension.setupWallet({
+ accountsToSetup: [
+ { assets: [{ token: "ETH", balance: 0.001 }], deploy: true },
+ ],
+ })
+ await extension.account.ensureSelectedAccount(
+ extension.account.accountName1,
+ )
+ await extension.navigation.showSettingsLocator.click()
+ await extension.settings.account(extension.account.accountName1).click()
+ await extension.settings.argentShield().click()
+ await extension.navigation.nextLocator.click()
+ await extension.account.email.fill("registeredemail@argent.xyz")
+ await extension.navigation.nextLocator.first().click()
+ await extension.account.fillPin("111111")
+ await expect(
+ extension.page.getByText(lang.account.argentShield.emailInUse),
+ ).toBeVisible()
+ })
+
+ test("Verify error message when user insert wrong code 3 times", async ({
+ extension,
+ }) => {
+ await extension.setupWallet({
+ accountsToSetup: [
+ { assets: [{ token: "ETH", balance: 0.001 }], deploy: true },
+ ],
+ })
+ const email = generateEmail()
+ await extension.account.ensureSelectedAccount(
+ extension.account.accountName1,
+ )
+ await extension.navigation.showSettingsLocator.click()
+ await extension.settings.account(extension.account.accountName1).click()
+ await extension.settings.argentShield().click()
+ await extension.navigation.nextLocator.click()
+ await extension.account.email.fill(email)
+ await extension.navigation.nextLocator.first().click()
+ await extension.account.fillPin("222222")
+ await expect(
+ extension.page.getByText(lang.account.argentShield.wrong2faCode),
+ ).toBeVisible()
+ await extension.page.locator('input[data-index="5"]').fill("3")
+ await extension.page.locator('input[data-index="5"]').fill("4")
+ await expect(
+ extension.page.getByText(lang.account.argentShield.failed2faCode),
+ ).toBeVisible()
+ await extension.page.locator('input[data-index="5"]').fill("5")
+ await expect(
+ extension.page.getByText(lang.account.argentShield.codeNotRequested2fa),
+ ).toBeVisible()
+ })
})
diff --git a/packages/e2e/extension/src/specs/accountSettings.spec.ts b/packages/e2e/extension/src/specs/accountSettings.spec.ts
index cd2301817..bf1dd7c12 100644
--- a/packages/e2e/extension/src/specs/accountSettings.spec.ts
+++ b/packages/e2e/extension/src/specs/accountSettings.spec.ts
@@ -1,6 +1,6 @@
import { expect } from "@playwright/test"
-import config from "../config"
+import config from "../../../shared/config"
import test from "../test"
import { lang } from "../languages"
@@ -9,19 +9,19 @@ test.describe("Account settings", () => {
await extension.wallet.newWalletOnboarding()
await extension.open()
await expect(extension.network.networkSelector).toBeVisible()
- await extension.network.selectNetwork("Testnet")
+ await extension.network.selectDefaultNetwork()
const [accountName1] = await extension.account.addAccount({
firstAccount: false,
})
- await extension.navigation.showSettings.click()
+ await extension.navigation.showSettingsLocator.click()
await extension.settings.account(accountName1!).click()
await extension.settings.setAccountName("My new account name")
await expect(extension.settings.accountName).toHaveValue(
"My new account name",
)
- await extension.navigation.back.click()
- await extension.navigation.close.click()
+ await extension.navigation.backLocator.click()
+ await extension.navigation.closeLocator.click()
await extension.account.ensureSelectedAccount("My new account name")
})
@@ -30,17 +30,17 @@ test.describe("Account settings", () => {
await extension.wallet.newWalletOnboarding()
await extension.open()
await expect(extension.network.networkSelector).toBeVisible()
- await extension.network.selectNetwork("Testnet")
+ await extension.network.selectDefaultNetwork()
const [accountName1] = await extension.account.addAccount({
firstAccount: false,
})
- await extension.navigation.showSettings.click()
+ await extension.navigation.showSettingsLocator.click()
await extension.settings.account(accountName1!).click()
await extension.settings.exportPrivateKey.click()
await extension.account.password.fill(config.password)
- await extension.account.exportPrivateKey.click()
- await extension.settings.privateKey.click()
+ await extension.account.unlockLocator.click()
+ await extension.settings.copy.click()
//ensure that copy is working
const clipboardPrivateKey = await extension.page.evaluate(
"navigator.clipboard.readText()",
@@ -49,7 +49,7 @@ test.describe("Account settings", () => {
await extension.settings.privateKey.textContent(),
)
- await extension.navigation.done.click()
+ await extension.navigation.backLocator.click()
await expect(extension.settings.exportPrivateKey).toBeVisible()
})
@@ -57,12 +57,12 @@ test.describe("Account settings", () => {
await extension.wallet.newWalletOnboarding()
await extension.open()
await expect(extension.network.networkSelector).toBeVisible()
- await extension.network.selectNetwork("Testnet")
+ await extension.network.selectDefaultNetwork()
const [accountName2] = await extension.account.addAccount({
firstAccount: false,
})
- await extension.navigation.showSettings.click()
+ await extension.navigation.showSettingsLocator.click()
await extension.settings.account(accountName2!).click()
await extension.settings.hideAccount.click()
await extension.settings.confirmHide.click()
@@ -71,7 +71,7 @@ test.describe("Account settings", () => {
await extension.settings.hiddenAccounts.click()
await extension.settings.unhideAccount(accountName2!).click()
- await extension.navigation.back.click()
+ await extension.navigation.backLocator.click()
await expect(extension.settings.hiddenAccounts).toBeHidden()
await expect(extension.account.account(accountName2!)).toBeVisible()
})
@@ -82,11 +82,11 @@ test.describe("Account settings", () => {
await extension.wallet.newWalletOnboarding()
await extension.open()
await expect(extension.network.networkSelector).toBeVisible()
- await extension.navigation.showSettings.click()
- await extension.navigation.lockWallet.click()
+ await extension.navigation.showSettingsLocator.click()
+ await extension.navigation.lockWalletLocator.click()
await extension.account.password.fill(config.password)
- await extension.navigation.unlock.click()
+ await extension.navigation.unlockLocator.click()
await expect(extension.network.networkSelector).toBeVisible()
})
@@ -96,14 +96,50 @@ test.describe("Account settings", () => {
await extension.wallet.newWalletOnboarding()
await extension.open()
await expect(extension.network.networkSelector).toBeVisible()
- await extension.navigation.showSettings.click()
- await extension.navigation.lockWallet.click()
+ await extension.navigation.showSettingsLocator.click()
+ await extension.navigation.lockWalletLocator.click()
- await extension.account.password.fill("wrongpassword123!")
- await extension.navigation.unlock.click()
+ await extension.account.password.fill("wrongPassword123!")
+ await extension.navigation.unlockLocator.click()
await expect(
extension.page.locator(`label:text-is("${lang.account.wrongPassword}")`),
).toBeVisible()
await expect(extension.account.password).toBeVisible()
})
+
+ test("Detect outside deploy", async ({ extension, secondExtension }) => {
+ const { seed } = await extension.setupWallet({
+ accountsToSetup: [{ assets: [{ token: "ETH", balance: 0.01 }] }],
+ })
+ await secondExtension.open()
+ await Promise.all([
+ extension.account.transfer({
+ originAccountName: extension.account.accountName1,
+ recipientAddress: config.destinationAddress!,
+ token: "ETH",
+ amount: "MAX",
+ }),
+ secondExtension.recoverWallet(seed),
+ ])
+
+ //ensure that balance is updated
+ await expect(extension.account.currentBalance("ETH")).toContainText("0.00")
+ await extension.navigation.showSettingsLocator.click()
+ await extension.settings.account(extension.account.accountName1).click()
+ await Promise.all([
+ expect(extension.settings.deployAccount).toBeHidden(),
+ expect(extension.settings.viewOnStarkScanLocator).toBeVisible(),
+ ])
+
+ await expect(secondExtension.network.networkSelector).toBeVisible()
+ await secondExtension.network.selectDefaultNetwork()
+ await secondExtension.navigation.showSettingsLocator.click()
+ await secondExtension.settings
+ .account(extension.account.accountName1)
+ .click()
+ await Promise.all([
+ expect(secondExtension.settings.deployAccount).toBeHidden(),
+ expect(secondExtension.settings.viewOnStarkScanLocator).toBeVisible(),
+ ])
+ })
})
diff --git a/packages/e2e/extension/src/specs/addressBook.spec.ts b/packages/e2e/extension/src/specs/addressBook.spec.ts
index 6617ffa2c..bb8bbc44d 100644
--- a/packages/e2e/extension/src/specs/addressBook.spec.ts
+++ b/packages/e2e/extension/src/specs/addressBook.spec.ts
@@ -1,54 +1,57 @@
import { expect } from "@playwright/test"
-import config from "../config"
+import config from "../../../shared/config"
import test from "../test"
test.describe("Address Book", () => {
test("Add, update, use and delete address", async ({ extension }) => {
await extension.setupWallet({
- accountsToSetup: [{ initialBalance: 0.002 }],
+ accountsToSetup: [{ assets: [{ token: "ETH", balance: 0.002 }] }],
})
- await extension.navigation.showSettings.click()
+ await extension.navigation.showSettingsLocator.click()
await extension.settings.addressBook.click()
//create
await extension.addressBook.add.click()
- await extension.addressBook.save.click()
+ await extension.addressBook.saveLocator.click()
await expect(extension.addressBook.nameRequired).toBeVisible()
await expect(extension.addressBook.addressRequired).toBeVisible()
await extension.addressBook.name.fill("My first address")
- await extension.addressBook.save.click()
+ await extension.addressBook.saveLocator.click()
await expect(extension.addressBook.nameRequired).not.toBeVisible()
await expect(extension.addressBook.addressRequired).toBeVisible()
await extension.addressBook.address.fill(config.account1Seed2!)
await expect(extension.addressBook.nameRequired).not.toBeVisible()
await expect(extension.addressBook.addressRequired).not.toBeVisible()
await extension.addressBook.network.click()
- await extension.addressBook.networkOption("Testnet").click()
- await extension.addressBook.save.click()
+ await extension.addressBook.networkOption("Goerli").click()
+ await extension.addressBook.saveLocator.click()
// update
await extension.addressBook.addressByName("My first address").click()
await extension.addressBook.name.fill("New name")
- await extension.addressBook.save.click()
+ await extension.addressBook.saveLocator.click()
await expect(extension.addressBook.addressByName("New name")).toBeVisible()
- await extension.navigation.back.click()
- await extension.navigation.close.click()
+ await extension.navigation.backLocator.click()
+ await extension.navigation.closeLocator.click()
//transfer to address
- await extension.account.token("Ethereum").click()
+ await extension.account.token("ETH").click()
await extension.addressBook.addressBook.click()
await extension.addressBook.addressByName("New name").click()
await extension.account.sendMax.click()
- await extension.navigation.reviewSend.click()
- await extension.navigation.confirm.click()
+ await extension.navigation.reviewSendLocator.click()
+ await extension.account.confirmTransaction()
const txHash = await extension.activity.getLastTxHash()
- await extension.validateTx(txHash!, config.account1Seed2!)
+ await extension.validateTx({
+ txHash: txHash!,
+ receiver: config.account1Seed2!,
+ })
//delete address
- await extension.navigation.menuTokens.click()
- await extension.navigation.showSettings.click()
+ await extension.navigation.menuTokensLocator.click()
+ await extension.navigation.showSettingsLocator.click()
await extension.settings.addressBook.click()
await extension.addressBook.addressByName("New name").click()
await extension.addressBook.deleteAddress.click()
@@ -60,38 +63,41 @@ test.describe("Address Book", () => {
test("Add address after typing", async ({ extension }) => {
await extension.setupWallet({
- accountsToSetup: [{ initialBalance: 0.002 }],
+ accountsToSetup: [{ assets: [{ token: "ETH", balance: 0.002 }] }],
})
await expect(extension.network.networkSelector).toBeVisible()
- await extension.network.selectNetwork("Testnet")
+ await extension.network.selectDefaultNetwork()
- await extension.account.token("Ethereum").click()
+ await extension.account.token("ETH").click()
await extension.account.recipientAddressQuery.type(config.account1Seed2!)
await extension.addressBook.add.click()
await expect(extension.addressBook.address).toHaveText(
config.account1Seed2!,
)
await extension.addressBook.name.fill("My address")
- await extension.addressBook.save.click()
+ await extension.addressBook.saveLocator.click()
await extension.account.contact("My address").click()
await extension.account.sendMax.click()
- await extension.navigation.reviewSend.click()
- await extension.navigation.confirm.click()
+ await extension.navigation.reviewSendLocator.click()
+ await extension.account.confirmTransaction()
const txHash = await extension.activity.getLastTxHash()
- await extension.validateTx(txHash!, config.account1Seed2!)
+ await extension.validateTx({
+ txHash: txHash!,
+ receiver: config.account1Seed2!,
+ })
})
test("Add address from send window", async ({ extension }) => {
await extension.setupWallet({
- accountsToSetup: [{ initialBalance: 0.002 }],
+ accountsToSetup: [{ assets: [{ token: "ETH", balance: 0.002 }] }],
})
await expect(extension.network.networkSelector).toBeVisible()
- await extension.network.selectNetwork("Testnet")
+ await extension.network.selectDefaultNetwork()
- await extension.account.token("Ethereum").click()
+ await extension.account.token("ETH").click()
await extension.setClipBoardContent(config.account1Seed2!)
await extension.account.recipientAddressQuery.focus()
await extension.paste()
@@ -101,13 +107,42 @@ test.describe("Address Book", () => {
config.account1Seed2!,
)
await extension.addressBook.name.fill("My address")
- await extension.addressBook.save.click()
+ await extension.addressBook.saveLocator.click()
await extension.account.sendMax.click()
- await extension.navigation.reviewSend.click()
- await extension.navigation.confirm.click()
+ await extension.navigation.reviewSendLocator.click()
+ await extension.account.confirmTransaction()
const txHash = await extension.activity.getLastTxHash()
- await extension.validateTx(txHash!, config.account1Seed2!)
+ await extension.validateTx({
+ txHash: txHash!,
+ receiver: config.account1Seed2!,
+ })
+ })
+
+ test("Add address - starknet.id", async ({ extension }) => {
+ await extension.setupWallet({
+ accountsToSetup: [{ assets: [{ token: "ETH", balance: 0.002 }] }],
+ })
+
+ await expect(extension.network.networkSelector).toBeVisible()
+ await extension.network.selectDefaultNetwork()
+
+ await extension.account.token("ETH").click()
+ await extension.account.recipientAddressQuery.type("qateste2e.stark")
+ await extension.addressBook.add.click()
+ await expect(extension.addressBook.address).toHaveText("qateste2e.stark")
+ await extension.addressBook.name.fill("My address")
+ await extension.addressBook.saveLocator.click()
+ await extension.account.contact("My address").click()
+
+ await extension.account.sendMax.click()
+ await extension.navigation.reviewSendLocator.click()
+ await extension.account.confirmTransaction()
+ const txHash = await extension.activity.getLastTxHash()
+ await extension.validateTx({
+ txHash: txHash!,
+ receiver: config.senderAddrs![0],
+ })
})
})
diff --git a/packages/e2e/extension/src/specs/dapps.spec.ts b/packages/e2e/extension/src/specs/dapps.spec.ts
index 8766058a1..af6c651a5 100644
--- a/packages/e2e/extension/src/specs/dapps.spec.ts
+++ b/packages/e2e/extension/src/specs/dapps.spec.ts
@@ -1,29 +1,34 @@
import { expect } from "@playwright/test"
import test from "../test"
-
-const aspectUrl = "https://testnet.aspect.co"
-const testDappUrl = "https://dapp-argentlabs.vercel.app"
+import { lang } from "../languages"
test.describe("Dapps", () => {
- test("connect from aspect", async ({ extension, browserContext }) => {
+ test("connect from starknet.id", async ({ extension, browserContext }) => {
//setup wallet
await extension.wallet.newWalletOnboarding()
await extension.open()
- await extension.dapps.requestConnectionFromDapp(browserContext, aspectUrl)
+ await extension.dapps.requestConnectionFromDapp(
+ browserContext,
+ "https://goerli.app.starknet.id",
+ )
//accept connection from ArgentX
await extension.dapps.accept.click()
//check connect dapps
- await extension.navigation.showSettings.click()
+ await extension.navigation.showSettingsLocator.click()
await extension.settings.connectedDapps.click()
await expect(
extension.dapps.connectedDapps(extension.account.accountName1, 1),
).toBeVisible()
await extension.dapps.account(extension.account.accountName1).click()
- await expect(extension.dapps.connected(aspectUrl)).toBeVisible()
+ await expect(
+ extension.dapps.connected("https://goerli.app.starknet.id"),
+ ).toBeVisible()
//disconnect dapp from ArgentX
- await extension.dapps.disconnect(aspectUrl).click()
- await expect(extension.dapps.connected(testDappUrl)).toBeHidden()
+ await extension.dapps.disconnect("https://goerli.app.starknet.id").click()
+ await expect(
+ extension.dapps.connected("https://dapp-argentlabs.vercel.app"),
+ ).toBeHidden()
await expect(extension.dapps.noConnectedDapps.first()).toBeVisible()
})
@@ -31,20 +36,29 @@ test.describe("Dapps", () => {
//setup wallet
await extension.wallet.newWalletOnboarding()
await extension.open()
- await extension.dapps.requestConnectionFromDapp(browserContext, testDappUrl)
+ await extension.dapps.requestConnectionFromDapp(
+ browserContext,
+ "https://dapp-argentlabs.vercel.app",
+ )
//accept connection from ArgentX
await extension.dapps.accept.click()
//check connect dapps
- await extension.navigation.showSettings.click()
+ await extension.navigation.showSettingsLocator.click()
await extension.settings.connectedDapps.click()
await expect(
extension.dapps.connectedDapps(extension.account.accountName1, 1),
).toBeVisible()
await extension.dapps.account(extension.account.accountName1).click()
- await expect(extension.dapps.connected(testDappUrl)).toBeVisible()
+ await expect(
+ extension.dapps.connected("https://dapp-argentlabs.vercel.app"),
+ ).toBeVisible()
//disconnect dapp from ArgentX
- await extension.dapps.disconnect(testDappUrl).click()
- await expect(extension.dapps.connected(testDappUrl)).toBeHidden()
+ await extension.dapps
+ .disconnect("https://dapp-argentlabs.vercel.app")
+ .click()
+ await expect(
+ extension.dapps.connected("https://dapp-argentlabs.vercel.app"),
+ ).toBeHidden()
await expect(extension.dapps.noConnectedDapps.first()).toBeVisible()
})
@@ -52,13 +66,19 @@ test.describe("Dapps", () => {
//setup wallet
await extension.wallet.newWalletOnboarding()
await extension.open()
- await extension.dapps.requestConnectionFromDapp(browserContext, aspectUrl)
+ await extension.dapps.requestConnectionFromDapp(
+ browserContext,
+ "https://goerli.app.starknet.id",
+ )
//accept connection from ArgentX
await extension.dapps.accept.click()
- await extension.dapps.requestConnectionFromDapp(browserContext, testDappUrl)
+ await extension.dapps.requestConnectionFromDapp(
+ browserContext,
+ "https://dapp-argentlabs.vercel.app",
+ )
//accept connection from ArgentX
await extension.dapps.accept.click()
- await extension.navigation.showSettings.click()
+ await extension.navigation.showSettingsLocator.click()
await extension.settings.connectedDapps.click()
await expect(
extension.dapps.connectedDapps(extension.account.accountName1, 2),
@@ -66,14 +86,22 @@ test.describe("Dapps", () => {
await extension.dapps.account(extension.account.accountName1).click()
await Promise.all([
- expect(extension.dapps.connected(testDappUrl)).toBeVisible(),
- expect(extension.dapps.connected(aspectUrl)).toBeVisible(),
+ expect(
+ extension.dapps.connected("https://dapp-argentlabs.vercel.app"),
+ ).toBeVisible(),
+ expect(
+ extension.dapps.connected("https://goerli.app.starknet.id"),
+ ).toBeVisible(),
])
await extension.dapps.disconnectAll().click()
await Promise.all([
- expect(extension.dapps.connected(testDappUrl)).toBeHidden(),
- expect(extension.dapps.connected(aspectUrl)).toBeHidden(),
+ expect(
+ extension.dapps.connected("https://dapp-argentlabs.vercel.app"),
+ ).toBeHidden(),
+ expect(
+ extension.dapps.connected("https://goerli.app.starknet.id"),
+ ).toBeHidden(),
])
await expect(extension.dapps.noConnectedDapps.first()).toBeVisible()
})
@@ -85,13 +113,19 @@ test.describe("Dapps", () => {
//setup wallet
await extension.wallet.newWalletOnboarding()
await extension.open()
- await extension.dapps.requestConnectionFromDapp(browserContext, aspectUrl)
+ await extension.dapps.requestConnectionFromDapp(
+ browserContext,
+ "https://goerli.app.starknet.id",
+ )
//accept connection from ArgentX
await extension.dapps.accept.click()
- await extension.dapps.requestConnectionFromDapp(browserContext, testDappUrl)
+ await extension.dapps.requestConnectionFromDapp(
+ browserContext,
+ "https://dapp-argentlabs.vercel.app",
+ )
//accept connection from ArgentX
await extension.dapps.accept.click()
- await extension.navigation.showSettings.click()
+ await extension.navigation.showSettingsLocator.click()
await extension.settings.connectedDapps.click()
await expect(
extension.dapps.connectedDapps(extension.account.accountName1, 2),
@@ -99,16 +133,26 @@ test.describe("Dapps", () => {
await extension.dapps.account(extension.account.accountName1).click()
await Promise.all([
- expect(extension.dapps.connected(testDappUrl)).toBeVisible(),
- expect(extension.dapps.connected(aspectUrl)).toBeVisible(),
+ expect(
+ extension.dapps.connected("https://dapp-argentlabs.vercel.app"),
+ ).toBeVisible(),
+ expect(
+ extension.dapps.connected("https://goerli.app.starknet.id"),
+ ).toBeVisible(),
])
- await extension.dapps.disconnect(testDappUrl).click()
+ await extension.dapps
+ .disconnect("https://dapp-argentlabs.vercel.app")
+ .click()
await Promise.all([
- expect(extension.dapps.connected(testDappUrl)).toBeHidden(),
- expect(extension.dapps.connected(aspectUrl)).toBeVisible(),
+ expect(
+ extension.dapps.connected("https://dapp-argentlabs.vercel.app"),
+ ).toBeHidden(),
+ expect(
+ extension.dapps.connected("https://goerli.app.starknet.id"),
+ ).toBeVisible(),
])
- await extension.navigation.back.click()
+ await extension.navigation.backLocator.click()
await expect(
extension.dapps.connectedDapps(extension.account.accountName1, 1),
).toBeVisible()
@@ -117,20 +161,29 @@ test.describe("Dapps", () => {
test("connect dapps by account", async ({ extension, browserContext }) => {
//setup wallet
await extension.setupWallet({
- accountsToSetup: [{ initialBalance: 0 }, { initialBalance: 0 }],
+ accountsToSetup: [
+ { assets: [{ token: "ETH", balance: 0 }] },
+ { assets: [{ token: "ETH", balance: 0 }] },
+ ],
})
await extension.open()
await extension.account.selectAccount(extension.account.accountName1)
- await extension.dapps.requestConnectionFromDapp(browserContext, aspectUrl)
+ await extension.dapps.requestConnectionFromDapp(
+ browserContext,
+ "https://goerli.app.starknet.id",
+ )
//accept connection from ArgentX
await extension.dapps.accept.click()
await extension.account.selectAccount(extension.account.accountName2)
- await extension.dapps.requestConnectionFromDapp(browserContext, testDappUrl)
+ await extension.dapps.requestConnectionFromDapp(
+ browserContext,
+ "https://dapp-argentlabs.vercel.app",
+ )
//accept connection from ArgentX
await extension.dapps.accept.click()
- await extension.navigation.showSettings.click()
+ await extension.navigation.showSettingsLocator.click()
await extension.settings.connectedDapps.click()
await Promise.all([
expect(
@@ -143,15 +196,99 @@ test.describe("Dapps", () => {
await extension.dapps.account(extension.account.accountName1).click()
await Promise.all([
- expect(extension.dapps.connected(testDappUrl)).toBeHidden(),
- expect(extension.dapps.connected(aspectUrl)).toBeVisible(),
+ expect(
+ extension.dapps.connected("https://dapp-argentlabs.vercel.app"),
+ ).toBeHidden(),
+ expect(
+ extension.dapps.connected("https://goerli.app.starknet.id"),
+ ).toBeVisible(),
])
- await extension.navigation.back.click()
+ await extension.navigation.backLocator.click()
await extension.dapps.account(extension.account.accountName2).click()
await Promise.all([
- expect(extension.dapps.connected(testDappUrl)).toBeVisible(),
- expect(extension.dapps.connected(aspectUrl)).toBeHidden(),
+ expect(
+ extension.dapps.connected("https://dapp-argentlabs.vercel.app"),
+ ).toBeVisible(),
+ expect(
+ extension.dapps.connected("https://goerli.app.starknet.id"),
+ ).toBeHidden(),
])
})
+
+ test("try sign a message using testDapp with a non deployed account", async ({
+ extension,
+ browserContext,
+ }) => {
+ //setup wallet
+ await extension.wallet.newWalletOnboarding()
+ await extension.open()
+ const dapp = await extension.dapps.requestConnectionFromDapp(
+ browserContext,
+ "https://dapp-argentlabs.vercel.app",
+ )
+ //accept connection from ArgentX
+ await extension.dapps.accept.click()
+ await dapp.locator('[id="short-text"]').fill("Test message!")
+ await dapp.locator('button:text-is("Sign")').click()
+ await expect(
+ extension.page.locator(
+ `button:text-is("${lang.account.activateAccount}")`,
+ ),
+ ).toBeVisible()
+ await extension.page
+ .locator(`button:text-is("${lang.account.activateAccount}")`)
+ .click()
+ await expect(
+ extension.page.locator(`text="${lang.account.notEnoughFoundsFee}"`),
+ ).toBeVisible()
+ })
+
+ test("sign message using testDapp with a deployed account", async ({
+ extension,
+ browserContext,
+ }) => {
+ //setup wallet
+ await extension.setupWallet({
+ accountsToSetup: [
+ { assets: [{ token: "ETH", balance: 0.001 }], deploy: true },
+ ],
+ })
+ await extension.open()
+ const dapp = await extension.dapps.requestConnectionFromDapp(
+ browserContext,
+ "https://dapp-argentlabs.vercel.app",
+ )
+ //accept connection from ArgentX
+ await extension.dapps.accept.click()
+ await dapp.locator('[id="short-text"]').fill("Test message!")
+ await dapp.locator('button:text-is("Sign")').click()
+ await expect(
+ extension.page.locator(
+ '//p[text()="Message"]//following::p[text()="Test message!"]',
+ ),
+ ).toBeVisible()
+ await extension.page.locator('[id="Sign"]').click()
+ })
+
+ test("connect to dapp flagged as critical", async ({
+ extension,
+ browserContext,
+ }) => {
+ //setup wallet
+ await extension.setupWallet({
+ accountsToSetup: [{ assets: [{ token: "ETH", balance: 0 }] }],
+ })
+ await extension.open()
+
+ await extension.dapps.requestConnectionFromDapp(
+ browserContext,
+ "https://starknetkit-blacked-listed.vercel.app",
+ )
+ await extension.dapps.checkCriticalRiskConnectionScreen()
+ await extension.dapps.acceptCriticalRiskConnection()
+ await extension.dapps.accept.click()
+ //https://argent.atlassian.net/browse/BLO-1939
+ // await extension.dapps.connectedDappsTooltip("https://starknetkit-blacked-listed.vercel.app")
+ })
})
diff --git a/packages/e2e/extension/src/specs/dappsBanner.spec.ts b/packages/e2e/extension/src/specs/dappsBanner.spec.ts
deleted file mode 100644
index 83842f3e2..000000000
--- a/packages/e2e/extension/src/specs/dappsBanner.spec.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { expect } from "@playwright/test"
-
-import test from "../test"
-import config from "../config"
-
-test.describe("Banner", () => {
- test("avnu banner should be visible after login", async ({ extension }) => {
- await extension.setupWallet({
- accountsToSetup: [{ initialBalance: 0.0001 }],
- })
-
- await expect(extension.network.networkSelector).toBeVisible()
- await expect(extension.account.avnuBanner).toBeVisible()
- })
-
- test("avnu banner should not be visible after dismissed", async ({
- extension,
- }) => {
- await extension.setupWallet({
- accountsToSetup: [{ initialBalance: 0.0001 }],
- })
-
- await expect(extension.network.networkSelector).toBeVisible()
- await expect(extension.account.avnuBanner).toBeVisible()
- await extension.account.avnuBannerClose.click()
- await expect(extension.account.avnuBanner).toBeHidden()
- })
-
- test("avnu banner shoud be visible after account recovery", async ({
- extension,
- }) => {
- await extension.open()
- await extension.recoverWallet(config.testNetSeed1!)
- await expect(extension.account.avnuBanner).toBeVisible()
- })
-
- test("ekubo banner should be visible after avnu banner has been dismissed", async ({
- extension,
- }) => {
- await extension.setupWallet({
- accountsToSetup: [{ initialBalance: 0.0001 }],
- })
-
- await expect(extension.network.networkSelector).toBeVisible()
- await expect(extension.account.avnuBanner).toBeVisible()
- await extension.account.avnuBannerClose.click()
- await expect(extension.account.avnuBanner).toBeHidden()
- await expect(extension.account.ekuboBanner).toBeVisible()
- })
-})
diff --git a/packages/e2e/extension/src/specs/invalidAddress.spec.ts b/packages/e2e/extension/src/specs/invalidAddress.spec.ts
index 987616a56..f3512f8af 100644
--- a/packages/e2e/extension/src/specs/invalidAddress.spec.ts
+++ b/packages/e2e/extension/src/specs/invalidAddress.spec.ts
@@ -4,12 +4,12 @@ import test from "../test"
test.describe("Invalid address", () => {
test("Invalid starknet id", async ({ extension }) => {
await extension.setupWallet({
- accountsToSetup: [{ initialBalance: 0.0001 }],
+ accountsToSetup: [{ assets: [{ token: "ETH", balance: 0.0001 }] }],
})
await extension.account.ensureSelectedAccount(
extension.account.accountName1,
)
- await extension.account.token("Ethereum").click()
+ await extension.account.token("ETH").click()
await extension.account.fillRecipientAddress({
recipientAddress: "e2e-test5345346eertgegeggfgdgdgdfgdgdf.stark",
validAddress: false,
@@ -23,12 +23,12 @@ test.describe("Invalid address", () => {
test("Invalid address (short address)", async ({ extension }) => {
await extension.setupWallet({
- accountsToSetup: [{ initialBalance: 0.0001 }],
+ accountsToSetup: [{ assets: [{ token: "ETH", balance: 0.0001 }] }],
})
await extension.account.ensureSelectedAccount(
extension.account.accountName1,
)
- await extension.account.token("Ethereum").click()
+ await extension.account.token("ETH").click()
await extension.account.fillRecipientAddress({
recipientAddress:
"0x0451fCcB2617Db213E0e661D525F16a52eCCF9E2b8D735f13E4F7de49A4Dc3a",
@@ -40,12 +40,12 @@ test.describe("Invalid address", () => {
test("Invalid address (checksum error)", async ({ extension }) => {
await extension.setupWallet({
- accountsToSetup: [{ initialBalance: 0.0001 }],
+ accountsToSetup: [{ assets: [{ token: "ETH", balance: 0.0001 }] }],
})
await extension.account.ensureSelectedAccount(
extension.account.accountName1,
)
- await extension.account.token("Ethereum").click()
+ await extension.account.token("ETH").click()
await extension.account.fillRecipientAddress({
recipientAddress:
"0x0451fCcB2617Db213E0e661D525F16a52eCCF9E2b8D735f13E4F7de49A4Dc3a3",
@@ -57,12 +57,12 @@ test.describe("Invalid address", () => {
test("Invalid address", async ({ extension }) => {
await extension.setupWallet({
- accountsToSetup: [{ initialBalance: 0.0001 }],
+ accountsToSetup: [{ assets: [{ token: "ETH", balance: 0.0001 }] }],
})
await extension.account.ensureSelectedAccount(
extension.account.accountName1,
)
- await extension.account.token("Ethereum").click()
+ await extension.account.token("ETH").click()
await extension.account.fillRecipientAddress({
recipientAddress:
"0x0451fCcB2617Db213E0e661D525F16a52eCCF9E2b8D735f13E4F7de49A4Dc3aq",
diff --git a/packages/e2e/extension/src/specs/links.spec.ts b/packages/e2e/extension/src/specs/links.spec.ts
index db7e72f4b..571062712 100644
--- a/packages/e2e/extension/src/specs/links.spec.ts
+++ b/packages/e2e/extension/src/specs/links.spec.ts
@@ -1,13 +1,12 @@
import { expect } from "@playwright/test"
-import { lang } from "../languages"
import test from "../test"
test.describe("Links", () => {
test("Check settings links", async ({ extension }) => {
await extension.wallet.newWalletOnboarding()
await extension.open()
- await extension.navigation.showSettings.click()
+ await extension.navigation.showSettingsLocator.click()
let href = await extension.settings.discord.getAttribute("href")
expect(href).toContain("https://discord.gg/T4PDFHxm6T")
href = await extension.settings.help.getAttribute("href")
@@ -16,9 +15,5 @@ test.describe("Links", () => {
)
href = await extension.settings.github.getAttribute("href")
expect(href).toContain("https://github.com/argentlabs/argent-x/issues")
- await extension.settings.privacyStatement.click()
- await expect(extension.settings.privacyStatementText).toHaveText(
- lang.common.privacyStatement,
- )
})
})
diff --git a/packages/e2e/extension/src/specs/multisig.spec.ts b/packages/e2e/extension/src/specs/multisig.spec.ts
index 193f64b82..b5c266106 100644
--- a/packages/e2e/extension/src/specs/multisig.spec.ts
+++ b/packages/e2e/extension/src/specs/multisig.spec.ts
@@ -1,44 +1,50 @@
import { expect } from "@playwright/test"
import test from "../test"
-import config from "../config"
-import { sleep } from "../utils/common"
+import config from "../../../shared/config"
+import { sleep } from "../../../shared/src/common"
+import { lang } from "../languages"
test.describe("Multisig", () => {
- test("add and activate 1/1 multisig ", async ({ extension }) => {
+ test.slow()
+ test("add and activate 1/1 multisig", async ({ extension }) => {
await extension.setupWallet({
accountsToSetup: [],
})
await extension.account.addMultisigAccount({})
- await extension.navigation.close.click()
+ await extension.navigation.closeLocator.click()
+ await expect(
+ extension.page.locator('[data-testid="activate-multisig"]'),
+ ).toBeHidden()
await extension.fundMultisigAccount({
accountName: extension.account.accountNameMulti1,
amount: 0.002,
})
+ await expect(
+ extension.page.locator('[data-testid="activate-multisig"]'),
+ ).toBeVisible()
await extension.activateMultisig(extension.account.accountNameMulti1)
- const amountTrx = await extension.account.transfer({
+ const { sendAmountTX, sendAmountFE } = await extension.account.transfer({
originAccountName: extension.account.accountNameMulti1,
recipientAddress: config.destinationAddress!,
- tokenName: "Ethereum",
- amount: "MAX",
+ token: "ETH",
+ amount: 0.001,
})
const txHash = await extension.activity.getLastTxHash()
- await extension.validateTx(
- txHash!,
- config.destinationAddress!,
- amountTrx,
- true,
- )
- await extension.navigation.menuTokens.click()
+ await extension.validateTx({
+ txHash: txHash!,
+ receiver: config.destinationAddress!,
+ sendAmountFE,
+ sendAmountTX,
+ uniqLocator: true,
+ })
+ await extension.navigation.menuTokensLocator.click()
//ensure that balance is updated
- await expect(extension.account.currentBalance("Ethereum")).toContainText(
- "0.000",
- { timeout: 60000 },
- )
+ await expect(extension.account.currentBalance("ETH")).toContainText("0.000")
})
- test("add and activate 1/2 multisig ", async ({
+ test("add and activate 1/2 multisig", async ({
extension,
secondExtension,
}) => {
@@ -50,7 +56,7 @@ test.describe("Multisig", () => {
})
const pubKey = await secondExtension.account.joinMultisig()
await extension.account.addMultisigAccount({ signers: [pubKey] })
- await extension.navigation.close.click()
+ await extension.navigation.closeLocator.click()
await extension.fundMultisigAccount({
accountName: extension.account.accountNameMulti1,
amount: 0.002,
@@ -62,7 +68,7 @@ test.describe("Multisig", () => {
secondExtension.account.accountNameMulti1,
),
).toHaveText("1/2")
- await secondExtension.navigation.close.click()
+ await secondExtension.navigation.closeLocator.click()
await secondExtension.account.selectAccount(
secondExtension.account.accountNameMulti1,
)
@@ -75,41 +81,39 @@ test.describe("Multisig", () => {
),
])
- const amountTrx = await extension.account.transfer({
+ const { sendAmountTX, sendAmountFE } = await extension.account.transfer({
originAccountName: extension.account.accountNameMulti1,
recipientAddress: config.destinationAddress!,
- tokenName: "Ethereum",
- amount: "MAX",
+ token: "ETH",
+ amount: 0.001,
})
const txHash = await extension.activity.getLastTxHash()
- await extension.validateTx(
- txHash!,
- config.destinationAddress!,
- amountTrx,
- true,
- )
- await extension.navigation.menuTokens.click()
+ await extension.validateTx({
+ txHash: txHash!,
+ receiver: config.destinationAddress!,
+ sendAmountFE,
+ sendAmountTX,
+ uniqLocator: true,
+ })
+ await extension.navigation.menuTokensLocator.click()
//ensure that balance is updated
await Promise.all([
- expect(extension.account.currentBalance("Ethereum")).toContainText(
- "0.000",
- { timeout: 120000 },
- ),
- expect(secondExtension.account.currentBalance("Ethereum")).toContainText(
+ expect(extension.account.currentBalance("ETH")).toContainText("0.000"),
+ expect(secondExtension.account.currentBalance("ETH")).toContainText(
"0.000",
- { timeout: 120000 },
),
])
- await secondExtension.validateTx(
- txHash!,
- config.destinationAddress!,
- amountTrx,
- true,
- )
+ await secondExtension.validateTx({
+ txHash: txHash!,
+ receiver: config.destinationAddress!,
+ sendAmountFE,
+ sendAmountTX,
+ uniqLocator: true,
+ })
})
- test("add and activate 2/2 multisig ", async ({
+ test("add and activate 2/2 multisig", async ({
extension,
secondExtension,
}) => {
@@ -124,7 +128,7 @@ test.describe("Multisig", () => {
signers: [pubKey],
confirmations: 2,
})
- await extension.navigation.close.click()
+ await extension.navigation.closeLocator.click()
await extension.fundMultisigAccount({
accountName: extension.account.accountNameMulti1,
amount: 0.002,
@@ -136,7 +140,7 @@ test.describe("Multisig", () => {
secondExtension.account.accountNameMulti1,
),
).toHaveText("2/2")
- await secondExtension.navigation.close.click()
+ await secondExtension.navigation.closeLocator.click()
await secondExtension.account.selectAccount(
secondExtension.account.accountNameMulti1,
)
@@ -149,41 +153,38 @@ test.describe("Multisig", () => {
),
])
- const amountTrx = await extension.account.transfer({
+ const { sendAmountTX, sendAmountFE } = await extension.account.transfer({
originAccountName: extension.account.accountNameMulti1,
recipientAddress: config.destinationAddress!,
- tokenName: "Ethereum",
- amount: "MAX",
+ token: "ETH",
+ amount: 0.001,
})
- await extension.navigation.menuActivity.click()
+ await extension.navigation.menuActivityLocator.click()
const txHash = await extension.activity.getLastTxHash()
- await extension.navigation.menuTokens.click()
+ await extension.navigation.menuTokensLocator.click()
- //acept tx from second extension
+ //accept tx from second extension
await expect(
- secondExtension.activity.menuPendingTransactionsIndicator,
- ).toBeVisible({ timeout: 120000 })
+ secondExtension.activity.menuPendingTransactionsIndicatorLocator,
+ ).toBeVisible()
await secondExtension.account.acceptTx(txHash!)
- await extension.validateTx(
- txHash!,
- config.destinationAddress!,
- amountTrx,
- true,
- )
- await extension.navigation.menuTokens.click()
- await secondExtension.navigation.menuTokens.click()
+ await extension.validateTx({
+ txHash: txHash!,
+ receiver: config.destinationAddress!,
+ sendAmountFE,
+ sendAmountTX,
+ uniqLocator: true,
+ })
+ await extension.navigation.menuTokensLocator.click()
+ await secondExtension.navigation.menuTokensLocator.click()
//ensure that balance is updated
await Promise.all([
- expect(extension.account.currentBalance("Ethereum")).toContainText(
+ expect(extension.account.currentBalance("ETH")).toContainText("0.000"),
+ expect(secondExtension.account.currentBalance("ETH")).toContainText(
"0.000",
- { timeout: 120000 },
- ),
- expect(secondExtension.account.currentBalance("Ethereum")).toContainText(
- "0.000",
- { timeout: 120000 },
),
])
})
@@ -203,7 +204,7 @@ test.describe("Multisig", () => {
signers: [pubKey],
confirmations: 2,
})
- await extension.navigation.close.click()
+ await extension.navigation.closeLocator.click()
await extension.fundMultisigAccount({
accountName: extension.account.accountNameMulti1,
amount: 0.002,
@@ -215,7 +216,7 @@ test.describe("Multisig", () => {
secondExtension.account.accountNameMulti1,
),
).toHaveText("2/2")
- await secondExtension.navigation.close.click()
+ await secondExtension.navigation.closeLocator.click()
await secondExtension.account.selectAccount(
secondExtension.account.accountNameMulti1,
)
@@ -236,15 +237,15 @@ test.describe("Multisig", () => {
let txHash = await extension.activity.getLastTxHash()
await secondExtension.account.acceptTx(txHash!)
- await secondExtension.navigation.menuTokens.click()
- await extension.navigation.menuTokens.click()
+ await secondExtension.navigation.menuTokensLocator.click()
+ await extension.navigation.menuTokensLocator.click()
await Promise.all([
- expect(extension.account.menuPendingTransactionsIndicator).toBeHidden({
- timeout: 120000,
- }),
expect(
- secondExtension.account.menuPendingTransactionsIndicator,
- ).toBeHidden({ timeout: 120000 }),
+ extension.account.menuPendingTransactionsIndicatorLocator,
+ ).toBeHidden(),
+ expect(
+ secondExtension.account.menuPendingTransactionsIndicatorLocator,
+ ).toBeHidden(),
])
//wait for events to be updated
@@ -258,48 +259,315 @@ test.describe("Multisig", () => {
),
])
//transfer
- const amountTrx = await extension.account.transfer({
+ const { sendAmountTX, sendAmountFE } = await extension.account.transfer({
originAccountName: extension.account.accountNameMulti1,
recipientAddress: config.destinationAddress!,
- tokenName: "Ethereum",
- amount: "MAX",
+ token: "ETH",
+ amount: 0.001,
})
txHash = await extension.activity.getLastTxHash()
//wait for events to be updated
await sleep(20 * 1000)
await Promise.all([
- expect(extension.account.menuPendingTransactionsIndicator).toBeHidden({
+ expect(
+ extension.account.menuPendingTransactionsIndicatorLocator,
+ ).toBeHidden(),
+ expect(
+ secondExtension.account.menuPendingTransactionsIndicatorLocator,
+ ).toBeHidden(),
+ ])
+ await Promise.all([
+ extension.validateTx({
+ txHash: txHash!,
+ receiver: config.destinationAddress!,
+ sendAmountFE,
+ sendAmountTX,
+ uniqLocator: true,
+ }),
+ secondExtension.validateTx({
+ txHash: txHash!,
+ receiver: config.destinationAddress!,
+ sendAmountFE,
+ sendAmountTX,
+ uniqLocator: true,
+ }),
+ ])
+ await Promise.all([
+ extension.navigation.menuTokensLocator.click(),
+ secondExtension.navigation.menuTokensLocator.click(),
+ ])
+
+ //ensure that balance is updated
+ await Promise.all([
+ expect(extension.account.currentBalance("ETH")).toContainText("0.000"),
+ expect(secondExtension.account.currentBalance("ETH")).toContainText(
+ "0.000",
+ ),
+ ])
+ })
+
+ test("User is notified after get removed from a multisig account", async ({
+ extension,
+ secondExtension,
+ }) => {
+ await extension.setupWallet({
+ accountsToSetup: [],
+ })
+ await secondExtension.setupWallet({
+ accountsToSetup: [],
+ })
+ const pubKey = await secondExtension.account.joinMultisig()
+ await extension.account.addMultisigAccount({
+ signers: [pubKey],
+ confirmations: 1,
+ })
+ await extension.navigation.closeLocator.click()
+ await extension.fundMultisigAccount({
+ accountName: extension.account.accountNameMulti1,
+ amount: 0.002,
+ })
+ await extension.activateMultisig(extension.account.accountNameMulti1)
+
+ await expect(
+ secondExtension.account.accountListConfirmations(
+ secondExtension.account.accountNameMulti1,
+ ),
+ ).toHaveText("1/2")
+ await secondExtension.navigation.closeLocator.click()
+ await secondExtension.account.selectAccount(
+ secondExtension.account.accountNameMulti1,
+ )
+ await Promise.all([
+ expect(extension.account.accountViewConfirmations).toHaveText(
+ "1/2 multisig",
+ ),
+ expect(secondExtension.account.accountViewConfirmations).toHaveText(
+ "1/2 multisig",
+ ),
+ ])
+
+ await extension.account.removeMultiSigOwner(
+ extension.account.accountNameMulti1,
+ pubKey,
+ )
+ await expect(
+ extension.account.menuPendingTransactionsIndicatorLocator,
+ ).toBeHidden()
+ await expect(
+ secondExtension.account.removedFromMultisigLocator,
+ ).toBeVisible()
+ })
+
+ test("Removed user should not see Multisig account after recover wallet", async ({
+ extension,
+ }) => {
+ await extension.open()
+ await extension.recoverWallet(config.testNetSeed3!)
+ await expect(extension.network.networkSelector).toBeVisible()
+ await extension.network.selectDefaultNetwork()
+
+ await extension.account.accountListSelector.click()
+ await expect(
+ extension.account.account(extension.account.accountName1),
+ ).toBeVisible()
+ await expect(
+ extension.account.account(extension.account.accountNameMulti1),
+ ).toBeHidden()
+ })
+ //https://argent.atlassian.net/browse/BLO-1935
+ test.skip("User should be able to hide/unhide Multisig account", async ({
+ extension,
+ }) => {
+ await extension.open()
+ await extension.recoverWallet(config.testNetSeed2!)
+ await expect(extension.network.networkSelector).toBeVisible()
+ await extension.network.selectDefaultNetwork()
+
+ await extension.account.selectAccount(extension.account.accountNameMulti1)
+ await extension.navigation.showSettingsLocator.click()
+ await extension.settings
+ .account(extension.account.accountNameMulti1)
+ .click()
+ await extension.settings.hideAccount.click()
+ await extension.settings.confirmHide.click()
+
+ await expect(
+ extension.account.account(extension.account.accountNameMulti1),
+ ).toBeHidden()
+
+ await extension.settings.hiddenAccounts.click()
+ await extension.settings
+ .unhideAccount(extension.account.accountNameMulti1)
+ .click()
+ await extension.navigation.backLocator.click()
+ await expect(extension.settings.hiddenAccounts).toBeHidden()
+ await expect(
+ extension.account.account(extension.account.accountNameMulti1),
+ ).toBeVisible()
+ })
+
+ test("Add token, token should only be visible if preference is set", async ({
+ extension,
+ }) => {
+ await extension.setupWallet({
+ accountsToSetup: [],
+ })
+ await extension.account.addMultisigAccount({})
+ await extension.navigation.closeLocator.click()
+ await expect(
+ extension.page.locator('[data-testid="activate-multisig"]'),
+ ).toBeHidden()
+ await extension.fundMultisigAccount({
+ accountName: extension.account.accountNameMulti1,
+ amount: 0.001,
+ })
+ await expect(
+ extension.page.locator('[data-testid="activate-multisig"]'),
+ ).toBeVisible()
+ await extension.activateMultisig(extension.account.accountNameMulti1)
+ await extension.page
+ .getByRole("link", { name: lang.account.newToken })
+ .click()
+ await extension.page
+ .locator('[name="address"]')
+ .fill(
+ "0x05A6B68181bb48501a7A447a3f99936827E41D77114728960f22892F02E24928",
+ )
+ await expect(extension.page.locator('[name="name"]')).toHaveValue("Astraly")
+ await Promise.all([
+ extension.navigation.continueLocator.click(),
+ extension.page.locator('text="Token added"').click(),
+ ])
+
+ await expect(extension.account.token("AST")).toBeHidden()
+ await extension.navigation.showSettingsLocator.click()
+ await extension.settings.preferences.click()
+ await expect(extension.preferences.hideTokensStatus).toBeEnabled()
+ await extension.preferences.hideTokens.click()
+ await extension.navigation.backLocator.click()
+ await extension.navigation.closeLocator.click()
+ await expect(extension.account.token("AST")).toBeVisible()
+ })
+
+ test("add owner to 1/1 activated multisig", async ({
+ extension,
+ secondExtension,
+ }) => {
+ await Promise.all([
+ extension.setupWallet({
+ accountsToSetup: [],
+ }),
+ ])
+ await Promise.all([
+ extension.account.addMultisigAccount({}),
+ secondExtension.setupWallet({
+ accountsToSetup: [],
+ }),
+ ])
+
+ await extension.navigation.closeLocator.click()
+ await expect(
+ extension.page.locator('[data-testid="activate-multisig"]'),
+ ).toBeHidden()
+ await extension.fundMultisigAccount({
+ accountName: extension.account.accountNameMulti1,
+ amount: 0.002,
+ })
+ await expect(
+ extension.page.locator('[data-testid="activate-multisig"]'),
+ ).toBeVisible()
+ await extension.activateMultisig(extension.account.accountNameMulti1)
+
+ const pubKey = await secondExtension.account.joinMultisig()
+
+ await extension.account.addOwnerToMultisig({
+ accountName: extension.account.accountNameMulti1,
+ pubKey,
+ confirmations: 2,
+ })
+ extension.navigation.menuTokensLocator.click(),
+ await expect(
+ extension.account.menuPendingTransactionsIndicatorLocator,
+ ).toBeHidden({
+ timeout: 120000,
+ })
+
+ await expect(
+ secondExtension.account.accountListConfirmations(
+ secondExtension.account.accountNameMulti1,
+ ),
+ ).toHaveText("2/2")
+ await secondExtension.navigation.closeLocator.click()
+ await secondExtension.account.selectAccount(
+ secondExtension.account.accountNameMulti1,
+ )
+ await Promise.all([
+ expect(extension.account.accountViewConfirmations).toHaveText(
+ "2/2 multisig",
+ ),
+ expect(secondExtension.account.accountViewConfirmations).toHaveText(
+ "2/2 multisig",
+ ),
+ ])
+
+ const { sendAmountTX, sendAmountFE } = await extension.account.transfer({
+ originAccountName: extension.account.accountNameMulti1,
+ recipientAddress: config.destinationAddress!,
+ token: "ETH",
+ amount: 0.001,
+ })
+ const txHash = await extension.activity.getLastTxHash()
+ await extension.navigation.menuTokensLocator.click()
+
+ //accept tx from second extension
+ await expect(
+ secondExtension.activity.menuPendingTransactionsIndicatorLocator,
+ ).toBeVisible({ timeout: 120000 })
+
+ await secondExtension.account.acceptTx(txHash!)
+ await secondExtension.navigation.menuTokensLocator.click()
+
+ await sleep(20 * 1000)
+ await Promise.all([
+ expect(
+ extension.account.menuPendingTransactionsIndicatorLocator,
+ ).toBeHidden({
timeout: 120000,
}),
expect(
- secondExtension.account.menuPendingTransactionsIndicator,
+ secondExtension.account.menuPendingTransactionsIndicatorLocator,
).toBeHidden({ timeout: 120000 }),
])
-
- await extension.validateTx(
- txHash!,
- config.destinationAddress!,
- amountTrx,
- true,
- )
- await extension.navigation.menuTokens.click()
+ await Promise.all([
+ extension.validateTx({
+ txHash: txHash!,
+ receiver: config.destinationAddress!,
+ sendAmountFE,
+ sendAmountTX,
+ uniqLocator: true,
+ }),
+ secondExtension.validateTx({
+ txHash: txHash!,
+ receiver: config.destinationAddress!,
+ sendAmountFE,
+ sendAmountTX,
+ uniqLocator: true,
+ }),
+ ])
+ await Promise.all([
+ extension.navigation.menuTokensLocator.click(),
+ secondExtension.navigation.menuTokensLocator.click(),
+ ])
//ensure that balance is updated
await Promise.all([
- expect(extension.account.currentBalance("Ethereum")).toContainText(
- "0.000",
- { timeout: 120000 },
- ),
- expect(secondExtension.account.currentBalance("Ethereum")).toContainText(
+ expect(extension.account.currentBalance("ETH")).toContainText("0.000", {
+ timeout: 120000,
+ }),
+ expect(secondExtension.account.currentBalance("ETH")).toContainText(
"0.000",
{ timeout: 120000 },
),
])
- await secondExtension.validateTx(
- txHash!,
- config.destinationAddress!,
- amountTrx,
- true,
- )
})
})
diff --git a/packages/e2e/extension/src/specs/network.spec.ts b/packages/e2e/extension/src/specs/network.spec.ts
index 281e70bd4..f3a2e3bb4 100644
--- a/packages/e2e/extension/src/specs/network.spec.ts
+++ b/packages/e2e/extension/src/specs/network.spec.ts
@@ -1,7 +1,7 @@
import { expect } from "@playwright/test"
import test from "../test"
-import config from "../config"
+import config from "../../../shared/config"
test.describe("Network", () => {
test("Available networks", async ({ extension }) => {
@@ -10,8 +10,8 @@ test.describe("Network", () => {
await expect(extension.network.networkSelector).toBeVisible()
await extension.network.ensureAvailableNetworks([
"Mainnet",
- `Testnet`,
- "Localhost 5050\nhttp://localhost:5050/rpc",
+ "Goerli",
+ "Devnet\nhttp://localhost:5050",
])
})
@@ -20,7 +20,7 @@ test.describe("Network", () => {
}) => {
await extension.wallet.newWalletOnboarding()
await extension.open()
- await extension.navigation.showSettings.click()
+ await extension.navigation.showSettingsLocator.click()
await extension.settings.developerSettings.click()
await extension.developerSettings.manageNetworks.click()
@@ -29,37 +29,37 @@ test.describe("Network", () => {
await extension.developerSettings.networkName.fill("My Network")
await extension.developerSettings.chainId.fill("SN_GOERLI")
await extension.developerSettings.rpcUrl.fill(config.testnetRpcUrl!)
- await extension.navigation.create.click()
+ await extension.navigation.createLocator.click()
await expect(
extension.developerSettings.networkByName("My Network"),
).toBeVisible()
- await extension.navigation.back.click()
- await extension.navigation.back.click()
- await extension.navigation.close.click()
- await extension.network.ensureSelectedNetwork("Testnet")
+ await extension.navigation.backLocator.click()
+ await extension.navigation.backLocator.click()
+ await extension.navigation.closeLocator.click()
+ await extension.network.ensureSelectedNetwork("Goerli")
// select network
await extension.network.selectNetwork("My Network")
// try to delete network
- await extension.navigation.showSettings.click()
+ await extension.navigation.showSettingsLocator.click()
await extension.settings.developerSettings.click()
await extension.developerSettings.manageNetworks.click()
await extension.developerSettings.deleteNetworkByName("My Network").click()
- await extension.navigation.cancel.click()
+ await extension.navigation.cancelLocator.click()
await expect(
extension.developerSettings.networkByName("My Network"),
).toBeVisible()
- await extension.navigation.back.click()
- await extension.navigation.back.click()
- await extension.navigation.close.click()
+ await extension.navigation.backLocator.click()
+ await extension.navigation.backLocator.click()
+ await extension.navigation.closeLocator.click()
await extension.network.ensureSelectedNetwork("My Network")
await expect(extension.account.createAccount).toBeVisible()
await expect(extension.account.noAccountBanner).toBeVisible()
// select other network
- await extension.network.selectNetwork("Testnet")
+ await extension.network.selectDefaultNetwork()
// delete network
- await extension.navigation.showSettings.click()
+ await extension.navigation.showSettingsLocator.click()
await extension.settings.developerSettings.click()
await extension.developerSettings.manageNetworks.click()
await extension.developerSettings.deleteNetworkByName("My Network").click()
@@ -73,7 +73,7 @@ test.describe("Network", () => {
}) => {
await extension.wallet.newWalletOnboarding()
await extension.open()
- await extension.navigation.showSettings.click()
+ await extension.navigation.showSettingsLocator.click()
await extension.settings.developerSettings.click()
await extension.developerSettings.manageNetworks.click()
@@ -83,21 +83,21 @@ test.describe("Network", () => {
await extension.developerSettings.chainId.fill("SN_GOERLI")
await extension.developerSettings.rpcUrl.fill(config.testnetRpcUrl!)
- await extension.navigation.create.click()
+ await extension.navigation.createLocator.click()
await expect(
extension.developerSettings.networkByName("My Network"),
).toBeVisible()
- await extension.navigation.back.click()
- await extension.navigation.back.click()
- await extension.navigation.close.click()
+ await extension.navigation.backLocator.click()
+ await extension.navigation.backLocator.click()
+ await extension.navigation.closeLocator.click()
// add account
await extension.network.selectNetwork("My Network")
await extension.account.addAccount({ firstAccount: true })
- await extension.network.selectNetwork("Testnet")
+ await extension.network.selectDefaultNetwork()
// try to restore networks
- await extension.navigation.showSettings.click()
+ await extension.navigation.showSettingsLocator.click()
await extension.settings.developerSettings.click()
await extension.developerSettings.manageNetworks.click()
await extension.developerSettings.restoreDefaultNetworks.click()
diff --git a/packages/e2e/extension/src/specs/nfts.spec.ts b/packages/e2e/extension/src/specs/nfts.spec.ts
new file mode 100644
index 000000000..8c1fef13c
--- /dev/null
+++ b/packages/e2e/extension/src/specs/nfts.spec.ts
@@ -0,0 +1,72 @@
+import { expect } from "@playwright/test"
+
+import config from "../../../shared/config"
+import test from "../test"
+const spokCampaignName = `${config.spokCampaignName!}`
+for (const feeToken of ["STRK", "ETH"] as const) {
+ test.describe(`Nfts ${feeToken}`, () => {
+ test(`User should be able to claim and send a NFT`, async ({
+ extension,
+ browserContext,
+ }) => {
+ const { accountAddresses } = await extension.setupWallet({
+ accountsToSetup: [
+ {
+ assets: [
+ { token: "ETH", balance: 0.01 },
+ { token: "STRK", balance: 0.005 },
+ ],
+ deploy: true,
+ feeToken,
+ },
+ { assets: [{ token: "ETH", balance: 0 }] },
+ ],
+ })
+ await extension.account.ensureSelectedAccount(
+ extension.account.accountName1,
+ )
+ const dapp = await extension.dapps.claimSpok(browserContext)
+ await extension.dapps.knownDappButton.click()
+ await extension.dapps.ensureKnowDappText()
+ await extension.dapps.closeButtonLocator.click()
+ await extension.dapps.accept.click()
+ await dapp.getByRole("button", { name: "Claim now" }).click()
+ await Promise.all([
+ extension.navigation.confirmLocator.click(),
+ expect(
+ extension.activity.menuPendingTransactionsIndicatorLocator,
+ ).toBeVisible(),
+ ])
+ await expect(
+ extension.activity.menuPendingTransactionsIndicatorLocator,
+ ).toBeHidden()
+
+ await extension.navigation.menuNTFsLocator.click()
+ await extension.nfts.collection(spokCampaignName).click()
+ await extension.nfts.nftByPosition().click()
+
+ await extension.account.send.click()
+ await extension.account.fillRecipientAddress({
+ recipientAddress: accountAddresses[1],
+ })
+ await extension.nfts.reviewSendLocator.click()
+ await Promise.all([
+ extension.account.confirmLocator.click(),
+ expect(
+ extension.activity.menuPendingTransactionsIndicatorLocator,
+ ).toBeVisible(),
+ ])
+ await expect(
+ extension.activity.menuPendingTransactionsIndicatorLocator,
+ ).toBeHidden()
+ await extension.navigation.menuNTFsLocator.click()
+ await expect(extension.nfts.collection(spokCampaignName)).toBeHidden()
+
+ await extension.account.ensureSelectedAccount(
+ extension.account.accountName2,
+ )
+ await extension.nfts.collection(spokCampaignName).click()
+ await extension.nfts.nftByPosition().click()
+ })
+ })
+}
diff --git a/packages/e2e/extension/src/specs/recovery.spec.ts b/packages/e2e/extension/src/specs/recovery.spec.ts
index feead1738..e09531a0e 100644
--- a/packages/e2e/extension/src/specs/recovery.spec.ts
+++ b/packages/e2e/extension/src/specs/recovery.spec.ts
@@ -1,6 +1,6 @@
import { expect } from "@playwright/test"
-import config from "../config"
+import config from "../../../shared/config"
import test from "../test"
test.describe("Recovery Wallet", () => {
@@ -9,10 +9,7 @@ test.describe("Recovery Wallet", () => {
}) => {
const { seed } = await extension.setupWallet({
accountsToSetup: [
- {
- initialBalance: 0.0005,
- deploy: true,
- },
+ { assets: [{ token: "ETH", balance: 0.0005 }], deploy: true },
],
})
@@ -31,7 +28,7 @@ test.describe("Recovery Wallet", () => {
await expect(extension.account.showAccountRecovery).toBeVisible()
await extension.account.showAccountRecovery.click()
await extension.account.confirmTheSeedPhrase.click()
- await extension.navigation.done.click()
+ await extension.navigation.doneLocator.click()
await expect(extension.account.setUpAccountRecovery).toBeHidden()
})
@@ -41,9 +38,9 @@ test.describe("Recovery Wallet", () => {
await extension.open()
await extension.recoverWallet(config.testNetSeed1!)
await expect(extension.network.networkSelector).toBeVisible()
- await extension.network.selectNetwork("Testnet")
+ await extension.network.selectDefaultNetwork()
await extension.account.selectAccount("Account 33")
- await expect(extension.account.currentBalance("Ethereum")).toContainText(
+ await expect(extension.account.currentBalance("ETH")).toContainText(
"0.0000097 ETH",
)
})
diff --git a/packages/e2e/extension/src/specs/sendMaxFunds.spec.ts b/packages/e2e/extension/src/specs/sendMaxFunds.spec.ts
index 78ea94904..9406b215a 100644
--- a/packages/e2e/extension/src/specs/sendMaxFunds.spec.ts
+++ b/packages/e2e/extension/src/specs/sendMaxFunds.spec.ts
@@ -1,80 +1,121 @@
import { expect } from "@playwright/test"
-import config from "../config"
+import config from "../../../shared/config"
import test from "../test"
+for (const feeToken of ["STRK", "ETH"] as const) {
+ test.describe(`Send MAX funds fee ${feeToken}`, () => {
+ test.slow()
+ //using STRK to pay fee, user should be able to transfer all ETH funds
+ const expectedUpdatedBalance = feeToken === "STRK" ? "0.0 ETH" : "0.00"
+ test("send MAX funds to other self account", async ({ extension }) => {
+ const { accountAddresses } = await extension.setupWallet({
+ accountsToSetup: [
+ {
+ assets: [
+ { token: "ETH", balance: 0.01 },
+ { token: "STRK", balance: 0.005 },
+ ],
+ },
+ { assets: [{ token: "ETH", balance: 0 }] },
+ ],
+ })
+ const { sendAmountTX, sendAmountFE } = await extension.account.transfer({
+ originAccountName: extension.account.accountName1,
+ recipientAddress: accountAddresses[1],
+ token: "ETH",
+ amount: "MAX",
+ feeToken,
+ })
+ const txHash = await extension.activity.getLastTxHash()
+ await extension.validateTx({
+ txHash: txHash!,
+ receiver: accountAddresses[1],
+ sendAmountFE,
+ sendAmountTX,
+ })
+ await extension.navigation.menuTokensLocator.click()
-test.describe("Send funds", () => {
- test("send MAX funds to other self account", async ({ extension }) => {
- const { accountAddresses } = await extension.setupWallet({
- accountsToSetup: [{ initialBalance: 0.01 }, { initialBalance: 0 }],
- })
- const amountTrx = await extension.account.transfer({
- originAccountName: extension.account.accountName1,
- recipientAddress: accountAddresses[1],
- tokenName: "Ethereum",
- amount: "MAX",
+ //ensure that balance is updated
+ await expect(extension.account.currentBalance("ETH")).toContainText(
+ expectedUpdatedBalance,
+ )
+ let balance = await extension.account.currentBalance("ETH").innerText()
+ expect(parseFloat(balance)).toBeLessThan(0.01)
+
+ await extension.account.ensureSelectedAccount(
+ extension.account.accountName2,
+ )
+ await expect(extension.account.currentBalance("ETH")).toContainText(
+ "0.01",
+ )
+ balance = await extension.account.currentBalance("ETH").innerText()
+ expect(parseFloat(balance)).toBeGreaterThan(0.0001)
})
- const txHash = await extension.activity.getLastTxHash()
- await extension.validateTx(txHash!, accountAddresses[1], amountTrx)
- await extension.navigation.menuTokens.click()
- //ensure that balance is updated
- await expect(extension.account.currentBalance("Ethereum")).toContainText(
- "0.00",
- { timeout: 60000 },
- )
- let balance = await extension.account.currentBalance("Ethereum").innerText()
- expect(parseFloat(balance)).toBeLessThan(0.01)
+ test("send MAX funds to other wallet/account", async ({ extension }) => {
+ await extension.setupWallet({
+ accountsToSetup: [
+ {
+ assets: [
+ { token: "ETH", balance: 0.002 },
+ { token: "STRK", balance: 0.005 },
+ ],
+ },
+ ],
+ })
- await extension.account.ensureSelectedAccount(
- extension.account.accountName2,
- )
- await expect(extension.account.currentBalance("Ethereum")).toContainText(
- "0.01",
- { timeout: 60000 },
- )
- balance = await extension.account.currentBalance("Ethereum").innerText()
- expect(parseFloat(balance)).toBeGreaterThan(0.0001)
- })
+ const { sendAmountTX, sendAmountFE } = await extension.account.transfer({
+ originAccountName: extension.account.accountName1,
+ recipientAddress: config.destinationAddress!,
+ token: "ETH",
+ amount: "MAX",
+ feeToken,
+ })
- test("send MAX funds to other wallet/account", async ({ extension }) => {
- await extension.setupWallet({
- accountsToSetup: [{ initialBalance: 0.002 }],
- })
+ const txHash = await extension.activity.getLastTxHash()
+ await extension.validateTx({
+ txHash: txHash!,
+ receiver: config.destinationAddress!,
+ sendAmountFE,
+ sendAmountTX,
+ })
+ await extension.navigation.menuTokensLocator.click()
- const amountTrx = await extension.account.transfer({
- originAccountName: extension.account.accountName1,
- recipientAddress: config.destinationAddress!,
- tokenName: "Ethereum",
- amount: "MAX",
+ //ensure that balance is updated
+ await expect(extension.account.currentBalance("ETH")).toContainText(
+ expectedUpdatedBalance,
+ )
+ const balance = await extension.account.currentBalance("ETH").innerText()
+ expect(parseFloat(balance)).toBeLessThan(0.002)
})
- const txHash = await extension.activity.getLastTxHash()
- await extension.validateTx(txHash!, config.destinationAddress!, amountTrx)
- await extension.navigation.menuTokens.click()
-
- //ensure that balance is updated
- await expect(extension.account.currentBalance("Ethereum")).toContainText(
- "0.000",
- { timeout: 60000 },
- )
- const balance = await extension.account
- .currentBalance("Ethereum")
- .innerText()
- expect(parseFloat(balance)).toBeLessThan(0.002)
- })
-
- test("User should be able to send funds to starknet id", async ({
- extension,
- }) => {
- await extension.setupWallet({ accountsToSetup: [{ initialBalance: 0.01 }] })
- const amountTrx = await extension.account.transfer({
- originAccountName: extension.account.accountName1,
- recipientAddress: "e2e-test.stark",
- tokenName: "Ethereum",
- amount: "MAX",
+ test("User should be able to send funds to starknet id", async ({
+ extension,
+ }) => {
+ await extension.setupWallet({
+ accountsToSetup: [
+ {
+ assets: [
+ { token: "ETH", balance: 0.01 },
+ { token: "STRK", balance: 0.005 },
+ ],
+ },
+ ],
+ })
+ const { sendAmountTX, sendAmountFE } = await extension.account.transfer({
+ originAccountName: extension.account.accountName1,
+ recipientAddress: "qateste2e.stark",
+ token: "ETH",
+ amount: "MAX",
+ feeToken,
+ })
+ const txHash = await extension.activity.getLastTxHash()
+ await extension.validateTx({
+ txHash: txHash!,
+ receiver: config.destinationAddress!,
+ sendAmountFE,
+ sendAmountTX,
+ })
})
- const txHash = await extension.activity.getLastTxHash()
- await extension.validateTx(txHash!, config.account1Seed3!, amountTrx)
})
-})
+}
diff --git a/packages/e2e/extension/src/specs/sendPartialFunds.spec.ts b/packages/e2e/extension/src/specs/sendPartialFunds.spec.ts
index 8876b7456..3712b7c40 100644
--- a/packages/e2e/extension/src/specs/sendPartialFunds.spec.ts
+++ b/packages/e2e/extension/src/specs/sendPartialFunds.spec.ts
@@ -1,86 +1,128 @@
import { expect } from "@playwright/test"
-import config from "../config"
+import config from "../../../shared/config"
import test from "../test"
+for (const feeToken of ["STRK", "ETH"] as const) {
+ test.describe(`Send partial funds fee ${feeToken}`, () => {
+ test.slow()
+ test("send partial funds to other self account", async ({ extension }) => {
+ const { accountAddresses } = await extension.setupWallet({
+ accountsToSetup: [
+ {
+ assets: [
+ { token: "ETH", balance: 0.01 },
+ { token: "STRK", balance: 0.005 },
+ ],
+ },
+ { assets: [{ token: "ETH", balance: 0 }] },
+ ],
+ })
+ const { sendAmountTX, sendAmountFE } = await extension.account.transfer({
+ originAccountName: extension.account.accountName1,
+ recipientAddress: accountAddresses[1],
+ token: "ETH",
+ amount: 0.005,
+ feeToken,
+ })
+ const txHash = await extension.activity.getLastTxHash()
+ await extension.validateTx({
+ txHash: txHash!,
+ receiver: accountAddresses[1],
+ sendAmountFE,
+ sendAmountTX,
+ })
+ await extension.navigation.menuTokensLocator.click()
-test.describe("Send funds", () => {
- test("send partial funds to other self account", async ({ extension }) => {
- const { accountAddresses } = await extension.setupWallet({
- accountsToSetup: [{ initialBalance: 0.01 }, { initialBalance: 0 }],
- })
- const amountTrx = await extension.account.transfer({
- originAccountName: extension.account.accountName1,
- recipientAddress: accountAddresses[1],
- tokenName: "Ethereum",
- amount: 0.005,
- })
- const txHash = await extension.activity.getLastTxHash()
- await extension.validateTx(txHash!, accountAddresses[1], amountTrx)
- await extension.navigation.menuTokens.click()
+ //ensure that balance is updated
+ await expect(extension.account.currentBalance("ETH")).toContainText(
+ "0.00",
+ )
+ const balance = await extension.account.currentBalance("ETH").innerText()
+ expect(parseFloat(balance)).toBeLessThan(0.01)
- //ensure that balance is updated
- await expect(extension.account.currentBalance("Ethereum")).toContainText(
- "0.00",
- { timeout: 60000 },
- )
- const balance = await extension.account
- .currentBalance("Ethereum")
- .innerText()
- expect(parseFloat(balance)).toBeLessThan(0.01)
-
- await extension.account.ensureSelectedAccount(
- extension.account.accountName2,
- )
- await expect(extension.account.currentBalance("Ethereum")).toContainText(
- "0.005",
- { timeout: 60000 },
- )
- })
-
- test("send partial funds to other wallet/account", async ({ extension }) => {
- await extension.setupWallet({ accountsToSetup: [{ initialBalance: 0.01 }] })
- const amountTrx = await extension.account.transfer({
- originAccountName: extension.account.accountName1,
- recipientAddress: config.destinationAddress!,
- tokenName: "Ethereum",
- amount: 0.005,
- fillRecipientAddress: "typing",
+ await extension.account.ensureSelectedAccount(
+ extension.account.accountName2,
+ )
+ await expect(extension.account.currentBalance("ETH")).toContainText(
+ "0.005",
+ )
})
- const txHash = await extension.activity.getLastTxHash()
- await extension.validateTx(txHash!, config.destinationAddress!, amountTrx)
- await extension.navigation.menuTokens.click()
+ test("send partial funds to other wallet/account", async ({
+ extension,
+ }) => {
+ await extension.setupWallet({
+ accountsToSetup: [
+ {
+ assets: [
+ { token: "ETH", balance: 0.01 },
+ { token: "STRK", balance: 0.005 },
+ ],
+ },
+ ],
+ })
+ const { sendAmountTX, sendAmountFE } = await extension.account.transfer({
+ originAccountName: extension.account.accountName1,
+ recipientAddress: config.destinationAddress!,
+ token: "ETH",
+ amount: 0.005,
+ fillRecipientAddress: "typing",
+ feeToken,
+ })
+
+ const txHash = await extension.activity.getLastTxHash()
+ await extension.validateTx({
+ txHash: txHash!,
+ receiver: config.destinationAddress!,
+ sendAmountFE,
+ sendAmountTX,
+ })
+ await extension.navigation.menuTokensLocator.click()
- //ensure that balance is updated
- await expect(extension.account.currentBalance("Ethereum")).toContainText(
- "0.00",
- { timeout: 60000 },
- )
- const balance = await extension.account
- .currentBalance("Ethereum")
- .innerText()
- expect(parseFloat(balance)).toBeLessThan(0.01)
+ //ensure that balance is updated
+ await expect(extension.account.currentBalance("ETH")).toContainText(
+ "0.00",
+ )
+ const balance = await extension.account.currentBalance("ETH").innerText()
+ expect(parseFloat(balance)).toBeLessThan(0.01)
- //send back remaining funds
- await extension.account.transfer({
- originAccountName: extension.account.accountName1,
- recipientAddress: config.destinationAddress!,
- tokenName: "Ethereum",
- amount: "MAX",
+ //send back remaining funds
+ await extension.account.transfer({
+ originAccountName: extension.account.accountName1,
+ recipientAddress: config.destinationAddress!,
+ token: "ETH",
+ amount: "MAX",
+ feeToken,
+ })
})
- })
- test("User should be able to send funds to starknet id", async ({
- extension,
- }) => {
- await extension.setupWallet({ accountsToSetup: [{ initialBalance: 0.01 }] })
- const amountTrx = await extension.account.transfer({
- originAccountName: extension.account.accountName1,
- recipientAddress: "e2e-test.stark",
- tokenName: "Ethereum",
- amount: "MAX",
+ test("User should be able to send funds to starknet id", async ({
+ extension,
+ }) => {
+ await extension.setupWallet({
+ accountsToSetup: [
+ {
+ assets: [
+ { token: "ETH", balance: 0.01 },
+ { token: "STRK", balance: 0.005 },
+ ],
+ },
+ ],
+ })
+ const { sendAmountTX, sendAmountFE } = await extension.account.transfer({
+ originAccountName: extension.account.accountName1,
+ recipientAddress: "qateste2e.stark",
+ token: "ETH",
+ amount: 0.009,
+ feeToken,
+ })
+ const txHash = await extension.activity.getLastTxHash()
+ await extension.validateTx({
+ txHash: txHash!,
+ receiver: config.destinationAddress!,
+ sendAmountFE,
+ sendAmountTX,
+ })
})
- const txHash = await extension.activity.getLastTxHash()
- await extension.validateTx(txHash!, config.account1Seed3!, amountTrx)
})
-})
+}
diff --git a/packages/e2e/extension/src/specs/tokens.spec.ts b/packages/e2e/extension/src/specs/tokens.spec.ts
new file mode 100644
index 000000000..e164df628
--- /dev/null
+++ b/packages/e2e/extension/src/specs/tokens.spec.ts
@@ -0,0 +1,64 @@
+import { expect } from "@playwright/test"
+
+import test from "../test"
+import { lang } from "../languages"
+import { transferTokens } from "../../../shared/src/assets"
+
+test.describe("Tokens", () => {
+ test("Add token, token should only be visible if preference is set", async ({
+ extension,
+ }) => {
+ await extension.setupWallet({
+ accountsToSetup: [{ assets: [{ token: "ETH", balance: 0.0001 }] }],
+ })
+ await extension.page
+ .getByRole("link", { name: lang.account.newToken })
+ .click()
+ await extension.page
+ .locator('[name="address"]')
+ .fill(
+ "0x05A6B68181bb48501a7A447a3f99936827E41D77114728960f22892F02E24928",
+ )
+ await expect(extension.page.locator('[name="name"]')).toHaveValue("Astraly")
+ await Promise.all([
+ extension.navigation.continueLocator.click(),
+ extension.page.locator('text="Token added"').click(),
+ ])
+
+ await expect(extension.account.token("AST")).toBeHidden()
+ await extension.navigation.showSettingsLocator.click()
+ await extension.settings.preferences.click()
+ await expect(extension.preferences.hideTokensStatus).toBeEnabled()
+ await extension.preferences.hideTokens.click()
+ await extension.navigation.backLocator.click()
+ await extension.navigation.closeLocator.click()
+ await expect(extension.account.token("AST")).toBeVisible()
+ })
+
+ test("Token should be auto discovered", async ({ extension }) => {
+ const { accountAddresses } = await extension.setupWallet({
+ accountsToSetup: [{ assets: [{ token: "ETH", balance: 0.00000001 }] }],
+ })
+
+ //ensure that balance is updated
+ await Promise.all([
+ expect(extension.account.currentBalance("ETH")).toHaveText(
+ "0.00000001 ETH",
+ ),
+ expect(extension.account.currentBalance("WBTC")).toBeHidden(),
+ ])
+
+ await transferTokens(0.00000002, accountAddresses[0], "WBTC")
+
+ //ensure that balance is updated
+ await Promise.all([
+ expect(extension.account.currentBalance("ETH")).toHaveText(
+ "0.00000001 ETH",
+ ),
+ expect(extension.account.currentBalance("WBTC")).toHaveText(
+ "0.00000002 WBTC",
+ { timeout: 180 * 1000 },
+ ),
+ ])
+ })
+})
diff --git a/packages/e2e/extension/src/specs/welcome.spec.ts b/packages/e2e/extension/src/specs/welcome.spec.ts
index 8a3281582..00d0568cb 100644
--- a/packages/e2e/extension/src/specs/welcome.spec.ts
+++ b/packages/e2e/extension/src/specs/welcome.spec.ts
@@ -12,26 +12,6 @@ test.describe("Welcome screen", () => {
])
})
- test("Disclaimer - Continue button should only be enabled if both options are accepted", async ({
- extension,
- }) => {
- await extension.wallet.createNewWallet.click()
- await Promise.all([
- expect(extension.wallet.banner2).toBeVisible(),
- expect(extension.wallet.disclaimerLostOfFunds).not.toBeChecked(),
- expect(extension.wallet.disclaimerAlphaVersion).not.toBeChecked(),
- expect(extension.wallet.continue).toBeDisabled(),
- ])
- await extension.wallet.disclaimerLostOfFunds.check()
- await expect(extension.wallet.continue).toBeDisabled()
-
- await extension.wallet.disclaimerAlphaVersion.check()
- await expect(extension.wallet.continue).toBeEnabled()
-
- await extension.wallet.disclaimerLostOfFunds.uncheck()
- await expect(extension.wallet.continue).toBeDisabled()
- })
-
test("create new account with success", async ({ extension }) => {
await extension.wallet.newWalletOnboarding()
await extension.open()
diff --git a/packages/e2e/extension/src/test.ts b/packages/e2e/extension/src/test.ts
index 1b2bbbba2..db27af857 100644
--- a/packages/e2e/extension/src/test.ts
+++ b/packages/e2e/extension/src/test.ts
@@ -1,8 +1,11 @@
-import * as fs from "fs"
+import {
+ artifactsDir,
+ isCI,
+ isKeepArtifacts,
+ keepVideos,
+ saveHtml,
+} from "../../shared/cfg/test"
import path from "path"
-import dotenv from "dotenv"
-
-dotenv.config()
import {
ChromiumBrowserContext,
Page,
@@ -11,23 +14,13 @@ import {
test as testBase,
} from "@playwright/test"
import { v4 as uuid } from "uuid"
-
-import config from "./config"
import type { TestExtensions } from "./fixtures"
import ExtensionPage from "./page-objects/ExtensionPage"
-const isCI = Boolean(process.env.CI)
const isExtensionURL = (url: string) => url.startsWith("chrome-extension://")
let browserCtx: ChromiumBrowserContext
-const outputFolder = (testInfo: TestInfo) =>
- testInfo.title.replace(/\s+/g, "_").replace(/\W/g, "")
-const artifactFilename = (testInfo: TestInfo) =>
- `${testInfo.retry}-${testInfo.status}-${pageId++}-${testInfo.workerIndex}`
-const keepArtifacts = (testInfo: TestInfo) =>
- testInfo.config.preserveOutput === "always" ||
- (testInfo.config.preserveOutput === "failures-only" &&
- testInfo.status === "failed") ||
- testInfo.status === "timedOut"
+const distDir = path.join(__dirname, "../../../extension/dist/")
+
const closePages = async (browserContext: ChromiumBrowserContext) => {
const pages = browserContext?.pages() || []
for (const page of pages) {
@@ -38,31 +31,6 @@ const closePages = async (browserContext: ChromiumBrowserContext) => {
}
}
-const keepHtml = async (testInfo: TestInfo, page: Page) => {
- if (keepArtifacts(testInfo)) {
- const htmlContent = await page.content()
- await fs.promises
- .mkdir(path.resolve(config.artifactsDir, outputFolder(testInfo)), {
- recursive: true,
- })
- .catch((error) => {
- console.error(error)
- })
- await fs.promises
- .writeFile(
- path.resolve(
- config.artifactsDir,
- outputFolder(testInfo),
- `${artifactFilename(testInfo)}.html`,
- ),
- htmlContent,
- )
- .catch((error) => {
- console.error(error)
- })
- }
-}
-
const createBrowserContext = () => {
const userDataDir = `/tmp/test-user-data-${uuid()}`
return chromium.launchPersistentContext(userDataDir, {
@@ -71,57 +39,23 @@ const createBrowserContext = () => {
`${isCI ? "--headless=new" : ""}`,
"--disable-dev-shm-usage",
"--ipc=host",
- `--disable-extensions-except=${config.distDir}`,
- `--load-extension=${config.distDir}`,
+ `--disable-extensions-except=${distDir}`,
+ `--load-extension=${distDir}`,
"--disable-gpu",
],
ignoreDefaultArgs: ["--disable-component-extensions-with-background-pages"],
recordVideo: {
- dir: config.artifactsDir,
+ dir: artifactsDir,
size: {
width: 800,
- height: 600,
+ height: 700,
},
},
})
}
-const initBrowserWithExtension = async (testInfo: TestInfo) => {
+const initBrowserWithExtension = async () => {
const browserContext = await createBrowserContext()
- // save video
- browserContext.on("page", async (page) => {
- page.on("load", async (page) => {
- try {
- await page.title()
- } catch (err) {
- console.warn(err)
- }
- })
-
- page.on("close", async (page) => {
- if (keepArtifacts(testInfo)) {
- await page
- .video()
- ?.saveAs(
- path.resolve(
- config.artifactsDir,
- outputFolder(testInfo),
- `${artifactFilename(testInfo)}.webm`,
- ),
- )
- .catch((error) => {
- console.error(error)
- })
- }
- await page
- .video()
- ?.delete()
- .catch((error) => {
- console.error(error)
- })
- })
- })
-
await browserContext.addInitScript("window.PLAYWRIGHT = true;")
await browserContext.addInitScript(() => {
window.localStorage.setItem(
@@ -130,7 +64,7 @@ const initBrowserWithExtension = async (testInfo: TestInfo) => {
)
})
- let page = browserContext.pages()[0]
+ let page: Page = browserContext.pages()[0]
await page.bringToFront()
await page.goto("chrome://inspect/#extensions")
@@ -145,7 +79,9 @@ const initBrowserWithExtension = async (testInfo: TestInfo) => {
await page.waitForTimeout(500)
const pages = browserContext.pages()
- const extPage = pages.find((x) => x.url() === extensionURL)
+ const extPage = pages.find(
+ (x: { url: () => string }) => x.url() === extensionURL,
+ )
if (extPage) {
page = extPage
}
@@ -154,34 +90,26 @@ const initBrowserWithExtension = async (testInfo: TestInfo) => {
}
await page.emulateMedia({ reducedMotion: "reduce" })
-
return { browserContext, extensionURL, page }
}
-//delete videos related with chrome://extensions/ page
-function cleanArtifactDir() {
- try {
- fs.readdirSync(config.artifactsDir)
- .filter((f) => f.endsWith("webm"))
- .forEach((fileToDelete) =>
- fs.rmSync(`${config.artifactsDir}/${fileToDelete}`),
- )
- } catch (error) {
- console.log(error)
- }
-}
-
-function createExtension() {
+function createExtension(label: string) {
return async ({}, use: any, testInfo: TestInfo) => {
const { browserContext, page, extensionURL } =
- await initBrowserWithExtension(testInfo)
+ await initBrowserWithExtension()
+
const extension = new ExtensionPage(page, extensionURL)
await closePages(browserContext)
browserCtx = browserContext
await use(extension)
- await keepHtml(testInfo, page)
- await browserContext.close()
- cleanArtifactDir()
+ const keepArtifacts = isKeepArtifacts(testInfo)
+ if (keepArtifacts) {
+ await saveHtml(testInfo, page, label)
+ await browserContext.close()
+ await keepVideos(testInfo, page, label)
+ } else {
+ await browserContext.close()
+ }
}
}
@@ -191,10 +119,9 @@ function getContext() {
}
}
-let pageId = 0
const test = testBase.extend({
- extension: createExtension(),
- secondExtension: createExtension(),
+ extension: createExtension("extension"),
+ secondExtension: createExtension("secondExtension"),
browserContext: getContext(),
})
diff --git a/packages/e2e/extension/src/utils/account.ts b/packages/e2e/extension/src/utils/account.ts
deleted file mode 100644
index c8e87c485..000000000
--- a/packages/e2e/extension/src/utils/account.ts
+++ /dev/null
@@ -1,148 +0,0 @@
-import {
- Account,
- uint256,
- num,
- TransactionExecutionStatus,
- RpcProvider,
- constants,
-} from "starknet"
-import { bigDecimal, isEqualAddress } from "@argent/shared"
-import { getBatchProvider } from "@argent/x-multicall"
-import config from "../config"
-import { expect } from "@playwright/test"
-
-export interface AccountsToSetup {
- initialBalance: number
- deploy?: boolean
-}
-
-console.log(
- "Creating RPC provider with url",
- process.env.ARGENT_TESTNET_RPC_URL,
-)
-if (!process.env.ARGENT_TESTNET_RPC_URL) {
- throw new Error("Missing ARGENT_TESTNET_RPC_URL env variable")
-}
-
-const provider = new RpcProvider({
- nodeUrl: process.env.ARGENT_TESTNET_RPC_URL,
- chainId: constants.StarknetChainId.SN_GOERLI,
- headers: {
- "argent-version": process.env.VERSION || "Unknown version",
- "argent-client": "argent-x",
- },
-})
-const tnkETH =
- "0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7" // address of ETH
-
-const maxRetries = 4
-
-const formatAmount = (amount: string) => {
- return parseInt(amount, 16) / Math.pow(10, 18)
-}
-
-export async function transferEth(amount: string, to: string) {
- console.log(
- "########################### transferEth ##################################",
- )
- const account = new Account(
- provider,
- config.senderAddr!,
- config.senderKey!,
- "1",
- )
- const initialBalance = await balanceEther(account.address)
- const initialBalanceFormatted = parseFloat(initialBalance) * Math.pow(10, 18)
- const { low, high } = uint256.bnToUint256(amount)
-
- if (initialBalanceFormatted < parseInt(amount)) {
- throw `Failed to tranfer: Not enought balance ${initialBalanceFormatted} < ${amount}`
- }
- let placeTXAttempt = 0
- let txHash
- while (placeTXAttempt < maxRetries) {
- /** timemout if we don't receive a valid execution response */
- const placeTXTimeout = setTimeout(() => {
- throw new Error("Place tx timed out")
- }, 30 * 1000) /** 30 seconds */
- try {
- placeTXAttempt++
- const tx = await account.execute({
- contractAddress: tnkETH,
- entrypoint: "transfer",
- calldata: [to, low, high],
- })
- txHash = tx.transaction_hash
- const txStatusResponse = await provider.waitForTransaction(
- tx.transaction_hash,
- )
- if (
- txStatusResponse &&
- "execution_status" in txStatusResponse &&
- txStatusResponse.execution_status ===
- TransactionExecutionStatus.SUCCEEDED
- ) {
- console.log(
- `[TX execution_status ${TransactionExecutionStatus.SUCCEEDED}] ${config.starkscanTestNetUrl}/tx/${tx.transaction_hash}`,
- )
- return tx.transaction_hash
- }
- const elements = [
- `[Failed to place TX] ${config.starkscanTestNetUrl}/tx/${tx.transaction_hash}`,
- ]
- if (txStatusResponse) {
- if ("execution_status" in txStatusResponse) {
- elements.push(
- `execution_status: ${txStatusResponse.execution_status}`,
- )
- }
- if ("revert_reason" in txStatusResponse) {
- elements.push(`revert_reason: ${txStatusResponse.revert_reason}`)
- }
- elements.push(`status: ${txStatusResponse.execution_status}`)
- } else {
- elements.push("unable to get tx status response")
- }
- const message = elements.join(", ")
- console.error(message)
- } catch (e) {
- //for debug only
- console.log(`Exception: ${txHash}`, e)
- } finally {
- clearTimeout(placeTXTimeout)
- }
- console.warn("Transfer failed, going to try again ")
- }
-}
-
-export async function balanceEther(accountAddress: string) {
- const balanceOfCall = {
- contractAddress: tnkETH,
- entrypoint: "balanceOf",
- calldata: [accountAddress],
- }
-
- const multicall = getBatchProvider(provider)
- const { result } = await multicall.callContract(balanceOfCall)
- const [low, high] = result
- const balance = bigDecimal.formatEther(uint256.uint256ToBN({ low, high }))
- return parseFloat(balance).toFixed(4)
-}
-
-export async function validateTx(
- txHash: string,
- reciever: string,
- amount?: number,
-) {
- console.log("txHash:", txHash)
- await provider.waitForTransaction(txHash)
- const txData = await provider.getTransaction(txHash)
- if (!("calldata" in txData)) {
- throw new Error(`Invalid transaction data: ${JSON.stringify(txData)}`)
- }
- const accAdd = txData.calldata[4].toString()
- expect(isEqualAddress(accAdd, reciever)).toBe(true)
- if (amount) {
- expect(formatAmount(txData.calldata[5].toString())).toBe(amount)
- }
-}
diff --git a/packages/e2e/merge-reports.config.js b/packages/e2e/merge-reports.config.js
new file mode 100644
index 000000000..c535276af
--- /dev/null
+++ b/packages/e2e/merge-reports.config.js
@@ -0,0 +1,4 @@
+export default {
+ testDir: "./all-blob-reports",
+ reporter: [["html", { open: "never" }]],
+}
diff --git a/packages/e2e/package.json b/packages/e2e/package.json
index 81179d70a..40bd4e31d 100644
--- a/packages/e2e/package.json
+++ b/packages/e2e/package.json
@@ -6,12 +6,11 @@
"license": "MIT",
"devDependencies": {
"@argent/shared": "workspace:^",
- "@argent/x-multicall": "^7.0.12",
- "@playwright/test": "^1.37.1",
+ "@playwright/test": "^1.40.1",
"@types/node": "^20.5.7",
"@types/uuid": "^9.0.3",
"dotenv": "^16.3.1",
- "starknet": "5.24.3",
+ "starknet": "6.0.0-beta.13",
"uuid": "^9.0.0"
},
"scripts": {
diff --git a/packages/e2e/shared/cfg/global.teardown.ts b/packages/e2e/shared/cfg/global.teardown.ts
new file mode 100644
index 000000000..2153e09df
--- /dev/null
+++ b/packages/e2e/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/packages/e2e/shared/cfg/test.ts b/packages/e2e/shared/cfg/test.ts
new file mode 100644
index 000000000..455aaa230
--- /dev/null
+++ b/packages/e2e/shared/cfg/test.ts
@@ -0,0 +1,74 @@
+import dotenv from "dotenv"
+dotenv.config()
+
+import * as fs from "fs"
+import path from "path"
+
+import { Page, TestInfo } from "@playwright/test"
+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,
+) => {
+ console.log({
+ 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,
+) => {
+ console.log({
+ 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/packages/e2e/extension/src/config.ts b/packages/e2e/shared/config.ts
similarity index 64%
rename from packages/e2e/extension/src/config.ts
rename to packages/e2e/shared/config.ts
index ca132f580..8f024471d 100644
--- a/packages/e2e/extension/src/config.ts
+++ b/packages/e2e/shared/config.ts
@@ -2,28 +2,35 @@ import path from "path"
import dotenv from "dotenv"
import fs from "fs"
-const envPath = path.resolve(__dirname, "../../.env")
+const envPath = path.resolve(__dirname, "../.env")
if (fs.existsSync(envPath)) {
dotenv.config({ path: envPath })
}
const config = {
password: "MyP@ss3!",
- artifactsDir: path.resolve(__dirname, "../../artifacts/playwright"),
- reportsDir: path.resolve(__dirname, "../../artifacts/reports"),
- distDir: path.join(__dirname, "../../../extension/dist/"),
testNetSeed1: process.env.E2E_TESTNET_SEED1, //wallet with 32 deployed accounts
testNetSeed2: process.env.E2E_TESTNET_SEED2, //wallet with 1 deployed account
testNetSeed3: process.env.E2E_TESTNET_SEED3, //wallet with 1 deployed account
- destinationAddress: process.env.E2E_SENDER_ADDRESS, //used as transfers destination
- senderAddr: process.env.E2E_SENDER_ADDRESS,
senderSeed: process.env.E2E_SENDER_SEED,
- senderKey: process.env.E2E_SENDER_PRIVATEKEY,
account1Seed2: process.env.E2E_ACCOUNT_1_SEED2,
account1Seed3: process.env.E2E_ACCOUNT_1_SEED3,
starknetTestNetUrl: process.env.STARKNET_TESTNET_URL,
starkscanTestNetUrl: process.env.STARKSCAN_TESTNET_URL,
testnetRpcUrl: process.env.ARGENT_TESTNET_RPC_URL,
+ 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
+ spokCampaignName: process.env.E2E_SPOK_CAMPAIGN_NAME,
+ spokCampaignUrl: process.env.E2E_SPOK_CAMPAIGN_URL,
+
+ //webwallet
+ validLogin: {
+ email: "testuser@mail.com",
+ pin: "1111111",
+ password: "myNewPass12!",
+ },
+ url: "http://localhost:3005",
}
// check that no value of config is undefined, otherwise throw error
diff --git a/packages/e2e/shared/src/assets.ts b/packages/e2e/shared/src/assets.ts
new file mode 100644
index 000000000..30d1f2cc8
--- /dev/null
+++ b/packages/e2e/shared/src/assets.ts
@@ -0,0 +1,288 @@
+import {
+ Account,
+ uint256,
+ TransactionExecutionStatus,
+ RpcProvider,
+ constants,
+ TransactionFinalityStatus,
+} from "starknet"
+import { isEqualAddress } from "@argent/shared"
+import config from "../config"
+import { expect } from "@playwright/test"
+import { sleep } from "./common"
+
+export type TokenSymbol = "ETH" | "WBTC" | "STRK" | "AST"
+export type FeeTokens = "ETH" | "STRK"
+export interface AccountsToSetup {
+ assets: {
+ token: TokenSymbol
+ balance: number
+ }[]
+ deploy?: boolean
+ feeToken?: FeeTokens
+}
+const rpcUrl = process.env.ARGENT_TESTNET_RPC_URL
+console.log("Creating RPC provider with url", rpcUrl)
+if (!rpcUrl) {
+ throw new Error("Missing ARGENT_TESTNET_RPC_URL env variable")
+}
+const startScanUrl = config.starkscanTestNetUrl
+if (!startScanUrl) {
+ throw new Error("Missing STARKSCAN_TESTNET_URL env variable")
+}
+const provider = new RpcProvider({
+ nodeUrl: rpcUrl,
+ chainId: constants.StarknetChainId.SN_GOERLI,
+ 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: "0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
+ decimals: 18,
+})
+tokenAddresses.set("WBTC", {
+ name: "Wrapped BTC",
+ address: "0x12d537dc323c439dc65c976fad242d5610d27cfb5f31689a0a319b8be7f3d56",
+ decimals: 8,
+})
+tokenAddresses.set("STRK", {
+ name: "Starknet Token",
+ address: "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d",
+ decimals: 18,
+})
+tokenAddresses.set("AST", {
+ name: "Astraly",
+ address: "0x05A6B68181bb48501a7A447a3f99936827E41D77114728960f22892F02E24928",
+ decimals: 18,
+})
+
+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() * config.senderKeys!.length,
+ )
+ const acc = new Account(
+ provider,
+ config.senderAddrs![randomAccountPosition],
+ config.senderKeys![randomAccountPosition],
+ "1",
+ )
+
+ const initialBalance = await getBalance(acc.address, token)
+ const initialBalanceFormatted =
+ parseFloat(initialBalance) * Math.pow(10, 18)
+ if (initialBalanceFormatted < parseInt(amount)) {
+ log.push(
+ `${
+ config.senderAddrs![randomAccountPosition]
+ } Not enough balance ${initialBalanceFormatted} < ${amount}`,
+ )
+ } else {
+ console.log({
+ op: "getAccount",
+ randomAccountPosition,
+ address: acc.address,
+ balance: initialBalance,
+ })
+ 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)}`
+ console.log({ 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) {
+ console.log({
+ TxStatus: TransactionExecutionStatus.SUCCEEDED,
+ url: `${startScanUrl}/tx/${tx.transaction_hash}`,
+ })
+ return tx.transaction_hash
+ }
+
+ console.error(
+ `[Failed to place TX] ${startScanUrl}/tx/${tx.transaction_hash} ${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
+}
+
+async function getBalance(accountAddress: string, token: TokenSymbol = "ETH") {
+ const tokenInfo = getTokenInfo(token)
+ console.log({ 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)
+
+ console.log({
+ op: "getBalance",
+ balance,
+ formattedBalance: balance,
+ })
+ return balance
+}
+
+export async function validateTx(
+ txHash: string,
+ receiver: string,
+ amount?: number,
+) {
+ const log: string[] = []
+ log.push("validateTx txHash:", txHash)
+ 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)}`,
+ )
+ }
+ const accAdd = txData.calldata[4].toString()
+ 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/packages/e2e/extension/src/utils/common.ts b/packages/e2e/shared/src/common.ts
similarity index 70%
rename from packages/e2e/extension/src/utils/common.ts
rename to packages/e2e/shared/src/common.ts
index b18c36337..a16f5c800 100644
--- a/packages/e2e/extension/src/utils/common.ts
+++ b/packages/e2e/shared/src/common.ts
@@ -1,12 +1,15 @@
export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
-export const expireBESession = async (email: string) => {
+export const expireBESession = async (
+ email: string,
+ app: "webwallet" | "argentx",
+) => {
const requestOptions = {
method: "GET",
}
const request = `${
process.env.ARGENT_API_BASE_URL
- }/debug/expireCredentials?application=argentx&email=${encodeURIComponent(
+ }/debug/expireCredentials?application=${app}&email=${encodeURIComponent(
email,
)}`
const response = await fetch(request, requestOptions)
diff --git a/packages/e2e/tsconfig.json b/packages/e2e/tsconfig.json
index 6a764dd01..4c48caee8 100644
--- a/packages/e2e/tsconfig.json
+++ b/packages/e2e/tsconfig.json
@@ -14,8 +14,9 @@
"inlineSourceMap": true,
"composite": true,
"types": ["node"],
- "noEmit": true
+ "noEmit": true,
+ "skipLibCheck": true
},
- "include": ["**/src"],
+ "include": ["**/src", "**/shared"],
"exclude": ["node_modules"]
}
diff --git a/packages/e2e/webwallet/playwright.config.ts b/packages/e2e/webwallet/playwright.config.ts
index d4d2d962c..d8f6c52e5 100644
--- a/packages/e2e/webwallet/playwright.config.ts
+++ b/packages/e2e/webwallet/playwright.config.ts
@@ -1,7 +1,5 @@
import type { PlaywrightTestConfig } from "@playwright/test"
-import config from "./src/config"
-
-const isCI = Boolean(process.env.CI)
+import { artifactsDir, isCI } from "../shared/cfg/test"
const playwrightConfig: PlaywrightTestConfig = {
projects: [
@@ -17,12 +15,13 @@ const playwrightConfig: PlaywrightTestConfig = {
browserName: "firefox",
},
},
- {
+ /*{
name: "WebWallet - WebKit",
use: {
browserName: "webkit",
},
},
+ */
],
expect: {
timeout: 20 * 1000, // 20 seconds
@@ -37,8 +36,9 @@ const playwrightConfig: PlaywrightTestConfig = {
reporter: isCI ? [["github"], ["blob"]] : "list",
forbidOnly: isCI,
- outputDir: config.artifactsDir,
+ outputDir: artifactsDir,
preserveOutput: isCI ? "failures-only" : "never",
+ globalTeardown: "../shared/cfg/global.teardown.ts",
}
export default playwrightConfig
diff --git a/packages/e2e/webwallet/src/config.ts b/packages/e2e/webwallet/src/config.ts
deleted file mode 100644
index 78ace2245..000000000
--- a/packages/e2e/webwallet/src/config.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import path from "path"
-
-export default {
- validLogin: {
- email: "testuser@mail.com",
- pin: "1111111",
- password: "myNewPass12!",
- },
- url: "http://localhost:3005",
- artifactsDir: path.resolve(__dirname, "../../artifacts/playwright"),
- reportsDir: path.resolve(__dirname, "../../artifacts/reports"),
-}
diff --git a/packages/e2e/webwallet/src/fixtures.ts b/packages/e2e/webwallet/src/fixtures.ts
index 6a7f1c148..e5e30c187 100644
--- a/packages/e2e/webwallet/src/fixtures.ts
+++ b/packages/e2e/webwallet/src/fixtures.ts
@@ -1,5 +1,7 @@
+import { BrowserContext } from "@playwright/test"
import type WebWalletPage from "./page-objects/WebWalletPage"
export interface TestPages {
webWallet: WebWalletPage
+ browserContext: BrowserContext
}
diff --git a/packages/e2e/webwallet/src/page-objects/Dapps.ts b/packages/e2e/webwallet/src/page-objects/Dapps.ts
new file mode 100644
index 000000000..f858e0413
--- /dev/null
+++ b/packages/e2e/webwallet/src/page-objects/Dapps.ts
@@ -0,0 +1,67 @@
+import { ChromiumBrowserContext, Page, expect } from "@playwright/test"
+import { ICredentials } from "./Login"
+import Navigation from "./Navigation"
+
+type DappUrl =
+ | "https://goerli.app.starknet.id"
+ | "https://dapp-argentlabs.vercel.app"
+ | "https://starknetkit-blacked-listed.vercel.app"
+
+export default class Dapps extends Navigation {
+ constructor(page: Page) {
+ super(page)
+ }
+
+ async requestConnectionFromDapp({
+ browserContext,
+ dappUrl,
+ credentials,
+ newAccount = false,
+ }: {
+ browserContext: ChromiumBrowserContext
+ dappUrl: DappUrl
+ credentials: ICredentials
+ newAccount: boolean
+ }) {
+ //open dapp page
+ const dapp = await browserContext.newPage()
+ await dapp.setViewportSize({ width: 1080, height: 720 })
+ await dapp.goto(dappUrl)
+
+ if (
+ dappUrl === "https://dapp-argentlabs.vercel.app" ||
+ dappUrl === "https://starknetkit-blacked-listed.vercel.app"
+ ) {
+ await expect(dapp.locator('button:has-text("Connect")')).toHaveCount(1)
+ await dapp.locator('button:has-text("Connect")').first().click()
+ } else {
+ await expect(dapp.locator("text=CONNECT ARGENT")).toBeVisible()
+ await dapp.locator("text=CONNECT ARGENT").click()
+ }
+
+ const popupPromise = dapp.waitForEvent("popup")
+ await expect(dapp.locator("p:text-is('Email')")).toBeVisible()
+ await dapp.locator("p:text-is('Email')").click()
+ const popup = await popupPromise
+ // Wait for the popup to load.
+ await popup.waitForLoadState()
+
+ await popup.locator("[name=email]").fill(credentials.email)
+ await popup.locator('button[type="submit"]').click()
+ await popup.locator('[id^="pin-input"]').first().click()
+ await popup.locator('[id^="pin-input"]').first().fill(credentials.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)
+ }
+ await popup.locator('button[type="submit"]').click()
+ await popup.waitForLoadState()
+ await expect(
+ popup.locator(`p:text-is("${credentials.email}")`),
+ ).toBeVisible()
+ await popup.locator('button[type="submit"]').click()
+ return dapp
+ }
+}
diff --git a/packages/e2e/webwallet/src/page-objects/Login.ts b/packages/e2e/webwallet/src/page-objects/Login.ts
index b2f0ae1df..f73257405 100644
--- a/packages/e2e/webwallet/src/page-objects/Login.ts
+++ b/packages/e2e/webwallet/src/page-objects/Login.ts
@@ -1,9 +1,9 @@
import { Page, expect } from "@playwright/test"
-import config from "../config"
+import config from "../../../shared/config"
import Navigation from "./Navigation"
-interface ICredentials {
+export interface ICredentials {
email: string
pin: string
password: string
@@ -25,7 +25,9 @@ export default class Login extends Navigation {
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")]',
@@ -63,4 +65,17 @@ export default class Login extends Navigation {
])
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}/dashboard`),
+ this.continue.click(),
+ ])
+ await expect(this.lock).toBeVisible()
+ }
}
diff --git a/packages/e2e/webwallet/src/page-objects/Navigation.ts b/packages/e2e/webwallet/src/page-objects/Navigation.ts
index 3937ea355..17b0cf6dd 100644
--- a/packages/e2e/webwallet/src/page-objects/Navigation.ts
+++ b/packages/e2e/webwallet/src/page-objects/Navigation.ts
@@ -6,6 +6,10 @@ export default class Navigation {
this.page = page
}
+ get backupPassword() {
+ return this.page.locator(`button:text-is("I've backed up my password")`)
+ }
+
get continue() {
return this.page.locator(`button:text-is("Continue")`)
}
diff --git a/packages/e2e/webwallet/src/page-objects/WebWalletPage.ts b/packages/e2e/webwallet/src/page-objects/WebWalletPage.ts
index 3ddf03b3b..d0765254c 100644
--- a/packages/e2e/webwallet/src/page-objects/WebWalletPage.ts
+++ b/packages/e2e/webwallet/src/page-objects/WebWalletPage.ts
@@ -1,21 +1,29 @@
import type { Page } from "@playwright/test"
+import { v4 as uuid } from "uuid"
-import config from "../config"
+import config from "../../../shared/config"
import Login from "./Login"
import Navigation from "./Navigation"
+export const generateEmail = () => `newWallet_${uuid()}@mail.com`
+
+import Dapps from "./Dapps"
export default class WebWalletPage {
page: Page
login: Login
navigation: Navigation
+ dapps: Dapps
constructor(page: Page) {
this.page = page
this.login = new Login(page)
this.navigation = new Navigation(page)
+ this.dapps = new Dapps(page)
}
open() {
return this.page.goto(config.url)
}
+
+ generateEmail = () => `e2e_webwallet_${uuid()}@mail.com`
}
diff --git a/packages/e2e/webwallet/src/specs/dapps.spec.ts b/packages/e2e/webwallet/src/specs/dapps.spec.ts
new file mode 100644
index 000000000..316a16727
--- /dev/null
+++ b/packages/e2e/webwallet/src/specs/dapps.spec.ts
@@ -0,0 +1,30 @@
+import test from "../test"
+import config from "../../../shared/config"
+
+test.describe(`Dapps`, () => {
+ test("Create wallet from Dapp", async ({ webWallet, browserContext }) => {
+ const email = webWallet.generateEmail()
+ const credentials = {
+ email,
+ pin: config.validLogin.pin,
+ password: config.validLogin.password,
+ }
+
+ await webWallet.dapps.requestConnectionFromDapp({
+ browserContext,
+ dappUrl: "https://dapp-argentlabs.vercel.app",
+ credentials,
+ newAccount: true,
+ })
+ await webWallet.login.success(credentials)
+ })
+
+ test("Connect from Dapp", async ({ webWallet, browserContext }) => {
+ await webWallet.dapps.requestConnectionFromDapp({
+ browserContext,
+ dappUrl: "https://dapp-argentlabs.vercel.app",
+ credentials: config.validLogin,
+ newAccount: false,
+ })
+ })
+})
diff --git a/packages/e2e/webwallet/src/specs/login.spec.ts b/packages/e2e/webwallet/src/specs/login.spec.ts
index a14ccfa0c..86a3e9ce2 100644
--- a/packages/e2e/webwallet/src/specs/login.spec.ts
+++ b/packages/e2e/webwallet/src/specs/login.spec.ts
@@ -1,9 +1,20 @@
import { expect } from "@playwright/test"
-import config from "../config"
+import config from "../../../shared/config"
import test from "../test"
+import { generateEmail } from "../page-objects/WebWalletPage"
test.describe(`Login page`, () => {
+ test("create new wallet", async ({ webWallet }) => {
+ await webWallet.login.createWallet({
+ email: generateEmail(),
+ pin: config.validLogin.pin,
+ password: config.validLogin.password,
+ })
+ await webWallet.navigation.backupPassword.click()
+ await expect(webWallet.navigation.backupPassword).not.toBeVisible()
+ })
+
test("can log in", async ({ webWallet }) => {
await webWallet.login.success()
})
diff --git a/packages/e2e/webwallet/src/test.ts b/packages/e2e/webwallet/src/test.ts
index b6753f254..5d2e57b53 100644
--- a/packages/e2e/webwallet/src/test.ts
+++ b/packages/e2e/webwallet/src/test.ts
@@ -1,48 +1,26 @@
-import * as fs from "fs"
-import path from "path"
+import {
+ artifactsDir,
+ isKeepArtifacts,
+ keepVideos,
+ saveHtml,
+} from "../../shared/cfg/test"
-import { Browser, Page, TestInfo, test as testBase } from "@playwright/test"
+import {
+ BrowserContext,
+ Browser,
+ TestInfo,
+ test as testBase,
+} from "@playwright/test"
-import config from "./config"
+import config from "../../shared/config"
import { TestPages } from "./fixtures"
import WebWalletPage from "./page-objects/WebWalletPage"
-const keepArtifacts = async (testInfo: TestInfo, page: Page) => {
- if (
- testInfo.config.preserveOutput === "always" ||
- (testInfo.config.preserveOutput === "failures-only" &&
- testInfo.status !== "passed")
- ) {
- //save HTML
- const folder = testInfo.title.replace(/\s+/g, "_").replace(/\W/g, "")
- const filename = `${testInfo.retry}-${testInfo.status}-${pageId}-${testInfo.workerIndex}.html`
- try {
- const htmlContent = await page.content()
- await fs.promises
- .mkdir(path.resolve(config.artifactsDir, folder), { recursive: true })
- .catch((error) => {
- console.error(error)
- })
- await fs.promises
- .writeFile(
- path.resolve(config.artifactsDir, folder, filename),
- htmlContent,
- )
- .catch((error) => {
- console.error(error)
- })
- } catch (error) {
- console.error("Error while saving HTML content", error)
- }
- }
-}
-let pageId = 0
+let browserCtx: BrowserContext
async function createContext({
browser,
baseURL,
- name,
- testInfo,
}: {
browser: Browser
baseURL: string
@@ -52,54 +30,16 @@ async function createContext({
const context = await browser.newContext({
ignoreHTTPSErrors: true,
acceptDownloads: true,
- recordVideo: process.env.CI
- ? {
- dir: config.artifactsDir,
- size: {
- width: 1366,
- height: 768,
- },
- }
- : undefined,
+ recordVideo: {
+ dir: artifactsDir,
+ size: {
+ width: 1366,
+ height: 768,
+ },
+ },
baseURL,
viewport: { width: 1366, height: 768 },
})
- context.on("page", async (page) => {
- page.on("load", async (page) => {
- try {
- await page.title()
- } catch (err) {
- console.warn(err)
- }
- })
-
- page.on("close", async (page) => {
- if (
- testInfo.config.preserveOutput === "always" ||
- (testInfo.config.preserveOutput === "failures-only" &&
- testInfo.status === "failed") ||
- testInfo.status === "timedOut"
- ) {
- const folder = testInfo.title.replace(/\s+/g, "_").replace(/\W/g, "")
- const filename = `${testInfo.retry}-${name}-${
- testInfo.status
- }-${pageId++}-${testInfo.workerIndex}.webm`
-
- await page
- .video()
- ?.saveAs(path.resolve(config.artifactsDir, folder, filename))
- .catch((error) => {
- console.error(error)
- })
- }
- page
- .video()
- ?.delete()
- .catch((error) => {
- console.error(error)
- })
- })
- })
await context.addInitScript("window.PLAYWRIGHT = true;")
return context
@@ -123,13 +63,27 @@ function createPage() {
const webWalletPage = new WebWalletPage(page)
await webWalletPage.open()
- await keepArtifacts(testInfo, page)
+ browserCtx = context
await use(webWalletPage)
- await context.close()
+ const keepArtifacts = isKeepArtifacts(testInfo)
+ if (keepArtifacts) {
+ await saveHtml(testInfo, page, "WebWallet")
+ await context.close()
+ await keepVideos(testInfo, page, "WebWallet")
+ } else {
+ await context.close()
+ }
}
}
+function getContext() {
+ return async ({}, use: any, _testInfo: TestInfo) => {
+ await use(browserCtx)
+ }
+}
+
const test = testBase.extend({
webWallet: createPage(),
+ browserContext: getContext(),
})
export default test
diff --git a/packages/extension/.env.example b/packages/extension/.env.example
index 4cae5919e..2f96e0482 100644
--- a/packages/extension/.env.example
+++ b/packages/extension/.env.example
@@ -3,8 +3,10 @@ SENTRY_AUTH_TOKEN=
RAMP_API_KEY=
ARGENT_API_BASE_URL=
ARGENT_X_STATUS_URL=
+ARGENT_X_NEWS_URL=
ARGENT_X_ENVIRONMENT=
ARGENT_TESTNET_RPC_URL=
+NEW_CAIRO_0_ENABLED=
# difference between commented and not commented variables is that the release CI will throw when a not commented env var is missing
# this is used to ensure the ci release has everything we expect it to have without doing explicit testing of the result
@@ -25,4 +27,5 @@ ARGENT_TESTNET_RPC_URL=
FAST=20
MEDIUM=60
SLOW=60*5
-VERY_SLOW=24*60*60
\ No newline at end of file
+VERY_SLOW=24*60*60
+ARGENT_HEALTHCHECK_BASE_URL=https://healthcheck.hydrogen.argent47.net
\ No newline at end of file
diff --git a/packages/extension/CHANGELOG.md b/packages/extension/CHANGELOG.md
index 085fcb817..3bbca7b10 100644
--- a/packages/extension/CHANGELOG.md
+++ b/packages/extension/CHANGELOG.md
@@ -1,5 +1,35 @@
# @argent-x/extension
+## 6.13.4
+
+### Patch Changes
+
+- e871c0954: Release
+
+## 6.13.3
+
+### Patch Changes
+
+- 6b1326d64: Release
+
+## 6.13.2
+
+### Patch Changes
+
+- 0e774f90f: Release
+
+## 6.13.1
+
+### Patch Changes
+
+- d89a36f73: Release
+
+## 6.13.0
+
+### Minor Changes
+
+- 914e376fb: Release
+
## 6.12.8
### Patch Changes
diff --git a/packages/extension/manifest/v2.json b/packages/extension/manifest/v2.json
index 0a3b2b561..aded2d052 100644
--- a/packages/extension/manifest/v2.json
+++ b/packages/extension/manifest/v2.json
@@ -1,8 +1,8 @@
{
"$schema": "https://json.schemastore.org/chrome-manifest.json",
- "name": "Argent X",
- "description": "The security of Ethereum with the scale of StarkNet",
- "version": "5.12.8",
+ "name": "Argent X - Starknet Wallet",
+ "description": "7 out of 10 Starknet users choose Argent X as their Starknet wallet. Join 2m+ Argent users now.",
+ "version": "5.13.4",
"manifest_version": 2,
"browser_action": {
"default_icon": {
diff --git a/packages/extension/manifest/v3.json b/packages/extension/manifest/v3.json
index c709d536a..ddec98bfc 100644
--- a/packages/extension/manifest/v3.json
+++ b/packages/extension/manifest/v3.json
@@ -1,8 +1,8 @@
{
"$schema": "https://json.schemastore.org/chrome-manifest.json",
- "name": "Argent X",
- "description": "The security of Ethereum with the scale of StarkNet",
- "version": "5.12.8",
+ "name": "Argent X - Starknet Wallet",
+ "description": "7 out of 10 Starknet users choose Argent X as their Starknet wallet. Join 2m+ Argent users now.",
+ "version": "5.13.4",
"manifest_version": 3,
"action": {
"default_icon": {
diff --git a/packages/extension/package.json b/packages/extension/package.json
index 39f1901a5..c788c90ea 100644
--- a/packages/extension/package.json
+++ b/packages/extension/package.json
@@ -1,6 +1,6 @@
{
"name": "@argent-x/extension",
- "version": "6.12.8",
+ "version": "6.13.4",
"main": "index.js",
"private": true,
"license": "MIT",
@@ -10,7 +10,7 @@
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^14.0.0",
"@types/async-retry": "^1.4.5",
- "@types/chrome": "^0.0.254",
+ "@types/chrome": "^0.0.256",
"@types/fs-extra": "^11.0.1",
"@types/lodash-es": "^4.17.6",
"@types/object-hash": "^3.0.2",
@@ -25,13 +25,13 @@
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react-swc": "^3.3.1",
- "@vitest/browser": "1.0.2",
+ "@vitest/browser": "1.2.0",
"@vitest/coverage-istanbul": "^1.0.0",
- "@vitest/coverage-v8": "1.0.2",
- "@vitest/ui": "1.0.2",
+ "@vitest/coverage-v8": "1.2.0",
+ "@vitest/ui": "1.2.0",
"chokidar": "^3.5.2",
"concurrently": "^8.0.1",
- "copy-webpack-plugin": "^11.0.0",
+ "copy-webpack-plugin": "^12.0.0",
"cross-fetch": "^4.0.0",
"dotenv": "^16.1.4",
"dotenv-webpack": "^8.0.0",
@@ -43,7 +43,7 @@
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^9.0.0",
"fs-extra": "^11.1.1",
- "happy-dom": "^12.10.3",
+ "happy-dom": "^13.0.0",
"html-webpack-plugin": "^5.5.0",
"isomorphic-fetch": "^3.0.0",
"minimatch": "^9.0.1",
@@ -55,7 +55,7 @@
"typescript-styled-plugin": "^0.18.2",
"url-loader": "^4.1.1",
"vite": "^5.0.0",
- "vitest": "1.0.2",
+ "vitest": "1.2.0",
"wait-for-expect": "^3.0.2",
"webpack": "^5.88.0",
"webpack-cli": "^5.1.1",
@@ -127,7 +127,8 @@
"react-select": "^5.4.0",
"react-textarea-autosize": "^8.3.4",
"semver": "^7.5.2",
- "starknet": "5.24.3",
+ "starknet": "5.25.0",
+ "starknet6": "npm:starknet@6.0.0-beta.11",
"starknet4": "npm:starknet@4.22.0",
"starknet4-deprecated": "npm:starknet@4.4.0",
"styled-components": "^5.3.5",
diff --git a/packages/extension/scripts/export.ts b/packages/extension/scripts/export.ts
index 35df97c5c..2994b99d6 100644
--- a/packages/extension/scripts/export.ts
+++ b/packages/extension/scripts/export.ts
@@ -36,8 +36,6 @@ const exclude = [
"**.log**",
"**/node_modules**",
"packages/dapp**",
- "packages/get-starknet**",
- "packages/starknet-react-webwallet-connector**",
"packages/web**",
]
diff --git a/packages/extension/src/assets/default-tokens.json b/packages/extension/src/assets/default-tokens.json
index 90aee589b..1c3db1657 100644
--- a/packages/extension/src/assets/default-tokens.json
+++ b/packages/extension/src/assets/default-tokens.json
@@ -19,6 +19,17 @@
"iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/eth.png",
"showAlways": true
},
+
+ {
+ "id": 1,
+ "address": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
+ "name": "Ether",
+ "symbol": "ETH",
+ "decimals": "18",
+ "network": "sepolia-alpha",
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/eth.png",
+ "showAlways": true
+ },
{
"id": 1,
"address": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
@@ -42,10 +53,41 @@
{
"id": 2,
"address": "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d",
- "name": "Stark",
+ "name": "Starknet",
+ "symbol": "STRK",
+ "decimals": "18",
+ "network": "mainnet-alpha",
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/strk.png",
+ "showAlways": true
+ },
+ {
+ "id": 2,
+ "address": "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d",
+ "name": "Starknet",
"symbol": "STRK",
"decimals": "18",
"network": "integration",
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/strk.png",
+ "showAlways": true
+ },
+ {
+ "id": 2,
+ "address": "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d",
+ "name": "Starknet",
+ "symbol": "STRK",
+ "decimals": "18",
+ "network": "goerli-alpha",
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/strk.png",
+ "showAlways": true
+ },
+ {
+ "id": 2,
+ "address": "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d",
+ "name": "Starknet",
+ "symbol": "STRK",
+ "decimals": "18",
+ "network": "sepolia-alpha",
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/strk.png",
"showAlways": true
},
{
@@ -66,6 +108,15 @@
"network": "goerli-alpha",
"iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/dai.png"
},
+ {
+ "id": 6,
+ "address": "0x03e85bfbb8e2a42b7bead9e88e9a1b19dbccf661471061807292120462396ec9",
+ "name": "DAI",
+ "symbol": "DAI",
+ "decimals": "18",
+ "network": "sepolia-alpha",
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/dai.png"
+ },
{
"id": 4,
"address": "0x03fe2b97c1fd336e750087d68b9b867997fd64a2661ff3ca5a7c771641e8e7ac",
@@ -84,6 +135,15 @@
"network": "goerli-alpha",
"iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/wbtc.png"
},
+ {
+ "id": 4,
+ "address": "0x12d537dc323c439dc65c976fad242d5610d27cfb5f31689a0a319b8be7f3d56",
+ "name": "Wrapped BTC",
+ "symbol": "WBTC",
+ "decimals": "8",
+ "network": "sepolia-alpha",
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/wbtc.png"
+ },
{
"id": 2,
"address": "0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8",
@@ -102,6 +162,15 @@
"network": "goerli-alpha",
"iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/usdc.png"
},
+ {
+ "id": 2,
+ "address": "0x005a643907b9a4bc6a55e9069c4fd5fd1f5c79a22470690f75556c4736e34426",
+ "name": "USD Coin",
+ "symbol": "USDC",
+ "decimals": "6",
+ "network": "sepolia-alpha",
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/usdc.png"
+ },
{
"id": 3,
"address": "0x068f5c6a61780768455de69077e07e89787839bf8166decfbf92b645209c0fb8",
@@ -119,5 +188,14 @@
"decimals": "6",
"network": "goerli-alpha",
"iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/usdt.png"
+ },
+ {
+ "id": 3,
+ "address": "0x386e8d061177f19b3b485c20e31137e6f6bc497cc635ccdfcab96fadf5add6a",
+ "name": "Tether USD",
+ "symbol": "USDT",
+ "decimals": "6",
+ "network": "sepolia-alpha",
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/usdt.png"
}
]
diff --git a/packages/extension/src/assets/vSTRK.json b/packages/extension/src/assets/vSTRK.json
new file mode 100644
index 000000000..87b58286d
--- /dev/null
+++ b/packages/extension/src/assets/vSTRK.json
@@ -0,0 +1,8 @@
+{
+ "address": "0x01a881a75bb478cedfd4d3ea19d2a4564350d78ea463a5287833526a416d5e31",
+ "name": "vSTRK",
+ "symbol": "STRK",
+ "decimals": 18,
+ "network": "mainnet-alpha",
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/strk.png"
+}
diff --git a/packages/extension/src/background/__new/procedures/accountMessaging/cancelEscape.ts b/packages/extension/src/background/__new/procedures/accountMessaging/cancelEscape.ts
index 29e18fcfa..d56f24fe0 100644
--- a/packages/extension/src/background/__new/procedures/accountMessaging/cancelEscape.ts
+++ b/packages/extension/src/background/__new/procedures/accountMessaging/cancelEscape.ts
@@ -2,7 +2,6 @@ import { z } from "zod"
import { extensionOnlyProcedure } from "../permissions"
import { baseWalletAccountSchema } from "../../../../shared/wallet.model"
-import { Account } from "starknet"
import { getEntryPointSafe } from "../../../../shared/utils/transactions"
import { AccountMessagingError } from "../../../../shared/errors/accountMessaging"
@@ -20,8 +19,8 @@ export const cancelEscapeProcedure = extensionOnlyProcedure
},
}) => {
try {
- const starknetAccount =
- (await wallet.getSelectedStarknetAccount()) as Account // Old accounts are not supported
+ const starknetAccount = await wallet.getSelectedStarknetAccount()
+
await actionService.add(
{
type: "TRANSACTION",
diff --git a/packages/extension/src/background/__new/procedures/accountMessaging/changeGuardian.ts b/packages/extension/src/background/__new/procedures/accountMessaging/changeGuardian.ts
index bc127e5f2..2d2adc420 100644
--- a/packages/extension/src/background/__new/procedures/accountMessaging/changeGuardian.ts
+++ b/packages/extension/src/background/__new/procedures/accountMessaging/changeGuardian.ts
@@ -2,7 +2,7 @@ import { z } from "zod"
import { extensionOnlyProcedure } from "../permissions"
import { baseWalletAccountSchema } from "../../../../shared/wallet.model"
-import { constants, num, Account } from "starknet"
+import { constants, num } from "starknet"
import { getEntryPointSafe } from "../../../../shared/utils/transactions"
import { AccountMessagingError } from "../../../../shared/errors/accountMessaging"
import { changeGuardianCalldataSchema } from "@argent/shared"
@@ -23,8 +23,8 @@ export const changeGuardianProcedure = extensionOnlyProcedure
}) => {
try {
const newGuardian = num.hexToDecimalString(guardian)
- const starknetAccount =
- (await wallet.getSelectedStarknetAccount()) as Account // Old accounts are not supported
+ const starknetAccount = await wallet.getSelectedStarknetAccount()
+
const isRemoveGuardian = num.toBigInt(newGuardian) === constants.ZERO
await actionService.add(
{
@@ -48,7 +48,6 @@ export const changeGuardianProcedure = extensionOnlyProcedure
},
},
{
- origin,
title: isRemoveGuardian
? "Remove Argent Shield"
: "Add Argent Shield",
diff --git a/packages/extension/src/background/__new/procedures/accountMessaging/escapeAndChangeGuardian.ts b/packages/extension/src/background/__new/procedures/accountMessaging/escapeAndChangeGuardian.ts
index e76542c84..51a931b7c 100644
--- a/packages/extension/src/background/__new/procedures/accountMessaging/escapeAndChangeGuardian.ts
+++ b/packages/extension/src/background/__new/procedures/accountMessaging/escapeAndChangeGuardian.ts
@@ -2,7 +2,7 @@ import { z } from "zod"
import { extensionOnlyProcedure } from "../permissions"
import { baseWalletAccountSchema } from "../../../../shared/wallet.model"
-import { constants, num, Account } from "starknet"
+import { constants, num } from "starknet"
import { getEntryPointSafe } from "../../../../shared/utils/transactions"
import { AccountMessagingError } from "../../../../shared/errors/accountMessaging"
import { AccountError } from "../../../../shared/errors/account"
@@ -34,8 +34,7 @@ export const escapeAndChangeGuardianProcedure = extensionOnlyProcedure
*/
const selectedAccount = await wallet.getAccount(account)
- const starknetAccount =
- (await wallet.getSelectedStarknetAccount()) as Account // Old accounts are not supported
+ const starknetAccount = await wallet.getSelectedStarknetAccount()
if (!selectedAccount) {
throw new AccountError({
diff --git a/packages/extension/src/background/__new/procedures/accountMessaging/getAccountDeploymentPayload.ts b/packages/extension/src/background/__new/procedures/accountMessaging/getAccountDeploymentPayload.ts
index 5236a326c..39dc2a950 100644
--- a/packages/extension/src/background/__new/procedures/accountMessaging/getAccountDeploymentPayload.ts
+++ b/packages/extension/src/background/__new/procedures/accountMessaging/getAccountDeploymentPayload.ts
@@ -1,7 +1,10 @@
import { z } from "zod"
import { connectedDappsProcedure } from "../permissions"
-import { baseWalletAccountSchema } from "../../../../shared/wallet.model"
+import {
+ baseWalletAccountSchema,
+ cairoVersionSchema,
+} from "../../../../shared/wallet.model"
import { AccountMessagingError } from "../../../../shared/errors/accountMessaging"
import { SessionError } from "../../../../shared/errors/session"
import { AccountError } from "../../../../shared/errors/account"
@@ -17,12 +20,15 @@ const getAccountDeploymentPayloadInputSchema = z
})
.optional()
-const deployAccountContractSchema = z.object({
- classHash: z.string(),
- constructorCalldata: rawArgsSchema,
- addressSalt: bigNumberishSchema.optional(),
- contractAddress: addressSchema.optional(),
-})
+const deployAccountContractSchema = z
+ .object({
+ classHash: z.string(),
+ constructorCalldata: rawArgsSchema,
+ addressSalt: bigNumberishSchema.optional(),
+ contractAddress: addressSchema.optional(),
+ version: cairoVersionSchema.optional(),
+ })
+ .or(z.null())
export const getAccountDeploymentPayloadProcedure = connectedDappsProcedure
.input(getAccountDeploymentPayloadInputSchema)
@@ -32,6 +38,7 @@ export const getAccountDeploymentPayloadProcedure = connectedDappsProcedure
input,
ctx: {
services: { wallet },
+ senderType,
},
}) => {
if (!(await wallet.isSessionOpen())) {
@@ -49,11 +56,15 @@ export const getAccountDeploymentPayloadProcedure = connectedDappsProcedure
})
}
+ if (senderType !== "extension" && walletAccount.needsDeploy === false) {
+ return null
+ }
+
return await wallet.getAccountDeploymentPayload(walletAccount)
} catch (e) {
throw new AccountMessagingError({
options: { error: e },
- code: "GET_ENCRYPTED_KEY_FAILED",
+ code: "ACCOUNT_DEPLOYMENT_PAYLOAD_FAILED",
})
}
},
diff --git a/packages/extension/src/background/__new/procedures/accountMessaging/triggerEscapeGuardian.ts b/packages/extension/src/background/__new/procedures/accountMessaging/triggerEscapeGuardian.ts
index 9641c5509..28e93aa5b 100644
--- a/packages/extension/src/background/__new/procedures/accountMessaging/triggerEscapeGuardian.ts
+++ b/packages/extension/src/background/__new/procedures/accountMessaging/triggerEscapeGuardian.ts
@@ -1,5 +1,4 @@
import { z } from "zod"
-import { Account } from "starknet"
import { extensionOnlyProcedure } from "../permissions"
import { baseWalletAccountSchema } from "../../../../shared/wallet.model"
@@ -20,8 +19,8 @@ export const triggerEscapeGuardianProcedure = extensionOnlyProcedure
},
}) => {
try {
- const starknetAccount =
- (await wallet.getSelectedStarknetAccount()) as Account // Old accounts are not supported
+ const starknetAccount = await wallet.getSelectedStarknetAccount()
+
await actionService.add(
{
type: "TRANSACTION",
diff --git a/packages/extension/src/background/__new/procedures/discover/index.ts b/packages/extension/src/background/__new/procedures/discover/index.ts
new file mode 100644
index 000000000..f8375c654
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/discover/index.ts
@@ -0,0 +1,6 @@
+import { router } from "../../trpc"
+import { viewedAtProcedure } from "./viewedAt"
+
+export const discoverRouter = router({
+ viewedAt: viewedAtProcedure,
+})
diff --git a/packages/extension/src/background/__new/procedures/discover/viewedAt.ts b/packages/extension/src/background/__new/procedures/discover/viewedAt.ts
new file mode 100644
index 000000000..f8ce79298
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/discover/viewedAt.ts
@@ -0,0 +1,12 @@
+import { z } from "zod"
+
+import { openSessionMiddleware } from "../../middleware/session"
+import { discoverService } from "../../services/discover"
+import { extensionOnlyProcedure } from "../permissions"
+
+export const viewedAtProcedure = extensionOnlyProcedure
+ .use(openSessionMiddleware)
+ .input(z.number())
+ .mutation(async ({ input }) => {
+ return discoverService.setViewedAt(input)
+ })
diff --git a/packages/extension/src/background/__new/procedures/feeToken/avoidFeeToken.ts b/packages/extension/src/background/__new/procedures/feeToken/avoidFeeToken.ts
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/extension/src/background/__new/procedures/feeToken/index.ts b/packages/extension/src/background/__new/procedures/feeToken/index.ts
new file mode 100644
index 000000000..cea3c6fe2
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/feeToken/index.ts
@@ -0,0 +1,6 @@
+import { router } from "../../trpc"
+import { preferFeeTokenProcedure } from "./preferFeeToken"
+
+export const feeTokenRouter = router({
+ preferFeeToken: preferFeeTokenProcedure,
+})
diff --git a/packages/extension/src/background/__new/procedures/feeToken/preferFeeToken.ts b/packages/extension/src/background/__new/procedures/feeToken/preferFeeToken.ts
new file mode 100644
index 000000000..a4b81b147
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/feeToken/preferFeeToken.ts
@@ -0,0 +1,15 @@
+import { addressSchema } from "@argent/shared"
+import { extensionOnlyProcedure } from "../permissions"
+
+export const preferFeeTokenProcedure = extensionOnlyProcedure
+ .input(addressSchema)
+ .mutation(
+ ({
+ input: feeTokenAddress,
+ ctx: {
+ services: { feeTokenService },
+ },
+ }) => {
+ return feeTokenService.preferFeeToken(feeTokenAddress)
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/permissions.ts b/packages/extension/src/background/__new/procedures/permissions.ts
index aed4dec9e..b0893ed5d 100644
--- a/packages/extension/src/background/__new/procedures/permissions.ts
+++ b/packages/extension/src/background/__new/procedures/permissions.ts
@@ -48,10 +48,15 @@ export const connectedDappsProcedure = publicProcedure.use(
})
}
+ const senderType = isPreauthorized
+ ? ("preauthorized" as const)
+ : ("extension" as const)
+
return next({
ctx: {
...ctx,
sender, // by passing it after checking, every method after this middleware will have a mandatory sender
+ senderType,
},
})
},
diff --git a/packages/extension/src/background/__new/procedures/provision/getStatus.ts b/packages/extension/src/background/__new/procedures/provision/getStatus.ts
new file mode 100644
index 000000000..1838994fa
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/provision/getStatus.ts
@@ -0,0 +1,17 @@
+import { openSessionMiddleware } from "../../middleware/session"
+import { extensionOnlyProcedure } from "../permissions"
+import { provisionStatusSchema } from "../../../../shared/provision/types"
+
+export const getStatusProcedure = extensionOnlyProcedure
+ .use(openSessionMiddleware)
+ .output(provisionStatusSchema)
+ .query(
+ async ({
+ ctx: {
+ services: { provisionService },
+ },
+ }) => {
+ const response = await provisionService.getStatus()
+ return response
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/provision/index.ts b/packages/extension/src/background/__new/procedures/provision/index.ts
new file mode 100644
index 000000000..b653912da
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/provision/index.ts
@@ -0,0 +1,6 @@
+import { router } from "../../trpc"
+import { getStatusProcedure } from "./getStatus"
+
+export const provisionRouter = router({
+ getStatus: getStatusProcedure,
+})
diff --git a/packages/extension/src/background/__new/procedures/recovery/clearErrorRecovering.ts b/packages/extension/src/background/__new/procedures/recovery/clearErrorRecovering.ts
new file mode 100644
index 000000000..30749018a
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/recovery/clearErrorRecovering.ts
@@ -0,0 +1,11 @@
+import { extensionOnlyProcedure } from "../permissions"
+
+export const clearErrorRecoveringProcedure = extensionOnlyProcedure.mutation(
+ async ({
+ ctx: {
+ services: { recoveryService },
+ },
+ }) => {
+ return recoveryService.clearErrorRecovering()
+ },
+)
diff --git a/packages/extension/src/background/__new/procedures/recovery/index.ts b/packages/extension/src/background/__new/procedures/recovery/index.ts
index e9acca882..88d5e228e 100644
--- a/packages/extension/src/background/__new/procedures/recovery/index.ts
+++ b/packages/extension/src/background/__new/procedures/recovery/index.ts
@@ -1,8 +1,10 @@
import { router } from "../../trpc"
+import { clearErrorRecoveringProcedure } from "./clearErrorRecovering"
import { recoverBackupProcedure } from "./recoverBackup"
import { recoverSeedphraseProcedure } from "./recoverSeedphrase"
export const recoveryRouter = router({
recoverBackup: recoverBackupProcedure,
recoverSeedPhrase: recoverSeedphraseProcedure,
+ clearErrorRecovering: clearErrorRecoveringProcedure,
})
diff --git a/packages/extension/src/background/__new/procedures/riskAssessment/assessRisk.ts b/packages/extension/src/background/__new/procedures/riskAssessment/assessRisk.ts
new file mode 100644
index 000000000..4cb669cc9
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/riskAssessment/assessRisk.ts
@@ -0,0 +1,15 @@
+import { extensionOnlyProcedure } from "../permissions"
+import { dappContextSchema } from "../../../../shared/riskAssessment/interface"
+
+export const assessRiskProcedure = extensionOnlyProcedure
+ .input(dappContextSchema)
+ .query(
+ async ({
+ input: dappContext,
+ ctx: {
+ services: { riskAssessmentService },
+ },
+ }) => {
+ return riskAssessmentService.assessRisk({ dappContext })
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/riskAssessment/index.ts b/packages/extension/src/background/__new/procedures/riskAssessment/index.ts
new file mode 100644
index 000000000..c90af3b40
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/riskAssessment/index.ts
@@ -0,0 +1,6 @@
+import { router } from "../../trpc"
+import { assessRiskProcedure } from "./assessRisk"
+
+export const riskAssessmentRouter = router({
+ assessRisk: assessRiskProcedure,
+})
diff --git a/packages/extension/src/background/__new/procedures/tokens/addToken.ts b/packages/extension/src/background/__new/procedures/tokens/addToken.ts
index e89c02ef7..7b85d6d88 100644
--- a/packages/extension/src/background/__new/procedures/tokens/addToken.ts
+++ b/packages/extension/src/background/__new/procedures/tokens/addToken.ts
@@ -1,10 +1,16 @@
import { extensionOnlyProcedure } from "../permissions"
import { TokenSchema } from "../../../../shared/token/__new/types/token.model"
-import { tokenService } from "../../../../shared/token/__new/service"
export const addTokenProcedure = extensionOnlyProcedure
.input(TokenSchema)
- .mutation(async ({ input: token }) => {
- // tokens that are added from the UI should never be shown with custom flag, even if the balance is 0
- return await tokenService.addToken({ ...token, custom: true })
- })
+ .mutation(
+ async ({
+ input: token,
+ ctx: {
+ services: { tokenService },
+ },
+ }) => {
+ // tokens that are added from the UI should never be shown with custom flag, even if the balance is 0
+ return await tokenService.addToken({ ...token, custom: true })
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/transactionEstimate/accountDeploy.ts b/packages/extension/src/background/__new/procedures/transactionEstimate/accountDeploy.ts
index 07ea0f4c2..947174261 100644
--- a/packages/extension/src/background/__new/procedures/transactionEstimate/accountDeploy.ts
+++ b/packages/extension/src/background/__new/procedures/transactionEstimate/accountDeploy.ts
@@ -29,7 +29,7 @@ export const estimateAccountDeployProcedure = extensionOnlyProcedure
: await wallet.getSelectedAccount()
if (!account) {
- throw Error("no accounts")
+ throw new AccountError({ code: "NOT_FOUND" })
}
try {
diff --git a/packages/extension/src/background/__new/procedures/transactionEstimate/estimate.ts b/packages/extension/src/background/__new/procedures/transactionEstimate/estimate.ts
index dc3e2ab8a..c5e48b275 100644
--- a/packages/extension/src/background/__new/procedures/transactionEstimate/estimate.ts
+++ b/packages/extension/src/background/__new/procedures/transactionEstimate/estimate.ts
@@ -11,6 +11,7 @@ import {
} from "./helpers"
import { estimatedFeesSchema } from "../../../../shared/transactionSimulation/fees/fees.model"
import { AccountError } from "../../../../shared/errors/account"
+import { getTxVersionFromFeeToken } from "../../../../shared/utils/getTransactionVersion"
const estimateRequestSchema = z.object({
account: baseWalletAccountSchema,
@@ -51,21 +52,15 @@ export const estimateTransactionProcedure = extensionOnlyProcedure
wallet,
)
- // TODO: consider selected fee token
+ const version = getTxVersionFromFeeToken(feeTokenAddress)
+
const estimatedFees = await snAccount.estimateFeeBulk(allInvocations, {
- skipValidate: true,
+ // skipValidate is true by default
+ version,
})
try {
- const aggregatedResponse = estimatedFeesToResponse(
- estimatedFees,
- allInvocations,
- )
-
- return {
- ...aggregatedResponse,
- feeTokenAddress,
- }
+ return estimatedFeesToResponse(estimatedFees, feeTokenAddress)
} catch (e) {
throw new AccountError({
code: "CANNOT_ESTIMATE_TRANSACTIONS",
diff --git a/packages/extension/src/background/__new/procedures/transactionEstimate/helpers.ts b/packages/extension/src/background/__new/procedures/transactionEstimate/helpers.ts
index c0cd6df08..84d990f3f 100644
--- a/packages/extension/src/background/__new/procedures/transactionEstimate/helpers.ts
+++ b/packages/extension/src/background/__new/procedures/transactionEstimate/helpers.ts
@@ -4,12 +4,12 @@ import {
Invocations,
ProviderInterface,
TransactionType,
-} from "starknet"
+} from "starknet6"
import { isAccountDeployed } from "../../../accountDeploy"
import type { EstimatedFees } from "../../../../shared/transactionSimulation/fees/fees.model"
import type { WalletAccount } from "../../../../shared/wallet.model"
import type { Wallet } from "../../../wallet"
-import { ETH_TOKEN_ADDRESS } from "../../../../shared/network/constants"
+import type { Address } from "@argent/shared"
type Invocation = Invocations[number]
@@ -22,20 +22,15 @@ export function callsToInvocation(calls: Call[]): Invocation {
export function estimatedFeesToResponse(
estimatedFees: EstimateFeeBulk,
- invocations: Invocations,
+ feeTokenAddress: Address,
): EstimatedFees {
- // check length is same
- if (estimatedFees.length !== invocations.length) {
- throw new Error("estimatedFeesToResponse: length mismatch")
- }
-
if (estimatedFees.length !== 1 && estimatedFees.length !== 2) {
throw new Error("estimatedFeesToResponse: length must be 1 or 2")
}
const fees: EstimatedFees = {
transactions: {
- feeTokenAddress: ETH_TOKEN_ADDRESS,
+ feeTokenAddress,
amount: 0n,
pricePerUnit: 0n,
},
@@ -50,7 +45,7 @@ export function estimatedFeesToResponse(
}
fees.deployment = {
- feeTokenAddress: ETH_TOKEN_ADDRESS,
+ feeTokenAddress,
amount: gas_consumed,
pricePerUnit: gas_price,
}
@@ -66,7 +61,7 @@ export function estimatedFeesToResponse(
}
fees.transactions = {
- feeTokenAddress: ETH_TOKEN_ADDRESS,
+ feeTokenAddress,
amount: gas_consumed,
pricePerUnit: gas_price,
}
diff --git a/packages/extension/src/background/__new/procedures/transactionReview/simulateAndReview.ts b/packages/extension/src/background/__new/procedures/transactionReview/simulateAndReview.ts
index 7758e921d..14ed57ea3 100644
--- a/packages/extension/src/background/__new/procedures/transactionReview/simulateAndReview.ts
+++ b/packages/extension/src/background/__new/procedures/transactionReview/simulateAndReview.ts
@@ -4,8 +4,12 @@ import { openSessionMiddleware } from "../../middleware/session"
import { extensionOnlyProcedure } from "../permissions"
import { transactionReviewTransactionsSchema } from "../../../../shared/transactionReview/interface"
import { enrichedSimulateAndReviewSchema } from "../../../../shared/transactionReview/schema"
+import { addressSchema } from "@argent/shared"
-const simulateAndReviewSchema = z.array(transactionReviewTransactionsSchema)
+const simulateAndReviewSchema = z.object({
+ feeTokenAddress: addressSchema,
+ transactions: z.array(transactionReviewTransactionsSchema),
+})
export const simulateAndReviewProcedure = extensionOnlyProcedure
.use(openSessionMiddleware)
@@ -13,7 +17,5 @@ export const simulateAndReviewProcedure = extensionOnlyProcedure
.output(enrichedSimulateAndReviewSchema)
.query(async ({ input, ctx: { services } }) => {
const { transactionReviewService } = services
- return transactionReviewService.simulateAndReview({
- transactions: input,
- })
+ return transactionReviewService.simulateAndReview(input)
})
diff --git a/packages/extension/src/background/__new/procedures/transfer/send.ts b/packages/extension/src/background/__new/procedures/transfer/send.ts
index 56c64fa0b..0cd43e775 100644
--- a/packages/extension/src/background/__new/procedures/transfer/send.ts
+++ b/packages/extension/src/background/__new/procedures/transfer/send.ts
@@ -11,6 +11,7 @@ const sendSchema = z.object({
}),
title: z.string(),
subtitle: z.string().optional(),
+ isMaxSend: z.boolean().optional(),
})
export const sendProcedure = extensionOnlyProcedure
@@ -18,7 +19,7 @@ export const sendProcedure = extensionOnlyProcedure
.output(z.string())
.mutation(
async ({
- input: { transactions, title, subtitle },
+ input: { transactions, title, subtitle, isMaxSend },
ctx: {
services: { actionService },
},
@@ -28,6 +29,9 @@ export const sendProcedure = extensionOnlyProcedure
type: "TRANSACTION",
payload: {
transactions,
+ meta: {
+ isMaxSend,
+ },
},
},
{
diff --git a/packages/extension/src/background/__new/router.ts b/packages/extension/src/background/__new/router.ts
index 19dfec54f..d4c8d815f 100644
--- a/packages/extension/src/background/__new/router.ts
+++ b/packages/extension/src/background/__new/router.ts
@@ -26,6 +26,14 @@ import { backgroundStarknetAddressService } from "./services/address"
import { networkService } from "../../shared/network/service"
import { sharedSwapService } from "../../shared/swap/service"
import { transactionEstimateRouter } from "./procedures/transactionEstimate"
+import { tokenService } from "../../shared/token/__new/service"
+import { riskAssessmentRouter } from "./procedures/riskAssessment"
+import { riskAssessmentService } from "./services/riskAssessment"
+import { feeTokenService } from "../../shared/feeToken/service"
+import { feeTokenRouter } from "./procedures/feeToken"
+import { provisionRouter } from "./procedures/provision"
+import { provisionService } from "./services/provision"
+import { discoverRouter } from "./procedures/discover"
const appRouter = router({
account: accountRouter,
@@ -43,6 +51,10 @@ const appRouter = router({
transactionReview: transactionReviewRouter,
transfer: transferRouter,
udc: udcRouter,
+ riskAssessment: riskAssessmentRouter,
+ feeToken: feeTokenRouter,
+ provision: provisionRouter,
+ discover: discoverRouter,
})
export type AppRouter = typeof appRouter
@@ -62,7 +74,11 @@ createChromeHandler({
transactionReviewService: backgroundTransactionReviewService,
swapService: sharedSwapService,
starknetAddressService: backgroundStarknetAddressService,
+ tokenService,
+ feeTokenService,
networkService,
+ riskAssessmentService,
+ provisionService,
},
}),
})
diff --git a/packages/extension/src/background/__new/services/account/worker/implementation.test.ts b/packages/extension/src/background/__new/services/account/worker/implementation.test.ts
index 2be14d9d8..cae33e6e1 100644
--- a/packages/extension/src/background/__new/services/account/worker/implementation.test.ts
+++ b/packages/extension/src/background/__new/services/account/worker/implementation.test.ts
@@ -1,14 +1,11 @@
import { Mocked, describe, expect, it, vi } from "vitest"
import { addressSchema } from "@argent/shared"
-import { noop } from "lodash-es"
import { getMockWalletAccount } from "../../../../../../test/walletAccount.mock"
import { IAccountService } from "../../../../../shared/account/service/interface"
-import { IDebounceService } from "../../../../../shared/debounce"
-import { STANDARD_ACCOUNT_CLASS_HASH } from "../../../../../shared/network/constants"
+import { TXV1_ACCOUNT_CLASS_HASH } from "../../../../../shared/network/constants"
import { IScheduleService } from "../../../../../shared/schedule/interface"
import { IActivityService } from "../../activity/interface"
-import { IBackgroundUIService } from "../../ui/interface"
import { AccountWorker } from "./implementation"
vi.mock("../../../../../shared/account/details/getAccountCairoVersionFromChain")
@@ -43,16 +40,10 @@ describe("AccountWorker", () => {
},
} as unknown as Mocked
- const backgroundUIService = {} as unknown as Mocked
-
- const debounceService = {} as unknown as Mocked
-
const accountWorker = new AccountWorker(
accountService,
activityService,
scheduleService,
- backgroundUIService,
- debounceService,
)
return {
@@ -60,8 +51,6 @@ describe("AccountWorker", () => {
accountService,
activityService,
scheduleService,
- backgroundUIService,
- debounceService,
}
}
@@ -133,16 +122,12 @@ describe("AccountWorker", () => {
accountService.get = vi.fn().mockResolvedValueOnce([mockAccount])
- worker.updateAccountClassHashImmediately = vi
- .fn()
- .mockImplementationOnce(noop)
-
await worker.updateAccountClassHash()
expect(accountService.upsert).toHaveBeenCalledWith([
{
...mockAccount,
- classHash: addressSchema.parse(STANDARD_ACCOUNT_CLASS_HASH),
+ classHash: addressSchema.parse(TXV1_ACCOUNT_CLASS_HASH),
},
])
})
@@ -166,10 +151,6 @@ describe("AccountWorker", () => {
},
])
- worker.updateAccountClassHashImmediately = vi
- .fn()
- .mockImplementationOnce(noop)
-
await worker.updateAccountCairoVersion()
expect(accountService.upsert).toHaveBeenCalledWith([
diff --git a/packages/extension/src/background/__new/services/account/worker/implementation.ts b/packages/extension/src/background/__new/services/account/worker/implementation.ts
index ecfb84ad6..e331f3b3d 100644
--- a/packages/extension/src/background/__new/services/account/worker/implementation.ts
+++ b/packages/extension/src/background/__new/services/account/worker/implementation.ts
@@ -4,22 +4,22 @@ import { WalletAccount } from "../../../../../shared/wallet.model"
import { getAccountClassHashFromChain } from "../../../../../shared/account/details/getAccountClassHashFromChain"
import { getAccountCairoVersionFromChain } from "../../../../../shared/account/details/getAccountCairoVersionFromChain"
import { IAccountService } from "../../../../../shared/account/service/interface"
-import { isUndefined, keyBy } from "lodash-es"
-import {
- onInstallAndUpgrade,
- onStartup,
-} from "../../worker/schedule/decorators"
+import { keyBy } from "lodash-es"
+import { onInstallAndUpgrade } from "../../worker/schedule/decorators"
import { AllowArray } from "../../../../../shared/storage/__new/interface"
import { pipe } from "../../worker/schedule/pipe"
-import { IBackgroundUIService } from "../../ui/interface"
-import { IDebounceService } from "../../../../../shared/debounce"
import { getOwnerForAccount } from "../../../../../shared/account/details/getOwner"
import {
+ GuardianChangedActivity,
IActivityService,
- SecurityActivityPayload,
+ AccountActivityPayload,
SignerChangedActivity,
+ AccountDeployActivity,
+ ProvisionActivity,
} from "../../activity/interface"
+import { getGuardianForAccount } from "../../../../../shared/account/details/getGuardian"
+import { ProvisionActivityPayload } from "../../../../../shared/activity/types"
export enum AccountUpdaterTaskId {
UPDATE_DEPLOYED = "accountUpdateDeployed",
@@ -38,13 +38,23 @@ export class AccountWorker {
private readonly accountService: IAccountService,
private readonly activityService: IActivityService,
private readonly scheduleService: IScheduleService,
- private readonly backgroundUIService: IBackgroundUIService,
- private readonly debounceService: IDebounceService,
) {
this.activityService.emitter.on(
SignerChangedActivity,
this.onSignerChanged.bind(this),
)
+ this.activityService.emitter.on(
+ AccountDeployActivity,
+ this.updateDeployed.bind(this),
+ )
+ this.activityService.emitter.on(
+ GuardianChangedActivity,
+ this.onGuardianChanged.bind(this),
+ )
+ this.activityService.emitter.on(
+ ProvisionActivity,
+ this.onProvisionActivity.bind(this),
+ )
}
runUpdaterForAllTasks = pipe(
@@ -57,6 +67,7 @@ export class AccountWorker {
])
})
+ /** @internal just exposed for testing */
async runUpdaterTask(tasks: AllowArray): Promise {
const updaterTasks = Array.isArray(tasks) ? tasks : [tasks]
for (const task of updaterTasks) {
@@ -74,6 +85,7 @@ export class AccountWorker {
}
}
+ /** @internal just exposed for testing */
async updateDeployed(): Promise {
const accounts = await this.accountService.get(
(account) => account.needsDeploy !== false,
@@ -101,6 +113,7 @@ export class AccountWorker {
await this.accountService.upsert(newlyDeployedAccounts)
}
+ /** @internal just exposed for testing */
async updateAccountClassHash(): Promise {
const accounts = await this.accountService.get()
@@ -122,6 +135,7 @@ export class AccountWorker {
await this.accountService.upsert(updated)
}
+ /** @internal just exposed for testing */
async updateAccountCairoVersion(): Promise {
const accounts = await this.accountService.get()
@@ -145,54 +159,47 @@ export class AccountWorker {
await this.accountService.upsert(updated)
}
- async updateAccountClassHashImmediately(): Promise {
- const accounts = await this.accountService.get()
- const needsImmediateUpdate = accounts.some((account) =>
- isUndefined(account.classHash),
+ async onSignerChanged(payload: AccountActivityPayload) {
+ const accounts = await this.accountService.getFromBaseWalletAccounts(
+ payload,
)
- if (needsImmediateUpdate) {
- // Let's keep console.log here for now, as it's a critical path.
- // It will be removed in Prod.
- console.log("Updating account class hash immediately")
- await this.updateAccountClassHash()
- } else {
- console.log("Account class hash up to date")
- }
- }
-
- async updateAccountCairoVersionImmediately(): Promise {
- const accounts = await this.accountService.get()
- // Using every here, because we don't want to fetch the cairo version for undeployed accounts
- const needsImmediateUpdate = accounts.every((account) =>
- isUndefined(account.cairoVersion),
+ const results = await Promise.allSettled(
+ accounts.map((account) => {
+ return getOwnerForAccount(account)
+ }),
)
- if (needsImmediateUpdate) {
- // Let's keep console.log here for now, as it's a critical path.
- // It will be removed in Prod.
- console.log("Updating account cairo version immediately")
- await this.updateAccountCairoVersion()
- } else {
- console.log("Account cairo version up to date")
- }
+ const updated = accounts.map((account, index) => {
+ const result = results[index]
+ const owner = result.status === "fulfilled" ? result.value : undefined
+ return {
+ ...account,
+ owner,
+ }
+ })
+ await this.accountService.upsert(updated)
}
- async onSignerChanged(payload: SecurityActivityPayload) {
+ async onGuardianChanged(payload: AccountActivityPayload) {
const accounts = await this.accountService.getFromBaseWalletAccounts(
payload,
)
const results = await Promise.allSettled(
accounts.map((account) => {
- return getOwnerForAccount(account)
+ return getGuardianForAccount(account)
}),
)
const updated = accounts.map((account, index) => {
const result = results[index]
- const owner = result.status === "fulfilled" ? result.value : undefined
+ const guardian = result.status === "fulfilled" ? result.value : undefined
return {
...account,
- owner,
+ guardian,
}
})
await this.accountService.upsert(updated)
}
+
+ async onProvisionActivity(payload: ProvisionActivityPayload) {
+ await this.accountService.handleProvisionedAccount(payload)
+ }
}
diff --git a/packages/extension/src/background/__new/services/account/worker/index.ts b/packages/extension/src/background/__new/services/account/worker/index.ts
index 3c05dd309..ef4f15e85 100644
--- a/packages/extension/src/background/__new/services/account/worker/index.ts
+++ b/packages/extension/src/background/__new/services/account/worker/index.ts
@@ -1,14 +1,10 @@
import { chromeScheduleService } from "../../../../../shared/schedule"
import { accountService } from "../../../../../shared/account/service"
import { AccountWorker } from "./implementation"
-import { backgroundUIService } from "../../ui"
-import { debounceService } from "../../../../../shared/debounce"
import { activityService } from "../../activity"
export const accountWorker = new AccountWorker(
accountService,
activityService,
chromeScheduleService,
- backgroundUIService,
- debounceService,
)
diff --git a/packages/extension/src/background/__new/services/action/background.ts b/packages/extension/src/background/__new/services/action/background.ts
index d88522d74..c240fa95a 100644
--- a/packages/extension/src/background/__new/services/action/background.ts
+++ b/packages/extension/src/background/__new/services/action/background.ts
@@ -19,6 +19,7 @@ import type { Respond } from "../../../respond"
import { Wallet } from "../../../wallet"
import type { IBackgroundActionService } from "./interface"
import { ActionError } from "../../../../shared/errors/action"
+import { IFeeTokenService } from "../../../../shared/feeToken/service/interface"
const getResultData = (resultMessage?: MessageType) => {
if (resultMessage && "data" in resultMessage) {
@@ -39,6 +40,7 @@ export default class BackgroundActionService
constructor(
private queue: IActionQueue,
private wallet: Wallet,
+ private feeTokenService: IFeeTokenService,
private respond: Respond,
) {}
@@ -55,7 +57,7 @@ export default class BackgroundActionService
/**
* Don't await handleActionApproval, this allows for existing patterns to use 'waitForMessage' after calling await clientActionService.approve(...)
*/
- handleActionApproval(action, this.wallet)
+ handleActionApproval(action, this.wallet, this.feeTokenService)
.then((resultMessage) => {
const error = getResultDataError(resultMessage)
if (error) {
@@ -86,7 +88,11 @@ export default class BackgroundActionService
startedApproving: Date.now(),
errorApproving: undefined,
})
- const resultMessage = await handleActionApproval(action, this.wallet)
+ const resultMessage = await handleActionApproval(
+ action,
+ this.wallet,
+ this.feeTokenService,
+ )
const error = getResultDataError(resultMessage)
if (error) {
await this.queue.updateMeta(actionHash, {
@@ -149,6 +155,13 @@ export default class BackgroundActionService
return this.queue.add(action, meta)
}
+ async addFront(
+ action: T,
+ meta?: Partial,
+ ): Promise> {
+ return this.queue.addFront(action, meta)
+ }
+
async remove(actionHash: string): Promise | null> {
return this.queue.remove(actionHash)
}
diff --git a/packages/extension/src/background/__new/services/action/index.ts b/packages/extension/src/background/__new/services/action/index.ts
index 4ec7fa3a1..e63d4dc0e 100644
--- a/packages/extension/src/background/__new/services/action/index.ts
+++ b/packages/extension/src/background/__new/services/action/index.ts
@@ -1,4 +1,5 @@
import { actionQueue } from "../../../../shared/actionQueue"
+import { feeTokenService } from "../../../../shared/feeToken/service"
import { respond } from "../../../respond"
import { walletSingleton } from "../../../walletSingleton"
import BackgroundActionService from "./background"
@@ -6,5 +7,6 @@ import BackgroundActionService from "./background"
export const backgroundActionService = new BackgroundActionService(
actionQueue,
walletSingleton,
+ feeTokenService,
respond,
)
diff --git a/packages/extension/src/background/__new/services/action/interface.ts b/packages/extension/src/background/__new/services/action/interface.ts
index aee69b794..522a609b6 100644
--- a/packages/extension/src/background/__new/services/action/interface.ts
+++ b/packages/extension/src/background/__new/services/action/interface.ts
@@ -13,5 +13,9 @@ export interface IBackgroundActionService extends IActionService {
action: T,
meta?: Partial,
): Promise>
+ addFront(
+ action: T,
+ meta?: Partial,
+ ): Promise>
remove(actionHash: ActionHash): Promise | null>
}
diff --git a/packages/extension/src/background/__new/services/activity/implementation.test.ts b/packages/extension/src/background/__new/services/activity/implementation.test.ts
index e9db33e41..93ecdc523 100644
--- a/packages/extension/src/background/__new/services/activity/implementation.test.ts
+++ b/packages/extension/src/background/__new/services/activity/implementation.test.ts
@@ -7,7 +7,10 @@ import type { IAccountService } from "../../../../shared/account/service/interfa
import type { IActivityStorage } from "../../../../shared/activity/types"
import type { IDebounceService } from "../../../../shared/debounce"
import { createScheduleServiceMock } from "../../../../shared/schedule/mock"
-import { InMemoryObjectStore } from "../../../../shared/storage/__new/__test__/inmemoryImplementations"
+import {
+ InMemoryKeyValueStore,
+ InMemoryObjectStore,
+} from "../../../../shared/storage/__new/__test__/inmemoryImplementations"
import type {
ContractAddress,
INftsContractsRepository,
@@ -19,8 +22,9 @@ import type { IBackgroundUIService } from "../ui/interface"
import { ActivityService } from "./implementation"
import { GuardianChangedActivity, NftActivity, type Events } from "./interface"
-import activities from "./__fixtures__/activities.json"
-import state from "./__fixtures__/state.json"
+import activities from "../../../../shared/activity/__fixtures__/activities.json"
+import state from "../../../../shared/activity/__fixtures__/state.json"
+import { WalletStorageProps } from "../../../wallet/backup/backup.service"
describe("ActivityService", () => {
const makeService = () => {
@@ -35,6 +39,10 @@ describe("ActivityService", () => {
},
})
+ const walletStore = new InMemoryKeyValueStore({
+ namespace: "wallet",
+ })
+
const walletSingleton = {
getSelectedAccount: vi.fn(),
} as unknown as Mocked
@@ -76,6 +84,7 @@ describe("ActivityService", () => {
scheduleService,
backgroundUIService,
debounceService,
+ walletStore,
)
return {
emitter,
diff --git a/packages/extension/src/background/__new/services/activity/implementation.ts b/packages/extension/src/background/__new/services/activity/implementation.ts
index 0474419b0..6c0733715 100644
--- a/packages/extension/src/background/__new/services/activity/implementation.ts
+++ b/packages/extension/src/background/__new/services/activity/implementation.ts
@@ -8,7 +8,10 @@ import {
import type Emittery from "emittery"
import type { IAccountService } from "../../../../shared/account/service/interface"
-import type { IActivityStorage } from "../../../../shared/activity/types"
+import type {
+ ActivitiesPayload,
+ IActivityStorage,
+} from "../../../../shared/activity/types"
import { ARGENT_API_BASE_URL } from "../../../../shared/api/constants"
import { argentApiNetworkForNetwork } from "../../../../shared/api/headers"
import { RefreshInterval } from "../../../../shared/config"
@@ -22,7 +25,7 @@ import { urlWithQuery } from "../../../../shared/utils/url"
import type { BaseWalletAccount } from "../../../../shared/wallet.model"
import type { Wallet } from "../../../wallet"
import type { IBackgroundUIService } from "../ui/interface"
-import { everyWhenOpen } from "../worker/schedule/decorators"
+import { everyWhenOpen, onAccountChanged } from "../worker/schedule/decorators"
import { pipe } from "../worker/schedule/pipe"
import {
AccountUpgradedActivity,
@@ -38,18 +41,22 @@ import {
TokenActivity,
TriggerEscapeGuardianActivity,
TriggerEscapeSignerActivity,
- type ActivitiesPayload,
type Events,
type IActivityService,
+ AccountDeployActivity,
+ ProvisionActivity,
} from "./interface"
import {
isActivityDetailsAction,
type ActivityDetailsAction,
type ActivityResponse,
-} from "./schema"
-import { getOverallLastModified } from "./utils/getOverallLastModified"
-import { parseFinanceActivities } from "./utils/parseFinanceActivities"
-import { parseSecurityActivities } from "./utils/parseSecurityActivities"
+} from "../../../../shared/activity/schema"
+import { getOverallLastModified } from "../../../../shared/activity/utils/getOverallLastModified"
+import { parseFinanceActivities } from "../../../../shared/activity/utils/parseFinanceActivities"
+import { parseAccountActivities } from "../../../../shared/activity/utils/parseAccountActivities"
+import { IKeyValueStorage } from "../../../../shared/storage"
+import { WalletStorageProps } from "../../../wallet/backup/backup.service"
+import { parseProvisionActivity } from "../../../../shared/activity/utils/parseProvisionActivity"
/** maps activity details action to an equivalent Event to emit */
@@ -81,9 +88,11 @@ export class ActivityService implements IActivityService {
private readonly scheduleService: IScheduleService,
private readonly backgroundUIService: IBackgroundUIService,
private readonly debounceService: IDebounceService,
+ private readonly old_walletStore: IKeyValueStorage,
) {}
runUpdateSelectedAccountActivities = pipe(
+ onAccountChanged(this.old_walletStore),
everyWhenOpen(
this.backgroundUIService,
this.scheduleService,
@@ -248,7 +257,7 @@ export class ActivityService implements IActivityService {
/** security */
- const accountAddressesByAction = parseSecurityActivities({
+ const accountAddressesByAction = parseAccountActivities({
activities: filteredActivities,
accountAddressesOnNetwork,
})
@@ -262,8 +271,23 @@ export class ActivityService implements IActivityService {
if (accounts.length) {
void this.emitter.emit(event, accounts)
}
+ } else if (action === "deploy") {
+ const accounts = accountsOnNetwork.filter((account) =>
+ includesAddress(account.address, addresses),
+ )
+ if (accounts.length) {
+ void this.emitter.emit(AccountDeployActivity, accounts)
+ }
}
})
+ /** Provision */
+ const provisionActivity = parseProvisionActivity(filteredActivities)
+ if (provisionActivity !== undefined) {
+ void this.emitter.emit(ProvisionActivity, {
+ account: activityAccount,
+ activity: provisionActivity,
+ })
+ }
}
async getModifiedAfter(
diff --git a/packages/extension/src/background/__new/services/activity/index.ts b/packages/extension/src/background/__new/services/activity/index.ts
index 57bc6891f..310ca67b5 100644
--- a/packages/extension/src/background/__new/services/activity/index.ts
+++ b/packages/extension/src/background/__new/services/activity/index.ts
@@ -11,6 +11,7 @@ import type { Events } from "./interface"
import { accountService } from "../../../../shared/account/service"
import { tokenService } from "../../../../shared/token/__new/service"
import { nftsContractsRepository } from "../../../../shared/storage/__new/repositories/nft"
+import { old_walletStore } from "../../../../shared/wallet/walletStore"
export { Activities as Activities } from "./interface"
@@ -27,4 +28,5 @@ export const activityService = new ActivityService(
chromeScheduleService,
backgroundUIService,
debounceService,
+ old_walletStore,
)
diff --git a/packages/extension/src/background/__new/services/activity/interface.ts b/packages/extension/src/background/__new/services/activity/interface.ts
index 380e8b06b..d6ebe1993 100644
--- a/packages/extension/src/background/__new/services/activity/interface.ts
+++ b/packages/extension/src/background/__new/services/activity/interface.ts
@@ -3,7 +3,11 @@ import type Emittery from "emittery"
import type { ContractAddress } from "../../../../shared/storage/__new/repositories/nft"
import type { BaseWalletAccount } from "../../../../shared/wallet.model"
-import type { Activity } from "./schema"
+import type { Activity } from "../../../../shared/activity/schema"
+import {
+ ActivitiesPayload,
+ ProvisionActivityPayload,
+} from "../../../../shared/activity/types"
/** raw */
export const Activities = Symbol("Activities")
@@ -12,7 +16,8 @@ export const Activities = Symbol("Activities")
export const TokenActivity = Symbol("TokenActivity")
export const NftActivity = Symbol("NftActivity")
-/** security */
+/** account */
+export const AccountDeployActivity = Symbol("AccountDeployActivity")
export const TriggerEscapeGuardianActivity = Symbol(
"TriggerEscapeGuardianActivity",
)
@@ -30,10 +35,8 @@ export const MultisigConfigurationUpdatedActivity = Symbol(
"MultisigConfigurationUpdatedActivity",
)
-export type ActivitiesPayload = {
- account: BaseWalletAccount
- activities: Activity[]
-}
+/** Provision */
+export const ProvisionActivity = Symbol("ProvisionActivity")
export type TokenActivityPayload = {
accounts: BaseWalletAccount[]
@@ -45,7 +48,7 @@ export type NftActivityPayload = {
nfts: ContractAddress[]
}
-export type SecurityActivityPayload = BaseWalletAccount[]
+export type AccountActivityPayload = BaseWalletAccount[]
/**
* Fired when new activities are discovered on an individual account
@@ -55,16 +58,18 @@ export type Events = {
[Activities]: ActivitiesPayload
[TokenActivity]: TokenActivityPayload
[NftActivity]: NftActivityPayload
- [TriggerEscapeGuardianActivity]: SecurityActivityPayload
- [TriggerEscapeSignerActivity]: SecurityActivityPayload
- [EscapeGuardianActivity]: SecurityActivityPayload
- [EscapeSignerActivity]: SecurityActivityPayload
- [GuardianChangedActivity]: SecurityActivityPayload
- [GuardianBackupChangedActivity]: SecurityActivityPayload
- [SignerChangedActivity]: SecurityActivityPayload
- [CancelEscapeActivity]: SecurityActivityPayload
- [AccountUpgradedActivity]: SecurityActivityPayload
- [MultisigConfigurationUpdatedActivity]: SecurityActivityPayload
+ [AccountDeployActivity]: AccountActivityPayload
+ [TriggerEscapeGuardianActivity]: AccountActivityPayload
+ [TriggerEscapeSignerActivity]: AccountActivityPayload
+ [EscapeGuardianActivity]: AccountActivityPayload
+ [EscapeSignerActivity]: AccountActivityPayload
+ [GuardianChangedActivity]: AccountActivityPayload
+ [GuardianBackupChangedActivity]: AccountActivityPayload
+ [SignerChangedActivity]: AccountActivityPayload
+ [CancelEscapeActivity]: AccountActivityPayload
+ [AccountUpgradedActivity]: AccountActivityPayload
+ [MultisigConfigurationUpdatedActivity]: AccountActivityPayload
+ [ProvisionActivity]: ProvisionActivityPayload
}
export interface IActivityService {
diff --git a/packages/extension/src/background/__new/services/activity/utils/parseSecurityActivities.test.ts b/packages/extension/src/background/__new/services/activity/utils/parseSecurityActivities.test.ts
deleted file mode 100644
index 311704c85..000000000
--- a/packages/extension/src/background/__new/services/activity/utils/parseSecurityActivities.test.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import type { Address } from "@argent/shared"
-import { describe, expect, test } from "vitest"
-
-import activities from "../__fixtures__/activities.json"
-import activitiesManyEscapes from "../__fixtures__/activities-many-escapes.json"
-import activitiesSignerChanged from "../__fixtures__/activities-signer-changed.json"
-import state from "../__fixtures__/state.json"
-import type { Activity } from "../schema"
-import { parseSecurityActivities } from "./parseSecurityActivities"
-
-describe("background/services/activity/utils", () => {
- describe("parseSecurityActivities", () => {
- describe("when valid", () => {
- test("returns a map of actions to account addresses", () => {
- expect(
- parseSecurityActivities({
- activities: activities as Activity[],
- accountAddressesOnNetwork:
- state.accountAddressesOnNetwork as Address[],
- }),
- ).toMatchInlineSnapshot(`
- {
- "guardianChanged": [
- "0x05f1f0a38429dcab9ffd8a786c0d827e84c1cbd8f60243e6d25d066a13af4a25",
- ],
- }
- `)
- expect(
- parseSecurityActivities({
- activities: activitiesManyEscapes as Activity[],
- accountAddressesOnNetwork: [
- "0x00c90c89d339d1611f971e9211bc6a8efafc82541a61703a702c17d291afe9bb",
- ] as Address[],
- }),
- ).toMatchInlineSnapshot(`
- {
- "cancelEscape": [
- "0x00c90c89d339d1611f971e9211bc6a8efafc82541a61703a702c17d291afe9bb",
- ],
- "guardianChanged": [
- "0x00c90c89d339d1611f971e9211bc6a8efafc82541a61703a702c17d291afe9bb",
- ],
- "triggerEscapeGuardian": [
- "0x00c90c89d339d1611f971e9211bc6a8efafc82541a61703a702c17d291afe9bb",
- ],
- }
- `)
- expect(
- parseSecurityActivities({
- activities: activitiesSignerChanged as Activity[],
- accountAddressesOnNetwork: [
- "0x02470ea294aa4b28ee4a473aaa8a1edc6c810c11684d1f29f1f3edd336fd0f34",
- ] as Address[],
- }),
- ).toMatchInlineSnapshot(`
- {
- "signerChanged": [
- "0x02470ea294aa4b28ee4a473aaa8a1edc6c810c11684d1f29f1f3edd336fd0f34",
- ],
- }
- `)
- })
- })
- })
-})
diff --git a/packages/extension/src/background/__new/services/activity/utils/parseSecurityActivities.ts b/packages/extension/src/background/__new/services/activity/utils/parseSecurityActivities.ts
deleted file mode 100644
index 3d56a7b24..000000000
--- a/packages/extension/src/background/__new/services/activity/utils/parseSecurityActivities.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { type Address, includesAddress } from "@argent/shared"
-
-import type { Activity, ActivityDetailsAction } from "../schema"
-
-interface ParseFinanceActivitiesProps {
- activities: Activity[]
- accountAddressesOnNetwork: Address[]
-}
-
-/**
- * Parses security-related activities from the provided list of activities, grouping them by action
- * and returning a map of addresses associated with each action.
- *
- * @param activities: Array of activities to parse.
- * @param accountAddressesOnNetwork: Array of account addresses that are known to be active on the
- * network.
- * @returns A map of actions to their associated addresses, where the addresses represent the
- * accounts involved in the respective actions.
- */
-
-export function parseSecurityActivities({
- activities,
- accountAddressesOnNetwork,
-}: ParseFinanceActivitiesProps) {
- const accountAddressesByAction: Partial<
- Record
- > = {}
-
- activities.forEach((activity) => {
- if (activity.group === "security") {
- const address = activity.wallet
- if (includesAddress(address, accountAddressesOnNetwork)) {
- const action = activity.details.action
- if (action) {
- if (!accountAddressesByAction[action]) {
- accountAddressesByAction[action] = []
- }
- if (!includesAddress(address, accountAddressesByAction[action])) {
- accountAddressesByAction[action]?.push(address)
- }
- }
- }
- }
- })
-
- return accountAddressesByAction
-}
diff --git a/packages/extension/src/background/__new/services/discover/implementation.ts b/packages/extension/src/background/__new/services/discover/implementation.ts
new file mode 100644
index 000000000..1f2d0e344
--- /dev/null
+++ b/packages/extension/src/background/__new/services/discover/implementation.ts
@@ -0,0 +1,68 @@
+import { type IHttpService } from "@argent/shared"
+
+import { RefreshInterval } from "../../../../shared/config"
+import type { IDebounceService } from "../../../../shared/debounce"
+import type {
+ IDiscoverStorage,
+ IDiscoverService,
+} from "../../../../shared/discover/interface"
+import { newsApiReponseSchema } from "../../../../shared/discover/schema"
+import type { IScheduleService } from "../../../../shared/schedule/interface"
+import type { IObjectStore } from "../../../../shared/storage/__new/interface"
+import type { IBackgroundUIService } from "../ui/interface"
+import { everyWhenOpen } from "../worker/schedule/decorators"
+import { pipe } from "../worker/schedule/pipe"
+import { ARGENT_X_NEWS_URL } from "../../../../shared/api/constants"
+
+export class DiscoverService implements IDiscoverService {
+ constructor(
+ private readonly discoverStore: IObjectStore,
+ private readonly httpService: IHttpService,
+ private readonly scheduleService: IScheduleService,
+ private readonly backgroundUIService: IBackgroundUIService,
+ private readonly debounceService: IDebounceService,
+ ) {}
+
+ runUpdateSelectedAccountActivities = pipe(
+ everyWhenOpen(
+ this.backgroundUIService,
+ this.scheduleService,
+ this.debounceService,
+ RefreshInterval.MEDIUM,
+ "DiscoverService.updateDiscover",
+ ),
+ )(async () => {
+ await this.updateDiscover()
+ })
+
+ async updateDiscover() {
+ if (!ARGENT_X_NEWS_URL) {
+ return this.resetData()
+ }
+ /** FIXME: cacheBust param is a temporary fix to force fresh content from static server */
+ const cacheBust = Date.now()
+ const result = await this.httpService.get(
+ `${ARGENT_X_NEWS_URL}?v=${cacheBust}`,
+ )
+ const parsedResult = newsApiReponseSchema.safeParse(result)
+ if (!parsedResult.success) {
+ // on failure, ensure we don't show stale data to end user
+ return this.resetData()
+ }
+ await this.discoverStore.set({
+ data: parsedResult.data,
+ })
+ }
+
+ private async resetData() {
+ await this.discoverStore.set({
+ data: null,
+ })
+ }
+
+ async setViewedAt(viewedAt: number) {
+ await this.discoverStore.set({
+ viewedAt,
+ })
+ }
+}
diff --git a/packages/extension/src/background/__new/services/discover/index.ts b/packages/extension/src/background/__new/services/discover/index.ts
new file mode 100644
index 000000000..e04d704e0
--- /dev/null
+++ b/packages/extension/src/background/__new/services/discover/index.ts
@@ -0,0 +1,14 @@
+import { debounceService } from "../../../../shared/debounce"
+import { discoverStore } from "../../../../shared/discover/storage"
+import { httpService } from "../../../../shared/http/singleton"
+import { chromeScheduleService } from "../../../../shared/schedule"
+import { backgroundUIService } from "../ui"
+import { DiscoverService } from "./implementation"
+
+export const discoverService = new DiscoverService(
+ discoverStore,
+ httpService,
+ chromeScheduleService,
+ backgroundUIService,
+ debounceService,
+)
diff --git a/packages/extension/src/background/__new/services/network/background.test.ts b/packages/extension/src/background/__new/services/network/background.test.ts
index 5734c5e18..768ebe2bd 100644
--- a/packages/extension/src/background/__new/services/network/background.test.ts
+++ b/packages/extension/src/background/__new/services/network/background.test.ts
@@ -1,4 +1,4 @@
-import { describe, expect, test, vi } from "vitest"
+import { Mocked, describe, expect, test, vi } from "vitest"
import { Network } from "../../../../shared/network"
import { defaultReadonlyNetworks } from "../../../../shared/network/defaults"
@@ -7,53 +7,75 @@ import { networksEqual } from "../../../../shared/network/store"
import { InMemoryRepository } from "../../../../shared/storage/__new/__test__/inmemoryImplementations"
import BackgroundNetworkService from "./background"
import { NetworkWithStatus } from "../../../../shared/network/type"
+import { IHttpService } from "@argent/shared"
+import { ETH_TOKEN_ADDRESS } from "../../../../shared/network/constants"
describe("BackgroundNetworkService", () => {
const makeService = () => {
const networkRepo = new InMemoryRepository({
namespace: "core:allNetworks",
compare: networksEqual,
+ defaults: [
+ ...defaultReadonlyNetworks,
+ {
+ id: "katana",
+ chainId: "katana",
+ name: "Katana",
+ rpcUrl: "https://katana.rpc",
+ possibleFeeTokenAddresses: [ETH_TOKEN_ADDRESS],
+ },
+ ],
})
const networkWithStatusRepo = new InMemoryRepository({
namespace: "core:allNetworkStatus",
compare: networksEqual,
})
const getNetworkStatuses = vi.fn()
+ const httpService = {
+ get: getNetworkStatuses,
+ post: vi.fn(),
+ delete: vi.fn(),
+ } as Mocked
const backgroundNetworkService = new BackgroundNetworkService(
networkRepo,
networkWithStatusRepo,
defaultReadonlyNetworks,
- getNetworkStatuses,
+ httpService,
)
return {
backgroundNetworkService,
networkWithStatusRepo,
- getNetworkStatuses,
+ httpService,
}
}
- test("updateStatuses", async () => {
- const {
- backgroundNetworkService,
- networkWithStatusRepo,
- getNetworkStatuses,
- } = makeService()
+ test(" updateStatuses with valid networks", async () => {
+ const { backgroundNetworkService, networkWithStatusRepo, httpService } =
+ makeService()
- getNetworkStatuses.mockResolvedValueOnce({
- "mainnet-alpha": "ok",
- "goerli-alpha": "degraded",
+ httpService.get.mockResolvedValueOnce({
+ state: "green",
})
await backgroundNetworkService.updateStatuses()
- expect(getNetworkStatuses).toHaveBeenCalled()
+ expect(httpService.get).toHaveBeenCalled()
const [ok] = await networkWithStatusRepo.get(
networkSelector("mainnet-alpha"),
)
- expect(ok).toHaveProperty("status", "ok")
+ expect(ok).toHaveProperty("status", "green")
+ })
+ test(" updateStatuses with unknown networks", async () => {
+ const { backgroundNetworkService, networkWithStatusRepo, httpService } =
+ makeService()
- const [degraded] = await networkWithStatusRepo.get(
- networkSelector("goerli-alpha"),
- )
- expect(degraded).toHaveProperty("status", "degraded")
+ httpService.get.mockResolvedValueOnce({
+ state: "whatever",
+ })
+
+ await backgroundNetworkService.updateStatuses()
+ expect(httpService.get).toHaveBeenCalled()
+
+ const [ok] = await networkWithStatusRepo.get(networkSelector("katana"))
+ expect(ok).toHaveProperty("status", "unknown")
})
})
diff --git a/packages/extension/src/background/__new/services/network/background.ts b/packages/extension/src/background/__new/services/network/background.ts
index 995d43771..d8f540a47 100644
--- a/packages/extension/src/background/__new/services/network/background.ts
+++ b/packages/extension/src/background/__new/services/network/background.ts
@@ -1,9 +1,14 @@
import { uniqWith } from "lodash-es"
-import { Network } from "../../../../shared/network"
+import { Network, NetworkStatus } from "../../../../shared/network"
import { INetworkRepo, networksEqual } from "../../../../shared/network/store"
-import { GetNetworkStatusesFn, IBackgroundNetworkService } from "./interface"
+import { IBackgroundNetworkService } from "./interface"
import { INetworkWithStatusRepo } from "../../../../shared/network/statusStore"
+import { IHttpService } from "@argent/shared"
+import urlJoin from "url-join"
+import { argentApiNetworkForNetwork } from "../../../../shared/api/headers"
+import { ARGENT_NETWORK_STATUS } from "../../../../shared/api/constants"
+import { NetworkError } from "../../../../shared/errors/network"
export default class BackgroundNetworkService
implements IBackgroundNetworkService
@@ -12,7 +17,7 @@ export default class BackgroundNetworkService
private readonly networkRepo: INetworkRepo,
private readonly networkWithStatusRepo: INetworkWithStatusRepo,
readonly defaultNetworks: Network[],
- private readonly getNetworkStatuses: GetNetworkStatusesFn,
+ private readonly httpService: IHttpService,
) {}
private async loadNetworks() {
@@ -23,13 +28,52 @@ export default class BackgroundNetworkService
return allNetworks
}
+ async getNetworkStatuses(networks: Network[]) {
+ return Promise.all(
+ networks.map(async (network) => {
+ if (ARGENT_NETWORK_STATUS === undefined) {
+ throw new NetworkError({ code: "ARGENT_NETWORK_STATUS_NOT_DEFINED" })
+ }
+ const backendNetworkId = argentApiNetworkForNetwork(network.id)
+
+ if (!backendNetworkId) {
+ return {
+ id: network.id,
+ status: "unknown" as NetworkStatus,
+ }
+ }
+
+ const url = urlJoin(ARGENT_NETWORK_STATUS, backendNetworkId)
+ try {
+ const response = await this.httpService.get<{
+ state: NetworkStatus
+ }>(url)
+
+ return {
+ status: response.state,
+ id: network.id,
+ }
+ } catch (error) {
+ return {
+ id: network.id,
+ status: "unknown" as NetworkStatus,
+ }
+ }
+ }),
+ )
+ }
async updateStatuses() {
const networks = await this.loadNetworks()
+
const networkStatuses = await this.getNetworkStatuses(networks)
+
const networkWithUpdatedStatuses = networks.map((network) => {
return {
id: network.id,
- status: networkStatuses[network.id] ?? "unknown",
+ status:
+ networkStatuses.find(
+ (networkStatus) => networkStatus.id === network.id,
+ )?.status ?? "unknown",
}
})
diff --git a/packages/extension/src/background/__new/services/network/index.ts b/packages/extension/src/background/__new/services/network/index.ts
index 27d38cd7f..337cbd931 100644
--- a/packages/extension/src/background/__new/services/network/index.ts
+++ b/packages/extension/src/background/__new/services/network/index.ts
@@ -1,18 +1,18 @@
import { debounceService } from "../../../../shared/debounce"
+import { httpService } from "../../../../shared/http/singleton"
import { defaultNetworks } from "../../../../shared/network"
import { networkStatusRepo } from "../../../../shared/network/statusStore"
import { networkRepo } from "../../../../shared/network/store"
import { chromeScheduleService } from "../../../../shared/schedule"
import { backgroundUIService } from "../ui"
import BackgroundNetworkService from "./background"
-import { getNetworkStatuses } from "./status"
import { NetworkWorker } from "./worker"
export const backgroundNetworkService = new BackgroundNetworkService(
networkRepo,
networkStatusRepo,
defaultNetworks,
- getNetworkStatuses,
+ httpService,
)
export const networkWorker = new NetworkWorker(
diff --git a/packages/extension/src/background/__new/services/network/status.ts b/packages/extension/src/background/__new/services/network/status.ts
deleted file mode 100644
index f9d43e6fd..000000000
--- a/packages/extension/src/background/__new/services/network/status.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { Network, NetworkStatus } from "../../../../shared/network"
-import { GetNetworkStatusesFn } from "./interface"
-import { getProvider } from "../../../../shared/network/provider"
-
-async function getNetworkStatus(network: Network): Promise {
- const provider = getProvider(network)
- const sync = await provider.getSyncingStats() // throws if not connected
-
- // Can only be false but inproperly typed in the current version of snjs
- if (typeof sync === "boolean") {
- // not syncing
- return "ok"
- }
-
- const blockDifference = sync.highest_block_num - sync.current_block_num
- if (blockDifference <= 2) {
- return "ok"
- }
- return "degraded"
-}
-
-export const getNetworkStatuses: GetNetworkStatusesFn = async (networks) => {
- const statuses = await Promise.allSettled(
- networks.map(async (network) => getNetworkStatus(network)),
- )
-
- return Object.fromEntries(
- networks.map(({ id }, i) => {
- const promise = statuses[i]
- if (promise.status === "fulfilled") {
- return [id, promise.value]
- } else {
- return [id, "error"]
- }
- }),
- )
-}
diff --git a/packages/extension/src/background/__new/services/network/worker.ts b/packages/extension/src/background/__new/services/network/worker.ts
index 3dc3a9f4d..70977bcb2 100644
--- a/packages/extension/src/background/__new/services/network/worker.ts
+++ b/packages/extension/src/background/__new/services/network/worker.ts
@@ -1,7 +1,7 @@
import { IScheduleService } from "../../../../shared/schedule/interface"
import { IBackgroundNetworkService } from "./interface"
-// import { RefreshInterval } from "../../../../shared/config"
-// import { everyWhenOpen } from "../worker/schedule/decorators"
+import { RefreshInterval } from "../../../../shared/config"
+import { everyWhenOpen } from "../worker/schedule/decorators"
import { IBackgroundUIService } from "../ui/interface"
import { IDebounceService } from "../../../../shared/debounce"
@@ -15,14 +15,13 @@ export class NetworkWorker {
private readonly debounceService: IDebounceService,
) {}
- // Temp: This is commented out until we have a final decision on RPC provider
- //updateNetworkStatuses = everyWhenOpen(
- // this.backgroundUIService,
- // this.scheduleService,
- // this.debounceService,
- // RefreshInterval.MEDIUM,
- // "NetworkWorker.updateNetworkStatuses",
- //)(async (): Promise => {
- // await this.backgroundNetworkService.updateStatuses()
- //})
+ updateNetworkStatuses = everyWhenOpen(
+ this.backgroundUIService,
+ this.scheduleService,
+ this.debounceService,
+ RefreshInterval.MEDIUM,
+ "NetworkWorker.updateNetworkStatuses",
+ )(async (): Promise => {
+ await this.backgroundNetworkService.updateStatuses()
+ })
}
diff --git a/packages/extension/src/background/__new/services/nft/worker/implementation.ts b/packages/extension/src/background/__new/services/nft/worker/implementation.ts
index 283af4ba6..077765e4d 100644
--- a/packages/extension/src/background/__new/services/nft/worker/implementation.ts
+++ b/packages/extension/src/background/__new/services/nft/worker/implementation.ts
@@ -48,7 +48,10 @@ export class NftsWorker {
changeSet?.oldValue,
)
if (hasSuccessTx) {
- setTimeout(() => void this.updateNftsCallback(), 5000) // Add a delay so the backend has time to index the nft
+ setTimeout(
+ () => void this.updateNftsCallback(),
+ RefreshInterval.FAST * 1000,
+ ) // Add a delay so the backend has time to index the nft
}
})
}
@@ -83,6 +86,7 @@ export class NftsWorker {
"starknet",
account.networkId,
contractsAddresses,
+ addressSchema.parse(account.address),
)
} catch (e) {
console.error(e)
diff --git a/packages/extension/src/background/__new/services/provision/implementation.ts b/packages/extension/src/background/__new/services/provision/implementation.ts
new file mode 100644
index 000000000..e045b47f1
--- /dev/null
+++ b/packages/extension/src/background/__new/services/provision/implementation.ts
@@ -0,0 +1,15 @@
+import { IHttpService } from "@argent/shared"
+import { IProvisionService } from "../../../../shared/provision/interface"
+import { ProvisionStatus } from "../../../../shared/provision/types"
+import { PROVISION_STATUS_ENDPOINT } from "../../../../shared/api/constants"
+
+export class ProvisionService implements IProvisionService {
+ constructor(private httpService: IHttpService) {}
+
+ getStatus() {
+ if (!PROVISION_STATUS_ENDPOINT) {
+ throw new Error("Provision status endpoint not defined")
+ }
+ return this.httpService.get(PROVISION_STATUS_ENDPOINT)
+ }
+}
diff --git a/packages/extension/src/background/__new/services/provision/index.ts b/packages/extension/src/background/__new/services/provision/index.ts
new file mode 100644
index 000000000..e04d288b2
--- /dev/null
+++ b/packages/extension/src/background/__new/services/provision/index.ts
@@ -0,0 +1,4 @@
+import { httpService } from "../../../../shared/http/singleton"
+import { ProvisionService } from "./implementation"
+
+export const provisionService = new ProvisionService(httpService)
diff --git a/packages/extension/src/background/__new/services/recovery/implementation.test.ts b/packages/extension/src/background/__new/services/recovery/implementation.test.ts
index 03cf26843..0ddba8e10 100644
--- a/packages/extension/src/background/__new/services/recovery/implementation.test.ts
+++ b/packages/extension/src/background/__new/services/recovery/implementation.test.ts
@@ -35,9 +35,12 @@ describe("BackgroundRecoveryService", () => {
const { recoveryStore, wallet, backgroundRecoveryService } = makeService()
await backgroundRecoveryService.byBackup("foo")
expect(recoveryStore.set).toHaveBeenNthCalledWith(1, {
- isRecovering: true,
+ errorRecovering: false,
})
expect(recoveryStore.set).toHaveBeenNthCalledWith(2, {
+ isRecovering: true,
+ })
+ expect(recoveryStore.set).toHaveBeenNthCalledWith(3, {
isRecovering: false,
})
expect(wallet.importBackup).toHaveBeenCalledWith("foo")
@@ -53,9 +56,12 @@ describe("BackgroundRecoveryService", () => {
} = makeService()
await backgroundRecoveryService.bySeedPhrase("foo", "bar")
expect(recoveryStore.set).toHaveBeenNthCalledWith(1, {
- isRecovering: true,
+ errorRecovering: false,
})
expect(recoveryStore.set).toHaveBeenNthCalledWith(2, {
+ isRecovering: true,
+ })
+ expect(recoveryStore.set).toHaveBeenNthCalledWith(3, {
isRecovering: false,
})
expect(wallet.restoreSeedPhrase).toHaveBeenCalledWith("foo", "bar")
diff --git a/packages/extension/src/background/__new/services/recovery/implementation.ts b/packages/extension/src/background/__new/services/recovery/implementation.ts
index 648354083..f0eb7bc21 100644
--- a/packages/extension/src/background/__new/services/recovery/implementation.ts
+++ b/packages/extension/src/background/__new/services/recovery/implementation.ts
@@ -1,4 +1,5 @@
import { IRecoveryService } from "../../../../shared/recovery/service/interface"
+import { recoveredAtKeyValueStore } from "../../../../shared/recovery/storage"
import { IRecoveryStorage } from "../../../../shared/recovery/types"
import { IObjectStore } from "../../../../shared/storage/__new/interface"
import { TransactionTrackerWorker } from "../../../transactions/service/starknet.service"
@@ -15,22 +16,51 @@ export class BackgroundRecoveryService implements IRecoveryService {
await this.recoveryStore.set({ isRecovering })
}
+ private async setErrorRecovering(errorRecovering: string | false) {
+ await this.recoveryStore.set({ errorRecovering })
+ }
+
async byBackup(backup: string) {
try {
+ await this.clearErrorRecovering()
await this.setIsRecovering(true)
await this.wallet.importBackup(backup)
+ } catch (error) {
+ console.error(error)
+ await this.setErrorRecovering(`${error}`)
+ throw error
} finally {
+ await this.updateLastRecoveredAt()
await this.setIsRecovering(false)
}
}
+ private async updateLastRecoveredAt() {
+ const lastRecoveredAt = await recoveredAtKeyValueStore.get(
+ "lastRecoveredAt",
+ )
+ if (lastRecoveredAt === null) {
+ void recoveredAtKeyValueStore.set("lastRecoveredAt", Date.now())
+ }
+ }
+
async bySeedPhrase(seedPhrase: string, newPassword: string) {
try {
+ await this.clearErrorRecovering()
await this.setIsRecovering(true)
await this.wallet.restoreSeedPhrase(seedPhrase, newPassword)
void this.transactionTracker.loadHistory()
+ } catch (error) {
+ console.error(error)
+ await this.setErrorRecovering(`${error}`)
+ throw error
} finally {
+ await this.updateLastRecoveredAt()
await this.setIsRecovering(false)
}
}
+
+ async clearErrorRecovering() {
+ await this.setErrorRecovering(false)
+ }
}
diff --git a/packages/extension/src/background/__new/services/riskAssessment/background.ts b/packages/extension/src/background/__new/services/riskAssessment/background.ts
new file mode 100644
index 000000000..c0fd2e195
--- /dev/null
+++ b/packages/extension/src/background/__new/services/riskAssessment/background.ts
@@ -0,0 +1,45 @@
+import { IHttpService } from "@argent/shared"
+import {
+ DappContext,
+ IRiskAssessmentService,
+} from "../../../../shared/riskAssessment/interface"
+import { RiskAssessment } from "../../../../shared/riskAssessment/schema"
+import { ARGENT_TRANSACTION_REVIEW_API_BASE_URL } from "../../../../shared/api/constants"
+import urlJoin from "url-join"
+import { argentApiNetworkForNetwork } from "../../../../shared/api/headers"
+import { RiskAssessmentError } from "../../../../shared/errors/riskAssessment"
+
+const riskAssessmentBaseEndpoint = urlJoin(
+ ARGENT_TRANSACTION_REVIEW_API_BASE_URL || "",
+ "domains/info/starknet",
+)
+
+export default class BackgroundRiskAssessmentService
+ implements IRiskAssessmentService
+{
+ constructor(private httpService: IHttpService) {}
+
+ private getRiskAssessmentEndpoint({
+ dappDomain,
+ network,
+ }: {
+ dappDomain: string
+ network: string
+ }) {
+ return `${riskAssessmentBaseEndpoint}/${argentApiNetworkForNetwork(
+ network,
+ )}?domain=${dappDomain}`
+ }
+
+ async assessRisk({ dappContext }: { dappContext: DappContext }) {
+ try {
+ const riskAssessmentEndpoint = this.getRiskAssessmentEndpoint(dappContext)
+ const result = await this.httpService.get(
+ riskAssessmentEndpoint,
+ )
+ return result
+ } catch (e) {
+ throw new RiskAssessmentError({ code: "ERROR_FETCHING" })
+ }
+ }
+}
diff --git a/packages/extension/src/background/__new/services/riskAssessment/index.ts b/packages/extension/src/background/__new/services/riskAssessment/index.ts
new file mode 100644
index 000000000..ed08e6656
--- /dev/null
+++ b/packages/extension/src/background/__new/services/riskAssessment/index.ts
@@ -0,0 +1,6 @@
+import { httpService } from "../../../../shared/http/singleton"
+import BackgroundRiskAssessmentService from "./background"
+
+export const riskAssessmentService = new BackgroundRiskAssessmentService(
+ httpService,
+)
diff --git a/packages/extension/src/background/__new/services/sentry/index.ts b/packages/extension/src/background/__new/services/sentry/index.ts
new file mode 100644
index 000000000..e89bce14e
--- /dev/null
+++ b/packages/extension/src/background/__new/services/sentry/index.ts
@@ -0,0 +1,5 @@
+import { baseSentryOptions } from "../../../../shared/sentry/options"
+import { settingsStore } from "../../../../shared/settings"
+import { SentryWorker } from "./worker"
+
+export const sentryWorker = new SentryWorker(baseSentryOptions, settingsStore)
diff --git a/packages/extension/src/background/__new/services/sentry/worker.ts b/packages/extension/src/background/__new/services/sentry/worker.ts
new file mode 100644
index 000000000..3bee666bc
--- /dev/null
+++ b/packages/extension/src/background/__new/services/sentry/worker.ts
@@ -0,0 +1,50 @@
+import * as Sentry from "@sentry/browser"
+
+import { ISettingsStorage } from "../../../../shared/settings/types"
+import { KeyValueStorage } from "../../../../shared/storage"
+
+export class SentryWorker {
+ constructor(
+ private readonly baseSentryOptions: Sentry.BrowserOptions,
+ private readonly settingsStore: KeyValueStorage,
+ ) {
+ // init Sentry immediately to capture any exceptions on startup
+ Sentry.init({
+ ...this.baseSentryOptions,
+ enabled: true,
+ })
+ this.settingsStore.subscribe(
+ "privacyErrorReporting",
+ this.onSettingsStoreChange.bind(this),
+ )
+ this.settingsStore.subscribe(
+ "privacyAutomaticErrorReporting",
+ this.onSettingsStoreChange.bind(this),
+ )
+ // re-init with async preferences
+ void this.initSentry()
+ }
+
+ onSettingsStoreChange() {
+ void this.initSentry()
+ }
+
+ async initSentry() {
+ const privacyErrorReporting = await this.settingsStore.get(
+ "privacyErrorReporting",
+ )
+ const privacyAutomaticErrorReporting = await this.settingsStore.get(
+ "privacyAutomaticErrorReporting",
+ )
+ Sentry.init({
+ ...this.baseSentryOptions,
+ enabled: privacyErrorReporting,
+ beforeSend(event) {
+ if (privacyAutomaticErrorReporting) {
+ return event
+ }
+ return null
+ },
+ })
+ }
+}
diff --git a/packages/extension/src/background/__new/services/token/worker/implementation.test.ts b/packages/extension/src/background/__new/services/token/worker/implementation.test.ts
index 1e8098e73..62d0aac7b 100644
--- a/packages/extension/src/background/__new/services/token/worker/implementation.test.ts
+++ b/packages/extension/src/background/__new/services/token/worker/implementation.test.ts
@@ -12,10 +12,12 @@ import { IScheduleService } from "../../../../../shared/schedule/interface"
import {
emitterMock,
recoverySharedServiceMock,
+ sessionServiceMock,
} from "../../../../wallet/test.utils"
import { IBackgroundUIService } from "../../ui/interface"
import { getMockNetwork } from "../../../../../../test/network.mock"
import {
+ getMockApiTokenDetails,
getMockBaseToken,
getMockToken,
getMockTokenPriceDetails,
@@ -49,7 +51,6 @@ describe("TokenWorker", () => {
beforeEach(() => {
// Initialize mocks
mockTokenService = {
- fetchTokensFromBackend: vi.fn(),
updateTokens: vi.fn(),
addToken: vi.fn(),
removeToken: vi.fn(),
@@ -65,6 +66,11 @@ describe("TokenWorker", () => {
updateTokenPrices: vi.fn(),
getFeeTokens: vi.fn(),
getBestFeeToken: vi.fn(),
+ fetchAccountTokenBalancesFromBackend: vi.fn(),
+ getTokensInfoFromBackendForNetwork: vi.fn(),
+ preferFeeToken: vi.fn(),
+ getFeeTokenPreference: vi.fn(),
+ handleProvisionTokens: vi.fn(),
} as Mocked
mockNetworkService = {
@@ -110,6 +116,7 @@ describe("TokenWorker", () => {
mockScheduleService,
mockDebounceService,
mockActivityService,
+ sessionServiceMock,
)
})
@@ -117,33 +124,62 @@ describe("TokenWorker", () => {
it("should fetch tokens for all networks and update the token service", async () => {
// Arrange
const mockNetworks = [
- getMockNetwork({ id: "1" }),
- getMockNetwork({ id: "2" }),
+ getMockNetwork({ id: "mainnet-alpha" }),
+ getMockNetwork({ id: "invalid-backend-network" }),
]
+
const mockTokens = [
- [getMockToken({ address: tokenAddress1, networkId: "1" })],
- [getMockToken({ address: tokenAddress2, networkId: "2" })],
+ getMockToken({ address: tokenAddress1 }),
+ getMockToken({ address: tokenAddress2 }),
+ ]
+
+ const mockApiTokens = [
+ getMockApiTokenDetails({ address: tokenAddress1 }),
+ getMockApiTokenDetails({ address: tokenAddress2 }),
]
+
mockNetworkService.get.mockResolvedValue(mockNetworks)
- mockTokenService.fetchTokensFromBackend
- .mockResolvedValueOnce(mockTokens[0])
- .mockResolvedValueOnce(mockTokens[1])
- await tokenWorker.fetchAndUpdateTokensFromBackend()
+ mockTokenService.getTokensInfoFromBackendForNetwork
+ .mockResolvedValueOnce([mockApiTokens[0]])
+ .mockResolvedValueOnce([mockApiTokens[1]])
+
+ mockTokenService.getTokens
+ .mockResolvedValueOnce([mockTokens[0]])
+ .mockResolvedValueOnce([mockTokens[1]])
+
+ await tokenWorker.refreshTokenRepoWithTokensInfoFromBackend()
expect(mockNetworkService.get).toHaveBeenCalled()
- expect(mockTokenService.fetchTokensFromBackend).toHaveBeenCalledTimes(2)
- expect(mockTokenService.fetchTokensFromBackend).toHaveBeenNthCalledWith(
- 1,
- mockNetworks[0].id,
- )
- expect(mockTokenService.fetchTokensFromBackend).toHaveBeenNthCalledWith(
- 2,
- mockNetworks[1].id,
- )
- expect(mockTokenService.updateTokens).toHaveBeenCalledWith(
- mockTokens.flat(),
- )
+
+ expect(
+ mockTokenService.getTokensInfoFromBackendForNetwork,
+ ).toHaveBeenCalledTimes(2)
+ expect(
+ mockTokenService.getTokensInfoFromBackendForNetwork,
+ ).toHaveBeenNthCalledWith(1, mockNetworks[0].id)
+ expect(
+ mockTokenService.getTokensInfoFromBackendForNetwork,
+ ).toHaveBeenNthCalledWith(2, mockNetworks[1].id)
+
+ // merged tokens
+ expect(mockTokenService.updateTokens).toHaveBeenNthCalledWith(1, [
+ {
+ ...mockApiTokens[0],
+ ...mockTokens[0],
+ },
+ ])
+ expect(mockTokenService.updateTokens).toHaveBeenNthCalledWith(2, [
+ {
+ ...mockApiTokens[1], // from api
+ ...mockTokens[1],
+ },
+ {
+ ...mockApiTokens[1], // from tradable
+ ...mockTokens[1],
+ networkId: "invalid-backend-network",
+ },
+ ])
})
})
diff --git a/packages/extension/src/background/__new/services/token/worker/implementation.ts b/packages/extension/src/background/__new/services/token/worker/implementation.ts
index 1d53f8210..82eae4db2 100644
--- a/packages/extension/src/background/__new/services/token/worker/implementation.ts
+++ b/packages/extension/src/background/__new/services/token/worker/implementation.ts
@@ -1,3 +1,8 @@
+import {
+ isArgentNetworkId,
+ includesAddress,
+ isEqualAddress,
+} from "@argent/shared"
import { RefreshInterval } from "../../../../../shared/config"
import type { IDebounceService } from "../../../../../shared/debounce"
import { defaultNetwork } from "../../../../../shared/network"
@@ -18,21 +23,22 @@ import { BaseWalletAccount } from "../../../../../shared/wallet.model"
import type { WalletStorageProps } from "../../../../../shared/wallet/walletStore"
import { Recovered } from "../../../../wallet/recovery/interface"
import { WalletRecoverySharedService } from "../../../../wallet/recovery/shared.service"
+import { WalletSessionService } from "../../../../wallet/session/session.service"
import {
TokenActivity,
type IActivityService,
type TokenActivityPayload,
+ ProvisionActivity,
} from "../../activity/interface"
import type { IBackgroundUIService } from "../../ui/interface"
import {
every,
everyWhenOpen,
onInstallAndUpgrade,
- onStartup,
} from "../../worker/schedule/decorators"
import { pipe } from "../../worker/schedule/pipe"
-
-const NETWORKS_WITH_BACKEND_SUPPORT = ["goerli-alpha", "mainnet-alpha"]
+import { mergeTokensWithDefaults } from "../../../../../shared/token/__new/repository/mergeTokens"
+import { ProvisionActivityPayload } from "../../../../../shared/activity/types"
/**
* This class is responsible for managing token updates, including token balances and prices.
@@ -49,6 +55,7 @@ export class TokenWorker {
private readonly scheduleService: IScheduleService,
private readonly debounceService: IDebounceService,
private readonly activityService: IActivityService,
+ private readonly sessionService: WalletSessionService,
) {
// Listen for account changes
this.walletStore.subscribe(
@@ -73,6 +80,12 @@ export class TokenWorker {
TokenActivity,
this.onTokenActivity.bind(this),
)
+
+ // Listen to provision
+ this.activityService.emitter.on(
+ ProvisionActivity,
+ this.onProvisionActivity.bind(this),
+ )
}
async getSelectedAccount() {
@@ -84,15 +97,15 @@ export class TokenWorker {
* Update tokens
* Fetches tokens for all networks and updates the token service
*/
- runFetchAndUpdateTokensFromBackend = pipe(
+ runRefreshTokenRepoWithTokensInfoFromBackend = pipe(
onInstallAndUpgrade(this.scheduleService), // This will run the function on update
every(
this.scheduleService,
- RefreshInterval.VERY_SLOW,
- "TokenWorker.updateTokens",
- ), // This will run the function every 24 hours
+ RefreshInterval.SLOW,
+ "TokenWorker.refreshTokenRepoWithTokensInfoFromBackend",
+ ), // This will run the function every 5 mins
)(async (): Promise => {
- await this.fetchAndUpdateTokensFromBackend()
+ await this.refreshTokenRepoWithTokensInfoFromBackend()
})
/**
@@ -119,19 +132,40 @@ export class TokenWorker {
this.backgroundUIService,
this.scheduleService,
this.debounceService,
- RefreshInterval.MEDIUM,
+ RefreshInterval.FAST,
"TokenWorker.fetchAndUpdateTokenPricesFromBackend",
), // This will run the function every minute when the UI is open
)(async (): Promise => {
await this.fetchAndUpdateTokenPricesFromBackend()
})
+ runOnOpenAndUnlocked = pipe(
+ everyWhenOpen(
+ this.backgroundUIService,
+ this.scheduleService,
+ this.debounceService,
+ RefreshInterval.MEDIUM,
+ "TokenWorker.onOpenAndUnlocked",
+ ), // This will run the function when the wallet is opened and unlocked, debounced to one minute
+ )(async () => {
+ const selectedAccount = await this.getSelectedAccount()
+ if (!selectedAccount) {
+ return
+ }
+ await this.runUpdatesForAccount(selectedAccount)
+ })
+
async onSelectedAccountChange(account?: BaseWalletAccount | null) {
if (!account) {
return
}
+ await this.runUpdatesForAccount(account)
+ }
+
+ async runUpdatesForAccount(account: BaseWalletAccount) {
void this.maybeUpdateTokensFromBackendForAccount(account)
void this.updateTokenBalancesFromOnChain(account)
+ void this.discoverTokensFromBackendForAccount(account)
}
async onTransactionRepoChange(changeSet: StorageChange) {
@@ -171,20 +205,43 @@ export class TokenWorker {
void this.fetchAndUpdateTokenPricesFromBackend()
}
- async fetchAndUpdateTokensFromBackend() {
+ async refreshTokenRepoWithTokensInfoFromBackend() {
const networks = await this.networkService.get()
- // Fetch tokens for all networks in parallel
- const tokensFromAllNetworks = await Promise.allSettled(
+ await Promise.allSettled(
networks.map((network) =>
- this.tokenService.fetchTokensFromBackend(network.id),
+ this.refreshTokenRepoWithTokensInfoFromBackendForNetwork(network.id),
),
)
- const tokens = tokensFromAllNetworks
- .map((result) => result.status === "fulfilled" && result.value)
- .filter((t): t is Token[] => Boolean(t))
- .flat()
+ }
- await this.tokenService.updateTokens(tokens)
+ async refreshTokenRepoWithTokensInfoFromBackendForNetwork(networkId: string) {
+ const tokensInfoOnNetwork =
+ await this.tokenService.getTokensInfoFromBackendForNetwork(networkId)
+ if (!tokensInfoOnNetwork) {
+ return
+ }
+ const tokensOnNetwork = await this.tokenService.getTokens(
+ (token) => token.networkId === networkId,
+ )
+ // refresh the local tokens with tokens info
+ const updatedTokens = tokensOnNetwork.map((tokenOnNetwork) => {
+ const tokenInfoOnNetwork = tokensInfoOnNetwork.find(
+ (tokenInfoOnNetwork) =>
+ isEqualAddress(tokenInfoOnNetwork.address, tokenOnNetwork.address),
+ )
+ return tokenInfoOnNetwork
+ ? { ...tokenOnNetwork, ...tokenInfoOnNetwork }
+ : tokenOnNetwork
+ })
+
+ // explicitly filter out tokens that are not tradable
+ const tradableTokens = tokensInfoOnNetwork
+ .filter((token) => token.tradable)
+ .map((t) => ({ ...t, networkId }))
+
+ await this.tokenService.updateTokens(
+ mergeTokensWithDefaults(tradableTokens, updatedTokens),
+ )
}
async updateTokenBalancesFromOnChain(
@@ -201,7 +258,7 @@ export class TokenWorker {
const tokensWithBalance =
await this.tokenService.fetchTokenBalancesFromOnChain(accounts)
- return await this.tokenService.updateTokenBalances(tokensWithBalance)
+ return await this.tokenService.updateTokenBalances(tokensWithBalance) // Update token balances in the token service
}
async maybeUpdateTokensFromBackendForAccount(account: BaseWalletAccount) {
@@ -210,7 +267,7 @@ export class TokenWorker {
)
if (!tokens.length) {
- await this.fetchAndUpdateTokensFromBackend()
+ await this.refreshTokenRepoWithTokensInfoFromBackend()
}
}
@@ -228,7 +285,7 @@ export class TokenWorker {
if (!selectedAccount) {
return
}
- if (NETWORKS_WITH_BACKEND_SUPPORT.includes(selectedAccount.networkId)) {
+ if (isArgentNetworkId(selectedAccount.networkId)) {
return
}
await this.fetchAndUpdateTokenBalancesFromOnChain(selectedAccount)
@@ -254,7 +311,7 @@ export class TokenWorker {
*/
async onRecovered(recoveredAccounts: BaseWalletAccount[]) {
if (recoveredAccounts.length > 0) {
- await this.fetchAndUpdateTokensFromBackend()
+ await this.refreshTokenRepoWithTokensInfoFromBackend()
await this.fetchAndUpdateTokenPricesFromBackend()
await this.fetchAndUpdateTokenBalancesFromOnChain(recoveredAccounts)
}
@@ -267,4 +324,54 @@ export class TokenWorker {
async onTokenActivity({ accounts, tokens }: TokenActivityPayload) {
await this.fetchAndUpdateTokenBalancesFromOnChain(accounts, tokens)
}
+
+ async discoverTokensFromBackendForAccount(account: BaseWalletAccount) {
+ const accountTokenBalancesFromBackend =
+ await this.tokenService.fetchAccountTokenBalancesFromBackend(account)
+
+ const tokensOnNetwork = await this.tokenService.getTokens(
+ (token) => account.networkId === token.networkId,
+ )
+
+ const knownTokenAddresses = tokensOnNetwork.map((token) => token.address)
+
+ const discoveredTokens = accountTokenBalancesFromBackend.filter(
+ (accountTokenBalance) => {
+ return !includesAddress(
+ accountTokenBalance.address,
+ knownTokenAddresses,
+ )
+ },
+ )
+ if (!discoveredTokens.length) {
+ return
+ }
+
+ const tokensInfoOnNetwork =
+ await this.tokenService.getTokensInfoFromBackendForNetwork(
+ account.networkId,
+ )
+ if (!tokensInfoOnNetwork) {
+ return
+ }
+ /** both sets of tokens are already on the same network */
+ const discoveredTokensInfo: Token[] = []
+ discoveredTokens.forEach((discoveredToken) => {
+ const tokenInfo = tokensInfoOnNetwork.find((tokenInfo) =>
+ isEqualAddress(discoveredToken.address, tokenInfo.address),
+ )
+ if (tokenInfo) {
+ discoveredTokensInfo.push({
+ ...tokenInfo,
+ networkId: account.networkId,
+ })
+ }
+ })
+ await this.tokenService.addToken(discoveredTokensInfo)
+ }
+
+ async onProvisionActivity(payload: ProvisionActivityPayload) {
+ await this.tokenService.handleProvisionTokens(payload)
+ await this.refreshTokenRepoWithTokensInfoFromBackend()
+ }
}
diff --git a/packages/extension/src/background/__new/services/token/worker/index.ts b/packages/extension/src/background/__new/services/token/worker/index.ts
index 7c065d3d2..f200df4fb 100644
--- a/packages/extension/src/background/__new/services/token/worker/index.ts
+++ b/packages/extension/src/background/__new/services/token/worker/index.ts
@@ -1,7 +1,10 @@
import { activityService } from "../../activity"
import { backgroundUIService } from "../../ui"
import { transactionsRepo } from "../../../../../shared/transactions/store"
-import { recoverySharedService } from "../../../../walletSingleton"
+import {
+ recoverySharedService,
+ sessionService,
+} from "../../../../walletSingleton"
import { debounceService } from "../../../../../shared/debounce"
import { networkService } from "../../../../../shared/network/service"
import { chromeScheduleService } from "../../../../../shared/schedule"
@@ -22,4 +25,5 @@ export const tokenWorker = new TokenWorker(
chromeScheduleService,
debounceService,
activityService,
+ sessionService,
)
diff --git a/packages/extension/src/background/__new/services/transactionReview/background.test.ts b/packages/extension/src/background/__new/services/transactionReview/background.test.ts
new file mode 100644
index 000000000..6bd9eda32
--- /dev/null
+++ b/packages/extension/src/background/__new/services/transactionReview/background.test.ts
@@ -0,0 +1,222 @@
+import { Address, IHttpService } from "@argent/shared"
+import { Account, EstimateFee } from "starknet6"
+import { Mocked, describe, expect, test, vi } from "vitest"
+
+import type { KeyValueStorage } from "../../../../shared/storage"
+import type {
+ ITransactionReviewLabelsStore,
+ TransactionReviewTransactions,
+} from "../../../../shared/transactionReview/interface"
+import type { WalletAccount } from "../../../../shared/wallet.model"
+import type { Wallet } from "../../../wallet"
+import BackgroundTransactionReviewService from "./background"
+import type { ITransactionReviewWorker } from "./worker/interface"
+
+import sendFixture from "../../../../shared/transactionReview/__fixtures__/send.json"
+import simulationErrorUnexpectedFixture from "../../../../shared/transactionReview/__fixtures__/simulation-error-unexpected.json"
+
+describe("BackgroundTransactionReviewService", () => {
+ const makeService = () => {
+ const walletSingleton = {
+ getSelectedAccount: vi.fn(),
+ getSelectedStarknetAccount: vi.fn(),
+ } as unknown as Mocked
+
+ const httpService = {
+ get: vi.fn(),
+ post: vi.fn(),
+ } as unknown as Mocked
+
+ const transactionReviewLabelsStore = {
+ get: vi.fn(),
+ set: vi.fn(),
+ subscribe: vi.fn(),
+ } as unknown as Mocked>
+
+ const transactionReviewWorker = {
+ maybeUpdateLabels: vi.fn(),
+ } as unknown as Mocked
+
+ const backgroundTransactionReviewService =
+ new BackgroundTransactionReviewService(
+ walletSingleton,
+ httpService,
+ transactionReviewLabelsStore,
+ transactionReviewWorker,
+ )
+
+ const networkId = "goerli-alpha"
+
+ walletSingleton.getSelectedAccount.mockResolvedValue({
+ address: "0x123",
+ networkId,
+ network: {
+ id: networkId,
+ },
+ } as WalletAccount)
+
+ const starknetAccount = {
+ cairoVersion: "1",
+ getNonce: vi.fn(),
+ getChainId: vi.fn(),
+ estimateFee: vi.fn(),
+ } as unknown as Mocked
+
+ starknetAccount.estimateFee.mockResolvedValue({
+ gas_consumed: 123n,
+ gas_price: 456n,
+ } as EstimateFee)
+
+ walletSingleton.getSelectedStarknetAccount.mockResolvedValue(
+ starknetAccount,
+ )
+
+ const feeTokenAddress: Address = "0x123456"
+
+ return {
+ backgroundTransactionReviewService,
+ walletSingleton,
+ httpService,
+ transactionReviewLabelsStore,
+ transactionReviewWorker,
+ feeTokenAddress,
+ starknetAccount,
+ }
+ }
+ describe("simulateAndReview", () => {
+ describe("when backend returns success", () => {
+ describe("and there are no errors", () => {
+ test("returns simulation and review", async () => {
+ const {
+ backgroundTransactionReviewService,
+ httpService,
+ feeTokenAddress,
+ } = makeService()
+
+ const transactions: TransactionReviewTransactions[] = [
+ {
+ type: "INVOKE",
+ calls: [],
+ },
+ ]
+
+ httpService.post.mockResolvedValueOnce(sendFixture)
+
+ const result =
+ await backgroundTransactionReviewService.simulateAndReview({
+ transactions,
+ feeTokenAddress,
+ })
+
+ expect(result).toMatchObject(sendFixture)
+
+ expect(result.enrichedFeeEstimation).toMatchInlineSnapshot(`
+ {
+ "deployment": undefined,
+ "transactions": {
+ "amount": 1674n,
+ "feeTokenAddress": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
+ "max": {
+ "maxFee": 3348008526928n,
+ },
+ "pricePerUnit": 1000000009n,
+ },
+ }
+ `)
+ })
+ })
+ describe("and there are errors", () => {
+ test("falls back to on-chain simulation", async () => {
+ const {
+ backgroundTransactionReviewService,
+ httpService,
+ feeTokenAddress,
+ starknetAccount,
+ } = makeService()
+
+ const transactions: TransactionReviewTransactions[] = [
+ {
+ type: "INVOKE",
+ calls: [],
+ },
+ ]
+
+ httpService.post.mockResolvedValueOnce(
+ simulationErrorUnexpectedFixture,
+ )
+
+ const fallbackToOnchainFeeEstimationSpy = vi.spyOn(
+ backgroundTransactionReviewService,
+ "fallbackToOnchainFeeEstimation",
+ )
+
+ const result =
+ await backgroundTransactionReviewService.simulateAndReview({
+ transactions,
+ feeTokenAddress,
+ })
+
+ expect(fallbackToOnchainFeeEstimationSpy).toHaveBeenCalledOnce()
+
+ expect(starknetAccount.estimateFee).toHaveBeenCalledOnce()
+
+ expect(result).toMatchObject({
+ isBackendDown: true,
+ enrichedFeeEstimation: {
+ transactions: {
+ amount: 123n,
+ feeTokenAddress,
+ pricePerUnit: 456n,
+ },
+ },
+ })
+ })
+ })
+ describe("when backend fails with error", () => {
+ test("falls back to on-chain simulation", async () => {
+ const {
+ backgroundTransactionReviewService,
+ httpService,
+ feeTokenAddress,
+ starknetAccount,
+ } = makeService()
+
+ const transactions: TransactionReviewTransactions[] = [
+ {
+ type: "INVOKE",
+ calls: [],
+ },
+ ]
+
+ httpService.post.mockRejectedValueOnce(new Error())
+
+ const fallbackToOnchainFeeEstimationSpy = vi.spyOn(
+ backgroundTransactionReviewService,
+ "fallbackToOnchainFeeEstimation",
+ )
+
+ const result =
+ await backgroundTransactionReviewService.simulateAndReview({
+ transactions,
+ feeTokenAddress,
+ })
+
+ expect(fallbackToOnchainFeeEstimationSpy).toHaveBeenCalledOnce()
+
+ expect(starknetAccount.estimateFee).toHaveBeenCalledOnce()
+
+ expect(result).toMatchObject({
+ isBackendDown: true,
+ enrichedFeeEstimation: {
+ transactions: {
+ amount: 123n,
+ feeTokenAddress,
+ pricePerUnit: 456n,
+ },
+ },
+ })
+ })
+ })
+ })
+ })
+})
diff --git a/packages/extension/src/background/__new/services/transactionReview/background.ts b/packages/extension/src/background/__new/services/transactionReview/background.ts
index 4f45459e8..adfcdbb0d 100644
--- a/packages/extension/src/background/__new/services/transactionReview/background.ts
+++ b/packages/extension/src/background/__new/services/transactionReview/background.ts
@@ -1,6 +1,6 @@
import urlJoin from "url-join"
-import { type IHttpService, ensureArray } from "@argent/shared"
+import { type IHttpService, ensureArray, Address } from "@argent/shared"
import {
Account,
CairoVersion,
@@ -8,9 +8,8 @@ import {
Calldata,
Invocations,
TransactionType,
- hash,
num,
-} from "starknet"
+} from "starknet6"
import type {
ITransactionReviewLabelsStore,
@@ -31,8 +30,10 @@ import { EstimatedFees } from "../../../../shared/transactionSimulation/fees/fee
import { KeyValueStorage } from "../../../../shared/storage"
import { ITransactionReviewWorker } from "./worker/interface"
import { ARGENT_TRANSACTION_REVIEW_API_BASE_URL } from "../../../../shared/api/constants"
-import { ETH_TOKEN_ADDRESS } from "../../../../shared/network/constants"
import { getEstimatedFeeFromSimulationAndRespectWatermarkFee } from "../../../../shared/transactionSimulation/utils"
+import { getTxVersionFromFeeToken } from "../../../../shared/utils/getTransactionVersion"
+import { isArgentNetwork } from "../../../../shared/network/utils"
+import { getNonce } from "../../../nonce"
interface ApiTransactionReviewV2RequestBody {
transactions: Array<{
@@ -66,10 +67,12 @@ export default class BackgroundTransactionReviewService
starknetAccount,
calls,
isDeployed,
+ feeTokenAddress,
}: {
starknetAccount: Account
calls: Call[]
isDeployed: boolean
+ feeTokenAddress: Address
}) {
try {
const selectedAccount = await this.wallet.getSelectedAccount()
@@ -78,9 +81,11 @@ export default class BackgroundTransactionReviewService
throw new AccountError({ code: "NOT_FOUND" })
}
+ const version = getTxVersionFromFeeToken(feeTokenAddress)
+
const fees: EstimatedFees = {
transactions: {
- feeTokenAddress: ETH_TOKEN_ADDRESS,
+ feeTokenAddress,
amount: 0n,
pricePerUnit: 0n,
},
@@ -100,11 +105,14 @@ export default class BackgroundTransactionReviewService
payload: calls,
},
]
- const [deployEstimate, txEstimate] =
- await starknetAccount.estimateFeeBulk(bulkTransactions, {
- skipValidate: true,
+ const [deployEstimate, txEstimate] = await starknetAccount
+ .estimateFeeBulk(bulkTransactions, {
+ version,
+ })
+ .catch((error) => {
+ console.error(error)
+ throw error
})
-
if (
!deployEstimate.gas_consumed ||
!deployEstimate.gas_price ||
@@ -118,12 +126,12 @@ export default class BackgroundTransactionReviewService
}
fees.deployment = {
- feeTokenAddress: ETH_TOKEN_ADDRESS,
+ feeTokenAddress,
amount: deployEstimate.gas_consumed,
pricePerUnit: deployEstimate.gas_price,
}
fees.transactions = {
- feeTokenAddress: ETH_TOKEN_ADDRESS,
+ feeTokenAddress,
amount: txEstimate.gas_consumed,
pricePerUnit: txEstimate.gas_price,
}
@@ -133,6 +141,7 @@ export default class BackgroundTransactionReviewService
calls,
{
skipValidate: true,
+ version,
},
)
@@ -144,13 +153,16 @@ export default class BackgroundTransactionReviewService
}
fees.transactions = {
- feeTokenAddress: ETH_TOKEN_ADDRESS,
+ feeTokenAddress,
amount: gas_consumed,
pricePerUnit: gas_price,
}
}
- await addEstimatedFee(fees, calls)
+ await addEstimatedFee(fees, {
+ type: TransactionType.INVOKE,
+ payload: calls,
+ })
return fees
} catch (error) {
@@ -216,26 +228,50 @@ export default class BackgroundTransactionReviewService
simulateAndReviewResult,
)
- await addEstimatedFee(
- fee,
- initialTransactions[isDeploymentTransaction ? 1 : 0].calls ?? [],
- )
+ await addEstimatedFee(fee, {
+ type: TransactionType.INVOKE,
+ payload: initialTransactions[isDeploymentTransaction ? 1 : 0].calls ?? [],
+ })
return fee
}
async simulateAndReview({
transactions,
+ feeTokenAddress,
}: {
transactions: TransactionReviewTransactions[]
+ feeTokenAddress: Address
}) {
+ const selectedAccount = await this.wallet.getSelectedAccount()
const account = await this.wallet.getSelectedStarknetAccount()
- const isDeploymentTransaction = Boolean(
- transactions.find((tx) => tx.type === "DEPLOY_ACCOUNT"),
+ const isDeploymentTransaction = transactions.some(
+ (tx) => tx.type === "DEPLOY_ACCOUNT",
)
+
+ if (!selectedAccount) {
+ throw new AccountError({ code: "NOT_SELECTED" })
+ }
+
try {
- const nonce = isDeploymentTransaction ? "0x0" : await account.getNonce()
- const version = num.toHex(hash.feeTransactionVersion)
+ if (!isArgentNetwork(selectedAccount?.network)) {
+ // If it's not an argent network we fallback to onchain fee estimation
+ console.warn(
+ `Falling back to onchain fee estimation as ${selectedAccount?.network.id} is not an argent network`,
+ )
+ return this.fallbackToOnchainFeeEstimation({
+ account,
+ transactions,
+ isDeploymentTransaction,
+ feeTokenAddress,
+ })
+ }
+
+ const version = getTxVersionFromFeeToken(feeTokenAddress)
+
+ const nonce = isDeploymentTransaction
+ ? "0x0"
+ : await getNonce(selectedAccount, account)
if (!("getChainId" in account)) {
throw new AccountError({
@@ -263,10 +299,10 @@ export default class BackgroundTransactionReviewService
}),
),
}
+
const result = await this.httpService.post(
simulateAndReviewEndpoint,
{
- method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
@@ -276,14 +312,18 @@ export default class BackgroundTransactionReviewService
simulateAndReviewSchema,
)
- // if there is a simulation error then there is also no actual simulation
- // or fee information, and no way to proceed with fee estimation
- // returning the result will surface the error to the user in the ui
+ // if there is any simulation error then we should fall-back to on-chain so the user is not blocked
const hasSimulationError = result.transactions.some((transaction) =>
isTransactionSimulationError(transaction),
)
if (hasSimulationError) {
- return result
+ console.warn(
+ `Falling back to onchain fee estimation as there was an error in the backend simulation response:`,
+ result,
+ )
+ throw new ReviewError({
+ code: "BACKEND_SIMULATION_ERROR",
+ })
}
const enrichedFeeEstimation = await this.getEnrichedFeeEstimation(
@@ -297,34 +337,54 @@ export default class BackgroundTransactionReviewService
}
} catch (e) {
console.error(e)
- try {
- const invokeCalls = isDeploymentTransaction
- ? this.getCallsFromTx(transactions[1])
- : this.getCallsFromTx(transactions[0])
+ return this.fallbackToOnchainFeeEstimation({
+ transactions,
+ account,
+ isDeploymentTransaction,
+ feeTokenAddress,
+ })
+ }
+ }
- if (!invokeCalls) {
- throw new ReviewError({
- code: "NO_CALLS_FOUND",
- })
- }
- // Backend is failing we use the fallback method to estimate fees
- const enrichedFeeEstimation = await this.fetchFeesOnchain({
- starknetAccount: account,
- calls: invokeCalls,
- isDeployed: !isDeploymentTransaction,
- })
- return {
- transactions: [],
- enrichedFeeEstimation,
- isBackendDown: true,
- }
- } catch (error) {
- console.error(error)
+ async fallbackToOnchainFeeEstimation({
+ transactions,
+ account,
+ isDeploymentTransaction,
+ feeTokenAddress,
+ }: {
+ transactions: TransactionReviewTransactions[]
+ account: Account
+ isDeploymentTransaction: boolean
+ feeTokenAddress: Address
+ }) {
+ try {
+ const invokeCalls = isDeploymentTransaction
+ ? this.getCallsFromTx(transactions[1])
+ : this.getCallsFromTx(transactions[0])
+
+ if (!invokeCalls) {
throw new ReviewError({
- message: `${error}`,
- code: "SIMULATE_AND_REVIEW_FAILED",
+ code: "NO_CALLS_FOUND",
})
}
+ // Backend is failing we use the fallback method to estimate fees
+ const enrichedFeeEstimation = await this.fetchFeesOnchain({
+ starknetAccount: account,
+ calls: invokeCalls,
+ isDeployed: !isDeploymentTransaction,
+ feeTokenAddress,
+ })
+ return {
+ transactions: [],
+ enrichedFeeEstimation,
+ isBackendDown: true,
+ }
+ } catch (error) {
+ console.error(error)
+ throw new ReviewError({
+ message: `${error}`,
+ code: "SIMULATE_AND_REVIEW_FAILED",
+ })
}
}
diff --git a/packages/extension/src/background/__new/services/worker/schedule/decorators.test.ts b/packages/extension/src/background/__new/services/worker/schedule/decorators.test.ts
index c21d3c273..7e9ead5b8 100644
--- a/packages/extension/src/background/__new/services/worker/schedule/decorators.test.ts
+++ b/packages/extension/src/background/__new/services/worker/schedule/decorators.test.ts
@@ -8,9 +8,13 @@ import {
onlyIfOpen,
debounce,
everyWhenOpen,
+ onUnlocked,
+ onlyIfUnlocked,
+ everyWhenOpenAndUnlocked,
} from "./decorators"
import { getMockBackgroundUIService } from "./mockBackgroundUIService"
import { getMockDebounceService } from "../../../../../shared/debounce/mock"
+import { getMockSessionService } from "./mockSessionService"
describe("decorators", () => {
describe("onStartup", () => {
@@ -90,6 +94,42 @@ describe("decorators", () => {
})
})
+ describe("onUnlock", async () => {
+ test("should call the function when the session service is unlocked", async () => {
+ const [mockSessionServiceManager, mockSessionService] =
+ getMockSessionService()
+ const fn = vi.fn()
+ onUnlocked(mockSessionService)(fn)
+ expect(fn).toHaveBeenCalledTimes(0)
+ await mockSessionServiceManager.setLocked(false)
+ expect(fn).toHaveBeenCalledTimes(1)
+ await mockSessionServiceManager.setLocked(true)
+ expect(fn).toHaveBeenCalledTimes(1)
+ await mockSessionServiceManager.setLocked(false)
+ expect(fn).toHaveBeenCalledTimes(2)
+ })
+ })
+
+ describe("onlyIfUnlocked", async () => {
+ test("should call the function when the session service is unlocked", async () => {
+ const [mockSessionServiceManager, mockSessionService] =
+ getMockSessionService()
+ const fn = vi.fn()
+ const oiuFn = onlyIfUnlocked(mockSessionService)(fn)
+ await oiuFn()
+ expect(fn).toHaveBeenCalledTimes(1)
+ await mockSessionServiceManager.setLocked(true)
+ await oiuFn()
+ expect(fn).toHaveBeenCalledTimes(1)
+ await mockSessionServiceManager.setLocked(false)
+ await oiuFn()
+ expect(fn).toHaveBeenCalledTimes(2)
+ await mockSessionServiceManager.setLocked(true)
+ await oiuFn()
+ expect(fn).toHaveBeenCalledTimes(2)
+ })
+ })
+
describe("debounce", () => {
test("should call the function when not in debounce interval", async () => {
const debounceService = getMockDebounceService()
@@ -163,4 +203,75 @@ describe("decorators", () => {
expect(fn).toHaveBeenCalledTimes(4)
})
})
+
+ describe("everyWhenOpenAndUnlocked", () => {
+ test("should call the function when the background ui service is opened and the session service is unlocked", async () => {
+ const [mockBackgroundUIServiceManager, mockBackgroundUIService] =
+ getMockBackgroundUIService()
+ const [scheduleServiceManager, scheduleService] =
+ createScheduleServiceMock()
+ const [mockSessionServiceManager, mockSessionService] =
+ getMockSessionService()
+
+ const debounceService = getMockDebounceService()
+ const fn = vi.fn()
+ const fnExec = everyWhenOpenAndUnlocked(
+ mockBackgroundUIService,
+ scheduleService,
+ mockSessionService,
+ debounceService,
+ 1,
+ "test",
+ )(fn)
+
+ // wait 1 loop
+ await new Promise((resolve) => setTimeout(resolve, 0))
+
+ // test scheduleService
+ expect(scheduleService.registerImplementation).toHaveBeenCalledWith({
+ id: expect.stringContaining("every@1s:"),
+ callback: expect.any(Function),
+ })
+ expect(scheduleService.every).toBeCalledTimes(1)
+
+ // test onLocked and onOpen
+ await mockSessionServiceManager.setLocked(true)
+ expect(fn).toHaveBeenCalledTimes(0)
+ await mockBackgroundUIServiceManager.setOpened(true)
+ expect(fn).toHaveBeenCalledTimes(0)
+ await mockSessionServiceManager.setLocked(false)
+ expect(fn).toHaveBeenCalledTimes(1)
+ await mockBackgroundUIServiceManager.setOpened(false)
+ expect(fn).toHaveBeenCalledTimes(1)
+ await mockBackgroundUIServiceManager.setOpened(true)
+ expect(fn).toHaveBeenCalledTimes(2)
+ await mockBackgroundUIServiceManager.setOpened(false)
+ await mockSessionServiceManager.setLocked(true)
+ await mockBackgroundUIServiceManager.setOpened(true)
+ expect(fn).toHaveBeenCalledTimes(2)
+ await mockSessionServiceManager.setLocked(false)
+ expect(fn).toHaveBeenCalledTimes(3)
+
+ // test scheduleService
+ await scheduleServiceManager.fireAll("every")
+ expect(fn).toHaveBeenCalledTimes(4)
+
+ // test debounce
+ expect(debounceService.debounce).toHaveBeenCalledTimes(4)
+ await fnExec()
+ expect(debounceService.debounce).toHaveBeenCalledTimes(5)
+ expect(debounceService.debounce).toHaveBeenCalledWith({
+ id: expect.stringContaining("debounce@1s:"),
+ debounce: 1,
+ callback: expect.any(Function),
+ })
+ expect(fn).toHaveBeenCalledTimes(5)
+
+ // does not call debounce when not open
+ await mockBackgroundUIServiceManager.setOpened(false)
+ await fnExec()
+ expect(debounceService.debounce).toHaveBeenCalledTimes(5)
+ expect(fn).toHaveBeenCalledTimes(5)
+ })
+ })
})
diff --git a/packages/extension/src/background/__new/services/worker/schedule/decorators.ts b/packages/extension/src/background/__new/services/worker/schedule/decorators.ts
index 90cf2295d..e2dee126c 100644
--- a/packages/extension/src/background/__new/services/worker/schedule/decorators.ts
+++ b/packages/extension/src/background/__new/services/worker/schedule/decorators.ts
@@ -1,10 +1,22 @@
import { IDebounceService } from "../../../../../shared/debounce"
import { IScheduleService } from "../../../../../shared/schedule/interface"
+import { Locked } from "../../../../wallet/session/interface"
+import { WalletSessionService } from "../../../../wallet/session/session.service"
+import { IKeyValueStorage } from "../../../../../shared/storage"
+import { WalletStorageProps } from "../../../../wallet/backup/backup.service"
import { IBackgroundUIService, Opened } from "../../ui/interface"
import { pipe } from "./pipe"
type Fn = (...args: unknown[]) => Promise
+export const onAccountChanged =
+ (walletStore: IKeyValueStorage) =>
+ (fn: T): T => {
+ walletStore.subscribe("selected", fn)
+
+ return fn
+ }
+
/**
* Function to schedule a task on startup.
* @param {IScheduleService} scheduleService - The schedule service.
@@ -68,6 +80,10 @@ export type MinimalIBackgroundUIService = Pick<
"opened" | "emitter"
>
+export type MinimalWalletSessionService = Pick<
+ WalletSessionService,
+ "locked" | "emitter"
+>
/**
* Function to schedule a task to run when the UI is opened.
* @param {IBackgroundUIService} backgroundUIService - The background UI service.
@@ -97,6 +113,26 @@ export const onClose =
return fn
}
+export const onUnlocked =
+ (sessionService: MinimalWalletSessionService) =>
+ (fn: T): T => {
+ sessionService.emitter.on(Locked, async (locked) => {
+ if (!locked) {
+ await fn()
+ }
+ })
+
+ return fn
+ }
+
+export const onlyIfUnlocked =
+ (sessionService: MinimalWalletSessionService) =>
+ (fn: T): T => {
+ return ((...args: unknown[]) => {
+ return !sessionService.locked ? fn(...args) : noopAs(fn)(...args)
+ }) as T
+ }
+
function noopAs(_fn: T): T {
const noop = () => {}
return noop as T
@@ -160,3 +196,46 @@ export const everyWhenOpen = (
every(scheduleService, seconds, name),
)
}
+
+/**
+ * Function to run a task when the wallet is opened and unlocked, debounced by specified seconds
+ */
+
+export const whenOpenAndUnlocked = (
+ backgroundUIService: MinimalIBackgroundUIService,
+ sessionService: MinimalWalletSessionService,
+ debounceService: IDebounceService,
+ seconds: number,
+ name: string,
+) => {
+ return pipe(
+ debounce(debounceService, seconds, name),
+ onlyIfOpen(backgroundUIService),
+ onlyIfUnlocked(sessionService),
+ onOpen(backgroundUIService),
+ onUnlocked(sessionService),
+ )
+}
+
+/**
+ * Function to schedule a task to run every specified seconds when the UI is opened and unlocked
+ */
+
+export const everyWhenOpenAndUnlocked = (
+ backgroundUIService: MinimalIBackgroundUIService,
+ scheduleService: IScheduleService,
+ sessionService: MinimalWalletSessionService,
+ debounceService: IDebounceService,
+ seconds: number,
+ name: string,
+) => {
+ return pipe(
+ debounce(debounceService, seconds, name),
+ onlyIfOpen(backgroundUIService),
+ onlyIfUnlocked(sessionService),
+ onOpen(backgroundUIService),
+ onUnlocked(sessionService),
+ onInstallAndUpgrade(scheduleService),
+ every(scheduleService, seconds, name),
+ )
+}
diff --git a/packages/extension/src/background/__new/services/worker/schedule/mockSessionService.ts b/packages/extension/src/background/__new/services/worker/schedule/mockSessionService.ts
new file mode 100644
index 000000000..bc37613d1
--- /dev/null
+++ b/packages/extension/src/background/__new/services/worker/schedule/mockSessionService.ts
@@ -0,0 +1,31 @@
+import Emittery from "emittery"
+import { MinimalWalletSessionService } from "./decorators"
+import { Events, Locked } from "../../../../wallet/session/interface"
+
+interface MockSssionServiceManager {
+ setLocked(locked: boolean): Promise
+}
+
+export const getMockSessionService = (): [
+ MockSssionServiceManager,
+ MinimalWalletSessionService,
+] => {
+ const emitter = new Emittery()
+ let locked = false
+ const setLocked = (newLocked: boolean) => {
+ locked = newLocked
+ return emitter.emit(Locked, locked)
+ }
+ const sessionService: MinimalWalletSessionService = {
+ get locked() {
+ return locked
+ },
+ emitter,
+ }
+ return [
+ {
+ setLocked,
+ },
+ sessionService,
+ ]
+}
diff --git a/packages/extension/src/background/__new/trpc.ts b/packages/extension/src/background/__new/trpc.ts
index 339fd092b..ce45d4384 100644
--- a/packages/extension/src/background/__new/trpc.ts
+++ b/packages/extension/src/background/__new/trpc.ts
@@ -13,6 +13,10 @@ import type { IStarknetAddressService } from "@argent/shared"
import type { INetworkService } from "../../shared/network/service/interface"
import { ISharedSwapService } from "../../shared/swap/service/interface"
import superjson from "superjson"
+import { ITokenService } from "../../shared/token/__new/service/interface"
+import { IRiskAssessmentService } from "../../shared/riskAssessment/interface"
+import { IFeeTokenService } from "../../shared/feeToken/service/interface"
+import { IProvisionService } from "../../shared/provision/interface"
interface Context {
sender?: chrome.runtime.MessageSender
@@ -26,7 +30,11 @@ interface Context {
recoveryService: IRecoveryService
starknetAddressService: IStarknetAddressService
swapService: ISharedSwapService
+ tokenService: ITokenService
+ feeTokenService: IFeeTokenService
networkService: INetworkService
+ riskAssessmentService: IRiskAssessmentService
+ provisionService: IProvisionService
}
}
diff --git a/packages/extension/src/background/accountDeployAction.ts b/packages/extension/src/background/accountDeployAction.ts
index 09a8a243f..8546704e4 100644
--- a/packages/extension/src/background/accountDeployAction.ts
+++ b/packages/extension/src/background/accountDeployAction.ts
@@ -1,11 +1,14 @@
import { ExtensionActionItemOfType } from "../shared/actionQueue/types"
+import { IFeeTokenService } from "../shared/feeToken/service/interface"
import { addTransaction } from "../shared/transactions/store"
import { checkTransactionHash } from "../shared/transactions/utils"
+import { getTxVersionFromFeeToken } from "../shared/utils/getTransactionVersion"
import { Wallet } from "./wallet"
export const accountDeployAction = async (
action: ExtensionActionItemOfType<"DEPLOY_ACCOUNT">,
wallet: Wallet,
+ feeTokenService: IFeeTokenService,
) => {
if (!(await wallet.isSessionOpen())) {
throw Error("you need an open session")
@@ -19,7 +22,12 @@ export const accountDeployAction = async (
throw Error("Account already deployed")
}
- const { account, txHash } = await wallet.deployAccount(selectedAccount)
+ const bestFeeToken = await feeTokenService.getBestFeeToken(selectedAccount)
+ const version = getTxVersionFromFeeToken(bestFeeToken.address)
+
+ const { account, txHash } = await wallet.deployAccount(selectedAccount, {
+ version,
+ })
if (!checkTransactionHash(txHash)) {
throw Error(
diff --git a/packages/extension/src/background/accountUpgrade.ts b/packages/extension/src/background/accountUpgrade.ts
index 00a59d006..465e04b8d 100644
--- a/packages/extension/src/background/accountUpgrade.ts
+++ b/packages/extension/src/background/accountUpgrade.ts
@@ -5,7 +5,7 @@ import { ArgentAccountType, BaseWalletAccount } from "../shared/wallet.model"
import { IBackgroundActionService } from "./__new/services/action/interface"
import { Wallet } from "./wallet"
import { AccountError } from "../shared/errors/account"
-import { isAccountV5 } from "@argent/shared"
+import { addressSchema, isAccountV5 } from "@argent/shared"
export interface IUpgradeAccount {
account: BaseWalletAccount
wallet: Wallet
@@ -40,18 +40,22 @@ export const upgradeAccount = async ({
const implementationClassHash =
newImplementation[accountTypeWithCairo0Check] ?? newImplementation.standard
+ const parsedImplClassHash = addressSchema.parse(implementationClassHash)
+
if (!isAccountV5(starknetAccount)) {
throw new AccountError({ code: "UPGRADE_NOT_SUPPORTED" })
}
const upgradeCalldata = {
- implementation: implementationClassHash,
+ implementation: parsedImplClassHash,
// new starknet accounts have a new upgrade interface to allow for transactions right after upgrade
calldata: [0],
}
const calldata = CallData.compile(upgradeCalldata)
- await actionService.add(
+
+ // Always add upgrade transaction to the front of the queue
+ await actionService.addFront(
{
type: "TRANSACTION",
payload: {
@@ -60,7 +64,10 @@ export const upgradeAccount = async ({
entrypoint: "upgrade",
calldata,
},
- meta: { isUpgrade: true, title: "Switch account type" },
+ meta: {
+ title: "Switch account type",
+ newClassHash: parsedImplClassHash,
+ },
},
},
{
diff --git a/packages/extension/src/background/actionHandlers.ts b/packages/extension/src/background/actionHandlers.ts
index 56310c346..516be5b49 100644
--- a/packages/extension/src/background/actionHandlers.ts
+++ b/packages/extension/src/background/actionHandlers.ts
@@ -19,6 +19,7 @@ import { Wallet } from "./wallet"
import { preAuthorizationService } from "../shared/preAuthorization/service"
import { networkSchema } from "../shared/network"
import { encodeChainId } from "../shared/utils/encodeChainId"
+import { IFeeTokenService } from "../shared/feeToken/service/interface"
const handleTransactionAction = async ({
action,
@@ -66,6 +67,7 @@ const handleTransactionAction = async ({
export const handleActionApproval = async (
action: ExtensionActionItem,
wallet: Wallet,
+ feeTokenService: IFeeTokenService,
): Promise => {
const actionHash = action.meta.hash
const selectedAccount = await wallet.getSelectedAccount()
@@ -94,7 +96,11 @@ export const handleActionApproval = async (
}
case "TRANSACTION": {
- return handleTransactionAction({ action, networkId, wallet })
+ return handleTransactionAction({
+ action,
+ networkId,
+ wallet,
+ })
}
case "DEPLOY_ACCOUNT": {
@@ -103,7 +109,11 @@ export const handleActionApproval = async (
// networkId,
// }) // TODO: temporary disabled
- const txHash = await accountDeployAction(action, wallet)
+ const txHash = await accountDeployAction(
+ action,
+ wallet,
+ feeTokenService,
+ )
void analytics.track("deployAccount", {
status: "success",
diff --git a/packages/extension/src/background/background.ts b/packages/extension/src/background/background.ts
index 6794900ad..9a78acd84 100644
--- a/packages/extension/src/background/background.ts
+++ b/packages/extension/src/background/background.ts
@@ -5,11 +5,13 @@ import type { MessagingKeys } from "./keys/messagingKeys"
import type { Respond } from "./respond"
import { Wallet } from "./wallet"
import { TransactionTrackerWorker } from "./transactions/service/starknet.service"
+import { IFeeTokenService } from "../shared/feeToken/service/interface"
export interface BackgroundService {
wallet: Wallet
transactionTrackerWorker: TransactionTrackerWorker
actionService: IBackgroundActionService
+ feeTokenService: IFeeTokenService
}
export class UnhandledMessage extends Error {
diff --git a/packages/extension/src/background/devnet/declareAccounts.ts b/packages/extension/src/background/devnet/declareAccounts.ts
index 57280dce0..44c5cc4d2 100644
--- a/packages/extension/src/background/devnet/declareAccounts.ts
+++ b/packages/extension/src/background/devnet/declareAccounts.ts
@@ -4,10 +4,6 @@ import urlJoin from "url-join"
import { Network, getProvider } from "../../shared/network"
import { LoadContracts } from "../wallet/loadContracts"
-import {
- ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES,
- PROXY_CONTRACT_CLASS_HASHES,
-} from "../wallet/starknet.constants"
interface PreDeployedAccount {
address: string
@@ -71,7 +67,7 @@ export const declareContracts = memoize(
if (!isProxyClassDeclared) {
const proxy = await deployAccount.declare({
- classHash: PROXY_CONTRACT_CLASS_HASHES[0],
+ classHash: computedProxyClassHash,
contract: proxyContract,
})
@@ -84,7 +80,7 @@ export const declareContracts = memoize(
if (!isAccountClassDeclared) {
const account = await deployAccount.declare({
- classHash: ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES[0],
+ classHash: computedAccountClassHash,
contract: accountContract,
})
diff --git a/packages/extension/src/background/index.ts b/packages/extension/src/background/index.ts
index 5ded3ae6b..cc72db17e 100644
--- a/packages/extension/src/background/index.ts
+++ b/packages/extension/src/background/index.ts
@@ -1,6 +1,15 @@
-import browser from "webextension-polyfill"
import * as Sentry from "@sentry/browser"
+import browser from "webextension-polyfill"
+
import { getBrowserAction } from "../shared/browser"
+import { sentryWorker } from "./__new/services/sentry"
+
+try {
+ // Try to start Sentry immediately
+ initSentryWorker()
+} catch (error) {
+ console.error("Exception while initialising sentryWorker", error)
+}
try {
// catch any errors from init.ts
@@ -16,3 +25,10 @@ try {
})
}, 0)
}
+
+// Prevent tree-shaking unused worker variables
+function initSentryWorker() {
+ return {
+ sentryWorker,
+ }
+}
diff --git a/packages/extension/src/background/keys/keyDerivation.ts b/packages/extension/src/background/keys/keyDerivation.ts
index 21d49152a..7ee9287b9 100644
--- a/packages/extension/src/background/keys/keyDerivation.ts
+++ b/packages/extension/src/background/keys/keyDerivation.ts
@@ -54,7 +54,7 @@ export function getStarkPair(
}
/**
- * Grinds a private key to a valid StarkNet private key
+ * Grinds a private key to a valid Starknet private key
* @param privateKey
* @returns Unsantized hex string
*/
diff --git a/packages/extension/src/background/messageHandling/handle.ts b/packages/extension/src/background/messageHandling/handle.ts
index fc535ca29..382f46c26 100644
--- a/packages/extension/src/background/messageHandling/handle.ts
+++ b/packages/extension/src/background/messageHandling/handle.ts
@@ -25,6 +25,7 @@ import { handleUdcMessaging } from "../udcMessaging"
import { walletSingleton } from "../walletSingleton"
import { safeMessages, safeIfPreauthorizedMessages } from "./messages"
import browser from "webextension-polyfill"
+import { feeTokenService } from "../../shared/feeToken/service"
const handlers = [
handleAccountMessage,
@@ -50,6 +51,7 @@ export const handleMessage = async (
wallet: walletSingleton,
transactionTrackerWorker: transactionTrackerWorker,
actionService: backgroundActionService,
+ feeTokenService,
}
const extensionUrl = browser.runtime.getURL("")
diff --git a/packages/extension/src/background/migrations/index.ts b/packages/extension/src/background/migrations/index.ts
index 60474a231..ddce1b337 100644
--- a/packages/extension/src/background/migrations/index.ts
+++ b/packages/extension/src/background/migrations/index.ts
@@ -18,12 +18,12 @@ enum WalletMigrations {
}
enum NetworkMigrations {
- rpcEverywhere = "network:rpcEverywhere",
+ rpcEverywhere = "network:rpcEverywhere:r2",
}
enum TokenMigrations {
v59 = "token:v59",
- v510 = "token:v510:r2",
+ v510 = "token:v510:r3",
}
enum PreAuthorizationMigrations {
diff --git a/packages/extension/src/background/migrations/wallet/v5.8.1.ts b/packages/extension/src/background/migrations/wallet/v5.8.1.ts
index bf60171e8..39c53b6ef 100644
--- a/packages/extension/src/background/migrations/wallet/v5.8.1.ts
+++ b/packages/extension/src/background/migrations/wallet/v5.8.1.ts
@@ -8,6 +8,7 @@ import { BaseWalletAccount, WalletAccount } from "../../../shared/wallet.model"
import { accountsEqual } from "../../../shared/utils/accountsEqual"
import { WalletCryptoStarknetService } from "../../wallet/crypto/starknet.service"
import { WalletStorageProps } from "../../../shared/wallet/walletStore"
+import { getAccountContractAddress } from "../../wallet/findImplementationForAddress"
export async function determineMigrationNeededV581(
cryptoStarknetService: WalletCryptoStarknetService,
@@ -20,11 +21,11 @@ export async function determineMigrationNeededV581(
const { pubKey } = await cryptoStarknetService.getKeyPairByDerivationPath(
account.signer.derivationPath,
)
- const falseyAccountAddress =
- cryptoStarknetService.getCairo1AccountContractAddress(
- STANDARD_CAIRO_0_ACCOUNT_CLASS_HASH,
- pubKey,
- )
+ const falseyAccountAddress = getAccountContractAddress(
+ "1",
+ STANDARD_CAIRO_0_ACCOUNT_CLASS_HASH,
+ pubKey,
+ )
return [account, isEqualAddress(falseyAccountAddress, account.address)]
}),
diff --git a/packages/extension/src/background/multisig/multisigDeployAction.ts b/packages/extension/src/background/multisig/multisigDeployAction.ts
index e2d91880a..0677ba2d6 100644
--- a/packages/extension/src/background/multisig/multisigDeployAction.ts
+++ b/packages/extension/src/background/multisig/multisigDeployAction.ts
@@ -5,13 +5,17 @@ import { addTransaction } from "../../shared/transactions/store"
import { Wallet } from "../wallet"
import { estimatedFeeToMaxFeeTotal } from "../../shared/transactionSimulation/utils"
import { checkTransactionHash } from "../../shared/transactions/utils"
+import { ETH_TOKEN_ADDRESS } from "../../shared/network/constants"
+import { TransactionInvokeVersion } from "../../shared/utils/transactionVersion"
+import { AccountError } from "../../shared/errors/account"
+import { SessionError } from "../../shared/errors/session"
export const addMultisigDeployAction = async (
action: ExtensionActionItemOfType<"DEPLOY_MULTISIG">,
wallet: Wallet,
) => {
if (!(await wallet.isSessionOpen())) {
- throw Error("you need an open session")
+ throw new SessionError({ code: "NO_OPEN_SESSION" })
}
const { account: baseAccount } = action.payload
const selectedMultisig = await wallet.getMultisigAccount(baseAccount)
@@ -19,16 +23,22 @@ export const addMultisigDeployAction = async (
const multisigNeedsDeploy = selectedMultisig.needsDeploy
if (!multisigNeedsDeploy) {
- throw Error("Account already deployed")
+ throw new AccountError({ code: "ACCOUNT_ALREADY_DEPLOYED" })
}
+ // TODO: TXV3 - allow for deploying multisig with STRK fee token
+ const version: TransactionInvokeVersion = "0x1"
+ const feeTokenAddress = ETH_TOKEN_ADDRESS
+
+ // TODO: refactor to use the fee estimation repo
const maxFee = await wallet
- .getAccountDeploymentFee(selectedMultisig)
+ .getAccountDeploymentFee(selectedMultisig, feeTokenAddress)
.then(estimatedFeeToMaxFeeTotal)
.catch(() => num.toBigInt(20e14))
const { account, txHash } = await wallet.deployAccount(selectedMultisig, {
maxFee,
+ version,
})
if (!checkTransactionHash(txHash)) {
diff --git a/packages/extension/src/background/nonce.ts b/packages/extension/src/background/nonce.ts
index 935528ff1..30b83644b 100644
--- a/packages/extension/src/background/nonce.ts
+++ b/packages/extension/src/background/nonce.ts
@@ -1,4 +1,4 @@
-import { num, Account } from "starknet"
+import { num, Account } from "starknet6"
import { KeyValueStorage } from "../shared/storage"
import { BaseWalletAccount, WalletAccount } from "../shared/wallet.model"
@@ -17,8 +17,15 @@ export async function getNonce(
starknetAccount: Account,
): Promise {
const storageAddress = getAccountIdentifier(account)
- const result = await starknetAccount.getNonce()
- const nonceBn = num.toBigInt(result)
+ let nonceBn = BigInt(0)
+
+ try {
+ const result = await starknetAccount.getNonce()
+ nonceBn = num.toBigInt(result)
+ } catch {
+ console.warn("Onchain getNonce failed, using stored nonce.")
+ }
+
const storedNonce = await nonceStore.get(storageAddress)
if (account.type === "multisig") {
diff --git a/packages/extension/src/background/transactions/onupdate/index.ts b/packages/extension/src/background/transactions/onupdate/index.ts
index 03453ef72..35c84817b 100644
--- a/packages/extension/src/background/transactions/onupdate/index.ts
+++ b/packages/extension/src/background/transactions/onupdate/index.ts
@@ -8,8 +8,8 @@ import { TransactionUpdateListener } from "./type"
import { handleUpgradeTransaction } from "./upgrade"
const addedOrUpdatedHandlers: TransactionUpdateListener[] = [
- handleUpgradeTransaction,
handleDeployAccountTransaction,
+ handleUpgradeTransaction,
handleDeclareContractTransaction,
handleChangeGuardianTransaction,
handleMultisigUpdates,
@@ -19,9 +19,11 @@ const addedOrUpdatedHandlers: TransactionUpdateListener[] = [
export const runAddedOrUpdatedHandlers: TransactionUpdateListener = async (
updates,
) => {
- await Promise.allSettled(
- addedOrUpdatedHandlers.map((handler) => handler(updates)),
- )
+ // We need this in serial and not parallel because some handlers depend on the
+ // results of others (e.g. the upgrade handler needs the account to be deployed)
+ for (const handler of addedOrUpdatedHandlers) {
+ await handler(updates)
+ }
}
const changedStatusHandlers: TransactionUpdateListener[] = [
diff --git a/packages/extension/src/background/transactions/onupdate/upgrade.ts b/packages/extension/src/background/transactions/onupdate/upgrade.ts
index e81ea7729..6463c3d8c 100644
--- a/packages/extension/src/background/transactions/onupdate/upgrade.ts
+++ b/packages/extension/src/background/transactions/onupdate/upgrade.ts
@@ -1,16 +1,35 @@
-import { updateAccountDetails } from "../../../shared/account/update"
import { TransactionUpdateListener } from "./type"
+import { optimisticImplUpdate } from "../../../shared/account/optimisticImplUpdate"
+import { accountService } from "../../../shared/account/service"
+import { isSafeUpgradeTransaction } from "../../../shared/utils/isUpgradeTransaction"
+import { isSuccessfulTransaction } from "../../../shared/transactions/utils"
+import { accountsEqual } from "../../../shared/utils/accountsEqual"
+import { WalletAccount } from "../../../shared/wallet.model"
export const handleUpgradeTransaction: TransactionUpdateListener = async (
transactions,
) => {
- const upgrades = transactions.filter(
- (transaction) => transaction.meta?.isUpgrade,
- )
- if (upgrades.length > 0) {
- await updateAccountDetails(
- "implementation",
- upgrades.map((transaction) => transaction.account),
- )
+ const upgradeTxns = transactions.filter(
+ (tx) => isSafeUpgradeTransaction(tx) && isSuccessfulTransaction(tx),
+ ) // Check if the transaction is a safe upgrade transaction and if it was successful
+
+ if (upgradeTxns.length === 0) {
+ return
}
+
+ const allAccounts = await accountService.get()
+
+ const updatedAccounts = upgradeTxns.reduce((acc, tx) => {
+ const account = allAccounts.find((a) => accountsEqual(a, tx.account))
+ if (account) {
+ const updatedAccount = optimisticImplUpdate(
+ account,
+ tx.meta?.newClassHash,
+ )
+ acc.push(updatedAccount)
+ }
+ return acc
+ }, [])
+
+ await accountService.upsert(updatedAccounts)
}
diff --git a/packages/extension/src/background/transactions/sources/onchain.spec.ts b/packages/extension/src/background/transactions/sources/onchain.spec.ts
index 344cd28d0..5b021a465 100644
--- a/packages/extension/src/background/transactions/sources/onchain.spec.ts
+++ b/packages/extension/src/background/transactions/sources/onchain.spec.ts
@@ -35,7 +35,9 @@ describe("getTransactionsUpdate", () => {
getTransactionStatus: () => ({
finality_status: status,
}),
- getTransactionReceipt,
+ getTransactionReceipt: () => ({
+ finality_status: status,
+ }),
})
const test = await getTransactionsUpdate([mockTransaction])
@@ -86,9 +88,11 @@ describe("getTransactionsUpdate", () => {
account: { address: "0x1", networkId: "goerli-alpha" } as WalletAccount,
}
- const getTransactionReceipt = vi
- .fn()
- .mockResolvedValue({ revert_reason: "foo" })
+ const getTransactionReceipt = vi.fn().mockResolvedValue({
+ revert_reason: "foo",
+ execution_status: "REVERTED",
+ finality_status: "RECEIVED",
+ })
vi.mocked(mocks).getProvider.mockReturnValue({
getTransactionStatus: () => ({
diff --git a/packages/extension/src/background/transactions/sources/onchain.ts b/packages/extension/src/background/transactions/sources/onchain.ts
index 03dfe5814..13e49e386 100644
--- a/packages/extension/src/background/transactions/sources/onchain.ts
+++ b/packages/extension/src/background/transactions/sources/onchain.ts
@@ -1,8 +1,8 @@
import { getProvider } from "../../../shared/network"
import {
- ExtendedFinalityStatus,
Transaction,
getInFlightTransactions,
+ SUCCESS_STATUSES,
} from "../../../shared/transactions"
import { getTransactionsStatusUpdate } from "../determineUpdates"
import { getTransactionStatus } from "../../../shared/transactions/utils"
@@ -18,14 +18,30 @@ export async function getTransactionsUpdate(transactions: Transaction[]) {
const { finality_status, execution_status } =
await provider.getTransactionStatus(transaction.hash)
+ // getTransactionStatus goes straight to the sequencer, hence it's much faster than the RPC nodes
+ // because of that we need to wait for the RPC nodes to have a receipt as well
try {
- if (execution_status === "REVERTED") {
- const tx = await provider.getTransactionReceipt(transaction.hash)
+ if (
+ execution_status === "REVERTED" ||
+ SUCCESS_STATUSES.includes(finality_status)
+ ) {
+ const receipt = await provider.getTransactionReceipt(transaction.hash)
+ const {
+ finality_status: receiptFinalityStatus,
+ execution_status: receiptExecutionStatus,
+ } = receipt
- if ("revert_reason" in tx) {
+ if (
+ finality_status !== receiptFinalityStatus ||
+ execution_status !== receiptExecutionStatus
+ ) {
+ return transaction
+ }
+
+ if ("revert_reason" in receipt) {
return {
...transaction,
- revertReason: tx.revert_reason,
+ revertReason: receipt.revert_reason,
status: {
finality_status,
execution_status,
diff --git a/packages/extension/src/background/transactions/transactionExecution.ts b/packages/extension/src/background/transactions/transactionExecution.ts
index d6dbcc383..ca836a5e4 100644
--- a/packages/extension/src/background/transactions/transactionExecution.ts
+++ b/packages/extension/src/background/transactions/transactionExecution.ts
@@ -1,4 +1,4 @@
-import { num } from "starknet"
+import { TransactionType, num } from "starknet"
import {
ExtQueueItem,
TransactionActionPayload,
@@ -24,7 +24,12 @@ import {
getTransactionStatus,
} from "../../shared/transactions/utils"
import { isAccountV5 } from "@argent/shared"
-import { estimatedFeeToMaxFeeTotal } from "../../shared/transactionSimulation/utils"
+import { estimatedFeeToMaxResourceBounds } from "../../shared/transactionSimulation/utils"
+import { SessionError } from "../../shared/errors/session"
+import { AccountError } from "../../shared/errors/account"
+import { getTxVersionFromFeeToken } from "../../shared/utils/getTransactionVersion"
+import { TransactionError } from "../../shared/errors/transaction"
+import { isSafeUpgradeTransaction } from "../../shared/utils/isUpgradeTransaction"
export type TransactionAction = ExtQueueItem<{
type: "TRANSACTION"
@@ -37,33 +42,26 @@ export const executeTransactionAction = async (
) => {
const { transactions, abis, transactionsDetail, meta = {} } = action.payload
const allTransactions = await transactionsStore.get()
- const preComputedFees = await getEstimatedFees(transactions)
+ const preComputedFees = await getEstimatedFees({
+ type: TransactionType.INVOKE,
+ payload: transactions,
+ })
if (!preComputedFees) {
- throw Error("PreComputedFees not defined")
+ throw new TransactionError({ code: "NO_PRE_COMPUTED_FEES" })
}
- const suggestedMaxFee =
- transactionsDetail?.maxFee ??
- estimatedFeeToMaxFeeTotal(preComputedFees.transactions)
- const suggestedMaxADFee = preComputedFees.deployment
- ? estimatedFeeToMaxFeeTotal(preComputedFees.deployment)
- : 0n
-
- const maxFee = suggestedMaxFee
- const maxADFee = suggestedMaxADFee
-
// void analytics.track("executeTransaction", {
// usesCachedFees: Boolean(preComputedFees),
// }) // TODO: temporary disabled
if (!(await wallet.isSessionOpen())) {
- throw Error("you need an open session")
+ throw new SessionError({ code: "NO_OPEN_SESSION" })
}
const selectedAccount = await wallet.getSelectedAccount()
if (!selectedAccount) {
- throw Error("no accounts")
+ throw new AccountError({ code: "NOT_FOUND" })
}
const multisig =
@@ -80,7 +78,7 @@ export const executeTransactionAction = async (
})
const hasUpgradePending = pendingAccountTransactions.some(
- (tx) => tx.meta?.isUpgrade,
+ isSafeUpgradeTransaction,
)
const starknetAccount = await wallet.getStarknetAccount(
@@ -101,9 +99,14 @@ export const executeTransactionAction = async (
? num.toHex(transactionsDetail?.nonce || 0)
: await getNonce(selectedAccount, starknetAccount)
- if (accountNeedsDeploy) {
+ const version = getTxVersionFromFeeToken(
+ preComputedFees.transactions.feeTokenAddress,
+ )
+
+ if (accountNeedsDeploy && preComputedFees.deployment) {
const { account, txHash } = await wallet.deployAccount(selectedAccount, {
- maxFee: maxADFee,
+ version,
+ ...estimatedFeeToMaxResourceBounds(preComputedFees.deployment),
})
if (!checkTransactionHash(txHash)) {
throw Error(
@@ -141,7 +144,8 @@ export const executeTransactionAction = async (
const transaction = await acc.execute(transactions, abis, {
...transactionsDetail,
nonce,
- maxFee,
+ version,
+ ...estimatedFeeToMaxResourceBounds(preComputedFees.transactions),
})
if (!checkTransactionHash(transaction.transaction_hash, selectedAccount)) {
@@ -171,9 +175,5 @@ export const executeTransactionAction = async (
await increaseStoredNonce(selectedAccount)
}
- if ("isUpgrade" in meta && meta.isUpgrade) {
- await resetStoredNonce(selectedAccount) // reset nonce after upgrade. This is needed because nonce was managed by AccountContract before 0.10.0
- }
-
return transaction
}
diff --git a/packages/extension/src/background/transactions/transactionMessaging.ts b/packages/extension/src/background/transactions/transactionMessaging.ts
index 12fee92d0..07a6a13a2 100644
--- a/packages/extension/src/background/transactions/transactionMessaging.ts
+++ b/packages/extension/src/background/transactions/transactionMessaging.ts
@@ -2,7 +2,6 @@ import {
CallData,
Invocations,
TransactionType,
- hash,
num,
transaction,
} from "starknet"
@@ -20,12 +19,21 @@ import { TransactionError } from "../../shared/errors/transaction"
import { getEstimatedFeeFromBulkSimulation } from "../../shared/transactionSimulation/utils"
import { isAccountV4, isAccountV5 } from "@argent/shared"
import { EstimatedFees } from "../../shared/transactionSimulation/fees/fees.model"
-import { ETH_TOKEN_ADDRESS } from "../../shared/network/constants"
import { addEstimatedFee } from "../../shared/transactionSimulation/fees/estimatedFeesRepository"
+import {
+ getSimulationTxVersionFromFeeToken,
+ getTxVersionFromFeeToken,
+ getTxVersionFromFeeTokenForDeclareContract,
+} from "../../shared/utils/getTransactionVersion"
export const handleTransactionMessage: HandleMessage<
TransactionMessage
-> = async ({ msg, origin, background: { wallet, actionService }, respond }) => {
+> = async ({
+ msg,
+ origin,
+ background: { wallet, actionService, feeTokenService },
+ respond,
+}) => {
switch (msg.type) {
case "EXECUTE_TRANSACTION": {
const { meta } = await actionService.add(
@@ -46,13 +54,12 @@ export const handleTransactionMessage: HandleMessage<
}
case "ESTIMATE_DECLARE_CONTRACT_FEE": {
- const { address, networkId, ...rest } = msg.data
+ const { account, feeTokenAddress, payload } = msg.data
const selectedAccount = await wallet.getSelectedAccount()
- const selectedStarknetAccount =
- address && networkId
- ? await wallet.getStarknetAccount({ address, networkId })
- : await wallet.getSelectedStarknetAccount()
+ const selectedStarknetAccount = account
+ ? await wallet.getStarknetAccount(account)
+ : await wallet.getSelectedStarknetAccount()
if (!selectedStarknetAccount) {
throw Error("no accounts")
@@ -60,13 +67,18 @@ export const handleTransactionMessage: HandleMessage<
const fees: EstimatedFees = {
transactions: {
- feeTokenAddress: ETH_TOKEN_ADDRESS,
+ feeTokenAddress,
amount: 0n,
pricePerUnit: 0n,
},
}
try {
+ const version = getTxVersionFromFeeTokenForDeclareContract(
+ feeTokenAddress,
+ payload,
+ )
+
if (
selectedAccount?.needsDeploy &&
!(await isAccountDeployed(
@@ -86,15 +98,14 @@ export const handleTransactionMessage: HandleMessage<
},
{
type: TransactionType.DECLARE,
- payload: {
- ...rest,
- },
+ payload,
},
]
const estimateFeeBulk =
await selectedStarknetAccount.estimateFeeBulk(bulkTransactions, {
skipValidate: true,
+ version,
})
if (
@@ -107,7 +118,7 @@ export const handleTransactionMessage: HandleMessage<
}
fees.deployment = {
- feeTokenAddress: ETH_TOKEN_ADDRESS,
+ feeTokenAddress,
amount: num.toBigInt(estimateFeeBulk[0].gas_consumed),
pricePerUnit: num.toBigInt(estimateFeeBulk[0].gas_price),
}
@@ -131,8 +142,8 @@ export const handleTransactionMessage: HandleMessage<
} else {
if ("estimateDeclareFee" in selectedStarknetAccount) {
const { gas_consumed, gas_price } =
- await selectedStarknetAccount.estimateDeclareFee({
- ...rest,
+ await selectedStarknetAccount.estimateDeclareFee(payload, {
+ version,
})
if (!gas_consumed || !gas_price) {
@@ -146,6 +157,11 @@ export const handleTransactionMessage: HandleMessage<
}
}
+ await addEstimatedFee(fees, {
+ type: TransactionType.DECLARE,
+ payload,
+ })
+
return respond({
type: "ESTIMATE_DECLARE_CONTRACT_FEE_RES",
data: fees,
@@ -165,10 +181,12 @@ export const handleTransactionMessage: HandleMessage<
}
case "ESTIMATE_DEPLOY_CONTRACT_FEE": {
- const { classHash, constructorCalldata, salt, unique } = msg.data
+ const { payload, account, feeTokenAddress } = msg.data
const selectedAccount = await wallet.getSelectedAccount()
- const selectedStarknetAccount = await wallet.getSelectedStarknetAccount()
+ const selectedStarknetAccount = account
+ ? await wallet.getStarknetAccount(account)
+ : await wallet.getSelectedStarknetAccount()
if (!selectedStarknetAccount || !selectedAccount) {
throw Error("no accounts")
@@ -176,12 +194,14 @@ export const handleTransactionMessage: HandleMessage<
const fees: EstimatedFees = {
transactions: {
- feeTokenAddress: ETH_TOKEN_ADDRESS,
+ feeTokenAddress,
amount: 0n,
pricePerUnit: 0n,
},
}
+ const version = getTxVersionFromFeeToken(feeTokenAddress)
+
try {
if (
selectedAccount?.needsDeploy &&
@@ -200,12 +220,7 @@ export const handleTransactionMessage: HandleMessage<
},
{
type: TransactionType.DEPLOY,
- payload: {
- classHash,
- salt,
- unique,
- constructorCalldata,
- },
+ payload,
},
]
@@ -222,7 +237,7 @@ export const handleTransactionMessage: HandleMessage<
}
fees.deployment = {
- feeTokenAddress: ETH_TOKEN_ADDRESS,
+ feeTokenAddress,
amount: num.toBigInt(estimateFeeBulk[0].gas_consumed),
pricePerUnit: num.toBigInt(estimateFeeBulk[0].gas_price),
}
@@ -246,11 +261,8 @@ export const handleTransactionMessage: HandleMessage<
} else {
if ("estimateDeployFee" in selectedStarknetAccount) {
const { gas_consumed, gas_price } =
- await selectedStarknetAccount.estimateDeployFee({
- classHash,
- salt,
- unique,
- constructorCalldata,
+ await selectedStarknetAccount.estimateDeployFee(payload, {
+ version,
})
if (!gas_consumed || !gas_price) {
@@ -264,6 +276,11 @@ export const handleTransactionMessage: HandleMessage<
}
}
+ await addEstimatedFee(fees, {
+ type: TransactionType.DEPLOY,
+ payload,
+ })
+
return respond({
type: "ESTIMATE_DEPLOY_CONTRACT_FEE_RES",
data: fees,
@@ -300,17 +317,14 @@ export const handleTransactionMessage: HandleMessage<
})
}
- let nonce
-
- try {
- nonce = await starknetAccount.getNonce()
- } catch {
- nonce = "0"
- }
+ const nonce = await starknetAccount.getNonce().catch(() => "0")
const chainId = await starknetAccount.getChainId()
- const version = num.toHex(hash.feeTransactionVersion)
+ const bestFeeToken = await feeTokenService.getBestFeeToken(
+ selectedAccount,
+ )
+ const version = getSimulationTxVersionFromFeeToken(bestFeeToken.address)
const calldata = transaction.getExecuteCalldata(
transactions,
@@ -375,7 +389,9 @@ export const handleTransactionMessage: HandleMessage<
}
case "SIMULATE_TRANSACTIONS": {
- const transactions = Array.isArray(msg.data) ? msg.data : [msg.data]
+ const transactions = Array.isArray(msg.data.call)
+ ? msg.data.call
+ : [msg.data.call]
try {
const selectedAccount = await wallet.getSelectedAccount()
@@ -396,18 +412,13 @@ export const handleTransactionMessage: HandleMessage<
})
}
- let nonce
-
- try {
- nonce = await starknetAccount.getNonce()
- } catch {
- nonce = "0"
- }
+ const nonce = await starknetAccount.getNonce().catch(() => "0")
const chainId = await starknetAccount.getChainId()
- const version = num.toHex(hash.feeTransactionVersion)
-
+ const version = getSimulationTxVersionFromFeeToken(
+ msg.data.feeTokenAddress,
+ )
const calldata = transaction.getExecuteCalldata(
transactions,
starknetAccount.cairoVersion,
@@ -453,7 +464,7 @@ export const handleTransactionMessage: HandleMessage<
const result = await fetchTransactionBulkSimulation({
invocations,
- chainId,
+ chainId: chainId as any, // TODO: migrate to snjsv6 completely
})
const estimatedFee = getEstimatedFeeFromBulkSimulation(result)
@@ -461,7 +472,10 @@ export const handleTransactionMessage: HandleMessage<
let simulationWithFees = null
if (result) {
- await addEstimatedFee(estimatedFee, transactions)
+ await addEstimatedFee(estimatedFee, {
+ type: TransactionType.INVOKE,
+ payload: transactions,
+ })
simulationWithFees = {
simulation: result,
feeEstimation: estimatedFee,
diff --git a/packages/extension/src/background/udcAction.ts b/packages/extension/src/background/udcAction.ts
index c1967b8ed..079dea2da 100644
--- a/packages/extension/src/background/udcAction.ts
+++ b/packages/extension/src/background/udcAction.ts
@@ -1,7 +1,6 @@
import {
CallData,
DeclareContractPayload,
- Invocations,
TransactionType,
UniversalDeployerContractPayload,
constants,
@@ -13,12 +12,18 @@ import { isAccountDeployed } from "./accountDeploy"
import { analytics } from "./analytics"
import { getNonce, increaseStoredNonce } from "./nonce"
import { addTransaction } from "../shared/transactions/store"
-import { modifySnjsFeeOverhead } from "../shared/utils/argentMaxFee"
import { Wallet } from "./wallet"
import { AccountError } from "../shared/errors/account"
import { WalletError } from "../shared/errors/wallet"
import { UdcError } from "../shared/errors/udc"
import { checkTransactionHash } from "../shared/transactions/utils"
+import { getEstimatedFees } from "../shared/transactionSimulation/fees/estimatedFeesRepository"
+import {
+ getTxVersionFromFeeToken,
+ getTxVersionFromFeeTokenForDeclareContract,
+} from "../shared/utils/getTransactionVersion"
+import { estimatedFeeToMaxResourceBounds } from "../shared/transactionSimulation/utils"
+import { TransactionError } from "../shared/errors/transaction"
const { UDC } = constants
@@ -54,47 +59,35 @@ export const udcDeclareContract = async (
networkId: selectedAccount.networkId,
})
- let maxADFee = "0"
- let maxDeclareFee = "0"
+ const preComputedFees = await getEstimatedFees({
+ type: TransactionType.DECLARE,
+ payload,
+ })
+
+ if (!preComputedFees) {
+ throw new TransactionError({ code: "NO_PRE_COMPUTED_FEES" })
+ }
+
+ const version = getTxVersionFromFeeTokenForDeclareContract(
+ preComputedFees.transactions.feeTokenAddress,
+ payload,
+ )
+
+ const accountNeedsDeploy = !(await isAccountDeployed(
+ selectedAccount,
+ starknetAccount.getClassAt.bind(starknetAccount),
+ ))
- const declareNonce = selectedAccount.needsDeploy
+ const declareNonce = accountNeedsDeploy
? num.toHex(1)
: await getNonce(selectedAccount, starknetAccount)
- if (
- selectedAccount.needsDeploy &&
- !(await isAccountDeployed(
- selectedAccount,
- starknetAccount.getClassAt.bind(starknetAccount),
- ))
- ) {
- if ("estimateFeeBulk" in starknetAccount) {
- const bulkTransactions: Invocations = [
- {
- type: TransactionType.DEPLOY_ACCOUNT,
- payload: await wallet.getAccountDeploymentPayload(selectedAccount),
- },
- {
- type: TransactionType.DECLARE,
- payload,
- },
- ]
- const estimateFeeBulk = await starknetAccount.estimateFeeBulk(
- bulkTransactions,
- { skipValidate: true },
- )
-
- maxADFee = modifySnjsFeeOverhead({
- suggestedMaxFee: estimateFeeBulk[0].suggestedMaxFee,
- })
- maxDeclareFee = modifySnjsFeeOverhead({
- suggestedMaxFee: estimateFeeBulk[1].suggestedMaxFee,
- })
- }
+ if (accountNeedsDeploy && preComputedFees.deployment) {
const { account, txHash: accountDeployTxHash } = await wallet.deployAccount(
selectedAccount,
{
- maxFee: maxADFee,
+ version,
+ ...estimatedFeeToMaxResourceBounds(preComputedFees.deployment),
},
)
@@ -102,7 +95,7 @@ export const udcDeclareContract = async (
throw new UdcError({ code: "DEPLOY_TX_NOT_ADDED" })
}
- analytics.track("deployAccount", {
+ void analytics.track("deployAccount", {
status: "success",
trigger: "transaction",
networkId: account.networkId,
@@ -117,32 +110,18 @@ export const udcDeclareContract = async (
type: TransactionType.DEPLOY_ACCOUNT,
},
})
- } else {
- if ("getSuggestedMaxFee" in starknetAccount) {
- const suggestedMaxFee = await starknetAccount.getSuggestedMaxFee(
- {
- type: TransactionType.DECLARE,
- payload,
- },
- {
- nonce: declareNonce,
- },
- )
- maxDeclareFee = modifySnjsFeeOverhead({ suggestedMaxFee })
- } else {
- throw new UdcError({ code: "NO_STARKNET_DECLARE_FEE" })
- }
}
if ("declareIfNot" in starknetAccount) {
const { transaction_hash: declareTxHash, class_hash: deployedClassHash } =
await starknetAccount.declareIfNot(payload, {
nonce: declareNonce,
- maxFee: maxDeclareFee,
+ version,
+ ...estimatedFeeToMaxResourceBounds(preComputedFees.transactions),
})
if (!checkTransactionHash(declareTxHash)) {
- throw new UdcError({ code: "DEPLOY_TX_NOT_ADDED" })
+ throw new UdcError({ code: "CONTRACT_ALREADY_DECLARED" })
}
await increaseStoredNonce(selectedAccount)
@@ -184,52 +163,34 @@ export const udcDeployContract = async (
networkId: selectedAccount.networkId,
})
- let maxADFee = "0"
- let maxDeployFee = "0"
+ const preComputedFees = await getEstimatedFees({
+ type: TransactionType.DEPLOY,
+ payload,
+ })
+
+ if (!preComputedFees) {
+ throw new TransactionError({ code: "NO_PRE_COMPUTED_FEES" })
+ }
- const deployNonce = selectedAccount.needsDeploy
+ const version = getTxVersionFromFeeToken(
+ preComputedFees.transactions.feeTokenAddress,
+ )
+
+ const accountNeedsDeploy = !(await isAccountDeployed(
+ selectedAccount,
+ starknetAccount.getClassAt.bind(starknetAccount),
+ ))
+
+ const deployNonce = accountNeedsDeploy
? num.toHex(num.toBigInt(1))
: await getNonce(selectedAccount, starknetAccount)
- if (
- selectedAccount.needsDeploy &&
- !(await isAccountDeployed(
- selectedAccount,
- starknetAccount.getClassAt.bind(starknetAccount),
- ))
- ) {
- if ("estimateFeeBulk" in starknetAccount) {
- const bulkTransactions: Invocations = [
- {
- type: TransactionType.DEPLOY_ACCOUNT,
- payload: await wallet.getAccountDeploymentPayload(selectedAccount),
- },
- {
- type: TransactionType.DEPLOY,
- payload: {
- classHash: payload.classHash,
- constructorCalldata: payload.constructorCalldata,
- salt: payload.salt,
- unique: payload.unique,
- },
- },
- ]
- const estimateFeeBulk = await starknetAccount.estimateFeeBulk(
- bulkTransactions,
- { skipValidate: true },
- )
-
- maxADFee = modifySnjsFeeOverhead({
- suggestedMaxFee: estimateFeeBulk[0].suggestedMaxFee,
- })
- maxDeployFee = modifySnjsFeeOverhead({
- suggestedMaxFee: estimateFeeBulk[1].suggestedMaxFee,
- })
- }
+ if (accountNeedsDeploy && preComputedFees.deployment) {
const { account, txHash: accountDeployTxHash } = await wallet.deployAccount(
selectedAccount,
{
- maxFee: maxADFee,
+ version,
+ ...estimatedFeeToMaxResourceBounds(preComputedFees.deployment),
},
)
@@ -237,7 +198,7 @@ export const udcDeployContract = async (
throw new UdcError({ code: "DEPLOY_TX_NOT_ADDED" })
}
- analytics.track("deployAccount", {
+ void analytics.track("deployAccount", {
status: "success",
trigger: "transaction",
networkId: account.networkId,
@@ -252,26 +213,6 @@ export const udcDeployContract = async (
type: "DEPLOY_ACCOUNT",
},
})
- } else {
- if ("getSuggestedMaxFee" in starknetAccount) {
- const suggestedMaxFee = await starknetAccount.getSuggestedMaxFee(
- {
- type: TransactionType.DEPLOY,
- payload: {
- classHash: payload.classHash,
- constructorCalldata: payload.constructorCalldata,
- salt: payload.salt,
- unique: payload.unique,
- },
- },
- {
- nonce: deployNonce,
- },
- )
- maxDeployFee = modifySnjsFeeOverhead({ suggestedMaxFee })
- } else {
- throw new UdcError({ code: "NO_STARKNET_DECLARE_FEE" })
- }
}
if ("deploy" in starknetAccount) {
@@ -291,12 +232,13 @@ export const udcDeployContract = async (
},
{
nonce: deployNonce,
- maxFee: maxDeployFee,
+ version,
+ ...estimatedFeeToMaxResourceBounds(preComputedFees.transactions),
},
)
if (!checkTransactionHash(deployTxHash)) {
- throw new UdcError({ code: "DEPLOY_TX_NOT_ADDED" })
+ throw new UdcError({ code: "NO_DEPLOY_CONTRACT" })
}
const contractAddress = contract_address[0]
diff --git a/packages/extension/src/background/udcMessaging.ts b/packages/extension/src/background/udcMessaging.ts
index cafdc9689..14a529715 100644
--- a/packages/extension/src/background/udcMessaging.ts
+++ b/packages/extension/src/background/udcMessaging.ts
@@ -12,20 +12,18 @@ export const handleUdcMessaging: HandleMessage = async ({
// TODO: refactor after we have a plan for inpage
case "REQUEST_DECLARE_CONTRACT": {
const { data } = msg
- const { address, networkId, ...rest } = data
- if (address && networkId) {
+ const { account, payload } = data
+ if (account) {
await wallet.selectAccount({
- address,
- networkId,
+ address: account.address,
+ networkId: account.networkId,
})
}
const action = await actionService.add(
{
type: "DECLARE_CONTRACT",
- payload: {
- ...rest,
- },
+ payload,
},
{
origin,
diff --git a/packages/extension/src/background/wallet/account/shared.service.ts b/packages/extension/src/background/wallet/account/shared.service.ts
index de5df252b..a3ae06cd2 100644
--- a/packages/extension/src/background/wallet/account/shared.service.ts
+++ b/packages/extension/src/background/wallet/account/shared.service.ts
@@ -7,7 +7,7 @@ import {
} from "../../../shared/wallet.model"
import { withHiddenSelector } from "../../../shared/account/selectors"
import { PendingMultisig } from "../../../shared/multisig/types"
-import { Network, defaultNetwork } from "../../../shared/network"
+import { defaultNetwork } from "../../../shared/network"
import { BaseWalletAccount, WalletAccount } from "../../../shared/wallet.model"
import {
MULTISIG_DERIVATION_PATH,
@@ -28,6 +28,15 @@ export interface WalletSession {
password: string
}
+interface GetAccountArgs
+ extends Pick<
+ WalletAccount,
+ "address" | "network" | "needsDeploy" | "classHash"
+ > {
+ index: number
+ name?: string
+}
+
export class WalletAccountSharedService {
constructor(
public readonly store: IObjectStore,
@@ -63,14 +72,16 @@ export class WalletAccountSharedService {
return defaultAccountName
}
- public getDefaultStandardAccount(
- index: number,
- address: string,
- network: Network,
- needsDeploy: boolean,
- ): WalletAccount {
+ public getDefaultStandardAccount({
+ index,
+ address,
+ network,
+ needsDeploy,
+ name,
+ classHash,
+ }: GetAccountArgs): WalletAccount {
return {
- name: `Account ${index + 1}`,
+ name: name || `Account ${index + 1}`,
address,
network,
networkId: network.id,
@@ -79,18 +90,20 @@ export class WalletAccountSharedService {
derivationPath: getPathForIndex(index, STANDARD_DERIVATION_PATH),
type: "local_secret",
},
+ classHash,
needsDeploy,
}
}
- public getDefaultMultisigAccount(
- index: number,
- address: string,
- network: Network,
- needsDeploy: boolean,
- ): WalletAccount {
+ public getDefaultMultisigAccount({
+ index,
+ address,
+ network,
+ needsDeploy,
+ name,
+ }: GetAccountArgs): WalletAccount {
return {
- name: `Multisig ${index + 1}`,
+ name: name || `Multisig ${index + 1}`,
address,
networkId: network.id,
network,
diff --git a/packages/extension/src/background/wallet/account/starknet.service.test.ts b/packages/extension/src/background/wallet/account/starknet.service.test.ts
index 3a98a82e5..6d398ffe8 100644
--- a/packages/extension/src/background/wallet/account/starknet.service.test.ts
+++ b/packages/extension/src/background/wallet/account/starknet.service.test.ts
@@ -9,7 +9,7 @@ import {
cryptoStarknetServiceMock,
sessionServiceMock,
} from "../test.utils"
-import { Account } from "starknet"
+import { Account } from "starknet6"
import { grindKey } from "../../keys/keyDerivation"
import { MultisigSigner } from "../../../shared/multisig/signer"
diff --git a/packages/extension/src/background/wallet/account/starknet.service.ts b/packages/extension/src/background/wallet/account/starknet.service.ts
index bba3868cf..bfdff71bf 100644
--- a/packages/extension/src/background/wallet/account/starknet.service.ts
+++ b/packages/extension/src/background/wallet/account/starknet.service.ts
@@ -1,8 +1,8 @@
-import { Account } from "starknet"
+import { getProvider6 } from "../../../shared/network"
+import { Account } from "starknet6"
import { MultisigAccount } from "../../../shared/multisig/account"
import { PendingMultisig } from "../../../shared/multisig/types"
-import { getProvider } from "../../../shared/network"
import { INetworkService } from "../../../shared/network/service/interface"
import { IRepository } from "../../../shared/storage/__new/interface"
import { getAccountCairoVersion } from "../../../shared/utils/argentAccountVersion"
@@ -40,7 +40,7 @@ export class WalletAccountStarknetService {
throw new AccountError({ code: "NOT_FOUND" })
}
- const provider = getProvider(
+ const provider = getProvider6(
account.network && account.network.rpcUrl
? account.network
: await this.networkService.getById(selector.networkId),
@@ -59,7 +59,7 @@ export class WalletAccountStarknetService {
const starknetAccount = new Account(
provider,
account.address,
- pkOrSigner,
+ pkOrSigner as any, // TODO: migrate to snjsv6 completely
cairoVersion,
)
@@ -70,7 +70,7 @@ export class WalletAccountStarknetService {
const starknetAccount = new Account(
provider,
account.address,
- pkOrSigner,
+ pkOrSigner as any, // TODO: migrate to snjsv6 completely
"1",
)
@@ -96,7 +96,7 @@ export class WalletAccountStarknetService {
const starknetAccount = new Account(
provider,
account.address,
- pkOrSigner,
+ pkOrSigner as any, // TODO: migrate to snjsv6 completely
accountCairoVersion,
)
diff --git a/packages/extension/src/background/wallet/crypto/shared.service.ts b/packages/extension/src/background/wallet/crypto/shared.service.ts
index e569c6cdc..f79d382cc 100644
--- a/packages/extension/src/background/wallet/crypto/shared.service.ts
+++ b/packages/extension/src/background/wallet/crypto/shared.service.ts
@@ -12,7 +12,6 @@ import { WalletSessionService } from "../session/session.service"
import type { WalletSession } from "../session/walletSession.model"
import { IWalletDeploymentService } from "../deployment/interface"
import { IObjectStore } from "../../../shared/storage/__new/interface"
-import { WalletError } from "../../../shared/errors/wallet"
import { walletToKeystore } from "../utils"
export class WalletCryptoSharedService {
@@ -26,10 +25,6 @@ export class WalletCryptoSharedService {
) {}
public async restoreSeedPhrase(seedPhrase: string, newPassword: string) {
- const session = await this.sessionStore.get()
- if ((await this.backupService.isInitialized()) || session) {
- throw new WalletError({ code: "ALREADY_INITIALIZED" })
- }
const ethersWallet = HDNodeWallet.fromPhrase(seedPhrase)
const encryptedBackup = await encryptKeystoreJson(
walletToKeystore(ethersWallet),
diff --git a/packages/extension/src/background/wallet/crypto/starknet.service.ts b/packages/extension/src/background/wallet/crypto/starknet.service.ts
index 353590254..89490b7b9 100644
--- a/packages/extension/src/background/wallet/crypto/starknet.service.ts
+++ b/packages/extension/src/background/wallet/crypto/starknet.service.ts
@@ -1,4 +1,4 @@
-import { isEqualAddress } from "@argent/shared"
+import { Hex, isEqualAddress } from "@argent/shared"
import { CairoVersion, CallData, hash } from "starknet"
import { withHiddenSelector } from "../../../shared/account/selectors"
import { MultisigSigner } from "../../../shared/multisig/signer"
@@ -31,17 +31,20 @@ import {
IObjectStore,
IRepository,
} from "../../../shared/storage/__new/interface"
-import {
- ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES,
- PROXY_CONTRACT_CLASS_HASHES,
-} from "../starknet.constants"
import { decodeBase58Array } from "@argent/shared"
import { MULTISIG_DERIVATION_PATH } from "../../../shared/wallet.service"
import { sortMultisigByDerivationPath } from "../../../shared/utils/accountsMultisigSort"
import { SessionError } from "../../../shared/errors/session"
import { getAccountCairoVersion } from "../../../shared/utils/argentAccountVersion"
import { AccountError } from "../../../shared/errors/account"
-import { STANDARD_CAIRO_0_ACCOUNT_CLASS_HASH } from "../../../shared/network/constants"
+import {
+ ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES,
+ C0_PROXY_CONTRACT_CLASS_HASHES,
+} from "../../../shared/account/starknet.constants"
+import {
+ Implementation,
+ findImplementationForAccount,
+} from "../findImplementationForAddress"
const { getSelectorFromName, calculateContractAddressFromHash } = hash
export class WalletCryptoStarknetService {
@@ -144,9 +147,7 @@ export class WalletCryptoStarknetService {
account.signer.derivationPath,
)
- const starkPub = starkPair.pubKey
-
- return { publicKey: starkPub, account }
+ return { publicKey: starkPair.pubKey, account }
}
/**
@@ -226,12 +227,10 @@ export class WalletCryptoStarknetService {
async getAccountClassHashForNetwork(
network: Network,
accountType: ArgentAccountType,
- ): Promise {
+ ): Promise {
if (network.accountClassHash && network.accountClassHash.standard) {
- return (
- network.accountClassHash[accountType] ??
- network.accountClassHash.standard
- )
+ return (network.accountClassHash[accountType] ??
+ network.accountClassHash.standard) as Hex
}
const deployerAccount = await getPreDeployedAccount(network)
@@ -242,58 +241,10 @@ export class WalletCryptoStarknetService {
this.loadContracts,
)
- return accountClassHash
- }
-
- return ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES[0]
- }
-
- public getCairo0AccountContractAddress(
- accountClassHash: string,
- pubKey: string,
- ): string {
- const constructorCallData = {
- implementation: accountClassHash,
- selector: getSelectorFromName("initialize"),
- calldata: CallData.compile({
- signer: pubKey,
- guardian: "0",
- }),
- }
-
- const deployAccountPayload = {
- classHash: PROXY_CONTRACT_CLASS_HASHES[0],
- constructorCalldata: CallData.compile(constructorCallData),
- addressSalt: pubKey,
- }
-
- return calculateContractAddressFromHash(
- deployAccountPayload.addressSalt,
- deployAccountPayload.classHash,
- deployAccountPayload.constructorCalldata,
- 0,
- )
- }
-
- public getCairo1AccountContractAddress(
- accountClassHash: string,
- pubKey: string,
- ) {
- const deployAccountPayload = {
- classHash: accountClassHash,
- constructorCalldata: CallData.compile({
- signer: pubKey,
- guardian: "0",
- }),
- addressSalt: pubKey,
+ return accountClassHash as Hex
}
- return calculateContractAddressFromHash(
- deployAccountPayload.addressSalt,
- deployAccountPayload.classHash,
- deployAccountPayload.constructorCalldata,
- 0,
- )
+ return ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES.CAIRO_1[0] as Hex
}
public async getUndeployedAccountCairoVersion(
@@ -317,37 +268,30 @@ export class WalletCryptoStarknetService {
return "1" // multisig is always Cairo 1
}
- const accountClassHash =
- account.classHash ??
- (await this.getAccountClassHashForNetwork(account.network, account.type))
-
const { publicKey } = await this.getPublicKey(account)
- const cairo1Address = this.getCairo1AccountContractAddress(
- accountClassHash,
- publicKey,
- )
-
- if (isEqualAddress(account.address, cairo1Address)) {
- console.log("Undeployed Account is a Cairo 1 account")
- return "1"
+ // If no class hash is provided by the account, we want to add the network implementation to check
+ const networkImplementation: Implementation = {
+ cairoVersion: "1",
+ accountClassHash: await this.getAccountClassHashForNetwork(
+ account.network,
+ account.type,
+ ),
}
- const cairo0Address = this.getCairo0AccountContractAddress(
- STANDARD_CAIRO_0_ACCOUNT_CLASS_HASH, // last Cairo 0 implementation
- publicKey,
- )
-
- if (isEqualAddress(account.address, cairo0Address)) {
- console.log("Undeployed Account is a Cairo 0 account")
- return "0"
+ try {
+ const { cairoVersion } = findImplementationForAccount(
+ publicKey,
+ account,
+ [networkImplementation],
+ )
+ return cairoVersion
+ } catch (error) {
+ throw new AccountError({
+ code: "UNDEPLOYED_ACCOUNT_CAIRO_VERSION_NOT_FOUND",
+ options: { error },
+ })
}
-
- // We don't check for bad class hash 0x01a7820094feaf82d53f53f214b81292d717e7bb9a92bb2488092cd306f3993f
- // because it's deprecated, so we should not have any account with this class hash that should need this function
- throw new AccountError({
- code: "UNDEPLOYED_ACCOUNT_CAIRO_VERSION_NOT_FOUND",
- })
}
public async getCalculatedMultisigAddress(
@@ -386,7 +330,7 @@ export class WalletCryptoStarknetService {
}
const deployMultisigPayload = {
- classHash: PROXY_CONTRACT_CLASS_HASHES[0],
+ classHash: C0_PROXY_CONTRACT_CLASS_HASHES[0],
constructorCalldata: CallData.compile(constructorCallData),
addressSalt: starkPub,
}
diff --git a/packages/extension/src/background/wallet/deployment/interface.ts b/packages/extension/src/background/wallet/deployment/interface.ts
index 283bb1bf5..75064f7ef 100644
--- a/packages/extension/src/background/wallet/deployment/interface.ts
+++ b/packages/extension/src/background/wallet/deployment/interface.ts
@@ -1,4 +1,5 @@
import {
+ CairoVersion,
DeployAccountContractPayload as StarknetDeployAccountContractPayload,
InvocationsDetails as StarknetInvocationDetails,
} from "starknet"
@@ -13,7 +14,10 @@ import { Address } from "@argent/shared"
// Extend to support multichain
type InvocationsDetails = StarknetInvocationDetails
-type DeployAccountContractPayload = StarknetDeployAccountContractPayload
+export type DeployAccountContractPayload =
+ StarknetDeployAccountContractPayload & {
+ version: CairoVersion
+ }
export interface IWalletDeploymentService {
deployAccount(
diff --git a/packages/extension/src/background/wallet/deployment/starknet.service.ts b/packages/extension/src/background/wallet/deployment/starknet.service.ts
index 5402924ed..70fda3bbe 100644
--- a/packages/extension/src/background/wallet/deployment/starknet.service.ts
+++ b/packages/extension/src/background/wallet/deployment/starknet.service.ts
@@ -7,11 +7,10 @@ import {
} from "@argent/shared"
import {
CallData,
- DeployAccountContractPayload,
DeployAccountContractTransaction,
- InvocationsDetails,
+ EstimateFeeDetails,
hash,
-} from "starknet"
+} from "starknet6"
import { withHiddenSelector } from "../../../shared/account/selectors"
import { PendingMultisig } from "../../../shared/multisig/types"
@@ -47,20 +46,48 @@ import { WalletBackupService } from "../backup/backup.service"
import { WalletCryptoStarknetService } from "../crypto/starknet.service"
import { WalletSessionService } from "../session/session.service"
import type { WalletSession } from "../session/walletSession.model"
-import { PROXY_CONTRACT_CLASS_HASHES } from "../starknet.constants"
-import { IWalletDeploymentService } from "./interface"
+import {
+ IWalletDeploymentService,
+ DeployAccountContractPayload,
+} from "./interface"
import { SessionError } from "../../../shared/errors/session"
import { WalletError } from "../../../shared/errors/wallet"
import {
ETH_TOKEN_ADDRESS,
- STANDARD_CAIRO_0_ACCOUNT_CLASS_HASH,
+ STRK_TOKEN_ADDRESS,
} from "../../../shared/network/constants"
import { AccountError } from "../../../shared/errors/account"
-import { modifySnjsFeeOverhead } from "../../../shared/utils/argentMaxFee"
import { EstimatedFee } from "../../../shared/transactionSimulation/fees/fees.model"
-import { estimatedFeeToMaxFeeTotal } from "../../../shared/transactionSimulation/utils"
+import { estimatedFeeToMaxResourceBounds } from "../../../shared/transactionSimulation/utils"
+import { BigNumberish, num, constants, CairoVersion } from "starknet"
+import { getTxVersionFromFeeToken } from "../../../shared/utils/getTransactionVersion"
+import {
+ Implementation,
+ findImplementationForAccount,
+ getAccountDeploymentPayload,
+} from "../findImplementationForAddress"
+
+const { calculateContractAddressFromHash } = hash
-const { getSelectorFromName, calculateContractAddressFromHash } = hash
+// TODO: import from starknet6 when available
+const BN_TRANSACTION_VERSION_3 = 3n
+const BN_FEE_TRANSACTION_VERSION_3 = 2n ** 128n + BN_TRANSACTION_VERSION_3
+
+function mapVersionToFeeToken(version: BigNumberish): Address {
+ if (
+ num.toBigInt(version) === constants.BN_TRANSACTION_VERSION_1 ||
+ num.toBigInt(version) === constants.BN_FEE_TRANSACTION_VERSION_1
+ ) {
+ return ETH_TOKEN_ADDRESS
+ }
+ if (
+ num.toBigInt(version) === BN_TRANSACTION_VERSION_3 ||
+ num.toBigInt(version) === BN_FEE_TRANSACTION_VERSION_3
+ ) {
+ return STRK_TOKEN_ADDRESS
+ }
+ throw new Error("Unsupported tx version for fee token mapping")
+}
export class WalletDeploymentStarknetService
implements IWalletDeploymentService
@@ -80,7 +107,7 @@ export class WalletDeploymentStarknetService
public async deployAccount(
walletAccount: WalletAccount,
- transactionDetails?: InvocationsDetails | undefined,
+ transactionDetails?: EstimateFeeDetails | undefined,
): Promise<{ account: WalletAccount; txHash: string }> {
const starknetAccount =
await this.accountStarknetService.getStarknetAccount(walletAccount)
@@ -95,18 +122,25 @@ export class WalletDeploymentStarknetService
if (!isAccountV5(starknetAccount)) {
throw new AccountError({ code: "CANNOT_DEPLOY_OLD_ACCOUNTS" })
}
- const maxFee = transactionDetails?.maxFee
- ? modifySnjsFeeOverhead({
- suggestedMaxFee: transactionDetails.maxFee,
- })
- : estimatedFeeToMaxFeeTotal(
- await this.getAccountDeploymentFee(walletAccount),
- )
+
+ const maxFeeOrBounds =
+ transactionDetails?.maxFee || transactionDetails?.resourceBounds
+ ? {
+ maxFee: transactionDetails?.maxFee,
+ resourceBounds: transactionDetails?.resourceBounds,
+ }
+ : estimatedFeeToMaxResourceBounds(
+ await this.getAccountDeploymentFee(
+ walletAccount,
+ mapVersionToFeeToken(transactionDetails?.version ?? "0x1"),
+ ),
+ )
+
const { transaction_hash } = await starknetAccount.deployAccount(
deployAccountPayload,
{
...transactionDetails,
- maxFee,
+ ...maxFeeOrBounds,
},
)
@@ -126,7 +160,7 @@ export class WalletDeploymentStarknetService
public async getAccountDeploymentFee(
walletAccount: WalletAccount,
- feeTokenAddress: Address = ETH_TOKEN_ADDRESS,
+ feeTokenAddress: Address,
): Promise {
const starknetAccount =
await this.accountStarknetService.getStarknetAccount(walletAccount)
@@ -143,9 +177,13 @@ export class WalletDeploymentStarknetService
code: "CANNOT_ESTIMATE_FEE_OLD_ACCOUNTS_DEPLOYMENT",
})
}
+
+ const version = getTxVersionFromFeeToken(feeTokenAddress)
+
const { gas_consumed, gas_price } =
await starknetAccount.estimateAccountDeployFee(deployAccountPayload, {
skipValidate: true,
+ version,
})
if (!gas_consumed || !gas_price) {
@@ -193,107 +231,27 @@ export class WalletDeploymentStarknetService
const starkPub = starkPair.pubKey
- // Try to get the account class hash from walletAccount if it exists
- // If it doesn't exist, get it from the network object
- const accountClassHash =
- walletAccount.classHash ??
- (await this.cryptoStarknetService.getAccountClassHashForNetwork(
- walletAccount.network,
- walletAccount.type,
- ))
-
- const constructorCallData = {
- implementation: accountClassHash,
- selector: getSelectorFromName("initialize"),
- calldata: CallData.compile({ signer: starkPub, guardian: "0" }),
- }
-
- const deployAccountPayloadCairo0 = {
- classHash: PROXY_CONTRACT_CLASS_HASHES[0],
- contractAddress: walletAccount.address,
- constructorCalldata: CallData.compile(constructorCallData),
- addressSalt: starkPub,
- }
-
- const deployAccountPayloadCairo1 = {
- classHash: accountClassHash,
- contractAddress: walletAccount.address,
- constructorCalldata: CallData.compile({
- signer: starkPub,
- guardian: "0",
- }),
- addressSalt: starkPub,
- }
-
- let deployAccountPayload
-
- if (walletAccount.type === "standardCairo0") {
- deployAccountPayload = deployAccountPayloadCairo0
- } else {
- deployAccountPayload = deployAccountPayloadCairo1
- }
-
- const calculatedAccountAddress = calculateContractAddressFromHash(
- deployAccountPayload.addressSalt,
- deployAccountPayload.classHash,
- deployAccountPayload.constructorCalldata,
- 0,
- )
-
- if (isEqualAddress(walletAccount.address, calculatedAccountAddress)) {
- return deployAccountPayload
- }
-
- // Warn if the account was created using Cairo 0 implementation and the address does not match
- console.warn(
- "Calculated address does not match Cairo 1 account address. Trying Cairo 0 implementation",
- )
-
- const cairo0Calldata = CallData.compile({
- ...constructorCallData,
- implementation: STANDARD_CAIRO_0_ACCOUNT_CLASS_HASH, // last Cairo 0 implementation
- })
-
- // Try to deploy using Cairo 0 implementation
- const cairo0CalculatedAccountAddress = calculateContractAddressFromHash(
- deployAccountPayloadCairo0.addressSalt,
- deployAccountPayloadCairo0.classHash,
- cairo0Calldata,
- 0,
- )
-
- if (isEqualAddress(walletAccount.address, cairo0CalculatedAccountAddress)) {
- console.warn("Address matches Cairo 0 implementation")
- deployAccountPayloadCairo0.constructorCalldata = cairo0Calldata
- return deployAccountPayloadCairo0
+ // If no class hash is provided by the account, we want to add the network implementation to check
+ const networkImplementation: Implementation = {
+ cairoVersion: "1",
+ accountClassHash:
+ await this.cryptoStarknetService.getAccountClassHashForNetwork(
+ walletAccount.network,
+ walletAccount.type,
+ ),
}
- console.warn(
- "Calculated address does not match Cairo 0 account address. Trying old implementation",
- )
-
- // In the end, try to deploy using the old implementation
- const oldCalldata = CallData.compile({
- ...constructorCallData,
- implementation:
- "0x1a7820094feaf82d53f53f214b81292d717e7bb9a92bb2488092cd306f3993f", // old implementation, ask @janek why
- })
-
- const oldCalculatedAddress = calculateContractAddressFromHash(
- deployAccountPayload.addressSalt,
- deployAccountPayload.classHash,
- oldCalldata,
- 0,
+ const { accountClassHash, cairoVersion } = findImplementationForAccount(
+ starkPub,
+ walletAccount,
+ [networkImplementation],
)
- if (isEqualAddress(oldCalculatedAddress, walletAccount.address)) {
- console.warn("Address matches old implementation")
- deployAccountPayload.constructorCalldata = oldCalldata
- } else {
- throw new AccountError({ code: "CALCULATED_ADDRESS_NO_MATCH" })
+ return {
+ ...getAccountDeploymentPayload(cairoVersion, accountClassHash, starkPub),
+ version: cairoVersion as CairoVersion,
+ contractAddress: walletAccount.address,
}
-
- return deployAccountPayload
}
public async getMultisigDeploymentPayload(
@@ -346,7 +304,7 @@ export class WalletDeploymentStarknetService
throw new AccountError({ code: "CALCULATED_ADDRESS_NO_MATCH" })
}
- return deployMultisigPayload
+ return { ...deployMultisigPayload, version: "1" }
}
// TODO: remove this once testing of cairo 1 is done
@@ -378,17 +336,7 @@ export class WalletDeploymentStarknetService
"standardCairo0",
)
- const payload = {
- classHash: PROXY_CONTRACT_CLASS_HASHES[0],
- constructorCalldata: CallData.compile({
- implementation: accountClassHash,
- selector: getSelectorFromName("initialize"),
- calldata: CallData.compile({ signer: pubKey, guardian: "0" }),
- }),
- addressSalt: pubKey,
- }
-
- return payload
+ return getAccountDeploymentPayload("0", accountClassHash, pubKey)
}
public async getDeployContractPayloadForAccountIndex(
@@ -419,16 +367,7 @@ export class WalletDeploymentStarknetService
"standard",
)
- const payload = {
- classHash: accountClassHash,
- constructorCalldata: CallData.compile({
- signer: pubKey,
- guardian: "0",
- }),
- addressSalt: pubKey,
- }
-
- return payload
+ return getAccountDeploymentPayload("1", accountClassHash, pubKey)
}
public async getDeployContractPayloadForMultisig({
@@ -556,7 +495,7 @@ export class WalletDeploymentStarknetService
},
type,
classHash: addressSchema.parse(payload.classHash), // This is only true for new Cairo 1 accounts. For Cairo 0, this is the proxy contract class hash
- cairoVersion: "1",
+ cairoVersion: type === "standardCairo0" ? "0" : "1",
needsDeploy: !isDeployed,
}
diff --git a/packages/extension/src/background/wallet/findImplementationForAddress.test.ts b/packages/extension/src/background/wallet/findImplementationForAddress.test.ts
new file mode 100644
index 000000000..7a1d7906a
--- /dev/null
+++ b/packages/extension/src/background/wallet/findImplementationForAddress.test.ts
@@ -0,0 +1,207 @@
+import { test } from "vitest"
+import {
+ findImplementationForAccount,
+ getAccountDeploymentPayload,
+ getAccountContractAddress,
+ Implementation,
+} from "./findImplementationForAddress"
+import { WalletAccount } from "../../shared/wallet.model"
+import { ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES } from "../../shared/account/starknet.constants"
+import { STANDARD_DEVNET_ACCOUNT_CLASS_HASH } from "../../shared/network/constants"
+
+const validCairoVersion = "1"
+const validAccountClassHash =
+ "0x01a736d6ed154502257f02b1ccdf4d9d1089f80811cd6acad48e6b6a9d1f2003"
+const validPubKey =
+ "0x79c1b964b5c2996ca1ba107616e0c3a9b671d488b696886606270dc5784e131"
+const validAddress = // confirmed inside AX
+ "0xb9209b483a8f0b75ea6244827f66227f619e9dc055b18b16b26732c76dbd9d"
+
+describe("findImplementationForAccount", () => {
+ test("with invalid address", () => {
+ const invalidAddress = "0x456"
+ const account: Pick<
+ WalletAccount,
+ "address" | "classHash" | "cairoVersion"
+ > = {
+ address: invalidAddress,
+ classHash: validAccountClassHash,
+ cairoVersion: validCairoVersion,
+ }
+ const additionalImplementations: Implementation[] = []
+
+ expect(() =>
+ findImplementationForAccount(
+ validPubKey,
+ account,
+ additionalImplementations,
+ ),
+ ).toThrowErrorMatchingInlineSnapshot(
+ `[AccountError: Calculated address does not match account address]`,
+ )
+ })
+
+ test("with invalid pubkey", () => {
+ const invalidPubkey = "0xinvalid"
+ const account: Pick<
+ WalletAccount,
+ "address" | "classHash" | "cairoVersion"
+ > = {
+ address: "0x456",
+ classHash: validAccountClassHash,
+ cairoVersion: validCairoVersion,
+ }
+ const additionalImplementations: Implementation[] = []
+
+ expect(() =>
+ findImplementationForAccount(
+ invalidPubkey,
+ account,
+ additionalImplementations,
+ ),
+ ).toThrowErrorMatchingInlineSnapshot(
+ `[SyntaxError: Cannot convert 0xinvalid to a BigInt]`,
+ )
+ })
+
+ test("with no additional implementations", () => {
+ const account: Pick<
+ WalletAccount,
+ "address" | "classHash" | "cairoVersion"
+ > = {
+ address: validAddress,
+ classHash: validAccountClassHash,
+ cairoVersion: validCairoVersion,
+ }
+
+ const result = findImplementationForAccount(validPubKey, account)
+ expect(result).toMatchInlineSnapshot(`
+ {
+ "accountClassHash": "0x01a736d6ed154502257f02b1ccdf4d9d1089f80811cd6acad48e6b6a9d1f2003",
+ "cairoVersion": "1",
+ }
+ `)
+ })
+
+ test("with additional implementations", () => {
+ const invalidCairoVersion = "0"
+ const invalidAddress =
+ "0x5663ac1c17abd6cb78c09b160e409f9c6acfeff368c3c3a61e458f1d5c6dfd4"
+ const invalidImplementation: Implementation = {
+ accountClassHash: validAccountClassHash,
+ cairoVersion: invalidCairoVersion,
+ }
+ const account: Pick<
+ WalletAccount,
+ "address" | "classHash" | "cairoVersion"
+ > = {
+ address: invalidAddress,
+ classHash: validAccountClassHash,
+ cairoVersion: invalidCairoVersion,
+ }
+ const additionalImplementations: Implementation[] = [invalidImplementation]
+
+ const result = findImplementationForAccount(
+ validPubKey,
+ account,
+ additionalImplementations,
+ )
+ expect(result).toEqual(additionalImplementations[0])
+ })
+})
+
+describe("getAccountDeploymentPayload", () => {
+ const pubKey = "0x123"
+
+ test("when cairoVersion is 1 and accountClassHash is CAIRO_1[0]", () => {
+ const cairoVersion = "1"
+ const accountClassHash = ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES.CAIRO_1[0]
+
+ const result = getAccountDeploymentPayload(
+ cairoVersion,
+ accountClassHash,
+ pubKey,
+ )
+
+ expect(result).toMatchInlineSnapshot(`
+ {
+ "addressSalt": "0x123",
+ "classHash": "0x29927c8af6bccf3f6fda035981e765a7bdbf18a2dc0d630494f8758aa908e2b",
+ "constructorCalldata": [
+ "291",
+ "0",
+ ],
+ }
+ `)
+ })
+
+ test("when cairoVersion is 0 and accountClassHash is CAIRO_0[0]", () => {
+ const cairoVersion = "0"
+ const accountClassHash = ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES.CAIRO_0[0]
+
+ const result = getAccountDeploymentPayload(
+ cairoVersion,
+ accountClassHash,
+ pubKey,
+ )
+
+ expect(result).toMatchInlineSnapshot(`
+ {
+ "addressSalt": "0x123",
+ "classHash": "0x25ec026985a3bf9d0cc1fe17326b245dfdc3ff89b8fde106542a3ea56c5a918",
+ "constructorCalldata": [
+ "1449178161945088530446351771646113898511736767359683664273252560520029776866",
+ "215307247182100370520050591091822763712463273430149262739280891880522753123",
+ "2",
+ "291",
+ "0",
+ ],
+ }
+ `)
+ // Add your assertions here
+ })
+
+ test("when cairoVersion is 1 and accountClassHash is STANDARD_DEVNET_ACCOUNT_CLASS_HASH", () => {
+ const cairoVersion = "1"
+ const accountClassHash = STANDARD_DEVNET_ACCOUNT_CLASS_HASH
+
+ const result = getAccountDeploymentPayload(
+ cairoVersion,
+ accountClassHash,
+ pubKey,
+ )
+
+ expect(result).toMatchInlineSnapshot(`
+ {
+ "addressSalt": "0x123",
+ "classHash": "0x4d07e40e93398ed3c76981e72dd1fd22557a78ce36c0515f679e27f0bb5bc5f",
+ "constructorCalldata": [
+ "291",
+ ],
+ }
+ `)
+ })
+})
+
+describe("getAccountContractAddress", () => {
+ test("valid AX account", () => {
+ const result = getAccountContractAddress(
+ validCairoVersion,
+ validAccountClassHash,
+ validPubKey,
+ )
+
+ expect(result).toEqual(validAddress)
+ })
+ test("bugged account C0", () => {
+ const result = getAccountContractAddress(
+ "0",
+ validAccountClassHash,
+ validPubKey,
+ )
+
+ expect(result).toMatchInlineSnapshot(
+ `"0x5663ac1c17abd6cb78c09b160e409f9c6acfeff368c3c3a61e458f1d5c6dfd4"`,
+ )
+ })
+})
diff --git a/packages/extension/src/background/wallet/findImplementationForAddress.ts b/packages/extension/src/background/wallet/findImplementationForAddress.ts
new file mode 100644
index 000000000..8aec29aff
--- /dev/null
+++ b/packages/extension/src/background/wallet/findImplementationForAddress.ts
@@ -0,0 +1,136 @@
+import { hexSchema, isEqualAddress } from "@argent/shared"
+import { WalletAccount, cairoVersionSchema } from "../../shared/wallet.model"
+import { z } from "zod"
+import {
+ ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES,
+ C0_PROXY_CONTRACT_CLASS_HASHES,
+} from "../../shared/account/starknet.constants"
+import { uniqWith } from "lodash-es"
+import { CallData, Calldata, hash } from "starknet"
+import { AccountError } from "../../shared/errors/account"
+import { STANDARD_DEVNET_ACCOUNT_CLASS_HASH } from "../../shared/network/constants"
+
+export const implementationSchema = z.object({
+ cairoVersion: cairoVersionSchema,
+ accountClassHash: hexSchema,
+})
+export type Implementation = z.infer
+export const isEqualImplementation = (a: Implementation, b: Implementation) =>
+ a.cairoVersion === b.cairoVersion &&
+ isEqualAddress(a.accountClassHash, b.accountClassHash)
+
+export function findImplementationForAccount(
+ pubkey: string,
+ account: Pick,
+ additionalImplementations: Implementation[] = [],
+): Implementation {
+ const parsedAccountImplementation = implementationSchema.parse({
+ cairoVersion: account.cairoVersion ?? "1",
+ accountClassHash:
+ account.classHash ?? ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES.CAIRO_1[0],
+ })
+ const parsedAdditionalImplementations = z
+ .array(implementationSchema)
+ .parse(additionalImplementations)
+
+ const defaultImplementations = [
+ ...ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES.CAIRO_0.map(
+ (ch): Implementation => ({ cairoVersion: "0", accountClassHash: ch }),
+ ),
+ ...ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES.CAIRO_1.map(
+ (ch): Implementation => ({ cairoVersion: "1", accountClassHash: ch }),
+ ),
+ ]
+
+ const uniqueImplementations = uniqWith(
+ [
+ parsedAccountImplementation,
+ ...parsedAdditionalImplementations,
+ ...defaultImplementations,
+ ],
+ isEqualImplementation,
+ )
+
+ // calculate addresses for each implementation
+ const implementationsWithAddresses = uniqueImplementations.map(
+ (implementation) => ({
+ implementation,
+ address: getAccountContractAddress(
+ implementation.cairoVersion,
+ implementation.accountClassHash,
+ pubkey,
+ ),
+ }),
+ )
+
+ // find the implementation that matches the account address
+ const matchingImplementation = implementationsWithAddresses.find(
+ (implementation) => isEqualAddress(implementation.address, account.address),
+ )
+
+ if (!matchingImplementation) {
+ throw new AccountError({ code: "CALCULATED_ADDRESS_NO_MATCH" })
+ }
+
+ return matchingImplementation.implementation
+}
+
+export function getAccountDeploymentPayload(
+ cairoVersion: string,
+ accountClassHash: string,
+ pubKey: string,
+ /** @deprecated This is only used for backwards compatibility with the old proxy contract, should not be used */
+ c0ProxyClassHash: string = C0_PROXY_CONTRACT_CLASS_HASHES[0],
+) {
+ const isDevnetImpl =
+ cairoVersion !== "0" &&
+ isEqualAddress(STANDARD_DEVNET_ACCOUNT_CLASS_HASH, accountClassHash)
+ const implCalldata = {
+ signer: pubKey,
+ ...(isDevnetImpl ? {} : { guardian: "0" }),
+ }
+ const constructorCallData:
+ | {
+ signer: string
+ guardian?: string
+ }
+ | {
+ implementation: string
+ selector: string
+ calldata: Calldata
+ } =
+ cairoVersion === "0"
+ ? {
+ implementation: accountClassHash,
+ selector: hash.getSelectorFromName("initialize"),
+ calldata: CallData.compile(implCalldata),
+ }
+ : implCalldata
+
+ const deployAccountPayload = {
+ classHash: cairoVersion === "0" ? c0ProxyClassHash : accountClassHash,
+ constructorCalldata: CallData.compile(constructorCallData),
+ addressSalt: pubKey,
+ }
+
+ return deployAccountPayload
+}
+
+export function getAccountContractAddress(
+ cairoVersion: string,
+ accountClassHash: string,
+ pubKey: string,
+) {
+ const deployAccountPayload = getAccountDeploymentPayload(
+ cairoVersion,
+ accountClassHash,
+ pubKey,
+ )
+
+ return hash.calculateContractAddressFromHash(
+ deployAccountPayload.addressSalt,
+ deployAccountPayload.classHash,
+ deployAccountPayload.constructorCalldata,
+ 0,
+ )
+}
diff --git a/packages/extension/src/background/wallet/index.ts b/packages/extension/src/background/wallet/index.ts
index 0acd90511..160989f68 100644
--- a/packages/extension/src/background/wallet/index.ts
+++ b/packages/extension/src/background/wallet/index.ts
@@ -1,4 +1,4 @@
-import { Account, InvocationsDetails } from "starknet"
+import { Account, InvocationsDetails } from "starknet6"
import {
ArgentAccountType,
@@ -169,7 +169,7 @@ export class Wallet {
}
public async getAccountDeploymentFee(
walletAccount: WalletAccount,
- feeTokenAddress?: Address,
+ feeTokenAddress: Address,
) {
return this.walletDeploymentStarknetService.getAccountDeploymentFee(
walletAccount,
diff --git a/packages/extension/src/background/wallet/recovery/starknet.service.ts b/packages/extension/src/background/wallet/recovery/starknet.service.ts
index abd910693..37aa220ca 100644
--- a/packages/extension/src/background/wallet/recovery/starknet.service.ts
+++ b/packages/extension/src/background/wallet/recovery/starknet.service.ts
@@ -1,8 +1,5 @@
import { WalletAccountSharedService } from "./../account/shared.service"
-import {
- networkIdToStarknetNetwork,
- networkToDiscoveryNetwork,
-} from "./../../../shared/utils/starknetNetwork"
+import { networkIdToStarknetNetwork } from "./../../../shared/utils/starknetNetwork"
import { union, partition, memoize } from "lodash-es"
import { num } from "starknet"
@@ -30,7 +27,7 @@ import {
generatePublicKeys,
} from "../../keys/keyDerivation"
import { WalletCryptoStarknetService } from "../crypto/starknet.service"
-import { ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES__NEW } from "../starknet.constants"
+import { ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES } from "../../../shared/account/starknet.constants"
import { IWalletRecoveryService } from "./interface"
import { IRepository } from "../../../shared/storage/__new/interface"
import urlJoin from "url-join"
@@ -44,7 +41,9 @@ import {
ApiMultisigDataForSignerSchema,
} from "../../../shared/multisig/multisig.model"
import { RecoveryError } from "../../../shared/errors/recovery"
-import { isContractDeployed } from "@argent/shared"
+import { Address, isContractDeployed } from "@argent/shared"
+import { getAccountContractAddress } from "../findImplementationForAddress"
+import { argentApiNetworkForNetwork } from "../../../shared/api/headers"
const INITIAL_PUB_KEY_COUNT = 5
@@ -62,12 +61,14 @@ export class WalletRecoveryStarknetService implements IWalletRecoveryService {
) {}
private getCairo1AccountContractAddressMemoized = memoize(
- this.cryptoStarknetService.getCairo1AccountContractAddress,
+ (classHash: string, publicKey: string) =>
+ getAccountContractAddress("1", classHash, publicKey),
(classHash, publicKey) => `${classHash}:${publicKey}`,
)
private getCairo0AccountContractAddressMemoized = memoize(
- this.cryptoStarknetService.getCairo0AccountContractAddress,
+ (classHash: string, publicKey: string) =>
+ getAccountContractAddress("0", classHash, publicKey),
(classHash, publicKey) => `${classHash}:${publicKey}`,
)
@@ -114,7 +115,7 @@ export class WalletRecoveryStarknetService implements IWalletRecoveryService {
}
private getStandardAccountDiscoveryUrl(network: Network) {
- const backendNetwork = networkToDiscoveryNetwork(network)
+ const backendNetwork = argentApiNetworkForNetwork(network.id)
if (
process.env.NODE_ENV !== "production" && // be more strict in development
@@ -183,7 +184,7 @@ export class WalletRecoveryStarknetService implements IWalletRecoveryService {
) {
// Discover Cairo1 standard accounts
const cairo1AccountClassHashes = union(
- ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES__NEW.CAIRO_1,
+ ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES.CAIRO_1,
[...(defaultClassHash ? [defaultClassHash] : [])],
)
@@ -194,10 +195,14 @@ export class WalletRecoveryStarknetService implements IWalletRecoveryService {
)
const account = this.walletAccountSharedService.getDefaultStandardAccount(
- pubKeyWithIndex.index,
- address,
- network,
- false,
+ {
+ index: pubKeyWithIndex.index,
+ address,
+ network,
+ needsDeploy: false,
+ name: `Account ${pubKeyWithIndex.index + 1}`,
+ classHash: accountClassHash as Address,
+ },
)
return {
@@ -214,7 +219,7 @@ export class WalletRecoveryStarknetService implements IWalletRecoveryService {
) {
// Discover Cairo0 standard accounts
const cairo0AccountClassHashes = union(
- ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES__NEW.CAIRO_0,
+ ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES.CAIRO_0,
[...(defaultClassHash ? [defaultClassHash] : [])],
)
@@ -225,10 +230,14 @@ export class WalletRecoveryStarknetService implements IWalletRecoveryService {
)
const account = this.walletAccountSharedService.getDefaultStandardAccount(
- pubKeyWithIndex.index,
- address,
- network,
- false,
+ {
+ index: pubKeyWithIndex.index,
+ address,
+ network,
+ needsDeploy: false,
+ name: `Account ${pubKeyWithIndex.index + 1}`,
+ classHash: accountClassHash as Address,
+ },
)
return {
@@ -360,12 +369,12 @@ export class WalletRecoveryStarknetService implements IWalletRecoveryService {
})
return {
- ...this.walletAccountSharedService.getDefaultMultisigAccount(
- pubKeyWithIndex.index,
- validMultisig.address,
+ ...this.walletAccountSharedService.getDefaultMultisigAccount({
+ index: pubKeyWithIndex.index,
+ address: validMultisig.address,
network,
- false,
- ),
+ needsDeploy: false,
+ }),
type: "multisig",
}
})
diff --git a/packages/extension/src/inpage/ArgentXAccount.ts b/packages/extension/src/inpage/ArgentXAccount.ts
index a232b54c8..04fb72df4 100644
--- a/packages/extension/src/inpage/ArgentXAccount.ts
+++ b/packages/extension/src/inpage/ArgentXAccount.ts
@@ -88,10 +88,12 @@ export class ArgentXAccount extends Account {
sendMessage({
type: "REQUEST_DECLARE_CONTRACT",
data: {
- contract,
- classHash,
- casm,
- compiledClassHash,
+ payload: {
+ contract,
+ classHash,
+ casm,
+ compiledClassHash,
+ },
},
})
const { actionHash } = await waitForMessage(
diff --git a/packages/extension/src/inpage/ArgentXProvider4.ts b/packages/extension/src/inpage/ArgentXProvider4.ts
index b4e3ce46c..8f877a602 100644
--- a/packages/extension/src/inpage/ArgentXProvider4.ts
+++ b/packages/extension/src/inpage/ArgentXProvider4.ts
@@ -1,22 +1,16 @@
import { Call, Provider, ProviderInterface } from "starknet4"
import { Network } from "../shared/network"
-import {
- getRandomPublicRPCNode,
- isArgentNetwork,
-} from "../shared/network/utils"
+import { getRandomPublicRPCNode } from "../shared/network/utils"
import { getProviderv4 } from "../shared/network/provider"
+import { argentApiNetworkForNetwork } from "../shared/api/headers"
export class ArgentXProviderV4 extends Provider implements ProviderInterface {
constructor(network: Network) {
// Only expose sequencer provider for argent networks
- if (isArgentNetwork(network)) {
+ const key = argentApiNetworkForNetwork(network.id)
+ if (key) {
const publicRpcNode = getRandomPublicRPCNode(network)
-
- const nodeUrl =
- network.id === "mainnet-alpha"
- ? publicRpcNode.mainnet
- : publicRpcNode.testnet
-
+ const nodeUrl = publicRpcNode[key]
super({ rpc: { nodeUrl } })
} else {
// Otherwise, it's a custom network, so we expose the custom provider
diff --git a/packages/extension/src/inpage/requestMessageHandlers.ts b/packages/extension/src/inpage/requestMessageHandlers.ts
index 125dc9462..025eaa173 100644
--- a/packages/extension/src/inpage/requestMessageHandlers.ts
+++ b/packages/extension/src/inpage/requestMessageHandlers.ts
@@ -8,6 +8,7 @@ import type { Network } from "../shared/network/type"
import { sendMessage, waitForMessage } from "./messageActions"
import { ETH_TOKEN_ADDRESS } from "../shared/network/constants"
import { inpageMessageClient } from "./trpcClient"
+import { CairoVersion } from "starknet"
export async function handleAddTokenRequest(
callParams: WatchAssetParameters,
@@ -179,6 +180,7 @@ interface GetDeploymentDataResult {
class_hash: string // Represented as 'felt252'
salt: string // Represented as 'felt252'
calldata: string[] // Array of 'felt252', length := calldata_len
+ version: CairoVersion
}
const toHex = (x: bigint) => `0x${x.toString(16)}`
@@ -186,12 +188,21 @@ const toHex = (x: bigint) => `0x${x.toString(16)}`
const isStringArray = (x: any): x is string[] =>
x.every((y: any) => typeof y === "string")
-export async function handleDeploymentData(): Promise {
+export async function handleDeploymentData(): Promise {
const deploymentData =
await inpageMessageClient.accountMessaging.getAccountDeploymentPayload.query()
- const { classHash, constructorCalldata, addressSalt, contractAddress } =
- deploymentData
+ if (!deploymentData) {
+ return deploymentData
+ }
+
+ const {
+ version,
+ classHash,
+ constructorCalldata,
+ addressSalt,
+ contractAddress,
+ } = deploymentData
if (!classHash || !constructorCalldata || !addressSalt || !contractAddress) {
throw new Error("Deployment data not found")
@@ -209,5 +220,6 @@ export async function handleDeploymentData(): Promise {
class_hash: classHash,
salt: _addressSalt,
calldata: _callData,
+ version,
}
}
diff --git a/packages/extension/src/shared/account/details/getAccountCairoVersionFromChain.ts b/packages/extension/src/shared/account/details/getAccountCairoVersionFromChain.ts
index 47501ed75..d95756483 100644
--- a/packages/extension/src/shared/account/details/getAccountCairoVersionFromChain.ts
+++ b/packages/extension/src/shared/account/details/getAccountCairoVersionFromChain.ts
@@ -28,7 +28,7 @@ export async function getAccountCairoVersionFromChain(
return {
address: accs[i].address,
networkId,
- cairoVersion: response,
+ cairoVersion: response || accs[i].cairoVersion, // If the onchain call fails, keep the cached one
}
})
return result
diff --git a/packages/extension/src/shared/account/details/getAccountClassHashFromChain.test.ts b/packages/extension/src/shared/account/details/getAccountClassHashFromChain.test.ts
index c87a7e4f0..fb5868749 100644
--- a/packages/extension/src/shared/account/details/getAccountClassHashFromChain.test.ts
+++ b/packages/extension/src/shared/account/details/getAccountClassHashFromChain.test.ts
@@ -7,7 +7,7 @@ import { getMulticallForNetwork } from "../../multicall"
import { getMockWalletAccount } from "../../../../test/walletAccount.mock"
import {
MULTISIG_ACCOUNT_CLASS_HASH,
- STANDARD_ACCOUNT_CLASS_HASH,
+ TXV1_ACCOUNT_CLASS_HASH,
} from "../../network/constants"
import { addressSchema } from "@argent/shared"
import {
@@ -40,7 +40,7 @@ describe("getAccountClassHashFromChain", () => {
mockNetworkService.getById.mockResolvedValueOnce({
...mockNetwork,
accountClassHash: {
- standard: STANDARD_ACCOUNT_CLASS_HASH,
+ standard: TXV1_ACCOUNT_CLASS_HASH,
},
})
@@ -52,7 +52,7 @@ describe("getAccountClassHashFromChain", () => {
}),
]
- mockTryGetClassHash.mockResolvedValueOnce(STANDARD_ACCOUNT_CLASS_HASH)
+ mockTryGetClassHash.mockResolvedValueOnce(TXV1_ACCOUNT_CLASS_HASH)
const call = {
contractAddress: accounts[0].address,
@@ -60,22 +60,18 @@ describe("getAccountClassHashFromChain", () => {
}
mockGetMulticallForNetwork.mockReturnValueOnce({
- callContract: vi
- .fn()
- .mockResolvedValueOnce([STANDARD_ACCOUNT_CLASS_HASH]),
+ callContract: vi.fn().mockResolvedValueOnce([TXV1_ACCOUNT_CLASS_HASH]),
} as any)
mockGetProvider.mockReturnValueOnce({
- getClassHashAt: vi
- .fn()
- .mockResolvedValueOnce(STANDARD_ACCOUNT_CLASS_HASH),
+ getClassHashAt: vi.fn().mockResolvedValueOnce(TXV1_ACCOUNT_CLASS_HASH),
} as any)
const results = await getAccountClassHashFromChain(accounts)
expect(results[0].classHash).not.toBeUndefined()
expect(results[0].classHash).toEqual(
- addressSchema.parse(STANDARD_ACCOUNT_CLASS_HASH),
+ addressSchema.parse(TXV1_ACCOUNT_CLASS_HASH),
)
expect(mockTryGetClassHash).toHaveBeenCalledWith(
@@ -91,7 +87,7 @@ describe("getAccountClassHashFromChain", () => {
address: accounts[0].address,
networkId: accounts[0].networkId,
type: "standard",
- classHash: addressSchema.parse(STANDARD_ACCOUNT_CLASS_HASH),
+ classHash: addressSchema.parse(TXV1_ACCOUNT_CLASS_HASH),
})
})
@@ -101,7 +97,7 @@ describe("getAccountClassHashFromChain", () => {
mockNetworkService.getById.mockResolvedValueOnce({
...mockNetwork,
accountClassHash: {
- standard: STANDARD_ACCOUNT_CLASS_HASH,
+ standard: TXV1_ACCOUNT_CLASS_HASH,
multisig: MULTISIG_ACCOUNT_CLASS_HASH,
},
})
@@ -111,7 +107,7 @@ describe("getAccountClassHashFromChain", () => {
address: "0x01",
networkId: mockNetwork.id,
network: mockNetwork,
- classHash: STANDARD_ACCOUNT_CLASS_HASH,
+ classHash: TXV1_ACCOUNT_CLASS_HASH,
}),
getMockWalletAccount({
address: "0x02",
@@ -123,7 +119,7 @@ describe("getAccountClassHashFromChain", () => {
]
mockTryGetClassHash
- .mockResolvedValueOnce(STANDARD_ACCOUNT_CLASS_HASH)
+ .mockResolvedValueOnce(TXV1_ACCOUNT_CLASS_HASH)
.mockResolvedValueOnce(MULTISIG_ACCOUNT_CLASS_HASH)
const first_call = {
@@ -137,22 +133,18 @@ describe("getAccountClassHashFromChain", () => {
}
mockGetMulticallForNetwork.mockReturnValueOnce({
- callContract: vi
- .fn()
- .mockResolvedValueOnce([STANDARD_ACCOUNT_CLASS_HASH]),
+ callContract: vi.fn().mockResolvedValueOnce([TXV1_ACCOUNT_CLASS_HASH]),
} as any)
mockGetProvider.mockReturnValueOnce({
- getClassHashAt: vi
- .fn()
- .mockResolvedValueOnce(STANDARD_ACCOUNT_CLASS_HASH),
+ getClassHashAt: vi.fn().mockResolvedValueOnce(TXV1_ACCOUNT_CLASS_HASH),
} as any)
const results = await getAccountClassHashFromChain(accounts)
expect(results[0].classHash).not.toBeUndefined()
expect(results[0].classHash).toEqual(
- addressSchema.parse(STANDARD_ACCOUNT_CLASS_HASH),
+ addressSchema.parse(TXV1_ACCOUNT_CLASS_HASH),
)
expect(results[1].classHash).not.toBeUndefined()
@@ -167,7 +159,7 @@ describe("getAccountClassHashFromChain", () => {
callContract: expect.any(Function),
getClassHashAt: expect.any(Function),
}),
- STANDARD_ACCOUNT_CLASS_HASH,
+ TXV1_ACCOUNT_CLASS_HASH,
)
expect(mockTryGetClassHash).toHaveBeenNthCalledWith(
@@ -184,7 +176,7 @@ describe("getAccountClassHashFromChain", () => {
address: accounts[0].address,
networkId: accounts[0].networkId,
type: "standard",
- classHash: addressSchema.parse(STANDARD_ACCOUNT_CLASS_HASH),
+ classHash: addressSchema.parse(TXV1_ACCOUNT_CLASS_HASH),
})
expect(results[1]).toEqual({
@@ -201,7 +193,7 @@ describe("getAccountClassHashFromChain", () => {
mockNetworkService.getById.mockResolvedValueOnce({
...mockNetwork,
accountClassHash: {
- standard: STANDARD_ACCOUNT_CLASS_HASH,
+ standard: TXV1_ACCOUNT_CLASS_HASH,
},
})
@@ -216,25 +208,23 @@ describe("getAccountClassHashFromChain", () => {
mockGetProvider.mockReturnValueOnce({
callContract: vi
.fn()
- .mockResolvedValueOnce({ result: [STANDARD_ACCOUNT_CLASS_HASH] }),
- getClassHashAt: vi
- .fn()
- .mockResolvedValueOnce(STANDARD_ACCOUNT_CLASS_HASH),
+ .mockResolvedValueOnce({ result: [TXV1_ACCOUNT_CLASS_HASH] }),
+ getClassHashAt: vi.fn().mockResolvedValueOnce(TXV1_ACCOUNT_CLASS_HASH),
} as any)
- mockTryGetClassHash.mockResolvedValueOnce(STANDARD_ACCOUNT_CLASS_HASH)
+ mockTryGetClassHash.mockResolvedValueOnce(TXV1_ACCOUNT_CLASS_HASH)
const results = await getAccountClassHashFromChain(accounts)
expect(results[0].classHash).not.toBeUndefined()
expect(results[0].classHash).toEqual(
- addressSchema.parse(STANDARD_ACCOUNT_CLASS_HASH),
+ addressSchema.parse(TXV1_ACCOUNT_CLASS_HASH),
)
expect(mockTryGetClassHash).toHaveBeenCalledTimes(1)
expect(results[0]).toEqual({
address: accounts[0].address,
networkId: accounts[0].networkId,
type: "standard",
- classHash: addressSchema.parse(STANDARD_ACCOUNT_CLASS_HASH),
+ classHash: addressSchema.parse(TXV1_ACCOUNT_CLASS_HASH),
})
})
})
diff --git a/packages/extension/src/shared/account/details/getAccountClassHashFromChain.ts b/packages/extension/src/shared/account/details/getAccountClassHashFromChain.ts
index fe8905ff9..f986772b6 100644
--- a/packages/extension/src/shared/account/details/getAccountClassHashFromChain.ts
+++ b/packages/extension/src/shared/account/details/getAccountClassHashFromChain.ts
@@ -11,7 +11,7 @@ import { addressSchema } from "@argent/shared"
import { tryGetClassHash } from "./tryGetClassHash"
import {
MULTISIG_ACCOUNT_CLASS_HASH,
- STANDARD_ACCOUNT_CLASS_HASH,
+ TXV1_ACCOUNT_CLASS_HASH,
} from "../../network/constants"
export type AccountClassHashFromChain = Pick<
@@ -26,7 +26,7 @@ const getDefaultClassHash = (account: WalletAccount) => {
if (account.type === "multisig") {
return MULTISIG_ACCOUNT_CLASS_HASH
}
- return STANDARD_ACCOUNT_CLASS_HASH
+ return TXV1_ACCOUNT_CLASS_HASH
}
/**
diff --git a/packages/extension/src/shared/account/details/getEscape.ts b/packages/extension/src/shared/account/details/getEscape.ts
index 79d44bb7d..4d8b20ec9 100644
--- a/packages/extension/src/shared/account/details/getEscape.ts
+++ b/packages/extension/src/shared/account/details/getEscape.ts
@@ -9,6 +9,7 @@ import {
ESCAPE_TYPE_SIGNER,
Escape,
} from "./escape.model"
+import { multicallWithCairo0Fallback } from "./multicallWithCairo0Fallback"
/**
* Get escape state from account
@@ -17,20 +18,12 @@ import {
export const getEscapeForAccount = async (account: BaseWalletAccount) => {
const network = await networkService.getById(account.networkId)
- // Prioritize Cairo 1 get_escape over cairo 0 getEscape
const call: Call = {
contractAddress: account.address,
entrypoint: "get_escape",
}
const multicall = getMulticallForNetwork(network)
- let response: { result: string[] } = { result: [] }
-
- try {
- response = await multicall.callContract(call)
- } catch {
- call.entrypoint = "getEscape"
- response = await multicall.callContract(call)
- }
+ const response = await multicallWithCairo0Fallback(call, multicall)
return shapeResponse(response.result)
}
diff --git a/packages/extension/src/shared/account/details/getGuardian.ts b/packages/extension/src/shared/account/details/getGuardian.ts
index 75624fd9e..ff616f113 100644
--- a/packages/extension/src/shared/account/details/getGuardian.ts
+++ b/packages/extension/src/shared/account/details/getGuardian.ts
@@ -3,30 +3,18 @@ import { Call, constants, num } from "starknet"
import { getMulticallForNetwork } from "../../multicall"
import { networkService } from "../../network/service"
import { BaseWalletAccount } from "../../wallet.model"
-
-/**
- * Get guardian address of account, or undefined if getGuardian returns `0x0` or account is not current implementation
- */
+import { multicallWithCairo0Fallback } from "./multicallWithCairo0Fallback"
export const getGuardianForAccount = async (
account: BaseWalletAccount,
): Promise => {
const network = await networkService.getById(account.networkId)
- // Prioritize Cairo 1 get_guardian over cairo 0 getGuardian
const call: Call = {
contractAddress: account.address,
entrypoint: "get_guardian",
}
const multicall = getMulticallForNetwork(network)
- let response: { result: string[] } = { result: [] }
-
- try {
- response = await multicall.callContract(call)
- } catch {
- call.entrypoint = "getGuardian"
- response = await multicall.callContract(call)
- }
- // if guardian is 0, return undefined
+ const response = await multicallWithCairo0Fallback(call, multicall)
return num.toHex(response.result[0]) === num.toHex(constants.ZERO)
? undefined
: response.result[0]
diff --git a/packages/extension/src/shared/account/details/getImplementation.ts b/packages/extension/src/shared/account/details/getImplementation.ts
index 1eac4bc75..dbd8a8a36 100644
--- a/packages/extension/src/shared/account/details/getImplementation.ts
+++ b/packages/extension/src/shared/account/details/getImplementation.ts
@@ -3,7 +3,7 @@ import { accountsEqual } from "../../utils/accountsEqual"
import { getMulticallForNetwork } from "../../multicall"
import { getProvider } from "../../network"
-import { STANDARD_ACCOUNT_CLASS_HASH } from "../../network/constants"
+import { TXV1_ACCOUNT_CLASS_HASH } from "../../network/constants"
import { networkService } from "../../network/service"
import { BaseWalletAccount } from "../../wallet.model"
import { IAccountService } from "../service/interface"
@@ -40,7 +40,7 @@ export const getImplementationForAccount = async (
return (
walletAccount.network.accountClassHash?.[walletAccount.type] ??
- STANDARD_ACCOUNT_CLASS_HASH
+ TXV1_ACCOUNT_CLASS_HASH
)
}
}
diff --git a/packages/extension/src/shared/account/details/getOwner.ts b/packages/extension/src/shared/account/details/getOwner.ts
index 923aa7baa..510515736 100644
--- a/packages/extension/src/shared/account/details/getOwner.ts
+++ b/packages/extension/src/shared/account/details/getOwner.ts
@@ -3,6 +3,7 @@ import { Call } from "starknet"
import { getMulticallForNetwork } from "../../multicall"
import { networkService } from "../../network/service"
import { BaseWalletAccount } from "../../wallet.model"
+import { multicallWithCairo0Fallback } from "./multicallWithCairo0Fallback"
/**
* Get owner public key of account
@@ -12,19 +13,12 @@ export const getOwnerForAccount = async (
account: BaseWalletAccount,
): Promise => {
const network = await networkService.getById(account.networkId)
- // Prioritize Cairo 1 get_owner over cairo 0 getOwner
const call: Call = {
contractAddress: account.address,
entrypoint: "get_owner",
}
const multicall = getMulticallForNetwork(network)
- let response: { result: string[] } = { result: [] }
+ const response = await multicallWithCairo0Fallback(call, multicall)
- try {
- response = await multicall.callContract(call)
- } catch {
- call.entrypoint = "getOwner"
- response = await multicall.callContract(call)
- }
return response.result[0]
}
diff --git a/packages/extension/src/shared/account/details/multicallWithCairo0Fallback.test.ts b/packages/extension/src/shared/account/details/multicallWithCairo0Fallback.test.ts
new file mode 100644
index 000000000..970085045
--- /dev/null
+++ b/packages/extension/src/shared/account/details/multicallWithCairo0Fallback.test.ts
@@ -0,0 +1,59 @@
+import { MinimalProviderInterface } from "@argent/x-multicall"
+import { Call } from "starknet"
+import { Mocked, describe, expect, test } from "vitest"
+
+import { multicallWithCairo0Fallback } from "./multicallWithCairo0Fallback"
+
+describe("shared/account/details", () => {
+ describe("multicallWithCairo0Fallback", () => {
+ describe("when the call is successful", () => {
+ test("makes a single Cairo 1 call and returns the result", async () => {
+ const multicall = {
+ callContract: vi.fn(),
+ } as Mocked
+ const call: Call = {
+ entrypoint: "fooBar",
+ contractAddress: "0x0",
+ }
+ multicall.callContract.mockResolvedValueOnce({ result: ["baz"] })
+ const result = await multicallWithCairo0Fallback(call, multicall)
+ expect(multicall.callContract).toHaveBeenCalledOnce()
+ expect(multicall.callContract).toHaveBeenLastCalledWith(
+ expect.objectContaining({ entrypoint: "foo_bar" }),
+ )
+ expect(result).toEqual({
+ result: ["baz"],
+ })
+ })
+ })
+ describe("when the call fails", () => {
+ test("makes a Cairo 1 then a Cairo 0 call and returns the result", async () => {
+ const multicall = {
+ callContract: vi.fn(),
+ } as Mocked
+ const call: Call = {
+ entrypoint: "fooBar",
+ contractAddress: "0x0",
+ }
+ // { result: undefined } is not valid, but is returned if the first call fails
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error
+ multicall.callContract.mockResolvedValueOnce({ result: undefined })
+ multicall.callContract.mockResolvedValueOnce({ result: ["baz"] })
+ const result = await multicallWithCairo0Fallback(call, multicall)
+ expect(multicall.callContract).toHaveBeenCalledTimes(2)
+ expect(multicall.callContract).toHaveBeenNthCalledWith(
+ 1,
+ expect.objectContaining({ entrypoint: "foo_bar" }),
+ )
+ expect(multicall.callContract).toHaveBeenNthCalledWith(
+ 2,
+ expect.objectContaining({ entrypoint: "fooBar" }),
+ )
+ expect(result).toEqual({
+ result: ["baz"],
+ })
+ })
+ })
+ })
+})
diff --git a/packages/extension/src/shared/account/details/multicallWithCairo0Fallback.ts b/packages/extension/src/shared/account/details/multicallWithCairo0Fallback.ts
new file mode 100644
index 000000000..f64696898
--- /dev/null
+++ b/packages/extension/src/shared/account/details/multicallWithCairo0Fallback.ts
@@ -0,0 +1,29 @@
+import { Call } from "starknet"
+import { MinimalProviderInterface } from "@argent/x-multicall"
+
+import { getEntryPointSafe } from "../../utils/transactions"
+
+export async function multicallWithCairo0Fallback(
+ call: Call,
+ multicall: MinimalProviderInterface,
+) {
+ // Prioritize Cairo 1
+ try {
+ const callCairo1 = {
+ ...call,
+ entrypoint: getEntryPointSafe(call.entrypoint, "1"),
+ }
+ const response = await multicall.callContract(callCairo1)
+ if (response.result !== undefined) {
+ return response
+ }
+ } catch {
+ // ignore multicall error
+ }
+ // Fallback to Cairo 0
+ const callCairo0 = {
+ ...call,
+ entrypoint: getEntryPointSafe(call.entrypoint, "0"),
+ }
+ return await multicall.callContract(callCairo0)
+}
diff --git a/packages/extension/src/shared/account/details/tryGetClassHash.ts b/packages/extension/src/shared/account/details/tryGetClassHash.ts
index 7e356ff18..98c5d48e1 100644
--- a/packages/extension/src/shared/account/details/tryGetClassHash.ts
+++ b/packages/extension/src/shared/account/details/tryGetClassHash.ts
@@ -1,5 +1,5 @@
import { Call, ProviderInterface } from "starknet"
-import { STANDARD_ACCOUNT_CLASS_HASH } from "../../network/constants"
+import { TXV1_ACCOUNT_CLASS_HASH } from "../../network/constants"
import { addressSchema } from "@argent/shared"
export async function tryGetClassHash(
@@ -18,7 +18,7 @@ export async function tryGetClassHash(
if (fallbackClassHash) {
return fallbackClassHash
}
- return STANDARD_ACCOUNT_CLASS_HASH
+ return TXV1_ACCOUNT_CLASS_HASH
}
}
}
diff --git a/packages/extension/src/shared/account/optimisticImplUpdate.test.ts b/packages/extension/src/shared/account/optimisticImplUpdate.test.ts
new file mode 100644
index 000000000..a541d35e7
--- /dev/null
+++ b/packages/extension/src/shared/account/optimisticImplUpdate.test.ts
@@ -0,0 +1,51 @@
+import { getMockWalletAccount } from "../../../test/walletAccount.mock"
+import { optimisticImplUpdate } from "./optimisticImplUpdate"
+import { ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES } from "./starknet.constants"
+
+describe("optimisticImplUpdate", () => {
+ it("should return the account if newClassHash is not defined", () => {
+ const account = getMockWalletAccount()
+ const newClassHash = undefined
+ const result = optimisticImplUpdate(account, newClassHash)
+ expect(result).toEqual(account)
+ })
+ it("should return the account with the new class hash and the cairo version 1 if newClassHash is in CAIRO_1", () => {
+ const account = getMockWalletAccount({
+ classHash: ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES.CAIRO_0[0],
+ cairoVersion: "0",
+ })
+ const newClassHash = ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES.CAIRO_1[0]
+ const result = optimisticImplUpdate(account, newClassHash)
+ expect(result).toEqual({
+ ...account,
+ classHash: newClassHash,
+ cairoVersion: "1",
+ })
+ })
+ it("should return the account with the new class hash and the cairo version 0 if newClassHash is in CAIRO_0", () => {
+ const account = getMockWalletAccount({
+ classHash: ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES.CAIRO_1[0],
+ cairoVersion: "1",
+ })
+ const newClassHash = ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES.CAIRO_0[0]
+ const result = optimisticImplUpdate(account, newClassHash)
+ expect(result).toEqual({
+ ...account,
+ classHash: newClassHash,
+ cairoVersion: "0",
+ })
+ })
+ it("should return the account with the new class hash and the current cairo version if newClassHash is not in CAIRO_1 or CAIRO_0", () => {
+ const account = getMockWalletAccount({
+ classHash: "0x123",
+ cairoVersion: "1",
+ })
+ const newClassHash = "0xabc"
+ const result = optimisticImplUpdate(account, newClassHash)
+ expect(result).toEqual({
+ ...account,
+ classHash: newClassHash,
+ cairoVersion: "1",
+ })
+ })
+})
diff --git a/packages/extension/src/shared/account/optimisticImplUpdate.ts b/packages/extension/src/shared/account/optimisticImplUpdate.ts
new file mode 100644
index 000000000..8921d5b51
--- /dev/null
+++ b/packages/extension/src/shared/account/optimisticImplUpdate.ts
@@ -0,0 +1,22 @@
+import { Address, isEqualAddress } from "@argent/shared"
+import { WalletAccount } from "../wallet.model"
+import { ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES } from "./starknet.constants"
+
+// Use this with caution, as it might not reflect the onchain state,
+// but just an optimistic update
+export const optimisticImplUpdate = (
+ account: WalletAccount,
+ newClassHash?: Address,
+): WalletAccount => {
+ if (!newClassHash) return account
+
+ const { CAIRO_0, CAIRO_1 } = ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES
+
+ const cairoVersion = CAIRO_1.some((c1) => isEqualAddress(c1, newClassHash))
+ ? "1"
+ : CAIRO_0.some((c0) => isEqualAddress(c0, newClassHash))
+ ? "0"
+ : account.cairoVersion // fallback to the current account's cairo version
+
+ return { ...account, classHash: newClassHash, cairoVersion }
+}
diff --git a/packages/extension/src/shared/account/service/implementation.ts b/packages/extension/src/shared/account/service/implementation.ts
index b2548d6b6..29eacb297 100644
--- a/packages/extension/src/shared/account/service/implementation.ts
+++ b/packages/extension/src/shared/account/service/implementation.ts
@@ -5,7 +5,7 @@ import { accountsEqual } from "../../utils/accountsEqual"
import { withoutHiddenSelector } from "../selectors"
import type { IAccountRepo } from "../store"
import type { IAccountService } from "./interface"
-
+import { ProvisionActivityPayload } from "../../activity/types"
export class AccountService implements IAccountService {
constructor(
private readonly chainService: IChainService,
@@ -72,4 +72,15 @@ export class AccountService implements IAccountService {
async getDeployed(baseAccount: BaseWalletAccount): Promise {
return this.chainService.getDeployed(baseAccount)
}
+
+ async handleProvisionedAccount(payload: ProvisionActivityPayload) {
+ await this.update(
+ (account) => accountsEqual(account, payload.account),
+ (account) => ({
+ ...account,
+ provisionAmount: payload.activity.transfers[0].asset.amount,
+ provisionDate: payload.activity.lastModified,
+ }),
+ )
+ }
}
diff --git a/packages/extension/src/shared/account/service/interface.ts b/packages/extension/src/shared/account/service/interface.ts
index 6f18506ba..4f2a761db 100644
--- a/packages/extension/src/shared/account/service/interface.ts
+++ b/packages/extension/src/shared/account/service/interface.ts
@@ -1,3 +1,4 @@
+import { ProvisionActivityPayload } from "../../activity/types"
import { AllowArray, SelectorFn } from "../../storage/__new/interface"
import { BaseWalletAccount, WalletAccount } from "../../wallet.model"
@@ -16,4 +17,7 @@ export interface IAccountService {
// getters
getDeployed(baseAccount: BaseWalletAccount): Promise
+
+ // handlers
+ handleProvisionedAccount(payload: ProvisionActivityPayload): Promise
}
diff --git a/packages/extension/src/background/wallet/starknet.constants.ts b/packages/extension/src/shared/account/starknet.constants.ts
similarity index 51%
rename from packages/extension/src/background/wallet/starknet.constants.ts
rename to packages/extension/src/shared/account/starknet.constants.ts
index 1298e7f6f..7770ed3fe 100644
--- a/packages/extension/src/background/wallet/starknet.constants.ts
+++ b/packages/extension/src/shared/account/starknet.constants.ts
@@ -1,14 +1,10 @@
-export const PROXY_CONTRACT_CLASS_HASHES = [
+export const C0_PROXY_CONTRACT_CLASS_HASHES = [
"0x25ec026985a3bf9d0cc1fe17326b245dfdc3ff89b8fde106542a3ea56c5a918",
-]
-export const ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES = [
- "0x033434ad846cdd5f23eb73ff09fe6fddd568284a0fb7d1be20ee482f044dabe2",
- "0x1a7820094feaf82d53f53f214b81292d717e7bb9a92bb2488092cd306f3993f",
- "0x3e327de1c40540b98d05cbcb13552008e36f0ec8d61d46956d2f9752c294328",
- "0x7e28fb0161d10d1cf7fe1f13e7ca57bce062731a3bd04494dfd2d0412699727",
-]
+] as const
-export const ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES__NEW = {
+// ClassHashes are arranged from latest to oldest
+// For example, CAIRO_0[0] is the latest version of CAIRO_0
+export const ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES = {
CAIRO_0: [
"0x033434ad846cdd5f23eb73ff09fe6fddd568284a0fb7d1be20ee482f044dabe2",
"0x1a7820094feaf82d53f53f214b81292d717e7bb9a92bb2488092cd306f3993f",
@@ -17,6 +13,8 @@ export const ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES__NEW = {
],
CAIRO_1: [
+ "0x29927c8af6bccf3f6fda035981e765a7bdbf18a2dc0d630494f8758aa908e2b",
+ "0x02fadbf77a721b94bdcc3032d86a8921661717fa55145bccf88160ee2a5efcd1",
"0x1a736d6ed154502257f02b1ccdf4d9d1089f80811cd6acad48e6b6a9d1f2003",
],
-}
+} as const
diff --git a/packages/extension/src/shared/actionQueue/queue/interface.ts b/packages/extension/src/shared/actionQueue/queue/interface.ts
index aafb92b6f..7adbd381d 100644
--- a/packages/extension/src/shared/actionQueue/queue/interface.ts
+++ b/packages/extension/src/shared/actionQueue/queue/interface.ts
@@ -8,6 +8,17 @@ export interface IActionQueue {
item: U,
meta?: Partial,
) => Promise>
+ addFront: (
+ item: U,
+ meta?: Partial,
+ ) => Promise>
+ update: (
+ hash: string,
+ updatedItem: Partial<{
+ meta: Partial>
+ }> &
+ Partial,
+ ) => Promise | null>
updateMeta: (
hash: string,
meta: Partial>,
diff --git a/packages/extension/src/shared/actionQueue/queue/queue.test.ts b/packages/extension/src/shared/actionQueue/queue/queue.test.ts
index 2c7b04ebc..6aa95c0a6 100644
--- a/packages/extension/src/shared/actionQueue/queue/queue.test.ts
+++ b/packages/extension/src/shared/actionQueue/queue/queue.test.ts
@@ -19,7 +19,20 @@ const txFixture: ActionItem = {
},
}
+const txFixture2: ActionItem = {
+ type: "TRANSACTION",
+ payload: {
+ transactions: {
+ contractAddress: "0x456",
+ entrypoint: "fooBar",
+ calldata: [],
+ },
+ createdAt: 456,
+ },
+}
+
const txFixtureHash = objectHash(txFixture)
+const txFixtureHash2 = objectHash(txFixture2)
describe("actionQueue", () => {
const actionQueueRepo = new InMemoryRepository({
@@ -44,6 +57,19 @@ describe("actionQueue", () => {
expect(item.meta.hash).toEqual(txFixtureHash)
})
+ it("automatically adds an item to the front of the queue", async () => {
+ await actionQueue.add(txFixture)
+ await actionQueue.addFront(txFixture2)
+ const [item] = await actionQueue.getAll()
+ expect(item.meta.hash).toEqual(txFixtureHash2)
+ })
+
+ it("automatically adds an item to the front even the queue is empty", async () => {
+ await actionQueue.addFront(txFixture)
+ const [item] = await actionQueue.getAll()
+ expect(item.meta.hash).toEqual(txFixtureHash)
+ })
+
it("automatically removes expired items on getAll", async () => {
await actionQueue.add(txFixture, { expires: 100 })
const [item] = await actionQueue.getAll()
@@ -66,6 +92,10 @@ describe("actionQueue", () => {
await actionQueue.add(txFixture)
const items = await actionQueue.getAll()
expect(items.length).toEqual(1)
+
+ await actionQueue.addFront(txFixture)
+ const itemsFront = await actionQueue.getAll()
+ expect(itemsFront.length).toEqual(1)
})
it("updates meta", async () => {
diff --git a/packages/extension/src/shared/actionQueue/queue/queue.ts b/packages/extension/src/shared/actionQueue/queue/queue.ts
index 15489c238..300c8cf81 100644
--- a/packages/extension/src/shared/actionQueue/queue/queue.ts
+++ b/packages/extension/src/shared/actionQueue/queue/queue.ts
@@ -59,6 +59,7 @@ export function getActionQueue(
async function add(
item: U,
meta?: Partial,
+ front = false,
): Promise> {
if (isTransactionActionItem(item) && !item.payload.createdAt) {
/**
@@ -78,14 +79,24 @@ export function getActionQueue(
},
}
- await storage.upsert(newItem)
+ await storage.upsert(newItem, front ? "unshift" : "push")
return newItem
}
- async function updateMeta(
+ async function addFront(
+ item: U,
+ meta?: Partial,
+ ): Promise> {
+ return add(item, meta, true)
+ }
+
+ async function update(
hash: string,
- meta: Partial>,
+ updatedItem: Partial<{
+ meta: Partial>
+ }> &
+ Partial,
): Promise | null> {
const item = await get(hash)
@@ -95,9 +106,10 @@ export function getActionQueue(
const newItem = {
...item,
+ ...updatedItem,
meta: {
...item.meta,
- ...meta,
+ ...updatedItem.meta,
},
}
@@ -106,6 +118,16 @@ export function getActionQueue(
return newItem
}
+ async function updateMeta(
+ hash: string,
+ meta: Partial>,
+ ): Promise | null> {
+ return update(hash, { meta } as Partial<{
+ meta: Partial>
+ }> &
+ Partial)
+ }
+
async function remove(hash: string): Promise | null> {
const [item] = await storage.remove((item) => item.meta.hash === hash)
return item ?? null
@@ -119,6 +141,8 @@ export function getActionQueue(
get,
getAll,
add,
+ addFront,
+ update,
updateMeta,
remove,
removeAll,
diff --git a/packages/extension/src/shared/activity/__fixtures__/activities-deploy.json b/packages/extension/src/shared/activity/__fixtures__/activities-deploy.json
new file mode 100644
index 000000000..3d243be5a
--- /dev/null
+++ b/packages/extension/src/shared/activity/__fixtures__/activities-deploy.json
@@ -0,0 +1,219 @@
+[
+ {
+ "compositeId": "be7ca513de1bff27d342073c21f7290e9f2991611a7a3cb85c6796f396399ef1",
+ "id": "9bc71a4c-1da1-4f4f-9168-d06112ea6be2",
+ "status": "success",
+ "wallet": "0x07df7c3ed69cbacf5c4e39f0e3270699f53160b29ee14a8691057fa68bf78c42",
+ "txSender": "0x074d0d03c4333fe1979cb2523ae35763bb9af7e57d7edce2ce9a581618e9203b",
+ "source": "transaction-monitor",
+ "type": "multicall",
+ "group": "finance",
+ "submitted": 1707156314000,
+ "lastModified": 1707156503458,
+ "transaction": {
+ "network": "starknet",
+ "hash": "0x031d5c57c49081855a8e472a6383b78a50a9408afadc4e1c04e310e8352f5fdf",
+ "status": "success",
+ "blockNumber": 945585,
+ "transactionIndex": 22
+ },
+ "transferSummary": [
+ {
+ "asset": {
+ "type": "token",
+ "tokenAddress": "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d",
+ "amount": "12500000000000000000000",
+ "fiatAmount": {
+ "currency": "USD",
+ "currencyAmount": 3474257.2
+ }
+ },
+ "sent": true
+ },
+ {
+ "asset": {
+ "type": "token",
+ "tokenAddress": "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d",
+ "amount": "25000000000000000000000",
+ "fiatAmount": {
+ "currency": "USD",
+ "currencyAmount": 6948514.4
+ }
+ },
+ "sent": false
+ },
+ {
+ "asset": {
+ "type": "token",
+ "tokenAddress": "0x01a881a75bb478cedfd4d3ea19d2a4564350d78ea463a5287833526a416d5e31",
+ "amount": "12500000000000000000000"
+ },
+ "sent": false
+ }
+ ],
+ "transfers": [
+ {
+ "type": "payment",
+ "leg": "credit",
+ "counterparty": "0x2b2e8b8eb3429540c58c0dc69ebb2981267196fe0ca2e361056b852445ee766",
+ "asset": {
+ "type": "token",
+ "tokenAddress": "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d",
+ "amount": "25000000000000000000000",
+ "fiatAmount": {
+ "currency": "USD",
+ "currencyAmount": 6948514.4
+ }
+ },
+ "counterpartyNetwork": "starknet"
+ },
+ {
+ "type": "payment",
+ "leg": "debit",
+ "counterparty": "0x1a881a75bb478cedfd4d3ea19d2a4564350d78ea463a5287833526a416d5e31",
+ "asset": {
+ "type": "token",
+ "tokenAddress": "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d",
+ "amount": "12500000000000000000000",
+ "fiatAmount": {
+ "currency": "USD",
+ "currencyAmount": 3474257.2
+ }
+ },
+ "counterpartyNetwork": "starknet"
+ },
+ {
+ "type": "payment",
+ "leg": "credit",
+ "counterparty": "0x0",
+ "asset": {
+ "type": "token",
+ "tokenAddress": "0x01a881a75bb478cedfd4d3ea19d2a4564350d78ea463a5287833526a416d5e31",
+ "amount": "12500000000000000000000"
+ },
+ "counterpartyNetwork": "starknet"
+ }
+ ],
+ "fees": [],
+ "relatedAddresses": [
+ {
+ "address": "0x02b2e8b8eb3429540c58c0dc69ebb2981267196fe0ca2e361056b852445ee766",
+ "network": "starknet",
+ "type": "wallet"
+ },
+ {
+ "address": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
+ "network": "starknet",
+ "type": "token"
+ },
+ {
+ "address": "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d",
+ "network": "starknet",
+ "type": "token"
+ },
+ {
+ "address": "0x074d0d03c4333fe1979cb2523ae35763bb9af7e57d7edce2ce9a581618e9203b",
+ "network": "starknet",
+ "type": "wallet"
+ },
+ {
+ "address": "0x01a881a75bb478cedfd4d3ea19d2a4564350d78ea463a5287833526a416d5e31",
+ "network": "starknet",
+ "type": "token"
+ },
+ {
+ "address": "0x05f26b643443257ba3477bcf79d0b9c08168442d88d0bf8bf3ac739199c260f9",
+ "network": "starknet",
+ "type": "wallet"
+ },
+ {
+ "address": "0x01a881a75bb478cedfd4d3ea19d2a4564350d78ea463a5287833526a416d5e31",
+ "network": "starknet",
+ "type": "wallet"
+ },
+ {
+ "address": "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "network": "starknet",
+ "type": "wallet"
+ }
+ ],
+ "networkDetails": {
+ "ethereumNetwork": "goerli",
+ "chainId": "TESTNET"
+ },
+ "details": {
+ "calls": [
+ {
+ "details": {
+ "deployer": "0x74d0d03c4333fe1979cb2523ae35763bb9af7e57d7edce2ce9a581618e9203b",
+ "contractAddress": "0x7df7c3ed69cbacf5c4e39f0e3270699f53160b29ee14a8691057fa68bf78c42",
+ "type": "deploy"
+ }
+ },
+ {
+ "details": {
+ "counterparty": "0x2b2e8b8eb3429540c58c0dc69ebb2981267196fe0ca2e361056b852445ee766",
+ "leg": "credit",
+ "asset": {
+ "type": "token",
+ "tokenAddress": "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d",
+ "amount": "25000000000000000000000",
+ "fiatAmount": {
+ "currency": "USD",
+ "currencyAmount": 6948514.4
+ }
+ },
+ "context": {
+ "isProvisionAirdrop": true
+ },
+ "type": "payment",
+ "counterpartyNetwork": "starknet"
+ }
+ },
+ {
+ "details": {
+ "dappAddress": "0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d",
+ "function": {
+ "name": "lock_and_delegate_by_sig",
+ "parameters": [
+ {
+ "type": "core::starknet::contract_address::ContractAddress",
+ "name": "account",
+ "value": "0x7df7c3ed69cbacf5c4e39f0e3270699f53160b29ee14a8691057fa68bf78c42"
+ },
+ {
+ "type": "core::starknet::contract_address::ContractAddress",
+ "name": "delegatee",
+ "value": "0x7df7c3ed69cbacf5c4e39f0e3270699f53160b29ee14a8691057fa68bf78c42"
+ },
+ {
+ "type": "core::integer::u256",
+ "name": "amount",
+ "value": "12500000000000000000000"
+ },
+ {
+ "type": "core::felt252",
+ "name": "nonce",
+ "value": "0x0"
+ },
+ {
+ "type": "core::integer::u64",
+ "name": "expiry",
+ "value": "1711649205508"
+ },
+ {
+ "type": "core::array::Array::",
+ "name": "signature",
+ "value": "[\"0x2a9dbfab9eaa43bf8e6899de306b6e53561a4b4aaaa04dcf1285672fbad0540\",\"0x35257a31f674998de10cde06eeee28236aca62cc8830ed0b65ac7180f6f8529\"]"
+ }
+ ]
+ },
+ "type": "dappInteraction"
+ }
+ }
+ ],
+ "type": "multicall"
+ },
+ "network": "starknet"
+ }
+]
diff --git a/packages/extension/src/shared/activity/__fixtures__/activities-handle-deposit.json b/packages/extension/src/shared/activity/__fixtures__/activities-handle-deposit.json
new file mode 100644
index 000000000..ee76aa699
--- /dev/null
+++ b/packages/extension/src/shared/activity/__fixtures__/activities-handle-deposit.json
@@ -0,0 +1,78 @@
+[
+ {
+ "compositeId": "af005bb92402c942ae2f0479d5b87f5c5cd8aebcccc01ecfd3013bc905a7e25d",
+ "id": "e0ce64d5-68cb-4a77-9f22-02e1945eb6b6",
+ "status": "success",
+ "wallet": "0x05a69dcffb37d127801e5422377b3eb31861568a26ad35870e23e7069fb8a6cf",
+ "txSender": "0x04c5772d1914fe6ce891b64eb35bf3522aeae1315647314aac58b01137607f3f",
+ "source": "transaction-monitor",
+ "type": "payment",
+ "group": "finance",
+ "submitted": 1706282918000,
+ "lastModified": 1706283105994,
+ "transaction": {
+ "network": "starknet",
+ "hash": "0x037eb609980b5cd53a19bc618dc99652fa8fcb3dc18e83bc756969c2b663741f",
+ "status": "success",
+ "blockNumber": 25822,
+ "transactionIndex": 2
+ },
+ "transferSummary": [
+ {
+ "asset": {
+ "type": "token",
+ "tokenAddress": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
+ "amount": "30000000000000000",
+ "fiatAmount": { "currency": "USD", "currencyAmount": 67.67 }
+ },
+ "sent": false
+ }
+ ],
+ "transfers": [
+ {
+ "type": "payment",
+ "leg": "credit",
+ "counterparty": "0x8453fc6cd1bcfe8d4dfc069c400b433054d47bdc",
+ "asset": {
+ "type": "token",
+ "tokenAddress": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
+ "amount": "30000000000000000",
+ "fiatAmount": { "currency": "USD", "currencyAmount": 67.67 }
+ },
+ "counterpartyNetwork": "ethereum"
+ }
+ ],
+ "fees": [],
+ "relatedAddresses": [
+ {
+ "address": "0x8453fc6cd1bcfe8d4dfc069c400b433054d47bdc",
+ "network": "ethereum",
+ "type": "wallet"
+ },
+ {
+ "address": "0x05a69dcffb37d127801e5422377b3eb31861568a26ad35870e23e7069fb8a6cf",
+ "network": "starknet",
+ "type": "wallet"
+ },
+ {
+ "address": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
+ "network": "starknet",
+ "type": "token"
+ }
+ ],
+ "networkDetails": { "ethereumNetwork": "sepolia", "chainId": "SEPOLIA" },
+ "details": {
+ "counterparty": "0x8453fc6cd1bcfe8d4dfc069c400b433054d47bdc",
+ "leg": "credit",
+ "asset": {
+ "type": "token",
+ "tokenAddress": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
+ "amount": "30000000000000000",
+ "fiatAmount": { "currency": "USD", "currencyAmount": 67.67 }
+ },
+ "type": "payment",
+ "counterpartyNetwork": "ethereum"
+ },
+ "network": "starknet"
+ }
+]
diff --git a/packages/extension/src/background/__new/services/activity/__fixtures__/activities-many-escapes.json b/packages/extension/src/shared/activity/__fixtures__/activities-many-escapes.json
similarity index 100%
rename from packages/extension/src/background/__new/services/activity/__fixtures__/activities-many-escapes.json
rename to packages/extension/src/shared/activity/__fixtures__/activities-many-escapes.json
diff --git a/packages/extension/src/background/__new/services/activity/__fixtures__/activities-signer-changed.json b/packages/extension/src/shared/activity/__fixtures__/activities-signer-changed.json
similarity index 100%
rename from packages/extension/src/background/__new/services/activity/__fixtures__/activities-signer-changed.json
rename to packages/extension/src/shared/activity/__fixtures__/activities-signer-changed.json
diff --git a/packages/extension/src/background/__new/services/activity/__fixtures__/activities.json b/packages/extension/src/shared/activity/__fixtures__/activities.json
similarity index 100%
rename from packages/extension/src/background/__new/services/activity/__fixtures__/activities.json
rename to packages/extension/src/shared/activity/__fixtures__/activities.json
diff --git a/packages/extension/src/background/__new/services/activity/__fixtures__/state.json b/packages/extension/src/shared/activity/__fixtures__/state.json
similarity index 100%
rename from packages/extension/src/background/__new/services/activity/__fixtures__/state.json
rename to packages/extension/src/shared/activity/__fixtures__/state.json
diff --git a/packages/extension/src/background/__new/services/activity/schema.test.ts b/packages/extension/src/shared/activity/schema.test.ts
similarity index 99%
rename from packages/extension/src/background/__new/services/activity/schema.test.ts
rename to packages/extension/src/shared/activity/schema.test.ts
index 0a30c854b..3aca5562c 100644
--- a/packages/extension/src/background/__new/services/activity/schema.test.ts
+++ b/packages/extension/src/shared/activity/schema.test.ts
@@ -3,6 +3,7 @@ import { describe, expect, test } from "vitest"
import activities from "./__fixtures__/activities.json"
import activitiesManyEscapes from "./__fixtures__/activities-many-escapes.json"
import activitiesSignerChanged from "./__fixtures__/activities-signer-changed.json"
+
import { activitiesSchema } from "./schema"
describe("background/services/activity", () => {
diff --git a/packages/extension/src/background/__new/services/activity/schema.ts b/packages/extension/src/shared/activity/schema.ts
similarity index 83%
rename from packages/extension/src/background/__new/services/activity/schema.ts
rename to packages/extension/src/shared/activity/schema.ts
index f56877deb..86ab8cbc1 100644
--- a/packages/extension/src/background/__new/services/activity/schema.ts
+++ b/packages/extension/src/shared/activity/schema.ts
@@ -67,8 +67,11 @@ const detailsActionSchema = z.enum([
"multisigConfigurationUpdated",
])
-export const activityDetailsSchema = z.object({
- type: typeSchema,
+const functionSchema = z.object({
+ name: z.string(),
+ parameters: z.array(z.unknown()).optional(),
+})
+const baseActivityDetailsSchema = z.object({
action: detailsActionSchema.optional(),
context: z
.object({
@@ -76,11 +79,25 @@ export const activityDetailsSchema = z.object({
newGuardian: z.string().optional(),
newImplementation: z.string().optional(),
newVersion: z.string().optional(),
+
+ /// SUBJECT TO CHANGE
+ isProvisionAirdrop: z.boolean().optional(),
})
.optional(),
srcAsset: assetSchema.optional(),
destAsset: assetSchema.optional(),
+ type: z.string().optional(),
+ contractAddress: addressSchema.optional(),
+ function: functionSchema.optional(),
})
+export const activityDetailsSchema = z
+ .object({
+ type: typeSchema,
+ calls: z
+ .array(z.object({ details: baseActivityDetailsSchema.optional() }))
+ .optional(),
+ })
+ .merge(baseActivityDetailsSchema)
export const activitySchema = z.object({
compositeId: z.string(),
@@ -111,6 +128,7 @@ export const activityResponseSchema = z.object({
export type ActivityResponse = z.infer
export type Activity = z.infer
+
export type ActivityDetailsAction = z.infer
export function isActivityDetailsAction(
diff --git a/packages/extension/src/shared/activity/types.ts b/packages/extension/src/shared/activity/types.ts
index 188aab1f9..53a4b26a2 100644
--- a/packages/extension/src/shared/activity/types.ts
+++ b/packages/extension/src/shared/activity/types.ts
@@ -1,3 +1,15 @@
+import { Activity } from "./schema"
+import { BaseWalletAccount } from "../wallet.model"
+
export interface IActivityStorage {
modifiedAfter: Record
}
+export type ActivitiesPayload = {
+ account: BaseWalletAccount
+ activities: Activity[]
+}
+
+export type ProvisionActivityPayload = {
+ account: BaseWalletAccount
+ activity: Activity
+}
diff --git a/packages/extension/src/background/__new/services/activity/utils/getOverallLastModified.test.ts b/packages/extension/src/shared/activity/utils/getOverallLastModified.test.ts
similarity index 100%
rename from packages/extension/src/background/__new/services/activity/utils/getOverallLastModified.test.ts
rename to packages/extension/src/shared/activity/utils/getOverallLastModified.test.ts
diff --git a/packages/extension/src/background/__new/services/activity/utils/getOverallLastModified.ts b/packages/extension/src/shared/activity/utils/getOverallLastModified.ts
similarity index 100%
rename from packages/extension/src/background/__new/services/activity/utils/getOverallLastModified.ts
rename to packages/extension/src/shared/activity/utils/getOverallLastModified.ts
diff --git a/packages/extension/src/shared/activity/utils/hasDelegationActivity.test.ts b/packages/extension/src/shared/activity/utils/hasDelegationActivity.test.ts
new file mode 100644
index 000000000..d4d7c3147
--- /dev/null
+++ b/packages/extension/src/shared/activity/utils/hasDelegationActivity.test.ts
@@ -0,0 +1,46 @@
+import { Activity } from "../schema"
+import { hasDelegationActivity } from "./hasDelegationActivity"
+
+describe("hasDelegationActivity", () => {
+ const createBaseActivity = (calls: Activity["details"]["calls"]) =>
+ ({
+ details: { calls },
+ } as unknown as Activity)
+
+ it("returns false when activity has no details", () => {
+ const activity = {} as unknown as Activity
+ expect(hasDelegationActivity(activity)).toBe(false)
+ })
+
+ it("returns false when details are present but no calls", () => {
+ const activity = createBaseActivity([])
+ expect(hasDelegationActivity(activity)).toBe(false)
+ })
+
+ it("returns false when calls do not match the function name", () => {
+ const activity = createBaseActivity([
+ { details: { function: { name: "otherFunction" } } },
+ ])
+ expect(hasDelegationActivity(activity)).toBe(false)
+ })
+
+ it("returns true when a call matches the function name", () => {
+ const activity = createBaseActivity([
+ { details: { function: { name: "lock_and_delegate_by_sig" } } },
+ ])
+ expect(hasDelegationActivity(activity)).toBe(true)
+ })
+
+ it("returns false when calls have null or undefined details", () => {
+ const activity = createBaseActivity([{}, { details: undefined }])
+ expect(hasDelegationActivity(activity)).toBe(false)
+ })
+
+ it("returns true when mixed calls contain at least one matching function name", () => {
+ const activity = createBaseActivity([
+ { details: { function: { name: "otherFunction" } } },
+ { details: { function: { name: "lock_and_delegate_by_sig" } } },
+ ])
+ expect(hasDelegationActivity(activity)).toBe(true)
+ })
+})
diff --git a/packages/extension/src/shared/activity/utils/hasDelegationActivity.ts b/packages/extension/src/shared/activity/utils/hasDelegationActivity.ts
new file mode 100644
index 000000000..0939b9a7a
--- /dev/null
+++ b/packages/extension/src/shared/activity/utils/hasDelegationActivity.ts
@@ -0,0 +1,11 @@
+import { Activity } from "../schema"
+
+export const hasDelegationActivity = (activity: Activity) => {
+ const hasDelegation = activity.details?.calls?.some((call) => {
+ if (call?.details?.function?.name) {
+ return call?.details?.function.name === "lock_and_delegate_by_sig"
+ }
+ return false
+ })
+ return Boolean(hasDelegation)
+}
diff --git a/packages/extension/src/shared/activity/utils/isProvisionWithDeploymentActivity.test.ts b/packages/extension/src/shared/activity/utils/isProvisionWithDeploymentActivity.test.ts
new file mode 100644
index 000000000..1de3c310d
--- /dev/null
+++ b/packages/extension/src/shared/activity/utils/isProvisionWithDeploymentActivity.test.ts
@@ -0,0 +1,61 @@
+import { Activity } from "../schema"
+import { isProvisionWithDeploymentActivity } from "./isProvisionWithDeploymentActivity"
+
+describe("isProvisionWithDeploymentActivity", () => {
+ const createBaseActivity = () =>
+ ({
+ type: "multicall",
+ details: {
+ calls: [],
+ },
+ wallet: "0x1",
+ } as unknown as Activity)
+
+ it("returns false when there are no calls in details", () => {
+ const activity = createBaseActivity()
+ expect(isProvisionWithDeploymentActivity(activity)).toBe(false)
+ })
+
+ it("returns false when calls are present but none match", () => {
+ const activity = createBaseActivity()
+ activity?.details?.calls?.push({
+ details: {
+ type: "non-deploy-type",
+ contractAddress: "0x0",
+ },
+ })
+
+ expect(isProvisionWithDeploymentActivity(activity)).toBe(false)
+ })
+
+ it("returns false when matching call has a different wallet address", () => {
+ const activity = createBaseActivity()
+ activity?.details?.calls?.push({
+ details: {
+ type: "deploy",
+ contractAddress: "0x2",
+ },
+ })
+
+ expect(isProvisionWithDeploymentActivity(activity)).toBe(false)
+ })
+
+ it("returns true when matching call has the same wallet address", () => {
+ const activity = createBaseActivity()
+ activity?.details?.calls?.push({
+ details: {
+ type: "deploy",
+ contractAddress: "0x1",
+ },
+ })
+
+ expect(isProvisionWithDeploymentActivity(activity)).toBe(true)
+ })
+
+ it("returns false when activity type is not multicall", () => {
+ const activity = createBaseActivity()
+ activity.type = "approval"
+
+ expect(isProvisionWithDeploymentActivity(activity)).toBe(false)
+ })
+})
diff --git a/packages/extension/src/shared/activity/utils/isProvisionWithDeploymentActivity.ts b/packages/extension/src/shared/activity/utils/isProvisionWithDeploymentActivity.ts
new file mode 100644
index 000000000..a5d8a5d99
--- /dev/null
+++ b/packages/extension/src/shared/activity/utils/isProvisionWithDeploymentActivity.ts
@@ -0,0 +1,13 @@
+import { isEqualAddress } from "@argent/shared"
+import { Activity } from "../schema"
+
+export const isProvisionWithDeploymentActivity = (activity: Activity) => {
+ return (
+ activity.type === "multicall" &&
+ activity.details.calls?.some(
+ (call) =>
+ call?.details?.type === "deploy" &&
+ isEqualAddress(call?.details.contractAddress || "", activity.wallet),
+ )
+ )
+}
diff --git a/packages/extension/src/shared/activity/utils/parseAccountActivities.test.ts b/packages/extension/src/shared/activity/utils/parseAccountActivities.test.ts
new file mode 100644
index 000000000..7171aa0e3
--- /dev/null
+++ b/packages/extension/src/shared/activity/utils/parseAccountActivities.test.ts
@@ -0,0 +1,100 @@
+import type { Address } from "@argent/shared"
+import { describe, expect, test } from "vitest"
+
+import activities from "../__fixtures__/activities.json"
+import activitiesManyEscapes from "../__fixtures__/activities-many-escapes.json"
+import activitiesSignerChanged from "../__fixtures__/activities-signer-changed.json"
+import activitiesDeploy from "../__fixtures__/activities-deploy.json"
+import state from "../__fixtures__/state.json"
+import type { Activity } from "../schema"
+import { parseAccountActivities } from "./parseAccountActivities"
+
+describe("background/services/activity/utils", () => {
+ describe("parseAccountActivities", () => {
+ test("returns a map of actions to account addresses for activities", () => {
+ expect(
+ parseAccountActivities({
+ activities: activities as Activity[],
+ accountAddressesOnNetwork:
+ state.accountAddressesOnNetwork as Address[],
+ }),
+ ).toMatchInlineSnapshot(`
+ {
+ "guardianChanged": [
+ "0x05f1f0a38429dcab9ffd8a786c0d827e84c1cbd8f60243e6d25d066a13af4a25",
+ ],
+ }
+ `)
+ })
+ test("returns a map of actions to account addresses for activitiesManyEscapes", () => {
+ expect(
+ parseAccountActivities({
+ activities: activitiesManyEscapes as Activity[],
+ accountAddressesOnNetwork: [
+ "0x00c90c89d339d1611f971e9211bc6a8efafc82541a61703a702c17d291afe9bb",
+ ] as Address[],
+ }),
+ ).toMatchInlineSnapshot(`
+ {
+ "cancelEscape": [
+ "0x00c90c89d339d1611f971e9211bc6a8efafc82541a61703a702c17d291afe9bb",
+ ],
+ "guardianChanged": [
+ "0x00c90c89d339d1611f971e9211bc6a8efafc82541a61703a702c17d291afe9bb",
+ ],
+ "triggerEscapeGuardian": [
+ "0x00c90c89d339d1611f971e9211bc6a8efafc82541a61703a702c17d291afe9bb",
+ ],
+ }
+ `)
+ })
+ test("returns a map of actions to account addresses for activitiesSignerChanged", () => {
+ expect(
+ parseAccountActivities({
+ activities: activitiesSignerChanged as Activity[],
+ accountAddressesOnNetwork: [
+ "0x02470ea294aa4b28ee4a473aaa8a1edc6c810c11684d1f29f1f3edd336fd0f34",
+ ] as Address[],
+ }),
+ ).toMatchInlineSnapshot(`
+ {
+ "signerChanged": [
+ "0x02470ea294aa4b28ee4a473aaa8a1edc6c810c11684d1f29f1f3edd336fd0f34",
+ ],
+ }
+ `)
+ })
+ test("returns a map of actions to account addresses for activitiesDeploy", () => {
+ expect(
+ parseAccountActivities({
+ activities: activitiesDeploy as Activity[],
+ accountAddressesOnNetwork: [
+ "0x07df7c3ed69cbacf5c4e39f0e3270699f53160b29ee14a8691057fa68bf78c42",
+ ] as Address[],
+ }),
+ ).toMatchInlineSnapshot(`
+ {
+ "deploy": [
+ "0x07df7c3ed69cbacf5c4e39f0e3270699f53160b29ee14a8691057fa68bf78c42",
+ ],
+ }
+ `)
+ })
+ test("returns a map of actions to account addresses for activitiesDeploy using non 0 based address", () => {
+ expect(
+ parseAccountActivities({
+ activities: activitiesDeploy as Activity[],
+ accountAddressesOnNetwork: [
+ "0x7df7c3ed69cbacf5c4e39f0e3270699f53160b29ee14a8691057fa68bf78c42",
+ ] as Address[],
+ }),
+ ).toMatchInlineSnapshot(`
+ {
+ "deploy": [
+ "0x07df7c3ed69cbacf5c4e39f0e3270699f53160b29ee14a8691057fa68bf78c42",
+ ],
+ }
+ `)
+ })
+ })
+})
diff --git a/packages/extension/src/shared/activity/utils/parseAccountActivities.ts b/packages/extension/src/shared/activity/utils/parseAccountActivities.ts
new file mode 100644
index 000000000..2f1494d97
--- /dev/null
+++ b/packages/extension/src/shared/activity/utils/parseAccountActivities.ts
@@ -0,0 +1,56 @@
+import { type Address, ensureArray, includesAddress } from "@argent/shared"
+
+import type { Activity, ActivityDetailsAction } from "../schema"
+import { isProvisionWithDeploymentActivity } from "./isProvisionWithDeploymentActivity"
+
+interface ParseAccountActivitiesProps {
+ activities: Activity[]
+ accountAddressesOnNetwork: Address[]
+}
+
+/**
+ * Parses security-related activities from the provided list of activities, grouping them by action
+ * and returning a map of addresses associated with each action.
+ *
+ * @param activities: Array of activities to parse.
+ * @param accountAddressesOnNetwork: Array of account addresses that are known to be active on the
+ * network.
+ * @returns A map of actions to their associated addresses, where the addresses represent the
+ * accounts involved in the respective actions.
+ */
+
+export function parseAccountActivities({
+ activities,
+ accountAddressesOnNetwork,
+}: ParseAccountActivitiesProps) {
+ const accountAddressesByAction: Partial<
+ Record
+ > = {}
+
+ activities.forEach((activity) => {
+ const address = activity.wallet
+ const action =
+ activity.type === "deploy" ? activity.type : activity.details.action
+
+ if (
+ activity.group === "security" &&
+ includesAddress(address, accountAddressesOnNetwork) &&
+ action
+ ) {
+ accountAddressesByAction[action] = accountAddressesByAction[action] || []
+ if (!includesAddress(address, accountAddressesByAction[action])) {
+ accountAddressesByAction[action] = ensureArray(
+ accountAddressesByAction[action],
+ ).concat(address)
+ }
+ }
+ // This is to cover the case where starknet uses a multicall to deploy an account and provision it in the same transaction
+ if (isProvisionWithDeploymentActivity(activity)) {
+ accountAddressesByAction["deploy"] = ensureArray(
+ accountAddressesByAction["deploy"],
+ ).concat(address)
+ }
+ })
+
+ return accountAddressesByAction
+}
diff --git a/packages/extension/src/background/__new/services/activity/utils/parseFinanceActivities.test.ts b/packages/extension/src/shared/activity/utils/parseFinanceActivities.test.ts
similarity index 69%
rename from packages/extension/src/background/__new/services/activity/utils/parseFinanceActivities.test.ts
rename to packages/extension/src/shared/activity/utils/parseFinanceActivities.test.ts
index 772fa25eb..5951e8914 100644
--- a/packages/extension/src/background/__new/services/activity/utils/parseFinanceActivities.test.ts
+++ b/packages/extension/src/shared/activity/utils/parseFinanceActivities.test.ts
@@ -1,11 +1,13 @@
import type { Address } from "@argent/shared"
import { describe, expect, test } from "vitest"
-import activities from "../__fixtures__/activities.json"
-import state from "../__fixtures__/state.json"
import type { Activity } from "../schema"
import { parseFinanceActivities } from "./parseFinanceActivities"
+import activities from "../__fixtures__/activities.json"
+import depositActivities from "../__fixtures__/activities-handle-deposit.json"
+import state from "../__fixtures__/state.json"
+
describe("background/services/activity/utils", () => {
describe("parseSecurityActivities", () => {
describe("when valid", () => {
@@ -46,6 +48,30 @@ describe("background/services/activity/utils", () => {
}
`)
})
+ test("returns a map of actions to account addresses for deposit activity", () => {
+ expect(
+ parseFinanceActivities({
+ activities: depositActivities as Activity[],
+ accountAddressesOnNetwork:
+ state.accountAddressesOnNetwork as Address[],
+ tokenAddressesOnNetwork: state.tokenAddressesOnNetwork as Address[],
+ nftAddressesOnNetwork: state.nftAddressesOnNetwork as Address[],
+ }),
+ ).toMatchInlineSnapshot(`
+ {
+ "nftActivity": {
+ "accountAddresses": [],
+ "tokenAddresses": [],
+ },
+ "tokenActivity": {
+ "accountAddresses": [],
+ "tokenAddresses": [
+ "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
+ ],
+ },
+ }
+ `)
+ })
})
})
})
diff --git a/packages/extension/src/background/__new/services/activity/utils/parseFinanceActivities.ts b/packages/extension/src/shared/activity/utils/parseFinanceActivities.ts
similarity index 100%
rename from packages/extension/src/background/__new/services/activity/utils/parseFinanceActivities.ts
rename to packages/extension/src/shared/activity/utils/parseFinanceActivities.ts
diff --git a/packages/extension/src/shared/activity/utils/parseProvisionActivity.test.ts b/packages/extension/src/shared/activity/utils/parseProvisionActivity.test.ts
new file mode 100644
index 000000000..6492acb40
--- /dev/null
+++ b/packages/extension/src/shared/activity/utils/parseProvisionActivity.test.ts
@@ -0,0 +1,64 @@
+import { Activity } from "../schema"
+import { parseProvisionActivity } from "./parseProvisionActivity"
+
+describe("parseProvisionActivity", () => {
+ const createBaseActivity = (
+ type: Activity["type"],
+ status: Activity["status"],
+ hasProvision = false,
+ ) =>
+ ({
+ type,
+ status,
+ details: {
+ calls: hasProvision
+ ? [{ details: { context: { isProvisionAirdrop: true } } }]
+ : [],
+ context: hasProvision ? { isProvisionAirdrop: true } : undefined,
+ },
+ } as unknown as Activity)
+
+ it("returns undefined when no activities are provided", () => {
+ expect(parseProvisionActivity([])).toBeUndefined()
+ })
+
+ it("returns undefined when no activities are successful", () => {
+ const activities = [createBaseActivity("multicall", "pending")]
+ expect(parseProvisionActivity(activities)).toBeUndefined()
+ })
+
+ it("returns a successful multicall activity with provision", () => {
+ const provisionActivity = createBaseActivity("multicall", "success", true)
+ const activities = [provisionActivity]
+ expect(parseProvisionActivity(activities)).toEqual(provisionActivity)
+ })
+
+ it("returns a successful non-multicall activity with provision", () => {
+ const provisionActivity = createBaseActivity("approval", "success", true)
+ const activities = [provisionActivity]
+ expect(parseProvisionActivity(activities)).toEqual(provisionActivity)
+ })
+
+ it("returns undefined when successful activities do not have provision", () => {
+ const activities = [
+ createBaseActivity("multicall", "success"),
+ createBaseActivity("approval", "success"),
+ ]
+ expect(parseProvisionActivity(activities)).toBeUndefined()
+ })
+
+ it("returns the last multicall activity with provision when multiple are present", () => {
+ const firstProvisionActivity = createBaseActivity(
+ "multicall",
+ "success",
+ true,
+ )
+ const secondProvisionActivity = createBaseActivity(
+ "multicall",
+ "success",
+ true,
+ )
+ const activities = [firstProvisionActivity, secondProvisionActivity]
+ expect(parseProvisionActivity(activities)).toEqual(secondProvisionActivity)
+ })
+})
diff --git a/packages/extension/src/shared/activity/utils/parseProvisionActivity.ts b/packages/extension/src/shared/activity/utils/parseProvisionActivity.ts
new file mode 100644
index 000000000..62888897f
--- /dev/null
+++ b/packages/extension/src/shared/activity/utils/parseProvisionActivity.ts
@@ -0,0 +1,32 @@
+import { Activity } from "../schema"
+
+export const parseProvisionActivity = (
+ activities: Activity[],
+): Activity | undefined => {
+ let activityWithProvision: Activity | undefined
+
+ const successfulActivities = activities.filter(
+ (activity) => activity.status === "success",
+ )
+ const multicallActivities = successfulActivities.filter(
+ (activity) => activity.type === "multicall",
+ )
+
+ const nonMulticallActivities = successfulActivities.filter(
+ (activity) => activity.type !== "multicall",
+ )
+ multicallActivities.forEach((multicallActivity) => {
+ const callWithProvision = multicallActivity.details.calls?.find(
+ (call) => call.details?.context?.isProvisionAirdrop,
+ )
+ if (callWithProvision) {
+ activityWithProvision = multicallActivity
+ }
+ })
+ nonMulticallActivities.forEach((nonMulticallActivity) => {
+ if (nonMulticallActivity.details?.context?.isProvisionAirdrop) {
+ activityWithProvision = nonMulticallActivity
+ }
+ })
+ return activityWithProvision
+}
diff --git a/packages/extension/src/shared/analytics.ts b/packages/extension/src/shared/analytics.ts
index 4462e1101..08b3ca00c 100644
--- a/packages/extension/src/shared/analytics.ts
+++ b/packages/extension/src/shared/analytics.ts
@@ -3,6 +3,8 @@ import { encode } from "starknet"
import { CreateAccountType } from "./wallet.model"
import { KeyValueStorage } from "./storage"
+import { isEmpty } from "lodash-es"
+import { settingsStore } from "./settings"
const SEGMENT_TRACK_URL = "https://api.segment.io/v1/track"
@@ -270,7 +272,13 @@ export function getAnalytics(
}
return {
track: async (event, ...[data]) => {
- if (!SEGMENT_WRITE_KEY) {
+ const privacyShareAnalyticsData = await settingsStore.get(
+ "privacyShareAnalyticsData",
+ )
+ if (!privacyShareAnalyticsData) {
+ return
+ }
+ if (isEmpty(SEGMENT_WRITE_KEY)) {
console.groupCollapsed(`Analytics: ${event}`)
console.log("You see this log because no SEGMENT_WRITE_KEY is set")
console.log(data)
diff --git a/packages/extension/src/shared/api/constants.ts b/packages/extension/src/shared/api/constants.ts
index 09ff6a82b..3ffed45ee 100644
--- a/packages/extension/src/shared/api/constants.ts
+++ b/packages/extension/src/shared/api/constants.ts
@@ -38,6 +38,8 @@ export const ARGENT_X_STATUS_URL = process.env.ARGENT_X_STATUS_URL
export const ARGENT_X_STATUS_ENABLED = isValidString(ARGENT_X_STATUS_URL)
+export const ARGENT_X_NEWS_URL = process.env.ARGENT_X_NEWS_URL
+
export const ARGENT_EXPLORER_BASE_URL = ARGENT_API_ENABLED
? urlJoin(ARGENT_API_BASE_URL, "explorer/starknet")
: undefined
@@ -83,3 +85,38 @@ export const ARGENT_MULTISIG_DISCOVERY_URL = ARGENT_MULTISIG_URL
export const ARGENT_SWAP_BASE_URL = ARGENT_API_ENABLED
? urlJoin(ARGENT_API_BASE_URL, "tokens/swap")
: undefined
+
+export const ARGENT_X_LEGAL_PRIVACY_POLICY_URL =
+ "https://www.argent.xyz/legal/privacy/argent-x/"
+
+export const ARGENT_X_LEGAL_TERMS_OF_SERVICE_URL =
+ "https://www.argent.xyz/legal/argent-extension-terms-of-service/"
+
+const ARGENT_HEALTHCHECK_BASE_URL = process.env
+ .ARGENT_HEALTHCHECK_BASE_URL as any // we validate it with isValidString
+
+const ARGENT_HEALTHCHECK_ENABLED = isValidString(ARGENT_HEALTHCHECK_BASE_URL)
+
+export const PROVISION_STATUS_ENDPOINT = ARGENT_HEALTHCHECK_ENABLED
+ ? urlJoin(ARGENT_HEALTHCHECK_BASE_URL, "provision-status.json")
+ : undefined
+
+// GOERLI only, mainnet ones are still unknown
+export const PROVISION_CONTRACT_ADDRESSES =
+ process.env.ARGENT_X_ENVIRONMENT === "prod"
+ ? [
+ "0x03ce54e2104cd65bb2117c8d401b0ce30139fafffb2ebf8811f70b362d8fac6e",
+ "0x0128492AB86D97475CDC074A06A827014E6AA10DA9BD745B26CCAFB8C1A54A9A",
+ "0x06793D9E6ED7182978454C79270E5B14D2655204BA6565CE9B0AA8A3C3121025",
+ "0x0517daba3622259ae4fffab72bb716d89c30df0994c6ab25ede61bd139639724",
+ ]
+ : [
+ "0x02b2e8b8eb3429540c58c0dc69ebb2981267196fe0ca2e361056b852445ee766",
+ "0x0512e19eb3daa35c94592a251f939c8bb7e81795b6eca6148964b5778bf7dd6d",
+ "0x0761357121b07055dae758496c210da9ab7b422a831a6b90efa3704d85d128d0",
+ "0x0524983b9b9322fa94d94758d9d8cdd94c936479c77775babcc921bf1e1ad2b6",
+ ]
+
+export const ARGENT_NETWORK_STATUS = ARGENT_API_ENABLED
+ ? urlJoin(ARGENT_API_BASE_URL, "status/starknet")
+ : undefined
diff --git a/packages/extension/src/shared/api/headers.ts b/packages/extension/src/shared/api/headers.ts
index 10ebd13b9..51fb24833 100644
--- a/packages/extension/src/shared/api/headers.ts
+++ b/packages/extension/src/shared/api/headers.ts
@@ -1,3 +1,5 @@
+import type { ArgentBackendNetworkId, ArgentNetworkId } from "@argent/shared"
+
const makeArgentXHeaders = () => ({
"argent-version": process.env.VERSION || "Unknown version",
"argent-client": "argent-x",
@@ -7,12 +9,18 @@ export const argentXHeaders = makeArgentXHeaders()
/** convert KnownNetworksType to 'goerli' or 'mainnet' expected by API */
-export const argentApiNetworkForNetwork = (network: string) => {
- return network === "goerli-alpha"
- ? "goerli"
- : network === "mainnet-alpha"
- ? "mainnet"
- : null
+export const argentApiNetworkForNetwork = (
+ network: ArgentNetworkId | string,
+): ArgentBackendNetworkId | null => {
+ switch (network) {
+ case "goerli-alpha":
+ return "goerli"
+ case "sepolia-alpha":
+ return "sepolia"
+ case "mainnet-alpha":
+ return "mainnet"
+ }
+ return null
}
export const argentApiHeadersForNetwork = (network: string) => {
diff --git a/packages/extension/src/shared/chain/service/implementation.test.ts b/packages/extension/src/shared/chain/service/implementation.test.ts
index 395b62287..05327fc4d 100644
--- a/packages/extension/src/shared/chain/service/implementation.test.ts
+++ b/packages/extension/src/shared/chain/service/implementation.test.ts
@@ -52,7 +52,7 @@ describe("StarknetChainService", () => {
getTransactionStatus: () => ({
finality_status: status,
}),
- getTransactionReceipt,
+ getTransactionReceipt: () => ({ finality_status: status }),
})
const txWithStatus = await starknetChainService.getTransactionStatus(
diff --git a/packages/extension/src/shared/chain/service/implementation.ts b/packages/extension/src/shared/chain/service/implementation.ts
index d4d289357..27a700be4 100644
--- a/packages/extension/src/shared/chain/service/implementation.ts
+++ b/packages/extension/src/shared/chain/service/implementation.ts
@@ -1,5 +1,3 @@
-import { RpcProvider, TransactionStatus as StarknetTxStatus } from "starknet"
-
import { getProvider } from "../../network"
import { INetworkService } from "../../network/service/interface"
import {
@@ -9,24 +7,7 @@ import {
} from "../../transactions/interface"
import { BaseContract, IChainService } from "./interface"
import { isContractDeployed } from "@argent/shared"
-
-function starknetStatusToTransactionStatus(
- status: T,
- error: T extends "REJECTED" | "REVERTED" ? () => Error : never,
-): TransactionStatus {
- switch (status) {
- case StarknetTxStatus.RECEIVED:
- return { status: "pending" }
- case StarknetTxStatus.ACCEPTED_ON_L2:
- case StarknetTxStatus.ACCEPTED_ON_L1:
- return { status: "confirmed" }
- case StarknetTxStatus.REJECTED:
- case StarknetTxStatus.REVERTED:
- return { status: "failed", reason: error() }
- default:
- throw new Error(`Unknown status: ${status}`)
- }
-}
+import { SUCCESS_STATUSES } from "../../transactions"
export class StarknetChainService implements IChainService {
constructor(private networkService: Pick) {}
@@ -50,15 +31,23 @@ export class StarknetChainService implements IChainService {
// TODO: Use constants
const isFailed =
execution_status === "REVERTED" || finality_status === "REJECTED"
- const isSuccessful =
- finality_status === "ACCEPTED_ON_L2" ||
- finality_status === "ACCEPTED_ON_L1"
+ let isSuccessful = false
+ // getTransactionStatus goes straight to the sequencer, hence it's much faster than the RPC nodes
+ // because of that we need to wait for the RPC nodes to have a receipt as well
try {
- if (execution_status === "REVERTED") {
+ if (
+ execution_status === "REVERTED" ||
+ SUCCESS_STATUSES.includes(finality_status)
+ ) {
// Only get the receipt if the transaction reverted
const receipt = await provider.getTransactionReceipt(transaction.hash)
- error_reason = receipt.revert_reason
+
+ if ("revert_reason" in receipt) {
+ error_reason = receipt.revert_reason
+ }
+
+ isSuccessful = SUCCESS_STATUSES.includes(receipt.finality_status)
}
} catch (e) {
console.warn(
diff --git a/packages/extension/src/shared/discover/interface.ts b/packages/extension/src/shared/discover/interface.ts
new file mode 100644
index 000000000..793991ed2
--- /dev/null
+++ b/packages/extension/src/shared/discover/interface.ts
@@ -0,0 +1,10 @@
+import { NewsApiReponse } from "./schema"
+
+export interface IDiscoverService {
+ setViewedAt(viewedAt: number): Promise
+}
+
+export type IDiscoverStorage = {
+ data: NewsApiReponse | null
+ viewedAt: number
+}
diff --git a/packages/extension/src/shared/discover/schema.ts b/packages/extension/src/shared/discover/schema.ts
new file mode 100644
index 000000000..9316eb23a
--- /dev/null
+++ b/packages/extension/src/shared/discover/schema.ts
@@ -0,0 +1,23 @@
+import { z } from "zod"
+
+export const newsItemSchema = z.object({
+ title: z.string().optional(),
+ description: z.string().optional(),
+ created: z.string().optional(),
+ modified: z.string().optional(),
+ startTime: z.string().optional(),
+ endTime: z.string().optional(),
+ badgeText: z.string().optional(),
+ backgroundImageUrl: z.string().optional(),
+ linkUrl: z.string().optional(),
+ dappId: z.string().optional(),
+})
+
+export const newsApiReponseSchema = z.object({
+ lastModified: z.string(),
+ news: z.array(newsItemSchema),
+})
+
+export type NewsItem = z.infer
+
+export type NewsApiReponse = z.infer
diff --git a/packages/extension/src/shared/discover/storage.ts b/packages/extension/src/shared/discover/storage.ts
new file mode 100644
index 000000000..c29633e01
--- /dev/null
+++ b/packages/extension/src/shared/discover/storage.ts
@@ -0,0 +1,15 @@
+import { KeyValueStorage } from "../storage"
+import { adaptKeyValue } from "../storage/__new/keyvalue"
+import type { IDiscoverStorage } from "./interface"
+
+const keyValueStorage = new KeyValueStorage(
+ {
+ viewedAt: 0,
+ data: null,
+ },
+ {
+ namespace: "service:discover",
+ },
+)
+
+export const discoverStore = adaptKeyValue(keyValueStorage)
diff --git a/packages/extension/src/shared/errors/accountMessaging.ts b/packages/extension/src/shared/errors/accountMessaging.ts
index d05a15dd0..3958d8a33 100644
--- a/packages/extension/src/shared/errors/accountMessaging.ts
+++ b/packages/extension/src/shared/errors/accountMessaging.ts
@@ -9,6 +9,7 @@ export enum ACCOUNT_MESSAGING_ERROR_MESSAGES {
GET_NEXT_PUBLIC_KEY_FAILED = "Get next public key failed",
GET_PUBLIC_KEY_FAILED = "Get public key failed",
TRIGGER_ESCAPE_FAILED = "Trigger escape failed",
+ ACCOUNT_DEPLOYMENT_PAYLOAD_FAILED = "Account deployment payload failed",
}
export type AccountMessagingErrorMessage =
keyof typeof ACCOUNT_MESSAGING_ERROR_MESSAGES
diff --git a/packages/extension/src/shared/errors/network.ts b/packages/extension/src/shared/errors/network.ts
index a480d923e..a3fe391c5 100644
--- a/packages/extension/src/shared/errors/network.ts
+++ b/packages/extension/src/shared/errors/network.ts
@@ -4,6 +4,7 @@ export enum NETWORK_ERROR_MESSAGES {
NOT_FOUND = "Network not found",
NETWORK_STATUS_RESPONSE_PARSING_FAILED = "Failed to parse checkly response",
NETWORK_STATUS_REQUEST_FAILED = "Failed to request network status",
+ ARGENT_NETWORK_STATUS_NOT_DEFINED = "ARGENT_NETWORK_STATUS is not defined",
}
export type NetworkValidationErrorMessage = keyof typeof NETWORK_ERROR_MESSAGES
diff --git a/packages/extension/src/shared/errors/review.ts b/packages/extension/src/shared/errors/review.ts
index a038bbf5f..f9023a4c7 100644
--- a/packages/extension/src/shared/errors/review.ts
+++ b/packages/extension/src/shared/errors/review.ts
@@ -4,6 +4,7 @@ export enum REVIEW_ERROR_MESSAGE {
SIMULATE_AND_REVIEW_FAILED = "Something went wrong fetching review",
NO_CALLS_FOUND = "No calls found",
ONCHAIN_FEE_ESTIMATION_FAILED = "Failed to estimate fees onchain",
+ BACKEND_SIMULATION_ERROR = "There was an error in the backend simulation response",
}
export type ReviewErrorMessage = keyof typeof REVIEW_ERROR_MESSAGE
diff --git a/packages/extension/src/shared/errors/riskAssessment.ts b/packages/extension/src/shared/errors/riskAssessment.ts
new file mode 100644
index 000000000..e81f050f0
--- /dev/null
+++ b/packages/extension/src/shared/errors/riskAssessment.ts
@@ -0,0 +1,15 @@
+import { BaseError, BaseErrorPayload } from "./baseError"
+
+export enum RISK_ASSESSMENT_ERROR_MESSAGE {
+ ERROR_FETCHING = "Encountered an error while fetching risk assessment",
+}
+
+export type RiskAssessmentErrorMessage =
+ keyof typeof RISK_ASSESSMENT_ERROR_MESSAGE
+
+export class RiskAssessmentError extends BaseError {
+ constructor(payload: BaseErrorPayload) {
+ super(payload, RISK_ASSESSMENT_ERROR_MESSAGE)
+ this.name = "RiskAssessmentError"
+ }
+}
diff --git a/packages/extension/src/shared/errors/transaction.ts b/packages/extension/src/shared/errors/transaction.ts
index dd491e3c3..2a1df5d23 100644
--- a/packages/extension/src/shared/errors/transaction.ts
+++ b/packages/extension/src/shared/errors/transaction.ts
@@ -6,6 +6,7 @@ export enum TRANSACTION_ERROR_MESSAGE {
SIMULATION_DISABLED = "Transaction simulation is disabled",
SIMULATION_ERROR = "Transaction simulation failed",
DEPRECATED_ACCOUNT = "Deprecated account",
+ NO_PRE_COMPUTED_FEES = "There was an issue computing fees - please reject this transaction and try again",
}
export type TransactionErrorMessage = keyof typeof TRANSACTION_ERROR_MESSAGE
diff --git a/packages/extension/src/shared/errors/udc.ts b/packages/extension/src/shared/errors/udc.ts
index e344519c2..14577c6c2 100644
--- a/packages/extension/src/shared/errors/udc.ts
+++ b/packages/extension/src/shared/errors/udc.ts
@@ -3,7 +3,9 @@ import { BaseError, BaseErrorPayload } from "./baseError"
export enum UDC_ERROR_MESSAGES {
FETCH_CONTRACT_CONTRUCTOR_PARAMS = "Error while fetching contract constructor params",
CAIRO_1_NOT_SUPPORTED = "Cairo1 contracts are not supported",
+ CAIRO_0_DECLARE_NOT_SUPPORTED = "Declaring Cairo0 contracts is no longer supported",
DEPLOY_TX_NOT_ADDED = "Deploy Account Transaction could not get added to the sequencer",
+ CONTRACT_ALREADY_DECLARED = "Contract is already declared",
NO_STARKNET_DECLARE = "Account does not support Starknet declare",
NO_STARKNET_DECLARE_FEE = "Account does not support Starknet Declare Fee",
NO_DECLARE_CONTRACT = "Could not declare contract",
diff --git a/packages/extension/src/shared/feeToken/constants.ts b/packages/extension/src/shared/feeToken/constants.ts
new file mode 100644
index 000000000..a9de7ebbe
--- /dev/null
+++ b/packages/extension/src/shared/feeToken/constants.ts
@@ -0,0 +1,8 @@
+import { ETH_TOKEN_ADDRESS, STRK_TOKEN_ADDRESS } from "../network/constants"
+
+export const FEE_TOKEN_PREFERENCE_BY_ADDRESS = [
+ STRK_TOKEN_ADDRESS,
+ ETH_TOKEN_ADDRESS,
+]
+
+export const FEE_TOKEN_PREFERENCE_BY_SYMBOL = ["STRK", "ETH"]
diff --git a/packages/extension/src/shared/feeToken/repository/preference.ts b/packages/extension/src/shared/feeToken/repository/preference.ts
new file mode 100644
index 000000000..c9b979fd1
--- /dev/null
+++ b/packages/extension/src/shared/feeToken/repository/preference.ts
@@ -0,0 +1,19 @@
+import { STRK_TOKEN_ADDRESS } from "../../network/constants"
+import { KeyValueStorage } from "../../storage"
+import { adaptKeyValue } from "../../storage/__new/keyvalue"
+import { FeeTokenPreference } from "../types/preference.model"
+
+export const feeTokenPreferenceKeyValueStore =
+ new KeyValueStorage(
+ {
+ prefer: STRK_TOKEN_ADDRESS,
+ },
+ {
+ namespace: "core:feeTokensPreference",
+ areaName: "local",
+ },
+ )
+
+export const feeTokenPreferenceStore = adaptKeyValue(
+ feeTokenPreferenceKeyValueStore,
+)
diff --git a/packages/extension/src/shared/feeToken/service/implementation.test.ts b/packages/extension/src/shared/feeToken/service/implementation.test.ts
new file mode 100644
index 000000000..81e9e3e7c
--- /dev/null
+++ b/packages/extension/src/shared/feeToken/service/implementation.test.ts
@@ -0,0 +1,258 @@
+import { Mocked } from "vitest"
+import { NetworkService } from "../../network/service/implementation"
+import { INetworkService } from "../../network/service/interface"
+import { INetworkRepo } from "../../network/store"
+import {
+ MockFnObjectStore,
+ MockFnRepository,
+} from "../../storage/__new/__test__/mockFunctionImplementation"
+import { ITokenBalanceRepository } from "../../token/__new/repository/tokenBalance"
+import { FeeTokenService } from "./implementation"
+import { TokenService } from "../../token/__new/service/implementation"
+import { ITokenRepository } from "../../token/__new/repository/token"
+import { StarknetChainService } from "../../chain/service/implementation"
+import { IAccountService } from "../../account/service/interface"
+import { AccountService } from "../../account/service/implementation"
+import {
+ ETH_TOKEN_ADDRESS,
+ STRK_TOKEN_ADDRESS,
+ TXV3_ACCOUNT_CLASS_HASH,
+} from "../../network/constants"
+import { Address, IHttpService, addressSchema } from "@argent/shared"
+import { stark } from "starknet"
+import { defaultNetwork } from "../../network"
+import { getMockNetwork } from "../../../../test/network.mock"
+import {
+ getMockBaseToken,
+ getMockTokenWithBalance,
+} from "../../../../test/token.mock"
+import { ITransactionsRepository } from "../../transactions/store"
+import { FeeTokenPreference } from "../types/preference.model"
+import { ITokenPriceRepository } from "../../token/__new/repository/tokenPrice"
+
+const randomAddress1 = addressSchema.parse(stark.randomAddress())
+
+const BASE_INFO_ENDPOINT = "https://token.info.argent47.net/v1"
+const BASE_PRICES_ENDPOINT = "https://token.prices.argent47.net/v1"
+
+describe("FeeTokenService", () => {
+ let tokenService: TokenService
+ let feeTokenService: FeeTokenService
+ let mockNetworkService: Mocked
+ let mockTokenRepo: MockFnRepository
+ let mockTokenBalanceRepo: MockFnRepository
+ let mockTokenPriceRepo: MockFnRepository
+ let mockTransactionsRepo: MockFnRepository
+ let mockNetworkRepo: MockFnRepository
+ let mockAccountService: Mocked
+ let mockFeeTokenPreferenceStore: MockFnObjectStore
+
+ beforeEach(() => {
+ mockTokenRepo = new MockFnRepository()
+ mockTokenBalanceRepo = new MockFnRepository()
+ mockTokenPriceRepo = new MockFnRepository()
+ mockNetworkRepo = new MockFnRepository()
+ mockTransactionsRepo = new MockFnRepository()
+
+ mockNetworkService = vi.mocked(
+ new NetworkService(mockNetworkRepo),
+ )
+ const mockAccountRepo = new MockFnRepository()
+ const chainService = new StarknetChainService(mockNetworkService)
+ mockAccountService = vi.mocked(
+ new AccountService(chainService, mockAccountRepo),
+ )
+ mockFeeTokenPreferenceStore = new MockFnObjectStore()
+ mockFeeTokenPreferenceStore.get = vi.fn().mockResolvedValue({
+ prefer: STRK_TOKEN_ADDRESS,
+ })
+
+ const mockHttpService = {
+ get: vi.fn(),
+ } as unknown as Mocked
+
+ const mockTokenInfoStore = {
+ namespace: "core:tokenInfo",
+ get: vi.fn(),
+ set: vi.fn(),
+ subscribe: vi.fn(),
+ }
+
+ tokenService = new TokenService(
+ mockNetworkService,
+ mockTokenRepo,
+ mockTokenBalanceRepo,
+ mockTokenPriceRepo,
+ mockTokenInfoStore,
+ mockHttpService,
+ BASE_INFO_ENDPOINT,
+ BASE_PRICES_ENDPOINT,
+ )
+
+ feeTokenService = new FeeTokenService(
+ tokenService,
+ mockAccountService,
+ mockNetworkService,
+ mockFeeTokenPreferenceStore,
+ )
+ })
+
+ afterEach(() => {
+ vi.clearAllMocks()
+ })
+
+ test("getFeeTokens returns the correct fee tokens and respects order preference", async () => {
+ const mockAccount = {
+ classHash: TXV3_ACCOUNT_CLASS_HASH as Address,
+ address: randomAddress1,
+ networkId: defaultNetwork.id,
+ }
+ const mockNetwork = getMockNetwork()
+ const mockBaseTokens = [
+ getMockBaseToken({ networkId: mockNetwork.id }),
+ getMockBaseToken({ address: "0x456", networkId: mockNetwork.id }),
+ ]
+
+ const mockTokens = [
+ getMockTokenWithBalance({
+ ...mockBaseTokens[0],
+ symbol: "ETH",
+ balance: BigInt(10e17).toString(),
+ account: mockAccount,
+ address: ETH_TOKEN_ADDRESS,
+ }),
+ getMockTokenWithBalance({
+ ...mockBaseTokens[1],
+ balance: BigInt(10e16).toString(),
+ account: mockAccount,
+ }),
+
+ getMockTokenWithBalance({
+ ...mockBaseTokens[1],
+ balance: BigInt(20e18).toString(),
+ symbol: "STRK",
+ account: mockAccount,
+ address: STRK_TOKEN_ADDRESS,
+ }),
+ ]
+
+ mockNetworkService.getById = vi.fn().mockResolvedValueOnce(mockNetwork)
+ mockTokenBalanceRepo.get.mockResolvedValueOnce(mockTokens)
+ mockTokenRepo.get.mockResolvedValueOnce(mockTokens)
+ mockTransactionsRepo.get.mockResolvedValueOnce([])
+
+ const result = await feeTokenService.getFeeTokens(mockAccount)
+ expect(result).toEqual([mockTokens[2], mockTokens[0]])
+ })
+
+ test("should return the correct fee tokens given a mock account", async () => {
+ const mockAccount = {
+ classHash: TXV3_ACCOUNT_CLASS_HASH as Address,
+ address: randomAddress1,
+ networkId: defaultNetwork.id,
+ }
+ const mockNetwork = getMockNetwork()
+ const mockBaseTokens = [
+ getMockBaseToken({ networkId: mockNetwork.id }),
+ getMockBaseToken({ address: "0x456", networkId: mockNetwork.id }),
+ ]
+
+ const mockTokens = [
+ getMockTokenWithBalance({
+ ...mockBaseTokens[1],
+ balance: BigInt(20e18).toString(),
+ symbol: "STRK",
+ account: mockAccount,
+ address: STRK_TOKEN_ADDRESS,
+ }),
+ getMockTokenWithBalance({
+ ...mockBaseTokens[0],
+ balance: BigInt(10e17).toString(),
+ account: mockAccount,
+ symbol: "ETH",
+ address: ETH_TOKEN_ADDRESS,
+ }),
+ ]
+
+ mockNetworkService.getById = vi.fn().mockResolvedValueOnce(mockNetwork)
+ mockTokenBalanceRepo.get.mockResolvedValueOnce(mockTokens)
+ mockTokenRepo.get.mockResolvedValueOnce(mockTokens)
+ mockTransactionsRepo.get.mockResolvedValueOnce([])
+
+ const result = await feeTokenService.getFeeTokens(mockAccount)
+ expect(result).toEqual(mockTokens)
+ })
+
+ test("getBestFeeToken returns the correct fee token", async () => {
+ const mockAccount = {
+ classHash: "0x123" as Address,
+ address: randomAddress1,
+ networkId: defaultNetwork.id,
+ }
+ const mockNetwork = getMockNetwork()
+ const mockBaseTokens = [
+ getMockBaseToken({ networkId: mockNetwork.id }),
+ getMockBaseToken({ address: "0x456", networkId: mockNetwork.id }),
+ ]
+
+ const mockTokens = [
+ getMockTokenWithBalance({
+ ...mockBaseTokens[0],
+ balance: BigInt(10e17).toString(),
+ account: mockAccount,
+ address: ETH_TOKEN_ADDRESS,
+ }),
+ getMockTokenWithBalance({
+ ...mockBaseTokens[1],
+ balance: BigInt(10e16).toString(),
+ account: mockAccount,
+ }),
+ ]
+
+ mockNetworkService.getById = vi.fn().mockResolvedValueOnce(mockNetwork)
+ mockTokenBalanceRepo.get.mockResolvedValueOnce(mockTokens)
+ mockTokenRepo.get.mockResolvedValueOnce(mockTokens)
+ mockTransactionsRepo.get.mockResolvedValueOnce([])
+
+ const result = await feeTokenService.getBestFeeToken(mockAccount)
+ expect(result).toEqual(mockTokens[0])
+ })
+
+ test("getBestFeeToken returns the token with the highest balance", async () => {
+ const mockAccount = {
+ classHash: TXV3_ACCOUNT_CLASS_HASH as Address,
+ address: randomAddress1,
+ networkId: defaultNetwork.id,
+ }
+ const mockNetwork = getMockNetwork()
+ const mockBaseTokens = [
+ getMockBaseToken({ networkId: mockNetwork.id }),
+ getMockBaseToken({ address: "0x456", networkId: mockNetwork.id }),
+ ]
+
+ const mockTokens = [
+ getMockTokenWithBalance({
+ ...mockBaseTokens[0],
+ balance: BigInt(10e17).toString(),
+ account: mockAccount,
+ symbol: "ETH",
+ address: ETH_TOKEN_ADDRESS,
+ }),
+ getMockTokenWithBalance({
+ ...mockBaseTokens[1],
+ balance: BigInt(10e18).toString(),
+ symbol: "STRK",
+ account: mockAccount,
+ address: STRK_TOKEN_ADDRESS,
+ }),
+ ]
+
+ mockNetworkService.getById = vi.fn().mockResolvedValueOnce(mockNetwork)
+ mockTokenBalanceRepo.get.mockResolvedValueOnce(mockTokens)
+ mockTokenRepo.get.mockResolvedValueOnce(mockTokens)
+ mockTransactionsRepo.get.mockResolvedValueOnce([])
+
+ const result = await feeTokenService.getBestFeeToken(mockAccount)
+ expect(result).toEqual(mockTokens[1])
+ })
+})
diff --git a/packages/extension/src/shared/feeToken/service/implementation.ts b/packages/extension/src/shared/feeToken/service/implementation.ts
new file mode 100644
index 000000000..7d79661ad
--- /dev/null
+++ b/packages/extension/src/shared/feeToken/service/implementation.ts
@@ -0,0 +1,96 @@
+import { accountsEqual } from "./../../utils/accountsEqual"
+import { IAccountService } from "../../account/service/interface"
+import { ITokenService } from "../../token/__new/service/interface"
+import { TokenWithBalance } from "../../token/__new/types/tokenBalance.model"
+import { BaseWalletAccount, WalletAccount } from "../../wallet.model"
+import { IFeeTokenService } from "./interface"
+import { INetworkService } from "../../network/service/interface"
+import { Address, isEqualAddress } from "@argent/shared"
+import {
+ classHashSupportsTxV3,
+ feeTokenNeedsTxV3Support,
+} from "../../network/txv3"
+import { equalToken } from "../../token/__new/utils"
+import { FEE_TOKEN_PREFERENCE_BY_SYMBOL } from "../constants"
+import { FeeTokenPreference } from "../types/preference.model"
+import { pickBestFeeToken } from "../utils"
+import { IObjectStore } from "../../storage/__new/interface"
+
+export class FeeTokenService implements IFeeTokenService {
+ constructor(
+ private readonly tokenService: ITokenService,
+ private readonly accountService: IAccountService,
+ private readonly networkService: INetworkService,
+ private readonly feeTokenPreferenceStore: IObjectStore,
+ ) {}
+
+ async getFeeTokens(
+ account: BaseWalletAccount & Pick,
+ ): Promise {
+ const tokens = await this.tokenService.getTokens()
+ const classHash =
+ account.classHash ??
+ (await this.accountService
+ .get((x) => accountsEqual(x, account))
+ .then(([a]) => a.classHash))
+ const network = await this.networkService.getById(account.networkId)
+ const networkFeeTokens = tokens.filter((token) =>
+ network.possibleFeeTokenAddresses.some((ft) =>
+ isEqualAddress(ft, token.address),
+ ),
+ )
+
+ const accountFeeTokens = networkFeeTokens.filter((token) => {
+ if (feeTokenNeedsTxV3Support(token)) {
+ return classHashSupportsTxV3(classHash)
+ }
+ return true
+ })
+ const feeTokenBalances = await this.tokenService.getTokenBalancesForAccount(
+ account,
+ accountFeeTokens,
+ )
+ const feeTokensWithBalances: TokenWithBalance[] = accountFeeTokens.map(
+ (token) => {
+ const tokenBalance = feeTokenBalances.find((tb) =>
+ equalToken(tb, token),
+ ) ?? {
+ balance: "0",
+ account: { address: account.address, networkId: account.networkId },
+ }
+ return {
+ ...token,
+ ...tokenBalance,
+ }
+ },
+ )
+ // sort by fee token preference defined in FEE_TOKEN_PREFERENCE_BY_SYMBOL
+ return feeTokensWithBalances.sort((a, b) => {
+ const [aIndex, bIndex] = [a, b].map((token) =>
+ FEE_TOKEN_PREFERENCE_BY_SYMBOL.indexOf(token.symbol),
+ )
+ return aIndex === -1 ? 1 : bIndex === -1 ? -1 : aIndex - bIndex
+ })
+ }
+
+ async getBestFeeToken(
+ account: BaseWalletAccount & Pick,
+ ): Promise {
+ const possibleFeeTokenWithBalances = await this.getFeeTokens(account)
+ const { prefer: preferredFeeToken } = await this.getFeeTokenPreference()
+
+ return pickBestFeeToken(possibleFeeTokenWithBalances, {
+ prefer: [preferredFeeToken],
+ })
+ }
+
+ async getFeeTokenPreference(): Promise {
+ return await this.feeTokenPreferenceStore.get()
+ }
+
+ async preferFeeToken(feeTokenAddress: Address): Promise {
+ await this.feeTokenPreferenceStore.set({
+ prefer: feeTokenAddress,
+ })
+ }
+}
diff --git a/packages/extension/src/shared/feeToken/service/index.ts b/packages/extension/src/shared/feeToken/service/index.ts
new file mode 100644
index 000000000..c3e7a18d5
--- /dev/null
+++ b/packages/extension/src/shared/feeToken/service/index.ts
@@ -0,0 +1,12 @@
+import { accountService } from "../../account/service"
+import { networkService } from "../../network/service"
+import { tokenService } from "../../token/__new/service"
+import { feeTokenPreferenceStore } from "../repository/preference"
+import { FeeTokenService } from "./implementation"
+
+export const feeTokenService = new FeeTokenService(
+ tokenService,
+ accountService,
+ networkService,
+ feeTokenPreferenceStore,
+)
diff --git a/packages/extension/src/shared/feeToken/service/interface.ts b/packages/extension/src/shared/feeToken/service/interface.ts
new file mode 100644
index 000000000..b4424e238
--- /dev/null
+++ b/packages/extension/src/shared/feeToken/service/interface.ts
@@ -0,0 +1,14 @@
+import { TokenWithBalance } from "../../token/__new/types/tokenBalance.model"
+import { BaseWalletAccount, WalletAccount } from "../../wallet.model"
+import { FeeTokenPreference } from "../types/preference.model"
+
+export interface IFeeTokenService {
+ getFeeTokens(
+ account: BaseWalletAccount & Pick,
+ ): Promise
+ getBestFeeToken(
+ account: BaseWalletAccount & Pick,
+ ): Promise
+ getFeeTokenPreference(): Promise
+ preferFeeToken(tokenAddress: string): Promise
+}
diff --git a/packages/extension/src/shared/feeToken/types/preference.model.ts b/packages/extension/src/shared/feeToken/types/preference.model.ts
new file mode 100644
index 000000000..c3b45472a
--- /dev/null
+++ b/packages/extension/src/shared/feeToken/types/preference.model.ts
@@ -0,0 +1,17 @@
+import { z } from "zod"
+import { addressSchema } from "@argent/shared"
+
+export const FeeTokenPreferenceSchema = z.object({
+ prefer: addressSchema,
+})
+
+export type FeeTokenPreference = z.infer
+
+export const FeeTokenPreferenceOptionSchema = z.object({
+ avoid: z.array(addressSchema).optional(),
+ prefer: z.array(addressSchema).optional(),
+})
+
+export type FeeTokenPreferenceOption = z.infer<
+ typeof FeeTokenPreferenceOptionSchema
+>
diff --git a/packages/extension/src/ui/features/accountTokens/useFeeTokenBalance.test.ts b/packages/extension/src/shared/feeToken/utils.test.ts
similarity index 80%
rename from packages/extension/src/ui/features/accountTokens/useFeeTokenBalance.test.ts
rename to packages/extension/src/shared/feeToken/utils.test.ts
index aecaf914e..f7b9b7387 100644
--- a/packages/extension/src/ui/features/accountTokens/useFeeTokenBalance.test.ts
+++ b/packages/extension/src/shared/feeToken/utils.test.ts
@@ -1,7 +1,8 @@
import { describe, it, expect } from "vitest"
-import { pickBestFeeToken } from "./useFeeTokenBalance" // Adjust the import as necessary
-import { BaseTokenWithBalance } from "../../../shared/token/__new/types/tokenBalance.model"
import { Address } from "@argent/shared"
+import { BaseTokenWithBalance } from "../token/__new/types/tokenBalance.model"
+import { STRK_TOKEN_ADDRESS } from "../network/constants"
+import { pickBestFeeToken } from "./utils"
function getMockToken(options: {
address: Address
@@ -20,24 +21,24 @@ const mockTokens: BaseTokenWithBalance[] = [
getMockToken({ address: "0x3", balance: "0" }),
getMockToken({ address: "0x4", balance: "0" }),
getMockToken({ address: "0x5", balance: "0" }),
- getMockToken({ address: "0x6", balance: "12" }),
+ getMockToken({ address: STRK_TOKEN_ADDRESS, balance: "12" }),
getMockToken({ address: "0x7", balance: "10" }),
getMockToken({ address: "0x8", balance: "0" }),
]
describe("pickBestFeeToken", () => {
- it("should return the token with the highest balance", () => {
+ it("should return the token from the default list", () => {
const result = pickBestFeeToken(mockTokens)
expect(result).toEqual(mockTokens[5])
})
- it("should return the first token if all balances are 0", () => {
+ it("should return token from default list if all balances are 0", () => {
const zeroBalanceTokens = mockTokens.map((token) => ({
...token,
balance: "0",
}))
const result = pickBestFeeToken(zeroBalanceTokens)
- expect(result).toEqual(zeroBalanceTokens[0])
+ expect(result).toEqual(zeroBalanceTokens[5])
})
it("should prefer tokens in the 'prefer' list", () => {
@@ -46,7 +47,7 @@ describe("pickBestFeeToken", () => {
})
it("should avoid tokens in the 'avoid' list", () => {
- const result = pickBestFeeToken(mockTokens, { avoid: ["0x6"] })
+ const result = pickBestFeeToken(mockTokens, { avoid: [STRK_TOKEN_ADDRESS] })
expect(result).toEqual(mockTokens[1])
})
diff --git a/packages/extension/src/shared/feeToken/utils.ts b/packages/extension/src/shared/feeToken/utils.ts
new file mode 100644
index 000000000..30069657c
--- /dev/null
+++ b/packages/extension/src/shared/feeToken/utils.ts
@@ -0,0 +1,84 @@
+import { Address, isEqualAddress } from "@argent/shared"
+import { FeeTokenPreferenceOption } from "./types/preference.model"
+import { num } from "starknet"
+import { arrayOrderWith } from "../utils/arrayOrderWith"
+import { FEE_TOKEN_PREFERENCE_BY_ADDRESS } from "./constants"
+import { ETH_TOKEN_ADDRESS } from "../network/constants"
+
+export const pickBestFeeToken = <
+ BS extends { address: Address; balance: string | bigint },
+>(
+ balances: BS[],
+ { avoid = [], prefer = [] }: FeeTokenPreferenceOption = {},
+): BS => {
+ // sort by prefered tokens, neutral tokens, then avoid tokens
+ // sort each group by the provided array order and secondarily by balance
+ const sortedBalances = balances
+ .map(
+ (balance) =>
+ [
+ balance,
+ {
+ balance: num.toBigInt(balance.balance),
+ prefer: prefer.includes(balance.address),
+ avoid: avoid.includes(balance.address),
+ },
+ ] as const,
+ )
+
+ .sort(([aa, a], [bb, b]) => {
+ // if one of them has no balance, it should be last
+ if (!a.balance && b.balance) return 1
+ if (a.balance && !b.balance) return -1
+
+ // otherwise, sort by prefer, then avoid, then by prefer/avoid array order, then by fee token preference
+ if (a.prefer !== b.prefer) {
+ return b.prefer ? 1 : -1
+ }
+ if (a.avoid !== b.avoid) {
+ return a.avoid ? 1 : -1
+ }
+
+ if (a.prefer && b.prefer) {
+ const preferArrayPrio = arrayOrderWith(
+ prefer,
+ aa.address,
+ bb.address,
+ isEqualAddress,
+ )
+ if (preferArrayPrio !== 0) {
+ return preferArrayPrio
+ }
+ }
+ if (a.avoid && b.avoid) {
+ const avoidArrayPrio = arrayOrderWith(
+ prefer,
+ bb.address,
+ aa.address,
+ isEqualAddress,
+ )
+ if (avoidArrayPrio !== 0) {
+ return avoidArrayPrio
+ }
+ }
+
+ return arrayOrderWith(
+ FEE_TOKEN_PREFERENCE_BY_ADDRESS,
+ aa.address,
+ bb.address,
+ isEqualAddress,
+ )
+ })
+ .map(([balance]) => balance)
+
+ // filter tokens with 0 balance out
+ const filteredBalances = sortedBalances.filter(
+ (balance) => num.toBigInt(balance.balance) > 0n,
+ )
+
+ const result: BS = filteredBalances[0] ??
+ sortedBalances[0] ?? { address: ETH_TOKEN_ADDRESS, balance: "0" } // fallback to ETH to prevent errors
+
+ // return the first token with a balance or the first token if all balances are 0, or a default object if no tokens are found
+ return result
+}
diff --git a/packages/extension/src/shared/messages/TransactionMessage.ts b/packages/extension/src/shared/messages/TransactionMessage.ts
index 64a83b683..a1ac8d23e 100644
--- a/packages/extension/src/shared/messages/TransactionMessage.ts
+++ b/packages/extension/src/shared/messages/TransactionMessage.ts
@@ -1,19 +1,14 @@
-import type {
- Abi,
- AllowArray,
- Call,
- InvocationsDetails,
- UniversalDeployerContractPayload,
-} from "starknet"
+import type { Abi, AllowArray, Call, InvocationsDetails } from "starknet"
import { Transaction } from "../transactions"
import {
SimulateTransactionsRequest,
TransactionSimulationWithFees,
} from "../transactionSimulation/types"
-import { DeclareContract } from "../udc/schema"
+import { DeclareContract, DeployContract } from "../udc/schema"
import { TransactionError } from "../errors/transaction"
import { EstimatedFees } from "../transactionSimulation/fees/fees.model"
+import { Address } from "@argent/shared"
export interface ExecuteTransactionRequest {
transactions: Call | Call[]
@@ -44,7 +39,7 @@ export type TransactionMessage =
}
| {
type: "ESTIMATE_DEPLOY_CONTRACT_FEE"
- data: UniversalDeployerContractPayload
+ data: DeployContract
}
| { type: "ESTIMATE_DEPLOY_CONTRACT_FEE_REJ"; data: { error: string } }
| {
@@ -68,7 +63,7 @@ export type TransactionMessage =
}
| {
type: "SIMULATE_TRANSACTIONS"
- data: AllowArray
+ data: { call: AllowArray; feeTokenAddress: Address }
}
| {
type: "SIMULATE_TRANSACTIONS_RES"
diff --git a/packages/extension/src/shared/messages/UdcMessage.ts b/packages/extension/src/shared/messages/UdcMessage.ts
index 0cae45ce6..1e4d19d25 100644
--- a/packages/extension/src/shared/messages/UdcMessage.ts
+++ b/packages/extension/src/shared/messages/UdcMessage.ts
@@ -1,7 +1,10 @@
import { DeclareContract } from "../udc/schema"
export type UdcMessage =
- | { type: "REQUEST_DECLARE_CONTRACT"; data: DeclareContract }
+ | {
+ type: "REQUEST_DECLARE_CONTRACT"
+ data: Omit
+ }
| { type: "REQUEST_DECLARE_CONTRACT_RES"; data: { actionHash: string } }
| {
type: "REQUEST_DECLARE_CONTRACT_REJ"
diff --git a/packages/extension/src/shared/multisig/account.ts b/packages/extension/src/shared/multisig/account.ts
index fd41e32af..54d59758f 100644
--- a/packages/extension/src/shared/multisig/account.ts
+++ b/packages/extension/src/shared/multisig/account.ts
@@ -10,13 +10,27 @@ import {
ProviderInterface,
ProviderOptions,
TransactionType,
- hash,
num,
-} from "starknet"
+} from "starknet6"
import { MultisigPendingTransaction } from "./pendingTransactionsStore"
import { MultisigSigner } from "./signer"
import { IMultisigBackendService } from "./service/backend/interface"
import { isAccountV5 } from "@argent/shared"
+import {
+ txVersionSchema,
+ TransactionVersion,
+} from "../utils/transactionVersion"
+
+function denyTxV3(
+ version: TransactionVersion,
+): asserts version is Exclude<
+ TransactionVersion,
+ "0x3" | "0x100000000000000000000000000000003"
+> {
+ if (version === "0x3" || version === "0x100000000000000000000000000000003") {
+ throw Error("Only txv1 is supported")
+ }
+}
export class MultisigAccount extends Account {
public readonly multisigBackendService: IMultisigBackendService
@@ -71,29 +85,67 @@ export class MultisigAccount extends Account {
): Promise {
const transactions = Array.isArray(calls) ? calls : [calls]
const nonce = num.toHex(transactionsDetail.nonce ?? (await this.getNonce()))
- const version = num.toBigInt(hash.transactionVersion).toString()
+ const version = txVersionSchema.parse(transactionsDetail.version)
const chainId = await this.getChainId()
const maxFee =
transactionsDetail.maxFee ??
- (await this.getSuggestedMaxFee(
- {
- type: TransactionType.INVOKE,
- payload: calls,
- },
- {
- skipValidate: true,
- nonce,
- },
- ))
-
- const signerDetails: InvocationsSignerDetails = {
+ (
+ await this.getSuggestedFee(
+ {
+ type: TransactionType.INVOKE,
+ payload: calls,
+ },
+ {
+ version: "0x1", // TODO: this should be "0x3
+ skipValidate: true,
+ nonce,
+ },
+ )
+ ).suggestedMaxFee
+
+ // TODO: enable once TXV3 is supported
+ // const signerDetails: InvocationsSignerDetails =
+ // version !== "0x3" && version !== "0x100000000000000000000000000000003"
+ // ? ({
+ // // txv2 and below
+ // ...transactionsDetail,
+ // walletAddress: this.address,
+ // chainId,
+ // nonce,
+ // version,
+ // cairoVersion: this.cairoVersion,
+ // maxFee,
+ // } satisfies V2InvocationsSignerDetails)
+ // : ({
+ // // txv3
+ // ...transactionsDetail,
+ // walletAddress: this.address,
+ // chainId,
+ // nonce,
+ // version,
+ // cairoVersion: this.cairoVersion,
+ // accountDeploymentData: [],
+ // feeDataAvailabilityMode: RPC.EDataAvailabilityMode.L1,
+ // nonceDataAvailabilityMode: RPC.EDataAvailabilityMode.L1,
+ // paymasterData: [],
+ // resourceBounds: {
+ // l1_gas: { max_amount: "0x0", max_price_per_unit: "0x0" },
+ // l2_gas: { max_amount: "0x0", max_price_per_unit: "0x0" },
+ // },
+ // tip: 0,
+ // } satisfies V3InvocationsSignerDetails)
+
+ denyTxV3(version)
+
+ const signerDetails = {
+ ...transactionsDetail,
walletAddress: this.address,
- chainId,
+ chainId: chainId as any, // TODO: migrate to snjsv6 completely
nonce,
version,
- maxFee,
cairoVersion: this.cairoVersion,
+ maxFee,
}
const signature = await this.signer.signTransaction(
@@ -115,7 +167,15 @@ export class MultisigAccount extends Account {
) {
const chainId = await this.getChainId()
- const { calls, maxFee, nonce, version } = transactionToSign.transaction
+ const {
+ calls,
+ maxFee,
+ nonce,
+ version: transactionsDetailVersion,
+ } = transactionToSign.transaction
+ const version = txVersionSchema.parse(transactionsDetailVersion)
+
+ denyTxV3(version)
const signerDetails: InvocationsSignerDetails = {
walletAddress: this.address,
@@ -131,7 +191,7 @@ export class MultisigAccount extends Account {
return this.multisigBackendService.addRequestSignature({
address: this.address,
transactionToSign,
- chainId,
+ chainId: chainId as any, // TODO: migrate to snjsv6 completely
signature,
})
}
diff --git a/packages/extension/src/shared/multisig/signer.ts b/packages/extension/src/shared/multisig/signer.ts
index a4a0bfff5..7febc9879 100644
--- a/packages/extension/src/shared/multisig/signer.ts
+++ b/packages/extension/src/shared/multisig/signer.ts
@@ -1,12 +1,11 @@
import {
- Abi,
Call,
DeployAccountSignerDetails,
InvocationsSignerDetails,
Signature,
Signer,
stark,
-} from "starknet"
+} from "starknet6"
export class MultisigSigner extends Signer {
constructor(pk: Uint8Array | string) {
@@ -30,12 +29,10 @@ export class MultisigSigner extends Signer {
public async signTransaction(
transactions: Call[],
transactionsDetail: InvocationsSignerDetails,
- abis?: Abi[] | undefined,
): Promise {
const signatures = await super.signTransaction(
transactions,
transactionsDetail,
- abis,
)
const formattedSignatures = stark.signatureToDecimalArray(signatures)
diff --git a/packages/extension/src/shared/network/constants.ts b/packages/extension/src/shared/network/constants.ts
index 2765b9e9f..ca903b39c 100644
--- a/packages/extension/src/shared/network/constants.ts
+++ b/packages/extension/src/shared/network/constants.ts
@@ -6,15 +6,18 @@ export const STRK_TOKEN_ADDRESS =
"0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d"
// This should always be the latest. If you need to use the old or custom one, don't use this constant.
-export const STANDARD_ACCOUNT_CLASS_HASH =
+export const TXV1_ACCOUNT_CLASS_HASH =
"0x1a736d6ed154502257f02b1ccdf4d9d1089f80811cd6acad48e6b6a9d1f2003"
export const TXV3_ACCOUNT_CLASS_HASH =
- "0x028463df0e5e765507ae51f9e67d6ae36c7e5af793424eccc9bc22ad705fc09d"
+ "0x29927c8af6bccf3f6fda035981e765a7bdbf18a2dc0d630494f8758aa908e2b"
export const STANDARD_CAIRO_0_ACCOUNT_CLASS_HASH =
"0x033434ad846cdd5f23eb73ff09fe6fddd568284a0fb7d1be20ee482f044dabe2"
+export const STANDARD_DEVNET_ACCOUNT_CLASS_HASH =
+ "0x4d07e40e93398ed3c76981e72dd1fd22557a78ce36c0515f679e27f0bb5bc5f"
+
export const PLUGIN_ACCOUNT_CLASS_HASH =
"0x4ee23ad83fb55c1e3fac26e2cd951c60abf3ddc851caa9a7fbb9f5eddb2091"
@@ -34,12 +37,14 @@ export const MULTICALL_CONTRACT_ADDRESS =
// Public RPC nodes
export const BLAST_RPC_NODE: PublicRpcNode = {
mainnet: "https://starknet-mainnet.public.blastapi.io",
- testnet: "https://starknet-testnet.public.blastapi.io",
+ goerli: "https://starknet-testnet.public.blastapi.io",
+ sepolia: "https://starknet-sepolia.public.blastapi.io",
} as const
export const LAVA_RPC_NODE: PublicRpcNode = {
mainnet: "https://rpc.starknet.lava.build",
- testnet: "https://rpc.starknet-testnet.lava.build",
+ goerli: "https://rpc.starknet-testnet.lava.build",
+ sepolia: "https://rpc.starknet-sepolia.lava.build",
} as const
export const PUBLIC_RPC_NODES = [BLAST_RPC_NODE, LAVA_RPC_NODE] as const
diff --git a/packages/extension/src/shared/network/defaults.ts b/packages/extension/src/shared/network/defaults.ts
index 8c3b24961..09b99ff63 100644
--- a/packages/extension/src/shared/network/defaults.ts
+++ b/packages/extension/src/shared/network/defaults.ts
@@ -1,3 +1,4 @@
+import urlJoin from "url-join"
import {
ARGENT_5_MINUTE_ESCAPE_TESTING_ACCOUNT_CLASS_HASH,
BETTER_MULTICAL_ACCOUNT_CLASS_HASH,
@@ -6,14 +7,39 @@ import {
MULTICALL_CONTRACT_ADDRESS,
MULTISIG_ACCOUNT_CLASS_HASH,
PLUGIN_ACCOUNT_CLASS_HASH,
- STANDARD_ACCOUNT_CLASS_HASH,
+ TXV1_ACCOUNT_CLASS_HASH,
STANDARD_CAIRO_0_ACCOUNT_CLASS_HASH,
TXV3_ACCOUNT_CLASS_HASH,
+ STANDARD_DEVNET_ACCOUNT_CLASS_HASH,
} from "./constants"
import type { Network, NetworkWithStatus } from "./type"
import { getDefaultNetwork } from "./utils"
-const DEV_ONLY_NETWORKS: Network[] = [
+const argentXEnv = process.env.ARGENT_X_ENVIRONMENT || ""
+
+const ARGENT_X_ENV_PROD_ONLY_NETWORKS: Network[] = [
+ {
+ id: "sepolia-alpha",
+ name: "Sepolia",
+ chainId: "SN_SEPOLIA",
+ rpcUrl: urlJoin(
+ process.env.ARGENT_API_BASE_URL || "",
+ "starknet/sepolia/rpc/v0.6",
+ ),
+ explorerUrl: "https://sepolia.voyager.online",
+ l1ExplorerUrl: "https://sepolia.etherscan.io",
+ accountClassHash: {
+ standard: TXV3_ACCOUNT_CLASS_HASH,
+ txv1Standard: TXV1_ACCOUNT_CLASS_HASH,
+ /** NOTE: multisig currently not supported on Sepolia */
+ },
+ multicallAddress: MULTICALL_CONTRACT_ADDRESS,
+ possibleFeeTokenAddresses: [ETH_TOKEN_ADDRESS],
+ readonly: true,
+ },
+]
+
+const NODE_ENV_DEV_ONLY_NETWORKS: Network[] = [
{
id: "integration",
name: "Integration",
@@ -24,7 +50,7 @@ const DEV_ONLY_NETWORKS: Network[] = [
},
// multicallAddress: MULTICALL_CONTRACT_ADDRESS, // not defined on integration
possibleFeeTokenAddresses: [ETH_TOKEN_ADDRESS, STRK_TOKEN_ADDRESS],
- explorerUrl: "https://integration.voyager.online",
+ explorerUrl: "https://integration.starkscan.co",
readonly: true,
},
]
@@ -38,6 +64,10 @@ export const defaultNetworksStatuses: NetworkWithStatus[] = [
id: "goerli-alpha",
status: "unknown",
},
+ {
+ id: "sepolia-alpha",
+ status: "unknown",
+ },
{
id: "localhost",
status: "unknown",
@@ -49,47 +79,55 @@ export const defaultNetworks: Network[] = [
id: "mainnet-alpha",
name: "Mainnet",
chainId: "SN_MAIN",
- rpcUrl: "https://cloud.argent-api.com/v1/starknet/mainnet/rpc/v0.5",
+ rpcUrl: "https://cloud.argent-api.com/v1/starknet/mainnet/rpc/v0.6",
explorerUrl: "https://voyager.online",
l1ExplorerUrl: "https://etherscan.io",
accountClassHash: {
- standard: STANDARD_ACCOUNT_CLASS_HASH,
+ standard: TXV3_ACCOUNT_CLASS_HASH,
+ txv1Standard: TXV1_ACCOUNT_CLASS_HASH,
multisig: MULTISIG_ACCOUNT_CLASS_HASH,
+ standardCairo0: STANDARD_CAIRO_0_ACCOUNT_CLASS_HASH,
},
multicallAddress: MULTICALL_CONTRACT_ADDRESS,
- possibleFeeTokenAddresses: [ETH_TOKEN_ADDRESS],
+ possibleFeeTokenAddresses: [ETH_TOKEN_ADDRESS, STRK_TOKEN_ADDRESS],
readonly: true,
},
{
id: "goerli-alpha",
- name: "Testnet",
+ name: "Goerli",
chainId: "SN_GOERLI",
- rpcUrl: process.env.ARGENT_TESTNET_RPC_URL ?? "",
+ rpcUrl: urlJoin(
+ process.env.ARGENT_API_BASE_URL || "",
+ "starknet/goerli/rpc/v0.6",
+ ),
explorerUrl: "https://goerli.voyager.online",
faucetUrl: "https://faucet.goerli.starknet.io",
l1ExplorerUrl: "https://goerli.etherscan.io",
accountClassHash: {
- standard: STANDARD_ACCOUNT_CLASS_HASH,
+ standard: TXV3_ACCOUNT_CLASS_HASH,
+ txv1Standard: TXV1_ACCOUNT_CLASS_HASH,
plugin: PLUGIN_ACCOUNT_CLASS_HASH,
betterMulticall: BETTER_MULTICAL_ACCOUNT_CLASS_HASH,
argent5MinuteEscapeTestingAccount:
ARGENT_5_MINUTE_ESCAPE_TESTING_ACCOUNT_CLASS_HASH,
multisig: MULTISIG_ACCOUNT_CLASS_HASH,
+ standardCairo0: STANDARD_CAIRO_0_ACCOUNT_CLASS_HASH,
},
multicallAddress: MULTICALL_CONTRACT_ADDRESS,
- possibleFeeTokenAddresses: [ETH_TOKEN_ADDRESS],
+ possibleFeeTokenAddresses: [ETH_TOKEN_ADDRESS, STRK_TOKEN_ADDRESS],
readonly: true,
},
- ...(process.env.NODE_ENV === "development" ? DEV_ONLY_NETWORKS : []),
+ ...(argentXEnv === "prod" ? ARGENT_X_ENV_PROD_ONLY_NETWORKS : []),
+ ...(process.env.NODE_ENV === "development" ? NODE_ENV_DEV_ONLY_NETWORKS : []),
{
id: "localhost",
chainId: "SN_GOERLI",
- rpcUrl: "http://localhost:5050/rpc",
- explorerUrl: "https://devnet.starkscan.co",
- name: "Localhost 5050",
+ rpcUrl: "http://localhost:5050",
+ explorerUrl: "http://localhost:4000/testnet/",
+ name: "Devnet",
possibleFeeTokenAddresses: [ETH_TOKEN_ADDRESS],
accountClassHash: {
- standard: STANDARD_CAIRO_0_ACCOUNT_CLASS_HASH,
+ standard: STANDARD_DEVNET_ACCOUNT_CLASS_HASH,
},
},
]
diff --git a/packages/extension/src/shared/network/index.ts b/packages/extension/src/shared/network/index.ts
index 49864ff16..42c2320a7 100644
--- a/packages/extension/src/shared/network/index.ts
+++ b/packages/extension/src/shared/network/index.ts
@@ -3,6 +3,6 @@ export {
defaultNetworks,
defaultCustomNetworks,
} from "./defaults"
-export { getProvider } from "./provider"
+export { getProvider, getProvider6 } from "./provider"
export { networkSchema } from "./schema"
export type { Network, NetworkStatus } from "./type"
diff --git a/packages/extension/src/shared/network/makeSafeNetworks.test.ts b/packages/extension/src/shared/network/makeSafeNetworks.test.ts
new file mode 100644
index 000000000..187d53b04
--- /dev/null
+++ b/packages/extension/src/shared/network/makeSafeNetworks.test.ts
@@ -0,0 +1,88 @@
+import {} from "vitest"
+import { makeSafeNetworks } from "./makeSafeNetworks"
+
+import { defaultNetworks } from "./defaults"
+import { Network } from "."
+import { ETH_TOKEN_ADDRESS } from "./constants"
+
+const legacyNetwork = {
+ accountClassHash: {
+ standard:
+ "0x1a736d6ed154502257f02b1ccdf4d9d1089f80811cd6acad48e6b6a9d1f2003",
+ },
+ chainId: "SN_SEPOLIA",
+ feeTokenAddress:
+ "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
+ id: "sepolia",
+ multicallAddress:
+ "0x05754af3760f3356da99aea5c3ec39ccac7783d925a19666ebbeca58ff0087f4",
+ name: "Sepolia",
+ rpcUrl: "https://foo.bar",
+} as unknown as Network
+
+const legacyNetworkNoFeeToken = {
+ accountClassHash: {
+ standard:
+ "0x1a736d6ed154502257f02b1ccdf4d9d1089f80811cd6acad48e6b6a9d1f2003",
+ },
+ chainId: "SN_SEPOLIA",
+ id: "sepolia2",
+ multicallAddress:
+ "0x05754af3760f3356da99aea5c3ec39ccac7783d925a19666ebbeca58ff0087f4",
+ name: "Sepolia 2",
+ rpcUrl: "https://foo.bar",
+} as unknown as Network
+
+const invalidNetwork = {
+ chainId: "SN_SEPOLIA",
+ id: "sepolia3",
+ name: "Sepolia 3",
+} as unknown as Network
+
+describe("shared/network/makeSafeNetworks", () => {
+ describe("when valid", () => {
+ test("returns unmodified networks", () => {
+ expect(makeSafeNetworks(defaultNetworks)).toEqual(defaultNetworks)
+ })
+ })
+ describe("when invalid", () => {
+ test("returns modified, valid networks", () => {
+ expect(
+ makeSafeNetworks([
+ legacyNetwork,
+ legacyNetworkNoFeeToken,
+ invalidNetwork,
+ ]),
+ ).toEqual([
+ {
+ accountClassHash: {
+ standard:
+ "0x1a736d6ed154502257f02b1ccdf4d9d1089f80811cd6acad48e6b6a9d1f2003",
+ },
+ chainId: "SN_SEPOLIA",
+ id: "sepolia",
+ multicallAddress:
+ "0x05754af3760f3356da99aea5c3ec39ccac7783d925a19666ebbeca58ff0087f4",
+ name: "Sepolia",
+ possibleFeeTokenAddresses: [
+ "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
+ ],
+ rpcUrl: "https://foo.bar",
+ },
+ {
+ accountClassHash: {
+ standard:
+ "0x1a736d6ed154502257f02b1ccdf4d9d1089f80811cd6acad48e6b6a9d1f2003",
+ },
+ chainId: "SN_SEPOLIA",
+ id: "sepolia2",
+ multicallAddress:
+ "0x05754af3760f3356da99aea5c3ec39ccac7783d925a19666ebbeca58ff0087f4",
+ name: "Sepolia 2",
+ possibleFeeTokenAddresses: [ETH_TOKEN_ADDRESS],
+ rpcUrl: "https://foo.bar",
+ },
+ ])
+ })
+ })
+})
diff --git a/packages/extension/src/shared/network/makeSafeNetworks.ts b/packages/extension/src/shared/network/makeSafeNetworks.ts
new file mode 100644
index 000000000..e271f56cd
--- /dev/null
+++ b/packages/extension/src/shared/network/makeSafeNetworks.ts
@@ -0,0 +1,36 @@
+import { Address } from "@argent/shared"
+import type { Network } from "./type"
+import { ETH_TOKEN_ADDRESS } from "./constants"
+import { networkSchema } from "."
+import { isEmpty } from "lodash-es"
+
+export const makeSafeNetworks = (unsafeNetworks: Network[]): Network[] => {
+ return unsafeNetworks.flatMap((unsafeNetwork) => {
+ if (networkSchema.safeParse(unsafeNetwork).success) {
+ return [unsafeNetwork]
+ }
+ // try to fix the network if it is using old `feeTokenAddress` shape
+ if (isEmpty(unsafeNetwork.possibleFeeTokenAddresses)) {
+ // move feeTokenAddress -> possibleFeeTokenAddresses[feeTokenAddress]
+ if (
+ "feeTokenAddress" in unsafeNetwork &&
+ !isEmpty(unsafeNetwork.feeTokenAddress)
+ ) {
+ unsafeNetwork.possibleFeeTokenAddresses = [
+ unsafeNetwork.feeTokenAddress as Address,
+ ]
+ delete unsafeNetwork.feeTokenAddress
+ if (networkSchema.safeParse(unsafeNetwork).success) {
+ return [unsafeNetwork]
+ }
+ }
+ // try possibleFeeTokenAddresses[ETH_TOKEN_ADDRESS]
+ unsafeNetwork.possibleFeeTokenAddresses = [ETH_TOKEN_ADDRESS]
+ if (networkSchema.safeParse(unsafeNetwork).success) {
+ return [unsafeNetwork]
+ }
+ }
+ // omit network that failed schema
+ return []
+ })
+}
diff --git a/packages/extension/src/shared/network/provider.ts b/packages/extension/src/shared/network/provider.ts
index b0da1aa1c..47d8962b4 100644
--- a/packages/extension/src/shared/network/provider.ts
+++ b/packages/extension/src/shared/network/provider.ts
@@ -1,19 +1,30 @@
import { memoize } from "lodash-es"
-import { RpcProvider, constants, shortString } from "starknet"
+import { RpcProvider, constants } from "starknet"
+import { RpcProvider as RpcProvider6, shortString } from "starknet6"
import { RpcProvider as RpcProviderV4 } from "starknet4"
import { Network } from "./type"
import { argentXHeaders } from "../api/headers"
-export const getProviderForRpcUrlAndChainId = memoize(
- (rpcUrl: string, chainId: constants.StarknetChainId): RpcProvider => {
+export const getProviderForRpcUrl = memoize(
+ (rpcUrl: string, chainId?: constants.StarknetChainId): RpcProvider => {
return new RpcProvider({
nodeUrl: rpcUrl,
chainId,
headers: argentXHeaders,
})
},
- (a: string, b: string) => `${a}::${b}`,
+ (a: string, b: string = "") => `${a}::${b}`,
+)
+export const getProviderForRpcUrl6 = memoize(
+ (rpcUrl: string, chainId?: constants.StarknetChainId): RpcProvider6 => {
+ return new RpcProvider6({
+ nodeUrl: rpcUrl,
+ chainId,
+ headers: argentXHeaders,
+ })
+ },
+ (a: string, b: string = "") => `${a}::${b}`,
)
/**
@@ -22,11 +33,17 @@ export const getProviderForRpcUrlAndChainId = memoize(
* @returns
*/
export function getProvider(network: Network): RpcProvider {
- // Initialising RpcProvider with chainId removes the need for initial RPC calls to `starknet_chainId`
const chainId = shortString.encodeShortString(
network.chainId,
) as constants.StarknetChainId
- return getProviderForRpcUrlAndChainId(network.rpcUrl, chainId)
+ return getProviderForRpcUrl(network.rpcUrl, chainId)
+}
+
+export function getProvider6(network: Network): RpcProvider6 {
+ const chainId = shortString.encodeShortString(
+ network.chainId,
+ ) as constants.StarknetChainId
+ return getProviderForRpcUrl6(network.rpcUrl, chainId)
}
/** ======================================================================== */
diff --git a/packages/extension/src/shared/network/schema.ts b/packages/extension/src/shared/network/schema.ts
index 86f9a170d..2f8e86d9c 100644
--- a/packages/extension/src/shared/network/schema.ts
+++ b/packages/extension/src/shared/network/schema.ts
@@ -7,12 +7,8 @@ export const baseNetworkSchema = z.object({
id: z.string().min(2).max(31),
})
-export const networkStatusSchema = z.enum([
- "ok",
- "degraded",
- "error",
- "unknown",
-])
+export const networkStatusSchema = z.enum(["red", "amber", "green", "unknown"])
+
export const networkSchema = baseNetworkSchema.extend({
name: z.string().min(2).max(128),
chainId: z
@@ -38,6 +34,12 @@ export const networkSchema = baseNetworkSchema.extend({
message: `Account class hash must match the following: /^0x[a-f0-9]+$/i`,
})
.optional(),
+ txv1Standard: z
+ .string()
+ .regex(REGEX_HEXSTRING, {
+ message: `Account class hash must match the following: /^0x[a-f0-9]+$/i`,
+ })
+ .optional(),
standardCairo0: z
.string()
.regex(REGEX_HEXSTRING, {
diff --git a/packages/extension/src/shared/network/store.ts b/packages/extension/src/shared/network/store.ts
index 68d8d842e..449994244 100644
--- a/packages/extension/src/shared/network/store.ts
+++ b/packages/extension/src/shared/network/store.ts
@@ -4,6 +4,7 @@ import type { IRepository } from "../storage/__new/interface"
import { adaptArrayStorage } from "../storage/__new/repository"
import { defaultNetworks, defaultReadonlyNetworks } from "./defaults"
import type { BaseNetwork, Network } from "./type"
+import { makeSafeNetworks } from "./makeSafeNetworks"
export type INetworkRepo = IRepository
@@ -15,9 +16,10 @@ export const networksEqual = (a: BaseNetwork, b: BaseNetwork) => a.id === b.id
export const allNetworksStore = new ArrayStorage(defaultNetworks, {
namespace: "core:allNetworks",
compare: networksEqual,
- deserialize(value: Network[]): Network[] {
+ deserialize(unsafeNetworks: Network[]): Network[] {
+ const safeNetworks = makeSafeNetworks(unsafeNetworks)
// overwrite the stored values for the default networks with the default values
- return mergeArrayStableWith(value, defaultReadonlyNetworks, {
+ return mergeArrayStableWith(safeNetworks, defaultReadonlyNetworks, {
compareFn: networksEqual,
insertMode: "unshift",
})
diff --git a/packages/extension/src/shared/network/txv3.ts b/packages/extension/src/shared/network/txv3.ts
index ccfb94749..f274452a3 100644
--- a/packages/extension/src/shared/network/txv3.ts
+++ b/packages/extension/src/shared/network/txv3.ts
@@ -4,7 +4,7 @@ import { BaseToken } from "../token/__new/types/token.model"
const tokensRequireTxV3Support = [STRK_TOKEN_ADDRESS]
-export function feeTokenNeedsTxV3Support(token: BaseToken) {
+export function feeTokenNeedsTxV3Support(token: Pick) {
return tokensRequireTxV3Support.some((address) =>
isEqualAddress(address, token.address),
)
diff --git a/packages/extension/src/shared/network/type.ts b/packages/extension/src/shared/network/type.ts
index aed62724a..50ba049be 100644
--- a/packages/extension/src/shared/network/type.ts
+++ b/packages/extension/src/shared/network/type.ts
@@ -1,3 +1,4 @@
+import type { ArgentNetworkId, ArgentBackendNetworkId } from "@argent/shared"
import { z } from "zod"
import {
@@ -15,13 +16,6 @@ export type NetworkWithStatus = z.infer
export type NetworkStatus = z.infer
-export type DefaultNetworkId =
- | "mainnet-alpha"
- | "goerli-alpha"
- | "localhost"
- | "integration"
+export type DefaultNetworkId = ArgentNetworkId | "localhost" | "integration"
-export type PublicRpcNode = {
- mainnet: string
- testnet: string
-}
+export type PublicRpcNode = Record
diff --git a/packages/extension/src/shared/network/utils.ts b/packages/extension/src/shared/network/utils.ts
index 30f437e0a..b9adb9e03 100644
--- a/packages/extension/src/shared/network/utils.ts
+++ b/packages/extension/src/shared/network/utils.ts
@@ -1,9 +1,14 @@
import { constants } from "starknet"
-import { isEqualAddress } from "@argent/shared"
+import {
+ isEqualAddress,
+ type ArgentNetworkId,
+ isArgentNetworkId,
+} from "@argent/shared"
import type { ArgentAccountType } from "../wallet.model"
-import type { Network, PublicRpcNode } from "./type"
+import type { DefaultNetworkId, Network, PublicRpcNode } from "./type"
import { PUBLIC_RPC_NODES } from "./constants"
+import { argentApiNetworkForNetwork } from "../api/headers"
// LEGACY ⬇️
export function mapImplementationToArgentAccountType(
@@ -29,7 +34,7 @@ export function mapImplementationToArgentAccountType(
export function getNetworkIdFromChainId(
encodedChainId: string,
-): "mainnet-alpha" | "goerli-alpha" {
+): ArgentNetworkId {
switch (encodedChainId) {
case constants.StarknetChainId.SN_MAIN:
return "mainnet-alpha"
@@ -37,12 +42,15 @@ export function getNetworkIdFromChainId(
case constants.StarknetChainId.SN_GOERLI:
return "goerli-alpha"
+ case constants.StarknetChainId.SN_SEPOLIA:
+ return "sepolia-alpha"
+
default:
throw new Error(`Unknown chainId: ${encodedChainId}`)
}
}
-export function getDefaultNetworkId() {
+export function getDefaultNetworkId(): DefaultNetworkId {
const argentXEnv = process.env.ARGENT_X_ENVIRONMENT
if (!argentXEnv) {
@@ -82,7 +90,7 @@ export function getDefaultNetwork(defaultNetworks: Network[]): Network {
}
export function isArgentNetwork(network: Network) {
- return network.id === "mainnet-alpha" || network.id === "goerli-alpha"
+ return isArgentNetworkId(network.id)
}
export function getRandomPublicRPCNode(network: Network) {
@@ -101,13 +109,11 @@ export function getPublicRPCNodeUrls(network: Network) {
if (!isArgentNetwork) {
throw new Error(`Not an Argent network: ${network.id}`)
}
- const key: keyof PublicRpcNode =
- network.id === "mainnet-alpha" ? "mainnet" : "testnet"
- const nodeUrls = PUBLIC_RPC_NODES.map((node) => node[key])
-
- if (!nodeUrls) {
+ const key = argentApiNetworkForNetwork(network.id)
+ if (!key) {
throw new Error(`No nodes found for network: ${network.id}`)
}
+ const nodeUrls = PUBLIC_RPC_NODES.map((node) => node[key])
return nodeUrls
}
diff --git a/packages/extension/src/shared/nft/implementation.ts b/packages/extension/src/shared/nft/implementation.ts
index 91067bbf6..545ff44e0 100644
--- a/packages/extension/src/shared/nft/implementation.ts
+++ b/packages/extension/src/shared/nft/implementation.ts
@@ -1,10 +1,12 @@
import {
- Address,
+ type Address,
+ type ArgentBackendNetworkId,
ArgentBackendNftService,
- Collection,
- NftItem,
- PaginatedItems,
+ type Collection,
+ type NftItem,
+ type PaginatedItems,
isEqualAddress,
+ PaginatedCollections,
} from "@argent/shared"
import { differenceWith, groupBy, isEqual } from "lodash-es"
import { AllowArray, constants, num, shortString } from "starknet"
@@ -26,7 +28,7 @@ import {
type NftMarketplace,
} from "./marketplaces"
-const chainIdToPandoraNetwork = (chainId: string): "mainnet" | "goerli" => {
+const chainIdToPandoraNetwork = (chainId: string): ArgentBackendNetworkId => {
const encodedChainId = num.isHex(chainId)
? chainId
: shortString.encodeShortString(chainId)
@@ -36,6 +38,8 @@ const chainIdToPandoraNetwork = (chainId: string): "mainnet" | "goerli" => {
return "mainnet"
case constants.StarknetChainId.SN_GOERLI:
return "goerli"
+ case constants.StarknetChainId.SN_SEPOLIA:
+ return "sepolia"
}
throw new Error(`Unsupported network ${chainId}`)
}
@@ -102,7 +106,7 @@ export class NFTService implements INFTService {
private async fetchNftsUrl(
chain: string,
- network: "mainnet" | "goerli",
+ network: ArgentBackendNetworkId,
accountAddress: string,
page = 1,
): Promise {
@@ -128,6 +132,35 @@ export class NFTService implements INFTService {
return paginateditems
}
+ private async fetchCollectionsUrl(
+ chain: string,
+ network: ArgentBackendNetworkId,
+ accountAddress: string,
+ page = 1,
+ ): Promise {
+ const paginateditems: PaginatedCollections =
+ await this.argentNftService.getProfileCollections(
+ chain,
+ network,
+ accountAddress,
+ page,
+ )
+ if (page < paginateditems.totalPages) {
+ const nextPage: PaginatedCollections = await this.fetchCollectionsUrl(
+ chain,
+ network,
+ accountAddress,
+ paginateditems.page + 1,
+ )
+
+ return {
+ ...paginateditems,
+ collections: paginateditems.collections.concat(nextPage.collections),
+ }
+ }
+ return paginateditems
+ }
+
async getCollection(
chain: string,
networkId: string,
@@ -148,26 +181,32 @@ export class NFTService implements INFTService {
chain: string,
networkId: string,
contractsAddresses: ContractAddress[],
+ accountAddress: string,
) {
const axNetwork = await this.networkService.getById(networkId)
const pandoraNetwork = chainIdToPandoraNetwork(axNetwork.chainId)
await this.nftsContractsRepository.upsert(contractsAddresses)
- const collections = groupBy(
+ const collectionsRepository = groupBy(
await this.nftsCollectionsRepository.get(),
"contractAddress",
)
const toPush: Collection[] = []
- for (const contract of contractsAddresses) {
- if (!collections[contract.contractAddress]) {
- const { nfts, ...rest } = await this.argentNftService.getCollection(
- chain,
- pandoraNetwork,
- contract.contractAddress,
- )
- toPush.push({ ...rest, networkId })
+
+ const { collections } = await this.fetchCollectionsUrl(
+ chain,
+ pandoraNetwork,
+ accountAddress,
+ )
+
+ for (const collection of collections) {
+ // already stored
+ if (collectionsRepository[collection.contractAddress]) {
+ continue
}
+
+ toPush.push({ ...collection, networkId })
}
if (toPush.length > 0) {
diff --git a/packages/extension/src/shared/nft/interface.ts b/packages/extension/src/shared/nft/interface.ts
index 06fabd3bb..590edd5e1 100644
--- a/packages/extension/src/shared/nft/interface.ts
+++ b/packages/extension/src/shared/nft/interface.ts
@@ -25,6 +25,7 @@ export interface INFTService {
chain: string,
networkId: string,
contractsAddresses: ContractAddress[],
+ accountAddress: string,
) => Promise
upsert: (
nfts: AllowArray,
diff --git a/packages/extension/src/shared/provision/interface.ts b/packages/extension/src/shared/provision/interface.ts
new file mode 100644
index 000000000..ea030d10b
--- /dev/null
+++ b/packages/extension/src/shared/provision/interface.ts
@@ -0,0 +1,5 @@
+import { ProvisionStatus } from "./types"
+
+export interface IProvisionService {
+ getStatus: () => Promise
+}
diff --git a/packages/extension/src/shared/provision/types.ts b/packages/extension/src/shared/provision/types.ts
new file mode 100644
index 000000000..83672c2d5
--- /dev/null
+++ b/packages/extension/src/shared/provision/types.ts
@@ -0,0 +1,19 @@
+import { z } from "zod"
+
+export const statusSchema = z.union([
+ z.literal("notActive"),
+ z.literal("eligibilityCheck"),
+ z.literal("active"),
+ z.literal("paused"),
+ z.literal("disabled"),
+])
+
+export const provisionStatusSchema = z.object({
+ status: statusSchema,
+ link: z.string().optional(),
+ bannerTitle: z.string(),
+ bannerDescription: z.string(),
+})
+
+export type ProvisionStatus = z.infer
+export type Status = z.infer
diff --git a/packages/extension/src/shared/recovery/service/interface.ts b/packages/extension/src/shared/recovery/service/interface.ts
index 48568c2b7..69c88c481 100644
--- a/packages/extension/src/shared/recovery/service/interface.ts
+++ b/packages/extension/src/shared/recovery/service/interface.ts
@@ -1,4 +1,5 @@
export interface IRecoveryService {
- byBackup: (backup: string) => Promise
- bySeedPhrase: (seedPhrase: string, newPassword: string) => Promise
+ byBackup(backup: string): Promise
+ bySeedPhrase(seedPhrase: string, newPassword: string): Promise
+ clearErrorRecovering(): Promise
}
diff --git a/packages/extension/src/shared/recovery/storage.ts b/packages/extension/src/shared/recovery/storage.ts
index f2ef60f56..edcda4541 100644
--- a/packages/extension/src/shared/recovery/storage.ts
+++ b/packages/extension/src/shared/recovery/storage.ts
@@ -5,6 +5,8 @@ import { IRecoveryStorage } from "./types"
const keyValueStorage = new KeyValueStorage(
{
isRecovering: false,
+ errorRecovering: false,
+ isClearingStorage: false,
},
{
namespace: "service:recovery",
@@ -12,3 +14,14 @@ const keyValueStorage = new KeyValueStorage(
)
export const recoveryStore = adaptKeyValue(keyValueStorage)
+
+export const recoveredAtKeyValueStore = new KeyValueStorage<{
+ lastRecoveredAt: number | null
+}>(
+ { lastRecoveredAt: null },
+ {
+ namespace: "core:recoveredAt",
+ areaName: "local",
+ },
+)
+export const recoveredAtStore = adaptKeyValue(recoveredAtKeyValueStore)
diff --git a/packages/extension/src/shared/recovery/types.ts b/packages/extension/src/shared/recovery/types.ts
index 852d09e59..8c938e15a 100644
--- a/packages/extension/src/shared/recovery/types.ts
+++ b/packages/extension/src/shared/recovery/types.ts
@@ -1,3 +1,5 @@
export interface IRecoveryStorage {
isRecovering: boolean
+ errorRecovering: string | false
+ isClearingStorage: boolean
}
diff --git a/packages/extension/src/shared/riskAssessment/interface.ts b/packages/extension/src/shared/riskAssessment/interface.ts
new file mode 100644
index 000000000..605f55cbf
--- /dev/null
+++ b/packages/extension/src/shared/riskAssessment/interface.ts
@@ -0,0 +1,17 @@
+import { z } from "zod"
+import { RiskAssessment } from "./schema"
+
+export const dappContextSchema = z.object({
+ dappDomain: z.string(),
+ network: z.string(),
+})
+
+export type DappContext = z.infer
+
+export interface IRiskAssessmentService {
+ assessRisk({
+ dappContext,
+ }: {
+ dappContext: DappContext
+ }): Promise
+}
diff --git a/packages/extension/src/shared/riskAssessment/schema.ts b/packages/extension/src/shared/riskAssessment/schema.ts
new file mode 100644
index 000000000..de39ed47b
--- /dev/null
+++ b/packages/extension/src/shared/riskAssessment/schema.ts
@@ -0,0 +1,38 @@
+import { z } from "zod"
+import { warningSchema } from "../transactionReview/schema"
+
+const linkSchema = z.object({
+ name: z.string(),
+ url: z.string(),
+ position: z.number(),
+})
+
+const contractSchema = z.object({
+ address: z.string(),
+ chain: z.string(),
+})
+
+const dappSchema = z.object({
+ dappId: z.string().optional(),
+ name: z.string().optional(),
+ description: z.string().optional(),
+ logoUrl: z.string().optional(),
+ iconUrl: z.string().optional(),
+ argentVerified: z.boolean().default(false),
+ dappDomain: z.string().optional(),
+ isUnknown: z.boolean().default(true),
+ links: z.array(linkSchema),
+ contracts: z.array(contractSchema),
+ urlSoundex: z.array(z.string()),
+ unknown: z.boolean().default(true),
+})
+
+export const riskAssessmentSchema = z.object({
+ dapp: dappSchema.optional(),
+ warning: warningSchema.optional(),
+ status: z.number().optional(),
+ error: z.string().optional(),
+})
+
+export type Dapp = z.infer
+export type RiskAssessment = z.infer
diff --git a/packages/extension/src/shared/sentry/options.ts b/packages/extension/src/shared/sentry/options.ts
new file mode 100644
index 000000000..619c1ed88
--- /dev/null
+++ b/packages/extension/src/shared/sentry/options.ts
@@ -0,0 +1,21 @@
+import * as Sentry from "@sentry/browser"
+
+const environment = process.env.SENTRY_ENVIRONMENT ?? process.env.NODE_ENV
+
+const release = getRelease()
+
+export const baseSentryOptions: Sentry.BrowserOptions = {
+ dsn: process.env.SENTRY_DSN,
+ environment,
+ release,
+ autoSessionTracking: false, // don't want to track user sessions.
+}
+
+function getRelease() {
+ const commitHash = process.env.COMMIT_HASH
+ const release = process.env.npm_package_version
+ if (environment === "staging" && commitHash) {
+ return `${release}-rc__${commitHash}`
+ }
+ return release
+}
diff --git a/packages/extension/src/shared/settings/defaultBlockExplorers.ts b/packages/extension/src/shared/settings/defaultBlockExplorers.ts
index efb205957..6c69e5613 100644
--- a/packages/extension/src/shared/settings/defaultBlockExplorers.ts
+++ b/packages/extension/src/shared/settings/defaultBlockExplorers.ts
@@ -14,6 +14,7 @@ export const defaultBlockExplorers: BlockExplorers = {
logo: "StarknetLogo",
url: {
"mainnet-alpha": "https://starkscan.co",
+ "sepolia-alpha": "https://sepolia.starkscan.co",
"goerli-alpha": "https://testnet.starkscan.co",
localhost: "https://devnet.starkscan.co",
},
@@ -23,6 +24,7 @@ export const defaultBlockExplorers: BlockExplorers = {
logo: "VoyagerLogo",
url: {
"mainnet-alpha": "https://voyager.online",
+ "sepolia-alpha": "https://sepolia.voyager.online",
"goerli-alpha": "https://goerli.voyager.online",
localhost: "https://goerli.voyager.online/local-version",
},
diff --git a/packages/extension/src/shared/settings/store.ts b/packages/extension/src/shared/settings/store.ts
index 8bd0c66bd..80af92491 100644
--- a/packages/extension/src/shared/settings/store.ts
+++ b/packages/extension/src/shared/settings/store.ts
@@ -8,7 +8,7 @@ export const settingsStore = new KeyValueStorage(
privacyUseArgentServices: true,
privacyShareAnalyticsData: true,
privacyErrorReporting: Boolean(process.env.SENTRY_DSN), // use SENRY_DSN to enable error reporting
- privacyAutomaticErrorReporting: false,
+ privacyAutomaticErrorReporting: true,
experimentalAllowChooseAccount: false,
blockExplorerKey: defaultBlockExplorerKey,
nftMarketplaceKey: "unframed",
diff --git a/packages/extension/src/shared/shield/GuardianSelfSigner.ts b/packages/extension/src/shared/shield/GuardianSelfSigner.ts
index b88727fe5..c59ea5ac6 100644
--- a/packages/extension/src/shared/shield/GuardianSelfSigner.ts
+++ b/packages/extension/src/shared/shield/GuardianSelfSigner.ts
@@ -1,13 +1,12 @@
import {
- Abi,
Call,
DeclareSignerDetails,
DeployAccountSignerDetails,
InvocationsSignerDetails,
Signature,
stark,
-} from "starknet"
-import { Signer, typedData } from "starknet"
+} from "starknet6"
+import { Signer, typedData } from "starknet6"
/**
* Use case: `escapeGuardian` cannot be used to remove or set guardian to ZERO
@@ -34,57 +33,25 @@ export class GuardianSelfSigner extends Signer {
public async signTransaction(
transactions: Call[],
transactionsDetail: InvocationsSignerDetails,
- abis?: Abi[],
): Promise {
const signatures = await super.signTransaction(
transactions,
transactionsDetail,
- abis,
)
const formattedSignatures = stark.signatureToDecimalArray(signatures)
return [...formattedSignatures, ...formattedSignatures]
}
- public async signDeployAccountTransaction({
- classHash,
- contractAddress,
- constructorCalldata,
- addressSalt,
- maxFee,
- version,
- chainId,
- nonce,
- }: DeployAccountSignerDetails) {
- const signatures = await super.signDeployAccountTransaction({
- classHash,
- contractAddress,
- constructorCalldata,
- addressSalt,
- maxFee,
- version,
- chainId,
- nonce,
- })
+ public async signDeployAccountTransaction(
+ details: DeployAccountSignerDetails,
+ ) {
+ const signatures = await super.signDeployAccountTransaction(details)
const formattedSignatures = stark.signatureToDecimalArray(signatures)
return [...formattedSignatures, ...formattedSignatures]
}
- public async signDeclareTransaction({
- classHash,
- senderAddress,
- chainId,
- maxFee,
- version,
- nonce,
- }: DeclareSignerDetails) {
- const signatures = await super.signDeclareTransaction({
- classHash,
- senderAddress,
- chainId,
- maxFee,
- version,
- nonce,
- })
+ public async signDeclareTransaction(details: DeclareSignerDetails) {
+ const signatures = await super.signDeclareTransaction(details)
const formattedSignatures = stark.signatureToDecimalArray(signatures)
return [...formattedSignatures, ...formattedSignatures]
}
diff --git a/packages/extension/src/shared/shield/GuardianSignerArgentX.ts b/packages/extension/src/shared/shield/GuardianSignerArgentX.ts
index 9eccc8427..6a146e2f8 100644
--- a/packages/extension/src/shared/shield/GuardianSignerArgentX.ts
+++ b/packages/extension/src/shared/shield/GuardianSignerArgentX.ts
@@ -1,6 +1,6 @@
-import { CosignerOffchainMessage, GuardianSigner } from "@argent/guardian"
-import type { CosignerMessage } from "@argent/guardian"
-import { Signature, hash, num } from "starknet"
+import { GuardianSigner } from "@argent/guardian"
+import type { CosignerMessage, CosignerOffchainMessage } from "@argent/guardian"
+import { Signature, hash, num } from "starknet6"
import { isEqualAddress } from "@argent/shared"
import { isTokenExpired } from "./backend/account"
@@ -25,11 +25,15 @@ export class GuardianSignerArgentX extends GuardianSigner {
isOffchainMessage = false,
): Promise {
/** special case - check guardianSignerNotRequired */
- const selector = cosignerMessage.message?.calldata?.[2]
-
if (
+ "type" in cosignerMessage &&
+ (cosignerMessage.type === "starknet" ||
+ cosignerMessage.type === "starknetV3") &&
guardianSignerNotRequiredSelectors.find((notRequiredSelector) =>
- isEqualAddress(notRequiredSelector, selector),
+ isEqualAddress(
+ notRequiredSelector,
+ cosignerMessage.message.calldata[2], // calldata[2] is the selector
+ ),
)
) {
return []
diff --git a/packages/extension/src/shared/storage/__new/__test__/inmemoryImplementations.ts b/packages/extension/src/shared/storage/__new/__test__/inmemoryImplementations.ts
index c84b232f9..2440fceef 100644
--- a/packages/extension/src/shared/storage/__new/__test__/inmemoryImplementations.ts
+++ b/packages/extension/src/shared/storage/__new/__test__/inmemoryImplementations.ts
@@ -80,7 +80,10 @@ export class InMemoryRepository implements IRepository {
return selector ? this._data.filter(selector) : this._data
}
- async upsert(value: AllowArray | SetterFn): Promise {
+ async upsert(
+ value: AllowArray | SetterFn,
+ insertMode: "push" | "unshift" = "push",
+ ): Promise {
const oldValue = [...this._data]
const items = isFunction(value)
? value(oldValue)
@@ -100,7 +103,7 @@ export class InMemoryRepository implements IRepository {
this._data[index] = item
updated++
} else {
- this._data.push(item)
+ this._data[insertMode](item)
created++
}
}
diff --git a/packages/extension/src/shared/storage/__new/chrome.ts b/packages/extension/src/shared/storage/__new/chrome.ts
index 93b4f234d..9c07ab2ef 100644
--- a/packages/extension/src/shared/storage/__new/chrome.ts
+++ b/packages/extension/src/shared/storage/__new/chrome.ts
@@ -81,7 +81,10 @@ export class ChromeRepository implements IRepository {
return removedValues
}
- async upsert(value: AllowArray | SetterFn): Promise {
+ async upsert(
+ value: AllowArray | SetterFn,
+ insertMode: "push" | "unshift" = "push",
+ ): Promise {
// use mergeArrayStableWith to merge the new values with the existing values
const items = await this.getStorage()
@@ -93,10 +96,10 @@ export class ChromeRepository implements IRepository {
} else {
newValues = [value]
}
-
const mergedValues = mergeArrayStableWith(items, newValues, {
compareFn: this.options.compare.bind(this),
mergeFn: this.options.merge.bind(this),
+ insertMode,
})
await this.set(mergedValues)
diff --git a/packages/extension/src/shared/storage/__new/interface.ts b/packages/extension/src/shared/storage/__new/interface.ts
index 906dbbc15..21a774c44 100644
--- a/packages/extension/src/shared/storage/__new/interface.ts
+++ b/packages/extension/src/shared/storage/__new/interface.ts
@@ -67,7 +67,10 @@ export interface IRepository {
* @param value - An array of items, a single item, or a setter function that operates on an array of items.
* @returns A Promise that resolves to a boolean indicating whether the operation succeeded.
*/
- upsert(value: AllowArray | SetterFn): Promise
+ upsert(
+ value: AllowArray | SetterFn,
+ insertMode?: "push" | "unshift",
+ ): Promise
/**
* Removes items from the repository based on the provided value or selector function.
diff --git a/packages/extension/src/shared/storage/__new/prune.ts b/packages/extension/src/shared/storage/__new/prune.ts
index 74152f61c..07b262be1 100644
--- a/packages/extension/src/shared/storage/__new/prune.ts
+++ b/packages/extension/src/shared/storage/__new/prune.ts
@@ -28,6 +28,7 @@ const localStoragePatterns: Pattern[] = [
/"feeTokenBalance"/,
[/^core:transactions$/, pruneTransactions],
/^dev:storage/,
+ /^core:tokenInfo$/,
]
/** keep in-flight transactions as they can't be retreived from backend or on-chain */
diff --git a/packages/extension/src/shared/storage/__new/repository.ts b/packages/extension/src/shared/storage/__new/repository.ts
index e04d28acd..9d87e7709 100644
--- a/packages/extension/src/shared/storage/__new/repository.ts
+++ b/packages/extension/src/shared/storage/__new/repository.ts
@@ -17,8 +17,11 @@ export function adaptArrayStorage(storage: ArrayStorage): IRepository {
return storage.get(selector)
},
- async upsert(value: AllowArray | SetterFn): Promise {
- await storage.push(value)
+ async upsert(
+ value: AllowArray | SetterFn,
+ insertMode: "push" | "unshift" = "push",
+ ): Promise {
+ await storage[insertMode](value)
return { created: Date.now(), updated: Date.now() }
},
diff --git a/packages/extension/src/shared/token/__new/constants.ts b/packages/extension/src/shared/token/__new/constants.ts
index 5909cfd49..89c657789 100644
--- a/packages/extension/src/shared/token/__new/constants.ts
+++ b/packages/extension/src/shared/token/__new/constants.ts
@@ -12,6 +12,11 @@ export const ETH: Record = {
"0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
networkId: "goerli-alpha",
},
+ [constants.StarknetChainId.SN_SEPOLIA]: {
+ address:
+ "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
+ networkId: "sepolia-alpha",
+ },
}
export const USDC: Record = {
@@ -25,4 +30,9 @@ export const USDC: Record = {
"0x005a643907b9a4bc6a55e9069c4fd5fd1f5c79a22470690f75556c4736e34426",
networkId: "goerli-alpha",
},
+ [constants.StarknetChainId.SN_SEPOLIA]: {
+ address:
+ "0x03a909c1f2d1900d0c96626fac1bedf1e82b92110e5c529b05f9138951b93535",
+ networkId: "sepolia-alpha",
+ },
}
diff --git a/packages/extension/src/shared/token/__new/repository/mergeTokens.test.ts b/packages/extension/src/shared/token/__new/repository/mergeTokens.test.ts
new file mode 100644
index 000000000..24418d484
--- /dev/null
+++ b/packages/extension/src/shared/token/__new/repository/mergeTokens.test.ts
@@ -0,0 +1,87 @@
+import { describe, test } from "vitest"
+
+import type { Token } from "../types/token.model"
+import { mergeTokens, mergeTokensWithDefaults } from "./mergeTokens"
+
+const MOCK_TOKEN_1: Token = {
+ symbol: "MOCK1",
+ address: "0x1",
+ networkId: "mainnet-alpha",
+ name: "Mock1",
+ decimals: 18,
+}
+
+const MOCK_TOKEN_2: Token = {
+ symbol: "MOCK2",
+ address: "0x2",
+ networkId: "mainnet-alpha",
+ name: "Mock2",
+ decimals: 18,
+}
+
+const MOCK_TOKEN_3: Token = {
+ symbol: "MOCK3",
+ address: "0x3",
+ networkId: "mainnet-alpha",
+ name: "Mock3",
+ decimals: 18,
+}
+
+describe("shared/token/repository", () => {
+ describe("mergeTokens", () => {
+ test("merges tokens, keeping old values and updating new values", () => {
+ const firstToken: Token = {
+ ...MOCK_TOKEN_1,
+ pricingId: 1,
+ }
+ const secondToken: Token = {
+ ...MOCK_TOKEN_1,
+ name: "Mock1Updated",
+ }
+ expect(mergeTokens(firstToken, secondToken)).toEqual({
+ address: "0x1",
+ decimals: 18,
+ name: "Mock1Updated",
+ networkId: "mainnet-alpha",
+ pricingId: 1,
+ symbol: "MOCK1",
+ })
+ })
+ })
+ describe("mergeTokensWithDefaults", () => {
+ test("merges tokens, keeping old values and updating new values, inserting new tokens", () => {
+ const tokens: Token[] = [
+ {
+ ...MOCK_TOKEN_1,
+ name: "Mock1Existing",
+ pricingId: 1,
+ },
+ {
+ ...MOCK_TOKEN_2,
+ name: "Mock2Existing",
+ iconUrl: "https://foo.bar",
+ },
+ ]
+ const defaultTokens = [MOCK_TOKEN_1, MOCK_TOKEN_2, MOCK_TOKEN_3]
+ expect(mergeTokensWithDefaults(tokens, defaultTokens)).toEqual([
+ MOCK_TOKEN_3,
+ {
+ address: "0x1",
+ decimals: 18,
+ name: "Mock1",
+ networkId: "mainnet-alpha",
+ pricingId: 1,
+ symbol: "MOCK1",
+ },
+ {
+ address: "0x2",
+ decimals: 18,
+ iconUrl: "https://foo.bar",
+ name: "Mock2",
+ networkId: "mainnet-alpha",
+ symbol: "MOCK2",
+ },
+ ])
+ })
+ })
+})
diff --git a/packages/extension/src/shared/token/__new/repository/mergeTokens.ts b/packages/extension/src/shared/token/__new/repository/mergeTokens.ts
new file mode 100644
index 000000000..19d6804b9
--- /dev/null
+++ b/packages/extension/src/shared/token/__new/repository/mergeTokens.ts
@@ -0,0 +1,20 @@
+import type { Token } from "../types/token.model"
+import { equalToken, parsedDefaultTokens } from "../utils"
+import { mergeArrayStableWith } from "../../../storage/__new/base"
+
+export const mergeTokens = (oldValue: Token, newValue: Token) => ({
+ ...oldValue,
+ ...newValue,
+})
+
+export const mergeTokensWithDefaults = (
+ tokens: Token[],
+ defaultTokens: Token[] = parsedDefaultTokens,
+) => {
+ const repoTokensWithDefaults = mergeArrayStableWith(tokens, defaultTokens, {
+ compareFn: equalToken,
+ mergeFn: mergeTokens,
+ insertMode: "unshift",
+ })
+ return repoTokensWithDefaults
+}
diff --git a/packages/extension/src/shared/token/__new/repository/token.ts b/packages/extension/src/shared/token/__new/repository/token.ts
index 7a5bc49ca..8db5badd0 100644
--- a/packages/extension/src/shared/token/__new/repository/token.ts
+++ b/packages/extension/src/shared/token/__new/repository/token.ts
@@ -1,8 +1,10 @@
import browser from "webextension-polyfill"
+
import { ChromeRepository } from "../../../storage/__new/chrome"
-import { IRepository } from "../../../storage/__new/interface"
-import { BaseToken, Token } from "../types/token.model"
+import type { IRepository } from "../../../storage/__new/interface"
+import type { Token } from "../types/token.model"
import { equalToken, parsedDefaultTokens } from "../utils"
+import { mergeTokens, mergeTokensWithDefaults } from "./mergeTokens"
export type ITokenRepository = IRepository
@@ -11,11 +13,12 @@ export const tokenRepo: ITokenRepository = new ChromeRepository(
{
areaName: "local",
namespace: "core:tokens",
- compare: (a: BaseToken, b: BaseToken) => equalToken(a, b),
- merge: (oldValue: Token, newValue: Token) => ({
- ...oldValue,
- ...newValue,
- }),
+ compare: equalToken,
+ merge: mergeTokens,
defaults: parsedDefaultTokens,
+ deserialize(repoTokens: Token[]): Token[] {
+ // overwrite the stored values for the default tokens with the default values
+ return mergeTokensWithDefaults(repoTokens, parsedDefaultTokens)
+ },
},
)
diff --git a/packages/extension/src/shared/token/__new/repository/tokenInfo.ts b/packages/extension/src/shared/token/__new/repository/tokenInfo.ts
new file mode 100644
index 000000000..7fef433e2
--- /dev/null
+++ b/packages/extension/src/shared/token/__new/repository/tokenInfo.ts
@@ -0,0 +1,12 @@
+import { KeyValueStorage } from "../../../storage"
+import { adaptKeyValue } from "../../../storage/__new/keyvalue"
+import { TokenInfoByNetwork } from "../types/tokenInfo.model"
+
+const keyValueStorage = new KeyValueStorage(
+ {},
+ {
+ namespace: "core:tokenInfo",
+ },
+)
+
+export const tokenInfoStore = adaptKeyValue(keyValueStorage)
diff --git a/packages/extension/src/shared/token/__new/service/implementation.test.ts b/packages/extension/src/shared/token/__new/service/implementation.test.ts
index 45c3243ed..a7d013cef 100644
--- a/packages/extension/src/shared/token/__new/service/implementation.test.ts
+++ b/packages/extension/src/shared/token/__new/service/implementation.test.ts
@@ -19,71 +19,29 @@ import {
getMockNetworkWithoutMulticall,
} from "../../../../../test/network.mock"
import { INetworkRepo } from "../../../network/store"
-import { rest } from "msw"
-import { setupServer } from "msw/node"
import { GatewayError, shortString, stark } from "starknet"
-import { Address, addressSchema } from "@argent/shared"
+import { IHttpService, addressSchema } from "@argent/shared"
import { TokenError } from "../../../errors/token"
-import {
- ETH_TOKEN_ADDRESS,
- STRK_TOKEN_ADDRESS,
- TXV3_ACCOUNT_CLASS_HASH,
-} from "../../../network/constants"
+import { IObjectStore } from "../../../storage/__new/interface"
+import { TokenInfoByNetwork } from "../types/tokenInfo.model"
const BASE_INFO_ENDPOINT = "https://token.info.argent47.net/v1"
-const BASE_INFO_ENDPOINT_INVALID = "https://token.info.argent47.net/v2"
const BASE_PRICES_ENDPOINT = "https://token.prices.argent47.net/v1"
-// const BASE_URL_WITH_WILDCARD = BASE_URL_ENDPOINT + "*"
-
const randomAddress1 = addressSchema.parse(stark.randomAddress())
const randomAddress2 = addressSchema.parse(stark.randomAddress())
-const server = setupServer(
- rest.get(BASE_INFO_ENDPOINT, (req, res, ctx) => {
- return res(
- ctx.json({
- tokens: [
- getMockApiTokenDetails({ id: 1, address: randomAddress1 }),
- getMockApiTokenDetails({
- id: 2,
- address: randomAddress2,
- pricingId: 2,
- }),
- ],
- }),
- )
- }),
- rest.get(BASE_INFO_ENDPOINT_INVALID, (req, res, ctx) => {
- return res(
- ctx.json([
- getMockApiTokenDetails({ address: randomAddress1 }),
- getMockApiTokenDetails({ address: randomAddress2, pricingId: 2 }),
- ]),
- )
- }),
- rest.get(BASE_PRICES_ENDPOINT, (req, res, ctx) => {
- return res(
- ctx.json({
- prices: [
- getMockTokenPriceDetails({ pricingId: 1, ethValue: "0.32" }),
- getMockTokenPriceDetails({ pricingId: 2, ethValue: "0.64" }),
- ],
- }),
- )
- }),
-)
-
describe("TokenService", () => {
let tokenService: TokenService
let mockNetworkService: Mocked
+
let mockNetworkRepo: MockFnRepository
let mockTokenRepo: MockFnRepository
let mockTokenBalanceRepo: MockFnRepository
let mockTokenPriceRepo: MockFnRepository
- beforeAll(() => {
- server.listen()
- })
+ let mockHttpService: Mocked
+ let mockTokenInfoStore: Mocked>
+
beforeEach(() => {
mockTokenRepo = new MockFnRepository()
mockTokenBalanceRepo = new MockFnRepository()
@@ -94,11 +52,24 @@ describe("TokenService", () => {
new NetworkService(mockNetworkRepo),
)
+ mockHttpService = {
+ get: vi.fn(),
+ } as unknown as Mocked
+
+ mockTokenInfoStore = {
+ namespace: "core:tokenInfo",
+ get: vi.fn(),
+ set: vi.fn(),
+ subscribe: vi.fn(),
+ }
+
tokenService = new TokenService(
mockNetworkService,
mockTokenRepo,
mockTokenBalanceRepo,
mockTokenPriceRepo,
+ mockTokenInfoStore,
+ mockHttpService,
BASE_INFO_ENDPOINT,
BASE_PRICES_ENDPOINT,
)
@@ -152,53 +123,203 @@ describe("TokenService", () => {
expect(mockTokenPriceRepo.upsert).toHaveBeenCalledWith(mockTokenPrices)
})
- describe("fetch tokens from backend", () => {
- it("should fetch tokens from backend", async () => {
- const mockNetworkId = defaultNetwork.id
- const defaultMockApiTokeDetails = getMockApiTokenDetails()
- const mockToken1 = getMockToken({
- id: 1,
- address: randomAddress1,
- networkId: mockNetworkId,
- iconUrl: defaultMockApiTokeDetails.iconUrl,
- showAlways: undefined,
- custom: undefined,
- popular: defaultMockApiTokeDetails.popular,
- pricingId: defaultMockApiTokeDetails.pricingId,
- tradable: true,
+ describe("get tokens info from backend", () => {
+ const mockNetworkId = defaultNetwork.id
+ const defaultMockApiTokeDetails = getMockApiTokenDetails()
+ const mockToken1 = getMockApiTokenDetails({
+ id: 1,
+ address: randomAddress1,
+ iconUrl: defaultMockApiTokeDetails.iconUrl,
+ popular: defaultMockApiTokeDetails.popular,
+ pricingId: defaultMockApiTokeDetails.pricingId,
+ tradable: true,
+ })
+ const mockToken2 = getMockApiTokenDetails({
+ id: 2,
+ address: randomAddress2,
+ iconUrl: defaultMockApiTokeDetails.iconUrl,
+ popular: defaultMockApiTokeDetails.popular,
+ pricingId: 2,
+ tradable: true,
+ })
+ const mockTokens = [mockToken1, mockToken2]
+
+ describe("when storage is empty", () => {
+ it("should fetch tokens from backend", async () => {
+ mockTokenInfoStore.get.mockResolvedValueOnce({})
+ mockHttpService.get.mockResolvedValueOnce({ tokens: mockTokens })
+ const result = await tokenService.getTokensInfoFromBackendForNetwork(
+ mockNetworkId,
+ )
+ expect(mockTokenInfoStore.get).toHaveBeenCalledOnce()
+ expect(mockHttpService.get).toHaveBeenCalledOnce()
+ expect(mockTokenInfoStore.set).toHaveBeenCalledOnce()
+ expect(result).toEqual(mockTokens)
})
- const mockToken2 = getMockToken({
- id: 2,
- address: randomAddress2,
- networkId: mockNetworkId,
- showAlways: undefined,
- iconUrl: defaultMockApiTokeDetails.iconUrl,
- custom: undefined,
- popular: defaultMockApiTokeDetails.popular,
- pricingId: 2,
- tradable: true,
+ })
+
+ describe("when storage has recent tokens", () => {
+ it("should return tokens from storage and not fetch from backend", async () => {
+ mockTokenInfoStore.get.mockResolvedValueOnce({
+ [mockNetworkId]: {
+ updatedAt: Date.now(),
+ data: mockTokens,
+ },
+ })
+ mockHttpService.get.mockResolvedValueOnce({ tokens: mockTokens })
+ const result = await tokenService.getTokensInfoFromBackendForNetwork(
+ mockNetworkId,
+ )
+ expect(mockTokenInfoStore.get).toHaveBeenCalledOnce()
+ expect(mockHttpService.get).not.toHaveBeenCalled()
+ expect(mockTokenInfoStore.set).not.toHaveBeenCalled()
+ expect(result).toEqual(mockTokens)
})
- const mockTokens = [mockToken1, mockToken2]
- mockTokenRepo.get.mockResolvedValueOnce(mockTokens)
- const result = await tokenService.fetchTokensFromBackend(mockNetworkId)
- expect(result).toEqual(mockTokens)
})
+ describe("when storage has old tokens", () => {
+ it("should fetch tokens from backend", async () => {
+ mockTokenInfoStore.get.mockResolvedValueOnce({
+ [mockNetworkId]: {
+ updatedAt: 0,
+ data: mockTokens,
+ },
+ })
+ mockHttpService.get.mockResolvedValueOnce({ tokens: mockTokens })
+ const result = await tokenService.getTokensInfoFromBackendForNetwork(
+ mockNetworkId,
+ )
+ expect(mockTokenInfoStore.get).toHaveBeenCalledOnce()
+ expect(mockHttpService.get).toHaveBeenCalledOnce()
+ expect(mockTokenInfoStore.set).toHaveBeenCalledOnce()
+ expect(result).toEqual(mockTokens)
+ })
+ })
+ })
+
+ describe("fetch account token balances from backend", () => {
+ describe("when default network", async () => {
+ describe("and the response is not initialised", async () => {
+ it("should retry until initialised", async () => {
+ const networkId = defaultNetwork.id
+ mockHttpService.get
+ .mockResolvedValueOnce({ status: "initialising" })
+ .mockResolvedValueOnce({ status: "initialising" })
+ .mockResolvedValueOnce({ status: "initialising" })
+ .mockResolvedValueOnce({
+ status: "initialised",
+ balances: [
+ {
+ tokenAddress: "0x123",
+ tokenBalance: "123",
+ },
+ ],
+ })
+ const result =
+ await tokenService.fetchAccountTokenBalancesFromBackend(
+ {
+ address: "0x123",
+ networkId,
+ },
+ {
+ minTimeout: 0,
+ maxTimeout: 0,
+ },
+ )
+ expect(mockHttpService.get).toHaveBeenCalledTimes(4)
+ expect(mockHttpService.get).toHaveBeenCalledWith(
+ expect.stringMatching(
+ /activity\/starknet\/(goerli|sepolia|mainnet)\/account\/0x123\/balance$/,
+ ),
+ )
+ expect(result).toEqual([
+ {
+ account: {
+ address: "0x123",
+ networkId,
+ },
+ address:
+ "0x0000000000000000000000000000000000000000000000000000000000000123",
+ balance: "123",
+ networkId,
+ },
+ ])
+ })
+ })
+ describe("and the response is initialised", async () => {
+ it("should return first response", async () => {
+ const networkId = defaultNetwork.id
+ mockHttpService.get.mockResolvedValueOnce({
+ status: "initialised",
+ balances: [
+ {
+ tokenAddress: "0x123",
+ tokenBalance: "123",
+ },
+ ],
+ })
+ const result =
+ await tokenService.fetchAccountTokenBalancesFromBackend(
+ {
+ address: "0x123",
+ networkId,
+ },
+ {
+ minTimeout: 0,
+ maxTimeout: 0,
+ },
+ )
+ expect(mockHttpService.get).toHaveBeenCalledTimes(1)
+ expect(mockHttpService.get).toHaveBeenCalledWith(
+ expect.stringMatching(
+ /activity\/starknet\/(goerli|sepolia|mainnet)\/account\/0x123\/balance$/,
+ ),
+ )
+ expect(result).toEqual([
+ {
+ account: {
+ address: "0x123",
+ networkId,
+ },
+ address:
+ "0x0000000000000000000000000000000000000000000000000000000000000123",
+ balance: "123",
+ networkId,
+ },
+ ])
+ })
+ })
+ })
+ describe("when not default network", () => {
+ it("should return empty array", async () => {
+ const result = await tokenService.fetchAccountTokenBalancesFromBackend({
+ address: "0x123",
+ networkId: "invalid-network",
+ })
+ expect(result).toEqual([])
+ })
+ })
+ })
+
+ describe("fetch tokens from backend", () => {
it("should return without fetching tokens if it is not a default network", async () => {
const mockNetworkId = "mockNetworkId"
- const mockTokens = [getMockToken({ networkId: mockNetworkId })]
- mockTokenRepo.get.mockResolvedValueOnce(mockTokens)
- const result = await tokenService.fetchTokensFromBackend(mockNetworkId)
- expect(result).toEqual(mockTokens)
+ const result = await tokenService.getTokensInfoFromBackendForNetwork(
+ mockNetworkId,
+ )
+ expect(mockTokenInfoStore.get).not.toHaveBeenCalled()
+ expect(result).toBeUndefined()
})
- it("should throw parsing error if token info response is not valid", async () => {
+ it("should return undefined if token info response is not valid", async () => {
const invalidTokenService = new TokenService(
mockNetworkService,
mockTokenRepo,
mockTokenBalanceRepo,
mockTokenPriceRepo,
- BASE_INFO_ENDPOINT_INVALID,
+ mockTokenInfoStore,
+ mockHttpService,
+ BASE_INFO_ENDPOINT,
BASE_PRICES_ENDPOINT,
)
@@ -223,10 +344,13 @@ describe("TokenService", () => {
pricingId: 2,
})
const mockTokens = [mockToken1, mockToken2]
+ mockTokenInfoStore.get.mockResolvedValueOnce({})
mockTokenRepo.get.mockResolvedValueOnce(mockTokens)
- await expect(
- invalidTokenService.fetchTokensFromBackend(mockNetworkId),
- ).rejects.toThrowError(new TokenError({ code: "TOKEN_PARSING_ERROR" }))
+ const result =
+ await invalidTokenService.getTokensInfoFromBackendForNetwork(
+ mockNetworkId,
+ )
+ expect(result).toBeUndefined()
})
})
@@ -360,6 +484,12 @@ describe("TokenService", () => {
pricingId: i + 1,
}),
)
+ mockHttpService.get.mockResolvedValueOnce({
+ prices: [
+ getMockTokenPriceDetails({ pricingId: 1, ethValue: "0.32" }),
+ getMockTokenPriceDetails({ pricingId: 2, ethValue: "0.64" }),
+ ],
+ })
mockTokenPriceRepo.get.mockResolvedValueOnce([])
const result = await tokenService.fetchTokenPricesFromBackend(
mockTokens,
@@ -386,6 +516,12 @@ describe("TokenService", () => {
pricingId: i + 1,
}),
)
+ mockHttpService.get.mockResolvedValueOnce({
+ prices: [
+ getMockTokenPriceDetails({ pricingId: 1, ethValue: "0.32" }),
+ getMockTokenPriceDetails({ pricingId: 2, ethValue: "0.64" }),
+ ],
+ })
mockTokenPriceRepo.get.mockResolvedValueOnce(mockTokenPrices)
const result = await tokenService.fetchTokenPricesFromBackend(
mockTokens,
@@ -726,155 +862,4 @@ describe("TokenService", () => {
[`${mockAccount.address}:${mockAccount.networkId}`]: "2200",
})
})
-
- test("getFeeTokens returns the correct fee tokens and respects order preference", async () => {
- const mockAccount = {
- classHash: TXV3_ACCOUNT_CLASS_HASH as Address,
- address: randomAddress1,
- networkId: defaultNetwork.id,
- }
- const mockNetwork = getMockNetwork()
- const mockBaseTokens = [
- getMockBaseToken({ networkId: mockNetwork.id }),
- getMockBaseToken({ address: "0x456", networkId: mockNetwork.id }),
- ]
-
- const mockTokens = [
- getMockTokenWithBalance({
- ...mockBaseTokens[0],
- symbol: "ETH",
- balance: BigInt(10e17).toString(),
- account: mockAccount,
- address: ETH_TOKEN_ADDRESS,
- }),
- getMockTokenWithBalance({
- ...mockBaseTokens[1],
- balance: BigInt(10e16).toString(),
- account: mockAccount,
- }),
-
- getMockTokenWithBalance({
- ...mockBaseTokens[1],
- balance: BigInt(20e18).toString(),
- symbol: "STRK",
- account: mockAccount,
- address: STRK_TOKEN_ADDRESS,
- }),
- ]
-
- mockNetworkService.getById = vi.fn().mockResolvedValueOnce(mockNetwork)
- mockTokenBalanceRepo.get.mockResolvedValueOnce(mockTokens)
- mockTokenRepo.get.mockResolvedValueOnce(mockTokens)
-
- const result = await tokenService.getFeeTokens(mockAccount)
- expect(result).toEqual([mockTokens[2], mockTokens[0]])
- })
-
- test("should return the correct fee tokens given a mock account", async () => {
- const mockAccount = {
- classHash: TXV3_ACCOUNT_CLASS_HASH as Address,
- address: randomAddress1,
- networkId: defaultNetwork.id,
- }
- const mockNetwork = getMockNetwork()
- const mockBaseTokens = [
- getMockBaseToken({ networkId: mockNetwork.id }),
- getMockBaseToken({ address: "0x456", networkId: mockNetwork.id }),
- ]
-
- const mockTokens = [
- getMockTokenWithBalance({
- ...mockBaseTokens[1],
- balance: BigInt(20e18).toString(),
- symbol: "STRK",
- account: mockAccount,
- address: STRK_TOKEN_ADDRESS,
- }),
- getMockTokenWithBalance({
- ...mockBaseTokens[0],
- balance: BigInt(10e17).toString(),
- account: mockAccount,
- symbol: "ETH",
- address: ETH_TOKEN_ADDRESS,
- }),
- ]
-
- mockNetworkService.getById = vi.fn().mockResolvedValueOnce(mockNetwork)
- mockTokenBalanceRepo.get.mockResolvedValueOnce(mockTokens)
- mockTokenRepo.get.mockResolvedValueOnce(mockTokens)
-
- const result = await tokenService.getFeeTokens(mockAccount)
- expect(result).toEqual(mockTokens)
- })
-
- test("getBestFeeToken returns the correct fee token", async () => {
- const mockAccount = {
- classHash: "0x123" as Address,
- address: randomAddress1,
- networkId: defaultNetwork.id,
- }
- const mockNetwork = getMockNetwork()
- const mockBaseTokens = [
- getMockBaseToken({ networkId: mockNetwork.id }),
- getMockBaseToken({ address: "0x456", networkId: mockNetwork.id }),
- ]
-
- const mockTokens = [
- getMockTokenWithBalance({
- ...mockBaseTokens[0],
- balance: BigInt(10e17).toString(),
- account: mockAccount,
- address: ETH_TOKEN_ADDRESS,
- }),
- getMockTokenWithBalance({
- ...mockBaseTokens[1],
- balance: BigInt(10e16).toString(),
- account: mockAccount,
- }),
- ]
-
- mockNetworkService.getById = vi.fn().mockResolvedValueOnce(mockNetwork)
- mockTokenBalanceRepo.get.mockResolvedValueOnce(mockTokens)
- mockTokenRepo.get.mockResolvedValueOnce(mockTokens)
-
- const result = await tokenService.getBestFeeToken(mockAccount)
- expect(result).toEqual(mockTokens[0])
- })
-
- test("getBestFeeToken returns the token with the highest balance", async () => {
- const mockAccount = {
- classHash: TXV3_ACCOUNT_CLASS_HASH as Address,
- address: randomAddress1,
- networkId: defaultNetwork.id,
- }
- const mockNetwork = getMockNetwork()
- const mockBaseTokens = [
- getMockBaseToken({ networkId: mockNetwork.id }),
- getMockBaseToken({ address: "0x456", networkId: mockNetwork.id }),
- ]
-
- const mockTokens = [
- getMockTokenWithBalance({
- ...mockBaseTokens[0],
- balance: BigInt(10e17).toString(),
- account: mockAccount,
- symbol: "ETH",
- address: ETH_TOKEN_ADDRESS,
- }),
- getMockTokenWithBalance({
- ...mockBaseTokens[1],
- balance: BigInt(10e18).toString(),
- symbol: "STRK",
- account: mockAccount,
- address: STRK_TOKEN_ADDRESS,
- }),
- ]
-
- mockNetworkService.getById = vi.fn().mockResolvedValueOnce(mockNetwork)
- mockTokenBalanceRepo.get.mockResolvedValueOnce(mockTokens)
- mockTokenRepo.get.mockResolvedValueOnce(mockTokens)
-
- const result = await tokenService.getBestFeeToken(mockAccount)
- expect(result).toEqual(mockTokens[1])
- })
})
diff --git a/packages/extension/src/shared/token/__new/service/implementation.ts b/packages/extension/src/shared/token/__new/service/implementation.ts
index 1f577f1d8..30c42f9a5 100644
--- a/packages/extension/src/shared/token/__new/service/implementation.ts
+++ b/packages/extension/src/shared/token/__new/service/implementation.ts
@@ -4,17 +4,15 @@ import { ITokenBalanceRepository } from "../repository/tokenBalance"
import { ITokenPriceRepository } from "../repository/tokenPrice"
import { ITokenService } from "./interface"
import {
- ApiTokenDataResponseSchema,
+ ApiAccountTokenBalances,
BaseToken,
BaseTokenSchema,
Token,
+ apiAccountTokenBalancesSchema,
} from "../types/token.model"
import { convertTokenAmountToCurrencyValue, equalToken } from "../utils"
-import {
- BaseTokenWithBalance,
- TokenWithBalance,
-} from "../types/tokenBalance.model"
-import { BaseWalletAccount, WalletAccount } from "../../../wallet.model"
+import { BaseTokenWithBalance } from "../types/tokenBalance.model"
+import { BaseWalletAccount } from "../../../wallet.model"
import {
ApiPriceDataResponseSchema,
TokenPriceDetails,
@@ -22,21 +20,34 @@ import {
} from "../types/tokenPrice.model"
import { accountsEqual } from "../../../utils/accountsEqual"
import { groupBy, uniq } from "lodash-es"
-import { SelectorFn } from "../../../storage/__new/interface"
-import { bigDecimal, ensureArray, isEqualAddress } from "@argent/shared"
+import { IObjectStore, SelectorFn } from "../../../storage/__new/interface"
+import {
+ IHttpService,
+ addressSchema,
+ bigDecimal,
+ ensureArray,
+ stripAddressZeroPadding,
+} from "@argent/shared"
import { INetworkService } from "../../../network/service/interface"
import { getMulticallForNetwork } from "../../../multicall"
import { getProvider } from "../../../network/provider"
import { Network, defaultNetwork } from "../../../network"
-import { fetcherWithArgentApiHeadersForNetwork } from "../../../api/fetcher"
-import { getAccountIdentifier } from "../../../wallet.service"
import { TokenError } from "../../../errors/token"
import {
- classHashSupportsTxV3,
- feeTokenNeedsTxV3Support,
-} from "../../../network/txv3"
-
-const FEE_TOKEN_PREFERENCE_BY_SYMBOL = ["STRK", "ETH"]
+ ApiTokenInfo,
+ ApiTokensInfoResponse,
+ TokenInfoByNetwork,
+ apiTokensInfoResponseSchema,
+} from "../types/tokenInfo.model"
+import { RefreshInterval } from "../../../config"
+import retry from "async-retry"
+import { getDefaultNetworkId } from "../../../network/utils"
+import { ARGENT_API_BASE_URL } from "../../../api/constants"
+import { argentApiNetworkForNetwork } from "../../../api/headers"
+import urlJoin from "url-join"
+import { ProvisionActivityPayload } from "../../../activity/types"
+import vSTRK from "../../../../assets/vSTRK.json"
+import { hasDelegationActivity } from "../../../activity/utils/hasDelegationActivity"
/**
* TokenService class implements ITokenService interface.
@@ -58,6 +69,8 @@ export class TokenService implements ITokenService {
private readonly tokenRepo: ITokenRepository,
private readonly tokenBalanceRepo: ITokenBalanceRepository,
private readonly tokenPriceRepo: ITokenPriceRepository,
+ private readonly tokenInfoStore: IObjectStore,
+ private readonly httpService: IHttpService,
TOKENS_INFO_URL: string | undefined,
TOKENS_PRICES_URL: string | undefined,
) {
@@ -120,60 +133,52 @@ export class TokenService implements ITokenService {
}
/**
- * Fetch tokens from the backend.
+ * Lazy fetch tokens info from local storage or backend max RefreshInterval.VERY_SLOW
* @param {string} networkId - The network id.
- * @returns {Promise} - The fetched tokens.
+ * @returns {Promise} - The fetched tokens or undefined if there was an error or not default network
*/
- async fetchTokensFromBackend(networkId: string): Promise {
+ async getTokensInfoFromBackendForNetwork(
+ networkId: string,
+ ): Promise {
+ /** the backend currently only returns token info for its specific network */
const isDefaultNetwork = defaultNetwork.id === networkId
-
- const tokensOnNetwork = await this.tokenRepo.get(
- (t) => t.networkId === networkId,
- )
if (!isDefaultNetwork) {
- return tokensOnNetwork
+ return
+ }
+ const tokenInfoByNetwork = await this.tokenInfoStore.get()
+
+ /** if we have data and updatedAt within RefreshInterval.VERY_SLOW, then return that data */
+ if (
+ tokenInfoByNetwork &&
+ tokenInfoByNetwork[networkId] &&
+ tokenInfoByNetwork[networkId].data
+ ) {
+ if (
+ Date.now() - tokenInfoByNetwork[networkId].updatedAt <
+ RefreshInterval.SLOW * 1000
+ ) {
+ return tokenInfoByNetwork[networkId].data
+ }
}
- // Prepare a map to avoid find operation
- const tokenMap = new Map(
- tokensOnNetwork.map((t) => [getAccountIdentifier(t), t]),
+ /** fetch data and check it's valid format */
+ const response = await this.httpService.get(
+ this.TOKENS_INFO_URL,
)
-
- const fetcher = fetcherWithArgentApiHeadersForNetwork(networkId)
- const response = await fetcher(this.TOKENS_INFO_URL)
- const parsedResponse = ApiTokenDataResponseSchema.safeParse(response)
-
+ const parsedResponse = apiTokensInfoResponseSchema.safeParse(response)
if (!parsedResponse.success) {
- throw new TokenError({ code: "TOKEN_PARSING_ERROR" })
+ return
}
- const tokens = parsedResponse.data.tokens
- .filter(
- (t) =>
- t.popular ||
- tokensOnNetwork.some((tn) => equalToken(tn, { ...t, networkId })),
- )
- .map((token) => {
- const cached = tokenMap.get(
- getAccountIdentifier({ address: token.address, networkId }),
- )
- return {
- id: token.id,
- address: token.address,
- decimals: token.decimals || cached?.decimals || 18,
- name: token.name,
- symbol: token.symbol,
- iconUrl: token.iconUrl || cached?.iconUrl,
- pricingId: token.pricingId || cached?.pricingId,
- showAlways: cached?.showAlways,
- networkId,
- custom: cached?.custom,
- popular: token.popular || cached?.popular,
- tradable: token.tradable || cached?.tradable,
- }
- })
-
- return tokens
+ /** store and update the updatedAt timestamp */
+ const data = parsedResponse.data.tokens
+ await this.tokenInfoStore.set({
+ [networkId]: {
+ updatedAt: Date.now(),
+ data,
+ },
+ })
+ return data
}
/**
@@ -203,22 +208,27 @@ export class TokenService implements ITokenService {
const tokenBalances: BaseTokenWithBalance[] = []
for (const networkId in accountsGroupedByNetwork) {
- const tokensOnCurrentNetwork = tokensGroupedByNetwork[networkId] // filter tokens based on networkId
- const network = await this.networkService.getById(networkId)
- if (network.multicallAddress) {
- const balances = await this.fetchTokenBalancesWithMulticall(
- network,
- accountsGroupedByNetwork,
- tokensOnCurrentNetwork,
- )
- tokenBalances.push(...balances)
- } else {
- const balances = await this.fetchTokenBalancesWithoutMulticall(
- network,
- accountsArray,
- tokensOnCurrentNetwork,
- )
- tokenBalances.push(...balances)
+ try {
+ const tokensOnCurrentNetwork = tokensGroupedByNetwork[networkId] // filter tokens based on networkId
+ const network = await this.networkService.getById(networkId)
+ if (network.multicallAddress) {
+ const balances = await this.fetchTokenBalancesWithMulticall(
+ network,
+ accountsGroupedByNetwork,
+ tokensOnCurrentNetwork,
+ )
+ tokenBalances.push(...balances)
+ } else {
+ const balances = await this.fetchTokenBalancesWithoutMulticall(
+ network,
+ accountsArray,
+ tokensOnCurrentNetwork,
+ )
+ tokenBalances.push(...balances)
+ }
+ } catch (e) {
+ /** Catch error to be resilient to individual network failure */
+ console.error(`fetchTokenBalancesFromOnChain error on ${networkId}`, e)
}
}
return tokenBalances
@@ -311,8 +321,7 @@ export class TokenService implements ITokenService {
return tokenPrices
}
- const fetcher = fetcherWithArgentApiHeadersForNetwork(defaultNetwork.id)
- const response = await fetcher(this.TOKENS_PRICES_URL)
+ const response = await this.httpService.get(this.TOKENS_PRICES_URL)
const parsedResponse = ApiPriceDataResponseSchema.safeParse(response)
if (!parsedResponse.success) {
@@ -548,65 +557,83 @@ export class TokenService implements ITokenService {
return totalCurrencyBalanceForAccounts
}
- async getFeeTokens(
- account: BaseWalletAccount & Required>,
- ): Promise {
- const tokens = await this.getTokens()
- const network = await this.networkService.getById(account.networkId)
- const networkFeeTokens = tokens.filter((token) =>
- network.possibleFeeTokenAddresses.some((ft) =>
- isEqualAddress(ft, token.address),
- ),
- )
- const accountFeeTokens = networkFeeTokens.filter((token) => {
- if (feeTokenNeedsTxV3Support(token)) {
- return classHashSupportsTxV3(account.classHash)
- }
- return true
- })
- const feeTokenBalances = await this.getTokenBalancesForAccount(
- account,
- accountFeeTokens,
+ async fetchAccountTokenBalancesFromBackend(
+ account: BaseWalletAccount,
+ opts?: retry.Options,
+ ): Promise {
+ const defaultNetworkId = getDefaultNetworkId()
+ /** This service only works for the default network */
+ if (account.networkId !== defaultNetworkId) {
+ return []
+ }
+ const apiBaseUrl = ARGENT_API_BASE_URL
+ const argentApiNetwork = argentApiNetworkForNetwork(account.networkId)
+ if (!argentApiNetwork) {
+ return []
+ }
+
+ const url = urlJoin(
+ apiBaseUrl,
+ "activity",
+ "starknet",
+ argentApiNetwork,
+ "account",
+ stripAddressZeroPadding(account.address),
+ "balance",
)
- const feeTokensWithBalances: TokenWithBalance[] = accountFeeTokens.map(
- (token) => {
- const tokenBalance = feeTokenBalances.find((tb) =>
- equalToken(tb, token),
- ) ?? {
- balance: "0",
- account: { address: account.address, networkId: account.networkId },
+
+ /** retry until status is "initialised" */
+ const accountTokenBalances = await retry(
+ async (bail) => {
+ let response
+ try {
+ response = await this.httpService.get(url)
+ } catch (e) {
+ /** bail without retry if there is any fetching error */
+ bail(new Error("Error fetching"))
+ return []
}
- return {
- ...token,
- ...tokenBalance,
+ const parsedRespose = apiAccountTokenBalancesSchema.safeParse(response)
+ if (!parsedRespose.success) {
+ bail(new Error("Error parsing response"))
+ return []
}
+ if (parsedRespose.data.status !== "initialised") {
+ /** causes a retry */
+ throw new Error("Not initialised yet")
+ }
+ return parsedRespose.data.balances
+ },
+ {
+ /** seems to take 5-10 sec for initialised state */
+ retries: 5,
+ minTimeout: 5000,
+ ...opts,
},
)
- // sort by fee token preference defined in FEE_TOKEN_PREFERENCE_BY_SYMBOL
- return feeTokensWithBalances.sort((a, b) => {
- const [aIndex, bIndex] = [a, b].map((token) =>
- FEE_TOKEN_PREFERENCE_BY_SYMBOL.indexOf(token.symbol),
- )
- return aIndex === -1 ? 1 : bIndex === -1 ? -1 : aIndex - bIndex
- })
+
+ const baseTokenWithBalances: BaseTokenWithBalance[] =
+ accountTokenBalances.map((accountTokenBalance) => {
+ return {
+ address: accountTokenBalance.tokenAddress,
+ balance: accountTokenBalance.tokenBalance,
+ networkId: account.networkId,
+ account,
+ }
+ })
+
+ return baseTokenWithBalances
}
- async getBestFeeToken(
- account: BaseWalletAccount & Required>,
- ): Promise