(null)
- const [shouldDeploy, setShouldDeploy] = useState(false)
- const [deployClassHash, setDeployClassHash] = useState("")
- const [addNetworkError, setAddNetworkError] = useState("")
const [sessionSigner] = useState(utils.randomPrivateKey())
const [sessionAccount, setSessionAccount] = useState<
@@ -65,7 +43,7 @@ export const TokenDapp: FC<{
const buttonsDisabled = ["approve", "pending"].includes(transactionStatus)
useEffect(() => {
- ;(async () => {
+ const waitTx = async () => {
if (lastTransactionHash && transactionStatus === "pending") {
setTransactionError("")
try {
@@ -80,71 +58,9 @@ export const TokenDapp: FC<{
setTransactionError(message)
}
}
- })()
- }, [transactionStatus, lastTransactionHash])
-
- // if (network !== "goerli-alpha" && network !== "mainnet-alpha") {
- // return (
- // <>
- //
- // There is no demo token for this network, but you can deploy one and
- // add its address to this file:
- //
- //
- //
packages/dapp/src/token.service.ts
- //
- // >
- // )
- // }
-
- const handleMintSubmit = async (e: React.FormEvent) => {
- e.preventDefault()
- try {
- setTransactionStatus("approve")
-
- console.log("mint", mintAmount)
- const result = await mintToken(mintAmount)
- console.log(result)
-
- setLastTransactionHash(result.transaction_hash)
- setTransactionStatus("pending")
- } catch (e) {
- console.error(e)
- setTransactionStatus("idle")
- }
- }
-
- const handleTransferSubmit = async (e: React.FormEvent) => {
- try {
- e.preventDefault()
- setTransactionStatus("approve")
-
- const result = await transfer(transferTo, transferAmount)
- console.log(result)
-
- setLastTransactionHash(result.transaction_hash)
- setTransactionStatus("pending")
- } catch (e) {
- console.error(e)
- setTransactionStatus("idle")
- }
- }
-
- const handleSignSubmit = async (skipDeploy: boolean) => {
- try {
- setTransactionStatus("approve")
-
- console.log("sign", shortText)
- const result = await signMessage(shortText, skipDeploy)
- console.log(result)
-
- setLastSig(stark.formatSignature(result))
- setTransactionStatus("success")
- } catch (e) {
- console.error(e)
- setTransactionStatus("idle")
}
- }
+ waitTx()
+ }, [transactionStatus, lastTransactionHash])
const handleOpenSessionSubmit = async (e: React.FormEvent) => {
e.preventDefault()
@@ -188,7 +104,6 @@ export const TokenDapp: FC<{
account.address,
parseInputAmountToUint256("0.000000001"),
)
- console.log(result)
setLastTransactionHash(result.transaction_hash)
setTransactionStatus("pending")
@@ -198,376 +113,124 @@ export const TokenDapp: FC<{
}
}
- const handleDeclare = async (e: React.FormEvent) => {
- try {
- e.preventDefault()
- if (!contract) {
- throw new Error("No contract")
- }
- if (!classHash) {
- throw new Error("No class hash")
- }
- const payload: DeclareContractPayload = {
- contract,
- classHash,
- }
- if (casm) {
- payload.casm = casm
- delete payload.classHash
- }
- if (shouldDeploy) {
- const result = await declareAndDeploy(payload)
- console.log(result)
- setLastTransactionHash(result.deploy.transaction_hash)
- } else {
- const result = await declare(payload)
- console.log(result)
- setLastTransactionHash(result.transaction_hash)
- }
- setTransactionStatus("pending")
- } catch (e) {
- console.error(e)
- setTransactionStatus("idle")
- }
- }
-
- const contractIsSierra = useMemo(() => {
- return contract && isSierra(contract)
- }, [contract])
-
- const handleDeploy = async (e: React.FormEvent) => {
- try {
- e.preventDefault()
- if (!deployClassHash) {
- throw new Error("No class hash")
- }
- const payload: UniversalDeployerContractPayload = {
- classHash: deployClassHash,
- }
- const result = await deploy(payload)
- console.log(result)
- setLastTransactionHash(result.transaction_hash)
- setTransactionStatus("pending")
- } catch (e) {
- console.error(e)
- setTransactionStatus("idle")
- }
- }
-
- const handleAddNetwork = async () => {
- await addNetwork({
- id: "dapp-test",
- chainId: "SN_DAPP_TEST",
- chainName: "Test chain name",
- baseUrl: "http://localhost:5050",
- })
- }
-
return (
<>
-
- Transaction status: {transactionStatus}
-
+
{lastTransactionHash && (
-
+ <>
+
+
+
+ >
)}
{transactionError && (
-
- Transaction error:{" "}
-
-
+
+ Transaction error:
+
+
)}
-
-
-
-
-
-
- {showSession && (
-
- )}
-
-
-
-
-
-
-
-
ERC20
- ETH token address
-
-
-
- {truncateAddress(ETHTokenAddress)}
-
-
-
-
-
-
-
{addTokenError}
-
-
-
Network
-
-
- {addNetworkError}
-
-
+ ) : (
+
+ )}
+
+ {showSession && (
+ <>
+
+ >
+ )}
+ {!starknetReact && (
+
+
+
+
+ )}
+ {!starknetReact && (
+
+
+
+
+ )}
+
>
)
}
diff --git a/packages/dapp/src/components/Transfer.tsx b/packages/dapp/src/components/Transfer.tsx
new file mode 100644
index 000000000..0b3ce7bec
--- /dev/null
+++ b/packages/dapp/src/components/Transfer.tsx
@@ -0,0 +1,85 @@
+import { Button, H2, Input } from "@argent/ui"
+import { Flex } from "@chakra-ui/react"
+import { FC, useState } from "react"
+import { transfer } from "../services/token.service"
+import { Status } from "../types/Status"
+
+interface TransferProps {
+ setTransactionStatus: (status: Status) => void
+ setLastTransactionHash: (status: string) => void
+ transactionStatus: Status
+}
+
+const Transfer: FC = ({
+ setTransactionStatus,
+ setLastTransactionHash,
+ transactionStatus,
+}) => {
+ const [transferTo, setTransferTo] = useState("")
+ const [transferAmount, setTransferAmount] = useState("1")
+
+ const buttonsDisabled =
+ ["approve", "pending"].includes(transactionStatus) ||
+ transferTo === "" ||
+ transferAmount === ""
+
+ const handleTransferSubmit = async (e: React.FormEvent) => {
+ try {
+ e.preventDefault()
+ setTransactionStatus("approve")
+ const { transaction_hash } = await transfer(transferTo, transferAmount)
+ setLastTransactionHash(transaction_hash)
+ setTransactionStatus("pending")
+ setTransferAmount("")
+ } catch (e) {
+ console.error(e)
+ setTransactionStatus("idle")
+ }
+ }
+
+ return (
+
+
+ Transfer token
+
+ setTransferTo(e.target.value)}
+ />
+
+ setTransferAmount(e.target.value)}
+ />
+
+
+
+
+ )
+}
+
+export { Transfer }
diff --git a/packages/dapp/src/components/TransferWithStarknetReact.tsx b/packages/dapp/src/components/TransferWithStarknetReact.tsx
new file mode 100644
index 000000000..f1d21e7c2
--- /dev/null
+++ b/packages/dapp/src/components/TransferWithStarknetReact.tsx
@@ -0,0 +1,104 @@
+import { bigDecimal } from "@argent/shared"
+import { Button, H2, Input } from "@argent/ui"
+import { Flex } from "@chakra-ui/react"
+import { useContractWrite } from "@starknet-react/core"
+import { FC, useMemo, useState } from "react"
+import { AccountInterface } from "starknet"
+import { ETHTokenAddress } from "../services/token.service"
+import { Status } from "../types/Status"
+
+interface TransferWithStarknetReactProps {
+ account: AccountInterface
+ setTransactionStatus: (status: Status) => void
+ setLastTransactionHash: (status: string) => void
+ transactionStatus: Status
+}
+
+const TransferWithStarknetReact: FC = ({
+ account,
+ setTransactionStatus,
+ setLastTransactionHash,
+ transactionStatus,
+}) => {
+ const [transferTo, setTransferTo] = useState("")
+ const [transferAmount, setTransferAmount] = useState("1")
+
+ const transferCalls = useMemo(() => {
+ return [
+ {
+ contractAddress: ETHTokenAddress,
+ entrypoint: "transfer",
+ calldata: [
+ account.address,
+ Number(bigDecimal.parseEther(transferAmount).value),
+ 0,
+ ],
+ },
+ ]
+ }, [account.address, transferAmount])
+
+ const { writeAsync: transferWithStarknetReact } = useContractWrite({
+ calls: transferCalls,
+ })
+
+ const buttonsDisabled = ["approve", "pending"].includes(transactionStatus)
+
+ const handleTransferSubmit = async (e: React.FormEvent) => {
+ try {
+ e.preventDefault()
+ setTransactionStatus("approve")
+ const { transaction_hash } = await transferWithStarknetReact()
+ setLastTransactionHash(transaction_hash)
+ setTransactionStatus("pending")
+ } catch (e) {
+ console.error(e)
+ setTransactionStatus("idle")
+ }
+ }
+
+ return (
+
+
+ Transfer token
+
+ setTransferTo(e.target.value)}
+ />
+
+ setTransferAmount(e.target.value)}
+ />
+
+
+
+
+ )
+}
+
+export { TransferWithStarknetReact }
diff --git a/packages/dapp/src/pages/_app.tsx b/packages/dapp/src/pages/_app.tsx
index 8382e6549..f8dd18f80 100644
--- a/packages/dapp/src/pages/_app.tsx
+++ b/packages/dapp/src/pages/_app.tsx
@@ -1,9 +1,36 @@
-import "../styles/globals.css"
-
import type { AppProps } from "next/app"
+import { ThemeProvider } from "@argent/ui"
+import { FC } from "react"
+import { useColorModeValue } from "@chakra-ui/react"
+
+const GlobalStyle: FC = () => {
+ const bgColor = useColorModeValue("#F9F9F9", "neutrals.1000")
+ return (
+ <>
+
+ >
+ )
+}
function MyApp({ Component, pageProps }: AppProps) {
- return
+ return (
+
+
+
+
+ )
}
export default MyApp
diff --git a/packages/dapp/src/pages/_document.tsx b/packages/dapp/src/pages/_document.tsx
new file mode 100644
index 000000000..1acf6f887
--- /dev/null
+++ b/packages/dapp/src/pages/_document.tsx
@@ -0,0 +1,34 @@
+import { theme } from "@argent/ui"
+import { ColorModeScript } from "@chakra-ui/react"
+import NextDocument, { Head, Html, Main, NextScript } from "next/document"
+
+export default class Document extends NextDocument {
+ render() {
+ return (
+
+
+ {/* eslint-disable-next-line @next/next/no-title-in-document-head */}
+ Example dapp
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
diff --git a/packages/dapp/src/pages/index.tsx b/packages/dapp/src/pages/index.tsx
index 0fb272ae1..0496dc1c0 100644
--- a/packages/dapp/src/pages/index.tsx
+++ b/packages/dapp/src/pages/index.tsx
@@ -1,49 +1,56 @@
-import { StarknetWindowObject } from "@argent/get-starknet"
import { supportsSessions } from "@argent/x-sessions"
-import type { NextPage } from "next"
-import Head from "next/head"
+import type { StarknetWindowObject } from "get-starknet-core"
import { useCallback, useEffect, useState } from "react"
import { AccountInterface } from "starknet"
+import { Header } from "../components/Header"
-import { TokenDapp } from "../components/TokenDapp"
+import { Button } from "@argent/ui"
+import { InfoRow } from "../components/InfoRow"
import { truncateAddress } from "../services/address.service"
import {
addWalletChangeListener,
- getChainId,
connectWallet,
+ disconnectWallet,
+ getChainId,
removeWalletChangeListener,
silentConnectWallet,
} from "../services/wallet.service"
-import styles from "../styles/Home.module.css"
+import { TokenDapp } from "../components/TokenDapp"
+import { Flex } from "@chakra-ui/react"
-const Home: NextPage = () => {
- const [address, setAddress] = useState()
+const StarknetKitDapp = () => {
const [supportSessions, setSupportsSessions] = useState(null)
const [chain, setChain] = useState(undefined)
const [isConnected, setConnected] = useState(false)
const [account, setAccount] = useState(null)
+ const [isSilentConnect, setIsSilentConnect] = useState(true)
useEffect(() => {
const handler = async () => {
- const wallet = await silentConnectWallet()
- setAddress(wallet?.selectedAddress)
- const chainId = await getChainId(wallet?.provider as any)
- setChain(chainId)
- setConnected(!!wallet?.isConnected)
- if (wallet?.account) {
- setAccount(wallet.account as any)
- }
- setSupportsSessions(null)
- if (wallet?.selectedAddress && wallet.provider) {
- try {
- const sessionSupport = await supportsSessions(
- wallet.selectedAddress,
- wallet.provider,
- )
- setSupportsSessions(sessionSupport)
- } catch {
- setSupportsSessions(false)
+ try {
+ const wallet = await silentConnectWallet()
+ const chainId = await getChainId(wallet?.provider as any)
+ setChain(chainId)
+ setConnected(!!wallet?.isConnected)
+ if (wallet?.account) {
+ setAccount(wallet.account as any)
+ }
+ setSupportsSessions(null)
+ if (wallet?.selectedAddress && wallet.provider) {
+ try {
+ const sessionSupport = await supportsSessions(
+ wallet.selectedAddress,
+ wallet.provider,
+ )
+ setSupportsSessions(sessionSupport)
+ } catch {
+ setSupportsSessions(false)
+ }
}
+ } catch (e) {
+ console.log(e)
+ } finally {
+ setIsSilentConnect(false)
}
}
@@ -66,7 +73,6 @@ const Home: NextPage = () => {
) =>
async () => {
const wallet = await connectWallet(enableWebWallet)
- setAddress(wallet?.selectedAddress)
const chainId = await getChainId(wallet?.provider as any)
setChain(chainId)
setConnected(!!wallet?.isConnected)
@@ -89,49 +95,79 @@ const Home: NextPage = () => {
[],
)
- return (
-
-
-
Test dapp
-
-
+ const handleDisconnect = useCallback(
+ () => async () => {
+ try {
+ await disconnectWallet()
+ setChain(undefined)
+ setConnected(false)
+ setAccount(null)
+ setSupportsSessions(null)
+ } catch (e) {
+ console.log(e)
+ }
+ },
+ [],
+ )
-
+ if (isSilentConnect) {
+ return (
+
+
+ Connecting wallet...
+
+
+ )
+ }
+
+ return (
+
+
{isConnected ? (
<>
-
- Wallet address: {address && truncateAddress(address)}
-
-
- supports sessions: {`${supportSessions}`}
-
-
- Url: {chain}
-
+
+
+
+
{account && (
-
+
)}
>
) : (
<>
-
+
-
- First connect wallet to use dapp.
+ Connect wallet with Starknetkit
+
>
)}
-
-
+
+
)
}
-export default Home
+export default StarknetKitDapp
diff --git a/packages/dapp/src/pages/starknetReactDapp.tsx b/packages/dapp/src/pages/starknetReactDapp.tsx
new file mode 100644
index 000000000..f25dcf1a9
--- /dev/null
+++ b/packages/dapp/src/pages/starknetReactDapp.tsx
@@ -0,0 +1,154 @@
+import { goerli } from "@starknet-react/chains"
+import {
+ Connector,
+ StarknetConfig,
+ publicProvider,
+ useAccount,
+ useConnect,
+ useDisconnect,
+} from "@starknet-react/core"
+import getConfig from "next/config"
+import {
+ ArgentMobileConnector,
+ isInArgentMobileAppBrowser,
+} from "starknetkit/argentMobile"
+import { InjectedConnector } from "starknetkit/injected"
+import { WebWalletConnector } from "starknetkit/webwallet"
+import { Header } from "../components/Header"
+
+import { Flex, Image } from "@chakra-ui/react"
+import React, { useEffect, useState } from "react"
+import { InfoRow } from "../components/InfoRow"
+import { TokenDapp } from "../components/TokenDapp"
+import { truncateAddress } from "../services/address.service"
+
+const { publicRuntimeConfig } = getConfig()
+const { webWalletUrl, argentMobileChainId } = publicRuntimeConfig
+
+export const availableConnectors = [
+ new InjectedConnector({ options: { id: "argentX" } }),
+ new InjectedConnector({ options: { id: "braavos" } }),
+ new ArgentMobileConnector({
+ dappName: "Example dapp",
+ chainId: argentMobileChainId,
+ }),
+ new WebWalletConnector({ url: webWalletUrl }),
+]
+
+const StarknetReactDappContent = () => {
+ const chains = [goerli]
+
+ const { account, status } = useAccount()
+ const { connect, connectors } = useConnect()
+ const { disconnect } = useDisconnect()
+ const [isClient, setIsClient] = useState(false)
+
+ /* 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
+ */
+ useEffect(() => {
+ setIsClient(true)
+ }, [])
+
+ if (!isClient) {
+ return <>>
+ }
+
+ const inAppBrowserFilter = (c: Connector) => {
+ if (isInArgentMobileAppBrowser()) {
+ return c.id === "argentX"
+ }
+ return c
+ }
+
+ return (
+ <>
+ {status === "connected" ? (
+ <>
+
+
+
+ {account && (
+
+ )}
+ >
+ ) : (
+ <>
+
+
+ {connectors.filter(inAppBrowserFilter).map((connector) => {
+ if (!connector.available()) {
+ return
+ }
+ const icon = connector.icon.dark ?? ""
+ const isSvg = icon?.startsWith("
+ )
+ })}
+
+ >
+ )}
+ >
+ )
+}
+
+const StarknetReactDapp = () => {
+ const chains = [goerli]
+ const providers = publicProvider()
+
+ return (
+
+
+
+
+
+
+
+ )
+}
+
+export default StarknetReactDapp
diff --git a/packages/dapp/src/services/wallet.service.ts b/packages/dapp/src/services/wallet.service.ts
index 0ad0c2bfd..dc5d51879 100644
--- a/packages/dapp/src/services/wallet.service.ts
+++ b/packages/dapp/src/services/wallet.service.ts
@@ -1,18 +1,29 @@
-import { StarknetWindowObject, connect } from "@argent/get-starknet"
-import type { AddStarknetChainParameters } from "get-starknet-core"
+import { createOffchainSession } from "@argent/x-sessions"
+import type {
+ AddStarknetChainParameters,
+ StarknetWindowObject,
+} from "get-starknet-core"
import {
AccountInterface,
DeclareContractPayload,
InvocationsDetails,
ProviderInterface,
UniversalDeployerContractPayload,
+ num,
shortString,
} from "starknet"
+import { connect, disconnect } from "starknetkit"
+import { ETHTokenAddress } from "./token.service"
+import getConfig from "next/config"
+import { Hex, bigDecimal } from "@argent/shared"
export type StarknetWindowObjectV5 = StarknetWindowObject & {
account: AccountInterface
}
+const { publicRuntimeConfig } = getConfig()
+const { webWalletUrl, argentMobileChainId } = publicRuntimeConfig
+
export let windowStarknet: StarknetWindowObjectV5 | null = null
export const starknetVersion = "v5"
@@ -20,6 +31,11 @@ export const starknetVersion = "v5"
export const silentConnectWallet = async () => {
const _windowStarknet = await connect({
modalMode: "neverAsk",
+ webWalletUrl,
+ argentMobileOptions: {
+ dappName: "Example dapp",
+ chainId: argentMobileChainId,
+ },
})
// 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
@@ -28,11 +44,13 @@ export const silentConnectWallet = async () => {
return windowStarknet ?? undefined
}
-export const connectWallet = async (enableWebWallet: boolean) => {
+export const connectWallet = async () => {
const _windowStarknet = await connect({
- exclude: enableWebWallet ? [] : ["argentWebWallet"],
- modalWalletAppearance: "all",
- enableArgentMobile: true,
+ webWalletUrl, // TODO: remove hardcoding
+ argentMobileOptions: {
+ dappName: "Example dapp",
+ chainId: argentMobileChainId,
+ },
})
// comment this when using webwallet -- enable is already done by @argent/get-starknet and webwallet is currently using only v4
@@ -42,6 +60,13 @@ export const connectWallet = async (enableWebWallet: boolean) => {
return windowStarknet ?? undefined
}
+export const disconnectWallet = async () => {
+ if (!windowStarknet?.isConnected) {
+ return
+ }
+ await disconnect()
+}
+
export const walletAddress = async (): Promise => {
if (!windowStarknet?.isConnected) {
return
@@ -107,6 +132,35 @@ export const signMessage = async (message: string, skipDeploy = false) => {
)
}
+export const createSessionKeys = async (
+ sessionKey: string,
+ approvedFees: string,
+ account: AccountInterface,
+) => {
+ if (!account) throw Error("starknet wallet not connected")
+
+ return await createOffchainSession(
+ {
+ sessionKey,
+ expirationTime: Math.floor((Date.now() + 1000 * 60 * 60 * 24) / 1000), // 1 day in seconds
+ allowedMethods: [
+ {
+ contractAddress: ETHTokenAddress,
+ method: "transfer",
+ },
+ ],
+ },
+ account,
+ {
+ tokenAddress: ETHTokenAddress, // Only used for test purposes in this dapp
+ maximumAmount: {
+ low: num.toHex(bigDecimal.parseUnits(approvedFees, 18).value) as Hex,
+ high: "0x0",
+ },
+ },
+ )
+}
+
export const waitForTransaction = async (hash: string) => {
if (!windowStarknet?.isConnected) {
return
diff --git a/packages/dapp/src/styles/Home.module.css b/packages/dapp/src/styles/Home.module.css
deleted file mode 100644
index 3446efeb9..000000000
--- a/packages/dapp/src/styles/Home.module.css
+++ /dev/null
@@ -1,102 +0,0 @@
-.container {
- padding: 0 2rem;
-}
-
-.main {
- min-height: 100vh;
- padding: 4rem 0;
- flex: 1;
- display: flex;
- flex-direction: column;
-}
-
-.connect {
- max-width: 300px;
-}
-
-.title a {
- color: #0070f3;
- text-decoration: none;
-}
-
-.title a:hover,
-.title a:focus,
-.title a:active {
- text-decoration: underline;
-}
-
-.title {
- font-size: 2rem;
-}
-
-.description {
- margin: 4rem 0;
- line-height: 1.5;
- font-size: 1.5rem;
-}
-
-.code {
- background: #fafafa;
- border-radius: 5px;
- padding: 0.75rem;
- font-size: 1.1rem;
- font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
- Bitstream Vera Sans Mono, Courier New, monospace;
-}
-
-.grid {
- display: flex;
- align-items: center;
- justify-content: center;
- flex-wrap: wrap;
- max-width: 800px;
-}
-
-.card {
- margin: 1rem;
- padding: 1.5rem;
- text-align: left;
- color: inherit;
- text-decoration: none;
- border: 1px solid #eaeaea;
- border-radius: 10px;
- transition: color 0.15s ease, border-color 0.15s ease;
- max-width: 300px;
-}
-
-.card:hover,
-.card:focus,
-.card:active {
- color: #0070f3;
- border-color: #0070f3;
-}
-
-.card h2 {
- margin: 0 0 1rem 0;
- font-size: 1.5rem;
-}
-
-.card p {
- margin: 0;
- font-size: 1.25rem;
- line-height: 1.5;
-}
-
-.logo {
- height: 1em;
- margin-left: 0.5rem;
-}
-
-.textarea {
- width: 100%;
- color: white;
- resize: vertical;
- min-height: 4em;
-}
-
-@media (max-width: 600px) {
- .grid {
- width: 100%;
- flex-direction: column;
- }
-}
diff --git a/packages/dapp/src/styles/globals.css b/packages/dapp/src/styles/globals.css
deleted file mode 100644
index 401999535..000000000
--- a/packages/dapp/src/styles/globals.css
+++ /dev/null
@@ -1,488 +0,0 @@
-* {
- box-sizing: border-box;
-}
-
-.columns {
- display: flex;
- grid-template-columns: 1fr 1fr;
- column-gap: 2em;
-}
-
-.columns > * {
- flex-basis: 0;
- flex-grow: 1;
-}
-
-/* MVP.css v1.8 - https://github.com/andybrewer/mvp */
-
-:root {
- --active-brightness: 0.85;
- --border-radius: 8px;
- --box-shadow: 0 2px 4px;
- --color: #118bee;
- --color-accent: #118bee15;
- --color-bg: #fff;
- --color-bg-secondary: #e9e9e9;
- --color-link: #118bee;
- --color-secondary: #920de9;
- --color-secondary-accent: #920de90b;
- --color-shadow: rgba(0, 0, 0, 0.1);
- --color-table: #118bee;
- --color-text: #000;
- --color-text-secondary: #999;
- --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
- Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
- --hover-brightness: 1.2;
- --justify-important: center;
- --justify-normal: left;
- --line-height: 1.5;
- --width-card: 285px;
- --width-card-medium: 460px;
- --width-card-wide: 800px;
- --width-content: 1080px;
-}
-
-@media (prefers-color-scheme: dark) {
- :root {
- --color: #0097fc;
- --color-accent: #0097fc4f;
- --color-bg: #333;
- --color-bg-secondary: #555;
- --color-link: #0097fc;
- --color-secondary: #e20de9;
- --color-secondary-accent: #e20de94f;
- --color-shadow: rgba(0, 0, 0, 0.2);
- --color-table: #0097fc;
- --color-text: #f7f7f7;
- --color-text-secondary: #aaa;
- }
-}
-
-/* Layout */
-article aside {
- background: var(--color-secondary-accent);
- border-left: 4px solid var(--color-secondary);
- padding: 0.01rem 0.8rem;
-}
-
-body {
- background: var(--color-bg);
- color: var(--color-text);
- font-family: var(--font-family);
- line-height: var(--line-height);
- margin: 0;
- overflow-x: hidden;
- padding: 0;
-}
-
-hr {
- background-color: var(--color-bg-secondary);
- border: none;
- height: 1px;
- margin: 4rem 0;
- width: 100%;
-}
-
-section {
- display: flex;
- flex-wrap: wrap;
- justify-content: var(--justify-important);
-}
-
-section img,
-article img {
- max-width: 100%;
-}
-
-section pre {
- overflow: auto;
-}
-
-section aside {
- border: 1px solid var(--color-bg-secondary);
- border-radius: var(--border-radius);
- box-shadow: var(--box-shadow) var(--color-shadow);
- margin: 1rem;
- padding: 1.25rem;
- width: var(--width-card);
-}
-
-section aside:hover {
- box-shadow: var(--box-shadow) var(--color-bg-secondary);
-}
-
-[hidden] {
- display: none;
-}
-
-/* Headers */
-article header,
-div header,
-main header {
- padding-top: 0;
-}
-
-header {
- text-align: var(--justify-important);
-}
-
-header a b,
-header a em,
-header a i,
-header a strong {
- margin-left: 0.5rem;
- margin-right: 0.5rem;
-}
-
-header nav img {
- margin: 1rem 0;
-}
-
-section header {
- padding-top: 0;
- width: 100%;
-}
-
-/* Nav */
-nav {
- align-items: center;
- display: flex;
- font-weight: bold;
- justify-content: space-between;
- margin-bottom: 7rem;
-}
-
-nav ul {
- list-style: none;
- padding: 0;
-}
-
-nav ul li {
- display: inline-block;
- margin: 0 0.5rem;
- position: relative;
- text-align: left;
-}
-
-/* Nav Dropdown */
-nav ul li:hover ul {
- display: block;
-}
-
-nav ul li ul {
- background: var(--color-bg);
- border: 1px solid var(--color-bg-secondary);
- border-radius: var(--border-radius);
- box-shadow: var(--box-shadow) var(--color-shadow);
- display: none;
- height: auto;
- left: -2px;
- padding: 0.5rem 1rem;
- position: absolute;
- top: 1.7rem;
- white-space: nowrap;
- width: auto;
- z-index: 1;
-}
-
-nav ul li ul::before {
- /* fill gap above to make mousing over them easier */
- content: "";
- position: absolute;
- left: 0;
- right: 0;
- top: -0.5rem;
- height: 0.5rem;
-}
-
-nav ul li ul li,
-nav ul li ul li a {
- display: block;
-}
-
-/* Typography */
-code,
-samp {
- background-color: var(--color-accent);
- border-radius: var(--border-radius);
- color: var(--color-text);
- display: inline-block;
- margin: 0 0.1rem;
- padding: 0 0.5rem;
-}
-
-details {
- margin: 1.3rem 0;
-}
-
-details summary {
- font-weight: bold;
- cursor: pointer;
-}
-
-h1,
-h2,
-h3,
-h4,
-h5,
-h6 {
- line-height: var(--line-height);
-}
-
-mark {
- padding: 0.1rem;
-}
-
-ol li,
-ul li {
- padding: 0.2rem 0;
-}
-
-p {
- margin: 0.75rem 0;
- padding: 0;
- width: 100%;
-}
-
-pre {
- margin: 1rem 0;
- max-width: var(--width-card-wide);
- padding: 1rem 0;
-}
-
-pre code,
-pre samp {
- display: block;
- max-width: var(--width-card-wide);
- padding: 0.5rem 2rem;
- white-space: pre-wrap;
-}
-
-small {
- color: var(--color-text-secondary);
-}
-
-sup {
- background-color: var(--color-secondary);
- border-radius: var(--border-radius);
- color: var(--color-bg);
- font-size: xx-small;
- font-weight: bold;
- margin: 0.2rem;
- padding: 0.2rem 0.3rem;
- position: relative;
- top: -2px;
-}
-
-/* Links */
-a {
- color: var(--color-link);
- display: inline-block;
- text-decoration: none;
-}
-
-a:active {
- filter: brightness(var(--active-brightness));
- text-decoration: underline;
-}
-
-a b,
-a em,
-a i,
-a strong,
-button {
- border-radius: var(--border-radius);
- display: inline-block;
- font-size: medium;
- font-weight: bold;
- line-height: var(--line-height);
- margin: 0.5rem 0;
- padding: 1rem 2rem;
-}
-
-button.flat {
- background: none;
- border: none;
- color: var(--color-link);
- cursor: pointer;
- font-weight: bold;
- padding: 0;
-}
-
-span.error-message {
- color: red;
- font-size: 16px;
- margin-left: 0.6em;
-}
-
-button {
- font-family: var(--font-family);
-}
-
-button:active {
- filter: brightness(var(--active-brightness));
-}
-
-button:hover {
- cursor: pointer;
- filter: brightness(var(--hover-brightness));
-}
-
-a b,
-a strong,
-button {
- background-color: var(--color-link);
- border: 2px solid var(--color-link);
- color: white;
-}
-
-a em,
-a i {
- border: 2px solid var(--color-link);
- border-radius: var(--border-radius);
- color: var(--color-link);
- display: inline-block;
- padding: 1rem 2rem;
-}
-
-article aside a {
- color: var(--color-secondary);
-}
-
-/* Images */
-figure {
- margin: 0;
- padding: 0;
-}
-
-figure img {
- max-width: 100%;
-}
-
-figure figcaption {
- color: var(--color-text-secondary);
-}
-
-/* Forms */
-
-button:disabled,
-input:disabled {
- background: var(--color-bg-secondary);
- border-color: var(--color-bg-secondary);
- color: var(--color-text-secondary);
- cursor: not-allowed;
-}
-
-button[disabled]:hover {
- filter: none;
-}
-
-form header {
- margin: 1.5rem 0;
- padding: 1.5rem 0;
-}
-
-input,
-label,
-select,
-textarea {
- display: block;
- font-size: inherit;
- max-width: var(--width-card-wide);
-}
-
-input[type="checkbox"],
-input[type="radio"] {
- display: inline-block;
-}
-
-input[type="checkbox"] + label,
-input[type="radio"] + label {
- display: inline-block;
- font-weight: normal;
- position: relative;
- top: 1px;
-}
-
-input,
-select,
-textarea {
- border: 1px solid var(--color-bg-secondary);
- border-radius: var(--border-radius);
- margin-bottom: 1rem;
- padding: 0.4rem 0.8rem;
-}
-
-input[readonly],
-textarea[readonly] {
- background-color: var(--color-bg-secondary);
-}
-
-label {
- font-weight: bold;
- margin-bottom: 0.2rem;
-}
-
-/* Tables */
-table {
- border: 1px solid var(--color-bg-secondary);
- border-radius: var(--border-radius);
- border-spacing: 0;
- display: inline-block;
- max-width: 100%;
- overflow-x: auto;
- padding: 0;
- white-space: nowrap;
-}
-
-table td,
-table th,
-table tr {
- padding: 0.4rem 0.8rem;
- text-align: var(--justify-important);
-}
-
-table thead {
- background-color: var(--color-table);
- border-collapse: collapse;
- border-radius: var(--border-radius);
- color: var(--color-bg);
- margin: 0;
- padding: 0;
-}
-
-table thead th:first-child {
- border-top-left-radius: var(--border-radius);
-}
-
-table thead th:last-child {
- border-top-right-radius: var(--border-radius);
-}
-
-table thead th:first-child,
-table tr td:first-child {
- text-align: var(--justify-normal);
-}
-
-table tr:nth-child(even) {
- background-color: var(--color-accent);
-}
-
-/* Quotes */
-blockquote {
- display: block;
- font-size: x-large;
- line-height: var(--line-height);
- margin: 1rem auto;
- max-width: var(--width-card-medium);
- padding: 1.5rem 1rem;
- text-align: var(--justify-important);
-}
-
-blockquote footer {
- color: var(--color-text-secondary);
- display: block;
- font-size: small;
- line-height: var(--line-height);
- padding: 1.5rem 0;
-}
diff --git a/packages/dapp/src/types/Status.ts b/packages/dapp/src/types/Status.ts
new file mode 100644
index 000000000..ebf92447f
--- /dev/null
+++ b/packages/dapp/src/types/Status.ts
@@ -0,0 +1 @@
+export type Status = "idle" | "approve" | "pending" | "success" | "failure"
diff --git a/packages/e2e/extension/src/config.ts b/packages/e2e/extension/src/config.ts
index 6a6335411..ca132f580 100644
--- a/packages/e2e/extension/src/config.ts
+++ b/packages/e2e/extension/src/config.ts
@@ -7,7 +7,7 @@ if (fs.existsSync(envPath)) {
dotenv.config({ path: envPath })
}
-export default {
+const config = {
password: "MyP@ss3!",
artifactsDir: path.resolve(__dirname, "../../artifacts/playwright"),
reportsDir: path.resolve(__dirname, "../../artifacts/reports"),
@@ -23,4 +23,14 @@ export default {
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,
}
+
+// check that no value of config is undefined, otherwise throw error
+Object.entries(config).forEach(([key, value]) => {
+ if (value === undefined) {
+ throw new Error(`Missing ${key} config variable; check .env file`)
+ }
+})
+
+export default config
diff --git a/packages/e2e/extension/src/languages/ILanguage.ts b/packages/e2e/extension/src/languages/ILanguage.ts
index 86f15fd62..21c3a0888 100644
--- a/packages/e2e/extension/src/languages/ILanguage.ts
+++ b/packages/e2e/extension/src/languages/ILanguage.ts
@@ -17,6 +17,9 @@ export interface ILanguage {
create: string
cancel: string
privacyStatement: string
+ approve: string
+ addArgentShield: string
+ dismiss: string
reviewSend: string
}
account: {
@@ -34,6 +37,7 @@ export interface ILanguage {
saveAddress: string
confirmTheSeedPhrase: string
showAccountRecovery: string
+ deployFirst: string
wrongPassword: string
invalidStarkIdError: string
shortAddressError: string
diff --git a/packages/e2e/extension/src/languages/en/index.ts b/packages/e2e/extension/src/languages/en/index.ts
index 5472f525e..2990985cf 100644
--- a/packages/e2e/extension/src/languages/en/index.ts
+++ b/packages/e2e/extension/src/languages/en/index.ts
@@ -18,6 +18,9 @@ const texts = {
cancel: "Cancel",
privacyStatement:
"GDPR statement for browser extension wallet: Argent takes the privacy and security of individuals very seriously and takes every reasonable measure and precaution to protect and secure the personal data that we process. The browser extension wallet does not collect any personal information nor does it correlate any of your personal information with anonymous data processed as part of its services. On top of this Argent has robust information security policies and procedures in place to make sure any processing complies with applicable laws. If you would like to know more or have any questions then please visit our website at https://www.argent.xyz/",
+ approve: "Approve",
+ addArgentShield: "Add Argent Shield",
+ dismiss: "Dismiss",
reviewSend: "Review send",
},
account: {
@@ -36,6 +39,8 @@ const texts = {
pendingTransactions: "Pending transactions",
recipientAddress: "Recipient's address",
saveAddress: "Save address",
+ deployFirst:
+ "You must deploy this account before Argent Shield can be added",
wrongPassword: "Incorrect password",
invalidStarkIdError: " not found",
shortAddressError: "Address must be 66 characters long",
diff --git a/packages/e2e/extension/src/page-objects/Account.ts b/packages/e2e/extension/src/page-objects/Account.ts
index baeea7d65..053694909 100644
--- a/packages/e2e/extension/src/page-objects/Account.ts
+++ b/packages/e2e/extension/src/page-objects/Account.ts
@@ -107,12 +107,19 @@ export default class Account extends Navigation {
return this.page.locator('[data-testid="account-tokens"] h2')
}
- invalidStarkIdError(id: string) {
+ /** FIME: revert to this function once testnet starknet id is returing a 'not found' state */
+ _invalidStarkIdError(id: string) {
return this.page.locator(
`form label:has-text('${id}${lang.account.invalidStarkIdError}')`,
)
}
+ invalidStarkIdError(_id: string) {
+ return this.page.locator(
+ `form label:has-text('Could not get address from stark name')`,
+ )
+ }
+
get shortAddressError() {
return this.page.locator(
`form label:has-text('${lang.account.shortAddressError}')`,
@@ -213,22 +220,26 @@ export default class Account extends Navigation {
return parseFloat(fee.split(" ")[0])
}
- async transferAmount() {
- /*
- https://argent.atlassian.net/browse/BLO-1713
- const sendTitleText = await this.page
- .locator('[data-testid="send-title"]')
- .innerText()
- const sendTitleAmount = sendTitleText.split(" ")[1]
- const balanceChange = await this.page
- .locator("[data-value]")
- .first()
- .getAttribute("data-value")
- expect(balanceChange).toBe(sendTitleAmount)
- */
- const amount = await this.page
- .locator('[data-testid="send-title"]')
- .getAttribute("data-value")
+ async txValidations(txAmount: 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 amount = await amountLocator
+ .getAttribute("data-testid")
+ .then((value) => parseInt(value!) / Math.pow(10, 18))
return amount
}
@@ -280,11 +291,11 @@ export default class Account extends Navigation {
}
await this.reviewSend.click()
- const trxAmount = await this.transferAmount()
+ const trxAmount = await this.txValidations(amount.toString())
if (submit) {
- await this.approve.click()
+ await this.confirm.click()
}
- return Math.abs(parseFloat(trxAmount!))
+ return trxAmount
}
async ensureTokenBalance({
@@ -405,6 +416,24 @@ export default class Account extends Navigation {
`navigator.clipboard.readText();`,
)
expect(seed).toBe(seedPhraseCopied)
- return seedPhraseCopied
+ return String(seedPhraseCopied)
+ }
+
+ // 2FA
+ get email() {
+ return this.page.locator('input[name="email"]')
+ }
+
+ get pinInput() {
+ return this.page.locator('[aria-label="Please enter your pin code"]')
+ }
+
+ async fillPin(pin: string) {
+ await this.pinInput.first().click()
+ await this.pinInput.first().fill(pin)
+ }
+
+ get deployNeededWarning() {
+ return this.page.locator(`p:has-text("${lang.account.deployFirst}")`)
}
}
diff --git a/packages/e2e/extension/src/page-objects/Dapps.ts b/packages/e2e/extension/src/page-objects/Dapps.ts
index 74465d681..c40f5c7e2 100644
--- a/packages/e2e/extension/src/page-objects/Dapps.ts
+++ b/packages/e2e/extension/src/page-objects/Dapps.ts
@@ -42,7 +42,10 @@ export default class Dapps extends Navigation {
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
.locator('div :text-matches("Connect Wallet", "i")')
.first()
diff --git a/packages/e2e/extension/src/page-objects/DeveloperSettings.ts b/packages/e2e/extension/src/page-objects/DeveloperSettings.ts
index 23978b04f..54a2ec17a 100644
--- a/packages/e2e/extension/src/page-objects/DeveloperSettings.ts
+++ b/packages/e2e/extension/src/page-objects/DeveloperSettings.ts
@@ -46,6 +46,10 @@ export default class DeveloperSettings {
return this.page.locator('[name="sequencerUrl"]')
}
+ get rpcUrl() {
+ return this.page.locator('[name="rpcUrl"]')
+ }
+
get create() {
return this.page.locator('button[type="submit"]')
}
diff --git a/packages/e2e/extension/src/page-objects/ExtensionPage.ts b/packages/e2e/extension/src/page-objects/ExtensionPage.ts
index 6f0dc5cf3..c31c8ff16 100644
--- a/packages/e2e/extension/src/page-objects/ExtensionPage.ts
+++ b/packages/e2e/extension/src/page-objects/ExtensionPage.ts
@@ -83,17 +83,73 @@ export default class ExtensionPage {
await expect(this.network.networkSelector).toBeVisible()
}
- getClipboard() {
- return this.page.evaluate(`navigator.clipboard.readText()`)
+ async getClipboard() {
+ return String(await this.page.evaluate(`navigator.clipboard.readText()`))
}
- async deployAccountByName(accountName: string) {
+ async addAccount() {
+ await this.account.addAccount({ firstAccount: false })
+ await this.account.copyAddress.click()
+ const accountAddress = await this.getClipboard()
+ expect(accountAddress).toMatch(/^0x0/)
+ return accountAddress
+ }
+
+ async deployAccount(accountName: string) {
+ if (accountName) {
+ await this.account.ensureSelectedAccount(accountName)
+ }
await this.navigation.showSettings.click()
await this.page.locator(`text=${accountName}`).click()
- await this.account.deployAccount.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()
+ 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()
+ }
+
+ async activate2fa(accountName: string, email: string, pin = "111111") {
+ //await this.page.pause()
+ await this.account.ensureSelectedAccount(accountName)
+ await this.navigation.showSettings.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 Promise.all([
+ expect(this.activity.menuPendingTransactionsIndicator).toBeHidden(),
+ expect(
+ this.page.locator('[data-testid="shield-on-account-view"]'),
+ ).toBeVisible(),
+ ])
+ await this.navigation.showSettings.click()
+ await expect(
+ this.page.locator('[data-testid="shield-on-settings"]'),
+ ).toBeVisible()
+ await this.settings.account(accountName).click()
+ await expect(
+ this.page.locator('[data-testid="shield-switch"]'),
+ ).toBeEnabled()
+ await this.navigation.back.click()
+ await this.navigation.close.click()
}
async setupWallet({
@@ -132,7 +188,7 @@ export default class ExtensionPage {
`${acc.initialBalance} ETH`,
)
if (acc.deploy) {
- await this.deployAccountByName(`Account ${accIndex + 1}`)
+ await this.deployAccount(`Account ${accIndex + 1}`)
}
}
}
@@ -141,8 +197,25 @@ export default class ExtensionPage {
}
async validateTx(reciever: string, amount?: number) {
- console.log(reciever, amount)
await this.navigation.menuActivity.click()
+ if (amount) {
+ const activityAmount = await this.page
+ .locator("button[ data-tx-hash] [data-value]")
+ .first()
+ .textContent()
+ .then((text) => text?.replace(/[^0-9.]+/, ""))
+
+ if (amount.toString().length > 6) {
+ expect(activityAmount).toBe(
+ parseFloat(amount.toString())
+ .toFixed(4)
+ .toString()
+ .match(/[\d\\.]+[^0]+/)?.[0],
+ )
+ } else {
+ expect(activityAmount).toBe(parseFloat(amount.toString()).toString())
+ }
+ }
await this.activity.ensureNoPendingTransactions()
const txs = await this.activity.activityTxHashs()
await validateTx(txs[0]!, reciever, amount)
diff --git a/packages/e2e/extension/src/page-objects/Navigation.ts b/packages/e2e/extension/src/page-objects/Navigation.ts
index bbe871dd8..1ea755224 100644
--- a/packages/e2e/extension/src/page-objects/Navigation.ts
+++ b/packages/e2e/extension/src/page-objects/Navigation.ts
@@ -16,7 +16,7 @@ export default class Navigation {
return this.page.locator(`[aria-label="${lang.common.close}"]`)
}
- get approve() {
+ get confirm() {
return this.page.locator(`button:text-is("${lang.common.confirm}")`)
}
@@ -101,4 +101,16 @@ export default class Navigation {
get cancel() {
return this.page.locator(`button:text-is("${lang.common.cancel}")`)
}
+
+ get approve() {
+ return this.page.locator(`button:text-is("${lang.common.approve}")`)
+ }
+
+ get addArgentShield() {
+ return this.page.locator(`button:text-is("${lang.common.addArgentShield}")`)
+ }
+
+ get dismiss() {
+ return this.page.locator(`button:text-is("${lang.common.dismiss}")`)
+ }
}
diff --git a/packages/e2e/extension/src/page-objects/Settings.ts b/packages/e2e/extension/src/page-objects/Settings.ts
index da816b751..fdf42062d 100644
--- a/packages/e2e/extension/src/page-objects/Settings.ts
+++ b/packages/e2e/extension/src/page-objects/Settings.ts
@@ -40,6 +40,12 @@ export default class Settings {
)
}
+ get deployAccount() {
+ return this.page.locator(
+ `//button//*[text()="${lang.settings.deployAccount}"]`,
+ )
+ }
+
get hideAccount() {
return this.page.locator(
`//button//*[text()="${lang.settings.hideAccount}"]`,
@@ -69,14 +75,8 @@ export default class Settings {
return this.page.locator(`button :text-is("${accountName}")`)
}
- get deleteAccount() {
- return this.page.locator(
- `button :text-is("${lang.settings.deleteAccount}")`,
- )
- }
-
- get confirmDelete() {
- return this.page.locator(`button:text-is("${lang.settings.delete}")`)
+ argentShield() {
+ return this.page.locator('[data-testid="shield-switch"]')
}
get privateKey() {
diff --git a/packages/e2e/extension/src/specs/2FA.spec.ts b/packages/e2e/extension/src/specs/2FA.spec.ts
new file mode 100644
index 000000000..773cfda9d
--- /dev/null
+++ b/packages/e2e/extension/src/specs/2FA.spec.ts
@@ -0,0 +1,90 @@
+import { expect } from "@playwright/test"
+
+import test from "../test"
+import { expireBESession } from "../utils/common"
+import { v4 as uuid } from "uuid"
+import config from "../config"
+
+const generateEmail = () => `e2e_2fa_${uuid()}@mail.com`
+
+test.describe("2FA", () => {
+ 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.settings.account(extension.account.accountName1).click()
+ await extension.settings.argentShield().first().click()
+ await expect(extension.account.deployNeededWarning).toBeVisible()
+ })
+
+ test("User should be able to enable 2FA and transfer funds", async ({
+ extension,
+ }) => {
+ const email = generateEmail()
+ await extension.setupWallet({
+ accountsToSetup: [{ initialBalance: 0.002, deploy: true }],
+ })
+ await extension.activate2fa(extension.account.accountName1, email)
+ await extension.account.transfer({
+ originAccountName: extension.account.accountName1,
+ recipientAddress: config.senderAddr!,
+ tokenName: "Ethereum",
+ amount: "MAX",
+ })
+ await extension.activity.checkActivity(1)
+ })
+
+ test("Recover wallet with 2FA, authentication needed before create a TX", async ({
+ extension,
+ }) => {
+ const email = generateEmail()
+ const { seed } = await extension.setupWallet({
+ accountsToSetup: [{ initialBalance: 0.002, deploy: true }],
+ })
+ await extension.activate2fa(extension.account.accountName1, email)
+
+ await extension.resetExtension()
+ await extension.recoverWallet(seed)
+
+ await extension.account.token("Ethereum").click()
+ await extension.account.fillRecipientAddress({
+ recipientAddress: config.senderAddr!,
+ })
+ await extension.account.email.fill(email)
+ await extension.navigation.next.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.activity.checkActivity(1)
+ })
+
+ test("Session expired, authentication needed before create a TX", async ({
+ extension,
+ }) => {
+ const email = generateEmail()
+ await extension.setupWallet({
+ accountsToSetup: [{ initialBalance: 0.002, deploy: true }],
+ })
+ await extension.activate2fa(extension.account.accountName1, email)
+ await expireBESession(email)
+ await extension.account.token("Ethereum").click()
+ await extension.account.fillRecipientAddress({
+ recipientAddress: config.senderAddr!,
+ })
+ 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.activity.checkActivity(1)
+ })
+})
diff --git a/packages/e2e/extension/src/specs/network.spec.ts b/packages/e2e/extension/src/specs/network.spec.ts
index aa87569f4..281e70bd4 100644
--- a/packages/e2e/extension/src/specs/network.spec.ts
+++ b/packages/e2e/extension/src/specs/network.spec.ts
@@ -1,6 +1,7 @@
import { expect } from "@playwright/test"
import test from "../test"
+import config from "../config"
test.describe("Network", () => {
test("Available networks", async ({ extension }) => {
@@ -8,9 +9,9 @@ test.describe("Network", () => {
await extension.open()
await expect(extension.network.networkSelector).toBeVisible()
await extension.network.ensureAvailableNetworks([
- "Mainnet\nhttps://cloud.argent-api.com/v1/starknet/mainnet/rpc/v0.4",
- "Testnet\nhttps://cloud.argent-api.com/v1/starknet/goerli/rpc/v0.4",
- "Localhost 5050\nhttp://localhost:5050",
+ "Mainnet",
+ `Testnet`,
+ "Localhost 5050\nhttp://localhost:5050/rpc",
])
})
@@ -27,10 +28,7 @@ test.describe("Network", () => {
await extension.developerSettings.addNetwork.click()
await extension.developerSettings.networkName.fill("My Network")
await extension.developerSettings.chainId.fill("SN_GOERLI")
- await extension.developerSettings.sequencerUrl.fill(
- "https://alpha4.starknet.io",
- )
-
+ await extension.developerSettings.rpcUrl.fill(config.testnetRpcUrl!)
await extension.navigation.create.click()
await expect(
extension.developerSettings.networkByName("My Network"),
@@ -83,9 +81,7 @@ test.describe("Network", () => {
await extension.developerSettings.addNetwork.click()
await extension.developerSettings.networkName.fill("My Network")
await extension.developerSettings.chainId.fill("SN_GOERLI")
- await extension.developerSettings.sequencerUrl.fill(
- "https://alpha4.starknet.io",
- )
+ await extension.developerSettings.rpcUrl.fill(config.testnetRpcUrl!)
await extension.navigation.create.click()
await expect(
diff --git a/packages/e2e/extension/src/specs/sendPartialFunds.spec.ts b/packages/e2e/extension/src/specs/sendPartialFunds.spec.ts
index 7eb52ff23..2923f7a0f 100644
--- a/packages/e2e/extension/src/specs/sendPartialFunds.spec.ts
+++ b/packages/e2e/extension/src/specs/sendPartialFunds.spec.ts
@@ -14,7 +14,7 @@ test.describe("Send funds", () => {
tokenName: "Ethereum",
amount: 0.005,
})
- expect(amountTrx).toBe(0.005)
+
await extension.validateTx(accountAddresses[1], amountTrx)
await extension.navigation.menuTokens.click()
@@ -46,7 +46,7 @@ test.describe("Send funds", () => {
amount: 0.005,
fillRecipientAddress: "typing",
})
- expect(amountTrx).toBe(0.005)
+
await extension.validateTx(config.destinationAddress!, amountTrx)
await extension.navigation.menuTokens.click()
diff --git a/packages/e2e/extension/src/utils/account.ts b/packages/e2e/extension/src/utils/account.ts
index c248aad4b..f27d5ca6b 100644
--- a/packages/e2e/extension/src/utils/account.ts
+++ b/packages/e2e/extension/src/utils/account.ts
@@ -1,11 +1,10 @@
import {
Account,
- SequencerProvider,
- constants,
uint256,
num,
- GetTransactionReceiptResponse,
TransactionExecutionStatus,
+ RpcProvider,
+ constants,
} from "starknet"
import { bigDecimal } from "@argent/shared"
import { getBatchProvider } from "@argent/x-multicall"
@@ -17,12 +16,20 @@ export interface AccountsToSetup {
deploy?: boolean
}
-const provider = new SequencerProvider({
- network: constants.NetworkName.SN_GOERLI,
+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,
})
const tnkETH =
"0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7" // address of ETH
-const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
const maxRetries = 4
@@ -42,13 +49,6 @@ const formatAmount = (amount: string) => {
return parseInt(amount, 16) / Math.pow(10, 18)
}
-const getTransaction = async (tx: string) => {
- return fetch(
- `${config.starknetTestNetUrl}/feeder_gateway/get_transaction?transactionHash=${tx}`,
- { method: "GET" },
- )
-}
-
export async function transferEth(amount: string, to: string) {
console.log(
"########################### transferEth ##################################",
@@ -81,23 +81,9 @@ export async function transferEth(amount: string, to: string) {
calldata: [to, low, high],
})
txHash = tx.transaction_hash
- let txStatusResponse
- let hasExecutionStatus = false
- while (!hasExecutionStatus) {
- const txStatus = await getTransaction(tx.transaction_hash)
- txStatusResponse =
- (await txStatus.json()) as GetTransactionReceiptResponse
- hasExecutionStatus =
- "execution_status" in txStatusResponse
- ? Boolean(txStatusResponse.execution_status)
- : false
- if (!hasExecutionStatus) {
- console.log(
- `[TX awating execution_status] hash: ${tx.transaction_hash}, status: ${txStatusResponse.status}`,
- )
- await sleep(5000)
- }
- }
+ const txStatusResponse = await provider.waitForTransaction(
+ tx.transaction_hash,
+ )
if (
txStatusResponse &&
"execution_status" in txStatusResponse &&
@@ -121,12 +107,7 @@ export async function transferEth(amount: string, to: string) {
if ("revert_reason" in txStatusResponse) {
elements.push(`revert_reason: ${txStatusResponse.revert_reason}`)
}
- if ("transaction_failure_reason" in txStatusResponse) {
- elements.push(
- `transaction_failure_reason.error_message: ${txStatusResponse.transaction_failure_reason.error_message}`,
- )
- }
- elements.push(`status: ${txStatusResponse.status}`)
+ elements.push(`status: ${txStatusResponse.execution_status}`)
} else {
elements.push("unable to get tx status response")
}
diff --git a/packages/e2e/extension/src/utils/common.ts b/packages/e2e/extension/src/utils/common.ts
new file mode 100644
index 000000000..b18c36337
--- /dev/null
+++ b/packages/e2e/extension/src/utils/common.ts
@@ -0,0 +1,18 @@
+export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
+
+export const expireBESession = async (email: string) => {
+ const requestOptions = {
+ method: "GET",
+ }
+ const request = `${
+ process.env.ARGENT_API_BASE_URL
+ }/debug/expireCredentials?application=argentx&email=${encodeURIComponent(
+ email,
+ )}`
+ const response = await fetch(request, requestOptions)
+ if (response.status != 200) {
+ console.error(response.body)
+ throw new Error(`Error expiring session: ${request}`)
+ }
+ return response.status
+}
diff --git a/packages/e2e/package.json b/packages/e2e/package.json
index ae54f3966..81179d70a 100644
--- a/packages/e2e/package.json
+++ b/packages/e2e/package.json
@@ -6,12 +6,12 @@
"license": "MIT",
"devDependencies": {
"@argent/shared": "workspace:^",
- "@argent/x-multicall": "^7.0.8",
+ "@argent/x-multicall": "^7.0.12",
"@playwright/test": "^1.37.1",
"@types/node": "^20.5.7",
"@types/uuid": "^9.0.3",
"dotenv": "^16.3.1",
- "starknet": "5.19.5",
+ "starknet": "5.24.3",
"uuid": "^9.0.0"
},
"scripts": {
diff --git a/packages/extension/.env.example b/packages/extension/.env.example
index 4c1143a7e..4cae5919e 100644
--- a/packages/extension/.env.example
+++ b/packages/extension/.env.example
@@ -4,6 +4,7 @@ RAMP_API_KEY=
ARGENT_API_BASE_URL=
ARGENT_X_STATUS_URL=
ARGENT_X_ENVIRONMENT=
+ARGENT_TESTNET_RPC_URL=
# 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
diff --git a/packages/extension/CHANGELOG.md b/packages/extension/CHANGELOG.md
index 9779ba1da..30c553e61 100644
--- a/packages/extension/CHANGELOG.md
+++ b/packages/extension/CHANGELOG.md
@@ -1,5 +1,31 @@
# @argent-x/extension
+## 6.11.0
+
+### Minor Changes
+
+- 70460cf84: Release
+
+## 6.10.4
+
+### Patch Changes
+
+- d53a2219f: Release
+- Updated dependencies [d53a2219f]
+ - @argent/x-sessions@6.5.0
+
+## 6.10.3
+
+### Patch Changes
+
+- f0d269918: Release
+
+## 6.10.2
+
+### Patch Changes
+
+- 3de82abf4: Release
+
## 6.10.1
### Patch Changes
diff --git a/packages/extension/manifest/v2.json b/packages/extension/manifest/v2.json
index 4913cfbb5..60615a7dc 100644
--- a/packages/extension/manifest/v2.json
+++ b/packages/extension/manifest/v2.json
@@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/chrome-manifest.json",
"name": "Argent X",
"description": "The security of Ethereum with the scale of StarkNet",
- "version": "5.10.1",
+ "version": "5.11.0",
"manifest_version": 2,
"browser_action": {
"default_icon": {
@@ -15,9 +15,9 @@
},
"permissions": [
"alarms",
- "downloads",
"tabs",
"storage",
+ "unlimitedStorage",
"notifications",
"http://localhost/*",
"https://alpha4.starknet.io/*",
diff --git a/packages/extension/manifest/v3.json b/packages/extension/manifest/v3.json
index b177eba48..ba14cb01d 100644
--- a/packages/extension/manifest/v3.json
+++ b/packages/extension/manifest/v3.json
@@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/chrome-manifest.json",
"name": "Argent X",
"description": "The security of Ethereum with the scale of StarkNet",
- "version": "5.10.1",
+ "version": "5.11.0",
"manifest_version": 3,
"action": {
"default_icon": {
@@ -15,9 +15,9 @@
},
"permissions": [
"alarms",
- "downloads",
"tabs",
"storage",
+ "unlimitedStorage",
"notifications",
"http://localhost/*"
],
diff --git a/packages/extension/package.json b/packages/extension/package.json
index 2cc9d651f..2d5eb23eb 100644
--- a/packages/extension/package.json
+++ b/packages/extension/package.json
@@ -1,6 +1,6 @@
{
"name": "@argent-x/extension",
- "version": "6.10.1",
+ "version": "6.11.0",
"main": "index.js",
"private": true,
"license": "MIT",
@@ -10,12 +10,12 @@
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^14.0.0",
"@types/async-retry": "^1.4.5",
- "@types/chrome": "^0.0.246",
+ "@types/chrome": "^0.0.248",
"@types/fs-extra": "^11.0.1",
"@types/lodash-es": "^4.17.6",
"@types/object-hash": "^3.0.2",
"@types/react": "^18.0.0",
- "@types/react-copy-to-clipboard": "5.0.5",
+ "@types/react-copy-to-clipboard": "5.0.6",
"@types/react-dom": "^18.0.0",
"@types/react-measure": "^2.0.8",
"@types/semver": "^7.3.10",
@@ -78,8 +78,8 @@
"@argent/shared": "^6.3.3",
"@argent/stack-router": "^6.3.1",
"@argent/ui": "^6.3.1",
- "@argent/x-multicall": "^7.0.8",
- "@argent/x-sessions": "^6.3.1",
+ "@argent/x-multicall": "^7.0.12",
+ "@argent/x-sessions": "^6.5.0",
"@argent/x-swap": "^6.3.1",
"@argent/x-window": "^6.3.1",
"@chakra-ui/icons": "^2.0.15",
@@ -102,6 +102,7 @@
"@trpc/server": "^10.31.0",
"@vitest/coverage-istanbul": "^0.34.0",
"async-retry": "^1.3.3",
+ "bignumber.js": "^9.1.2",
"colord": "^2.9.3",
"dexie": "^3.2.2",
"dexie-react-hooks": "^1.1.1",
@@ -109,7 +110,7 @@
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"ethers": "^6.8.0",
- "jose": "^4.3.6",
+ "jose": "^5.0.0",
"jotai": "^2.0.4",
"lodash-es": "^4.17.21",
"micro-starknet": "^0.2.3",
@@ -125,8 +126,8 @@
"react-router-dom": "^6.0.1",
"react-select": "^5.4.0",
"react-textarea-autosize": "^8.3.4",
- "starknet": "5.19.5",
"semver": "^7.5.2",
+ "starknet": "5.24.3",
"starknet4": "npm:starknet@4.22.0",
"starknet4-deprecated": "npm:starknet@4.4.0",
"styled-components": "^5.3.5",
diff --git a/packages/extension/src/background/__new/procedures/account/deploy.ts b/packages/extension/src/background/__new/procedures/account/deploy.ts
index 55bfdffa6..8ea0820ea 100644
--- a/packages/extension/src/background/__new/procedures/account/deploy.ts
+++ b/packages/extension/src/background/__new/procedures/account/deploy.ts
@@ -1,5 +1,5 @@
import { baseWalletAccountSchema } from "../../../../shared/wallet.model"
-import { deployAccountAction } from "../../../accountDeploy"
+import { addDeployAccountAction } from "../../../accountDeploy"
import { openSessionMiddleware } from "../../middleware/session"
import { extensionOnlyProcedure } from "../permissions"
@@ -10,12 +10,13 @@ export const deployAccountProcedure = extensionOnlyProcedure
async ({
input,
ctx: {
- services: { actionService },
+ services: { actionService, wallet },
},
}) => {
- await deployAccountAction({
+ await addDeployAccountAction({
account: input,
actionService,
+ wallet,
})
},
)
diff --git a/packages/extension/src/background/__new/procedures/accountMessaging/cancelEscape.ts b/packages/extension/src/background/__new/procedures/accountMessaging/cancelEscape.ts
index 2d3701976..29e18fcfa 100644
--- a/packages/extension/src/background/__new/procedures/accountMessaging/cancelEscape.ts
+++ b/packages/extension/src/background/__new/procedures/accountMessaging/cancelEscape.ts
@@ -43,6 +43,7 @@ export const cancelEscapeProcedure = extensionOnlyProcedure
},
{
origin,
+ title: "Keep Argent Shield",
},
)
} catch (error) {
diff --git a/packages/extension/src/background/__new/procedures/accountMessaging/changeGuardian.ts b/packages/extension/src/background/__new/procedures/accountMessaging/changeGuardian.ts
index dd8915d38..bc127e5f2 100644
--- a/packages/extension/src/background/__new/procedures/accountMessaging/changeGuardian.ts
+++ b/packages/extension/src/background/__new/procedures/accountMessaging/changeGuardian.ts
@@ -5,6 +5,7 @@ import { baseWalletAccountSchema } from "../../../../shared/wallet.model"
import { constants, num, Account } from "starknet"
import { getEntryPointSafe } from "../../../../shared/utils/transactions"
import { AccountMessagingError } from "../../../../shared/errors/accountMessaging"
+import { changeGuardianCalldataSchema } from "@argent/shared"
const changeGuardianSchema = z.object({
guardian: z.string(),
@@ -24,7 +25,7 @@ export const changeGuardianProcedure = extensionOnlyProcedure
const newGuardian = num.hexToDecimalString(guardian)
const starknetAccount =
(await wallet.getSelectedStarknetAccount()) as Account // Old accounts are not supported
-
+ const isRemoveGuardian = num.toBigInt(newGuardian) === constants.ZERO
await actionService.add(
{
type: "TRANSACTION",
@@ -35,20 +36,26 @@ export const changeGuardianProcedure = extensionOnlyProcedure
"changeGuardian",
starknetAccount.cairoVersion,
),
- calldata: [newGuardian],
+ calldata: changeGuardianCalldataSchema.parse([newGuardian]),
},
meta: {
isChangeGuardian: true,
title: "Change account guardian",
- type:
- num.toBigInt(newGuardian) === constants.ZERO // if guardian is 0, it's a remove guardian action
- ? "REMOVE_ARGENT_SHIELD"
- : "ADD_ARGENT_SHIELD",
+ type: isRemoveGuardian // if guardian is 0, it's a remove guardian action
+ ? "REMOVE_ARGENT_SHIELD"
+ : "ADD_ARGENT_SHIELD",
},
},
},
{
origin,
+ title: isRemoveGuardian
+ ? "Remove Argent Shield"
+ : "Add Argent Shield",
+ icon: isRemoveGuardian
+ ? "ArgentShieldDeactivateIcon"
+ : "ArgentShieldIcon",
+ subtitle: "",
},
)
} catch (error) {
diff --git a/packages/extension/src/background/__new/procedures/accountMessaging/escapeAndChangeGuardian.ts b/packages/extension/src/background/__new/procedures/accountMessaging/escapeAndChangeGuardian.ts
index cb135e49d..4116a22c9 100644
--- a/packages/extension/src/background/__new/procedures/accountMessaging/escapeAndChangeGuardian.ts
+++ b/packages/extension/src/background/__new/procedures/accountMessaging/escapeAndChangeGuardian.ts
@@ -7,6 +7,10 @@ import { constants, num, Account } from "starknet"
import { getEntryPointSafe } from "../../../../shared/utils/transactions"
import { AccountMessagingError } from "../../../../shared/errors/accountMessaging"
import { AccountError } from "../../../../shared/errors/account"
+import {
+ changeGuardianCalldataSchema,
+ escapeGuardianCalldataSchema,
+} from "@argent/shared"
const escapeAndChangeGuardianSchema = z.object({
account: baseWalletAccountSchema,
@@ -60,7 +64,9 @@ export const escapeAndChangeGuardianProcedure = extensionOnlyProcedure
"changeGuardian",
starknetAccount.cairoVersion,
),
- calldata: [num.hexToDecimalString(constants.ZERO.toString())],
+ calldata: changeGuardianCalldataSchema.parse([
+ num.hexToDecimalString(constants.ZERO.toString()),
+ ]),
},
meta: {
isChangeGuardian: true,
@@ -77,11 +83,14 @@ export const escapeAndChangeGuardianProcedure = extensionOnlyProcedure
/**
* Call `escapeGuardian` to change guardian to this account publicKey
*/
+ // TODO figure out what should be the title here
/** Cairo 0 takes public key as argument, Cairo 1 does not */
let calldata: string[] = []
if (starknetAccount.cairoVersion === "0") {
- calldata = [num.hexToDecimalString(publicKey)]
+ calldata = escapeGuardianCalldataSchema.parse([
+ num.hexToDecimalString(publicKey),
+ ])
}
await actionService.add(
diff --git a/packages/extension/src/background/__new/procedures/accountMessaging/getAccountDeploymentPayload.ts b/packages/extension/src/background/__new/procedures/accountMessaging/getAccountDeploymentPayload.ts
new file mode 100644
index 000000000..230606fdb
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/accountMessaging/getAccountDeploymentPayload.ts
@@ -0,0 +1,56 @@
+import { z } from "zod"
+
+import { extensionOnlyProcedure } from "../permissions"
+import { baseWalletAccountSchema } from "../../../../shared/wallet.model"
+import { AccountMessagingError } from "../../../../shared/errors/accountMessaging"
+import { SessionError } from "../../../../shared/errors/session"
+import { AccountError } from "../../../../shared/errors/account"
+import {
+ addressSchema,
+ bigNumberishSchema,
+ rawArgsSchema,
+} from "@argent/shared"
+
+const getAccountDeploymentPayloadInputSchema = z.object({
+ account: baseWalletAccountSchema,
+})
+
+const deployAccountContractSchema = z.object({
+ classHash: z.string(),
+ constructorCalldata: rawArgsSchema,
+ addressSalt: bigNumberishSchema.optional(),
+ contractAddress: addressSchema.optional(),
+})
+
+export const getAccountDeploymentPayloadProcedure = extensionOnlyProcedure
+ .input(getAccountDeploymentPayloadInputSchema)
+ .output(deployAccountContractSchema)
+ .query(
+ async ({
+ input: { account },
+ ctx: {
+ services: { wallet },
+ },
+ }) => {
+ if (!(await wallet.isSessionOpen())) {
+ throw new SessionError({
+ code: "NO_OPEN_SESSION",
+ })
+ }
+ try {
+ const walletAccount = await wallet.getAccount(account)
+ if (!walletAccount) {
+ throw new AccountError({
+ code: "NOT_FOUND",
+ })
+ }
+
+ return await wallet.getAccountDeploymentPayload(walletAccount)
+ } catch (e) {
+ throw new AccountMessagingError({
+ options: { error: e },
+ code: "GET_ENCRYPTED_KEY_FAILED",
+ })
+ }
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/accountMessaging/index.ts b/packages/extension/src/background/__new/procedures/accountMessaging/index.ts
index 7d0906624..439332d75 100644
--- a/packages/extension/src/background/__new/procedures/accountMessaging/index.ts
+++ b/packages/extension/src/background/__new/procedures/accountMessaging/index.ts
@@ -8,6 +8,7 @@ import { escapeAndChangeGuardianProcedure } from "./escapeAndChangeGuardian"
import { getPublicKeyProcedure } from "./getPublicKey"
import { getNextPublicKeyForMultisigProcedure } from "./getNextPublicKeyForMultisig"
import { getPublicKeysBufferForMultisigProcedure } from "./getPublicKeysBufferForMultisig"
+import { getAccountDeploymentPayloadProcedure } from "./getAccountDeploymentPayload"
export const accountMessagingRouter = router({
getEncryptedPrivateKey: getEncryptedPrivateKeyProcedure,
@@ -19,4 +20,5 @@ export const accountMessagingRouter = router({
getPublicKey: getPublicKeyProcedure,
getNextPublicKeyForMultisig: getNextPublicKeyForMultisigProcedure,
getPublicKeysBufferForMultisig: getPublicKeysBufferForMultisigProcedure,
+ getAccountDeploymentPayload: getAccountDeploymentPayloadProcedure,
})
diff --git a/packages/extension/src/background/__new/procedures/accountMessaging/triggerEscapeGuardian.ts b/packages/extension/src/background/__new/procedures/accountMessaging/triggerEscapeGuardian.ts
index 8b2bb6802..9641c5509 100644
--- a/packages/extension/src/background/__new/procedures/accountMessaging/triggerEscapeGuardian.ts
+++ b/packages/extension/src/background/__new/procedures/accountMessaging/triggerEscapeGuardian.ts
@@ -42,6 +42,7 @@ export const triggerEscapeGuardianProcedure = extensionOnlyProcedure
},
},
{
+ title: "Trigger escape guardian",
origin,
},
)
diff --git a/packages/extension/src/background/__new/procedures/address/getAddressFromDomainName.ts b/packages/extension/src/background/__new/procedures/address/getAddressFromDomainName.ts
new file mode 100644
index 000000000..c9d9e87c9
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/address/getAddressFromDomainName.ts
@@ -0,0 +1,30 @@
+import { addressSchema, starknetDomainNameSchema } from "@argent/shared"
+import { z } from "zod"
+
+import { extensionOnlyProcedure } from "../permissions"
+import { getMulticallForNetwork } from "../../../../shared/multicall"
+
+const inputSchema = z.object({
+ domain: starknetDomainNameSchema,
+ networkId: z.string(),
+})
+
+export const getAddressFromDomainNameProcedure = extensionOnlyProcedure
+ .input(inputSchema)
+ .output(addressSchema)
+ .query(
+ async ({
+ input: { domain, networkId },
+ ctx: {
+ services: { starknetAddressService, networkService },
+ },
+ }) => {
+ const network = await networkService.getById(networkId)
+ const multicall = getMulticallForNetwork(network)
+ return starknetAddressService.getAddressFromDomainName(
+ domain,
+ networkId,
+ multicall,
+ )
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/address/index.ts b/packages/extension/src/background/__new/procedures/address/index.ts
new file mode 100644
index 000000000..67e04da4a
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/address/index.ts
@@ -0,0 +1,8 @@
+import { router } from "../../trpc"
+import { getAddressFromDomainNameProcedure } from "./getAddressFromDomainName"
+import { parseAddressOrDomainProcedure } from "./parseAddressOrDomain"
+
+export const addressRouter = router({
+ getAddressFromDomainName: getAddressFromDomainNameProcedure,
+ parseAddressOrDomain: parseAddressOrDomainProcedure,
+})
diff --git a/packages/extension/src/background/__new/procedures/address/parseAddressOrDomain.ts b/packages/extension/src/background/__new/procedures/address/parseAddressOrDomain.ts
new file mode 100644
index 000000000..7fcbfe32d
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/address/parseAddressOrDomain.ts
@@ -0,0 +1,30 @@
+import { addressSchema, addressOrDomainSchema } from "@argent/shared"
+import { z } from "zod"
+
+import { extensionOnlyProcedure } from "../permissions"
+import { getMulticallForNetwork } from "../../../../shared/multicall"
+
+const inputSchema = z.object({
+ addressOrDomain: addressOrDomainSchema,
+ networkId: z.string(),
+})
+
+export const parseAddressOrDomainProcedure = extensionOnlyProcedure
+ .input(inputSchema)
+ .output(addressSchema)
+ .query(
+ async ({
+ input: { addressOrDomain, networkId },
+ ctx: {
+ services: { starknetAddressService, networkService },
+ },
+ }) => {
+ const network = await networkService.getById(networkId)
+ const multicall = getMulticallForNetwork(network)
+ return starknetAddressService.parseAddressOrDomain(
+ addressOrDomain,
+ networkId,
+ multicall,
+ )
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/tokens/index.ts b/packages/extension/src/background/__new/procedures/tokens/index.ts
index a031eb8cf..7a232b412 100644
--- a/packages/extension/src/background/__new/procedures/tokens/index.ts
+++ b/packages/extension/src/background/__new/procedures/tokens/index.ts
@@ -6,6 +6,7 @@ import { getAccountBalanceProcedure } from "./getAccountBalance"
import { getAllTokenBalancesProcedure } from "./getAllTokenBalances"
import { getCurrencyValueForTokensProcedure } from "./getCurrencyValueForTokens"
import { removeTokenProcedure } from "./removeToken"
+import { swapProcedure } from "./swap"
export const tokensRouter = router({
addToken: addTokenProcedure,
@@ -15,4 +16,5 @@ export const tokensRouter = router({
getAccountBalance: getAccountBalanceProcedure,
getAllTokenBalances: getAllTokenBalancesProcedure,
getCurrencyValueForTokens: getCurrencyValueForTokensProcedure,
+ swap: swapProcedure,
})
diff --git a/packages/extension/src/background/__new/procedures/tokens/swap.ts b/packages/extension/src/background/__new/procedures/tokens/swap.ts
new file mode 100644
index 000000000..482c02700
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/tokens/swap.ts
@@ -0,0 +1,36 @@
+import { callSchema } from "@argent/shared"
+import { z } from "zod"
+
+import { extensionOnlyProcedure } from "../permissions"
+
+export const swapSchema = z.object({
+ transactions: z.union([callSchema, z.array(callSchema)]),
+ title: z.string(),
+})
+
+export const swapProcedure = extensionOnlyProcedure
+ .input(swapSchema)
+ .output(z.string())
+ .mutation(
+ async ({
+ input: { transactions, title },
+ ctx: {
+ services: { actionService },
+ },
+ }) => {
+ const { meta } = await actionService.add(
+ {
+ type: "TRANSACTION",
+ payload: {
+ transactions,
+ },
+ },
+ {
+ title,
+ icon: "SwapIcon",
+ },
+ )
+
+ return meta.hash
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/transactionReview/getLabels.ts b/packages/extension/src/background/__new/procedures/transactionReview/getLabels.ts
new file mode 100644
index 000000000..3688f38ac
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/transactionReview/getLabels.ts
@@ -0,0 +1,9 @@
+import { openSessionMiddleware } from "../../middleware/session"
+import { extensionOnlyProcedure } from "../permissions"
+
+export const getLabelsProcedure = extensionOnlyProcedure
+ .use(openSessionMiddleware)
+ .query(async ({ ctx: { services } }) => {
+ const { transactionReviewService } = services
+ return transactionReviewService.getLabels()
+ })
diff --git a/packages/extension/src/background/__new/procedures/transactionReview/index.ts b/packages/extension/src/background/__new/procedures/transactionReview/index.ts
new file mode 100644
index 000000000..23f706806
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/transactionReview/index.ts
@@ -0,0 +1,8 @@
+import { router } from "../../trpc"
+import { getLabelsProcedure } from "./getLabels"
+import { simulateAndReviewProcedure } from "./simulateAndReview"
+
+export const transactionReviewRouter = router({
+ simulateAndReview: simulateAndReviewProcedure,
+ getLabels: getLabelsProcedure,
+})
diff --git a/packages/extension/src/background/__new/procedures/transactionReview/simulateAndReview.ts b/packages/extension/src/background/__new/procedures/transactionReview/simulateAndReview.ts
new file mode 100644
index 000000000..beb12e616
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/transactionReview/simulateAndReview.ts
@@ -0,0 +1,17 @@
+import { z } from "zod"
+
+import { openSessionMiddleware } from "../../middleware/session"
+import { extensionOnlyProcedure } from "../permissions"
+import { transactionReviewTransactionsSchema } from "../../../../shared/transactionReview/interface"
+import { enrichedSimulateAndReviewSchema } from "../../../../shared/transactionReview/schema"
+
+const approveActionSchema = z.array(transactionReviewTransactionsSchema)
+
+export const simulateAndReviewProcedure = extensionOnlyProcedure
+ .use(openSessionMiddleware)
+ .input(approveActionSchema)
+ .output(enrichedSimulateAndReviewSchema)
+ .query(async ({ input, ctx: { services } }) => {
+ const { transactionReviewService } = services
+ return transactionReviewService.simulateAndReview({ transactions: input })
+ })
diff --git a/packages/extension/src/background/__new/procedures/transfer/send.ts b/packages/extension/src/background/__new/procedures/transfer/send.ts
index fde4c0854..56c64fa0b 100644
--- a/packages/extension/src/background/__new/procedures/transfer/send.ts
+++ b/packages/extension/src/background/__new/procedures/transfer/send.ts
@@ -9,6 +9,8 @@ const sendSchema = z.object({
entrypoint: z.string(),
calldata: z.string().array(),
}),
+ title: z.string(),
+ subtitle: z.string().optional(),
})
export const sendProcedure = extensionOnlyProcedure
@@ -16,17 +18,24 @@ export const sendProcedure = extensionOnlyProcedure
.output(z.string())
.mutation(
async ({
- input: { transactions },
+ input: { transactions, title, subtitle },
ctx: {
services: { actionService },
},
}) => {
- const { meta } = await actionService.add({
- type: "TRANSACTION",
- payload: {
- transactions,
+ const { meta } = await actionService.add(
+ {
+ type: "TRANSACTION",
+ payload: {
+ transactions,
+ },
},
- })
+ {
+ title,
+ subtitle,
+ icon: "SendIcon",
+ },
+ )
return meta.hash
},
diff --git a/packages/extension/src/background/__new/procedures/udc/declareContractProcedure.ts b/packages/extension/src/background/__new/procedures/udc/declareContractProcedure.ts
new file mode 100644
index 000000000..e03c7c4e8
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/udc/declareContractProcedure.ts
@@ -0,0 +1,42 @@
+import { z } from "zod"
+
+import { extensionOnlyProcedure } from "../permissions"
+import { UdcError } from "../../../../shared/errors/udc"
+
+import { declareContractSchema } from "../../../../shared/udc/type"
+
+export const declareContractProcedure = extensionOnlyProcedure
+ .input(declareContractSchema)
+ .output(z.string())
+ .mutation(
+ async ({
+ input: { address, networkId, ...rest },
+ ctx: {
+ services: { actionService, wallet },
+ },
+ }) => {
+ if (address && networkId) {
+ await wallet.selectAccount({
+ address,
+ networkId,
+ })
+ }
+ try {
+ const action = await actionService.add(
+ {
+ type: "DECLARE_CONTRACT",
+ payload: {
+ ...rest,
+ },
+ },
+ {
+ origin,
+ icon: "DocumentIcon",
+ },
+ )
+ return action.meta.hash
+ } catch (e) {
+ throw new UdcError({ code: "NO_DEPLOY_CONTRACT" })
+ }
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/udc/deployContractProcedure.ts b/packages/extension/src/background/__new/procedures/udc/deployContractProcedure.ts
new file mode 100644
index 000000000..2bebffa1c
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/udc/deployContractProcedure.ts
@@ -0,0 +1,51 @@
+import { z } from "zod"
+
+import { extensionOnlyProcedure } from "../permissions"
+import { UdcError } from "../../../../shared/errors/udc"
+
+const deployContractSchema = z.object({
+ address: z.string(),
+ networkId: z.string(),
+ classHash: z.string(),
+ constructorCalldata: z.array(z.string()),
+ salt: z.string().optional(),
+ unique: z.boolean().optional(),
+})
+
+export const deployContractProcedure = extensionOnlyProcedure
+ .input(deployContractSchema)
+ .mutation(
+ async ({
+ input: {
+ address,
+ networkId,
+ classHash,
+ constructorCalldata,
+ salt,
+ unique,
+ },
+ ctx: {
+ services: { actionService, wallet },
+ },
+ }) => {
+ await wallet.selectAccount({ address, networkId })
+ try {
+ await actionService.add(
+ {
+ type: "DEPLOY_CONTRACT",
+ payload: {
+ classHash: classHash.toString(),
+ constructorCalldata,
+ salt,
+ unique,
+ },
+ },
+ {
+ icon: "DocumentIcon",
+ },
+ )
+ } catch (e) {
+ throw new UdcError({ code: "NO_DEPLOY_CONTRACT" })
+ }
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/udc/getConstructorParams.ts b/packages/extension/src/background/__new/procedures/udc/getConstructorParams.ts
index d6658071f..45d425b40 100644
--- a/packages/extension/src/background/__new/procedures/udc/getConstructorParams.ts
+++ b/packages/extension/src/background/__new/procedures/udc/getConstructorParams.ts
@@ -1,4 +1,3 @@
-import { LegacyContractClass } from "starknet"
import { z } from "zod"
import { getProvider } from "../../../../shared/network"
@@ -13,12 +12,17 @@ const getConstructorParamsSchema = z.object({
const basicContractClassSchema = z.object({
abi: z.array(z.any()),
+ contract_class_version: z.string(),
+ entry_points_by_type: z.any().optional(),
+ sierra_program: z.array(z.string()).optional(),
})
+export type BasicContractClass = z.infer
+
export const getConstructorParamsProcedure = extensionOnlyProcedure
.input(getConstructorParamsSchema)
.output(
- z.custom((item) => {
+ z.custom((item) => {
return basicContractClassSchema.parse(item)
}),
)
@@ -35,13 +39,17 @@ export const getConstructorParamsProcedure = extensionOnlyProcedure
const contract = await provider.getClassByHash(classHash)
- if ("sierra_program" in contract) {
- throw new UdcError({
- code: "CAIRO_1_NOT_SUPPORTED",
- })
+ const extendedContract: BasicContractClass = {
+ ...contract,
+ sierra_program:
+ "sierra_program" in contract ? contract.sierra_program : [],
+ contract_class_version:
+ "contract_class_version" in contract
+ ? contract.contract_class_version
+ : "0",
}
- return contract
+ return extendedContract
} catch (error) {
throw new UdcError({
options: { error },
diff --git a/packages/extension/src/background/__new/procedures/udc/index.ts b/packages/extension/src/background/__new/procedures/udc/index.ts
index 60bd6b88c..545f380b5 100644
--- a/packages/extension/src/background/__new/procedures/udc/index.ts
+++ b/packages/extension/src/background/__new/procedures/udc/index.ts
@@ -1,7 +1,10 @@
import { router } from "../../trpc"
-
+import { declareContractProcedure } from "./declareContractProcedure"
+import { deployContractProcedure } from "./deployContractProcedure"
import { getConstructorParamsProcedure } from "./getConstructorParams"
export const udcRouter = router({
getConstructorParams: getConstructorParamsProcedure,
+ deployContract: deployContractProcedure,
+ declareContract: declareContractProcedure,
})
diff --git a/packages/extension/src/background/__new/router.ts b/packages/extension/src/background/__new/router.ts
index 7ab07b4f1..597dfeb40 100644
--- a/packages/extension/src/background/__new/router.ts
+++ b/packages/extension/src/background/__new/router.ts
@@ -11,25 +11,32 @@ import { multisigRouter } from "./procedures/multisig"
import { recoveryRouter } from "./procedures/recovery"
import { sessionRouter } from "./procedures/session"
import { tokensRouter } from "./procedures/tokens"
+import { transactionReviewRouter } from "./procedures/transactionReview"
import { transferRouter } from "./procedures/transfer"
import { udcRouter } from "./procedures/udc"
import { backgroundActionService } from "./services/action"
import { backgroundArgentAccountService } from "./services/argentAccount"
import { backgroundMultisigService } from "./services/multisig"
+import { backgroundTransactionReviewService } from "./services/transactionReview"
import { router } from "./trpc"
import { backgroundRecoveryService } from "./services/recovery"
+import { addressRouter } from "./procedures/address"
+import { backgroundStarknetAddressService } from "./services/address"
+import { networkService } from "../../shared/network/service"
const appRouter = router({
account: accountRouter,
accountMessaging: accountMessagingRouter,
action: actionRouter,
+ address: addressRouter,
addressBook: addressBookRouter,
- recovery: recoveryRouter,
- tokens: tokensRouter,
- transfer: transferRouter,
argentAccount: argentAccountRouter,
multisig: multisigRouter,
+ recovery: recoveryRouter,
session: sessionRouter,
+ tokens: tokensRouter,
+ transactionReview: transactionReviewRouter,
+ transfer: transferRouter,
udc: udcRouter,
})
@@ -47,6 +54,9 @@ createChromeHandler({
argentAccountService: backgroundArgentAccountService,
multisigService: backgroundMultisigService,
recoveryService: backgroundRecoveryService,
+ transactionReviewService: backgroundTransactionReviewService,
+ starknetAddressService: backgroundStarknetAddressService,
+ networkService,
},
}),
})
diff --git a/packages/extension/src/background/__new/services/activity/implementation.test.ts b/packages/extension/src/background/__new/services/activity/implementation.test.ts
new file mode 100644
index 000000000..a573aafc7
--- /dev/null
+++ b/packages/extension/src/background/__new/services/activity/implementation.test.ts
@@ -0,0 +1,329 @@
+import { setupServer } from "msw/node"
+import { IActivityStorage } from "../../../../shared/activity/types"
+import { ActivityService } from "./implementation"
+import { Activity } from "./model"
+import { rest } from "msw"
+import { KeyValueStorage } from "../../../../shared/storage"
+
+describe("findLatestBalanceChangingTransaction", () => {
+ const makeService = () => {
+ const activityStore = {
+ set: vi.fn(),
+ } as unknown as KeyValueStorage
+
+ const activityService = new ActivityService(
+ "apiBase",
+ activityStore,
+ undefined,
+ )
+ return {
+ activityService,
+ activityStore,
+ }
+ }
+ it("returns null for empty array", () => {
+ const { activityService } = makeService()
+ expect(activityService.findLatestBalanceChangingTransaction([])).toBeNull()
+ })
+
+ it("returns null when all activities have empty transfers", () => {
+ const { activityService } = makeService()
+
+ const activities = [
+ { transaction: { blockNumber: 1, transactionIndex: 1 }, transfers: [] },
+ { transaction: { blockNumber: 2, transactionIndex: 2 }, transfers: [] },
+ ]
+ expect(
+ activityService.findLatestBalanceChangingTransaction(
+ activities as unknown as Activity[],
+ ),
+ ).toBeNull()
+ })
+
+ it("returns the latest activity with non-empty transfers", () => {
+ const { activityService } = makeService()
+
+ const activities = [
+ {
+ transaction: { blockNumber: 1, transactionIndex: 1 },
+ transfers: ["transfer1"],
+ },
+ {
+ transaction: { blockNumber: 2, transactionIndex: 2 },
+ transfers: ["transfer2"],
+ },
+ ] as unknown as Activity[]
+ expect(
+ activityService.findLatestBalanceChangingTransaction(activities),
+ ).toEqual(activities[1])
+ })
+
+ it("handles a single activity with non-empty transfers", () => {
+ const { activityService } = makeService()
+
+ const activity = {
+ transaction: { blockNumber: 1, transactionIndex: 1 },
+ transfers: ["transfer"],
+ }
+ expect(
+ activityService.findLatestBalanceChangingTransaction([
+ activity,
+ ] as unknown as Activity[]),
+ ).toEqual(activity)
+ })
+
+ it("returns the activity with highest transactionIndex for same blockNumber", () => {
+ const { activityService } = makeService()
+
+ const activities = [
+ {
+ transaction: { blockNumber: 1, transactionIndex: 2 },
+ transfers: ["transfer2"],
+ },
+ {
+ transaction: { blockNumber: 1, transactionIndex: 1 },
+ transfers: ["transfer1"],
+ },
+ ]
+ expect(
+ activityService.findLatestBalanceChangingTransaction(
+ activities as unknown as Activity[],
+ ),
+ ).toEqual(activities[0])
+ })
+
+ it("prioritizes pending transactions over ongoing ones", () => {
+ const { activityService } = makeService()
+
+ const activities = [
+ {
+ transaction: { transactionIndex: 2, status: "pending" },
+ transfers: ["transfer2"],
+ },
+ {
+ transaction: { blockNumber: 1, transactionIndex: 2 },
+ transfers: ["transfer2"],
+ },
+ {
+ transaction: { blockNumber: 1, transactionIndex: 1 },
+ transfers: ["transfer1"],
+ },
+ ]
+ expect(
+ activityService.findLatestBalanceChangingTransaction(
+ activities as unknown as Activity[],
+ ),
+ ).toEqual(activities[0])
+ })
+})
+
+const ADDRESS_WITHOUT_NEW_ACTIVITY = "0x1"
+const ADDRESS_WITH_NEW_ACTIVITY = "0x2"
+const ADDRESS_WITH_INVALID_DATA = "0x3"
+const OLD_TRANSACTION_HASH = "0x12345"
+const NEW_TRANSACTION_HASH = "0x123456"
+const OLD_ID = "4b3286e5-b122-45e5-8097-30b44926f553"
+/**
+ * @vitest-environment jsdom
+ */
+const OLD_ACTIVITIES = [
+ {
+ compositeId: "id",
+ id: OLD_ID,
+ status: "success",
+ wallet:
+ "0x06179cD342F04f69726D4D8B974c449dAC7002a58437Aef0E6eD23db40D31862",
+ txSender:
+ "0x06179cD342F04f69726D4D8B974c449dAC7002a58437Aef0E6eD23db40D31862",
+ source: "source",
+ type: "payment",
+ group: "finance",
+ submitted: 1234,
+ lastModified: 1234,
+ transaction: {
+ network: "goerli",
+ hash: OLD_TRANSACTION_HASH,
+ status: "success",
+ blockNumber: 1,
+ transactionIndex: 2,
+ },
+ transfers: [
+ {
+ type: "payment",
+ leg: "credit",
+ asset: {
+ type: "token",
+ tokenAddress:
+ "0x06179cD342F04f69726D4D8B974c449dAC7002a58437Aef0E6eD23db40D31862",
+ amount: "100",
+ fiatAmount: {
+ currency: "USD",
+ currencyAmount: 100,
+ },
+ },
+ },
+ ],
+ fees: [],
+ relatedAddresses: [],
+ network: "goerli",
+ },
+]
+const NEW_ACTIVITIES = [
+ {
+ compositeId: "id",
+ id: "4b3286e5-b122-45e5-8097-30b44926f555",
+ status: "success",
+ wallet:
+ "0x06179cD342F04f69726D4D8B974c449dAC7002a58437Aef0E6eD23db40D31862",
+ txSender:
+ "0x06179cD342F04f69726D4D8B974c449dAC7002a58437Aef0E6eD23db40D31862",
+ source: "source",
+ type: "payment",
+ group: "finance",
+ submitted: 1234,
+ lastModified: 1235,
+ transaction: {
+ network: "goerli",
+ hash: NEW_TRANSACTION_HASH,
+ status: "success",
+ blockNumber: 1,
+ transactionIndex: 3,
+ },
+ transfers: [
+ {
+ type: "payment",
+ leg: "credit",
+ asset: {
+ type: "token",
+ tokenAddress:
+ "0x06179cD342F04f69726D4D8B974c449dAC7002a58437Aef0E6eD23db40D31862",
+ amount: "100",
+ fiatAmount: {
+ currency: "USD",
+ currencyAmount: 100,
+ },
+ },
+ },
+ ],
+ fees: [],
+ relatedAddresses: [],
+ network: "goerli",
+ },
+]
+const server = setupServer(
+ rest.get("/apiBase/goerli/account/:address/activities", (req, res, ctx) => {
+ if (req.params.address === ADDRESS_WITHOUT_NEW_ACTIVITY) {
+ return res(
+ ctx.json({
+ activities: OLD_ACTIVITIES,
+ }),
+ )
+ }
+ if (req.params.address === ADDRESS_WITH_NEW_ACTIVITY) {
+ return res(
+ ctx.json({
+ activities: NEW_ACTIVITIES,
+ }),
+ )
+ }
+ if (req.params.address === ADDRESS_WITH_INVALID_DATA) {
+ return res(
+ ctx.json({
+ activities: [{ invalid: "data" }],
+ }),
+ )
+ }
+ }),
+)
+
+describe("shouldUpdateBalance", () => {
+ beforeAll(() => {
+ server.listen()
+ })
+ afterAll(() => server.close())
+ const makeService = () => {
+ const activityStore = {
+ set: vi.fn(),
+ get: vi.fn().mockResolvedValue({
+ [ADDRESS_WITHOUT_NEW_ACTIVITY]: {
+ id: OLD_ID,
+ lastModified: 1234,
+ },
+ }),
+ } as unknown as KeyValueStorage
+
+ const activityService = new ActivityService(
+ "/apiBase",
+ activityStore,
+ undefined,
+ )
+ return {
+ activityService,
+ activityStore,
+ }
+ }
+ it("returns false when there's no new activity", async () => {
+ const { activityService, activityStore } = makeService()
+ const spyGet = vi.spyOn(activityStore, "get")
+
+ const res = await activityService.shouldUpdateBalance({
+ address: ADDRESS_WITHOUT_NEW_ACTIVITY,
+ networkId: "goerli-alpha",
+ })
+ expect(spyGet).toHaveBeenCalled()
+ expect(res).toEqual({ shouldUpdate: false })
+ })
+ it("returns true when there's new activity and updates the storage", async () => {
+ const { activityService, activityStore } = makeService()
+ const spyGet = vi.spyOn(activityStore, "get")
+
+ const res = await activityService.shouldUpdateBalance({
+ address: ADDRESS_WITH_NEW_ACTIVITY,
+ networkId: "goerli-alpha",
+ })
+ expect(spyGet).toHaveBeenCalled()
+ expect(res).toEqual({
+ id: "4b3286e5-b122-45e5-8097-30b44926f555",
+ lastModified: 1235,
+ shouldUpdate: true,
+ })
+ })
+
+ describe("fetchActivities", () => {
+ beforeAll(() => {
+ server.listen()
+ })
+ afterAll(() => server.close())
+ test.each([
+ [ADDRESS_WITHOUT_NEW_ACTIVITY, OLD_ACTIVITIES],
+ [ADDRESS_WITH_NEW_ACTIVITY, NEW_ACTIVITIES],
+ ])(
+ "return the correctly parsed activities when fetching from the API",
+ async (address, payload) => {
+ const { activityService } = makeService()
+ const response = await activityService.fetchActivities({
+ address,
+ networkId: "goerli-alpha",
+ lastModified: 1234,
+ })
+
+ expect(response).toEqual(payload)
+ },
+ )
+ // TODO uncomment when we have final version of the API
+ // it("should throw when given an invalid output from the API", async () => {
+ // const { activityService } = makeService()
+ // try {
+ // await activityService.fetchActivities({
+ // address: ADDRESS_WITH_INVALID_DATA,
+ // networkId: "goerli-alpha",
+ // lastModified: 1234,
+ // })
+ // } catch (e) {
+ // // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // //@ts-expect-error
+ // expect(e.message).toEqual("Failed to parse backend response")
+ // }
+ // })
+ })
+})
diff --git a/packages/extension/src/background/__new/services/activity/implementation.ts b/packages/extension/src/background/__new/services/activity/implementation.ts
new file mode 100644
index 000000000..616ca73c6
--- /dev/null
+++ b/packages/extension/src/background/__new/services/activity/implementation.ts
@@ -0,0 +1,145 @@
+import { HTTPService, IHttpService } from "@argent/shared"
+
+import { Activity, ActivityResponse } from "./model"
+import { IActivityService } from "./interface"
+import { IActivity, IActivityStorage } from "../../../../shared/activity/types"
+import { argentApiNetworkForNetwork } from "../../../../shared/api/fetcher"
+import { ActivityError } from "../../../../shared/errors/activity"
+import { KeyValueStorage } from "../../../../shared/storage"
+
+export class ActivityService implements IActivityService {
+ private readonly httpService: IHttpService
+
+ constructor(
+ protected readonly apiBase: string,
+ private readonly activityStore: KeyValueStorage,
+ private readonly headers: HeadersInit | undefined,
+ ) {
+ this.httpService = new HTTPService({ headers: this.headers }, "json")
+ }
+
+ async fetchActivities({
+ address,
+ networkId,
+ lastModified,
+ }: {
+ address: string
+ networkId: string
+ lastModified?: number
+ }) {
+ const endpoint = `${this.apiBase}/${argentApiNetworkForNetwork(
+ networkId,
+ )}/account/${address}/activities${
+ lastModified ? `?modifiedAfter=${lastModified}` : ""
+ }`
+
+ const response = await this.httpService.get(endpoint)
+ if (!response) {
+ throw new ActivityError({ code: "FETCH_FAILED" })
+ }
+ return response.activities
+ // TODO uncomment this once we have the final API
+ // const parsedActivities = activityResponseSchema.safeParse(response)
+
+ // if (!parsedActivities.success) {
+ // throw new ActivityError({
+ // code: "PARSING_FAILED",
+ // })
+ // }
+ // return parsedActivities.data.activities
+ }
+
+ findLatestBalanceChangingTransaction(
+ activities: Activity[],
+ ): Activity | null {
+ if (!activities || activities.length === 0) {
+ return null
+ }
+
+ // All balance changing transactions have transfers
+ const balanceChangingActivities = activities.filter(
+ (activity) => activity.transfers && activity.transfers.length > 0,
+ )
+
+ if (balanceChangingActivities.length === 0) {
+ return null
+ }
+
+ const [latestBalanceChangingTransaction] = balanceChangingActivities.sort(
+ (a, b) => {
+ // Check if any of the transactions are pending
+ const aIsPending = a.transaction.status === "pending"
+ const bIsPending = b.transaction.status === "pending"
+
+ // Prioritize pending transactions
+ if (aIsPending && !bIsPending) {
+ return -1
+ } else if (!aIsPending && bIsPending) {
+ return 1
+ } else if (aIsPending && bIsPending) {
+ // If both are pending, sort by transactionIndex
+ return b.transaction.transactionIndex - a.transaction.transactionIndex
+ } else {
+ // If neither is pending, sort by blockNumber then transactionIndex
+ if (
+ a.transaction.blockNumber &&
+ b.transaction.blockNumber &&
+ a.transaction.blockNumber !== b.transaction.blockNumber
+ ) {
+ return b.transaction.blockNumber - a.transaction.blockNumber
+ } else {
+ return (
+ b.transaction.transactionIndex - a.transaction.transactionIndex
+ )
+ }
+ }
+ },
+ )
+ return latestBalanceChangingTransaction
+ }
+
+ async shouldUpdateBalance({
+ address,
+ networkId,
+ }: {
+ address: string
+ networkId: string
+ }) {
+ const latestBalanceChangingActivity = (
+ await this.activityStore.get("latestBalanceChangingActivity")
+ )?.[address]
+ const activities = await this.fetchActivities({
+ address,
+ networkId,
+ lastModified: latestBalanceChangingActivity?.lastModified,
+ })
+ const latestActivity = this.findLatestBalanceChangingTransaction(activities)
+ const shouldUpdate =
+ !latestBalanceChangingActivity?.id ||
+ latestActivity?.id !== latestBalanceChangingActivity?.id
+
+ if (shouldUpdate && latestActivity) {
+ return {
+ shouldUpdate,
+ lastModified: latestActivity.lastModified,
+ id: latestActivity.id,
+ }
+ }
+ return { shouldUpdate: false }
+ }
+
+ async addActivityToStore({
+ address,
+ lastModified,
+ id,
+ }: IActivity & {
+ address: string
+ }) {
+ await this.activityStore.set("latestBalanceChangingActivity", {
+ [address]: {
+ id: id,
+ lastModified: lastModified,
+ },
+ })
+ }
+}
diff --git a/packages/extension/src/background/__new/services/activity/index.ts b/packages/extension/src/background/__new/services/activity/index.ts
new file mode 100644
index 000000000..e35eb1e77
--- /dev/null
+++ b/packages/extension/src/background/__new/services/activity/index.ts
@@ -0,0 +1,15 @@
+import urlJoin from "url-join"
+import { ARGENT_API_BASE_URL } from "../../../../shared/api/constants"
+import { ActivityService } from "./implementation"
+import { activityStore } from "../../../../shared/activity/storage"
+
+const activityBaseUrl = urlJoin(ARGENT_API_BASE_URL || "", "/activity/starknet")
+
+export const activityService = new ActivityService(
+ activityBaseUrl,
+ activityStore,
+ {
+ "argent-version": process.env.VERSION ?? "Unknown version",
+ "argent-client": "argent-x",
+ },
+)
diff --git a/packages/extension/src/background/__new/services/activity/interface.ts b/packages/extension/src/background/__new/services/activity/interface.ts
new file mode 100644
index 000000000..c5dd9ee44
--- /dev/null
+++ b/packages/extension/src/background/__new/services/activity/interface.ts
@@ -0,0 +1,22 @@
+import { IActivity } from "../../../../shared/activity/types"
+import { BaseWalletAccount } from "../../../../shared/wallet.model"
+import { Activity } from "./model"
+
+export interface IActivityService {
+ fetchActivities({
+ address,
+ networkId,
+ }: BaseWalletAccount): Promise
+ shouldUpdateBalance({ address, networkId }: BaseWalletAccount): Promise<{
+ shouldUpdate: boolean
+ lastModified?: number
+ id?: string
+ }>
+ addActivityToStore({
+ address,
+ lastModified,
+ id,
+ }: IActivity & {
+ address: string
+ }): Promise
+}
diff --git a/packages/extension/src/background/__new/services/activity/model.ts b/packages/extension/src/background/__new/services/activity/model.ts
new file mode 100644
index 000000000..55534938d
--- /dev/null
+++ b/packages/extension/src/background/__new/services/activity/model.ts
@@ -0,0 +1,68 @@
+import { addressSchema } from "@argent/shared"
+import { z } from "zod"
+
+const transactionSchema = z.object({
+ network: z.string(),
+ hash: z.string(),
+ status: z.string(),
+ blockNumber: z.number().optional(),
+ transactionIndex: z.number(),
+})
+
+const assetSchema = z.object({
+ // probably an enum
+ type: z.string(),
+ tokenAddress: addressSchema,
+ amount: z.string().optional(),
+ fiatAmount: z
+ .object({
+ currency: z.string(),
+ currencyAmount: z.number(),
+ })
+ .optional(),
+})
+
+const transferSchema = z.object({
+ // seems to be the same as activity type
+ type: z.string(),
+ // is probably enum (credit)
+ leg: z.string(),
+ counterParty: addressSchema.optional(),
+ asset: assetSchema,
+ counterPartyNetwork: z.string().optional(),
+})
+
+const relatedAddressSchema = z.object({
+ address: addressSchema,
+ network: z.string(),
+ type: z.string(),
+})
+
+export const activitySchema = z.object({
+ compositeId: z.string(),
+ id: z.string().uuid(),
+ // todo can be refined
+ status: z.string(),
+ wallet: addressSchema,
+ txSender: addressSchema,
+ source: z.string(),
+ // can be refined ('payment')
+ type: z.string(),
+ // can be refined ('finance')
+ group: z.string(),
+ submitted: z.number(),
+ lastModified: z.number(),
+ transaction: transactionSchema,
+ transfers: z.array(transferSchema),
+ // to be clarified
+ fees: z.any(),
+ relatedAddresses: z.array(relatedAddressSchema),
+ network: z.string(),
+})
+
+export const activityResponseSchema = z.object({
+ activities: z.array(activitySchema),
+})
+
+export type ActivityResponse = z.infer
+export type Activity = z.infer
diff --git a/packages/extension/src/background/__new/services/address/index.ts b/packages/extension/src/background/__new/services/address/index.ts
new file mode 100644
index 000000000..acf24074e
--- /dev/null
+++ b/packages/extension/src/background/__new/services/address/index.ts
@@ -0,0 +1,12 @@
+import { StarknetAddressService } from "@argent/shared"
+import { ARGENT_API_BASE_URL } from "../../../../shared/api/constants"
+import { httpService } from "../http/singleton"
+import { getDefaultNetworkId } from "../../../../shared/network/utils"
+
+const allowedArgentNameNetworkId = getDefaultNetworkId()
+
+export const backgroundStarknetAddressService = new StarknetAddressService(
+ httpService,
+ ARGENT_API_BASE_URL,
+ allowedArgentNameNetworkId,
+)
diff --git a/packages/extension/src/background/__new/services/analytics/worker.ts b/packages/extension/src/background/__new/services/analytics/worker.ts
index f8fc60224..abff3111f 100644
--- a/packages/extension/src/background/__new/services/analytics/worker.ts
+++ b/packages/extension/src/background/__new/services/analytics/worker.ts
@@ -1,17 +1,15 @@
import type { IActiveStore } from "../../../../shared/analytics"
import type { IBackgroundUIService } from "../ui/interface"
-import { Opened } from "../ui/interface"
+import { onClose } from "../worker/schedule/decorators"
+import { pipe } from "../worker/schedule/pipe"
export class AnalyticsWorker {
constructor(
private readonly activeStore: IActiveStore,
private readonly backgroundUIService: IBackgroundUIService,
- ) {
- this.backgroundUIService.emitter.on(Opened, (opened) => {
- if (!opened) {
- /** Extension was closed */
- void this.activeStore.update("lastClosed")
- }
- })
- }
+ ) {}
+
+ onClose = pipe(onClose(this.backgroundUIService))(async () => {
+ await this.activeStore.update("lastClosed")
+ })
}
diff --git a/packages/extension/src/background/__new/services/http/singleton.ts b/packages/extension/src/background/__new/services/http/singleton.ts
index 6324f80ad..d1167cc15 100644
--- a/packages/extension/src/background/__new/services/http/singleton.ts
+++ b/packages/extension/src/background/__new/services/http/singleton.ts
@@ -1,3 +1,8 @@
import { HTTPService } from "@argent/shared"
-export const httpService = new HTTPService()
+export const httpService = new HTTPService({
+ headers: {
+ "argent-version": process.env.VERSION ?? "Unknown version",
+ "argent-client": "argent-x",
+ },
+})
diff --git a/packages/extension/src/background/__new/services/multisig/implementation.ts b/packages/extension/src/background/__new/services/multisig/implementation.ts
index c5d936cf0..449d1525d 100644
--- a/packages/extension/src/background/__new/services/multisig/implementation.ts
+++ b/packages/extension/src/background/__new/services/multisig/implementation.ts
@@ -13,7 +13,7 @@ import {
UpdateMultisigThresholdPayload,
} from "../../../../shared/multisig/multisig.model"
import { Wallet } from "../../../wallet"
-import { CallData } from "starknet"
+import { CallData, DeployAccountContractPayload } from "starknet"
import { IBackgroundActionService } from "../action/interface"
import { BaseWalletAccount } from "../../../../shared/wallet.model"
import {
@@ -22,7 +22,14 @@ import {
PendingMultisig,
} from "../../../../shared/multisig/types"
import { MultisigAccount } from "../../../../shared/multisig/account"
-import { decodeBase58, decodeBase58Array } from "@argent/shared"
+import {
+ addOwnersCalldataSchema,
+ changeThresholdCalldataSchema,
+ decodeBase58,
+ decodeBase58Array,
+ removeOwnersCalldataSchema,
+ replaceSignerCalldataSchema,
+} from "@argent/shared"
import { AccountError } from "../../../../shared/errors/account"
import { getMultisigPendingTransaction } from "../../../../shared/multisig/pendingTransactionsStore"
import { MultisigError } from "../../../../shared/errors/multisig"
@@ -74,23 +81,33 @@ export default class BackgroundMultisigService implements IMultisigService {
const signersPayload = {
entrypoint: MultisigEntryPointType.ADD_SIGNERS,
- calldata: CallData.compile({
- new_threshold: newThreshold.toString(),
- signers_to_add: decodeBase58Array(signersToAdd),
- }),
+ calldata: CallData.compile(
+ addOwnersCalldataSchema.parse({
+ new_threshold: newThreshold.toString(),
+ signers_to_add: decodeBase58Array(signersToAdd),
+ }),
+ ),
contractAddress: address,
}
-
- await this.actionService.add({
- type: "TRANSACTION",
- payload: {
- transactions: signersPayload,
- meta: {
- title: "Add signers",
- type: MultisigTransactionType.MULTISIG_ADD_SIGNERS,
+ const title = `Add owner${
+ signersToAdd.length > 1 ? "s" : ""
+ } and set confirmations to ${newThreshold}`
+ await this.actionService.add(
+ {
+ type: "TRANSACTION",
+ payload: {
+ transactions: signersPayload,
+ meta: {
+ title: "Add signers",
+ type: MultisigTransactionType.MULTISIG_ADD_SIGNERS,
+ },
},
},
- })
+ {
+ title,
+ icon: "MultisigJoinIcon",
+ },
+ )
}
async removeOwner(payload: RemoveOwnerMultisigPayload): Promise {
@@ -100,23 +117,33 @@ export default class BackgroundMultisigService implements IMultisigService {
const signersPayload = {
entrypoint: MultisigEntryPointType.REMOVE_SIGNERS,
- calldata: CallData.compile({
- new_threshold: newThreshold.toString(),
- signers_to_remove: signersToRemove,
- }),
+ calldata: CallData.compile(
+ removeOwnersCalldataSchema.parse({
+ new_threshold: newThreshold.toString(),
+ signers_to_remove: signersToRemove,
+ }),
+ ),
+
contractAddress: address,
}
-
- await this.actionService.add({
- type: "TRANSACTION",
- payload: {
- transactions: signersPayload,
- meta: {
- title: "Remove signers",
- type: MultisigTransactionType.MULTISIG_REMOVE_SIGNERS,
+ const title = `Remove owner${
+ signersToRemove.length > 1 ? "s" : ""
+ } and set confirmations to ${newThreshold}}`
+ await this.actionService.add(
+ {
+ type: "TRANSACTION",
+ payload: {
+ transactions: signersPayload,
+ meta: {
+ type: MultisigTransactionType.MULTISIG_REMOVE_SIGNERS,
+ },
},
},
- })
+ {
+ title,
+ icon: "MultisigRemoveIcon",
+ },
+ )
}
async replaceOwner(payload: ReplaceOwnerMultisigPayload): Promise {
@@ -127,23 +154,30 @@ export default class BackgroundMultisigService implements IMultisigService {
const signersPayload = {
entrypoint: MultisigEntryPointType.REPLACE_SIGNER,
- calldata: CallData.compile({
- signer_to_remove: decodedSignerToRemove,
- signer_to_add: decodedSignerToAdd,
- }),
+ calldata: CallData.compile(
+ replaceSignerCalldataSchema.parse({
+ signer_to_remove: decodedSignerToRemove,
+ signer_to_add: decodedSignerToAdd,
+ }),
+ ),
contractAddress: address,
}
- await this.actionService.add({
- type: "TRANSACTION",
- payload: {
- transactions: signersPayload,
- meta: {
- title: "Replace signer",
- type: MultisigTransactionType.MULTISIG_REPLACE_SIGNER,
+ await this.actionService.add(
+ {
+ type: "TRANSACTION",
+ payload: {
+ transactions: signersPayload,
+ meta: {
+ type: MultisigTransactionType.MULTISIG_REPLACE_SIGNER,
+ },
},
},
- })
+ {
+ title: "Replace owner",
+ icon: "MultisigReplaceIcon",
+ },
+ )
}
async addPendingAccount(networkId: string): Promise {
@@ -181,10 +215,34 @@ export default class BackgroundMultisigService implements IMultisigService {
}
async deploy(account: BaseWalletAccount): Promise {
- await this.actionService.add({
- type: "DEPLOY_MULTISIG_ACTION",
- payload: account,
- })
+ let displayCalldata: string[] = []
+ const walletAccount = await this.wallet.getAccount(account)
+ if (!walletAccount) {
+ throw new AccountError({ code: "MULTISIG_NOT_FOUND" })
+ }
+ try {
+ /** determine the calldata to display to the end user */
+ const deployAccountPayload =
+ await this.wallet.getMultisigDeploymentPayload(walletAccount)
+ const { constructorCalldata } = deployAccountPayload
+ displayCalldata = CallData.toCalldata(constructorCalldata)
+ } catch {
+ /** ignore non-critical error */
+ }
+
+ await this.actionService.add(
+ {
+ type: "DEPLOY_MULTISIG",
+ payload: {
+ account,
+ displayCalldata,
+ },
+ },
+ {
+ title: "Activate multisig",
+ icon: "MultisigIcon",
+ },
+ )
}
async updateThreshold(
@@ -194,19 +252,24 @@ export default class BackgroundMultisigService implements IMultisigService {
const thresholdPayload = {
entrypoint: MultisigEntryPointType.CHANGE_THRESHOLD,
- calldata: [newThreshold.toString()],
+ calldata: changeThresholdCalldataSchema.parse([newThreshold.toString()]),
contractAddress: address,
}
- await this.actionService.add({
- type: "TRANSACTION",
- payload: {
- transactions: thresholdPayload,
- meta: {
- title: "Change threshold",
- type: MultisigTransactionType.MULTISIG_CHANGE_THRESHOLD,
+ await this.actionService.add(
+ {
+ type: "TRANSACTION",
+ payload: {
+ transactions: thresholdPayload,
+ meta: {
+ type: MultisigTransactionType.MULTISIG_CHANGE_THRESHOLD,
+ },
},
},
- })
+ {
+ title: `Set confirmations to ${newThreshold}`,
+ icon: "ApproveIcon",
+ },
+ )
}
}
diff --git a/packages/extension/src/background/__new/services/network/status.ts b/packages/extension/src/background/__new/services/network/status.ts
index d69e673dd..f9d43e6fd 100644
--- a/packages/extension/src/background/__new/services/network/status.ts
+++ b/packages/extension/src/background/__new/services/network/status.ts
@@ -1,23 +1,13 @@
import { Network, NetworkStatus } from "../../../../shared/network"
import { GetNetworkStatusesFn } from "./interface"
-import {
- getProvider,
- getProviderForRpcUrl,
- shouldUseRpcProvider,
-} from "../../../../shared/network/provider"
+import { getProvider } from "../../../../shared/network/provider"
async function getNetworkStatus(network: Network): Promise {
const provider = getProvider(network)
- if (!shouldUseRpcProvider(network) || !network.rpcUrl) {
- // chainId can not be used, as snjs is shallowing the network error
- await provider.getBlock("latest") // throws if not connected
- return "ok"
- }
-
- const rpcProvider = getProviderForRpcUrl(network.rpcUrl)
- const sync = await rpcProvider.getSyncingStats() // throws if not connected
+ const sync = await provider.getSyncingStats() // throws if not connected
- if (sync === false) {
+ // Can only be false but inproperly typed in the current version of snjs
+ if (typeof sync === "boolean") {
// not syncing
return "ok"
}
diff --git a/packages/extension/src/background/__new/services/network/worker.ts b/packages/extension/src/background/__new/services/network/worker.ts
index 061ab8d54..a361d3711 100644
--- a/packages/extension/src/background/__new/services/network/worker.ts
+++ b/packages/extension/src/background/__new/services/network/worker.ts
@@ -15,12 +15,14 @@ export class NetworkWorker {
private readonly debounceService: IDebounceService,
) {}
- updateNetworkStatuses = everyWhenOpen(
- this.backgroundUIService,
- this.scheduleService,
- this.debounceService,
- RefreshInterval.MEDIUM,
- )(async (): Promise => {
- await this.backgroundNetworkService.updateStatuses()
- })
+ // 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()
+ //})
}
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 c2b8fbf7b..bf0ec0357 100644
--- a/packages/extension/src/background/__new/services/nft/worker/implementation.ts
+++ b/packages/extension/src/background/__new/services/nft/worker/implementation.ts
@@ -1,9 +1,8 @@
-import { addressSchema, getAccountIdentifier } from "@argent/shared"
+import { addressSchema } from "@argent/shared"
import { uniq } from "lodash-es"
-import { IBackgroundUIService, Opened } from "../../ui/interface"
+import { IBackgroundUIService } from "../../ui/interface"
import { Wallet } from "../../../../wallet"
-import { Locked } from "../../../../wallet/session/interface"
import { WalletSessionService } from "../../../../wallet/session/session.service"
import { RefreshInterval } from "../../../../../shared/config"
import { INFTService } from "../../../../../shared/nft/interface"
@@ -12,36 +11,32 @@ import { WalletStorageProps } from "../../../../../shared/wallet/walletStore"
import { ArrayStorage, KeyValueStorage } from "../../../../../shared/storage"
import { Transaction } from "../../../../../shared/transactions"
import { transactionSucceeded } from "../../../../../shared/utils/transactionSucceeded"
-import { INFTWorkerStore } from "../../../../../shared/nft/worker/interface"
-
-const TASK_ID = "NftsWorker.updateNfts"
-const REFRESH_PERIOD_MINUTES = Math.floor(RefreshInterval.SLOW / 60)
+import { everyWhenOpen } from "../../worker/schedule/decorators"
+import { pipe } from "../../worker/schedule/pipe"
+import { IDebounceService } from "../../../../../shared/debounce"
+import { Recovered } from "../../../../wallet/recovery/interface"
+import { WalletRecoverySharedService } from "../../../../wallet/recovery/shared.service"
export class NftsWorker {
constructor(
private readonly nftsService: INFTService,
- private readonly scheduleService: IScheduleService,
+ private readonly scheduleService: IScheduleService,
private readonly walletSingleton: Wallet,
private walletStore: KeyValueStorage,
private readonly transactionsStore: ArrayStorage,
public readonly sessionService: WalletSessionService,
private readonly backgroundUIService: IBackgroundUIService,
- private store: KeyValueStorage,
+ private readonly debounceService: IDebounceService,
+ private readonly recoverySharedService: WalletRecoverySharedService,
) {
- /** udpdate on a regular refresh interval */
- void this.scheduleService.registerImplementation({
- id: TASK_ID,
- callback: this.updateNfts.bind(this),
- })
-
- /** interval while the ui is open */
- this.backgroundUIService.emitter.on(Opened, this.onOpened.bind(this))
-
- /** update when the wallet unlocks */
- this.sessionService.emitter.on(Locked, this.onLocked.bind(this))
-
/** update when the account changes */
- this.walletStore.subscribe("selected", this.updateNfts.bind(this))
+ this.walletStore.subscribe("selected", this.updateNftsCallback.bind(this))
+
+ // Listen for recovery event
+ this.recoverySharedService.emitter.on(
+ Recovered,
+ this.updateNftsCallback.bind(this),
+ )
/** update when a transaction succeeds (could be nft-related) */
this.transactionsStore.subscribe((_, changeSet) => {
@@ -53,80 +48,16 @@ export class NftsWorker {
changeSet?.oldValue,
)
if (hasSuccessTx) {
- setTimeout(() => void this.updateNfts(), 5000) // Add a delay so the backend has time to index the nft
+ setTimeout(() => void this.updateNftsCallback(), 5000) // Add a delay so the backend has time to index the nft
}
})
}
- async getStateForCurrentAccount() {
+ updateNftsCallback = async () => {
const account = await this.walletSingleton.getSelectedAccount()
if (!account) {
return
}
- const accountIdentifier = getAccountIdentifier(account)
- const entry = await this.store.get(accountIdentifier)
- if (!entry) {
- await this.store.set(accountIdentifier, {
- isUpdating: false,
- lastUpdatedTimestamp: 0,
- })
- }
- return this.store.get(accountIdentifier)
- }
-
- async onOpened(opened: boolean) {
- if (opened) {
- const stateForCurrentAccount = await this.getStateForCurrentAccount()
- if (!stateForCurrentAccount) {
- return
- }
- const currentTimestamp = Date.now()
- const differenceInMilliseconds =
- currentTimestamp - stateForCurrentAccount.lastUpdatedTimestamp
- const differenceInMinutes = differenceInMilliseconds / (1000 * 60) // Convert milliseconds to minutes
-
- // If we haven't done a nft update for the current account in the past 5 minutes, do one on the spot when opening the extension
- if (differenceInMinutes > REFRESH_PERIOD_MINUTES) {
- void this.updateNfts()
- }
-
- void this.scheduleService.every(RefreshInterval.SLOW, {
- id: TASK_ID,
- })
- } else {
- void this.scheduleService.delete({
- id: TASK_ID,
- })
- }
- }
-
- onLocked(locked: boolean) {
- if (!locked) {
- void this.updateNfts()
- }
- }
-
- async updateNfts() {
- const stateForCurrentAccount = await this.getStateForCurrentAccount()
-
- if (!stateForCurrentAccount) {
- return
- }
- if (stateForCurrentAccount.isUpdating) {
- return
- }
-
- const account = await this.walletSingleton.getSelectedAccount()
- if (!account) {
- return
- }
-
- const accountIdentifier = getAccountIdentifier(account)
- const lastUpdatedTimestamp = Date.now()
- await this.store.set(accountIdentifier, {
- isUpdating: true,
- lastUpdatedTimestamp,
- })
try {
const nfts = await this.nftsService.getAssets(
@@ -156,10 +87,17 @@ export class NftsWorker {
} catch (e) {
console.error(e)
}
-
- await this.store.set(accountIdentifier, {
- isUpdating: false,
- lastUpdatedTimestamp,
- })
}
+
+ updateNfts = pipe(
+ everyWhenOpen(
+ this.backgroundUIService,
+ this.scheduleService,
+ this.debounceService,
+ RefreshInterval.SLOW,
+ "NftsWorker.updateNfts",
+ ),
+ )(async () => {
+ await this.updateNftsCallback()
+ })
}
diff --git a/packages/extension/src/background/__new/services/nft/worker/index.ts b/packages/extension/src/background/__new/services/nft/worker/index.ts
index 396f85303..6f21a65aa 100644
--- a/packages/extension/src/background/__new/services/nft/worker/index.ts
+++ b/packages/extension/src/background/__new/services/nft/worker/index.ts
@@ -1,9 +1,13 @@
-import { nftWorkerStore } from "../../../../../shared/nft/worker/store"
+import { nftService } from "../../../../../shared/nft"
+import { debounceService } from "../../../../../shared/debounce"
import { chromeScheduleService } from "../../../../../shared/schedule"
import { old_walletStore } from "../../../../../shared/wallet/walletStore"
-import { nftService } from "../../../../../ui/services/nfts"
import { transactionsStore } from "../../../../transactions/store"
-import { sessionService, walletSingleton } from "../../../../walletSingleton"
+import {
+ recoverySharedService,
+ sessionService,
+ walletSingleton,
+} from "../../../../walletSingleton"
import { backgroundUIService } from "../../ui"
import { NftsWorker } from "./implementation"
@@ -15,5 +19,6 @@ export const nftsWorker = new NftsWorker(
transactionsStore,
sessionService,
backgroundUIService,
- nftWorkerStore,
+ debounceService,
+ recoverySharedService,
)
diff --git a/packages/extension/src/background/__new/services/transactionReview/background.ts b/packages/extension/src/background/__new/services/transactionReview/background.ts
new file mode 100644
index 000000000..75845157e
--- /dev/null
+++ b/packages/extension/src/background/__new/services/transactionReview/background.ts
@@ -0,0 +1,333 @@
+import urlJoin from "url-join"
+
+import { type IHttpService, ensureArray } from "@argent/shared"
+import {
+ Account,
+ CairoVersion,
+ Call,
+ Calldata,
+ Invocations,
+ TransactionType,
+ hash,
+ num,
+} from "starknet"
+
+import type {
+ ITransactionReviewLabelsStore,
+ ITransactionReviewService,
+ TransactionReviewTransactions,
+} from "../../../../shared/transactionReview/interface"
+import type { StarknetTransactionTypes } from "../../../../shared/transactions"
+import { Wallet } from "../../../wallet"
+import {
+ SimulateAndReview,
+ simulateAndReviewSchema,
+} from "../../../../shared/transactionReview/schema"
+import { ReviewError } from "../../../../shared/errors/review"
+import { addEstimatedFees } from "../../../../shared/transactionSimulation/fees/estimatedFeesRepository"
+import { argentMaxFee } from "../../../../shared/utils/argentMaxFee"
+import { AccountError } from "../../../../shared/errors/account"
+import { transactionCallsAdapter } from "../../../transactions/transactionAdapter"
+import { EstimatedFees } from "../../../../shared/transactionSimulation/fees/fees.model"
+import { KeyValueStorage } from "../../../../shared/storage"
+import { ITransactionReviewWorker } from "./worker/interface"
+import { ARGENT_TRANSACTION_REVIEW_API_BASE_URL } from "../../../../shared/api/constants"
+
+interface ApiTransactionReviewV2RequestBody {
+ transactions: Array<{
+ type: StarknetTransactionTypes
+ chainId: string
+ cairoVersion: CairoVersion
+ nonce: string
+ version: string
+ account: string
+ calls?: Call[]
+ calldata?: Calldata
+ }>
+}
+
+const simulateAndReviewEndpoint = urlJoin(
+ ARGENT_TRANSACTION_REVIEW_API_BASE_URL || "",
+ "transactions/v2/review/starknet",
+)
+
+export default class BackgroundTransactionReviewService
+ implements ITransactionReviewService
+{
+ constructor(
+ private wallet: Wallet,
+ private httpService: IHttpService,
+ private readonly labelsStore: KeyValueStorage,
+ private worker: ITransactionReviewWorker,
+ ) {}
+
+ private async fetchFeesOnchain({
+ starknetAccount,
+ calls,
+ isDeployed,
+ }: {
+ starknetAccount: Account
+ calls: Call[]
+ isDeployed: boolean
+ }) {
+ try {
+ const selectedAccount = await this.wallet.getSelectedAccount()
+ const oldAccountTransactions = transactionCallsAdapter(calls)
+
+ if (!selectedAccount) {
+ throw new AccountError({ code: "NOT_FOUND" })
+ }
+ let txFee = "0",
+ maxTxFee = "0",
+ accountDeploymentFee: string | undefined,
+ maxADFee: string | undefined
+
+ if (!isDeployed) {
+ if ("estimateFeeBulk" in starknetAccount) {
+ const bulkTransactions: Invocations = [
+ {
+ type: TransactionType.DEPLOY_ACCOUNT,
+ payload: await this.wallet.getAccountDeploymentPayload(
+ selectedAccount,
+ ),
+ },
+ {
+ type: TransactionType.INVOKE,
+ payload: calls,
+ },
+ ]
+ const estimateFeeBulk = await starknetAccount.estimateFeeBulk(
+ bulkTransactions,
+ { skipValidate: true },
+ )
+
+ accountDeploymentFee = num.toHex(estimateFeeBulk[0].overall_fee)
+ txFee = num.toHex(estimateFeeBulk[1].overall_fee)
+
+ maxADFee = argentMaxFee({
+ suggestedMaxFee: estimateFeeBulk[0].suggestedMaxFee,
+ })
+ maxTxFee = argentMaxFee({
+ suggestedMaxFee: estimateFeeBulk[1].suggestedMaxFee,
+ })
+ }
+ } else {
+ const { overall_fee, suggestedMaxFee } =
+ await starknetAccount.estimateFee(calls, {
+ skipValidate: true,
+ })
+
+ txFee = num.toHex(overall_fee)
+ maxTxFee = num.toHex(suggestedMaxFee) // Here, maxFee = estimatedFee * 1.5x
+ }
+
+ const suggestedMaxFee = argentMaxFee({ suggestedMaxFee: maxTxFee })
+
+ await addEstimatedFees(
+ {
+ amount: txFee,
+ suggestedMaxFee,
+ accountDeploymentFee,
+ maxADFee,
+ },
+ calls,
+ )
+ return {
+ amount: txFee,
+ suggestedMaxFee,
+ accountDeploymentFee,
+ maxADFee,
+ }
+ } catch (error) {
+ throw new ReviewError({
+ code: "ONCHAIN_FEE_ESTIMATION_FAILED",
+ message: `${error}`,
+ })
+ }
+ }
+
+ private getCallsFromTx(tx: TransactionReviewTransactions) {
+ let calls
+ if (tx.calls) {
+ calls = ensureArray(tx.calls)
+ }
+ return calls
+ }
+
+ private getPayloadFromTransaction({
+ transaction,
+ nonce,
+ chainId,
+ version,
+ isDeploymentTransaction,
+ cairoVersion,
+ address,
+ }: {
+ transaction: TransactionReviewTransactions
+ nonce: string
+ chainId: string
+ version: string
+ isDeploymentTransaction: boolean
+ cairoVersion: CairoVersion
+ address: string
+ }) {
+ let transactionNonce = nonce
+ if (isDeploymentTransaction && transaction.type !== "DEPLOY_ACCOUNT") {
+ transactionNonce = num.toHex(1)
+ }
+ const calls = this.getCallsFromTx(transaction)
+
+ return {
+ type: transaction.type,
+ chainId,
+ cairoVersion: cairoVersion,
+ nonce: transactionNonce,
+ version,
+ account: address,
+ calls,
+ calldata: transaction.calldata,
+ salt: transaction.salt,
+ signature: transaction.signature,
+ classHash: transaction.classHash,
+ }
+ }
+
+ private async getEnrichedFeeEstimation(
+ initialTransactions: TransactionReviewTransactions[],
+ simulateAndReviewResult: SimulateAndReview,
+ isDeploymentTransaction: boolean,
+ ): Promise {
+ const { transactions } = simulateAndReviewResult
+
+ let invokeTransaction, accountDeploymentFee, maxADFee
+
+ if (isDeploymentTransaction) {
+ invokeTransaction = transactions[1]
+ accountDeploymentFee =
+ transactions[0].simulation?.feeEstimation.maxFee.toString()
+ maxADFee = accountDeploymentFee || "0"
+ } else {
+ invokeTransaction = transactions[0]
+ }
+
+ const amount =
+ invokeTransaction.simulation?.feeEstimation.overallFee.toString() ?? "0"
+ const suggestedMaxFee =
+ invokeTransaction.simulation?.feeEstimation.maxFee ?? "0"
+
+ await addEstimatedFees(
+ {
+ amount,
+ suggestedMaxFee,
+ accountDeploymentFee,
+ maxADFee,
+ },
+ initialTransactions[isDeploymentTransaction ? 1 : 0].calls ?? [],
+ )
+ return {
+ amount,
+ suggestedMaxFee,
+ accountDeploymentFee,
+ maxADFee,
+ }
+ }
+
+ async simulateAndReview({
+ transactions,
+ }: {
+ transactions: TransactionReviewTransactions[]
+ }) {
+ const account = await this.wallet.getSelectedStarknetAccount()
+ const isDeploymentTransaction = Boolean(
+ transactions.find((tx) => tx.type === "DEPLOY_ACCOUNT"),
+ )
+ try {
+ const nonce = isDeploymentTransaction ? "0x0" : await account.getNonce()
+ const version = num.toHex(hash.feeTransactionVersion)
+
+ if (!("getChainId" in account)) {
+ throw new AccountError({
+ message: "MISSING_METHOD",
+ })
+ }
+
+ if (typeof account.cairoVersion === "undefined") {
+ throw new AccountError({
+ message: "MISSING_METHOD",
+ })
+ }
+ const chainId = await account.getChainId()
+
+ const body: ApiTransactionReviewV2RequestBody = {
+ transactions: transactions.map((transaction) =>
+ this.getPayloadFromTransaction({
+ transaction,
+ nonce,
+ version,
+ chainId,
+ isDeploymentTransaction,
+ cairoVersion: account.cairoVersion,
+ address: account.address,
+ }),
+ ),
+ }
+ const result = await this.httpService.post(
+ simulateAndReviewEndpoint,
+ {
+ method: "POST",
+ headers: {
+ Accept: "application/json",
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(body),
+ },
+ simulateAndReviewSchema,
+ )
+ const enrichedFeeEstimation = await this.getEnrichedFeeEstimation(
+ transactions,
+ result,
+ isDeploymentTransaction,
+ )
+ return {
+ ...result,
+ enrichedFeeEstimation,
+ }
+ } catch (e) {
+ console.log(e)
+ try {
+ const invokeCalls = isDeploymentTransaction
+ ? this.getCallsFromTx(transactions[1])
+ : this.getCallsFromTx(transactions[0])
+
+ 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)
+ throw new ReviewError({
+ message: `${error}`,
+ code: "SIMULATE_AND_REVIEW_FAILED",
+ })
+ }
+ }
+ }
+
+ async getLabels() {
+ await this.worker.maybeUpdateLabels()
+ const labels = await this.labelsStore.get("labels")
+ return labels
+ }
+}
diff --git a/packages/extension/src/background/__new/services/transactionReview/index.ts b/packages/extension/src/background/__new/services/transactionReview/index.ts
new file mode 100644
index 000000000..4c14d7bb6
--- /dev/null
+++ b/packages/extension/src/background/__new/services/transactionReview/index.ts
@@ -0,0 +1,13 @@
+import BackgroundTransactionReviewService from "./background"
+import { walletSingleton } from "../../../walletSingleton"
+import { httpService } from "../http/singleton"
+import { transactionReviewLabelsStore } from "../../../../shared/transactionReview/store"
+import { transactionReviewWorker } from "./worker"
+
+export const backgroundTransactionReviewService =
+ new BackgroundTransactionReviewService(
+ walletSingleton,
+ httpService,
+ transactionReviewLabelsStore,
+ transactionReviewWorker,
+ )
diff --git a/packages/extension/src/background/__new/services/transactionReview/worker/implementation.test.ts b/packages/extension/src/background/__new/services/transactionReview/worker/implementation.test.ts
new file mode 100644
index 000000000..04db799ae
--- /dev/null
+++ b/packages/extension/src/background/__new/services/transactionReview/worker/implementation.test.ts
@@ -0,0 +1,85 @@
+import { describe, expect, test, vi } from "vitest"
+
+import { IHttpService } from "@argent/shared"
+import { KeyValueStorage } from "../../../../../shared/storage"
+import { ITransactionReviewLabelsStore } from "../../../../../shared/transactionReview/interface"
+import { TransactionReviewWorker } from "./implementation"
+import { IBackgroundUIService } from "../../ui/interface"
+import { emitterMock } from "../../../../wallet/test.utils"
+import { delay } from "../../../../../shared/utils/delay"
+
+describe("TransactionReviewWorker", () => {
+ const makeService = () => {
+ const transactionReviewLabelsStore = {
+ get: vi.fn(),
+ set: vi.fn(),
+ subscribe: vi.fn(),
+ }
+
+ const httpService = {
+ get: vi.fn(),
+ }
+
+ const backgroundUIService = {
+ emitter: emitterMock,
+ }
+
+ const transactionReviewWorker = new TransactionReviewWorker(
+ transactionReviewLabelsStore as unknown as KeyValueStorage,
+ httpService as unknown as IHttpService,
+ backgroundUIService as unknown as IBackgroundUIService,
+ )
+
+ return {
+ transactionReviewWorker,
+ transactionReviewLabelsStore,
+ httpService,
+ }
+ }
+ test("maybeUpdateLabels", async () => {
+ const {
+ transactionReviewWorker,
+ transactionReviewLabelsStore,
+ httpService,
+ } = makeService()
+ /** wait for init call */
+ await delay(0)
+ expect(httpService.get).toHaveBeenCalledOnce()
+ httpService.get.mockReset()
+
+ httpService.get.mockResolvedValueOnce([{ key: "foo", value: "bar" }])
+
+ await transactionReviewWorker.maybeUpdateLabels()
+
+ expect(transactionReviewLabelsStore.get).toHaveBeenCalledWith("updatedAt")
+ expect(transactionReviewLabelsStore.get).toHaveBeenCalledWith("labels")
+ expect(httpService.get).toHaveBeenCalledOnce()
+ expect(transactionReviewLabelsStore.set).toHaveBeenCalledWith("labels", [
+ { key: "foo", value: "bar" },
+ ])
+
+ httpService.get.mockReset()
+
+ /** mock updated a minute ago */
+ const aMomentAgo = Date.now() - 1000 * 60
+ transactionReviewLabelsStore.get
+ .mockResolvedValueOnce(aMomentAgo)
+ .mockResolvedValueOnce([{ key: "foo", value: "bar" }])
+ await transactionReviewWorker.maybeUpdateLabels()
+ expect(httpService.get).not.toHaveBeenCalled()
+
+ httpService.get.mockReset()
+ httpService.get.mockResolvedValueOnce([{ key: "foo", value: "baz" }])
+
+ /** mock updated days ago */
+ const daysAgo = Date.now() - 1000 * 60 * 60 * 60 * 24
+ transactionReviewLabelsStore.get
+ .mockResolvedValueOnce(daysAgo)
+ .mockResolvedValueOnce([{ key: "foo", value: "bar" }])
+ await transactionReviewWorker.maybeUpdateLabels()
+ expect(httpService.get).toHaveBeenCalledOnce()
+ expect(transactionReviewLabelsStore.set).toHaveBeenCalledWith("labels", [
+ { key: "foo", value: "baz" },
+ ])
+ })
+})
diff --git a/packages/extension/src/background/__new/services/transactionReview/worker/implementation.ts b/packages/extension/src/background/__new/services/transactionReview/worker/implementation.ts
new file mode 100644
index 000000000..caf2a0785
--- /dev/null
+++ b/packages/extension/src/background/__new/services/transactionReview/worker/implementation.ts
@@ -0,0 +1,65 @@
+import { IHttpService } from "@argent/shared"
+
+import urlJoin from "url-join"
+import { RefreshInterval } from "../../../../../shared/config"
+import { KeyValueStorage } from "../../../../../shared/storage"
+import {
+ ITransactionReviewLabel,
+ ITransactionReviewLabelsStore,
+} from "../../../../../shared/transactionReview/interface"
+import { ITransactionReviewWorker } from "./interface"
+import { IBackgroundUIService, Opened } from "../../ui/interface"
+import { ARGENT_TRANSACTION_REVIEW_API_BASE_URL } from "../../../../../shared/api/constants"
+
+const REFRESH_PERIOD_MINUTES = RefreshInterval.VERY_SLOW
+const labelsEndpoint = urlJoin(
+ ARGENT_TRANSACTION_REVIEW_API_BASE_URL || "",
+ "labels",
+)
+
+export class TransactionReviewWorker implements ITransactionReviewWorker {
+ constructor(
+ private readonly labelsStore: KeyValueStorage,
+ private httpService: IHttpService,
+ private readonly backgroundUIService: IBackgroundUIService,
+ ) {
+ this.backgroundUIService.emitter.on(Opened, this.onOpened.bind(this))
+ void this.maybeUpdateLabels()
+ }
+
+ async onOpened(opened: boolean) {
+ if (opened) {
+ await this.maybeUpdateLabels()
+ }
+ }
+
+ /** only update if REFRESH_PERIOD_MINUTES have elapsed since last update */
+
+ async maybeUpdateLabels() {
+ const updatedAt = await this.labelsStore.get("updatedAt")
+ const labels = await this.labelsStore.get("labels")
+ if (updatedAt && labels) {
+ const currentTimestamp = Date.now()
+ const differenceInMilliseconds = currentTimestamp - updatedAt
+ const differenceInMinutes = differenceInMilliseconds / (1000 * 60) // Convert milliseconds to minutes
+ if (differenceInMinutes < REFRESH_PERIOD_MINUTES) {
+ return
+ }
+ }
+ await this.updateLabels()
+ }
+
+ async updateLabels() {
+ try {
+ const labels = await this.httpService.get(
+ labelsEndpoint,
+ )
+ const updatedAt = Date.now()
+ await this.labelsStore.set("labels", labels)
+ await this.labelsStore.set("updatedAt", updatedAt)
+ } catch (error) {
+ // ignore error - will retry next time
+ console.warn("Error fetching trasnaction review labels", error)
+ }
+ }
+}
diff --git a/packages/extension/src/background/__new/services/transactionReview/worker/index.ts b/packages/extension/src/background/__new/services/transactionReview/worker/index.ts
new file mode 100644
index 000000000..229e80167
--- /dev/null
+++ b/packages/extension/src/background/__new/services/transactionReview/worker/index.ts
@@ -0,0 +1,10 @@
+import { transactionReviewLabelsStore } from "../../../../../shared/transactionReview/store"
+import { httpService } from "../../http/singleton"
+import { backgroundUIService } from "../../ui"
+import { TransactionReviewWorker } from "./implementation"
+
+export const transactionReviewWorker = new TransactionReviewWorker(
+ transactionReviewLabelsStore,
+ httpService,
+ backgroundUIService,
+)
diff --git a/packages/extension/src/background/__new/services/transactionReview/worker/interface.ts b/packages/extension/src/background/__new/services/transactionReview/worker/interface.ts
new file mode 100644
index 000000000..549ccade6
--- /dev/null
+++ b/packages/extension/src/background/__new/services/transactionReview/worker/interface.ts
@@ -0,0 +1,3 @@
+export interface ITransactionReviewWorker {
+ maybeUpdateLabels(): Promise
+}
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 cd1ab2ce1..c21d3c273 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
@@ -15,7 +15,7 @@ import { getMockDebounceService } from "../../../../../shared/debounce/mock"
describe("decorators", () => {
describe("onStartup", () => {
it("should call scheduleService.onStartup with the correct arguments", () => {
- const scheduleService = createScheduleServiceMock()
+ const [, scheduleService] = createScheduleServiceMock()
const fn = async () => {}
onStartup(scheduleService)(fn)
expect(scheduleService.onStartup).toHaveBeenCalledWith({
@@ -27,7 +27,7 @@ describe("decorators", () => {
describe("onInstallAndUpgrade", () => {
it("should call scheduleService.onInstallAndUpgrade with the correct arguments", () => {
- const scheduleService = createScheduleServiceMock()
+ const [, scheduleService] = createScheduleServiceMock()
const fn = async () => {}
onInstallAndUpgrade(scheduleService)(fn)
expect(scheduleService.onInstallAndUpgrade).toHaveBeenCalledWith({
@@ -39,9 +39,9 @@ describe("decorators", () => {
describe("every", () => {
it("should call scheduleService.registerImplementation and scheduleService.every with the correct arguments", async () => {
- const scheduleService = createScheduleServiceMock()
+ const [, scheduleService] = createScheduleServiceMock()
const fn = async () => {}
- every(scheduleService, 1)(fn)
+ every(scheduleService, 1, "test")(fn)
expect(scheduleService.registerImplementation).toHaveBeenCalledWith({
id: expect.stringContaining("every@1s:"),
callback: fn,
@@ -70,7 +70,7 @@ describe("decorators", () => {
})
})
- describe("onOpen", async () => {
+ describe("onlyIfOpen", async () => {
test("should call the function when the background ui service is opened", async () => {
const [mockBackgroundUIServiceManager, mockBackgroundUIService] =
getMockBackgroundUIService()
@@ -94,13 +94,13 @@ describe("decorators", () => {
test("should call the function when not in debounce interval", async () => {
const debounceService = getMockDebounceService()
const fn = vi.fn()
- const debouncedFn = debounce(debounceService, 1)(fn)
+ const debouncedFn = debounce(debounceService, 1, "test")(fn)
expect(fn).toHaveBeenCalledTimes(0)
expect(debounceService.debounce).toHaveBeenCalledTimes(0)
await debouncedFn()
expect(debounceService.debounce).toHaveBeenCalledWith({
id: expect.stringContaining("debounce@1s:"),
- debounce: 1,
+ debounce: 1, // usually seconds, but mock implementation makes this ms
callback: fn,
})
})
@@ -110,15 +110,29 @@ describe("decorators", () => {
test("should call the function when the background ui service is opened", async () => {
const [mockBackgroundUIServiceManager, mockBackgroundUIService] =
getMockBackgroundUIService()
- const scheduleService = createScheduleServiceMock()
+ const [scheduleServiceManager, scheduleService] =
+ createScheduleServiceMock()
const debounceService = getMockDebounceService()
const fn = vi.fn()
- everyWhenOpen(
+ const fnExec = everyWhenOpen(
mockBackgroundUIService,
scheduleService,
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 onOpen
expect(fn).toHaveBeenCalledTimes(0)
await mockBackgroundUIServiceManager.setOpened(true)
expect(fn).toHaveBeenCalledTimes(1)
@@ -126,6 +140,27 @@ describe("decorators", () => {
expect(fn).toHaveBeenCalledTimes(1)
await mockBackgroundUIServiceManager.setOpened(true)
expect(fn).toHaveBeenCalledTimes(2)
+
+ // test scheduleService
+ await scheduleServiceManager.fireAll("every")
+ expect(fn).toHaveBeenCalledTimes(3)
+
+ // test debounce
+ expect(debounceService.debounce).toHaveBeenCalledTimes(3)
+ await fnExec()
+ expect(debounceService.debounce).toHaveBeenCalledTimes(4)
+ expect(debounceService.debounce).toHaveBeenCalledWith({
+ id: expect.stringContaining("debounce@1s:"),
+ debounce: 1,
+ callback: expect.any(Function),
+ })
+ expect(fn).toHaveBeenCalledTimes(4)
+
+ // does not call debounce when not open
+ await mockBackgroundUIServiceManager.setOpened(false)
+ await fnExec()
+ expect(debounceService.debounce).toHaveBeenCalledTimes(4)
+ expect(fn).toHaveBeenCalledTimes(4)
})
})
})
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 7727c3f66..90cf2295d 100644
--- a/packages/extension/src/background/__new/services/worker/schedule/decorators.ts
+++ b/packages/extension/src/background/__new/services/worker/schedule/decorators.ts
@@ -2,7 +2,6 @@ import { IDebounceService } from "../../../../../shared/debounce"
import { IScheduleService } from "../../../../../shared/schedule/interface"
import { IBackgroundUIService, Opened } from "../../ui/interface"
import { pipe } from "./pipe"
-import { nanoid } from "nanoid"
type Fn = (...args: unknown[]) => Promise
@@ -47,9 +46,13 @@ export const onInstallAndUpgrade =
* @returns {Function} The scheduled function.
*/
export const every =
- (scheduleService: IScheduleService, seconds: number) =>
+ (
+ scheduleService: IScheduleService,
+ seconds: number,
+ name: string,
+ ) =>
(fn: T): T => {
- const id = `every@${seconds}s:${nanoid()}`
+ const id = `every@${seconds}s:${name}`
void scheduleService
.registerImplementation({
id,
@@ -82,6 +85,18 @@ export const onOpen =
return fn
}
+export const onClose =
+ (backgroundUIService: MinimalIBackgroundUIService) =>
+ (fn: T): T => {
+ backgroundUIService.emitter.on(Opened, async (open) => {
+ if (!open) {
+ await fn()
+ }
+ })
+
+ return fn
+ }
+
function noopAs(_fn: T): T {
const noop = () => {}
return noop as T
@@ -108,9 +123,13 @@ export const onlyIfOpen =
* @returns {Promise} The debounced function.
*/
export const debounce =
- (debounceService: IDebounceService, seconds: number) =>
+ (
+ debounceService: IDebounceService,
+ seconds: number,
+ name: string,
+ ) =>
(fn: T): (() => Promise) => {
- const id = `debounce@${seconds}s:${nanoid()}`
+ const id = `debounce@${seconds}s:${name}`
const task = { id, callback: fn, debounce: seconds }
return () => {
@@ -131,11 +150,13 @@ export const everyWhenOpen = (
scheduleService: IScheduleService,
debounceService: IDebounceService,
seconds: number,
+ name: string,
) => {
return pipe(
- onOpen(backgroundUIService),
- every(scheduleService, seconds),
+ debounce(debounceService, seconds, name),
onlyIfOpen(backgroundUIService),
- debounce(debounceService, seconds),
+ onOpen(backgroundUIService),
+ onInstallAndUpgrade(scheduleService),
+ every(scheduleService, seconds, name),
)
}
diff --git a/packages/extension/src/background/__new/services/worker/schedule/pipe.ts b/packages/extension/src/background/__new/services/worker/schedule/pipe.ts
index 450d2ee51..46dbae1bd 100644
--- a/packages/extension/src/background/__new/services/worker/schedule/pipe.ts
+++ b/packages/extension/src/background/__new/services/worker/schedule/pipe.ts
@@ -11,7 +11,7 @@ type PipeReturn[]> = ReturnType<
>
/**
- * Function to pipe a series of functions together.
+ * Function to pipe a series of functions together. Calls the functions in order, passing the result of the previous function to the next function.
* @param {Function[]} fns - The functions to pipe.
* @returns {Function} The piped function.
*/
diff --git a/packages/extension/src/background/__new/trpc.ts b/packages/extension/src/background/__new/trpc.ts
index 84a6bc855..20390e295 100644
--- a/packages/extension/src/background/__new/trpc.ts
+++ b/packages/extension/src/background/__new/trpc.ts
@@ -2,11 +2,15 @@ import { initTRPC } from "@trpc/server"
import type { IArgentAccountServiceBackground } from "../../shared/argentAccount/service/interface"
import { BaseError } from "../../shared/errors/baseError"
+import { BaseError as SharedBaseError } from "@argent/shared"
import type { IMultisigService } from "../../shared/multisig/service/messaging/interface"
import { MessagingKeys } from "../keys/messagingKeys"
import { Wallet } from "../wallet"
import type { IBackgroundActionService } from "./services/action/interface"
-import { IRecoveryService } from "../../shared/recovery/service/interface"
+import type { ITransactionReviewService } from "../../shared/transactionReview/interface"
+import type { IRecoveryService } from "../../shared/recovery/service/interface"
+import type { IStarknetAddressService } from "@argent/shared"
+import type { INetworkService } from "../../shared/network/service/interface"
interface Context {
sender?: chrome.runtime.MessageSender
@@ -16,7 +20,10 @@ interface Context {
messagingKeys: MessagingKeys
argentAccountService: IArgentAccountServiceBackground
multisigService: IMultisigService
+ transactionReviewService: ITransactionReviewService
recoveryService: IRecoveryService
+ starknetAddressService: IStarknetAddressService
+ networkService: INetworkService
}
}
@@ -27,7 +34,7 @@ const t = initTRPC.context().create({
const { shape, error } = opts
const { cause } = error
- if (cause instanceof BaseError) {
+ if (cause instanceof BaseError || cause instanceof SharedBaseError) {
return {
...shape,
data: {
@@ -38,7 +45,10 @@ const t = initTRPC.context().create({
context: cause.context,
},
}
- } else if (cause?.cause instanceof BaseError) {
+ } else if (
+ cause?.cause instanceof BaseError ||
+ cause?.cause instanceof SharedBaseError
+ ) {
// The production build is nesting the error in another cause
const nestedCause = cause.cause
diff --git a/packages/extension/src/background/accountDeploy.ts b/packages/extension/src/background/accountDeploy.ts
index e17cc420c..876cb86f1 100644
--- a/packages/extension/src/background/accountDeploy.ts
+++ b/packages/extension/src/background/accountDeploy.ts
@@ -1,20 +1,51 @@
-import { BlockNumber, num } from "starknet"
-import { BaseWalletAccount, WalletAccount } from "../shared/wallet.model"
-import { IBackgroundActionService } from "./__new/services/action/interface"
+import { BlockNumber, CallData, num } from "starknet"
+import type { BaseWalletAccount, WalletAccount } from "../shared/wallet.model"
+import type { Wallet } from "./wallet"
+import type { IBackgroundActionService } from "./__new/services/action/interface"
export interface IDeployAccount {
account: BaseWalletAccount
actionService: IBackgroundActionService
+ wallet: Wallet
}
-export const deployAccountAction = async ({
+/** TODO: this should be in a service with dependency injection and tests */
+
+export const addDeployAccountAction = async ({
account,
actionService,
+ wallet,
}: IDeployAccount) => {
- await actionService.add({
- type: "DEPLOY_ACCOUNT_ACTION",
- payload: account,
- })
+ const walletAccount = await wallet.getAccount(account)
+ if (!walletAccount) {
+ throw new Error("Account not found")
+ }
+
+ /** determine the calldata to display to the end user */
+ let displayCalldata: string[] = []
+
+ try {
+ const deployAccountPayload =
+ await wallet.getAccountOrMultisigDeploymentPayload(walletAccount)
+ const { constructorCalldata } = deployAccountPayload
+ displayCalldata = CallData.toCalldata(constructorCalldata)
+ } catch {
+ /** ignore non-critical error */
+ }
+
+ await actionService.add(
+ {
+ type: "DEPLOY_ACCOUNT",
+ payload: {
+ account,
+ displayCalldata,
+ },
+ },
+ {
+ title: "Activate account",
+ icon: "DeployIcon",
+ },
+ )
}
export const isAccountDeployed = async (
@@ -31,7 +62,7 @@ export const isAccountDeployed = async (
await getClassAt(account.address)
return true
} catch (e) {
- console.error(e)
+ console.warn(e)
return false
}
}
diff --git a/packages/extension/src/background/accountDeployAction.ts b/packages/extension/src/background/accountDeployAction.ts
index 5a4f1aade..89f5bfc0a 100644
--- a/packages/extension/src/background/accountDeployAction.ts
+++ b/packages/extension/src/background/accountDeployAction.ts
@@ -1,21 +1,16 @@
-import { ExtQueueItem } from "../shared/actionQueue/types"
-import { BaseWalletAccount } from "../shared/wallet.model"
+import { ExtensionActionItemOfType } from "../shared/actionQueue/types"
import { addTransaction } from "./transactions/store"
import { checkTransactionHash } from "./transactions/transactionExecution"
import { Wallet } from "./wallet"
-type DeployAccountAction = ExtQueueItem<{
- type: "DEPLOY_ACCOUNT_ACTION"
- payload: BaseWalletAccount
-}>
-
export const accountDeployAction = async (
- { payload: baseAccount }: DeployAccountAction,
+ action: ExtensionActionItemOfType<"DEPLOY_ACCOUNT">,
wallet: Wallet,
) => {
if (!(await wallet.isSessionOpen())) {
throw Error("you need an open session")
}
+ const { account: baseAccount } = action.payload
const selectedAccount = await wallet.getAccount(baseAccount)
const accountNeedsDeploy = selectedAccount?.needsDeploy
diff --git a/packages/extension/src/background/accountUpgrade.ts b/packages/extension/src/background/accountUpgrade.ts
index de8604c16..00a59d006 100644
--- a/packages/extension/src/background/accountUpgrade.ts
+++ b/packages/extension/src/background/accountUpgrade.ts
@@ -4,9 +4,8 @@ import { networkService } from "../shared/network/service"
import { ArgentAccountType, BaseWalletAccount } from "../shared/wallet.model"
import { IBackgroundActionService } from "./__new/services/action/interface"
import { Wallet } from "./wallet"
-import { isAccountV5 } from "../shared/utils/accountv4"
import { AccountError } from "../shared/errors/account"
-
+import { isAccountV5 } from "@argent/shared"
export interface IUpgradeAccount {
account: BaseWalletAccount
wallet: Wallet
@@ -52,15 +51,21 @@ export const upgradeAccount = async ({
}
const calldata = CallData.compile(upgradeCalldata)
- await actionService.add({
- type: "TRANSACTION",
- payload: {
- transactions: {
- contractAddress: fullAccount.address,
- entrypoint: "upgrade",
- calldata,
+ await actionService.add(
+ {
+ type: "TRANSACTION",
+ payload: {
+ transactions: {
+ contractAddress: fullAccount.address,
+ entrypoint: "upgrade",
+ calldata,
+ },
+ meta: { isUpgrade: true, title: "Switch account type" },
},
- meta: { isUpgrade: true, title: "Switch account type" },
},
- })
+ {
+ title: "Upgrade account",
+ icon: "UpgradeIcon",
+ },
+ )
}
diff --git a/packages/extension/src/background/actionHandlers.ts b/packages/extension/src/background/actionHandlers.ts
index be839a94e..51d9ea804 100644
--- a/packages/extension/src/background/actionHandlers.ts
+++ b/packages/extension/src/background/actionHandlers.ts
@@ -9,12 +9,60 @@ import { isEqualWalletAddress } from "../shared/wallet.service"
import { assertNever } from "../ui/services/assertNever"
import { accountDeployAction } from "./accountDeployAction"
import { analytics } from "./analytics"
-import { multisigDeployAction } from "./multisig/multisigDeployAction"
+import { addMultisigDeployAction } from "./multisig/multisigDeployAction"
import { openUi } from "./openUi"
-import { executeTransactionAction } from "./transactions/transactionExecution"
+import {
+ TransactionAction,
+ executeTransactionAction,
+} from "./transactions/transactionExecution"
import { udcDeclareContract, udcDeployContract } from "./udcAction"
import { Wallet } from "./wallet"
+import { networkSchema } from "../shared/network"
+import { encodeChainId } from "../shared/utils/encodeChainId"
+
+const handleTransactionAction = async ({
+ action,
+ networkId,
+ wallet,
+}: {
+ action: TransactionAction
+ networkId: string
+ wallet: Wallet
+}): Promise => {
+ const host = action.meta.origin
+ const actionHash = action.meta.hash
+
+ try {
+ void analytics.track("signedTransaction", {
+ networkId,
+ host,
+ })
+
+ const response = await executeTransactionAction(action, wallet)
+ void analytics.track("sentTransaction", {
+ success: true,
+ networkId,
+ host,
+ })
+
+ return {
+ type: "TRANSACTION_SUBMITTED",
+ data: { txHash: response.transaction_hash, actionHash },
+ }
+ } catch (error) {
+ void analytics.track("sentTransaction", {
+ success: false,
+ networkId,
+ host,
+ })
+
+ return {
+ type: "TRANSACTION_FAILED",
+ data: { actionHash, error: `${error}` },
+ }
+ }
+}
export const handleActionApproval = async (
action: ExtensionActionItem,
wallet: Wallet,
@@ -43,40 +91,10 @@ export const handleActionApproval = async (
}
case "TRANSACTION": {
- const host = action.meta.origin
- try {
- void analytics.track("signedTransaction", {
- networkId,
- host,
- })
-
- const response = await executeTransactionAction(action, wallet)
-
- void analytics.track("sentTransaction", {
- success: true,
- networkId,
- host,
- })
-
- return {
- type: "TRANSACTION_SUBMITTED",
- data: { txHash: response.transaction_hash, actionHash },
- }
- } catch (error) {
- void analytics.track("sentTransaction", {
- success: false,
- networkId,
- host,
- })
-
- return {
- type: "TRANSACTION_FAILED",
- data: { actionHash, error: `${error}` },
- }
- }
+ return handleTransactionAction({ action, networkId, wallet })
}
- case "DEPLOY_ACCOUNT_ACTION": {
+ case "DEPLOY_ACCOUNT": {
try {
void analytics.track("signedTransaction", {
networkId,
@@ -87,7 +105,7 @@ export const handleActionApproval = async (
void analytics.track("deployAccount", {
status: "success",
trigger: "sign",
- networkId: action.payload.networkId,
+ networkId: action.payload.account.networkId,
})
void analytics.track("sentTransaction", {
@@ -112,7 +130,7 @@ export const handleActionApproval = async (
void analytics.track("deployAccount", {
status: "failure",
- networkId: action.payload.networkId,
+ networkId: action.payload.account.networkId,
errorMessage: `${error}`,
})
@@ -123,18 +141,18 @@ export const handleActionApproval = async (
}
}
- case "DEPLOY_MULTISIG_ACTION": {
+ case "DEPLOY_MULTISIG": {
try {
void analytics.track("signedTransaction", {
networkId,
})
- const txHash = await multisigDeployAction(action, wallet)
+ const txHash = await addMultisigDeployAction(action, wallet)
void analytics.track("deployMultisig", {
status: "success",
trigger: "transaction",
- networkId: action.payload.networkId,
+ networkId: action.payload.account.networkId,
})
void analytics.track("sentTransaction", {
@@ -156,7 +174,7 @@ export const handleActionApproval = async (
void analytics.track("deployMultisig", {
status: "failure",
- networkId: action.payload.networkId,
+ networkId: action.payload.account.networkId,
errorMessage: `${error}`,
})
break
@@ -169,11 +187,40 @@ export const handleActionApproval = async (
options: { skipDeploy = false },
} = action.payload
if (!(await wallet.isSessionOpen())) {
- throw Error("you need an open session")
+ throw new Error("you need an open session")
}
const starknetAccount = await wallet.getSelectedStarknetAccount()
const selectedAccount = await wallet.getSelectedAccount()
+ if (!selectedAccount) {
+ return {
+ type: "SIGNATURE_FAILURE",
+ data: {
+ error: "No selected account",
+ actionHash,
+ },
+ }
+ }
+
+ // let's compare encoded formats of both chainIds
+ const encodedDomainChainId = encodeChainId(typedData.domain.chainId)
+ const encodedSelectedChainId = encodeChainId(
+ selectedAccount.network.chainId,
+ )
+ // typedData.domain.chainId is optional, so we need to check if it exists
+ if (
+ encodedDomainChainId &&
+ encodedSelectedChainId !== encodedDomainChainId
+ ) {
+ return {
+ type: "SIGNATURE_FAILURE",
+ data: {
+ error: `Cannot sign the message from a different chainId. Expected ${encodedSelectedChainId}, got ${encodedDomainChainId}`,
+ actionHash,
+ },
+ }
+ }
+
const signature = await starknetAccount.signMessage(typedData)
const formattedSignature = stark.signatureToDecimalArray(signature)
@@ -199,7 +246,8 @@ export const handleActionApproval = async (
case "REQUEST_ADD_CUSTOM_NETWORK": {
try {
- await networkService.add(action.payload)
+ const parsedNetwork = networkSchema.parse(action.payload)
+ await networkService.add(parsedNetwork)
return {
type: "APPROVE_REQUEST_ADD_CUSTOM_NETWORK",
data: { actionHash },
@@ -258,7 +306,7 @@ export const handleActionApproval = async (
}
}
- case "DECLARE_CONTRACT_ACTION": {
+ case "DECLARE_CONTRACT": {
try {
void analytics.track("signedDeclareTransaction", {
networkId,
@@ -293,7 +341,7 @@ export const handleActionApproval = async (
}
}
- case "DEPLOY_CONTRACT_ACTION": {
+ case "DEPLOY_CONTRACT": {
try {
void analytics.track("signedDeployTransaction", {
networkId,
@@ -363,21 +411,21 @@ export const handleActionRejection = async (
}
}
- case "DEPLOY_ACCOUNT_ACTION": {
+ case "DEPLOY_ACCOUNT": {
return {
type: "DEPLOY_ACCOUNT_ACTION_FAILED",
data: { actionHash },
}
}
- case "DEPLOY_MULTISIG_ACTION": {
+ case "DEPLOY_MULTISIG": {
break
}
case "SIGN": {
return {
type: "SIGNATURE_FAILURE",
- data: { actionHash },
+ data: { actionHash, error: "User rejected" },
}
}
@@ -402,13 +450,13 @@ export const handleActionRejection = async (
}
}
- case "DECLARE_CONTRACT_ACTION": {
+ case "DECLARE_CONTRACT": {
return {
type: "REQUEST_DECLARE_CONTRACT_REJ",
data: { actionHash },
}
}
- case "DEPLOY_CONTRACT_ACTION": {
+ case "DEPLOY_CONTRACT": {
return {
type: "REQUEST_DEPLOY_CONTRACT_REJ",
data: { actionHash },
diff --git a/packages/extension/src/background/actionMessaging.ts b/packages/extension/src/background/actionMessaging.ts
index dd4ebed55..c1bcc8a2f 100644
--- a/packages/extension/src/background/actionMessaging.ts
+++ b/packages/extension/src/background/actionMessaging.ts
@@ -17,6 +17,7 @@ export const handleActionMessage: HandleMessage = async ({
},
{
origin,
+ title: "Review signature request",
},
)
diff --git a/packages/extension/src/background/devnet/declareAccounts.ts b/packages/extension/src/background/devnet/declareAccounts.ts
index bdbbf1794..57280dce0 100644
--- a/packages/extension/src/background/devnet/declareAccounts.ts
+++ b/packages/extension/src/background/devnet/declareAccounts.ts
@@ -8,7 +8,6 @@ import {
ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES,
PROXY_CONTRACT_CLASS_HASHES,
} from "../wallet/starknet.constants"
-import { getNetworkUrl } from "../../shared/network/utils"
interface PreDeployedAccount {
address: string
@@ -20,7 +19,7 @@ export const getPreDeployedAccount = async (
index = 0,
): Promise => {
try {
- const networkUrl = getNetworkUrl(network)
+ const networkUrl = network.rpcUrl
const preDeployedAccounts = await fetch(
urlJoin(networkUrl, "predeployed_accounts"),
@@ -101,7 +100,7 @@ export const declareContracts = memoize(
accountClassHash: accountClassHash ?? computedAccountClassHash,
}
},
- (network) => `${network.sequencerUrl}`,
+ (network) => `${network.rpcUrl}`,
)
export const checkIfClassIsDeclared = async (
diff --git a/packages/extension/src/background/messageHandling/messages.ts b/packages/extension/src/background/messageHandling/messages.ts
index 1db62915d..43ebbd705 100644
--- a/packages/extension/src/background/messageHandling/messages.ts
+++ b/packages/extension/src/background/messageHandling/messages.ts
@@ -36,5 +36,4 @@ export const safeIfPreauthorizedMessages: MessageType["type"][] = [
"REQUEST_TOKEN",
"REQUEST_ADD_CUSTOM_NETWORK",
"REQUEST_SWITCH_CUSTOM_NETWORK",
- "REQUEST_DECLARE_CONTRACT",
]
diff --git a/packages/extension/src/background/migrations/index.ts b/packages/extension/src/background/migrations/index.ts
index 50641e61b..80815da87 100644
--- a/packages/extension/src/background/migrations/index.ts
+++ b/packages/extension/src/background/migrations/index.ts
@@ -1,6 +1,6 @@
import { walletSessionServiceEmitter } from "../walletSingleton"
import { Locked } from "../wallet/session/interface"
-import { runRemoveTestnet2Migration } from "./network/removeTestnet2"
+import { restoreDefaultNetworks } from "./network/restoreDefaultNetworksMigration"
import { KeyValueStorage } from "../../shared/storage"
import { runRemoveTestnet2Accounts, runV581Migration } from "./wallet"
import { runV59TokenMigration } from "./token/v5.9"
@@ -12,7 +12,7 @@ enum WalletMigrations {
}
enum NetworkMigrations {
- removeTestnet2 = "network:removeTestnet2",
+ rpcEverywhere = "network:rpcEverywhere",
}
enum TokenMigrations {
@@ -24,7 +24,7 @@ const migrationsStore = new KeyValueStorage(
{
[WalletMigrations.v581]: false,
[WalletMigrations.removeTestnet2Accounts]: false,
- [NetworkMigrations.removeTestnet2]: false,
+ [NetworkMigrations.rpcEverywhere]: false,
[TokenMigrations.v59]: false,
[TokenMigrations.v510]: false,
},
@@ -37,8 +37,8 @@ export const migrationListener = walletSessionServiceEmitter.on(
if (!locked) {
// TODO: come up with a better, generic mechanism for this
const v581Migration = await migrationsStore.get(WalletMigrations.v581)
- const networkMigration = await migrationsStore.get(
- NetworkMigrations.removeTestnet2,
+ const rpcEverywhereMigration = await migrationsStore.get(
+ NetworkMigrations.rpcEverywhere,
)
const removeTestnet2Accounts = await migrationsStore.get(
WalletMigrations.removeTestnet2Accounts,
@@ -53,9 +53,9 @@ export const migrationListener = walletSessionServiceEmitter.on(
await runRemoveTestnet2Accounts()
await migrationsStore.set(WalletMigrations.removeTestnet2Accounts, true)
}
- if (!networkMigration) {
- await runRemoveTestnet2Migration()
- await migrationsStore.set(NetworkMigrations.removeTestnet2, true)
+ if (!rpcEverywhereMigration) {
+ await restoreDefaultNetworks()
+ await migrationsStore.set(NetworkMigrations.rpcEverywhere, true)
}
if (!v59Migration) {
diff --git a/packages/extension/src/background/migrations/network/removeTestnet2.ts b/packages/extension/src/background/migrations/network/restoreDefaultNetworksMigration.ts
similarity index 67%
rename from packages/extension/src/background/migrations/network/removeTestnet2.ts
rename to packages/extension/src/background/migrations/network/restoreDefaultNetworksMigration.ts
index ccf2660d2..6a3989189 100644
--- a/packages/extension/src/background/migrations/network/removeTestnet2.ts
+++ b/packages/extension/src/background/migrations/network/restoreDefaultNetworksMigration.ts
@@ -1,5 +1,5 @@
import { networkService } from "../../../shared/network/service"
-export async function runRemoveTestnet2Migration() {
+export async function restoreDefaultNetworks() {
await networkService.restoreDefaults()
}
diff --git a/packages/extension/src/background/migrations/wallet/testnet2Accounts.ts b/packages/extension/src/background/migrations/wallet/testnet2Accounts.ts
index 949fa0015..1b7aaecc6 100644
--- a/packages/extension/src/background/migrations/wallet/testnet2Accounts.ts
+++ b/packages/extension/src/background/migrations/wallet/testnet2Accounts.ts
@@ -1,9 +1,9 @@
-import {
+import type {
IObjectStore,
IRepository,
} from "../../../shared/storage/__new/interface"
-import { WalletAccount } from "../../../shared/wallet.model"
-import { WalletStorageProps } from "../../wallet/account/shared.service"
+import type { WalletAccount } from "../../../shared/wallet.model"
+import type { WalletStorageProps } from "../../../shared/wallet/walletStore"
export async function getTestnet2Accounts(
walletStore: IRepository,
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 1716b1caa..bf60171e8 100644
--- a/packages/extension/src/background/migrations/wallet/v5.8.1.ts
+++ b/packages/extension/src/background/migrations/wallet/v5.8.1.ts
@@ -7,7 +7,7 @@ import {
import { BaseWalletAccount, WalletAccount } from "../../../shared/wallet.model"
import { accountsEqual } from "../../../shared/utils/accountsEqual"
import { WalletCryptoStarknetService } from "../../wallet/crypto/starknet.service"
-import { WalletStorageProps } from "../../wallet/account/shared.service"
+import { WalletStorageProps } from "../../../shared/wallet/walletStore"
export async function determineMigrationNeededV581(
cryptoStarknetService: WalletCryptoStarknetService,
diff --git a/packages/extension/src/background/multisig/multisigDeployAction.ts b/packages/extension/src/background/multisig/multisigDeployAction.ts
index 316b29757..d42fc2cae 100644
--- a/packages/extension/src/background/multisig/multisigDeployAction.ts
+++ b/packages/extension/src/background/multisig/multisigDeployAction.ts
@@ -1,24 +1,19 @@
import { num } from "starknet"
-import { ExtQueueItem } from "../../shared/actionQueue/types"
-import { BaseWalletAccount } from "../../shared/wallet.model"
+import { ExtensionActionItemOfType } from "../../shared/actionQueue/types"
import { addTransaction } from "../transactions/store"
import { checkTransactionHash } from "../transactions/transactionExecution"
-import { argentMaxFee } from "../utils/argentMaxFee"
+import { argentMaxFee } from "../../shared/utils/argentMaxFee"
import { Wallet } from "../wallet"
-type DeployMultisigAction = ExtQueueItem<{
- type: "DEPLOY_MULTISIG_ACTION"
- payload: BaseWalletAccount
-}>
-
-export const multisigDeployAction = async (
- { payload: baseAccount }: DeployMultisigAction,
+export const addMultisigDeployAction = async (
+ action: ExtensionActionItemOfType<"DEPLOY_MULTISIG">,
wallet: Wallet,
) => {
if (!(await wallet.isSessionOpen())) {
throw Error("you need an open session")
}
+ const { account: baseAccount } = action.payload
const selectedMultisig = await wallet.getMultisigAccount(baseAccount)
const multisigNeedsDeploy = selectedMultisig.needsDeploy
@@ -34,10 +29,10 @@ export const multisigDeployAction = async (
selectedMultisig,
)
- maxFee = argentMaxFee(suggestedMaxFee)
+ maxFee = argentMaxFee({ suggestedMaxFee: suggestedMaxFee })
} catch (error) {
const fallbackPrice = num.toBigInt(10e14)
- maxFee = argentMaxFee(fallbackPrice)
+ maxFee = argentMaxFee({ suggestedMaxFee: fallbackPrice })
}
const { account, txHash } = await wallet.deployAccount(selectedMultisig, {
diff --git a/packages/extension/src/background/multisig/worker/implementation.ts b/packages/extension/src/background/multisig/worker/implementation.ts
index a157e9ef0..39b4d1937 100644
--- a/packages/extension/src/background/multisig/worker/implementation.ts
+++ b/packages/extension/src/background/multisig/worker/implementation.ts
@@ -1,10 +1,8 @@
import { flatMap, isEmpty, partition } from "lodash-es"
import { hash, transaction } from "starknet"
+import { getChainIdFromNetworkId } from "@argent/shared"
-import {
- IBackgroundUIService,
- Opened,
-} from "../../../background/__new/services/ui/interface"
+import { IBackgroundUIService } from "../../../background/__new/services/ui/interface"
import { IScheduleService } from "../../../shared/schedule/interface"
import { IMultisigBackendService } from "../../../shared/multisig/service/backend/interface"
import { INetworkService } from "../../../shared/network/service/interface"
@@ -33,7 +31,6 @@ import {
multisigPendingTransactionToTransaction,
} from "../../../shared/multisig/pendingTransactionsStore"
import { getMultisigTransactionType } from "../../../shared/multisig/utils/getMultisigTransactionType"
-import { getChainIdFromNetworkId } from "../../../shared/network/utils"
import { everyWhenOpen } from "../../__new/services/worker/schedule/decorators"
import { IDebounceService } from "../../../shared/debounce"
import { pipe } from "../../__new/services/worker/schedule/pipe"
@@ -57,6 +54,7 @@ export class MultisigWorker {
this.scheduleService,
this.debounceService,
RefreshInterval.FAST,
+ "MultisigWorker.updateAll",
),
)(async (): Promise => {
console.log("Updating multisig data")
diff --git a/packages/extension/src/background/nonce.ts b/packages/extension/src/background/nonce.ts
index d5880e065..935528ff1 100644
--- a/packages/extension/src/background/nonce.ts
+++ b/packages/extension/src/background/nonce.ts
@@ -4,8 +4,6 @@ import { KeyValueStorage } from "../shared/storage"
import { BaseWalletAccount, WalletAccount } from "../shared/wallet.model"
import { getAccountIdentifier } from "../shared/wallet.service"
-import { Account as AccountV4__deprecated } from "starknet4-deprecated"
-
const nonceStore = new KeyValueStorage>(
{},
{
@@ -16,7 +14,7 @@ const nonceStore = new KeyValueStorage>(
export async function getNonce(
account: WalletAccount,
- starknetAccount: Account | AccountV4__deprecated,
+ starknetAccount: Account,
): Promise {
const storageAddress = getAccountIdentifier(account)
const result = await starknetAccount.getNonce()
diff --git a/packages/extension/src/background/transactions/service/starknet.service.ts b/packages/extension/src/background/transactions/service/starknet.service.ts
index 03d1a9137..756ae3afb 100644
--- a/packages/extension/src/background/transactions/service/starknet.service.ts
+++ b/packages/extension/src/background/transactions/service/starknet.service.ts
@@ -146,6 +146,7 @@ export class TransactionTrackerWorker
}
async syncTransactionRepo() {
const allTransactions = await this.transactionsRepo.get()
+
const updatedTransactions = await getTransactionsUpdate(
// is smart enough to filter for just the pending transactions, as the rest needs no update
allTransactions,
diff --git a/packages/extension/src/background/transactions/sources/onchain.ts b/packages/extension/src/background/transactions/sources/onchain.ts
index b2cb34844..ed5121a9d 100644
--- a/packages/extension/src/background/transactions/sources/onchain.ts
+++ b/packages/extension/src/background/transactions/sources/onchain.ts
@@ -14,39 +14,46 @@ export async function getTransactionsUpdate(transactions: Transaction[]) {
const fetchedTransactions = await Promise.allSettled(
transactionsToCheck.map(async (transaction) => {
const provider = getProvider(transaction.account.network)
- const tx = await provider.getTransactionReceipt(transaction.hash)
+ const { finality_status, execution_status } =
+ await provider.getTransactionStatus(transaction.hash)
+
+ const isFailed =
+ execution_status === "REVERTED" || finality_status === "REJECTED"
+ if (!isFailed) {
+ return {
+ ...transaction,
+ finalityStatus: finality_status as TransactionFinalityStatus,
+ executionStatus: execution_status as TransactionExecutionStatus,
+ }
+ }
- let updatedTransaction: Transaction
+ const tx = await provider.getTransactionReceipt(transaction.hash)
// Handle Reverted transaction
if ("revert_reason" in tx) {
- updatedTransaction = {
+ const finalityStatus =
+ (tx.finality_status as TransactionFinalityStatus) ||
+ "status" in tx ||
+ TransactionFinalityStatus.NOT_RECEIVED // For backward compatibility on mainnet
+
+ return {
...transaction,
- finalityStatus:
- tx.finality_status ||
- tx.status ||
- TransactionFinalityStatus.NOT_RECEIVED, // For backward compatibility on mainnet
+ finalityStatus,
revertReason: tx.revert_reason,
}
// Handle Rejected transaction
} else if ("transaction_failure_reason" in tx) {
- updatedTransaction = {
+ const anyTx = tx as any
+ return {
...transaction,
- finalityStatus: tx.status ?? TransactionFinalityStatus.RECEIVED,
+ finalityStatus: anyTx.status ?? TransactionFinalityStatus.RECEIVED,
executionStatus: TransactionExecutionStatus.REJECTED,
- failureReason: tx.transaction_failure_reason,
- }
- } else {
- // Handle successful transaction
- updatedTransaction = {
- ...transaction,
- finalityStatus: tx.finality_status || tx.status, // For backward compatibility on mainnet
- executionStatus: tx.execution_status,
+ failureReason: anyTx.transaction_failure_reason,
}
}
- return updatedTransaction
+ return transaction
}),
)
diff --git a/packages/extension/src/background/transactions/transactionExecution.ts b/packages/extension/src/background/transactions/transactionExecution.ts
index 192172ddd..3297a58d9 100644
--- a/packages/extension/src/background/transactions/transactionExecution.ts
+++ b/packages/extension/src/background/transactions/transactionExecution.ts
@@ -18,12 +18,11 @@ import { accountsEqual } from "../../shared/utils/accountsEqual"
import { isAccountDeployed } from "../accountDeploy"
import { analytics } from "../analytics"
import { getNonce, increaseStoredNonce, resetStoredNonce } from "../nonce"
-import { argentMaxFee } from "../utils/argentMaxFee"
import { Wallet } from "../wallet"
import { getEstimatedFees } from "../../shared/transactionSimulation/fees/estimatedFeesRepository"
import { addTransaction, transactionsStore } from "./store"
-import { isAccountV5 } from "../../shared/utils/accountv4"
import { getMultisigAccountFromBaseWallet } from "../../shared/multisig/utils/baseMultisig"
+import { isAccountV5 } from "@argent/shared"
export const checkTransactionHash = (
transactionHash?: num.BigNumberish,
@@ -43,7 +42,7 @@ export const checkTransactionHash = (
}
}
-type TransactionAction = ExtQueueItem<{
+export type TransactionAction = ExtQueueItem<{
type: "TRANSACTION"
payload: TransactionActionPayload
}>
@@ -64,8 +63,8 @@ export const executeTransactionAction = async (
transactionsDetail?.maxFee ?? preComputedFees.suggestedMaxFee
const suggestedMaxADFee = preComputedFees.maxADFee ?? "0"
- const maxFee = argentMaxFee(suggestedMaxFee)
- const maxADFee = argentMaxFee(suggestedMaxADFee)
+ const maxFee = suggestedMaxFee
+ const maxADFee = suggestedMaxADFee
void analytics.track("executeTransaction", {
usesCachedFees: Boolean(preComputedFees),
diff --git a/packages/extension/src/background/transactions/transactionMessaging.ts b/packages/extension/src/background/transactions/transactionMessaging.ts
index 9c0dbf9c6..ade7ce572 100644
--- a/packages/extension/src/background/transactions/transactionMessaging.ts
+++ b/packages/extension/src/background/transactions/transactionMessaging.ts
@@ -4,7 +4,6 @@ import {
TransactionType,
hash,
num,
- stark,
transaction,
} from "starknet"
@@ -16,17 +15,14 @@ import {
import { getErrorObject } from "../../shared/utils/error"
import { isAccountDeployed } from "../accountDeploy"
import { HandleMessage, UnhandledMessage } from "../background"
-import {
- isAccountV4__deprecated,
- isAccountV5,
-} from "../../shared/utils/accountv4"
-import { argentMaxFee } from "../utils/argentMaxFee"
+import { argentMaxFee } from "../../shared/utils/argentMaxFee"
import { addEstimatedFees } from "../../shared/transactionSimulation/fees/estimatedFeesRepository"
import { transactionCallsAdapter } from "./transactionAdapter"
import { AccountError } from "../../shared/errors/account"
import { fetchTransactionBulkSimulation } from "../../shared/transactionSimulation/transactionSimulation.service"
import { TransactionError } from "../../shared/errors/transaction"
import { getEstimatedFeeFromSimulation } from "../../shared/transactionSimulation/utils"
+import { isAccountV4, isAccountV5 } from "@argent/shared"
export const handleTransactionMessage: HandleMessage<
TransactionMessage
@@ -40,6 +36,8 @@ export const handleTransactionMessage: HandleMessage<
},
{
origin,
+ title: "Review transaction",
+ icon: "NetworkIcon",
},
)
return respond({
@@ -51,7 +49,6 @@ export const handleTransactionMessage: HandleMessage<
case "ESTIMATE_TRANSACTION_FEE": {
const selectedAccount = await wallet.getSelectedAccount()
const transactions = msg.data
- const oldAccountTransactions = transactionCallsAdapter(transactions)
if (!selectedAccount) {
throw new AccountError({ code: "NOT_FOUND" })
@@ -93,21 +90,24 @@ export const handleTransactionMessage: HandleMessage<
accountDeploymentFee = num.toHex(estimateFeeBulk[0].overall_fee)
txFee = num.toHex(estimateFeeBulk[1].overall_fee)
- maxADFee = argentMaxFee(estimateFeeBulk[0].suggestedMaxFee)
- maxTxFee = argentMaxFee(estimateFeeBulk[1].suggestedMaxFee)
+ maxADFee = argentMaxFee({
+ suggestedMaxFee: estimateFeeBulk[0].suggestedMaxFee,
+ })
+ maxTxFee = argentMaxFee({
+ suggestedMaxFee: estimateFeeBulk[1].suggestedMaxFee,
+ })
}
} else {
- const { overall_fee, suggestedMaxFee } = isAccountV5(starknetAccount)
- ? await starknetAccount.estimateFee(transactions, {
- skipValidate: true,
- })
- : await starknetAccount.estimateFee(oldAccountTransactions)
+ const { overall_fee, suggestedMaxFee } =
+ await starknetAccount.estimateFee(transactions, {
+ skipValidate: true,
+ })
txFee = num.toHex(overall_fee)
maxTxFee = num.toHex(suggestedMaxFee) // Here, maxFee = estimatedFee * 1.5x
}
- const suggestedMaxFee = argentMaxFee(maxTxFee)
+ const suggestedMaxFee = argentMaxFee({ suggestedMaxFee: maxTxFee })
await addEstimatedFees(
{
@@ -153,10 +153,7 @@ export const handleTransactionMessage: HandleMessage<
const { overall_fee, suggestedMaxFee } =
await wallet.getAccountDeploymentFee(account)
- const maxADFee = num.toHex(
- stark.estimatedFeeToMaxFee(suggestedMaxFee, 1), // This adds the 1.5x overhead. i.e: suggestedMaxFee = maxFee * 2x = estimatedFee * 1.5x
- )
-
+ const maxADFee = argentMaxFee({ suggestedMaxFee })
return respond({
type: "ESTIMATE_ACCOUNT_DEPLOYMENT_FEE_RES",
data: {
@@ -173,7 +170,7 @@ export const handleTransactionMessage: HandleMessage<
type: "ESTIMATE_ACCOUNT_DEPLOYMENT_FEE_RES",
data: {
amount: num.toHex(fallbackPrice),
- maxADFee: argentMaxFee(fallbackPrice),
+ maxADFee: argentMaxFee({ suggestedMaxFee: fallbackPrice }),
},
})
}
@@ -241,7 +238,9 @@ export const handleTransactionMessage: HandleMessage<
accountDeploymentFee = num.toHex(estimateFeeBulk[0].overall_fee)
txFee = num.toHex(estimateFeeBulk[1].overall_fee)
- maxADFee = argentMaxFee(estimateFeeBulk[0].suggestedMaxFee)
+ maxADFee = argentMaxFee({
+ suggestedMaxFee: estimateFeeBulk[0].suggestedMaxFee,
+ })
maxTxFee = estimateFeeBulk[1].suggestedMaxFee.toString()
}
} else {
@@ -257,7 +256,7 @@ export const handleTransactionMessage: HandleMessage<
}
}
- const suggestedMaxFee = argentMaxFee(maxTxFee) // This add the 1.5x overhead. i.e: suggestedMaxFee = maxFee * 2x = estimatedFee * 1.5x
+ const suggestedMaxFee = argentMaxFee({ suggestedMaxFee: maxTxFee }) // This add the 1.5x overhead. i.e: suggestedMaxFee = maxFee * 2x = estimatedFee * 1.5x
return respond({
type: "ESTIMATE_DECLARE_CONTRACT_FEE_RES",
@@ -330,7 +329,9 @@ export const handleTransactionMessage: HandleMessage<
accountDeploymentFee = num.toHex(estimateFeeBulk[0].overall_fee)
txFee = num.toHex(estimateFeeBulk[1].overall_fee)
- maxADFee = argentMaxFee(estimateFeeBulk[0].suggestedMaxFee)
+ maxADFee = argentMaxFee({
+ suggestedMaxFee: estimateFeeBulk[0].suggestedMaxFee,
+ })
maxTxFee = estimateFeeBulk[1].suggestedMaxFee.toString()
}
} else {
@@ -349,7 +350,7 @@ export const handleTransactionMessage: HandleMessage<
}
}
- const suggestedMaxFee = argentMaxFee(maxTxFee) // This adds the 1.5x overhead. i.e: suggestedMaxFee = maxFee * 2x = estimatedFee * 1.5x
+ const suggestedMaxFee = argentMaxFee({ suggestedMaxFee: maxTxFee }) // This adds the 1.5x overhead. i.e: suggestedMaxFee = maxFee * 2x = estimatedFee * 1.5x
return respond({
type: "ESTIMATE_DEPLOY_CONTRACT_FEE_RES",
@@ -475,7 +476,7 @@ export const handleTransactionMessage: HandleMessage<
throw new AccountError({ code: "NOT_FOUND" })
}
const starknetAccount = await wallet.getSelectedStarknetAccount()
- if (isAccountV4__deprecated(starknetAccount)) {
+ if (isAccountV4(starknetAccount)) {
// Old accounts are not supported
// This should no longer happen as we prevent deprecated accounts from being used
return respond({
@@ -565,7 +566,7 @@ export const handleTransactionMessage: HandleMessage<
data: simulationWithFees,
})
} catch (error) {
- console.error("SIMULATE_TRANSACTIONS_REJ", error)
+ console.error("SIMULATE_TRANSACTIONS_REJ", error, "kek")
return respond({
type: "SIMULATE_TRANSACTIONS_REJ",
data: {
diff --git a/packages/extension/src/background/udcAction.ts b/packages/extension/src/background/udcAction.ts
index 40bd437b7..97cba8a24 100644
--- a/packages/extension/src/background/udcAction.ts
+++ b/packages/extension/src/background/udcAction.ts
@@ -14,7 +14,7 @@ import { analytics } from "./analytics"
import { getNonce, increaseStoredNonce } from "./nonce"
import { addTransaction } from "./transactions/store"
import { checkTransactionHash } from "./transactions/transactionExecution"
-import { argentMaxFee } from "./utils/argentMaxFee"
+import { argentMaxFee } from "../shared/utils/argentMaxFee"
import { Wallet } from "./wallet"
import { AccountError } from "../shared/errors/account"
import { WalletError } from "../shared/errors/wallet"
@@ -23,12 +23,12 @@ import { UdcError } from "../shared/errors/udc"
const { UDC } = constants
type DeclareContractAction = ExtQueueItem<{
- type: "DECLARE_CONTRACT_ACTION"
+ type: "DECLARE_CONTRACT"
payload: DeclareContractPayload
}>
type DeployContractAction = ExtQueueItem<{
- type: "DEPLOY_CONTRACT_ACTION"
+ type: "DEPLOY_CONTRACT"
payload: UniversalDeployerContractPayload
}>
@@ -84,8 +84,12 @@ export const udcDeclareContract = async (
{ skipValidate: true },
)
- maxADFee = argentMaxFee(estimateFeeBulk[0].suggestedMaxFee)
- maxDeclareFee = argentMaxFee(estimateFeeBulk[1].suggestedMaxFee)
+ maxADFee = argentMaxFee({
+ suggestedMaxFee: estimateFeeBulk[0].suggestedMaxFee,
+ })
+ maxDeclareFee = argentMaxFee({
+ suggestedMaxFee: estimateFeeBulk[1].suggestedMaxFee,
+ })
}
const { account, txHash: accountDeployTxHash } = await wallet.deployAccount(
selectedAccount,
@@ -124,7 +128,7 @@ export const udcDeclareContract = async (
nonce: declareNonce,
},
)
- maxDeclareFee = argentMaxFee(suggestedMaxFee)
+ maxDeclareFee = argentMaxFee({ suggestedMaxFee })
} else {
throw new UdcError({ code: "NO_STARKNET_DECLARE_FEE" })
}
@@ -215,8 +219,12 @@ export const udcDeployContract = async (
{ skipValidate: true },
)
- maxADFee = argentMaxFee(estimateFeeBulk[0].suggestedMaxFee)
- maxDeployFee = argentMaxFee(estimateFeeBulk[1].suggestedMaxFee)
+ maxADFee = argentMaxFee({
+ suggestedMaxFee: estimateFeeBulk[0].suggestedMaxFee,
+ })
+ maxDeployFee = argentMaxFee({
+ suggestedMaxFee: estimateFeeBulk[1].suggestedMaxFee,
+ })
}
const { account, txHash: accountDeployTxHash } = await wallet.deployAccount(
selectedAccount,
@@ -260,7 +268,7 @@ export const udcDeployContract = async (
nonce: deployNonce,
},
)
- maxDeployFee = argentMaxFee(suggestedMaxFee)
+ maxDeployFee = argentMaxFee({ suggestedMaxFee })
} else {
throw new UdcError({ code: "NO_STARKNET_DECLARE_FEE" })
}
diff --git a/packages/extension/src/background/udcMessaging.ts b/packages/extension/src/background/udcMessaging.ts
index 3d6ece6ad..cafdc9689 100644
--- a/packages/extension/src/background/udcMessaging.ts
+++ b/packages/extension/src/background/udcMessaging.ts
@@ -22,13 +22,14 @@ export const handleUdcMessaging: HandleMessage = async ({
const action = await actionService.add(
{
- type: "DECLARE_CONTRACT_ACTION",
+ type: "DECLARE_CONTRACT",
payload: {
...rest,
},
},
{
origin,
+ icon: "DocumentIcon",
},
)
@@ -39,42 +40,6 @@ export const handleUdcMessaging: HandleMessage = async ({
},
})
}
-
- // TODO: refactor after refactoring actionHandlers
- case "REQUEST_DEPLOY_CONTRACT": {
- const { data } = msg
- const {
- address,
- networkId,
- classHash,
- constructorCalldata,
- salt,
- unique,
- } = data
- await wallet.selectAccount({ address, networkId })
-
- const action = await actionService.add(
- {
- type: "DEPLOY_CONTRACT_ACTION",
- payload: {
- classHash: classHash.toString(),
- constructorCalldata,
- salt,
- unique,
- },
- },
- {
- origin,
- },
- )
-
- return respond({
- type: "REQUEST_DEPLOY_CONTRACT_RES",
- data: {
- actionHash: action.meta.hash,
- },
- })
- }
}
throw new UnhandledMessage()
diff --git a/packages/extension/src/background/utils/argentMaxFee.ts b/packages/extension/src/background/utils/argentMaxFee.ts
index f6dc15686..e69de29bb 100644
--- a/packages/extension/src/background/utils/argentMaxFee.ts
+++ b/packages/extension/src/background/utils/argentMaxFee.ts
@@ -1,12 +0,0 @@
-import { num } from "starknet"
-
-/**
- *
- * This method calculate the max fee for argent. Argent keeps the 1.5x overhead to the fee.
- *
- * @param suggestedMaxFee: fee calculated in starknetjs with the formula: overall_fee * 1.5
- * @returns argentMaxFee: currently equal to suggestedMaxFee
- *
- * */
-export const argentMaxFee = (suggestedMaxFee: num.BigNumberish) =>
- num.toHex(suggestedMaxFee)
diff --git a/packages/extension/src/background/wallet/account/shared.service.test.ts b/packages/extension/src/background/wallet/account/shared.service.test.ts
index 75671b920..0c9b2c25d 100644
--- a/packages/extension/src/background/wallet/account/shared.service.test.ts
+++ b/packages/extension/src/background/wallet/account/shared.service.test.ts
@@ -1,8 +1,5 @@
-import {
- WalletAccountSharedService,
- WalletSession,
- WalletStorageProps,
-} from "./shared.service"
+import { WalletAccountSharedService, WalletSession } from "./shared.service"
+import { WalletStorageProps } from "../../../shared/wallet/walletStore"
import { BaseMultisigWalletAccount } from "../../../shared/wallet.model"
import { PendingMultisig } from "../../../shared/multisig/types"
diff --git a/packages/extension/src/background/wallet/account/shared.service.ts b/packages/extension/src/background/wallet/account/shared.service.ts
index 2e4f7eff8..c9f70c819 100644
--- a/packages/extension/src/background/wallet/account/shared.service.ts
+++ b/packages/extension/src/background/wallet/account/shared.service.ts
@@ -21,18 +21,13 @@ import { accountsEqual } from "./../../../shared/utils/accountsEqual"
import { getPathForIndex } from "../../keys/keyDerivation"
import { AccountError } from "../../../shared/errors/account"
import { MULTISIG_ACCOUNT_CLASS_HASH } from "../../../shared/network/constants"
+import type { WalletStorageProps } from "../../../shared/wallet/walletStore"
export interface WalletSession {
secret: string
password: string
}
-export interface WalletStorageProps {
- backup?: string
- selected?: BaseWalletAccount | null
- discoveredOnce?: boolean
-}
-
export class WalletAccountSharedService {
constructor(
public readonly store: IObjectStore,
diff --git a/packages/extension/src/background/wallet/account/starknet.service.ts b/packages/extension/src/background/wallet/account/starknet.service.ts
index 4cfac309a..bba3868cf 100644
--- a/packages/extension/src/background/wallet/account/starknet.service.ts
+++ b/packages/extension/src/background/wallet/account/starknet.service.ts
@@ -1,14 +1,8 @@
-import { memoize } from "lodash-es"
import { Account } from "starknet"
-import {
- Account as AccountV4__deprecated,
- ec as ec__deprecated,
-} from "starknet4-deprecated"
import { MultisigAccount } from "../../../shared/multisig/account"
import { PendingMultisig } from "../../../shared/multisig/types"
import { getProvider } from "../../../shared/network"
-import { getProviderv4__deprecated } from "../../../shared/network/provider"
import { INetworkService } from "../../../shared/network/service/interface"
import { IRepository } from "../../../shared/storage/__new/interface"
import { getAccountCairoVersion } from "../../../shared/utils/argentAccountVersion"
@@ -16,7 +10,6 @@ import {
ArgentAccountType,
BaseWalletAccount,
} from "../../../shared/wallet.model"
-import { getAccountIdentifier } from "../../../shared/wallet.service"
import { isKeyPair } from "../../keys/keyDerivation"
import { WalletCryptoStarknetService } from "../crypto/starknet.service"
import { WalletSessionService } from "../session/session.service"
@@ -25,24 +18,6 @@ import { SessionError } from "../../../shared/errors/session"
import { AccountError } from "../../../shared/errors/account"
import { IMultisigBackendService } from "../../../shared/multisig/service/backend/interface"
-const isNonceManagedOnAccountContract = memoize(
- async (account: AccountV4__deprecated, _: BaseWalletAccount) => {
- try {
- // This will fetch nonce from account contract instead of Starknet OS
- await account.getNonce()
- return true
- } catch {
- return false
- }
- },
- (_, account) => {
- const id = getAccountIdentifier(account)
- // memoize for max 5 minutes
- const timestamp = Math.floor(Date.now() / 1000 / 60 / 5)
- return `${id}-${timestamp}`
- },
-)
-
export class WalletAccountStarknetService {
constructor(
private readonly pendingMultisigStore: IRepository,
@@ -56,7 +31,7 @@ export class WalletAccountStarknetService {
public async getStarknetAccount(
selector: BaseWalletAccount,
useLatest = false,
- ): Promise {
+ ): Promise {
if (!(await this.sessionService.isSessionOpen())) {
throw new SessionError({ code: "NO_OPEN_SESSION" })
}
@@ -66,7 +41,7 @@ export class WalletAccountStarknetService {
}
const provider = getProvider(
- account.network && account.network.sequencerUrl
+ account.network && account.network.rpcUrl
? account.network
: await this.networkService.getById(selector.networkId),
)
@@ -102,23 +77,6 @@ export class WalletAccountStarknetService {
return this.getStarknetAccountOfType(starknetAccount, account.type)
}
- /// TODO: Get rid of this deprecated code
- const providerV4 = getProviderv4__deprecated(account.network)
-
- const oldAccount = providerV4
- ? new AccountV4__deprecated(
- providerV4,
- account.address,
- isKeyPair(signer)
- ? ec__deprecated.getKeyPair(signer.getPrivate())
- : signer,
- )
- : null
-
- const isOldAccount = oldAccount
- ? await isNonceManagedOnAccountContract(oldAccount, account)
- : false
-
// Keep the fallback here as we don't want to block the users
// if the worker has not updated the account yet
const accountCairoVersion =
@@ -142,14 +100,10 @@ export class WalletAccountStarknetService {
accountCairoVersion,
)
- return isOldAccount && oldAccount
- ? oldAccount
- : this.getStarknetAccountOfType(starknetAccount, account.type)
+ return this.getStarknetAccountOfType(starknetAccount, account.type)
}
- public async getSelectedStarknetAccount(): Promise<
- Account | AccountV4__deprecated
- > {
+ public async getSelectedStarknetAccount(): Promise {
if (!(await this.sessionService.isSessionOpen())) {
throw Error("no open session")
}
@@ -182,10 +136,7 @@ export class WalletAccountStarknetService {
return pendingMultisig
}
- public getStarknetAccountOfType(
- account: Account | AccountV4__deprecated,
- type: ArgentAccountType,
- ) {
+ public getStarknetAccountOfType(account: Account, type: ArgentAccountType) {
if (type === "multisig") {
return MultisigAccount.fromAccount(account, this.multisigBackendService)
}
diff --git a/packages/extension/src/background/wallet/crypto/starknet.service.ts b/packages/extension/src/background/wallet/crypto/starknet.service.ts
index ac1e01f7b..2fe176a3f 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 { calldataSchema, isEqualAddress } from "@argent/shared"
import { CairoVersion, CallData, hash } from "starknet"
import { withHiddenSelector } from "../../../shared/account/selectors"
import { MultisigSigner } from "../../../shared/multisig/signer"
diff --git a/packages/extension/src/background/wallet/deployment/interface.ts b/packages/extension/src/background/wallet/deployment/interface.ts
index 23cd05055..05cf6300c 100644
--- a/packages/extension/src/background/wallet/deployment/interface.ts
+++ b/packages/extension/src/background/wallet/deployment/interface.ts
@@ -32,4 +32,10 @@ export interface IWalletDeploymentService {
type?: CreateAccountType, // Should not be able to create plugin accounts. Default to argent account
multisigPayload?: MultisigData,
): Promise
+ getMultisigDeploymentPayload(
+ walletAccount: WalletAccount,
+ ): Promise>
+ getAccountOrMultisigDeploymentPayload(
+ walletAccount: WalletAccount,
+ ): Promise>
}
diff --git a/packages/extension/src/background/wallet/deployment/starknet.service.ts b/packages/extension/src/background/wallet/deployment/starknet.service.ts
index 110750d3b..584aed0c1 100644
--- a/packages/extension/src/background/wallet/deployment/starknet.service.ts
+++ b/packages/extension/src/background/wallet/deployment/starknet.service.ts
@@ -1,5 +1,6 @@
import {
addressSchema,
+ isAccountV5,
isContractDeployed,
isEqualAddress,
} from "@argent/shared"
@@ -41,7 +42,6 @@ import {
getStarkPair,
} from "../../keys/keyDerivation"
import { getNonce, increaseStoredNonce } from "../../nonce"
-import { isAccountV5 } from "../../../shared/utils/accountv4"
import { WalletAccountStarknetService } from "../account/starknet.service"
import { WalletBackupService } from "../backup/backup.service"
import { WalletCryptoStarknetService } from "../crypto/starknet.service"
@@ -53,6 +53,7 @@ import { SessionError } from "../../../shared/errors/session"
import { WalletError } from "../../../shared/errors/wallet"
import { STANDARD_CAIRO_0_ACCOUNT_CLASS_HASH } from "../../../shared/network/constants"
import { AccountError } from "../../../shared/errors/account"
+import { argentMaxFee } from "../../../shared/utils/argentMaxFee"
const { getSelectorFromName, calculateContractAddressFromHash } = hash
@@ -83,25 +84,24 @@ export class WalletDeploymentStarknetService
throw new AccountError({ code: "CANNOT_DEPLOY_OLD_ACCOUNTS" })
}
- let deployAccountPayload: DeployAccountContractPayload
-
- if (walletAccount.type === "multisig") {
- deployAccountPayload = await this.getMultisigDeploymentPayload(
- walletAccount,
- )
- } else {
- deployAccountPayload = await this.getAccountDeploymentPayload(
- walletAccount,
- )
- }
+ const deployAccountPayload =
+ await this.getAccountOrMultisigDeploymentPayload(walletAccount)
if (!isAccountV5(starknetAccount)) {
throw new AccountError({ code: "CANNOT_DEPLOY_OLD_ACCOUNTS" })
}
-
+ const maxFee = transactionDetails?.maxFee
+ ? argentMaxFee({ suggestedMaxFee: transactionDetails?.maxFee })
+ : argentMaxFee({
+ suggestedMaxFee: (await this.getAccountDeploymentFee(walletAccount))
+ .suggestedMaxFee,
+ })
const { transaction_hash } = await starknetAccount.deployAccount(
deployAccountPayload,
- transactionDetails,
+ {
+ ...transactionDetails,
+ maxFee,
+ },
)
await this.accountSharedService.selectAccount(walletAccount)
@@ -109,6 +109,15 @@ export class WalletDeploymentStarknetService
return { account: walletAccount, txHash: transaction_hash }
}
+ public async getAccountOrMultisigDeploymentPayload(
+ walletAccount: WalletAccount,
+ ) {
+ if (walletAccount.type === "multisig") {
+ return this.getMultisigDeploymentPayload(walletAccount)
+ }
+ return this.getAccountDeploymentPayload(walletAccount)
+ }
+
public async getAccountDeploymentFee(
walletAccount: WalletAccount,
): Promise {
@@ -120,9 +129,8 @@ export class WalletDeploymentStarknetService
}
const deployAccountPayload =
- walletAccount.type === "multisig"
- ? await this.getMultisigDeploymentPayload(walletAccount)
- : await this.getAccountDeploymentPayload(walletAccount)
+ await this.getAccountOrMultisigDeploymentPayload(walletAccount)
+
if (!isAccountV5(starknetAccount)) {
throw new AccountError({
code: "CANNOT_ESTIMATE_FEE_OLD_ACCOUNTS_DEPLOYMENT",
diff --git a/packages/extension/src/background/wallet/index.ts b/packages/extension/src/background/wallet/index.ts
index 5812c0eb8..6504237a2 100644
--- a/packages/extension/src/background/wallet/index.ts
+++ b/packages/extension/src/background/wallet/index.ts
@@ -1,5 +1,4 @@
import { Account, InvocationsDetails } from "starknet"
-import { Account as Account4__deprecated } from "starknet4-deprecated"
import {
ArgentAccountType,
@@ -75,10 +74,7 @@ export class Wallet {
public async newPendingMultisig(networkId: string): Promise {
return this.walletAccountStarknetService.newPendingMultisig(networkId)
}
- public getStarknetAccountOfType(
- account: Account | Account4__deprecated,
- type: ArgentAccountType,
- ) {
+ public getStarknetAccountOfType(account: Account, type: ArgentAccountType) {
return this.walletAccountStarknetService.getStarknetAccountOfType(
account,
type,
@@ -188,6 +184,13 @@ export class Wallet {
walletAccount,
)
}
+ public async getAccountOrMultisigDeploymentPayload(
+ walletAccount: WalletAccount,
+ ) {
+ return this.walletDeploymentStarknetService.getAccountOrMultisigDeploymentPayload(
+ walletAccount,
+ )
+ }
public async getDeployContractPayloadForAccountIndex(
index: number,
networkId: string,
diff --git a/packages/extension/src/background/wallet/recovery/shared.service.test.ts b/packages/extension/src/background/wallet/recovery/shared.service.test.ts
index 74fc36ac5..19107a988 100644
--- a/packages/extension/src/background/wallet/recovery/shared.service.test.ts
+++ b/packages/extension/src/background/wallet/recovery/shared.service.test.ts
@@ -12,7 +12,7 @@ import {
IRepository,
} from "../../../shared/storage/__new/interface"
import { WalletAccount } from "../../../shared/wallet.model"
-import { WalletSession, WalletStorageProps } from "../account/shared.service"
+import { WalletSession } from "../account/shared.service"
import {
emitterMock,
getSessionStoreMock,
@@ -22,6 +22,7 @@ import {
import { WalletRecoverySharedService } from "./shared.service"
import { WalletRecoveryStarknetService } from "./starknet.service"
import { WalletError } from "../../../shared/errors/wallet"
+import { WalletStorageProps } from "../../../shared/wallet/walletStore"
describe("WalletRecoverySharedService", () => {
let service: WalletRecoverySharedService
diff --git a/packages/extension/src/background/wallet/recovery/shared.service.ts b/packages/extension/src/background/wallet/recovery/shared.service.ts
index 198f4e16b..15f6cfe85 100644
--- a/packages/extension/src/background/wallet/recovery/shared.service.ts
+++ b/packages/extension/src/background/wallet/recovery/shared.service.ts
@@ -5,11 +5,11 @@ import {
IRepository,
} from "../../../shared/storage/__new/interface"
import { WalletAccount } from "../../../shared/wallet.model"
-import { WalletStorageProps } from "../account/shared.service"
import { WalletSession } from "../session/walletSession.model"
import { Events, IWalletRecoveryService, Recovered } from "./interface"
import { WalletError } from "../../../shared/errors/wallet"
import Emittery from "emittery"
+import type { WalletStorageProps } from "../../../shared/wallet/walletStore"
export class WalletRecoverySharedService {
constructor(
diff --git a/packages/extension/src/background/wallet/test.utils.ts b/packages/extension/src/background/wallet/test.utils.ts
index 416c06c14..fc957ae11 100644
--- a/packages/extension/src/background/wallet/test.utils.ts
+++ b/packages/extension/src/background/wallet/test.utils.ts
@@ -11,7 +11,6 @@ import {
import {
WalletAccountSharedService,
WalletSession,
- WalletStorageProps,
} from "./account/shared.service"
import { WalletAccountStarknetService } from "./account/starknet.service"
import { WalletBackupService } from "./backup/backup.service"
@@ -24,6 +23,7 @@ import { WalletSessionService } from "./session/session.service"
import { Wallet } from "."
import { MultisigBackendService } from "../../shared/multisig/service/backend/implementation"
import { IScheduleService } from "../../shared/schedule/interface"
+import { WalletStorageProps } from "../../shared/wallet/walletStore"
const isDev = true
const isTest = true
diff --git a/packages/extension/src/background/workers.ts b/packages/extension/src/background/workers.ts
index 16eda233e..b7ff04b2f 100644
--- a/packages/extension/src/background/workers.ts
+++ b/packages/extension/src/background/workers.ts
@@ -6,6 +6,7 @@ import { onboardingWorker } from "./__new/services/onboarding"
import { tokenWorker } from "../shared/token/__new/worker"
import { accountWorker } from "../shared/account/worker"
import { knownDappsWorker } from "./../shared/knownDapps/worker"
+import { transactionReviewWorker } from "./__new/services/transactionReview/worker"
/** TODO: refactor: remove this facade */
export function initWorkers() {
@@ -18,5 +19,6 @@ export function initWorkers() {
networkWorker,
knownDappsWorker,
analyticsWorker,
+ transactionReviewWorker,
}
}
diff --git a/packages/extension/src/inpage/ArgentXAccount.ts b/packages/extension/src/inpage/ArgentXAccount.ts
index 7a2a9265f..a232b54c8 100644
--- a/packages/extension/src/inpage/ArgentXAccount.ts
+++ b/packages/extension/src/inpage/ArgentXAccount.ts
@@ -149,20 +149,24 @@ export class ArgentXAccount extends Account {
10 * 60 * 1000,
(x) => x.data.actionHash === actionHash,
)
- .then(() => "error" as const)
- .catch(() => {
- sendMessage({ type: "SIGNATURE_FAILURE", data: { actionHash } })
+ .then((x) => x)
+ .catch((e) => {
+ sendMessage({
+ type: "SIGNATURE_FAILURE",
+ data: { actionHash, error: e.message }, // this error will be thrown by waitForMessage after the timeout
+ })
return "timeout" as const
}),
])
- if (result === "error") {
- throw Error("User abort")
- }
if (result === "timeout") {
throw Error("User action timed out")
}
+ if ("error" in result) {
+ throw Error(result.error)
+ }
+
return result.signature
}
}
diff --git a/packages/extension/src/inpage/ArgentXAccount4.ts b/packages/extension/src/inpage/ArgentXAccount4.ts
index dd9c49549..1699d37a0 100644
--- a/packages/extension/src/inpage/ArgentXAccount4.ts
+++ b/packages/extension/src/inpage/ArgentXAccount4.ts
@@ -102,20 +102,24 @@ export class ArgentXAccount4 extends Account implements AccountInterface {
10 * 60 * 1000,
(x) => x.data.actionHash === actionHash,
)
- .then(() => "error" as const)
- .catch(() => {
- sendMessage({ type: "SIGNATURE_FAILURE", data: { actionHash } })
+ .then((x) => x)
+ .catch((e) => {
+ sendMessage({
+ type: "SIGNATURE_FAILURE",
+ data: { actionHash, error: e.message }, // this error will be thrown by waitForMessage after the timeout
+ })
return "timeout" as const
}),
])
- if (result === "error") {
- throw Error("User abort")
- }
if (result === "timeout") {
throw Error("User action timed out")
}
+ if ("error" in result) {
+ throw Error(result.error)
+ }
+
return stark.formatSignature(result.signature)
}
}
diff --git a/packages/extension/src/inpage/ArgentXProvider.ts b/packages/extension/src/inpage/ArgentXProvider.ts
index b26d46c96..6aa332713 100644
--- a/packages/extension/src/inpage/ArgentXProvider.ts
+++ b/packages/extension/src/inpage/ArgentXProvider.ts
@@ -1,23 +1,18 @@
+import { getChainIdFromNetworkId } from "@argent/shared"
import { BlockIdentifier, Call, Provider, ProviderInterface } from "starknet"
+
import { Network, getProvider } from "../shared/network"
-import {
- getRandomPublicRPCNode,
- isArgentNetwork,
-} from "../shared/network/utils"
+import { FallbackRpcProvider } from "../shared/network/FallbackRpcProvider"
+import { getPublicRPCNodeUrls, isArgentNetwork } from "../shared/network/utils"
export class ArgentXProvider extends Provider implements ProviderInterface {
constructor(network: Network) {
// Only expose sequencer provider for argent networks
if (isArgentNetwork(network)) {
- const publicRpcNode = getRandomPublicRPCNode(network)
- if (network.id === "mainnet-alpha") {
- if (!network.sequencerUrl) {
- throw new Error("Missing sequencer url for mainnet")
- }
- super({ sequencer: { baseUrl: network.sequencerUrl } })
- } else {
- super({ rpc: { nodeUrl: publicRpcNode.testnet } })
- }
+ // Initialising RpcProvider with chainId removes the need for initial RPC calls to `starknet_chainId`
+ const nodeUrls = getPublicRPCNodeUrls(network)
+ const chainId = getChainIdFromNetworkId(network.id)
+ super(new FallbackRpcProvider({ nodeUrls, chainId }))
} else {
// Otherwise, it's a custom network, so we expose the custom provider
super(getProvider(network))
diff --git a/packages/extension/src/inpage/ArgentXProvider4.ts b/packages/extension/src/inpage/ArgentXProvider4.ts
index 4a10d7d7a..b4e3ce46c 100644
--- a/packages/extension/src/inpage/ArgentXProvider4.ts
+++ b/packages/extension/src/inpage/ArgentXProvider4.ts
@@ -11,14 +11,13 @@ export class ArgentXProviderV4 extends Provider implements ProviderInterface {
// Only expose sequencer provider for argent networks
if (isArgentNetwork(network)) {
const publicRpcNode = getRandomPublicRPCNode(network)
- if (network.id === "mainnet-alpha") {
- if (!network.sequencerUrl) {
- throw new Error("Missing sequencer url for mainnet")
- }
- super({ sequencer: { baseUrl: network.sequencerUrl } })
- } else {
- super({ rpc: { nodeUrl: publicRpcNode.testnet } })
- }
+
+ const nodeUrl =
+ network.id === "mainnet-alpha"
+ ? publicRpcNode.mainnet
+ : publicRpcNode.testnet
+
+ super({ rpc: { nodeUrl } })
} else {
// Otherwise, it's a custom network, so we expose the custom provider
super(getProviderv4(network))
diff --git a/packages/extension/src/inpage/requestMessageHandlers.ts b/packages/extension/src/inpage/requestMessageHandlers.ts
index 22c834ac3..7045b9ed0 100644
--- a/packages/extension/src/inpage/requestMessageHandlers.ts
+++ b/packages/extension/src/inpage/requestMessageHandlers.ts
@@ -67,8 +67,7 @@ export async function handleAddNetworkRequest(
id: callParams.id,
name: callParams.chainName,
chainId: callParams.chainId,
- sequencerUrl: callParams.baseUrl,
- rpcUrl: callParams.rpcUrls?.[0],
+ rpcUrl: callParams.rpcUrls?.[0] ?? "",
explorerUrl: callParams.blockExplorerUrls?.[0],
accountClassHash: (callParams as any).accountImplementation,
},
diff --git a/packages/extension/src/shared/account/service/implementation.test.ts b/packages/extension/src/shared/account/service/implementation.test.ts
index 1a4ea0257..75d6448b4 100644
--- a/packages/extension/src/shared/account/service/implementation.test.ts
+++ b/packages/extension/src/shared/account/service/implementation.test.ts
@@ -1,53 +1,15 @@
-import { messageClient } from "../../../ui/services/messaging/trpc"
import { mockChainService } from "../../chain/service/__test__/mock"
-import {
- MockFnObjectStore,
- MockFnRepository,
-} from "../../storage/__new/__test__/mockFunctionImplementation"
+import { MockFnRepository } from "../../storage/__new/__test__/mockFunctionImplementation"
import type { BaseWalletAccount, WalletAccount } from "../../wallet.model"
-import type { IWalletStore } from "../../wallet/walletStore"
import { AccountService } from "./implementation"
-import { IMultisigService } from "../../multisig/service/messaging/interface"
describe("AccountService", () => {
let accountRepo: MockFnRepository
- let walletStore: IWalletStore
let accountService: AccountService
- let multisigService: IMultisigService
- const mutateMock = vi.fn()
- const messageClientMock = {
- account: {
- select: {
- mutate: mutateMock,
- },
- },
- } as unknown as jest.Mocked
beforeEach(() => {
accountRepo = new MockFnRepository()
- walletStore = new MockFnObjectStore()
- accountService = new AccountService(
- mockChainService,
- accountRepo,
- walletStore,
- messageClientMock,
- multisigService,
- )
- })
-
- describe("select", () => {
- it("should update wallet store with selected account", async () => {
- const baseAccount: BaseWalletAccount = {
- address: "0x123",
- networkId: "0x1",
- }
- await accountService.select(baseAccount)
-
- expect(mutateMock).toHaveBeenCalledWith({
- address: baseAccount.address,
- networkId: baseAccount.networkId,
- })
- })
+ accountService = new AccountService(mockChainService, accountRepo)
})
describe("get", () => {
diff --git a/packages/extension/src/shared/account/service/implementation.ts b/packages/extension/src/shared/account/service/implementation.ts
index 4bb4deca3..8b49f3bd3 100644
--- a/packages/extension/src/shared/account/service/implementation.ts
+++ b/packages/extension/src/shared/account/service/implementation.ts
@@ -1,116 +1,17 @@
-import { Account } from "../../../ui/features/accounts/Account"
-import { Multisig } from "../../../ui/features/multisig/Multisig"
-import { messageClient } from "../../../ui/services/messaging/trpc"
import { IChainService } from "../../chain/service/interface"
import type { AllowArray, SelectorFn } from "../../storage/__new/interface"
-import {
- baseWalletAccountSchema,
- type ArgentAccountType,
- type BaseWalletAccount,
- type CreateAccountType,
- type MultisigData,
- type WalletAccount,
-} from "../../wallet.model"
+import { type BaseWalletAccount, type WalletAccount } from "../../wallet.model"
import { accountsEqual } from "../../utils/accountsEqual"
-import type { IWalletStore } from "../../wallet/walletStore"
import { withoutHiddenSelector } from "../selectors"
import type { IAccountRepo } from "../store"
import type { IAccountService } from "./interface"
-import { IMultisigService } from "../../multisig/service/messaging/interface"
-// TODO: once the data presentation of account changes, this should be updated and tests should be added
-// TODO: once the messaging is trpc, we should add tests
export class AccountService implements IAccountService {
constructor(
private readonly chainService: IChainService,
private readonly accountRepo: IAccountRepo,
- private readonly walletStore: IWalletStore,
- private readonly trpcClient: typeof messageClient,
- private readonly multisigService: IMultisigService,
) {}
- async select(baseAccount: BaseWalletAccount | null): Promise {
- let parsedAccount = baseAccount
-
- if (parsedAccount) {
- parsedAccount = baseWalletAccountSchema.parse(baseAccount)
- }
-
- return this.trpcClient.account.select.mutate(parsedAccount)
- }
-
- async create(
- type: CreateAccountType,
- networkId: string,
- multisigPayload?: MultisigData,
- ): Promise {
- if (type === "multisig" && !multisigPayload) {
- throw new Error("Multisig payload is required")
- }
-
- let newAccount: Account
- if (type === "multisig") {
- // get rid of these extra abstractions
- newAccount = await Multisig.create(networkId, multisigPayload)
- } else {
- newAccount = await Account.create(networkId, type)
- }
-
- // get WalletAccount format
- const [hit] = await this.accountRepo.get((account) =>
- accountsEqual(account, newAccount),
- )
-
- if (!hit) {
- throw new Error("Something went wrong")
- }
-
- // switch background wallet to the account that was selected
- await this.select(newAccount)
-
- return hit
- }
-
- // TODO: make isomorphic
- async deploy(baseAccount: BaseWalletAccount): Promise {
- const [account] = await this.accountRepo.get((account) =>
- accountsEqual(account, baseAccount),
- )
-
- if (!account) {
- throw new Error("Account not found")
- }
-
- if (account.needsDeploy === false) {
- throw new Error("Account already deployed")
- }
-
- if (account.type === "multisig") {
- await this.multisigService.deploy(account)
- } else {
- await this.trpcClient.account.deploy.mutate(account)
- }
- }
-
- // TODO: make isomorphic
- async upgrade(
- baseWalletAccount: BaseWalletAccount,
- targetImplementationType?: ArgentAccountType | undefined,
- ): Promise {
- const baseAccount = baseWalletAccountSchema.parse(baseWalletAccount)
- const [account] = await this.get((a) => accountsEqual(a, baseAccount))
- const [upgradeNeeded, correctAcc] =
- await this.trpcClient.account.upgrade.mutate({
- account,
- targetImplementationType,
- })
-
- if (!upgradeNeeded) {
- // This means we have incorrect state locally, and we should update it with onchain state
- await this.upsert(correctAcc)
- }
- }
-
async get(
selector: SelectorFn = withoutHiddenSelector,
): Promise {
diff --git a/packages/extension/src/shared/account/service/index.ts b/packages/extension/src/shared/account/service/index.ts
index 6afc71e4f..d99622832 100644
--- a/packages/extension/src/shared/account/service/index.ts
+++ b/packages/extension/src/shared/account/service/index.ts
@@ -1,14 +1,8 @@
-import { messageClient } from "../../../ui/services/messaging/trpc"
import { starknetChainService } from "../../chain/service"
-import { walletStore } from "../../wallet/walletStore"
import { accountRepo } from "../store"
import { AccountService } from "./implementation"
-import { multisigService } from "../../../ui/services/multisig"
export const accountService = new AccountService(
starknetChainService,
accountRepo,
- walletStore,
- messageClient,
- multisigService,
)
diff --git a/packages/extension/src/shared/account/service/interface.ts b/packages/extension/src/shared/account/service/interface.ts
index 9c3f8dc00..880921167 100644
--- a/packages/extension/src/shared/account/service/interface.ts
+++ b/packages/extension/src/shared/account/service/interface.ts
@@ -1,28 +1,7 @@
import { AllowArray, SelectorFn } from "../../storage/__new/interface"
-import {
- ArgentAccountType,
- BaseWalletAccount,
- CreateAccountType,
- MultisigData,
- WalletAccount,
-} from "../../wallet.model"
+import { BaseWalletAccount, WalletAccount } from "../../wallet.model"
export interface IAccountService {
- // selected account
- select(baseAccount: BaseWalletAccount): Promise
-
- // account methods
- create(
- type: CreateAccountType,
- networkId: string,
- multisigPayload?: MultisigData,
- ): Promise
- deploy(baseAccount: BaseWalletAccount): Promise
- upgrade(
- baseAccount: BaseWalletAccount,
- targetImplementationType?: ArgentAccountType,
- ): Promise
-
// Repo methods
get(selector?: SelectorFn): Promise
upsert(account: AllowArray): Promise
diff --git a/packages/extension/src/shared/account/worker/implementation.test.ts b/packages/extension/src/shared/account/worker/implementation.test.ts
index a34943fe1..fca28246f 100644
--- a/packages/extension/src/shared/account/worker/implementation.test.ts
+++ b/packages/extension/src/shared/account/worker/implementation.test.ts
@@ -18,11 +18,7 @@ describe("AccountWorker", () => {
get: () => Promise.resolve([getMockWalletAccount({})]),
getDeployed: () => Promise.resolve(true),
upsert: vi.fn(),
- select: vi.fn(),
remove: vi.fn(),
- create: vi.fn(),
- deploy: vi.fn(),
- upgrade: vi.fn(),
setHide: vi.fn(),
setName: vi.fn(),
})
diff --git a/packages/extension/src/shared/actionQueue/queue/queue.test.ts b/packages/extension/src/shared/actionQueue/queue/queue.test.ts
index f57524fa2..2c7b04ebc 100644
--- a/packages/extension/src/shared/actionQueue/queue/queue.test.ts
+++ b/packages/extension/src/shared/actionQueue/queue/queue.test.ts
@@ -15,6 +15,7 @@ const txFixture: ActionItem = {
entrypoint: "fooBar",
calldata: [],
},
+ createdAt: 123,
},
}
diff --git a/packages/extension/src/shared/actionQueue/queue/queue.ts b/packages/extension/src/shared/actionQueue/queue/queue.ts
index 91c1c3b32..a9298850d 100644
--- a/packages/extension/src/shared/actionQueue/queue/queue.ts
+++ b/packages/extension/src/shared/actionQueue/queue/queue.ts
@@ -3,7 +3,7 @@ import oHash from "object-hash"
import type { IRepository } from "../../storage/__new/interface"
import type { ActionQueueItemMeta } from "../schema"
-import type { ExtQueueItem } from "../types"
+import { isTransactionActionItem, type ExtQueueItem } from "../types"
import type { IActionQueue } from "./interface"
function objectHash(obj: object | null) {
@@ -64,6 +64,13 @@ export function getActionQueue(
item: U,
meta?: Partial,
): Promise> {
+ if (isTransactionActionItem(item) && !item.payload.createdAt) {
+ /**
+ * ensure same transaction shapes have a unique hash based on time,
+ * e.g. swap tx may expire
+ */
+ item.payload.createdAt = Date.now()
+ }
const expires = meta?.expires || DEFAULT_EXPIRY_TIME_MS
const hash = objectHash(item)
const newItem = {
diff --git a/packages/extension/src/shared/actionQueue/schema.test.ts b/packages/extension/src/shared/actionQueue/schema.test.ts
new file mode 100644
index 000000000..9753e2003
--- /dev/null
+++ b/packages/extension/src/shared/actionQueue/schema.test.ts
@@ -0,0 +1,36 @@
+import { describe, expect, test } from "vitest"
+
+import { actionQueueItemMetaSchema } from "./schema"
+
+describe("actionQueue", () => {
+ describe("schema", () => {
+ describe("when valid", () => {
+ test("should be successful", () => {
+ expect(
+ actionQueueItemMetaSchema.safeParse({
+ hash: "0x0123",
+ expires: 0,
+ }).success,
+ ).toBeTruthy()
+ expect(
+ actionQueueItemMetaSchema.safeParse({
+ hash: "0x0123",
+ expires: 0,
+ icon: "SendIcon",
+ }).success,
+ ).toBeTruthy()
+ })
+ })
+ describe("when invalid", () => {
+ test("should not be successful", () => {
+ expect(
+ actionQueueItemMetaSchema.safeParse({
+ hash: "0x0123",
+ expires: 0,
+ icon: "InvalidIcon",
+ }).success,
+ ).toBeFalsy()
+ })
+ })
+ })
+})
diff --git a/packages/extension/src/shared/actionQueue/schema.ts b/packages/extension/src/shared/actionQueue/schema.ts
index 9fdc95482..5b2e185e0 100644
--- a/packages/extension/src/shared/actionQueue/schema.ts
+++ b/packages/extension/src/shared/actionQueue/schema.ts
@@ -1,15 +1,23 @@
import { z } from "zod"
+import { icons } from "@argent/ui"
export const actionHashSchema = z.string()
-
export type ActionHash = z.infer
+/** This dance converts Object.keys into z.enum */
+const [first, ...rest] = Object.keys(icons) as (keyof typeof icons)[]
+const allIconKeysSchema = z.enum([first, ...rest])
+export type AllIconKeys = z.infer
+
export const actionQueueItemMetaSchema = z.object({
hash: actionHashSchema,
expires: z.number(),
origin: z.string().url().optional(),
startedApproving: z.number().optional(),
errorApproving: z.string().optional(),
+ title: z.string().optional(),
+ subtitle: z.string().optional(),
+ icon: allIconKeysSchema.optional(),
})
export type ActionQueueItemMeta = z.infer
diff --git a/packages/extension/src/shared/actionQueue/types.ts b/packages/extension/src/shared/actionQueue/types.ts
index 95fb4f924..02a02eebe 100644
--- a/packages/extension/src/shared/actionQueue/types.ts
+++ b/packages/extension/src/shared/actionQueue/types.ts
@@ -6,6 +6,7 @@ import type {
UniversalDeployerContractPayload,
typedData,
} from "starknet"
+import { z } from "zod"
import { Network } from "../network"
import { TransactionMeta } from "../transactions"
@@ -20,6 +21,13 @@ export interface TransactionActionPayload {
abis?: Abi[]
transactionsDetail?: InvocationsDetails
meta?: TransactionMeta
+ createdAt?: number
+}
+
+type DeployActionPayload = {
+ account: BaseWalletAccount
+ /** the calldata to display to the end user - cosmetic only */
+ displayCalldata?: string[]
}
export type ActionItem =
@@ -34,12 +42,12 @@ export type ActionItem =
payload: TransactionActionPayload
}
| {
- type: "DEPLOY_ACCOUNT_ACTION"
- payload: BaseWalletAccount
+ type: "DEPLOY_ACCOUNT"
+ payload: DeployActionPayload
}
| {
- type: "DEPLOY_MULTISIG_ACTION"
- payload: BaseWalletAccount
+ type: "DEPLOY_MULTISIG"
+ payload: DeployActionPayload
}
| {
type: "SIGN"
@@ -64,12 +72,25 @@ export type ActionItem =
payload: Network
}
| {
- type: "DECLARE_CONTRACT_ACTION"
+ type: "DECLARE_CONTRACT"
payload: DeclareContractPayload
}
| {
- type: "DEPLOY_CONTRACT_ACTION"
+ type: "DEPLOY_CONTRACT"
payload: UniversalDeployerContractPayload
}
export type ExtensionActionItem = ExtQueueItem
+
+export type ExtensionActionItemOfType =
+ ExtQueueItem>
+
+const isTransactionActionItemSchema = z.object({
+ type: z.literal("TRANSACTION"),
+})
+
+export function isTransactionActionItem(
+ item: unknown,
+): item is ExtensionActionItemOfType<"TRANSACTION"> {
+ return isTransactionActionItemSchema.safeParse(item).success
+}
diff --git a/packages/extension/src/shared/activity/storage.ts b/packages/extension/src/shared/activity/storage.ts
new file mode 100644
index 000000000..2c1674e3b
--- /dev/null
+++ b/packages/extension/src/shared/activity/storage.ts
@@ -0,0 +1,11 @@
+import { KeyValueStorage } from "../storage"
+import { IActivityStorage } from "./types"
+
+export const activityStore = new KeyValueStorage(
+ {
+ latestBalanceChangingActivity: null,
+ },
+ {
+ namespace: "service:activity",
+ },
+)
diff --git a/packages/extension/src/shared/activity/types.ts b/packages/extension/src/shared/activity/types.ts
new file mode 100644
index 000000000..d159d524b
--- /dev/null
+++ b/packages/extension/src/shared/activity/types.ts
@@ -0,0 +1,9 @@
+export interface IActivity {
+ id: string
+ lastModified: number
+}
+export interface IActivityStorage {
+ latestBalanceChangingActivity: {
+ [address: string]: IActivity
+ } | null
+}
diff --git a/packages/extension/src/shared/addressBook/schema.ts b/packages/extension/src/shared/addressBook/schema.ts
index cea74c783..79bc4c8ab 100644
--- a/packages/extension/src/shared/addressBook/schema.ts
+++ b/packages/extension/src/shared/addressBook/schema.ts
@@ -1,4 +1,4 @@
-import { addressOrStarknetIdInputSchema } from "@argent/shared"
+import { addressOrDomainInputSchema } from "@argent/shared"
import { z } from "zod"
export const addressBookContactSchema = z.object({
@@ -12,7 +12,7 @@ export const addressBookContactSchema = z.object({
.string()
.trim()
.min(1, { message: "Address is required" })
- .pipe(addressOrStarknetIdInputSchema),
+ .pipe(addressOrDomainInputSchema),
})
export const addressBookContactNoIdSchema = addressBookContactSchema.omit({
diff --git a/packages/extension/src/shared/api/constants.ts b/packages/extension/src/shared/api/constants.ts
index 29423eb57..89ede8eea 100644
--- a/packages/extension/src/shared/api/constants.ts
+++ b/packages/extension/src/shared/api/constants.ts
@@ -30,7 +30,7 @@ export const ARGENT_TRANSACTION_REVIEW_STARKNET_URL =
? urlJoin(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
ARGENT_TRANSACTION_REVIEW_API_BASE_URL!,
- "transactions/review/starknet",
+ "transactions/v2/review/starknet",
)
: undefined
diff --git a/packages/extension/src/shared/api/fetcher.ts b/packages/extension/src/shared/api/fetcher.ts
index 7ee9146b2..a1c554bfc 100644
--- a/packages/extension/src/shared/api/fetcher.ts
+++ b/packages/extension/src/shared/api/fetcher.ts
@@ -1,6 +1,7 @@
/** generic json fetcher */
-import { useAppState } from "../../ui/app.state"
+import { defaultNetwork } from "../network"
+import { walletStore } from "../wallet/walletStore"
export type Fetcher = (
input: RequestInfo | URL,
@@ -85,12 +86,12 @@ export const fetcherWithArgentApiHeadersForNetwork = (
return fetcherWithArgentApiHeaders
}
-export const fetcherWithArgentApiHeaders = (fetcherImpl: Fetcher = fetcher) => {
- const { switcherNetworkId } = useAppState.getState()
- const fetcher = fetcherWithArgentApiHeadersForNetwork(
- switcherNetworkId,
- fetcherImpl,
- )
+export const fetcherWithArgentApiHeaders = async (
+ fetcherImpl: Fetcher = fetcher,
+) => {
+ const { selected } = await walletStore.get()
+ const networkId = selected?.networkId ?? defaultNetwork.id
+ const fetcher = fetcherWithArgentApiHeadersForNetwork(networkId, fetcherImpl)
return fetcher
}
diff --git a/packages/extension/src/shared/chain/service/implementation.ts b/packages/extension/src/shared/chain/service/implementation.ts
index 0a99e0380..6321d0026 100644
--- a/packages/extension/src/shared/chain/service/implementation.ts
+++ b/packages/extension/src/shared/chain/service/implementation.ts
@@ -1,4 +1,4 @@
-import { TransactionStatus as StarknetTxStatus } from "starknet"
+import { RpcProvider, TransactionStatus as StarknetTxStatus } from "starknet"
import { getProvider } from "../../network"
import { INetworkService } from "../../network/service/interface"
@@ -43,25 +43,38 @@ export class StarknetChainService implements IChainService {
): Promise {
const network = await this.networkService.getById(transaction.networkId)
const provider = getProvider(network)
- const receipt = await provider.getTransactionReceipt(transaction.hash)
let error_reason: string | undefined
+ const { finality_status, execution_status } =
+ await provider.getTransactionStatus(transaction.hash)
- if (!receipt.status) {
- throw new Error("Invalid response from Starknet node")
- }
+ // TODO: Use constants
+ const isFailed =
+ execution_status === "REVERTED" || finality_status === "REJECTED"
+ const isSuccessful =
+ finality_status === "ACCEPTED_ON_L2" ||
+ finality_status === "ACCEPTED_ON_L1"
- if ("revert_reason" in receipt) {
- error_reason = receipt.revert_reason
+ if (isFailed) {
+ // Only get the receipt if the transaction failed
+ const receipt = await provider.getTransactionReceipt(transaction.hash)
+ error_reason =
+ receipt.revert_reason ||
+ ("transaction_failure_reason" in receipt &&
+ (receipt.transaction_failure_reason as any)?.error_message)
}
- if ("transaction_failure_reason" in receipt) {
- error_reason = receipt.transaction_failure_reason.error_message
- }
+ error_reason =
+ error_reason ?? "Unknown Error while fetching transaction status"
+
+ const status: TransactionStatus = isFailed
+ ? {
+ status: "failed",
+ reason: new Error(error_reason),
+ }
+ : isSuccessful
+ ? { status: "confirmed" }
+ : { status: "pending" }
- const status = starknetStatusToTransactionStatus(
- receipt.status as StarknetTxStatus,
- () => new Error(error_reason),
- )
return {
...transaction,
status,
diff --git a/packages/extension/src/shared/debounce/chrome.test.ts b/packages/extension/src/shared/debounce/chrome.test.ts
new file mode 100644
index 000000000..d550bb48d
--- /dev/null
+++ b/packages/extension/src/shared/debounce/chrome.test.ts
@@ -0,0 +1,53 @@
+import { InMemoryKeyValueStore } from "../storage/__new/__test__/inmemoryImplementations"
+
+import { DebounceService, shouldRun } from "./chrome"
+import { beforeEach, describe, test, expect } from "vitest"
+
+describe("DebounceService", () => {
+ let debounceService: DebounceService
+ let kv: InMemoryKeyValueStore<{ [key: string]: number }>
+
+ beforeEach(() => {
+ kv = new InMemoryKeyValueStore<{ [key: string]: number }>({
+ namespace: "test",
+ })
+ debounceService = new DebounceService(kv)
+ })
+
+ test("shouldRun returns true if the task has not been run within the debounce time", () => {
+ const lastRun = Date.now() - 5000
+ const debounce = 1
+ const result = shouldRun(lastRun, debounce)
+ expect(result).toBe(true)
+ })
+
+ test("shouldRun returns false if the task has been run within the debounce time", () => {
+ const lastRun = Date.now()
+ const debounce = 1
+ const result = shouldRun(lastRun, debounce)
+ expect(result).toBe(false)
+ })
+
+ test("shouldRun works with seconds and not milliseconds", () => {
+ const lastRun = Date.now() - 1001
+ const debounce = 1
+ const result = shouldRun(lastRun, debounce)
+ expect(result).toBe(true)
+
+ const lastRun2 = Date.now() - 999
+ const result2 = shouldRun(lastRun2, debounce)
+ expect(result2).toBe(false)
+ })
+
+ test("debounceService.debounce should debounce the task if the task has not been run within the debounce time", async () => {
+ const debounce = 1 // in seconds
+ const task = {
+ id: "test",
+ callback: vi.fn(),
+ debounce,
+ }
+ void debounceService.debounce(task)
+ await debounceService.debounce(task)
+ expect(task.callback).toHaveBeenCalledTimes(1)
+ })
+})
diff --git a/packages/extension/src/shared/debounce/chrome.ts b/packages/extension/src/shared/debounce/chrome.ts
index eeb36350c..6ba9bbee8 100644
--- a/packages/extension/src/shared/debounce/chrome.ts
+++ b/packages/extension/src/shared/debounce/chrome.ts
@@ -1,12 +1,12 @@
import { BaseScheduledTask } from "../schedule/interface"
-import { KeyValueStorage } from "../storage"
+import { IKeyValueStorage } from "../storage"
import {
DebouncedImplementedScheduledTask,
IDebounceService,
} from "./interface"
-function shouldRun(lastRun: number, debounce: number): boolean {
- return Date.now() - lastRun > debounce
+export function shouldRun(lastRun: number, debounce: number): boolean {
+ return Date.now() - lastRun > debounce * 1000
}
export class DebounceService
@@ -16,13 +16,14 @@ export class DebounceService
private readonly taskIsRunning = new Map()
constructor(
- private readonly kv: KeyValueStorage<{
+ private readonly kv: IKeyValueStorage<{
[key: string]: number
}>,
) {}
async debounce(task: DebouncedImplementedScheduledTask): Promise {
const lastRun = await this.lastRun(task)
+
if (this.isRunning(task) || !shouldRun(lastRun ?? 0, task.debounce)) {
// if task is running or last run is within debounce,
return
@@ -30,6 +31,7 @@ export class DebounceService
this.taskIsRunning.set(task.id, true)
await this.kv.set(task.id, Date.now())
+ await task.callback()
this.taskIsRunning.set(task.id, false)
}
diff --git a/packages/extension/src/shared/debounce/interface.ts b/packages/extension/src/shared/debounce/interface.ts
index ef61f5307..9b4b817cc 100644
--- a/packages/extension/src/shared/debounce/interface.ts
+++ b/packages/extension/src/shared/debounce/interface.ts
@@ -5,7 +5,7 @@ import {
export interface DebouncedImplementedScheduledTask
extends ImplementedScheduledTask {
- debounce: number
+ debounce: number // in seconds
}
export interface IDebounceService {
diff --git a/packages/extension/src/shared/debounce/mock.ts b/packages/extension/src/shared/debounce/mock.ts
index b7b325543..6c967aa53 100644
--- a/packages/extension/src/shared/debounce/mock.ts
+++ b/packages/extension/src/shared/debounce/mock.ts
@@ -1,9 +1,21 @@
import { IDebounceService } from "."
export const getMockDebounceService = (): IDebounceService => {
+ const isRunning: Record = {}
+ const lastRun: Record = {}
+
return {
- debounce: vi.fn(() => Promise.resolve()),
- isRunning: vi.fn(),
- lastRun: vi.fn(),
+ debounce: vi.fn(async (params) => {
+ if (isRunning[params.id]) {
+ return
+ }
+ isRunning[params.id] = true
+ await new Promise((resolve) => setTimeout(resolve, params.debounce))
+ await params.callback()
+ lastRun[params.id] = Date.now()
+ isRunning[params.id] = false
+ }),
+ isRunning: vi.fn(({ id }) => isRunning[id]),
+ lastRun: vi.fn(async ({ id }) => lastRun[id]),
}
}
diff --git a/packages/extension/src/shared/devnet/mintFeeToken.ts b/packages/extension/src/shared/devnet/mintFeeToken.ts
index 000f21785..e85b4a547 100644
--- a/packages/extension/src/shared/devnet/mintFeeToken.ts
+++ b/packages/extension/src/shared/devnet/mintFeeToken.ts
@@ -1,7 +1,7 @@
import urlJoin from "url-join"
import { networkService as argentNetworkService } from "../network/service"
-import { getNetworkUrl } from "../network/utils"
+
import { BaseWalletAccount } from "../wallet.model"
import { INetworkService } from "../network/service/interface"
@@ -13,7 +13,7 @@ export const tryToMintFeeToken = async (
const network = await (networkService || argentNetworkService).getById(
account.networkId,
)
- const networkUrl = getNetworkUrl(network)
+ const networkUrl = network.rpcUrl
await fetch(urlJoin(networkUrl, "mint"), {
method: "POST",
headers: {
diff --git a/packages/extension/src/shared/errors/account.ts b/packages/extension/src/shared/errors/account.ts
index ccefe61db..23bdc13cb 100644
--- a/packages/extension/src/shared/errors/account.ts
+++ b/packages/extension/src/shared/errors/account.ts
@@ -14,6 +14,7 @@ export enum ACCOUNT_ERROR_MESSAGES {
UPGRADE_NOT_SUPPORTED = "Old Account upgrades are no longer supporte",
DEPLOYED_ACCOUNT_CAIRO_VERSION_NOT_FOUND = "Unable to determine cairo version of DEPLOYED account",
UNDEPLOYED_ACCOUNT_CAIRO_VERSION_NOT_FOUND = "Unable to determine cairo version of UNDEPLOYED account",
+ MISSING_METHOD = "Missing method",
}
export type AccountValidationErrorMessage = keyof typeof ACCOUNT_ERROR_MESSAGES
diff --git a/packages/extension/src/shared/errors/activity.ts b/packages/extension/src/shared/errors/activity.ts
new file mode 100644
index 000000000..342152af5
--- /dev/null
+++ b/packages/extension/src/shared/errors/activity.ts
@@ -0,0 +1,16 @@
+import { BaseError, BaseErrorPayload } from "./baseError"
+
+export enum ACTIVITY_ERROR_MESSAGES {
+ FETCH_FAILED = "Failed to fetch activities",
+ PARSING_FAILED = "Failed to parse backend response",
+}
+
+export type ActivityValidationErrorMessage =
+ keyof typeof ACTIVITY_ERROR_MESSAGES
+
+export class ActivityError extends BaseError {
+ constructor(payload: BaseErrorPayload) {
+ super(payload, ACTIVITY_ERROR_MESSAGES)
+ this.name = "AccountError"
+ }
+}
diff --git a/packages/extension/src/shared/errors/review.ts b/packages/extension/src/shared/errors/review.ts
new file mode 100644
index 000000000..a038bbf5f
--- /dev/null
+++ b/packages/extension/src/shared/errors/review.ts
@@ -0,0 +1,16 @@
+import { BaseError, BaseErrorPayload } from "./baseError"
+
+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",
+}
+
+export type ReviewErrorMessage = keyof typeof REVIEW_ERROR_MESSAGE
+
+export class ReviewError extends BaseError {
+ constructor(payload: BaseErrorPayload) {
+ super(payload, REVIEW_ERROR_MESSAGE)
+ this.name = "ReviewError"
+ }
+}
diff --git a/packages/extension/src/shared/errors/schema.ts b/packages/extension/src/shared/errors/schema.ts
new file mode 100644
index 000000000..c6f6fc729
--- /dev/null
+++ b/packages/extension/src/shared/errors/schema.ts
@@ -0,0 +1,17 @@
+import { z } from "zod"
+
+export const trpcErrorSchema = z.object({
+ data: z.object({
+ code: z.string().optional(), // "STARKNAME_NOT_FOUND",
+ name: z.string().optional(), // "AddressError"
+ message: z.string(), // "foo.stark not found"
+ }),
+})
+
+export const getMessageFromTrpcError = (error: unknown) => {
+ const trpcError = trpcErrorSchema.safeParse(error)
+ if (!trpcError.success) {
+ return
+ }
+ return trpcError.data.data.message
+}
diff --git a/packages/extension/src/shared/knownDapps.ts b/packages/extension/src/shared/knownDapps.ts
index c9820dc16..c5c13c4b7 100644
--- a/packages/extension/src/shared/knownDapps.ts
+++ b/packages/extension/src/shared/knownDapps.ts
@@ -14,6 +14,8 @@ export interface KnownDapp {
contracts: {
[network: string]: string[]
}
+ /** dapp url on dappland */
+ dappland?: string
}
const knownDapps: KnownDapp[] = untypedKnownDapps
diff --git a/packages/extension/src/shared/knownDapps/index.ts b/packages/extension/src/shared/knownDapps/index.ts
index 5b36f7e42..272b834f6 100644
--- a/packages/extension/src/shared/knownDapps/index.ts
+++ b/packages/extension/src/shared/knownDapps/index.ts
@@ -5,6 +5,12 @@ import { knownDappsRepository } from "../storage/__new/repositories/knownDapp"
export const argentKnownDappsService = new ArgentKnownDappsBackendService(
ARGENT_API_BASE_URL,
+ {
+ headers: {
+ "argent-version": process.env.VERSION ?? "Unknown version",
+ "argent-client": "argent-x",
+ },
+ },
)
export const knownDappsService = new KnownDappService(
diff --git a/packages/extension/src/shared/knownDapps/worker/implementation.ts b/packages/extension/src/shared/knownDapps/worker/implementation.ts
index 3166db4b3..bdd846b4e 100644
--- a/packages/extension/src/shared/knownDapps/worker/implementation.ts
+++ b/packages/extension/src/shared/knownDapps/worker/implementation.ts
@@ -1,11 +1,10 @@
import { IScheduleService } from "../../schedule/interface"
import { KnownDappService } from "../implementation"
import { RefreshInterval } from "../../config"
-import {
- every,
- onStartup,
-} from "../../../background/__new/services/worker/schedule/decorators"
+import { everyWhenOpen } from "../../../background/__new/services/worker/schedule/decorators"
import { pipe } from "../../../background/__new/services/worker/schedule/pipe"
+import { IBackgroundUIService } from "../../../background/__new/services/ui/interface"
+import { IDebounceService } from "../../debounce"
// Worker should be in background, not shared, as they are only used in the background
// TODO: move this file
@@ -17,11 +16,18 @@ export class KnownDappsWorker {
constructor(
private readonly scheduleService: IScheduleService,
private readonly knownDappsService: KnownDappService,
+ private readonly backgroundUIService: IBackgroundUIService,
+ private readonly debounceService: IDebounceService,
) {}
update = pipe(
- onStartup(this.scheduleService), // This will run the function on startup
- every(this.scheduleService, RefreshInterval.VERY_SLOW), // This will run the function every 24 hours
+ everyWhenOpen(
+ this.backgroundUIService,
+ this.scheduleService,
+ this.debounceService,
+ RefreshInterval.VERY_SLOW,
+ "KnownDappsWorker.update",
+ ), // This will run the function every 24 hours
)(async (): Promise => {
console.log("Updating known dapps data")
const dapps = await this.knownDappsService.getDapps()
diff --git a/packages/extension/src/shared/knownDapps/worker/index.ts b/packages/extension/src/shared/knownDapps/worker/index.ts
index 0198b5576..48e6e7e00 100644
--- a/packages/extension/src/shared/knownDapps/worker/index.ts
+++ b/packages/extension/src/shared/knownDapps/worker/index.ts
@@ -1,8 +1,12 @@
import { knownDappsService } from "../index"
import { chromeScheduleService } from "../../schedule"
import { KnownDappsWorker } from "./implementation"
+import { backgroundUIService } from "../../../background/__new/services/ui"
+import { debounceService } from "../../debounce"
export const knownDappsWorker = new KnownDappsWorker(
chromeScheduleService,
knownDappsService,
+ backgroundUIService,
+ debounceService,
)
diff --git a/packages/extension/src/shared/messages/ActionMessage.ts b/packages/extension/src/shared/messages/ActionMessage.ts
index a228158c6..c3d3829ab 100644
--- a/packages/extension/src/shared/messages/ActionMessage.ts
+++ b/packages/extension/src/shared/messages/ActionMessage.ts
@@ -10,7 +10,7 @@ export type ActionMessage =
data: { typedData: typedData.TypedData; options: SignMessageOptions }
}
| { type: "SIGN_MESSAGE_RES"; data: { actionHash: string } }
- | { type: "SIGNATURE_FAILURE"; data: { actionHash: string } }
+ | { type: "SIGNATURE_FAILURE"; data: { actionHash: string; error: string } }
| {
type: "SIGNATURE_SUCCESS"
data: { signature: ArraySignatureType; actionHash: string }
diff --git a/packages/extension/src/shared/messages/UdcMessage.ts b/packages/extension/src/shared/messages/UdcMessage.ts
index 42cd7917e..7b9408621 100644
--- a/packages/extension/src/shared/messages/UdcMessage.ts
+++ b/packages/extension/src/shared/messages/UdcMessage.ts
@@ -1,4 +1,4 @@
-import { DeclareContract, DeployContract } from "../udc/type"
+import { DeclareContract } from "../udc/type"
export type UdcMessage =
| { type: "REQUEST_DECLARE_CONTRACT"; data: DeclareContract }
@@ -19,8 +19,6 @@ export type UdcMessage =
type: "DECLARE_CONTRACT_ACTION_FAILED"
data: { actionHash: string; error?: string }
}
- | { type: "REQUEST_DEPLOY_CONTRACT"; data: DeployContract }
- | { type: "REQUEST_DEPLOY_CONTRACT_RES"; data: { actionHash: string } }
| {
type: "REQUEST_DEPLOY_CONTRACT_REJ"
data: { actionHash: string; error?: string }
diff --git a/packages/extension/src/shared/multicall/getMulticall.ts b/packages/extension/src/shared/multicall/getMulticall.ts
index e33bd5d4e..338769da8 100644
--- a/packages/extension/src/shared/multicall/getMulticall.ts
+++ b/packages/extension/src/shared/multicall/getMulticall.ts
@@ -27,7 +27,7 @@ const getMemoizeKey = (network: Network) => {
network.chainId,
getMulticallAddress(network),
maxBatchSize,
- network.prefer,
+ network.rpcUrl,
]
const key = elements.filter(Boolean).join("-")
return key
diff --git a/packages/extension/src/shared/multisig/account.ts b/packages/extension/src/shared/multisig/account.ts
index 839e9ab67..fd41e32af 100644
--- a/packages/extension/src/shared/multisig/account.ts
+++ b/packages/extension/src/shared/multisig/account.ts
@@ -13,11 +13,10 @@ import {
hash,
num,
} from "starknet"
-import { Account as AccountV4__deprecated } from "starknet4-deprecated"
import { MultisigPendingTransaction } from "./pendingTransactionsStore"
import { MultisigSigner } from "./signer"
-import { isAccountV4__deprecated } from "../utils/accountv4"
import { IMultisigBackendService } from "./service/backend/interface"
+import { isAccountV5 } from "@argent/shared"
export class MultisigAccount extends Account {
public readonly multisigBackendService: IMultisigBackendService
@@ -39,10 +38,10 @@ export class MultisigAccount extends Account {
}
static fromAccount(
- account: Account | AccountV4__deprecated,
+ account: Account,
multisigBackendService: IMultisigBackendService,
): MultisigAccount {
- if (isAccountV4__deprecated(account)) {
+ if (!isAccountV5(account)) {
throw Error("Multisig is not supported for old accounts")
}
@@ -59,9 +58,7 @@ export class MultisigAccount extends Account {
throw Error("Signer is not a MultisigSigner")
}
- static isMultisig(
- account: Account | AccountV4__deprecated,
- ): account is MultisigAccount {
+ static isMultisig(account: Account): account is MultisigAccount {
return (
"multisigBackendService" in account && !!account.multisigBackendService
)
diff --git a/packages/extension/src/ui/features/multisig/hooks/pubkeySchema.test.ts b/packages/extension/src/shared/multisig/multisig.model.test.ts
similarity index 96%
rename from packages/extension/src/ui/features/multisig/hooks/pubkeySchema.test.ts
rename to packages/extension/src/shared/multisig/multisig.model.test.ts
index 79975f1b0..31960f77f 100644
--- a/packages/extension/src/ui/features/multisig/hooks/pubkeySchema.test.ts
+++ b/packages/extension/src/shared/multisig/multisig.model.test.ts
@@ -1,4 +1,4 @@
-import { pubkeySchema } from "./useCreateMultisigForm"
+import { pubkeySchema } from "./multisig.model"
describe("pubkeySchema", () => {
it("should validate a string of 41 to 43 alphanumeric characters", () => {
diff --git a/packages/extension/src/shared/multisig/multisig.model.ts b/packages/extension/src/shared/multisig/multisig.model.ts
index b5ed6047d..1be5e6e8c 100644
--- a/packages/extension/src/shared/multisig/multisig.model.ts
+++ b/packages/extension/src/shared/multisig/multisig.model.ts
@@ -1,7 +1,10 @@
import { CallSchema } from "@argent/x-window"
import { z } from "zod"
import { multisigDataSchema } from "../wallet.model"
-import { pubkeySchema } from "../../ui/features/multisig/hooks/useCreateMultisigForm"
+
+export const pubkeySchema = z
+ .string()
+ .regex(/^[a-zA-Z0-9]{41,43}$/, "Incorrect signer pubkey")
export const ApiMultisigContentSchema = z.object({
address: z.string(),
diff --git a/packages/extension/src/shared/network/FallbackRpcProvider.test.ts b/packages/extension/src/shared/network/FallbackRpcProvider.test.ts
new file mode 100644
index 000000000..6e18bda39
--- /dev/null
+++ b/packages/extension/src/shared/network/FallbackRpcProvider.test.ts
@@ -0,0 +1,200 @@
+import { describe, expect, vi } from "vitest"
+import { FallbackRpcProvider } from "./FallbackRpcProvider"
+import { delay } from "../utils/delay"
+
+describe("FallbackRpcProvider", () => {
+ const makeProvider = async (mockReset = true) => {
+ const nodeUrls = [
+ "https://foo.xyz/rpc/v5.0",
+ "https://bar.xyz/rpc/v5.0",
+ "https://baz.xyz/rpc/v5.0",
+ ]
+ const fetchImplementation = vi.fn()
+ const backoffImplementation = vi.fn()
+ fetchImplementation.mockResolvedValueOnce({
+ ok: true,
+ status: 200,
+ json: async () => ({ result: "chain-foo" }),
+ })
+ const rpcProvider = new FallbackRpcProvider({
+ nodeUrls,
+ randomise: false,
+ fetchImplementation,
+ backoffImplementation,
+ })
+ /** wait fetch to chainId() in constructor to resolve */
+ await delay(0)
+ /** reset the mock by default */
+ mockReset && fetchImplementation.mockReset()
+ return {
+ nodeUrls,
+ fetchImplementation,
+ backoffImplementation,
+ rpcProvider,
+ }
+ }
+
+ describe("when instantiated", () => {
+ test("constructor calls starknet_chainId once only after nodeUrls are available", async () => {
+ const { nodeUrls, fetchImplementation, rpcProvider } = await makeProvider(
+ false,
+ )
+
+ expect(fetchImplementation).toHaveBeenCalledWith(
+ nodeUrls[0],
+ expect.objectContaining({
+ body: JSON.stringify({
+ jsonrpc: "2.0",
+ method: "starknet_chainId",
+ id: 0,
+ }),
+ }),
+ )
+
+ /** shouldn't call chainid method again */
+ fetchImplementation.mockReset()
+ expect(await rpcProvider.getChainId()).toEqual("chain-foo")
+ expect(fetchImplementation).not.toHaveBeenCalled()
+ })
+ })
+
+ describe("when using rpc methods", () => {
+ describe("and the response is ok", () => {
+ test("calls the first node and returns the expected value", async () => {
+ const { nodeUrls, fetchImplementation, rpcProvider } =
+ await makeProvider()
+ fetchImplementation.mockResolvedValueOnce({
+ ok: true,
+ status: 200,
+ json: async () => ({ result: "foo" }),
+ })
+ const res = await rpcProvider.getTransactionReceipt("0x123")
+ expect(fetchImplementation).toHaveBeenCalledWith(
+ nodeUrls[0],
+ expect.any(Object),
+ )
+ expect(res).toEqual("foo")
+ })
+ })
+ describe("and the response is 429 or 500", () => {
+ describe("but is eventually ok", () => {
+ test("falls back to other nodes with backoff and returns the expected value", async () => {
+ const {
+ nodeUrls,
+ fetchImplementation,
+ backoffImplementation,
+ rpcProvider,
+ } = await makeProvider()
+ fetchImplementation
+ .mockResolvedValueOnce({
+ ok: false,
+ status: 429,
+ })
+ .mockResolvedValueOnce({
+ ok: false,
+ status: 500,
+ })
+ .mockResolvedValueOnce({
+ ok: false,
+ status: 429,
+ })
+ .mockResolvedValueOnce({
+ ok: false,
+ status: 200,
+ json: async () => ({ result: "bar" }),
+ })
+ const res = await rpcProvider.getTransactionReceipt("0x123")
+ expect(fetchImplementation).toHaveBeenNthCalledWith(
+ 1,
+ nodeUrls[0],
+ expect.any(Object),
+ )
+ expect(fetchImplementation).toHaveBeenNthCalledWith(
+ 2,
+ nodeUrls[1],
+ expect.any(Object),
+ )
+ expect(fetchImplementation).toHaveBeenNthCalledWith(
+ 3,
+ nodeUrls[2],
+ expect.any(Object),
+ )
+ expect(fetchImplementation).toHaveBeenNthCalledWith(
+ 4,
+ nodeUrls[0],
+ expect.any(Object),
+ )
+ expect(backoffImplementation).toHaveBeenNthCalledWith(1, 1)
+ expect(backoffImplementation).toHaveBeenNthCalledWith(2, 2)
+ expect(backoffImplementation).toHaveBeenNthCalledWith(3, 3)
+ expect(backoffImplementation).not.toHaveBeenNthCalledWith(4, 4)
+ expect(res).toEqual("bar")
+ })
+ })
+ describe("and exhausts retries", () => {
+ test("falls back to other nodes with backoff and throws raw error", async () => {
+ const {
+ nodeUrls,
+ fetchImplementation,
+ backoffImplementation,
+ rpcProvider,
+ } = await makeProvider()
+ fetchImplementation
+ .mockResolvedValueOnce({
+ ok: false,
+ status: 429,
+ })
+ .mockResolvedValueOnce({
+ ok: false,
+ status: 500,
+ })
+ .mockResolvedValueOnce({
+ ok: false,
+ status: 429,
+ })
+ .mockResolvedValueOnce({
+ ok: false,
+ status: 500,
+ })
+ .mockResolvedValueOnce({
+ ok: false,
+ status: 429,
+ })
+ await expect(
+ rpcProvider.getTransactionReceipt("0x123"),
+ ).rejects.toThrow("rawResult.json is not a function")
+ expect(fetchImplementation).toHaveBeenNthCalledWith(
+ 1,
+ nodeUrls[0],
+ expect.any(Object),
+ )
+ expect(fetchImplementation).toHaveBeenNthCalledWith(
+ 2,
+ nodeUrls[1],
+ expect.any(Object),
+ )
+ expect(fetchImplementation).toHaveBeenNthCalledWith(
+ 3,
+ nodeUrls[2],
+ expect.any(Object),
+ )
+ expect(fetchImplementation).toHaveBeenNthCalledWith(
+ 4,
+ nodeUrls[0],
+ expect.any(Object),
+ )
+ expect(fetchImplementation).toHaveBeenNthCalledWith(
+ 5,
+ nodeUrls[1],
+ expect.any(Object),
+ )
+ expect(backoffImplementation).toHaveBeenNthCalledWith(1, 1)
+ expect(backoffImplementation).toHaveBeenNthCalledWith(2, 2)
+ expect(backoffImplementation).toHaveBeenNthCalledWith(3, 3)
+ expect(backoffImplementation).toHaveBeenNthCalledWith(4, 4)
+ expect(backoffImplementation).not.toHaveBeenNthCalledWith(5, 5)
+ })
+ })
+ })
+ })
+})
diff --git a/packages/extension/src/shared/network/FallbackRpcProvider.ts b/packages/extension/src/shared/network/FallbackRpcProvider.ts
new file mode 100644
index 000000000..12c60b034
--- /dev/null
+++ b/packages/extension/src/shared/network/FallbackRpcProvider.ts
@@ -0,0 +1,121 @@
+import {
+ RpcProvider,
+ RpcProviderOptions,
+ constants,
+ json as starknetJson,
+} from "starknet"
+import { delay } from "../utils/delay"
+import { exponentialBackoff } from "./exponentialBackoff"
+import { shuffle } from "lodash-es"
+
+export type RequestBody = {
+ id?: number | string
+ jsonrpc: "2.0"
+ method: string
+ params?: object
+}
+
+export type FallbackRpcProviderOptions = Omit & {
+ /** array of node URLs, will be randomised by default */
+ nodeUrls: string[]
+ /** whether to randomise the URLs, default true */
+ randomise?: boolean
+ maxRetryCount?: number
+ fetchImplementation?: typeof fetch
+ backoffImplementation?: (retryCount: number) => number
+}
+
+export class FallbackRpcProvider extends RpcProvider {
+ public nodeUrls: string[]
+ private nodeIndex
+ private maxRetryCount
+ private fetchImplementation
+ private backoffImplementation
+
+ constructor(optionsOrProvider: FallbackRpcProviderOptions) {
+ const {
+ nodeUrls,
+ randomise = true,
+ maxRetryCount = 5,
+ fetchImplementation,
+ backoffImplementation = exponentialBackoff,
+ ...rest
+ } = optionsOrProvider
+ if (!nodeUrls.length) {
+ throw new Error("nodeUrls must contain at least one element")
+ }
+ super({
+ ...rest,
+ nodeUrl: nodeUrls[0],
+ })
+ this.nodeUrls = randomise ? shuffle(nodeUrls) : nodeUrls
+ this.nodeIndex = randomise ? Math.floor(Math.random() * nodeUrls.length) : 0
+ this.maxRetryCount = maxRetryCount
+ this.fetchImplementation = fetchImplementation
+ this.backoffImplementation = backoffImplementation
+ void this.getChainId()
+ }
+
+ /**
+ * TODO: follow-up - update starknet.js that removes the following behaviour
+ *
+ * super calls async getChainId() in constructor before `this` or `this.nodeUrls` etc. are initialised
+ * as a workaround we return undefined here so that chainId doesn't get set
+ * then we call it again at the end of the constructor once `this` and `this.nodeUrls` are initialised
+ */
+ public async getChainId(): Promise {
+ if (!this.nodeUrls) {
+ // return undefined so the result doesn't get set
+ return undefined as unknown as constants.StarknetChainId
+ }
+ return super.getChainId()
+ }
+
+ /** TODO: follow-up - update starknet.js that also takes an id here */
+ public fetch(method: string, params?: object) {
+ const rpcRequestBody: RequestBody = {
+ jsonrpc: "2.0",
+ method,
+ ...(params && { params }),
+ id: 0,
+ }
+ return this.fetchWithRetry({
+ method: "POST",
+ body: starknetJson.stringify(rpcRequestBody),
+ headers: this.headers as Record,
+ })
+ }
+
+ public async fetchWithRetry(
+ init: RequestInit,
+ retryCount = 0,
+ ): Promise {
+ const nodeUrl = this.nodeUrls ? this.nodeUrls[this.nodeIndex] : this.nodeUrl
+ try {
+ // Can't keep a reference to fetch - causes 'Illegal invocation' error in background
+ const fetchImplementation = this.fetchImplementation ?? fetch
+ const nodeIndexUsed = this.nodeIndex
+ const response = await fetchImplementation(nodeUrl, init)
+ if (!response.ok) {
+ if (nodeIndexUsed === this.nodeIndex) {
+ // Switch to next node immediately
+ this.nodeIndex = (this.nodeIndex + 1) % this.nodeUrls.length
+ }
+ // We only want to retry on 429 and 5xx http errors
+ if (response.status < 500 && response.status !== 429) {
+ return response
+ }
+ // Try again up to maxRetryCount
+ retryCount++
+ if (retryCount < this.maxRetryCount) {
+ await delay(this.backoffImplementation(retryCount))
+ return this.fetchWithRetry(init, retryCount)
+ }
+ }
+ return response
+ } catch (e) {
+ console.error(e)
+ throw e
+ }
+ }
+}
diff --git a/packages/extension/src/shared/network/constants.ts b/packages/extension/src/shared/network/constants.ts
index 0bca5c4a4..18689d123 100644
--- a/packages/extension/src/shared/network/constants.ts
+++ b/packages/extension/src/shared/network/constants.ts
@@ -30,11 +30,11 @@ export const MULTICALL_CONTRACT_ADDRESS =
export const BLAST_RPC_NODE: PublicRpcNode = {
mainnet: "https://starknet-mainnet.public.blastapi.io",
testnet: "https://starknet-testnet.public.blastapi.io",
-}
+} as const
export const LAVA_RPC_NODE: PublicRpcNode = {
mainnet: "https://rpc.starknet.lava.build",
testnet: "https://rpc.starknet-testnet.lava.build",
-}
+} as const
-export const PUBLIC_RPC_NODES = [BLAST_RPC_NODE, LAVA_RPC_NODE]
+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 c231ec86c..856e208b4 100644
--- a/packages/extension/src/shared/network/defaults.ts
+++ b/packages/extension/src/shared/network/defaults.ts
@@ -16,7 +16,7 @@ const DEV_ONLY_NETWORKS: Network[] = [
id: "integration",
name: "Integration",
chainId: "SN_GOERLI",
- sequencerUrl: "https://external.integration.starknet.io",
+ rpcUrl: "https://cloud-dev.argent-api.com/v1/starknet/goerli/rpc/v0.6",
accountClassHash: {
standard: STANDARD_ACCOUNT_CLASS_HASH,
},
@@ -45,8 +45,7 @@ export const defaultNetworks: Network[] = [
id: "mainnet-alpha",
name: "Mainnet",
chainId: "SN_MAIN",
- sequencerUrl: "https://alpha-mainnet.starknet.io",
- rpcUrl: "https://cloud.argent-api.com/v1/starknet/mainnet/rpc/v0.4",
+ rpcUrl: "https://cloud.argent-api.com/v1/starknet/mainnet/rpc/v0.5",
explorerUrl: "https://voyager.online",
l1ExplorerUrl: "https://etherscan.io",
accountClassHash: {
@@ -56,14 +55,12 @@ export const defaultNetworks: Network[] = [
multicallAddress: MULTICALL_CONTRACT_ADDRESS,
feeTokenAddress: FEE_TOKEN_ADDRESS_ETH,
readonly: true,
- prefer: "sequencer",
},
{
id: "goerli-alpha",
name: "Testnet",
chainId: "SN_GOERLI",
- sequencerUrl: "https://alpha4.starknet.io",
- rpcUrl: "https://cloud.argent-api.com/v1/starknet/goerli/rpc/v0.4",
+ rpcUrl: process.env.ARGENT_TESTNET_RPC_URL ?? "",
explorerUrl: "https://goerli.voyager.online",
faucetUrl: "https://faucet.goerli.starknet.io",
l1ExplorerUrl: "https://goerli.etherscan.io",
@@ -78,13 +75,12 @@ export const defaultNetworks: Network[] = [
multicallAddress: MULTICALL_CONTRACT_ADDRESS,
feeTokenAddress: FEE_TOKEN_ADDRESS_ETH,
readonly: true,
- prefer: "rpc",
},
...(process.env.NODE_ENV === "development" ? DEV_ONLY_NETWORKS : []),
{
id: "localhost",
chainId: "SN_GOERLI",
- sequencerUrl: "http://localhost:5050",
+ rpcUrl: "http://localhost:5050/rpc",
explorerUrl: "https://devnet.starkscan.co",
name: "Localhost 5050",
feeTokenAddress: FEE_TOKEN_ADDRESS_ETH,
diff --git a/packages/extension/src/shared/network/exponentialBackoff.ts b/packages/extension/src/shared/network/exponentialBackoff.ts
new file mode 100644
index 000000000..c6f8a75ab
--- /dev/null
+++ b/packages/extension/src/shared/network/exponentialBackoff.ts
@@ -0,0 +1,10 @@
+/** TODO: add some light tests */
+
+export const exponentialBackoff = (retryCount: number) => {
+ /** exponential */
+ const delay = (Math.pow(2, retryCount) - 1) * 1000
+ /** randomise by +- 10% to smooth aggregate traffic */
+ const randomness = 0.9 + Math.random() * 0.2
+ const randomisedDelay = delay * randomness
+ return randomisedDelay
+}
diff --git a/packages/extension/src/shared/network/provider.ts b/packages/extension/src/shared/network/provider.ts
index 412e5fec8..606e36d35 100644
--- a/packages/extension/src/shared/network/provider.ts
+++ b/packages/extension/src/shared/network/provider.ts
@@ -1,88 +1,33 @@
import { memoize } from "lodash-es"
-import { ProviderInterface, RpcProvider, SequencerProvider } from "starknet"
-import {
- SequencerProvider as SequencerProviderV4,
- RpcProvider as RpcProviderV4,
-} from "starknet4"
-import { SequencerProvider as SequencerProviderv4__deprecated } from "starknet4-deprecated"
+import { RpcProvider, constants, shortString } from "starknet"
+import { RpcProvider as RpcProviderV4 } from "starknet4"
import { Network } from "./type"
-/**
- * Returns a sequencer provider for the given base URL.
- */
-const getProviderForBaseUrl = memoize((baseUrl: string): SequencerProvider => {
- return new SequencerProvider({ baseUrl })
-})
-
-export const shouldUseRpcProvider = (network: Network) => {
- const hasRpcUrl = !!network.rpcUrl?.length
- const hasSequencerUrl = !!network.sequencerUrl?.length
- const preferRpc = network.prefer === "rpc"
- return hasRpcUrl && (!hasSequencerUrl || preferRpc)
-}
-
-/**
- * Returns a RPC provider for the given RPC URL.
- *
- */
-export const getProviderForRpcUrl = memoize((rpcUrl: string): RpcProvider => {
- return new RpcProvider({ nodeUrl: rpcUrl })
-})
+export const getProviderForRpcUrlAndChainId = memoize(
+ (rpcUrl: string, chainId: constants.StarknetChainId): RpcProvider => {
+ return new RpcProvider({ nodeUrl: rpcUrl, chainId })
+ },
+ (a: string, b: string) => `${a}::${b}`,
+)
/**
* Returns a provider for the given network
* @param network
* @returns
*/
-export function getProvider(network: Network): ProviderInterface {
- if (network.rpcUrl && shouldUseRpcProvider(network)) {
- return getProviderForRpcUrl(network.rpcUrl)
- } else if (network.sequencerUrl) {
- return getProviderForBaseUrl(network.sequencerUrl)
- } else if (
- "baseUrl" in network &&
- network.baseUrl &&
- typeof network.baseUrl === "string"
- ) {
- return getProviderForBaseUrl(network.baseUrl)
- } else {
- throw new Error("No v5 provider available")
- }
+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)
}
/** ======================================================================== */
-const getProviderV4ForBaseUrl = memoize((baseUrl: string) => {
- return new SequencerProviderV4({ baseUrl })
-})
-
-export function getProviderV4ForRpcUrl(rpcUrl: string): RpcProviderV4 {
- return new RpcProviderV4({ nodeUrl: rpcUrl })
-}
-
-export function getProviderv4(network: Network) {
- if (network.rpcUrl) {
- return getProviderV4ForRpcUrl(network.rpcUrl)
- } else if (network.sequencerUrl) {
- return getProviderV4ForBaseUrl(network.sequencerUrl)
- } else {
- throw new Error("No v4 provider available")
- }
-}
-
-export function getProviderV4ForBaseUrl__deprecated(baseUrl: string) {
- return new SequencerProviderv4__deprecated({ baseUrl })
-}
-
-export function getProviderv4__deprecated(network: Network) {
- // Don't use RPC provider here as it's broken for v4
- if (network.sequencerUrl) {
- return getProviderV4ForBaseUrl__deprecated(network.sequencerUrl)
- } else {
- console.error("RPC is not supported for v4 deprecated provider")
- return undefined
- }
+export function getProviderv4(network: Network): RpcProviderV4 {
+ return new RpcProviderV4({ nodeUrl: network.rpcUrl })
}
/** ======================================================================== */
diff --git a/packages/extension/src/shared/network/schema.ts b/packages/extension/src/shared/network/schema.ts
index cabbfe7b8..430e2b1eb 100644
--- a/packages/extension/src/shared/network/schema.ts
+++ b/packages/extension/src/shared/network/schema.ts
@@ -13,86 +13,75 @@ export const networkStatusSchema = z.enum([
"error",
"unknown",
])
-export const networkSchema = baseNetworkSchema
- .extend({
- name: z.string().min(2).max(128),
- chainId: z
- .string()
- .min(2, "ChainId must be at least 2 characters")
- .max(31, "ChainId cannot be longer than 31 characters") // max 31 characters as required by starknet short strings
- .regex(/^[a-zA-Z0-9_]+$/, {
- message:
- "chain id must be hexadecimal string, uppercase alphanumeric or underscore, like 'SN_GOERLI'",
- }),
- prefer: z.enum(["sequencer", "rpc"]).default("sequencer").optional(),
- sequencerUrl: z
- .string()
- .url("Sequencer url must be a valid URL")
- .optional(),
- rpcUrl: z.string().url("RPC url must be a valid URL").optional(),
- feeTokenAddress: addressOrEmptyUndefinedSchema,
-
- accountImplementation: z.optional(
- z.string().regex(REGEX_HEXSTRING, {
- message: `Account class hash must match the following: /^0x[a-f0-9]+$/i`,
- }),
- ),
- accountClassHash: z.union([
- z.object({
- standard: 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, {
- message: `Account class hash must match the following: /^0x[a-f0-9]+$/i`,
- })
- .optional(),
- plugin: z
- .string()
- .regex(REGEX_HEXSTRING, {
- message: `Account class hash must match the following: /^0x[a-f0-9]+$/i`,
- })
- .optional(),
- multisig: z
- .string()
- .regex(REGEX_HEXSTRING, {
- message: `Account class hash must match the following: /^0x[a-f0-9]+$/i`,
- })
- .optional(),
- betterMulticall: z
- .string()
- .regex(REGEX_HEXSTRING, {
- message: `Account class hash must match the following: /^0x[a-f0-9]+$/i`,
- })
- .optional(),
- argent5MinuteEscapeTestingAccount: z
- .string()
- .regex(REGEX_HEXSTRING, {
- message: `Account class hash must match the following: /^0x[a-f0-9]+$/i`,
- })
- .optional(),
- }),
- z.undefined(),
- ]),
- explorerUrl: z.optional(z.string().url("explorer url must be a valid URL")),
- faucetUrl: z.optional(z.string().url("faucet url must be a valid URL")),
- l1ExplorerUrl: z.optional(
- z.string().url("l1 explorer url must be a valid URL"),
- ),
- blockExplorerUrl: z.optional(
- z.string().url("block explorer url must be a valid URL"),
- ),
- multicallAddress: addressOrEmptyUndefinedSchema,
- readonly: z.optional(z.boolean()),
- })
- .refine(
- (network) => network.rpcUrl || network.sequencerUrl,
- "RPC Url or Sequencer Url must be present",
- )
+export const networkSchema = baseNetworkSchema.extend({
+ name: z.string().min(2).max(128),
+ chainId: z
+ .string()
+ .min(2, "ChainId must be at least 2 characters")
+ .max(31, "ChainId cannot be longer than 31 characters") // max 31 characters as required by starknet short strings
+ .regex(/^[a-zA-Z0-9_]+$/, {
+ message:
+ "chain id must be hexadecimal string, uppercase alphanumeric or underscore, like 'SN_GOERLI'",
+ }),
+ rpcUrl: z.string().url("RPC url must be a valid URL"),
+ feeTokenAddress: addressOrEmptyUndefinedSchema,
+ accountImplementation: z.optional(
+ z.string().regex(REGEX_HEXSTRING, {
+ message: `Account class hash must match the following: /^0x[a-f0-9]+$/i`,
+ }),
+ ),
+ accountClassHash: z.union([
+ z.object({
+ standard: 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, {
+ message: `Account class hash must match the following: /^0x[a-f0-9]+$/i`,
+ })
+ .optional(),
+ plugin: z
+ .string()
+ .regex(REGEX_HEXSTRING, {
+ message: `Account class hash must match the following: /^0x[a-f0-9]+$/i`,
+ })
+ .optional(),
+ multisig: z
+ .string()
+ .regex(REGEX_HEXSTRING, {
+ message: `Account class hash must match the following: /^0x[a-f0-9]+$/i`,
+ })
+ .optional(),
+ betterMulticall: z
+ .string()
+ .regex(REGEX_HEXSTRING, {
+ message: `Account class hash must match the following: /^0x[a-f0-9]+$/i`,
+ })
+ .optional(),
+ argent5MinuteEscapeTestingAccount: z
+ .string()
+ .regex(REGEX_HEXSTRING, {
+ message: `Account class hash must match the following: /^0x[a-f0-9]+$/i`,
+ })
+ .optional(),
+ }),
+ z.undefined(),
+ ]),
+ explorerUrl: z.optional(z.string().url("explorer url must be a valid URL")),
+ faucetUrl: z.optional(z.string().url("faucet url must be a valid URL")),
+ l1ExplorerUrl: z.optional(
+ z.string().url("l1 explorer url must be a valid URL"),
+ ),
+ blockExplorerUrl: z.optional(
+ z.string().url("block explorer url must be a valid URL"),
+ ),
+ multicallAddress: addressOrEmptyUndefinedSchema,
+ readonly: z.optional(z.boolean()),
+})
export const networkWithStatusSchema = z.object({
id: z.string(),
diff --git a/packages/extension/src/shared/network/store.ts b/packages/extension/src/shared/network/store.ts
index 62650982d..68d8d842e 100644
--- a/packages/extension/src/shared/network/store.ts
+++ b/packages/extension/src/shared/network/store.ts
@@ -17,16 +17,10 @@ export const allNetworksStore = new ArrayStorage(defaultNetworks, {
compare: networksEqual,
deserialize(value: Network[]): Network[] {
// overwrite the stored values for the default networks with the default values
- const mergedArray = mergeArrayStableWith(value, defaultReadonlyNetworks, {
+ return mergeArrayStableWith(value, defaultReadonlyNetworks, {
compareFn: networksEqual,
insertMode: "unshift",
})
-
- // except for the prefer property, which should be kept
- return mergedArray.map((n) => ({
- ...n,
- prefer: value.find((v) => v.id === n.id)?.prefer ?? n.prefer,
- }))
},
})
diff --git a/packages/extension/src/shared/network/utils.ts b/packages/extension/src/shared/network/utils.ts
index 460ea97dd..3301fa28e 100644
--- a/packages/extension/src/shared/network/utils.ts
+++ b/packages/extension/src/shared/network/utils.ts
@@ -1,8 +1,8 @@
import { constants } from "starknet"
+import { isEqualAddress } from "@argent/shared"
-import { isEqualAddress } from "../../ui/services/addresses"
-import { ArgentAccountType } from "../wallet.model"
-import { DefaultNetworkId, Network } from "./type"
+import type { ArgentAccountType } from "../wallet.model"
+import type { DefaultNetworkId, Network, PublicRpcNode } from "./type"
import { PUBLIC_RPC_NODES } from "./constants"
// LEGACY ⬇️
@@ -27,21 +27,6 @@ export function mapImplementationToArgentAccountType(
return "standard"
}
-export function getChainIdFromNetworkId(
- networkId: string,
-): constants.StarknetChainId {
- switch (networkId) {
- case "mainnet-alpha":
- return constants.StarknetChainId.SN_MAIN
-
- case "goerli-alpha":
- return constants.StarknetChainId.SN_GOERLI
-
- default:
- throw new Error(`Unknown networkId: ${networkId}`)
- }
-}
-
export function getNetworkIdFromChainId(
encodedChainId: string,
): "mainnet-alpha" | "goerli-alpha" {
@@ -57,7 +42,7 @@ export function getNetworkIdFromChainId(
}
}
-export function getDefaultNetwork(defaultNetworks: Network[]): Network {
+export function getDefaultNetworkId() {
const argentXEnv = process.env.ARGENT_X_ENVIRONMENT
let defaultNetworkId: DefaultNetworkId
@@ -80,6 +65,12 @@ export function getDefaultNetwork(defaultNetworks: Network[]): Network {
throw new Error(`Unknown ARGENTX_ENVIRONMENT: ${argentXEnv}`)
}
+ return defaultNetworkId
+}
+
+export function getDefaultNetwork(defaultNetworks: Network[]): Network {
+ const defaultNetworkId = getDefaultNetworkId()
+
const defaultNetwork = defaultNetworks.find(
(dn) => dn.id === defaultNetworkId,
)
@@ -91,22 +82,6 @@ export function getDefaultNetwork(defaultNetworks: Network[]): Network {
return defaultNetwork
}
-export const getNetworkUrl = (network: Network) => {
- if (network.rpcUrl) {
- return network.rpcUrl
- } else if (network.sequencerUrl) {
- return network.sequencerUrl
- } else if (
- "baseUrl" in network &&
- network.baseUrl &&
- typeof network.baseUrl === "string"
- ) {
- return network.baseUrl
- } else {
- throw new Error("No network URL found")
- }
-}
-
export function isArgentNetwork(network: Network) {
return network.id === "mainnet-alpha" || network.id === "goerli-alpha"
}
@@ -122,3 +97,18 @@ export function getRandomPublicRPCNode(network: Network) {
return randomNode
}
+
+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) {
+ throw new Error(`No nodes found for network: ${network.id}`)
+ }
+
+ return nodeUrls
+}
diff --git a/packages/extension/src/shared/nft/implementation.ts b/packages/extension/src/shared/nft/implementation.ts
new file mode 100644
index 000000000..f42a0625f
--- /dev/null
+++ b/packages/extension/src/shared/nft/implementation.ts
@@ -0,0 +1,201 @@
+import {
+ Address,
+ ArgentBackendNftService,
+ Collection,
+ NftItem,
+ PaginatedItems,
+ isEqualAddress,
+} from "@argent/shared"
+import { differenceWith, groupBy, isEqual } from "lodash-es"
+import { AllowArray, constants, num, shortString } from "starknet"
+import { INFTService } from "./interface"
+import {
+ ContractAddress,
+ INftsCollectionsRepository,
+ INftsContractsRepository,
+ INftsRepository,
+} from "../storage/__new/repositories/nft"
+import { Network } from "../network"
+import { NetworkService } from "../network/service/implementation"
+
+const chainIdToPandoraNetwork = (chainId: string): "mainnet" | "goerli" => {
+ const encodedChainId = num.isHex(chainId)
+ ? chainId
+ : shortString.encodeShortString(chainId)
+
+ switch (encodedChainId) {
+ case constants.StarknetChainId.SN_MAIN:
+ return "mainnet"
+ case constants.StarknetChainId.SN_GOERLI:
+ return "goerli"
+ }
+ throw new Error(`Unsupported network ${chainId}`)
+}
+
+export class NFTService implements INFTService {
+ constructor(
+ private readonly networkService: NetworkService,
+ private readonly nftsRepository: INftsRepository,
+ private readonly nftsCollectionsRepository: INftsCollectionsRepository,
+ private readonly nftsContractsRepository: INftsContractsRepository,
+ private readonly argentNftService: ArgentBackendNftService,
+ ) {}
+
+ isSupported(network: Network) {
+ try {
+ chainIdToPandoraNetwork(network.chainId) // throws if not supported
+ return true
+ } catch {
+ return false
+ }
+ }
+
+ async getAsset(
+ chain: string,
+ networkId: string,
+ collectionAddress?: string,
+ tokenId?: string,
+ ) {
+ if (!collectionAddress || !tokenId) {
+ return null
+ }
+
+ const axNetwork = await this.networkService.getById(networkId)
+
+ const pandoraNetwork = chainIdToPandoraNetwork(axNetwork.chainId)
+
+ return this.argentNftService.getNft(
+ chain,
+ pandoraNetwork,
+ collectionAddress,
+ tokenId,
+ )
+ }
+
+ async getAssets(chain: string, networkId: string, accountAddress: string) {
+ try {
+ const axNetwork = await this.networkService.getById(networkId)
+
+ const pandoraNetwork = chainIdToPandoraNetwork(axNetwork.chainId)
+ const { nfts } = await this.fetchNftsUrl(
+ chain,
+ pandoraNetwork,
+ accountAddress,
+ )
+ return nfts.map((nft) => ({
+ ...nft,
+ networkId,
+ }))
+ } catch (e) {
+ throw new Error(`An error occured ${e}`)
+ }
+ }
+
+ private async fetchNftsUrl(
+ chain: string,
+ network: "mainnet" | "goerli",
+ accountAddress: string,
+ page = 1,
+ ): Promise {
+ const paginateditems: PaginatedItems = await this.argentNftService.getNfts(
+ chain,
+ network,
+ accountAddress,
+ page,
+ )
+ if (page < paginateditems.totalPages) {
+ const nextPage: PaginatedItems = await this.fetchNftsUrl(
+ chain,
+ network,
+ accountAddress,
+ paginateditems.page + 1,
+ )
+
+ return {
+ ...paginateditems,
+ nfts: paginateditems.nfts.concat(nextPage.nfts),
+ }
+ }
+ return paginateditems
+ }
+
+ async getCollection(
+ chain: string,
+ networkId: string,
+ contractAddress: Address,
+ ) {
+ const axNetwork = await this.networkService.getById(networkId)
+
+ const pandoraNetwork = chainIdToPandoraNetwork(axNetwork.chainId)
+ const collection = await this.argentNftService.getCollection(
+ chain,
+ pandoraNetwork,
+ contractAddress,
+ )
+ return collection
+ }
+
+ async setCollections(
+ chain: string,
+ networkId: string,
+ contractsAddresses: ContractAddress[],
+ ) {
+ const axNetwork = await this.networkService.getById(networkId)
+
+ const pandoraNetwork = chainIdToPandoraNetwork(axNetwork.chainId)
+ await this.nftsContractsRepository.upsert(contractsAddresses)
+ const collections = 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 })
+ }
+ }
+
+ if (toPush.length > 0) {
+ await this.nftsCollectionsRepository.upsert(toPush)
+ }
+ }
+
+ async upsert(
+ nfts: AllowArray,
+ owner: Address,
+ networkId: string,
+ ): Promise {
+ // check if there is a difference for the current owner and remove from storage
+ // this will be the retrieved when the new owner fetch the nfts
+ const repositoryNfts = await this.nftsRepository.get(
+ (nft) =>
+ isEqualAddress(nft.owner?.account_address ?? "", owner) &&
+ nft.networkId === networkId,
+ )
+ const differentNftsSameOwner = differenceWith(
+ repositoryNfts ?? [],
+ Array.isArray(nfts) ? nfts : [nfts],
+ isEqual,
+ )
+
+ const differentOwnerNfts = await this.nftsRepository.get(
+ (nft) =>
+ !isEqualAddress(nft.owner?.account_address ?? "", owner) &&
+ nft.networkId === networkId,
+ )
+
+ const removed = [...differentOwnerNfts, ...differentNftsSameOwner]
+
+ if (removed.length > 0) {
+ await this.nftsRepository.remove(removed)
+ }
+
+ await this.nftsRepository.upsert(nfts)
+ }
+}
diff --git a/packages/extension/src/shared/nft/index.ts b/packages/extension/src/shared/nft/index.ts
new file mode 100644
index 000000000..7eb1504be
--- /dev/null
+++ b/packages/extension/src/shared/nft/index.ts
@@ -0,0 +1,27 @@
+import { ArgentBackendNftService } from "@argent/shared"
+import { networkService } from "../network/service"
+import {
+ nftsCollectionsRepository,
+ nftsContractsRepository,
+ nftsRepository,
+} from "../storage/__new/repositories/nft"
+import { NFTService } from "./implementation"
+import { ARGENT_API_BASE_URL } from "../api/constants"
+
+export const argentNftService = new ArgentBackendNftService(
+ ARGENT_API_BASE_URL,
+ {
+ headers: {
+ "argent-version": process.env.VERSION ?? "Unknown version",
+ "argent-client": "argent-x",
+ },
+ },
+)
+
+export const nftService = new NFTService(
+ networkService,
+ nftsRepository,
+ nftsCollectionsRepository,
+ nftsContractsRepository,
+ argentNftService,
+)
diff --git a/packages/extension/src/shared/nft/interface.ts b/packages/extension/src/shared/nft/interface.ts
index 467179e30..36360baf6 100644
--- a/packages/extension/src/shared/nft/interface.ts
+++ b/packages/extension/src/shared/nft/interface.ts
@@ -31,12 +31,4 @@ export interface INFTService {
owner: Address,
networkId: string,
) => Promise
- transferNft: (
- accountAddress: string,
- contractAddress: string,
- tokenId: string,
- recipient: string,
- schema: string,
- network: Network,
- ) => Promise
}
diff --git a/packages/extension/src/shared/nft/test/index.test.ts b/packages/extension/src/shared/nft/test/index.test.ts
new file mode 100644
index 000000000..c8ffd66d1
--- /dev/null
+++ b/packages/extension/src/shared/nft/test/index.test.ts
@@ -0,0 +1,157 @@
+import { ArgentBackendNftService } from "@argent/shared"
+import { rest } from "msw"
+import { setupServer } from "msw/node"
+import { beforeEach, describe, expect, vi } from "vitest"
+import { NFTService } from "../implementation"
+import {
+ emptyJson,
+ expectedValidRes,
+ expectedValidRes2Accounts,
+ invalidJson,
+ validJson,
+} from "./nft.mock"
+import { constants } from "starknet"
+import {
+ nftsCollectionsRepository,
+ nftsContractsRepository,
+ nftsRepository,
+} from "../../storage/__new/repositories/nft"
+import { networkService } from "../../network/service"
+
+const BASE_URL_ENDPOINT = "https://api.hydrogen.argent47.net/v1"
+const INVALID_URL_ENDPOINT = BASE_URL_ENDPOINT + "INVALID"
+const EMPTY_URL_ENDPOINT = BASE_URL_ENDPOINT + "EMPTY"
+const BASE_URL_WITH_WILDCARD = BASE_URL_ENDPOINT + "*"
+
+/**
+ * @vitest-environment jsdom
+ */
+
+const server = setupServer(
+ rest.get(INVALID_URL_ENDPOINT, (req, res, ctx) => {
+ return res(ctx.json(invalidJson))
+ }),
+ rest.get(EMPTY_URL_ENDPOINT, (req, res, ctx) => {
+ return res(ctx.json(emptyJson))
+ }),
+ rest.get(BASE_URL_WITH_WILDCARD, (req, res, ctx) => {
+ return res(ctx.json(validJson))
+ }),
+)
+beforeAll(() => {
+ server.listen()
+})
+
+const repositorytMock = {
+ get: vi.fn().mockResolvedValue(expectedValidRes),
+ upsert: vi.fn().mockResolvedValue(undefined),
+ remove: vi.fn().mockResolvedValue(undefined),
+} as unknown as jest.Mocked
+
+const repositoryCollectionsRepositoryMock = {
+ get: vi.fn().mockResolvedValue(expectedValidRes),
+ upsert: vi.fn().mockResolvedValue(undefined),
+} as unknown as jest.Mocked
+
+const repositorytContractsMock = {
+ get: vi.fn().mockResolvedValue(expectedValidRes),
+ upsert: vi.fn().mockResolvedValue(undefined),
+} as unknown as jest.Mocked
+
+const argentNftServiceMock = {
+ getNfts: vi.fn().mockResolvedValue(validJson),
+} as unknown as jest.Mocked
+
+const networkServiceMock = {
+ getById: vi.fn().mockResolvedValue({
+ name: "testnet",
+ id: "goerli-alpha",
+ chainId: constants.StarknetChainId.SN_GOERLI,
+ sequencerUrl: "https://alpha4.starknet.io",
+ }),
+} as unknown as jest.Mocked
+
+describe("NFTService", () => {
+ let testClass: NFTService
+
+ beforeEach(() => {
+ testClass = new NFTService(
+ networkServiceMock,
+ repositorytMock,
+ repositoryCollectionsRepositoryMock,
+ repositorytContractsMock,
+ argentNftServiceMock,
+ )
+ })
+
+ afterEach(() => {
+ vi.clearAllMocks()
+ })
+
+ describe("getAssets", () => {
+ it("should return nfts", async () => {
+ const result = await testClass.getAssets(
+ "starknet",
+ "goerli-alpha",
+ "0x05f1f0a38429dcab9ffd8a786c0d827e84c1cbd8f60243e6d25d066a13af4a25",
+ )
+
+ expect(result).toEqual(expectedValidRes)
+ })
+ })
+
+ describe("upsert", () => {
+ it("should return nfts", async () => {
+ const result = await testClass.getAssets(
+ "starknet",
+ "goerli-alpha",
+ "0x05f1f0a38429dcab9ffd8a786c0d827e84c1cbd8f60243e6d25d066a13af4a25",
+ )
+
+ expect(result).toEqual(expectedValidRes)
+
+ await testClass.upsert(
+ result,
+ "0x05f1f0a38429dcab9ffd8a786c0d827e84c1cbd8f60243e6d25d066a13af4a25",
+ "goerli-alpha",
+ )
+
+ expect(result).toEqual(expectedValidRes)
+ expect(await repositorytMock.get()).toEqual(result)
+ expect(await repositorytMock.get()).length(1)
+ })
+ })
+
+ describe("upsert for different account", () => {
+ it("should return nfts", async () => {
+ const repositoryMock = {
+ get: vi.fn().mockResolvedValue(expectedValidRes2Accounts),
+ upsert: vi.fn().mockResolvedValue(undefined),
+ remove: vi.fn(),
+ } as unknown as jest.Mocked
+
+ const nftService = new NFTService(
+ networkServiceMock,
+ repositoryMock,
+ repositoryCollectionsRepositoryMock,
+ repositorytContractsMock,
+ argentNftServiceMock,
+ )
+
+ const result = await nftService.getAssets(
+ "starknet",
+ "goerli-alpha",
+ "0x05f1f0a38429dcab9ffd8a786c0d827e84c1cbd8f60243e6d25d066a13af4a25",
+ )
+
+ await nftService.upsert(
+ result,
+ "0x05f1f0a38429dcab9ffd8a786c0d827e84c1cbd8f60243e6d25d066a13af4a25",
+ "goerli-alpha",
+ )
+
+ expect(result).toEqual(expectedValidRes)
+ expect(repositoryMock.remove).toHaveBeenCalled()
+ })
+ })
+})
diff --git a/packages/extension/src/ui/services/nfts/test/nft.mock.ts b/packages/extension/src/shared/nft/test/nft.mock.ts
similarity index 100%
rename from packages/extension/src/ui/services/nfts/test/nft.mock.ts
rename to packages/extension/src/shared/nft/test/nft.mock.ts
diff --git a/packages/extension/src/shared/nft/worker/store.ts b/packages/extension/src/shared/nft/worker/store.ts
deleted file mode 100644
index 6c62a228b..000000000
--- a/packages/extension/src/shared/nft/worker/store.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { KeyValueStorage } from "../../storage"
-import { INFTWorkerStore } from "./interface"
-
-export const nftWorkerStore = new KeyValueStorage(
- {},
- {
- namespace: "core:nft:worker",
- },
-)
diff --git a/packages/extension/src/shared/schedule/mock.ts b/packages/extension/src/shared/schedule/mock.ts
index c103b7029..70cfb9cbd 100644
--- a/packages/extension/src/shared/schedule/mock.ts
+++ b/packages/extension/src/shared/schedule/mock.ts
@@ -1,13 +1,89 @@
-import { IScheduleService } from "./interface"
+import { IScheduleService, ImplementedScheduledTask } from "./interface"
+
+interface ScheduleServiceManager {
+ fireAll: (
+ event: "every" | "in" | "onStartup" | "onInstallAndUpgrade",
+ ) => Promise
+ fireTask: (taskId: string) => Promise
+ _addTaskImpl: (task: ImplementedScheduledTask) => void
+ _addTaskEvent: (
+ event: "every" | "in" | "onStartup" | "onInstallAndUpgrade",
+ taskId: string,
+ ) => void
+ _deleteTask: (taskId: string) => void
+}
+
+export const createScheduleServiceMock = (): [
+ ScheduleServiceManager,
+ IScheduleService,
+] => {
+ const implementations: Set = new Set()
+ const tasksMap = {
+ in: new Set(),
+ every: new Set(),
+ onStartup: new Set(),
+ onInstallAndUpgrade: new Set(),
+ }
+
+ const manager: ScheduleServiceManager = {
+ fireAll: async (
+ event: "every" | "in" | "onStartup" | "onInstallAndUpgrade",
+ ) => {
+ const tasks = tasksMap[event]
+ let success = 0
+ for (const id of tasks) {
+ const x = await manager.fireTask(id)
+ if (x) {
+ success++
+ }
+ }
+ return success
+ },
+ fireTask: async (taskId: string) => {
+ const task = [...implementations].find((t) => t.id === taskId)
+ if (task) {
+ await task.callback()
+ return true
+ }
+ return false
+ },
+ _addTaskImpl: (task: ImplementedScheduledTask) => {
+ implementations.add(task)
+ },
+ _addTaskEvent: (
+ event: "every" | "in" | "onStartup" | "onInstallAndUpgrade",
+ taskId: string,
+ ) => {
+ const tasks = tasksMap[event]
+ tasks.add(taskId)
+ },
+ _deleteTask: (taskId: string) => {
+ const task = [...implementations].find((t) => t.id === taskId)
+ if (task) {
+ implementations.delete(task)
+ }
+ },
+ }
-export const createScheduleServiceMock = (): IScheduleService => {
const service: IScheduleService = {
- delete: vi.fn(() => Promise.resolve()),
- in: vi.fn(() => Promise.resolve()),
- every: vi.fn(() => Promise.resolve()),
- onInstallAndUpgrade: vi.fn(() => Promise.resolve()),
- onStartup: vi.fn(() => Promise.resolve()),
- registerImplementation: vi.fn(() => Promise.resolve()),
+ delete: vi.fn((task) => Promise.resolve(manager._deleteTask(task.id))),
+ in: vi.fn((_, task) =>
+ Promise.resolve(manager._addTaskEvent("in", task.id)),
+ ),
+ every: vi.fn((_, task) =>
+ Promise.resolve(manager._addTaskEvent("every", task.id)),
+ ),
+ onInstallAndUpgrade: vi.fn(() =>
+ Promise.resolve(
+ manager._addTaskEvent("onInstallAndUpgrade", "onInstalled"),
+ ),
+ ),
+ onStartup: vi.fn(() =>
+ Promise.resolve(manager._addTaskEvent("onStartup", "onStartup")),
+ ),
+ registerImplementation: vi.fn((task) =>
+ Promise.resolve(manager._addTaskImpl(task)),
+ ),
}
- return service
+ return [manager, service]
}
diff --git a/packages/extension/src/shared/shield/backend/time.ts b/packages/extension/src/shared/shield/backend/time.ts
index 7d45cf7a1..27614cc07 100644
--- a/packages/extension/src/shared/shield/backend/time.ts
+++ b/packages/extension/src/shared/shield/backend/time.ts
@@ -11,7 +11,7 @@ interface GetTimeResponse {
export const getBackendTimeSeconds = async () => {
try {
- const fetcher = fetcherWithArgentApiHeaders()
+ const fetcher = await fetcherWithArgentApiHeaders()
const { time } = await fetcher(
urlJoin(ARGENT_API_BASE_URL, `time`),
)
diff --git a/packages/extension/src/shared/shield/jwtFetcher.ts b/packages/extension/src/shared/shield/jwtFetcher.ts
index 2ad8d253b..403ddaae6 100644
--- a/packages/extension/src/shared/shield/jwtFetcher.ts
+++ b/packages/extension/src/shared/shield/jwtFetcher.ts
@@ -18,7 +18,7 @@ export const jwtFetcher = async (
"Content-Type": "application/json",
},
}
- const fetcher = fetcherWithArgentApiHeaders()
+ const fetcher = await fetcherWithArgentApiHeaders()
try {
return await fetcher(input, initWithArgentJwtHeaders)
} catch (error) {
diff --git a/packages/extension/src/shared/storage/__new/__test__/inmemoryImplementations.ts b/packages/extension/src/shared/storage/__new/__test__/inmemoryImplementations.ts
index ebb8abd05..c84b232f9 100644
--- a/packages/extension/src/shared/storage/__new/__test__/inmemoryImplementations.ts
+++ b/packages/extension/src/shared/storage/__new/__test__/inmemoryImplementations.ts
@@ -1,4 +1,4 @@
-import { isArray, isEqual, isFunction } from "lodash-es"
+import { isArray, isEqual, isFunction, isString } from "lodash-es"
import {
AllowArray,
@@ -12,6 +12,7 @@ import {
StorageChange,
UpsertResult,
} from "../interface"
+import { IKeyValueStorage } from "../.."
export class InMemoryObjectStore implements IObjectStore {
public namespace: string
@@ -146,3 +147,116 @@ export class InMemoryRepository implements IRepository {
}
}
}
+
+export class InMemoryKeyValueStore>
+ implements IKeyValueStorage
+{
+ private _data: T
+ private _subscribers: Map<
+ keyof T,
+ Set<(value: any, changeSet: StorageChange) => AllowPromise>
+ > = new Map()
+ private _subscribersAll: Set<
+ (changeSet: StorageChange) => AllowPromise
+ > = new Set()
+ public namespace: string
+ public areaName: "local" | "sync"
+ public defaults: T
+
+ constructor(options: IObjectStoreOptions) {
+ this.namespace = options.namespace
+ this.areaName = "local"
+ this._data = options.defaults ? { ...options.defaults } : ({} as T)
+ this.defaults = options.defaults ?? ({} as T)
+
+ if (options.deserialize || options.serialize) {
+ throw new Error("Serialization is not supported in InMemoryObjectStore")
+ }
+ }
+
+ async get(key: K): Promise {
+ return this._data[key]
+ }
+
+ async set(key: K, value: T[K]): Promise {
+ const oldValue = this._data[key]
+ this._data[key] = value
+
+ const subscribers = this._subscribers.get(key)
+ if (subscribers) {
+ const change: StorageChange = { oldValue, newValue: value }
+ subscribers.forEach((subscriber) => {
+ void subscriber(value, change)
+ })
+ }
+ this._subscribersAll.forEach((subscriberAll) => {
+ const change: StorageChange = {
+ oldValue: this._data,
+ newValue: { ...this._data, [key]: value },
+ }
+ void subscriberAll(change)
+ })
+ }
+
+ async delete(key: K): Promise {
+ const oldValue = this._data[key]
+ delete this._data[key]
+
+ const subscribers = this._subscribers.get(key)
+ if (subscribers) {
+ const change: StorageChange = { oldValue, newValue: undefined }
+ subscribers.forEach((subscriber) => {
+ void subscriber(oldValue, change)
+ })
+ }
+ }
+
+ public subscribe(
+ ...args:
+ | [
+ key: K,
+ callback: (
+ value: T[K],
+ changeSet: StorageChange,
+ ) => AllowPromise,
+ ]
+ | [callback: (changeSet: StorageChange) => AllowPromise]
+ ) {
+ if (args.length === 2 && isString(args[0]) && isFunction(args[1])) {
+ return this.subscribeKey(args[0], args[1])
+ }
+ if (args.length === 1 && isFunction(args[0])) {
+ return this.subscribeAll(args[0])
+ }
+ throw new Error("Invalid subscribe arguments")
+ }
+
+ private subscribeKey(
+ key: K,
+ callback: (value: T[K], changeSet: StorageChange) => AllowPromise,
+ ): () => void {
+ let subscribers = this._subscribers.get(key as any)
+ if (!subscribers) {
+ subscribers = new Set()
+ this._subscribers.set(key as any, subscribers)
+ }
+
+ subscribers.add(callback)
+
+ return () => {
+ subscribers?.delete(callback)
+ if (subscribers && subscribers.size === 0) {
+ this._subscribers.delete(key)
+ }
+ }
+ }
+
+ private subscribeAll(
+ callback: (changeSet: StorageChange) => AllowPromise,
+ ) {
+ this._subscribersAll.add(callback)
+ return () => {
+ this._subscribersAll.delete(callback)
+ }
+ }
+}
diff --git a/packages/extension/src/shared/storage/__new/__test__/keyvalue.test.ts b/packages/extension/src/shared/storage/__new/__test__/keyvalue.test.ts
index ad7701039..23b235610 100644
--- a/packages/extension/src/shared/storage/__new/__test__/keyvalue.test.ts
+++ b/packages/extension/src/shared/storage/__new/__test__/keyvalue.test.ts
@@ -1,61 +1,97 @@
+import { MockStorage } from "../../__test__/chrome-storage.mock"
import { KeyValueStorage } from "../../keyvalue"
+import { AreaName, StorageArea } from "../../types"
import { IObjectStore } from "../interface"
import { adaptKeyValue } from "../keyvalue"
type TestData = {
foo: string | null
bar: number
+ baz?: string
}
-describe("adaptKeyValueStore", () => {
- let store: KeyValueStorage
- let adaptedStore: IObjectStore
-
- beforeAll(() => {
- store = new KeyValueStorage(
- { foo: null, bar: 2 },
- { namespace: "testAdapt", areaName: "local" },
- )
- adaptedStore = adaptKeyValue(store)
- })
-
- it("should get data from the store", async () => {
- const result = await adaptedStore.get()
- expect(result).toEqual({ foo: null, bar: 2 })
- })
-
- it("should set data to the store", async () => {
- await adaptedStore.set({ foo: "baz", bar: 3 })
- const barResult = await store.get("bar")
- const fooResult = await store.get("foo")
- expect(barResult).toEqual(3)
- expect(fooResult).toEqual("baz")
- })
-
- it("allows data to be set to null", async () => {
- await adaptedStore.set({ foo: null, bar: 3 })
- const barResult = await store.get("bar")
- const fooResult = await store.get("foo")
-
- expect(barResult).toEqual(3)
- expect(fooResult).toEqual(null)
- })
-
- it("should subscribe to the store", async () => {
- const callback = vi.fn()
- adaptedStore.subscribe(callback)
-
- await adaptedStore.set({ foo: "bar" })
-
- expect(callback).toHaveBeenCalledWith({ foo: "bar", bar: 3 })
- })
-
- it("should batch multiple changes into one callback", async () => {
- const callback = vi.fn()
- adaptedStore.subscribe(callback)
-
- await adaptedStore.set({ foo: "baz", bar: 4 })
-
- expect(callback).toHaveBeenCalledWith({ foo: "baz", bar: 4 })
- })
-})
+type TestDataType = [AreaName, 2 | 3]
+
+describe.each([
+ ["local", 2],
+ ["local", 3],
+ ["managed", 2],
+ ["managed", 3],
+ ["session", 2],
+ ["session", 3],
+])(
+ 'adaptKeyValueStore for area "%s" with manifest v%s',
+ (areaName, manifestVersion) => {
+ let store: KeyValueStorage
+ let adaptedStore: IObjectStore
+ let storageImplementation:
+ | StorageArea
+ | chrome.storage.StorageArea
+ | undefined
+ if (manifestVersion === 2) {
+ storageImplementation = new MockStorage("session")
+ }
+
+ beforeAll(() => {
+ store = new KeyValueStorage(
+ { foo: null, bar: 2 },
+ { namespace: "testAdapt", areaName },
+ storageImplementation,
+ )
+ adaptedStore = adaptKeyValue(store)
+ })
+
+ it("should get data from the store", async () => {
+ const result = await adaptedStore.get()
+ expect(result).toEqual({ foo: null, bar: 2 })
+ })
+
+ it("should set data to the store", async () => {
+ await adaptedStore.set({ foo: "baz", bar: 3 })
+ const barResult = await store.get("bar")
+ const fooResult = await store.get("foo")
+ expect(barResult).toEqual(3)
+ expect(fooResult).toEqual("baz")
+ })
+
+ it("allows data to be set to null", async () => {
+ await adaptedStore.set({ foo: null, bar: 3 })
+ const barResult = await store.get("bar")
+ const fooResult = await store.get("foo")
+
+ expect(barResult).toEqual(3)
+ expect(fooResult).toEqual(null)
+ })
+
+ it("should subscribe to the store", async () => {
+ const callback = vi.fn()
+ adaptedStore.subscribe(callback)
+
+ await adaptedStore.set({ foo: "bar" })
+
+ expect(callback).toHaveBeenCalledWith({ foo: "bar", bar: 3 })
+ })
+
+ it("should batch multiple changes into one callback", async () => {
+ const callback = vi.fn()
+ adaptedStore.subscribe(callback)
+
+ void adaptedStore.set({ foo: "foo", bar: 1 })
+ void adaptedStore.set({ foo: "bar" })
+ await adaptedStore.set({ foo: "baz", bar: 4 })
+
+ expect(callback).toHaveBeenCalledTimes(1)
+ expect(callback).toHaveBeenCalledWith({ foo: "baz", bar: 4 })
+ })
+
+ it("should subscribe to the store with initially undefined value", async () => {
+ const callback = vi.fn()
+ adaptedStore.subscribe(callback)
+
+ await adaptedStore.set({ baz: "foo" })
+
+ expect(callback).toHaveBeenCalledTimes(1)
+ expect(callback).toHaveBeenCalledWith({ foo: "baz", bar: 4, baz: "foo" })
+ })
+ },
+)
diff --git a/packages/extension/src/shared/storage/__new/keyvalue.ts b/packages/extension/src/shared/storage/__new/keyvalue.ts
index e0478c81a..eb2fbb14b 100644
--- a/packages/extension/src/shared/storage/__new/keyvalue.ts
+++ b/packages/extension/src/shared/storage/__new/keyvalue.ts
@@ -1,4 +1,4 @@
-import { debounce, noop, union } from "lodash-es"
+import { debounce } from "lodash-es"
import { KeyValueStorage } from "../keyvalue"
import { IObjectStore, StorageChange } from "./interface"
@@ -41,23 +41,13 @@ export function adaptKeyValue>(
subscribe(
callback: (value: StorageChange>) => void,
): () => void {
+ /** coalesce changes in same event loop */
const debounceTickCallback = debounce(
() => this.get().then(callback),
0,
{ leading: false },
)
- let unsub: () => void = noop
- void storage.getStoredKeys().then((keys) => {
- const defaultsKeys = Object.keys(storage.defaults)
- const allKeys = union(keys, defaultsKeys)
- const unsubs = allKeys.map((key) =>
- storage.subscribe(key, debounceTickCallback),
- )
- unsub = () => {
- unsubs.forEach((unsub) => unsub())
- }
- })
-
+ const unsub = storage.subscribe(debounceTickCallback)
return unsub
},
}
diff --git a/packages/extension/src/shared/storage/__new/prune.test.ts b/packages/extension/src/shared/storage/__new/prune.test.ts
new file mode 100644
index 000000000..7cfb79d2b
--- /dev/null
+++ b/packages/extension/src/shared/storage/__new/prune.test.ts
@@ -0,0 +1,126 @@
+import { describe, expect, test, vi } from "vitest"
+
+import {
+ IMinimalStorage,
+ Pattern,
+ copyObjectToStorage,
+ copyStorageToObject,
+ pruneStorageData,
+ setItemWithStorageQuotaExceededStrategy,
+} from "./prune"
+
+const QUOTA_CHARACTERS = 10
+
+const ERROR_STRING = `Quota exceeded - max ${QUOTA_CHARACTERS} characters`
+
+function createStorageMock(): IMinimalStorage {
+ return Object.create(
+ {},
+ {
+ getItem: {
+ value(key: string) {
+ return this[key] ?? null
+ },
+ },
+ setItem: {
+ value(key: string, value: any) {
+ const snapshot = this[key]
+ this[key] = `${value}`
+ const bytesUsed = Object.values(this).reduce(
+ (acc, currentString) => {
+ return acc + currentString.length
+ },
+ 0,
+ )
+ if (bytesUsed > QUOTA_CHARACTERS) {
+ if (snapshot) {
+ this[key] = snapshot
+ }
+ throw new DOMException(ERROR_STRING, "QuotaExceededError")
+ }
+ },
+ },
+ removeItem: {
+ value(key: string) {
+ delete this[key]
+ },
+ },
+ },
+ )
+}
+
+describe("prune", () => {
+ describe("pruneStorageData", () => {
+ it("should prune keys matching pattern", async () => {
+ const patterns: Pattern[] = [/foo/, /bar/]
+ const store = createStorageMock()
+ store.setItem("foo", "1")
+ store.setItem("bar", "2")
+ store.setItem("baz", "3")
+ pruneStorageData(store, patterns)
+ expect(Object.keys(store)).toEqual(["baz"])
+ })
+ it("should update values with pruning function", async () => {
+ const pruneFn = vi.fn((value) => (value === "2" ? "pruned" : value))
+ const patterns: Pattern[] = [[/baz/, pruneFn]]
+ const store = createStorageMock()
+ store.setItem("foo:baz", "1")
+ store.setItem("bar:baz", "2")
+ pruneStorageData(store, patterns)
+ expect(Object.keys(store)).toEqual(["foo:baz", "bar:baz"])
+ expect(pruneFn).toHaveBeenNthCalledWith(1, "1")
+ expect(pruneFn).toHaveBeenNthCalledWith(2, "2")
+ expect(store.getItem("foo:baz")).toEqual("1")
+ expect(store.getItem("bar:baz")).toEqual("pruned")
+ })
+ })
+ describe("copyStorageToObject", () => {
+ it("should copy storage to object", async () => {
+ const store = createStorageMock()
+ store.setItem("foo", "1")
+ store.setItem("bar", "2")
+ store.setItem("baz", "3")
+ const object = copyStorageToObject(store)
+ expect(object).toEqual({
+ foo: "1",
+ bar: "2",
+ baz: "3",
+ })
+ copyObjectToStorage(
+ {
+ foo: "2",
+ bar: "1",
+ },
+ store,
+ )
+ expect(store.getItem("foo")).toEqual("2")
+ expect(store.getItem("bar")).toEqual("1")
+ expect(store.getItem("baz")).toBeNull()
+ })
+ })
+ describe("setItemWithStorageQuotaExceededStrategy", () => {
+ it("should prune entries", async () => {
+ const patterns: Pattern[] = [/foo/, /bar/]
+ const store = createStorageMock()
+ setItemWithStorageQuotaExceededStrategy("foo", "01234", store, patterns)
+ setItemWithStorageQuotaExceededStrategy("bar", "56789", store, patterns)
+ expect(store.getItem("foo")).toEqual("01234")
+ expect(store.getItem("bar")).toEqual("56789")
+ /** exceed quota - expect prune */
+ setItemWithStorageQuotaExceededStrategy("baz", "abcde", store, patterns)
+ expect(store.getItem("foo")).toBeNull()
+ expect(store.getItem("bar")).toBeNull()
+ expect(store.getItem("baz")).toEqual("abcde")
+ /** exceed quota - individual item won't fit */
+ expect(() =>
+ setItemWithStorageQuotaExceededStrategy(
+ "baz",
+ "0123456789abcde",
+ store,
+ patterns,
+ ),
+ ).toThrowError(ERROR_STRING)
+ expect(store.getItem("baz")).toEqual("abcde")
+ })
+ })
+})
diff --git a/packages/extension/src/shared/storage/__new/prune.ts b/packages/extension/src/shared/storage/__new/prune.ts
index 79b75a0c5..8e55aabde 100644
--- a/packages/extension/src/shared/storage/__new/prune.ts
+++ b/packages/extension/src/shared/storage/__new/prune.ts
@@ -1,70 +1,136 @@
-import urlJoin from "url-join"
-import { ARGENT_API_BASE_URL } from "../../api/constants"
-
-const DEFAULT_THRESHOLD = 0.9 // 90%
-
-const pruneThreshold = DEFAULT_THRESHOLD
-
-const keysToRemoveFirst = [
- // Might be an old key as it does not seem to exist in newer wallets, so we want to remove it first.
- urlJoin(ARGENT_API_BASE_URL, "tokens/info?chain="),
- urlJoin(ARGENT_API_BASE_URL, "tokens/info?chain=starknet"),
-] // Add keys you want to prioritize for removal here.
-
-export function pruneData() {
- // Fetch all keys from the local storage.
- const keys = Object.keys(localStorage)
-
- // Sort keys based on their priority in keysToRemoveFirst.
- keys.sort((a, b) => {
- const indexA = keysToRemoveFirst.indexOf(a)
- const indexB = keysToRemoveFirst.indexOf(b)
+import { isArray } from "lodash-es"
+import {
+ ARGENT_API_BASE_URL,
+ ARGENT_EXPLORER_BASE_URL,
+} from "../../api/constants"
+import { Transaction, getInFlightTransactions } from "../../transactions"
+
+export interface IMinimalStorage
+ extends Pick {}
+
+export type PruneFn = (value: string) => string
+
+export type Pattern = RegExp | [RegExp, PruneFn]
+
+/** prune anything which can be retrieved again on-demand */
+
+const localStoragePatterns: Pattern[] = [
+ new RegExp(`${ARGENT_API_BASE_URL}`, "i"),
+ new RegExp(`${ARGENT_EXPLORER_BASE_URL}`, "i"),
+ /"useTransactionReviewV2"/,
+ /"simulateAndReview"/,
+ /"maxEthTransferEstimate"/,
+ /"accountDeploymentFeeEstimation"/,
+ /"nonce"/,
+ /"fee"/,
+ /"balanceOf"/,
+ /"accountTokenBalances"/,
+ /"feeTokenBalance"/,
+ [/^core:transactions$/, pruneTransactions],
+ /^dev:storage/,
+]
+
+/** keep in-flight transactions as they can't be retreived from backend or on-chain */
+
+export function pruneTransactions(value: string) {
+ try {
+ const transactions: Transaction[] = JSON.parse(value)
+ const prunedTransactions = getInFlightTransactions(transactions)
+ return JSON.stringify(prunedTransactions)
+ } catch (e) {
+ // ignore parsing error
+ }
+ return value
+}
- if (indexA !== -1 && indexB === -1) {
- return -1 // a comes before b
- }
- if (indexA === -1 && indexB !== -1) {
- return 1 // b comes before a
- }
- if (indexA !== -1 && indexB !== -1) {
- return indexA - indexB // sort by their position in keysToRemoveFirst
+export function copyStorageToObject(storage: IMinimalStorage = localStorage) {
+ const object: Record = {}
+ for (const key of Object.keys(storage)) {
+ const value = storage.getItem(key)
+ if (value !== null) {
+ object[key] = value
}
+ }
+ return object
+}
- // If neither key is in keysToRemoveFirst, keep the original order
- return 0
- })
+export function copyObjectToStorage(
+ object: Record,
+ storage: IMinimalStorage = localStorage,
+) {
+ for (const key of Object.keys(storage)) {
+ storage.removeItem(key)
+ }
+ for (const key in object) {
+ const value = object[key]
+ storage.setItem(key, value)
+ }
+}
- // Remove items based on sorted order till storage is below threshold or all items are removed.
- for (const key of keys) {
- localStorage.removeItem(key)
- if (checkIfBelowThreshold()) {
- break
+export function pruneStorageData(
+ storage: IMinimalStorage = localStorage,
+ patterns: Pattern[] = localStoragePatterns,
+) {
+ for (const key of Object.keys(storage)) {
+ const matches = patterns.some((pattern) => {
+ if (isArray(pattern)) {
+ const [regexp, pruneFn] = pattern
+ if (regexp.test(key)) {
+ const value = storage.getItem(key)
+ if (value !== null) {
+ storage.setItem(key, pruneFn(value))
+ return false
+ }
+ }
+ return false
+ }
+ return pattern.test(key)
+ })
+ if (matches) {
+ storage.removeItem(key)
}
}
}
-const MAX_STORAGE_BYTES = 5 * 1024 * 1024 // 5MB in bytes (what is usually allowed by browsers)
-
-export function checkStorageAndPrune() {
- const isBelowThreshold = checkIfBelowThreshold()
+export function isQuotaExceededError(e: unknown) {
+ return e instanceof DOMException && e.name === "QuotaExceededError"
+}
- if (!isBelowThreshold) {
- pruneData()
+/** strategy - tries to store value, on quota error prunes storage and then tries again - avoids expensive storage size checks */
+
+export function setItemWithStorageQuotaExceededStrategy(
+ key: string,
+ value: string,
+ storage: IMinimalStorage = localStorage,
+ patterns: Pattern[] = localStoragePatterns,
+) {
+ /** quota sizes vary - try to set the item and catch quota error */
+ try {
+ return storage.setItem(key, value)
+ } catch (e) {
+ /** only continue if quota was exceeded */
+ if (!isQuotaExceededError(e)) {
+ throw e
+ }
+ }
+ /** if there is a further error pruning or storing, revert to snapshot */
+ const snapshot = copyStorageToObject(storage)
+ try {
+ /** prune data and try again */
+ pruneStorageData(storage, patterns)
+ return storage.setItem(key, value)
+ } catch (e) {
+ /** TODO: could potentially further check for quota error here and try pruning matching key and value, then setting pruned value */
+ /** revert storage to original snapshot */
+ copyObjectToStorage(snapshot, storage)
+ throw e
}
}
-function getTotalUsedBytes() {
+export function getStorageUsedBytes(storage: Storage = localStorage) {
let usedBytes = 0
-
- for (const key in localStorage) {
- usedBytes += new Blob([localStorage[key]]).size
+ for (const key of Object.keys(storage)) {
+ usedBytes += new Blob([storage[key]]).size
}
-
return usedBytes
}
-
-function checkIfBelowThreshold() {
- const usedBytes = getTotalUsedBytes()
-
- return usedBytes / MAX_STORAGE_BYTES <= pruneThreshold
-}
diff --git a/packages/extension/src/shared/storage/__test__/keyvalue.test.ts b/packages/extension/src/shared/storage/__test__/keyvalue.test.ts
index fa2025a61..5a9fda17b 100644
--- a/packages/extension/src/shared/storage/__test__/keyvalue.test.ts
+++ b/packages/extension/src/shared/storage/__test__/keyvalue.test.ts
@@ -1,82 +1,158 @@
+import { describe, expect, test, vi } from "vitest"
+
import { IKeyValueStorage, KeyValueStorage } from "../keyvalue"
+import { AreaName, StorageArea } from "../types"
+import { MockStorage } from "./chrome-storage.mock"
-describe("full storage flow", () => {
- let store: IKeyValueStorage<{
- foo: string
- }>
- beforeAll(() => {
- store = new KeyValueStorage<{ foo: string }>(
- { foo: "bar" },
- { namespace: "test", areaName: "local" },
- )
- })
- test("throw when storage area is invalid", () => {
- expect(() => {
- store = new KeyValueStorage<{ foo: string }>(
+interface IStore {
+ foo: string
+}
+
+type TestDataType = [AreaName, 2 | 3]
+
+describe.each([
+ ["local", 2],
+ ["local", 3],
+ ["managed", 2],
+ ["managed", 3],
+ ["session", 2],
+ ["session", 3],
+])(
+ 'Full storage flow for area "%s" with manifest v%s',
+ (areaName, manifestVersion) => {
+ let store: IKeyValueStorage
+ let storageImplementation:
+ | StorageArea
+ | chrome.storage.StorageArea
+ | undefined
+ if (manifestVersion === 2) {
+ storageImplementation = new MockStorage("session")
+ }
+ beforeAll(() => {
+ store = new KeyValueStorage(
{ foo: "bar" },
- { namespace: "test", areaName: "invalid" as any },
+ { namespace: "test", areaName },
+ storageImplementation,
)
- }).toThrowErrorMatchingInlineSnapshot('"Unknown storage area: invalid"')
- })
- test("should return defaults", async () => {
- const value = await store.get("foo")
- expect(value).toBe("bar")
- })
- test("should write", async () => {
- await store.set("foo", "baz")
- const value = await store.get("foo")
- expect(value).toBe("baz")
- })
- test("should remove and return default value", async () => {
- await store.delete("foo")
- const value = await store.get("foo")
- expect(value).toBe("bar") // default
- })
-})
+ })
+ test(`should have a valid storage type: ${areaName}`, () => {
+ expect(typeof areaName).toBe("string")
+ })
+ test("should return defaults", async () => {
+ const value = await store.get("foo")
+ expect(value).toBe("bar")
+ })
+ test("should write", async () => {
+ await store.set("foo", "baz")
+ const value = await store.get("foo")
+ expect(value).toBe("baz")
+ })
+ test("should remove and return default value", async () => {
+ await store.delete("foo")
+ const value = await store.get("foo")
+ expect(value).toBe("bar") // default
+ })
+ },
+)
-describe("full storage flow with subscription", () => {
- let store: IKeyValueStorage<{
- foo: string
- }>
- beforeAll(() => {
- store = new KeyValueStorage<{ foo: string }>(
- { foo: "bar" },
- { namespace: "test", areaName: "local" },
- )
- })
- test("should write and notify", async () => {
- const handler = vi.fn()
- const unsub = store.subscribe("foo", handler)
- await store.set("foo", "baz")
+describe.each([
+ ["local", 2],
+ ["local", 3],
+ ["managed", 2],
+ ["managed", 3],
+ ["session", 2],
+ ["session", 3],
+])(
+ 'Full storage flow for area "%s" with manifest v%s with subscription',
+ (areaName, manifestVersion) => {
+ let storageImplementation:
+ | StorageArea
+ | chrome.storage.StorageArea
+ | undefined
+ if (manifestVersion === 2) {
+ storageImplementation = new MockStorage("session")
+ }
+ let store: IKeyValueStorage
+ beforeAll(() => {
+ store = new KeyValueStorage(
+ { foo: "bar" },
+ { namespace: "test", areaName },
+ storageImplementation,
+ )
+ })
+ test("should write and notify", async () => {
+ const handler = vi.fn()
+ const allHandler = vi.fn()
+ const unsub = store.subscribe("foo", handler)
+ const unsubAll = store.subscribe(allHandler)
+ await store.set("foo", "baz")
+
+ expect(handler).toHaveBeenCalledTimes(1)
+ expect(handler).toHaveBeenCalledWith("baz", {
+ newValue: "baz",
+ oldValue: undefined,
+ })
- expect(handler).toHaveBeenCalledTimes(1)
- expect(handler).toHaveBeenCalledWith("baz", {
- newValue: "baz",
- oldValue: undefined,
+ expect(allHandler).toHaveBeenCalledTimes(1)
+ expect(allHandler).toHaveBeenCalledWith({
+ newValue: {
+ foo: "baz",
+ },
+ oldValue: {},
+ })
+
+ const value = await store.get("foo")
+ expect(value).toBe("baz")
+ unsub()
+ unsubAll()
})
- const value = await store.get("foo")
- expect(value).toBe("baz")
- unsub()
- })
- test("should remove, fallback to default and notify", async () => {
- const handler = vi.fn()
- const unsub = store.subscribe("foo", handler)
- await store.delete("foo")
+ test("should remove, fallback to default and notify", async () => {
+ const handler = vi.fn()
+ const allHandler = vi.fn()
+ const unsub = store.subscribe("foo", handler)
+ const unsubAll = store.subscribe(allHandler)
+ await store.delete("foo")
+
+ expect(handler).toHaveBeenCalledTimes(1)
+ expect(handler).toHaveBeenCalledWith("bar", {
+ oldValue: "baz",
+ newValue: undefined,
+ })
+
+ expect(allHandler).toHaveBeenCalledTimes(1)
+ expect(allHandler).toHaveBeenCalledWith({
+ newValue: {},
+ oldValue: {
+ foo: "baz",
+ },
+ })
- expect(handler).toHaveBeenCalledTimes(1)
- expect(handler).toHaveBeenCalledWith("bar", {
- oldValue: "baz",
- newValue: undefined,
+ const value = await store.get("foo")
+ expect(value).toBe("bar")
+ unsub()
+ unsubAll()
})
- const value = await store.get("foo")
- expect(value).toBe("bar")
- unsub()
- })
- test("should unsubscribe", async () => {
- const handler = vi.fn()
- const unsub = store.subscribe("foo", handler)
- unsub()
- await store.set("foo", "baz")
- expect(handler).not.toHaveBeenCalled()
+ test("should unsubscribe", async () => {
+ const handler = vi.fn()
+ const allHandler = vi.fn()
+ const unsub = store.subscribe("foo", handler)
+ const unsubAll = store.subscribe(allHandler)
+ unsub()
+ unsubAll()
+ await store.set("foo", "baz")
+ expect(handler).not.toHaveBeenCalled()
+ expect(allHandler).not.toHaveBeenCalled()
+ })
+ },
+)
+
+describe("when invalid", () => {
+ test("throw when storage area is invalid", () => {
+ expect(() => {
+ new KeyValueStorage(
+ { foo: "bar" },
+ { namespace: "test", areaName: "invalid" as any },
+ )
+ }).toThrowErrorMatchingInlineSnapshot('"Unknown storage area: invalid"')
})
})
diff --git a/packages/extension/src/shared/storage/keyvalue.ts b/packages/extension/src/shared/storage/keyvalue.ts
index b3c9b2f9a..71d1abe8c 100644
--- a/packages/extension/src/shared/storage/keyvalue.ts
+++ b/packages/extension/src/shared/storage/keyvalue.ts
@@ -9,6 +9,7 @@ import {
StorageArea,
StorageChange,
} from "./types"
+import { isFunction, isString } from "lodash-es"
export interface IKeyValueStorage<
T extends Record = Record,
@@ -16,10 +17,15 @@ export interface IKeyValueStorage<
get(key: K): Promise
set(key: K, value: T[K]): Promise
delete(key: K): Promise
+ /** subscribe to changes for a single key */
subscribe(
key: K,
callback: (value: T[K], changeSet: StorageChange) => AllowPromise,
): () => void
+ /** subscribe to all changes */
+ subscribe(
+ callback: (changeSet: StorageChange) => AllowPromise,
+ ): () => void
}
export const isMockStorage = (storage: StorageArea): storage is MockStorage => {
@@ -37,10 +43,15 @@ export class KeyValueStorage<
constructor(
public readonly defaults: T,
optionsOrNamespace: StorageOptionsOrNameSpace,
+ storageImplementation?: StorageArea | browser.storage.StorageArea,
) {
const options = getOptionsWithDefaults(optionsOrNamespace)
this.namespace = options.namespace
this.areaName = options.areaName
+ if (storageImplementation) {
+ this.storageImplementation = storageImplementation
+ return
+ }
try {
this.storageImplementation = browser.storage[options.areaName]
if (!this.storageImplementation) {
@@ -59,8 +70,12 @@ export class KeyValueStorage<
}
}
+ private getStorageKeyPrefix(): string {
+ return this.namespace + ":"
+ }
+
private getStorageKey(key: K): string {
- return this.namespace + ":" + key.toString()
+ return this.getStorageKeyPrefix() + key.toString()
}
public async get(key: K): Promise {
@@ -88,7 +103,34 @@ export class KeyValueStorage<
public subscribe(
key: K,
callback: (value: T[K], changeSet: StorageChange) => AllowPromise,
+ ): () => void
+ public subscribe(
+ callback: (changeSet: StorageChange) => AllowPromise,
+ ): () => void
+ public subscribe(
+ ...args:
+ | [
+ key: K,
+ callback: (
+ value: T[K],
+ changeSet: StorageChange,
+ ) => AllowPromise,
+ ]
+ | [callback: (changeSet: StorageChange) => AllowPromise]
): () => void {
+ if (args.length === 2 && isString(args[0]) && isFunction(args[1])) {
+ return this.subscribeKey(args[0], args[1])
+ }
+ if (args.length === 1 && isFunction(args[0])) {
+ return this.subscribeAll(args[0])
+ }
+ throw new Error("Invalid subscribe arguments")
+ }
+
+ private subscribeKey(
+ key: K,
+ callback: (value: T[K], changeSet: StorageChange) => AllowPromise,
+ ) {
const storageKey = this.getStorageKey(key)
/** storage for manifest v2 */
@@ -128,6 +170,70 @@ export class KeyValueStorage<
return () => browser.storage.onChanged.removeListener(handler)
}
+ /** convert all changes into one payload for the single storage key */
+ private deriveChanges(
+ changes: Record,
+ storageKeyPrefix = this.getStorageKeyPrefix(),
+ ) {
+ let hasChanges = false
+ const derivedChanges: StorageChange = {
+ newValue: {},
+ oldValue: {},
+ }
+ for (const [key, value] of Object.entries(changes)) {
+ if (!key.startsWith(storageKeyPrefix)) {
+ continue
+ }
+ const storageKey = key.substring(storageKeyPrefix.length)
+ derivedChanges.newValue[storageKey] = value.newValue ?? this.defaults[key] // if newValue is undefined, it means the value was deleted from storage, so we use the default value
+ if (value.oldValue !== undefined) {
+ derivedChanges.oldValue[storageKey] = value.oldValue
+ }
+ hasChanges = true
+ }
+ if (hasChanges) {
+ return derivedChanges
+ }
+ }
+
+ private subscribeAll(
+ callback: (changeSet: StorageChange) => AllowPromise,
+ ) {
+ /** storage for manifest v2 */
+ if (isMockStorage(this.storageImplementation)) {
+ const handler = (changes: Record) => {
+ const derivedChanges = this.deriveChanges(changes)
+ if (derivedChanges) {
+ void callback(derivedChanges)
+ }
+ }
+
+ this.storageImplementation.onChanged.addListener(handler)
+
+ return () => {
+ if (isMockStorage(this.storageImplementation)) {
+ this.storageImplementation.onChanged.removeListener(handler)
+ }
+ }
+ }
+
+ const handler = (
+ changes: Record,
+ areaName: browser.storage.AreaName,
+ ) => {
+ if (this.areaName === areaName) {
+ const derivedChanges = this.deriveChanges(changes)
+ if (derivedChanges) {
+ void callback(derivedChanges)
+ }
+ }
+ }
+
+ browser.storage.onChanged.addListener(handler)
+
+ return () => browser.storage.onChanged.removeListener(handler)
+ }
+
/**
* @internal for migration purposes only
*/
diff --git a/packages/extension/src/shared/token/__new/types/token.model.ts b/packages/extension/src/shared/token/__new/types/token.model.ts
index 257948d62..7528f5c74 100644
--- a/packages/extension/src/shared/token/__new/types/token.model.ts
+++ b/packages/extension/src/shared/token/__new/types/token.model.ts
@@ -44,7 +44,11 @@ export const ApiTokenDetailsSchema = z.object({
refundable: z.boolean(),
listed: z.boolean(),
tradable: z.boolean(),
- category: z.union([z.literal("tokens"), z.literal("currencies")]),
+ category: z.union([
+ z.literal("tokens"),
+ z.literal("currencies"),
+ z.literal("savings"),
+ ]),
pricingId: z.number().optional(),
})
diff --git a/packages/extension/src/shared/token/__new/worker/implementation.test.ts b/packages/extension/src/shared/token/__new/worker/implementation.test.ts
index 6d4d7dfd0..bba27824c 100644
--- a/packages/extension/src/shared/token/__new/worker/implementation.test.ts
+++ b/packages/extension/src/shared/token/__new/worker/implementation.test.ts
@@ -2,7 +2,7 @@ import { IRepository } from "./../../../storage/__new/interface"
import { Mocked } from "vitest"
import { INetworkService } from "../../../network/service/interface"
import { ITokenService } from "../service/interface"
-import { TokenWorker, TokenWorkerSchedule } from "./implementation"
+import { TokenWorker } from "./implementation"
import { MockFnRepository } from "../../../storage/__new/__test__/mockFunctionImplementation"
import { WalletStorageProps } from "../../../wallet/walletStore"
import { KeyValueStorage } from "../../../storage"
@@ -13,7 +13,6 @@ import { IScheduleService } from "../../../schedule/interface"
import {
emitterMock,
recoverySharedServiceMock,
- sessionServiceMock,
} from "../../../../background/wallet/test.utils"
import { IBackgroundUIService } from "../../../../background/__new/services/ui/interface"
import { getMockNetwork } from "../../../../../test/network.mock"
@@ -28,9 +27,13 @@ import { BaseWalletAccount } from "../../../wallet.model"
import { BaseTokenWithBalance } from "../types/tokenBalance.model"
import { TokenPriceDetails } from "../types/tokenPrice.model"
import { defaultNetwork } from "../../../network"
+import { IDebounceService } from "../../../debounce"
+import { getMockDebounceService } from "../../../debounce/mock"
+import { createScheduleServiceMock } from "../../../schedule/mock"
+import { IActivityService } from "../../../../background/__new/services/activity/interface"
+import { IActivityStorage } from "../../../activity/types"
const accountAddress1 = addressSchema.parse(stark.randomAddress())
-const accountAddress2 = addressSchema.parse(stark.randomAddress())
const tokenAddress1 = addressSchema.parse(stark.randomAddress())
const tokenAddress2 = addressSchema.parse(stark.randomAddress())
@@ -42,9 +45,10 @@ describe("TokenWorker", () => {
let mockTransactionsRepo: IRepository
let mockTokenRepo: IRepository
let mockAccountService: IAccountService
- let mockScheduleService: IScheduleService
+ let mockScheduleService: IScheduleService
let mockBackgroundUIService: IBackgroundUIService
-
+ let mockDebounceService: IDebounceService
+ let mockActivityService: IActivityService
beforeEach(() => {
// Initialize mocks
mockTokenService = {
@@ -79,23 +83,29 @@ describe("TokenWorker", () => {
get: vi.fn(),
} as unknown as IAccountService
+ mockActivityService = {
+ shouldUpdateBalance: vi.fn().mockResolvedValue({
+ shouldUpdate: true,
+ lastModified: 1234,
+ id: "1234",
+ }),
+ addActivityToStore: vi.fn(),
+ } as unknown as IActivityService
+
mockTransactionsRepo = new MockFnRepository()
mockTokenRepo = new MockFnRepository()
mockBackgroundUIService = {
+ opened: true,
emitter: emitterMock,
openUiAndUnlock: vi.fn(),
- } as unknown as IBackgroundUIService
-
- mockScheduleService = {
- registerImplementation: vi.fn(),
- in: vi.fn(),
- every: vi.fn(),
- delete: vi.fn(),
- onInstallAndUpgrade: vi.fn(),
- onStartup: vi.fn(),
}
+ const [, _mockScheduleService] = createScheduleServiceMock()
+ mockScheduleService = _mockScheduleService
+
+ mockDebounceService = getMockDebounceService()
+
tokenWorker = new TokenWorker(
mockWalletStore,
mockTransactionsRepo,
@@ -103,10 +113,11 @@ describe("TokenWorker", () => {
mockTokenService,
mockAccountService,
mockNetworkService,
- sessionServiceMock,
recoverySharedServiceMock,
mockBackgroundUIService,
mockScheduleService,
+ mockDebounceService,
+ mockActivityService,
)
})
@@ -166,7 +177,7 @@ describe("TokenWorker", () => {
)
// Act
- await tokenWorker.updateTokenBalances(mockAccount)
+ await tokenWorker.updateTokenBalancesForAccount(mockAccount)
// Assert
expect(mockTokenService.getTokens).toHaveBeenCalledWith(
@@ -183,11 +194,11 @@ describe("TokenWorker", () => {
it("should fetch token balances for all accounts on the current network and update the token service when no account is provided", async () => {
const mockSelectedAccount: BaseWalletAccount = {
address: accountAddress1,
- networkId: "1",
+ networkId: "goerli-alpha",
}
const mockAccounts: BaseWalletAccount[] = [mockSelectedAccount]
- const mockBaseToken = getMockBaseToken({ networkId: "1" })
- const mockTokens: Token[] = [getMockToken({ networkId: "1" })]
+ const mockBaseToken = getMockBaseToken({ networkId: "goerli-alpha" })
+ const mockTokens: Token[] = [getMockToken({ networkId: "goerli-alpha" })]
const mockTokensWithBalances: BaseTokenWithBalance[] = [
{
...mockBaseToken,
@@ -203,6 +214,7 @@ describe("TokenWorker", () => {
.mockResolvedValue(mockTokensWithBalances)
await tokenWorker.updateTokenBalances()
+
expect(mockWalletStore.get).toHaveBeenCalledWith("selected")
expect(mockAccountService.get).toHaveBeenCalledWith(expect.any(Function))
expect(mockTokenService.getTokens).toHaveBeenCalledWith(
@@ -214,9 +226,85 @@ describe("TokenWorker", () => {
expect(mockTokenService.updateTokenBalances).toHaveBeenCalledWith(
mockTokensWithBalances,
)
+ expect(mockActivityService.addActivityToStore).toHaveBeenCalledWith({
+ address: mockSelectedAccount.address,
+ lastModified: 1234,
+ id: "1234",
+ })
})
})
+ it("should not fetch token balances for all accounts on the current network and not update the token service if activity service returns false", async () => {
+ const mockSelectedAccount: BaseWalletAccount = {
+ address: accountAddress1,
+ networkId: "goerli-alpha",
+ }
+ const mockAccounts: BaseWalletAccount[] = [mockSelectedAccount]
+ const mockBaseToken = getMockBaseToken({ networkId: "1" })
+ const mockTokens: Token[] = [getMockToken({ networkId: "1" })]
+ const mockTokensWithBalances: BaseTokenWithBalance[] = [
+ {
+ ...mockBaseToken,
+ account: mockSelectedAccount,
+ balance: "100",
+ },
+ ]
+ mockActivityService.shouldUpdateBalance = vi
+ .fn()
+ .mockResolvedValue({ shouldUpdate: false })
+ mockWalletStore.get = vi.fn().mockReturnValue(mockSelectedAccount)
+ mockAccountService.get = vi.fn().mockResolvedValue(mockAccounts)
+ mockTokenService.getTokens = vi.fn().mockResolvedValue(mockTokens)
+ mockTokenService.fetchTokenBalancesFromOnChain = vi
+ .fn()
+ .mockResolvedValue(mockTokensWithBalances)
+
+ await tokenWorker.updateTokenBalances()
+
+ expect(mockWalletStore.get).toHaveBeenCalledWith("selected")
+ expect(mockAccountService.get).toHaveBeenCalledWith(expect.any(Function))
+ expect(mockTokenService.getTokens).toHaveBeenCalledWith(
+ expect.any(Function),
+ )
+ expect(
+ mockTokenService.fetchTokenBalancesFromOnChain,
+ ).not.toHaveBeenCalled()
+ expect(mockTokenService.updateTokenBalances).not.toHaveBeenCalledWith(
+ mockTokensWithBalances,
+ )
+ })
+
+ it("should not fetch token balances from the backend if the selected network isnt support by backend", async () => {
+ const mockSelectedAccount: BaseWalletAccount = {
+ address: accountAddress1,
+ networkId: "insupportable",
+ }
+ const mockAccounts: BaseWalletAccount[] = [mockSelectedAccount]
+ const mockBaseToken = getMockBaseToken({ networkId: "1" })
+ const mockTokens: Token[] = [getMockToken({ networkId: "1" })]
+ const mockTokensWithBalances: BaseTokenWithBalance[] = [
+ {
+ ...mockBaseToken,
+ account: mockSelectedAccount,
+ balance: "100",
+ },
+ ]
+ mockActivityService.shouldUpdateBalance = vi.fn().mockResolvedValue(false)
+ mockWalletStore.get = vi.fn().mockReturnValue(mockSelectedAccount)
+ mockAccountService.get = vi.fn().mockResolvedValue(mockAccounts)
+ mockTokenService.getTokens = vi.fn().mockResolvedValue(mockTokens)
+ mockTokenService.fetchTokenBalancesFromOnChain = vi
+ .fn()
+ .mockResolvedValue(mockTokensWithBalances)
+
+ await tokenWorker.updateTokenBalances()
+
+ expect(mockWalletStore.get).toHaveBeenCalledWith("selected")
+ expect(mockAccountService.get).not.toHaveBeenCalledWith(
+ expect.any(Function),
+ )
+ })
+
describe("updateTokenPrices", () => {
it("should fetch token prices for the default network and update the token service", async () => {
// Arrange
@@ -244,37 +332,4 @@ describe("TokenWorker", () => {
)
})
})
-
- describe("onOpened ", () => {
- it("should start the token worker schedule when opened", async () => {
- // Act
- tokenWorker.onOpened(true)
-
- // Assert
- expect(mockScheduleService.every).toHaveBeenCalledTimes(3)
- expect(mockScheduleService.every).toHaveBeenNthCalledWith(1, 86400, {
- id: TokenWorkerSchedule.updateTokens,
- })
- expect(mockScheduleService.every).toHaveBeenNthCalledWith(2, 20, {
- id: TokenWorkerSchedule.updateTokenBalances,
- })
- expect(mockScheduleService.every).toHaveBeenNthCalledWith(3, 60, {
- id: TokenWorkerSchedule.updateTokenPrices,
- })
- })
-
- it("should delete the token worker schedule when closed", async () => {
- // Act
- tokenWorker.onOpened(false)
-
- // Assert
- expect(mockScheduleService.delete).toHaveBeenCalledTimes(2)
- expect(mockScheduleService.delete).toHaveBeenNthCalledWith(1, {
- id: TokenWorkerSchedule.updateTokenBalances,
- })
- expect(mockScheduleService.delete).toHaveBeenNthCalledWith(2, {
- id: TokenWorkerSchedule.updateTokenPrices,
- })
- })
- })
})
diff --git a/packages/extension/src/shared/token/__new/worker/implementation.ts b/packages/extension/src/shared/token/__new/worker/implementation.ts
index 0634e548a..f56c8864a 100644
--- a/packages/extension/src/shared/token/__new/worker/implementation.ts
+++ b/packages/extension/src/shared/token/__new/worker/implementation.ts
@@ -1,21 +1,13 @@
-import {
- IScheduleService,
- ImplementedScheduledTask,
-} from "../../../schedule/interface"
+import { IScheduleService } from "../../../schedule/interface"
import { ITokenService } from "../service/interface"
import { ITokenWorker } from "./interface"
import { INetworkService } from "../../../network/service/interface"
import { WalletStorageProps } from "../../../wallet/walletStore"
-import { BaseWalletAccount } from "../../../wallet.model"
+import { BaseWalletAccount, WalletAccount } from "../../../wallet.model"
import { defaultNetwork } from "../../../network"
import { IAccountService } from "../../../account/service/interface"
import { Token } from "../types/token.model"
-import { WalletSessionService } from "../../../../background/wallet/session/session.service"
-import { Locked } from "../../../../background/wallet/session/interface"
-import {
- IBackgroundUIService,
- Opened,
-} from "../../../../background/__new/services/ui/interface"
+import { IBackgroundUIService } from "../../../../background/__new/services/ui/interface"
import { Transaction } from "../../../transactions"
import { transactionSucceeded } from "../../../utils/transactionSucceeded"
import { IRepository } from "../../../storage/__new/interface"
@@ -24,20 +16,17 @@ import { Recovered } from "../../../../background/wallet/recovery/interface"
import { KeyValueStorage } from "../../../storage"
import { RefreshInterval } from "../../../config"
import {
+ every,
+ everyWhenOpen,
onInstallAndUpgrade,
onStartup,
} from "../../../../background/__new/services/worker/schedule/decorators"
import { pipe } from "../../../../background/__new/services/worker/schedule/pipe"
+import { IDebounceService } from "../../../debounce"
+import { IActivityService } from "../../../../background/__new/services/activity/interface"
+import { IActivityStorage } from "../../../activity/types"
-/**
- * Enum for scheduling token updates
- */
-export const enum TokenWorkerSchedule {
- updateTokens = "updateTokens", // Schedule for updating tokens
- updateTokenBalances = "updateTokenBalances", // Schedule for updating token balances
- updateTokenPrices = "updateTokenPrices", // Schedule for updating token prices
-}
-
+const NETWORKS_WITH_BACKEND_SUPPORT = ["goerli-alpha", "mainnet-alpha"]
/**
* Refresh periods for updates
*/
@@ -50,19 +39,20 @@ export const enum TokenWorkerSchedule {
* This class is responsible for managing token updates, including token balances and prices.
*/
export class TokenWorker implements ITokenWorker {
- private isUpdating = false // Flag to check if update is in progress
- private lastUpdatedTimestamp = 0 // Timestamp of the last update
-
/**
* Constructor for TokenWorker class
* @param {IWalletStore} walletStore - The wallet store
* @param {IRepository} transactionsRepo - The transactions store
+ * @param {IRepository} tokenRepo - The token repository
* @param {ITokenService} tokenService - The token service
* @param {IAccountService} accountService - The account service
* @param {INetworkService} networkService - The network service
- * @param {WalletSessionService} sessionService - The session service
+ * @param {WalletRecoverySharedService} recoverySharedService - The wallet recovery service
* @param {IBackgroundUIService} backgroundUIService - The background UI service
- * @param {IScheduleService} scheduleService - The schedule service
+ * @param {IScheduleService} scheduleService - The schedule service
+ * @param {IDebounceService} debounceService - The debounce service
+ * @param {IActivityService} activityService - The activity service
+ * @param {KeyValueStorage} activityStore - The activity store
*/
constructor(
private readonly walletStore: KeyValueStorage,
@@ -71,10 +61,11 @@ export class TokenWorker implements ITokenWorker {
private readonly tokenService: ITokenService, // Token service
private readonly accountService: IAccountService, // Account service
private readonly networkService: INetworkService, // Network service
- private readonly sessionService: WalletSessionService, // Session service
private readonly recoverySharedService: WalletRecoverySharedService, // Recovery shared service
private readonly backgroundUIService: IBackgroundUIService, // Background UI service
- private readonly scheduleService: IScheduleService, // Schedule service
+ private readonly scheduleService: IScheduleService, // Schedule service
+ private readonly debounceService: IDebounceService, // Debounce service
+ private readonly activityService: IActivityService, // Activity service
) {
// Run once on startup
void this.initialize()
@@ -85,36 +76,14 @@ export class TokenWorker implements ITokenWorker {
* Registers the schedules and event listeners
*/
initialize(): void {
- // Register schedules
- const updateTokensTask: ImplementedScheduledTask = {
- id: TokenWorkerSchedule.updateTokens,
- callback: this.updateTokens.bind(this),
- }
- void this.scheduleService.registerImplementation(updateTokensTask)
- void this.scheduleService.registerImplementation({
- id: TokenWorkerSchedule.updateTokenBalances,
- callback: this.updateTokenBalances.bind(this),
- })
- void this.scheduleService.registerImplementation({
- id: TokenWorkerSchedule.updateTokenPrices,
- callback: this.updateTokenPrices.bind(this),
- })
- // Schedule token updates every day
- void this.scheduleService.every(RefreshInterval.VERY_SLOW, {
- id: TokenWorkerSchedule.updateTokens,
- })
-
- // Listen for UI opened event
- this.backgroundUIService.emitter.on(Opened, this.onOpened.bind(this))
-
// Listen for account changes
- this.walletStore.subscribe("selected", (account) => {
- void this.updateTokenBalances(account) // Update token balances on account changenge
+ this.walletStore.subscribe("selected", async (account) => {
+ if (account) {
+ await this.checkAndUpdateTokens(account) // Check if tokens have priceId and update if needed
+ await this.updateTokenBalancesForAccount(account) // Update token balances on account changenge
+ }
})
- // Listen for session locked event
- this.sessionService.emitter.on(Locked, this.onLocked.bind(this))
-
// Listen for recovery event
this.recoverySharedService.emitter.on(
Recovered,
@@ -131,24 +100,27 @@ export class TokenWorker implements ITokenWorker {
changeSet?.oldValue,
)
if (hasSuccessTx) {
- void this.updateTokenBalances() // Update token balances on transaction success
+ const isOnBackendSupportedNetwork = changeSet.newValue.every((tx) =>
+ NETWORKS_WITH_BACKEND_SUPPORT.includes(tx.account.networkId),
+ )
+ void this.updateTokenBalancesCallback({
+ hasBackendSupport: isOnBackendSupportedNetwork,
+ }) // Update token balances on transaction success
}
})
- this.tokenRepo.subscribe(() => {
- void this.updateTokenBalances() // Update token balances on token change
- void this.updateTokenPrices() // Update token prices on token change
+ this.tokenRepo.subscribe((changeSet) => {
+ const isOnBackendSupportedNetwork = changeSet?.newValue?.every((token) =>
+ NETWORKS_WITH_BACKEND_SUPPORT.includes(token.networkId),
+ )
+ void this.updateTokenBalancesCallback({
+ hasBackendSupport: Boolean(isOnBackendSupportedNetwork),
+ }) // Update token balances on token change
+ void this.updateTokenPricesCallback() // Update token prices on token change
})
}
- /**
- * Update tokens
- * Fetches tokens for all networks and updates the token service
- */
- updateTokens = pipe(
- onStartup(this.scheduleService), // This will run the function on startup
- onInstallAndUpgrade(this.scheduleService), // This will run the function on update
- )(async (): Promise => {
+ updateTokensCallback = async (): Promise => {
const networks = await this.networkService.get() // Get all networks
// // Fetch tokens for all networks in parallel
@@ -163,61 +135,146 @@ export class TokenWorker implements ITokenWorker {
.flat()
await this.tokenService.updateTokens(tokens) // Update tokens in the token service
- })
+ }
- /**
- * Update token balances
- * Fetches token balances for the provided account or all accounts on the current network
- * @param baseWalletAccount - Base Wallet Account
- */
- async updateTokenBalances(
- baseWalletAccount?: BaseWalletAccount | null,
+ async updateTokenBalancesForAccount(
+ baseWalletAccount: BaseWalletAccount,
): Promise {
- if (this.isUpdating) {
- return
- }
+ const tokens = await this.tokenService.getTokens(
+ (t) => t.networkId === baseWalletAccount.networkId,
+ )
+ const tokensWithBalance =
+ await this.tokenService.fetchTokenBalancesFromOnChain(
+ [baseWalletAccount],
+ tokens,
+ )
- this.isUpdating = true // Set updating flag
- this.lastUpdatedTimestamp = Date.now() // Update timestamp
+ return await this.tokenService.updateTokenBalances(tokensWithBalance) // Update token balances in the token service
+ }
- if (baseWalletAccount) {
- const tokens = await this.tokenService.getTokens(
- (t) => t.networkId === baseWalletAccount.networkId,
- )
- const tokensWithBalance =
- await this.tokenService.fetchTokenBalancesFromOnChain(
- [baseWalletAccount],
- tokens,
- )
+ async checkAndUpdateTokens(
+ baseWalletAccount: BaseWalletAccount,
+ ): Promise {
+ const tokens = await this.tokenService.getTokens(
+ (t) =>
+ t.networkId === baseWalletAccount.networkId &&
+ t.pricingId !== undefined,
+ )
- this.isUpdating = false // Reset updating flag
- return await this.tokenService.updateTokenBalances(tokensWithBalance) // Update token balances in the token service
+ if (!tokens.length) {
+ await this.updateTokensCallback()
}
+ }
- // If no account is provided, fetch for all accounts on the current network
- const selectedAccount = await this.walletStore.get("selected")
- if (selectedAccount) {
- const accounts = await this.accountService.get(
- (a) => a.networkId === selectedAccount.networkId,
- )
- const tokens = await this.tokenService.getTokens(
- (t) => t.networkId === selectedAccount.networkId,
- )
+ /**
+ * Update tokens
+ * Fetches tokens for all networks and updates the token service
+ */
+ updateTokens = pipe(
+ onStartup(this.scheduleService), // This will run the function on startup
+ onInstallAndUpgrade(this.scheduleService), // This will run the function on update
+ every(this.scheduleService, RefreshInterval.VERY_SLOW, "updateTokens"), // This will run the function every 24 hours
+ )(async (): Promise => {
+ await this.updateTokensCallback()
+ })
- const tokensWithBalances =
- await this.tokenService.fetchTokenBalancesFromOnChain(accounts, tokens)
+ private async updateBalance({
+ tokens,
+ accounts,
+ }: {
+ tokens: Token[]
+ accounts: WalletAccount[]
+ }) {
+ const tokensWithBalances =
+ await this.tokenService.fetchTokenBalancesFromOnChain(accounts, tokens)
+
+ await this.tokenService.updateTokenBalances(tokensWithBalances) // Update token balances in the token service
+ }
- await this.tokenService.updateTokenBalances(tokensWithBalances) // Update token balances in the token service
+ updateTokenBalancesCallback = async ({
+ hasBackendSupport,
+ }: {
+ hasBackendSupport: boolean
+ }): Promise => {
+ const selectedAccount = await this.walletStore.get("selected")
+ if (!selectedAccount) {
+ return
}
+ const accounts = await this.accountService.get(
+ (a) => a.networkId === selectedAccount.networkId,
+ )
+ const tokens = await this.tokenService.getTokens(
+ (t) => t.networkId === selectedAccount.networkId,
+ )
+ if (!hasBackendSupport) {
+ await this.updateBalance({ tokens, accounts })
+ } else {
+ const shouldUpdateBalance =
+ await this.activityService.shouldUpdateBalance(selectedAccount)
+ if (
+ shouldUpdateBalance.shouldUpdate &&
+ shouldUpdateBalance.id &&
+ shouldUpdateBalance.lastModified
+ ) {
+ await this.updateBalance({ tokens, accounts })
- this.isUpdating = false // Reset updating flag
+ await this.activityService.addActivityToStore({
+ address: selectedAccount.address,
+ id: shouldUpdateBalance.id,
+ lastModified: shouldUpdateBalance.lastModified,
+ })
+ }
+ }
}
/**
- * Update token prices
- * Fetches token prices for the default network and updates the token service
+ * Update token balances
+ * Fetches token balances for the provided account or all accounts on the current network
+ * @param baseWalletAccount - Base Wallet Account
*/
- async updateTokenPrices(): Promise {
+ updateTokenBalances = pipe(
+ everyWhenOpen(
+ this.backgroundUIService,
+ this.scheduleService,
+ this.debounceService,
+ RefreshInterval.FAST,
+ "updateTokenBalances",
+ ), // This will run the function every 20 seconds when the UI is open
+ )(async (): Promise => {
+ const selectedAccount = await this.walletStore.get("selected")
+ if (
+ selectedAccount?.networkId &&
+ NETWORKS_WITH_BACKEND_SUPPORT.includes(selectedAccount.networkId)
+ ) {
+ await this.updateTokenBalancesCallback({ hasBackendSupport: true })
+ }
+ })
+
+ /**
+ * Update token balances for custom networks
+ * Fetches token balances for the provided account or all accounts on the current network
+ * @param baseWalletAccount - Base Wallet Account
+ */
+ updateCustomNetworksTokenBalances = pipe(
+ everyWhenOpen(
+ this.backgroundUIService,
+ this.scheduleService,
+ this.debounceService,
+ RefreshInterval.MEDIUM,
+ "updateCustomNetworksTokenBalances",
+ ), // This will run the function every 60 seconds when the UI is open
+ )(async (): Promise => {
+ const selectedAccount = await this.walletStore.get("selected")
+
+ if (
+ !selectedAccount?.networkId ||
+ !NETWORKS_WITH_BACKEND_SUPPORT.includes(selectedAccount.networkId)
+ ) {
+ await this.updateTokenBalancesCallback({ hasBackendSupport: false })
+ }
+ })
+
+ updateTokenPricesCallback = async (): Promise => {
// Token prices are only available for the default network
const defaultNetworkId = defaultNetwork.id
const tokens = await this.tokenService.getTokens(
@@ -231,18 +288,20 @@ export class TokenWorker implements ITokenWorker {
}
/**
- * Handle locked state
- * Updates token balances when the session is unlocked
- * @param locked - Locked state
+ * Update token prices
+ * Fetches token prices for the default network and updates the token service
*/
- async onLocked(locked: boolean) {
- if (!locked) {
- await Promise.all([
- this.updateTokenBalances(), // Update token balances when session is unlocked
- this.updateTokenPrices(), // Update token prices when session is unlocked
- ])
- }
- }
+ updateTokenPrices = pipe(
+ everyWhenOpen(
+ this.backgroundUIService,
+ this.scheduleService,
+ this.debounceService,
+ RefreshInterval.MEDIUM,
+ "updateTokenPrices",
+ ), // This will run the function every 2 minutes when the UI is open
+ )(async (): Promise => {
+ await this.updateTokenPricesCallback()
+ })
/**
* Handle Recovery
@@ -251,8 +310,8 @@ export class TokenWorker implements ITokenWorker {
*/
async onRecovered(recoveredAccounts: BaseWalletAccount[]) {
if (recoveredAccounts.length > 0) {
- await this.updateTokens() // Update tokens when wallet is recovered
- await this.updateTokenPrices() // Update token prices when wallet is recovered
+ await this.updateTokensCallback() // Update tokens when wallet is recovered
+ await this.updateTokenPricesCallback() // Update token prices when wallet is recovered
// On Recovery, we need to fetch the token balances for the recovered accounts
const tokensWithBalances =
@@ -261,40 +320,4 @@ export class TokenWorker implements ITokenWorker {
await this.tokenService.updateTokenBalances(tokensWithBalances)
}
}
-
- /**
- * Handle opened state
- * Updates token balances and schedules updates when the UI is opened
- * @param opened - Opened state
- */
- onOpened(opened: boolean) {
- if (opened) {
- const currentTimestamp = Date.now()
- const differenceInMilliseconds =
- currentTimestamp - this.lastUpdatedTimestamp
- const differenceInMinutes = differenceInMilliseconds / (1000 * 60) // Convert milliseconds to minutes
-
- // If we haven't done an update in the past 1 minute, do one on the spot when opening the extension
- if (differenceInMinutes > RefreshInterval.MEDIUM) {
- void this.updateTokenBalances() // Update token balances
- }
-
- // Schedule token balance and price updates
- void this.scheduleService.every(RefreshInterval.FAST, {
- id: TokenWorkerSchedule.updateTokenBalances,
- })
- void this.scheduleService.every(RefreshInterval.MEDIUM, {
- id: TokenWorkerSchedule.updateTokenPrices,
- })
- } else {
- // Delete scheduled updates when the UI is closed
- void this.scheduleService.delete({
- id: TokenWorkerSchedule.updateTokenBalances,
- })
-
- void this.scheduleService.delete({
- id: TokenWorkerSchedule.updateTokenPrices,
- })
- }
- }
}
diff --git a/packages/extension/src/shared/token/__new/worker/index.ts b/packages/extension/src/shared/token/__new/worker/index.ts
index c188468d0..6aa65cc7b 100644
--- a/packages/extension/src/shared/token/__new/worker/index.ts
+++ b/packages/extension/src/shared/token/__new/worker/index.ts
@@ -1,13 +1,12 @@
+import { activityService } from "../../../../background/__new/services/activity"
import { backgroundUIService } from "../../../../background/__new/services/ui"
import { transactionsRepo } from "../../../../background/transactions/store"
-import {
- recoverySharedService,
- sessionService,
-} from "../../../../background/walletSingleton"
+import { recoverySharedService } from "../../../../background/walletSingleton"
import { accountService } from "../../../account/service"
+import { debounceService } from "../../../debounce"
import { networkService } from "../../../network/service"
import { chromeScheduleService } from "../../../schedule"
-import { old_walletStore, walletStore } from "../../../wallet/walletStore"
+import { old_walletStore } from "../../../wallet/walletStore"
import { tokenRepo } from "../repository/token"
import { tokenService } from "../service"
@@ -20,8 +19,9 @@ export const tokenWorker = new TokenWorker(
tokenService,
accountService,
networkService,
- sessionService,
recoverySharedService,
backgroundUIService,
chromeScheduleService,
+ debounceService,
+ activityService,
)
diff --git a/packages/extension/src/shared/transactionReview/__fixtures__/simulation-error.json b/packages/extension/src/shared/transactionReview/__fixtures__/simulation-error.json
new file mode 100644
index 000000000..f40aeb8b9
--- /dev/null
+++ b/packages/extension/src/shared/transactionReview/__fixtures__/simulation-error.json
@@ -0,0 +1,147 @@
+{
+ "transactions": [
+ {
+ "reviewOfTransaction": {
+ "assessment": "neutral",
+ "warnings": [],
+ "reviews": [
+ {
+ "assessment": "neutral",
+ "warnings": [],
+ "action": {
+ "name": "ERC20_approve",
+ "properties": [
+ {
+ "type": "amount",
+ "label": "ERC20_approve_amount",
+ "token": {
+ "address": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
+ "name": "Ether",
+ "symbol": "ETH",
+ "decimals": 18,
+ "unknown": false,
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/eth.png",
+ "type": "ERC20"
+ },
+ "amount": "2020005050000000",
+ "usd": "4.75",
+ "editable": true
+ },
+ {
+ "type": "address",
+ "label": "ERC20_approve_to",
+ "address": "0x03374d4a1b4f8dc87bd680ddbd4f1181b3ec3cf5a8ef803bc4351603b063314f",
+ "verified": false
+ }
+ ],
+ "defaultProperties": [
+ {
+ "type": "token_address",
+ "label": "default_contract",
+ "token": {
+ "address": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
+ "name": "Ether",
+ "symbol": "ETH",
+ "decimals": 18,
+ "unknown": false,
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/eth.png",
+ "type": "ERC20"
+ }
+ },
+ {
+ "type": "calldata",
+ "label": "default_call",
+ "entrypoint": "approve",
+ "calldata": [
+ "1454648566693524972585253146654712267053014576236258096888965210222799040847",
+ "2020005050000000",
+ "0"
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "assessment": "neutral",
+ "warnings": [],
+ "action": {
+ "name": "buy",
+ "properties": [],
+ "defaultProperties": [
+ {
+ "type": "address",
+ "label": "default_contract",
+ "address": "0x03374d4a1b4f8dc87bd680ddbd4f1181b3ec3cf5a8ef803bc4351603b063314f",
+ "verified": false
+ },
+ {
+ "type": "calldata",
+ "label": "default_call",
+ "entrypoint": "buy",
+ "calldata": ["1", "10"]
+ }
+ ]
+ }
+ },
+ {
+ "assessment": "neutral",
+ "warnings": [],
+ "action": {
+ "name": "ERC20_approve",
+ "properties": [
+ {
+ "type": "amount",
+ "label": "ERC20_approve_amount",
+ "token": {
+ "address": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
+ "name": "Ether",
+ "symbol": "ETH",
+ "decimals": 18,
+ "unknown": false,
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/eth.png",
+ "type": "ERC20"
+ },
+ "amount": "0",
+ "usd": "0.00",
+ "editable": true
+ },
+ {
+ "type": "address",
+ "label": "ERC20_approve_to",
+ "address": "0x03374d4a1b4f8dc87bd680ddbd4f1181b3ec3cf5a8ef803bc4351603b063314f",
+ "verified": false
+ }
+ ],
+ "defaultProperties": [
+ {
+ "type": "token_address",
+ "label": "default_contract",
+ "token": {
+ "address": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
+ "name": "Ether",
+ "symbol": "ETH",
+ "decimals": 18,
+ "unknown": false,
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/eth.png",
+ "type": "ERC20"
+ }
+ },
+ {
+ "type": "calldata",
+ "label": "default_call",
+ "entrypoint": "approve",
+ "calldata": [
+ "1454648566693524972585253146654712267053014576236258096888965210222799040847",
+ "0",
+ "0"
+ ]
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "simulationError": { "label": "transaction_unknown_error", "code": -1 }
+ }
+ ]
+}
diff --git a/packages/extension/src/shared/transactionReview/__fixtures__/simulation.json b/packages/extension/src/shared/transactionReview/__fixtures__/simulation.json
new file mode 100644
index 000000000..117deb933
--- /dev/null
+++ b/packages/extension/src/shared/transactionReview/__fixtures__/simulation.json
@@ -0,0 +1,139 @@
+{
+ "transactions": [
+ {
+ "reviewOfTransaction": {
+ "assessment": "warn",
+ "warnings": [
+ { "reason": "undeployed_account", "details": {}, "severity": "info" }
+ ],
+ "reviews": [
+ {
+ "assessment": "warn",
+ "warnings": [
+ {
+ "reason": "undeployed_account",
+ "details": {},
+ "severity": "info"
+ }
+ ],
+ "action": {
+ "name": "ERC20_transfer",
+ "properties": [
+ {
+ "type": "amount",
+ "label": "ERC20_transfer_amount",
+ "token": {
+ "address": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
+ "name": "Ether",
+ "symbol": "ETH",
+ "decimals": 18,
+ "unknown": false,
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/eth.png",
+ "type": "ERC20"
+ },
+ "amount": "10177678004054592",
+ "usd": "23.96",
+ "editable": false
+ },
+ {
+ "type": "address",
+ "label": "ERC20_transfer_recipient",
+ "address": "0x05f1f0a38429dcab9ffd8a786c0d827e84c1cbd8f60243e6d25d066a13af4a25",
+ "verified": false
+ }
+ ],
+ "defaultProperties": [
+ {
+ "type": "token_address",
+ "label": "default_contract",
+ "token": {
+ "address": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
+ "name": "Ether",
+ "symbol": "ETH",
+ "decimals": 18,
+ "unknown": false,
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/eth.png",
+ "type": "ERC20"
+ }
+ },
+ {
+ "type": "calldata",
+ "label": "default_call",
+ "entrypoint": "transfer",
+ "calldata": [
+ "2689035213040902571798644155430358178496847363710771602620498934381075712549",
+ "10177678004054592",
+ "0"
+ ]
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "simulation": {
+ "approvals": [],
+ "transfers": [
+ {
+ "tokenAddress": "0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
+ "from": "0x5f1f0a38429dcab9ffd8a786c0d827e84c1cbd8f60243e6d25d066a13af4a25",
+ "to": "0x5f1f0a38429dcab9ffd8a786c0d827e84c1cbd8f60243e6d25d066a13af4a25",
+ "value": "10177678004054592",
+ "usdValue": "23.96",
+ "details": {
+ "address": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
+ "name": "Ether",
+ "symbol": "ETH",
+ "decimals": 18,
+ "unknown": false,
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/eth.png",
+ "type": "ERC20"
+ }
+ }
+ ],
+ "summary": [
+ {
+ "type": "transfer",
+ "label": "simulation_summary_send",
+ "value": "10177678004054592",
+ "usdValue": "23.96",
+ "token": {
+ "address": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
+ "name": "Ether",
+ "symbol": "ETH",
+ "decimals": 18,
+ "unknown": false,
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/eth.png",
+ "type": "ERC20"
+ },
+ "sent": true
+ },
+ {
+ "type": "transfer",
+ "label": "simulation_summary_receive",
+ "value": "10177678004054592",
+ "usdValue": "23.96",
+ "token": {
+ "address": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
+ "name": "Ether",
+ "symbol": "ETH",
+ "decimals": 18,
+ "unknown": false,
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/eth.png",
+ "type": "ERC20"
+ },
+ "sent": false
+ }
+ ],
+ "calculatedNonce": "0x22",
+ "feeEstimation": {
+ "overallFee": "2520000100800",
+ "gasPrice": "1000000040",
+ "gasUsage": "2520",
+ "unit": "wei",
+ "maxFee": "5040003826577"
+ }
+ }
+ }
+ ]
+}
diff --git a/packages/extension/src/shared/transactionReview/interface.ts b/packages/extension/src/shared/transactionReview/interface.ts
new file mode 100644
index 000000000..6bb50dc32
--- /dev/null
+++ b/packages/extension/src/shared/transactionReview/interface.ts
@@ -0,0 +1,38 @@
+import { z } from "zod"
+
+import { EnrichedSimulateAndReview } from "./schema"
+import { callSchema, hexSchema } from "@argent/shared"
+
+export const transactionReviewTransactionsSchema = z.object({
+ type: z
+ .enum(["DECLARE", "DEPLOY", "DEPLOY_ACCOUNT", "INVOKE"])
+ .default("INVOKE"),
+ calls: z.array(callSchema).or(callSchema).optional(),
+ calldata: z.array(z.string()).optional(),
+ classHash: hexSchema.optional(),
+ salt: hexSchema.optional(),
+ signature: z.array(z.string()).optional(),
+})
+
+export type TransactionReviewTransactions = z.infer<
+ typeof transactionReviewTransactionsSchema
+>
+
+export interface ITransactionReviewService {
+ simulateAndReview({
+ transactions,
+ }: {
+ transactions: TransactionReviewTransactions[]
+ }): Promise
+ getLabels(): Promise
+}
+
+export type ITransactionReviewLabel = {
+ key: string
+ value: string
+}
+
+export type ITransactionReviewLabelsStore = {
+ labels?: ITransactionReviewLabel[]
+ updatedAt?: number
+}
diff --git a/packages/extension/src/shared/transactionReview/schema.test.ts b/packages/extension/src/shared/transactionReview/schema.test.ts
new file mode 100644
index 000000000..e86946994
--- /dev/null
+++ b/packages/extension/src/shared/transactionReview/schema.test.ts
@@ -0,0 +1,136 @@
+import { describe, expect, test } from "vitest"
+
+import simulationResponse from "./__fixtures__/simulation.json"
+import simulationErrorResponse from "./__fixtures__/simulation-error.json"
+import {
+ TransactionReviewTransaction,
+ isNotTransactionSimulationError,
+ isTransactionSimulationError,
+ simulateAndReviewSchema,
+ getMessageFromSimulationError,
+} from "./schema"
+
+const missingSimulationResponse = {
+ transactions: [
+ {
+ reviewOfTransaction:
+ simulationResponse.transactions[0].reviewOfTransaction,
+ },
+ ],
+}
+
+const bothSimulationAndErrorResponse = {
+ transactions: [
+ {
+ reviewOfTransaction:
+ simulationResponse.transactions[0].reviewOfTransaction,
+ simulation: simulationResponse.transactions[0].simulation,
+ simulationError: simulationErrorResponse.transactions[0].simulationError,
+ },
+ ],
+}
+
+describe("transactionReview/schema", () => {
+ describe("simulateAndReviewSchema", () => {
+ describe("when valid", () => {
+ test("success should be true", () => {
+ expect(
+ simulateAndReviewSchema.safeParse(simulationResponse).success,
+ ).toBeTruthy()
+ expect(
+ simulateAndReviewSchema.safeParse(simulationErrorResponse).success,
+ ).toBeTruthy()
+ })
+ })
+ describe("when invalid", () => {
+ describe("and simulation is missing", () => {
+ test("success should be false", () => {
+ expect(
+ simulateAndReviewSchema.safeParse(missingSimulationResponse)
+ .success,
+ ).toBeFalsy()
+ })
+ })
+ describe("and there is both simulation and simulationError", () => {
+ test("success should be false", () => {
+ expect(
+ simulateAndReviewSchema.safeParse(bothSimulationAndErrorResponse)
+ .success,
+ ).toBeFalsy()
+ })
+ })
+ })
+ })
+ describe("isNotTransactionSimulationError", () => {
+ describe("when valid", () => {
+ test("returns true", () => {
+ expect(
+ isNotTransactionSimulationError(
+ simulationResponse.transactions[0] as TransactionReviewTransaction,
+ ),
+ ).toBeTruthy()
+ })
+ })
+ describe("when invalid", () => {
+ test("returns false", () => {
+ expect(
+ isNotTransactionSimulationError(
+ simulationErrorResponse
+ .transactions[0] as TransactionReviewTransaction,
+ ),
+ ).toBeFalsy()
+ })
+ })
+ })
+ describe("isTransactionSimulationError", () => {
+ describe("when valid", () => {
+ test("returns true", () => {
+ expect(
+ isTransactionSimulationError(
+ simulationErrorResponse
+ .transactions[0] as TransactionReviewTransaction,
+ ),
+ ).toBeTruthy()
+ })
+ })
+ describe("when invalid", () => {
+ test("returns false", () => {
+ expect(
+ isTransactionSimulationError(
+ simulationResponse.transactions[0] as TransactionReviewTransaction,
+ ),
+ ).toBeFalsy()
+ })
+ })
+ })
+ describe("getMessageFromSimulationError", () => {
+ test("given simulation error with error key, should use it in the returned message", () => {
+ expect(
+ getMessageFromSimulationError({
+ code: 10,
+ message: "foo",
+ error: "bar",
+ }),
+ ).toBe("bar")
+ })
+ test("given simulation error without error key, but with code and message, should use them in the returned message", () => {
+ expect(
+ getMessageFromSimulationError({
+ code: 10,
+ message: "foo",
+ }),
+ ).toBe("10: foo")
+ })
+ test("given simulation error without error key and code, should return fallback message", () => {
+ expect(getMessageFromSimulationError({ message: "foo" })).toBe(
+ "Unknown error",
+ )
+ })
+ test("given simulation error without error key and message, should return fallback message", () => {
+ expect(getMessageFromSimulationError({ code: 10 })).toBe("Unknown error")
+ })
+ test("given simulation error without any data, should return fallback message", () => {
+ expect(getMessageFromSimulationError({})).toBe("Unknown error")
+ })
+ })
+})
diff --git a/packages/extension/src/shared/transactionReview/schema.ts b/packages/extension/src/shared/transactionReview/schema.ts
new file mode 100644
index 000000000..6f1f74341
--- /dev/null
+++ b/packages/extension/src/shared/transactionReview/schema.ts
@@ -0,0 +1,309 @@
+import { addressSchemaArgentBackend } from "@argent/shared"
+import { z } from "zod"
+import { estimatedFeesSchema } from "../transactionSimulation/fees/fees.model"
+
+const linkSchema = z.object({
+ name: z.string(),
+ url: z.string(),
+ position: z.number(),
+})
+
+const tokenSchema = z.object({
+ address: z.string(),
+ name: z.string(),
+ symbol: z.string().optional(),
+ decimals: z.number(),
+ unknown: z.boolean(),
+ iconUrl: z.string().optional(),
+ type: z.string(),
+})
+
+export const propertySchema = z.union([
+ z.object({
+ type: z.literal("amount"),
+ label: z.string(),
+ token: tokenSchema,
+ amount: z.string(),
+ usd: z.string(),
+ editable: z.boolean(),
+ }),
+ z.object({
+ type: z.literal("address"),
+ label: z.string(),
+ address: z.string(),
+ addressName: z.string().optional(),
+ // tbd whether it's isVerified or verified
+ verified: z.boolean(),
+ }),
+ z.object({
+ type: z.literal("timestamp"),
+ label: z.string(),
+ value: z.string(),
+ }),
+ z.object({
+ type: z.literal("token_address"),
+ label: z.string(),
+ token: tokenSchema,
+ }),
+ z.object({
+ type: z.literal("calldata"),
+ label: z.string(),
+ entrypoint: z.string(),
+ calldata: z.array(z.string()),
+ }),
+ z.object({
+ type: z.literal("text"),
+ label: z.string(),
+ text: z.string(),
+ }),
+])
+
+export const actionSchema = z.object({
+ name: z.string(),
+ properties: z.array(propertySchema),
+ defaultProperties: z.array(propertySchema).optional(),
+})
+
+export const reasonsSchema = z.union([
+ z.literal("account_upgrade_to_unknown_implementation"),
+ z.literal("account_state_change"),
+ z.literal("contract_is_black_listed"),
+ z.literal("amount_mismatch_too_low"),
+ z.literal("amount_mismatch_too_high"),
+ z.literal("dst_token_black_listed"),
+ z.literal("internal_service_issue"),
+ z.literal("recipient_is_not_current_account"),
+ z.literal("recipient_is_token_address"),
+ z.literal("recipient_is_black_listed"),
+ z.literal("spender_is_black_listed"),
+ z.literal("operator_is_black_listed"),
+ z.literal("src_token_black_listed"),
+ z.literal("unknown_token"),
+ z.literal("undeployed_account"),
+ z.literal("contract_is_not_verified"),
+ z.literal("token_a_black_listed"),
+ z.literal("token_b_black_listed"),
+ z.literal("approval_too_high"),
+ // these exist in the backend but should never occur
+ // z.literal("multi_calls_on_account"),
+ // z.literal("unknown_selector"),
+])
+
+export const assessmentSchema = z.union([
+ z.literal("verified"),
+ z.literal("neutral"),
+ z.literal("partial"),
+ z.literal("warn"),
+])
+
+export const severitySchema = z.union([
+ z.literal("critical"),
+ z.literal("high"),
+ z.literal("caution"),
+ z.literal("info"),
+])
+
+const warningSchema = z.object({
+ reason: reasonsSchema,
+ details: z.record(z.string().or(z.number())).optional(),
+ severity: severitySchema,
+})
+
+const reviewSchema = z.object({
+ assessment: assessmentSchema,
+ warnings: z.array(warningSchema).optional(),
+ assessmentReasons: z.array(z.string()).optional(),
+ assessmentDetails: z
+ .object({
+ contract_address: z.string(),
+ })
+ .optional(),
+ action: actionSchema,
+})
+
+const targetedDappSchema = z.object({
+ name: z.string(),
+ description: z.string(),
+ logoUrl: z.string(),
+ iconUrl: z.string(),
+ argentVerified: z.boolean(),
+ links: z.array(linkSchema),
+})
+
+const reviewOfTransactionSchema = z
+ .object({
+ assessment: z.union([
+ z.literal("verified"),
+ z.literal("neutral"),
+ z.literal("partial"),
+ z.literal("warn"),
+ ]),
+ warnings: z.array(warningSchema).optional(),
+ assessmentDetails: z
+ .object({
+ contract_address: z.string(),
+ })
+ .optional(),
+ targetedDapp: targetedDappSchema.optional(),
+ reviews: z.array(reviewSchema),
+ })
+ .optional()
+
+const imageUrlsSchema = z.object({
+ banner: z.string().nullable().optional(),
+ preview: z.string().nullable().optional(),
+ full: z.string().nullable().optional(),
+ original: z.string().nullable().optional(),
+})
+
+const linksSchema = z.object({
+ twitter: z.string().optional(),
+ external: z.string().optional(),
+ discord: z.string().optional(),
+})
+
+const tokenDetailsSchema = z.object({
+ address: addressSchemaArgentBackend,
+ decimals: z.number().optional(),
+ symbol: z.string().optional(),
+ name: z.string(),
+ description: z.string().optional(),
+ type: z.string().optional(),
+ usdValue: z.string().optional(),
+ iconUrl: z.string().optional(),
+ unknown: z.boolean().optional(),
+ imageUrls: imageUrlsSchema.optional(),
+ links: linksSchema.optional(),
+})
+
+const approvalSchema = z.object({
+ tokenAddress: addressSchemaArgentBackend,
+ owner: addressSchemaArgentBackend,
+ spender: addressSchemaArgentBackend,
+ value: z.string().optional(),
+ approvalForAll: z.boolean(),
+ details: tokenDetailsSchema.optional(),
+})
+
+const transferSchema = z.object({
+ tokenAddress: addressSchemaArgentBackend,
+ from: addressSchemaArgentBackend,
+ to: addressSchemaArgentBackend,
+ tokenId: z.string().optional(),
+ value: z.string().optional(),
+ details: tokenDetailsSchema.optional(),
+})
+
+export const feeEstimationSchema = z.object({
+ overallFee: z.string(),
+ gasPrice: z.string(),
+ gasUsage: z.string(),
+ unit: z.string(),
+ maxFee: z.string(),
+})
+
+const summarySchema = z.object({
+ type: z.string(),
+ label: z.string(),
+ tokenId: z.string().optional(),
+ value: z.string().optional(),
+ usdValue: z.string().optional(),
+ token: tokenDetailsSchema,
+ sent: z.boolean().optional(),
+ tokenIdDetails: z
+ .object({
+ name: z.string().optional(),
+ description: z.string().optional(),
+ imageUrls: imageUrlsSchema.optional(),
+ })
+ .optional(),
+})
+
+const simulationSchema = z.object({
+ approvals: z.array(approvalSchema).optional(),
+ transfers: z.array(transferSchema).optional(),
+ calculatedNonce: z.string().optional(),
+ feeEstimation: feeEstimationSchema,
+ summary: z.array(summarySchema).optional(),
+})
+
+const simulationErrorSchema = z.object({
+ label: z.string().optional(),
+ code: z.number().optional(),
+ message: z.string().optional(),
+ error: z.string().optional(),
+})
+
+const transactionSimulationSchema = z.object({
+ reviewOfTransaction: reviewOfTransactionSchema,
+ simulation: simulationSchema,
+ simulationError: z.undefined(),
+})
+
+const transactionSimulationErrorSchema = z.object({
+ reviewOfTransaction: reviewOfTransactionSchema,
+ simulation: z.undefined(),
+ simulationError: simulationErrorSchema,
+})
+
+const transactionSchema = transactionSimulationSchema.or(
+ transactionSimulationErrorSchema,
+)
+
+export const simulateAndReviewSchema = z.object({
+ transactions: z.array(transactionSchema),
+})
+
+export const enrichedSimulateAndReviewSchema = z.object({
+ transactions: z.array(transactionSchema),
+ enrichedFeeEstimation: estimatedFeesSchema.optional(),
+ isBackendDown: z.boolean().default(false).optional(),
+})
+
+export type EnrichedSimulateAndReview = z.infer<
+ typeof enrichedSimulateAndReviewSchema
+>
+
+export type SimulateAndReview = z.infer
+export type AssessmentReason = z.infer
+export type AssessmentSeverity = z.infer
+export type Assessment = z.infer
+export type FeeEstimation = z.infer