@@ -422,8 +512,8 @@ export const TokenDapp: FC<{
try {
await addToken(ETHTokenAddress)
setAddTokenError("")
- } catch (error: any) {
- setAddTokenError(error.message)
+ } catch (error) {
+ setAddTokenError((error as any).message)
}
}}
>
@@ -437,8 +527,8 @@ export const TokenDapp: FC<{
try {
await addToken(DAITokenAddress)
setAddTokenError("")
- } catch (error: any) {
- setAddTokenError(error.message)
+ } catch (error) {
+ setAddTokenError((error as any).message)
}
}}
>
@@ -454,8 +544,8 @@ export const TokenDapp: FC<{
try {
await handleAddNetwork()
setAddNetworkError("")
- } catch (error: any) {
- setAddNetworkError(error.message)
+ } catch (error) {
+ setAddNetworkError((error as any).message)
}
}}
>
diff --git a/packages/dapp/src/pages/index.tsx b/packages/dapp/src/pages/index.tsx
index 5e600d412..0fb272ae1 100644
--- a/packages/dapp/src/pages/index.tsx
+++ b/packages/dapp/src/pages/index.tsx
@@ -9,7 +9,7 @@ import { TokenDapp } from "../components/TokenDapp"
import { truncateAddress } from "../services/address.service"
import {
addWalletChangeListener,
- chainId,
+ getChainId,
connectWallet,
removeWalletChangeListener,
silentConnectWallet,
@@ -27,7 +27,8 @@ const Home: NextPage = () => {
const handler = async () => {
const wallet = await silentConnectWallet()
setAddress(wallet?.selectedAddress)
- setChain(chainId(wallet?.provider as any))
+ const chainId = await getChainId(wallet?.provider as any)
+ setChain(chainId)
setConnected(!!wallet?.isConnected)
if (wallet?.account) {
setAccount(wallet.account as any)
@@ -66,7 +67,8 @@ const Home: NextPage = () => {
async () => {
const wallet = await connectWallet(enableWebWallet)
setAddress(wallet?.selectedAddress)
- setChain(chainId(wallet?.provider as any))
+ const chainId = await getChainId(wallet?.provider as any)
+ setChain(chainId)
setConnected(!!wallet?.isConnected)
if (wallet?.account) {
setAccount(wallet.account as any)
diff --git a/packages/dapp/src/services/token.service.ts b/packages/dapp/src/services/token.service.ts
index 80e25b6b0..a46ffcffb 100644
--- a/packages/dapp/src/services/token.service.ts
+++ b/packages/dapp/src/services/token.service.ts
@@ -1,6 +1,5 @@
-import { connect } from "@argent/get-starknet"
-import { utils } from "ethers"
-import { Abi, Contract, number, uint256 } from "starknet"
+import { bigDecimal } from "@argent/shared"
+import { Abi, Contract, num, uint256 } from "starknet"
import Erc20Abi from "../../abi/ERC20.json"
import { windowStarknet } from "./wallet.service"
@@ -11,15 +10,15 @@ export const ETHTokenAddress =
export const DAITokenAddress =
"0x00da114221cb83fa859dbdb4c44beeaa0bb37c7537ad5ae66fe5e0efd20e6eb3"
-function getUint256CalldataFromBN(bn: number.BigNumberish) {
- return { type: "struct" as const, ...uint256.bnToUint256(bn) }
+function getUint256CalldataFromBN(bn: num.BigNumberish) {
+ return uint256.bnToUint256(bn)
}
export function parseInputAmountToUint256(
input: string,
decimals: number = 18,
) {
- return getUint256CalldataFromBN(utils.parseUnits(input, decimals).toString())
+ return getUint256CalldataFromBN(bigDecimal.parseUnits(input, decimals))
}
export const mintToken = async (mintAmount: string): Promise
=> {
diff --git a/packages/dapp/src/services/wallet.service.ts b/packages/dapp/src/services/wallet.service.ts
index 2b2322bda..245a5ef93 100644
--- a/packages/dapp/src/services/wallet.service.ts
+++ b/packages/dapp/src/services/wallet.service.ts
@@ -1,21 +1,44 @@
import { StarknetWindowObject, connect } from "@argent/get-starknet"
import type { AddStarknetChainParameters } from "get-starknet-core"
-import { ProviderInterface, shortString } from "starknet"
+import {
+ AccountInterface,
+ DeclareContractPayload,
+ InvocationsDetails,
+ ProviderInterface,
+ UniversalDeployerContractPayload,
+ shortString,
+} from "starknet"
-export let windowStarknet: StarknetWindowObject | null = null
+export type StarknetWindowObjectV5 = StarknetWindowObject & {
+ account: AccountInterface
+}
+
+export let windowStarknet: StarknetWindowObjectV5 | null = null
+
+export const starknetVersion = "v5"
export const silentConnectWallet = async () => {
- const _windowStarknet = await connect({ modalMode: "neverAsk" })
- windowStarknet = _windowStarknet
+ const _windowStarknet = await connect({
+ modalMode: "neverAsk",
+ })
+ // comment this when using webwallet -- enable is already done by @argent/get-starknet and webwallet is currently using only v4
+ // to remove when @argent/get-starknet will support both v4 and v5
+ //await _windowStarknet?.enable({ starknetVersion })
+ windowStarknet = _windowStarknet as StarknetWindowObjectV5 | null
return windowStarknet ?? undefined
}
export const connectWallet = async (enableWebWallet: boolean) => {
const _windowStarknet = await connect({
exclude: enableWebWallet ? [] : ["argentWebWallet"],
- modalWalletAppearance: "email_first",
+ modalWalletAppearance: "all",
+ enableArgentMobile: true,
})
- windowStarknet = _windowStarknet
+
+ // comment this when using webwallet -- enable is already done by @argent/get-starknet and webwallet is currently using only v4
+ // to remove when @argent/get-starknet will support both v4 and v5
+ //await _windowStarknet?.enable({ starknetVersion })
+ windowStarknet = _windowStarknet as StarknetWindowObjectV5 | null
return windowStarknet ?? undefined
}
@@ -41,12 +64,15 @@ export const addToken = async (address: string): Promise => {
})
}
-export const chainId = (provider?: ProviderInterface): string | undefined => {
+export const getChainId = async (
+ provider?: ProviderInterface,
+): Promise => {
try {
if (!provider) {
throw Error("no provider")
}
- return shortString.decodeShortString(provider.chainId)
+ const chainId = await provider.getChainId()
+ return shortString.decodeShortString(chainId)
} catch {}
}
@@ -102,15 +128,37 @@ export const removeWalletChangeListener = async (
windowStarknet.off("accountsChanged", handleEvent)
}
-export const declare = async (contract: string, classHash: string) => {
+export const declare = async (
+ payload: DeclareContractPayload,
+ transactionsDetail?: InvocationsDetails,
+) => {
if (!windowStarknet?.isConnected) {
throw Error("starknet wallet not connected")
}
- return windowStarknet.account.declare({
- contract,
- classHash,
- })
+ return windowStarknet.account.declare(payload, transactionsDetail)
+}
+
+export const deploy = async (
+ payload: UniversalDeployerContractPayload,
+ details?: InvocationsDetails,
+) => {
+ if (!windowStarknet?.isConnected) {
+ throw Error("starknet wallet not connected")
+ }
+
+ return windowStarknet.account.deploy(payload, details)
+}
+
+export const declareAndDeploy = async (
+ payload: DeclareContractPayload,
+ transactionsDetail?: InvocationsDetails,
+) => {
+ if (!windowStarknet?.isConnected) {
+ throw Error("starknet wallet not connected")
+ }
+
+ return windowStarknet.account.declareAndDeploy(payload, transactionsDetail)
}
export const addNetwork = async (params: AddStarknetChainParameters) => {
diff --git a/packages/dapp/tsconfig.json b/packages/dapp/tsconfig.json
index d3b45b03d..93514bc17 100644
--- a/packages/dapp/tsconfig.json
+++ b/packages/dapp/tsconfig.json
@@ -8,8 +8,8 @@
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
- "module": "esnext",
- "moduleResolution": "node",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
diff --git a/packages/e2e/.env.example b/packages/e2e/.env.example
new file mode 100644
index 000000000..33e7af73b
--- /dev/null
+++ b/packages/e2e/.env.example
@@ -0,0 +1,4 @@
+ARGENT_X_ENVIRONMENT=
+TESTNET_SEED1=
+TESTNET_SEED2=
+TESTNET_SEED3=
\ No newline at end of file
diff --git a/packages/e2e/.vscode/settings.json b/packages/e2e/.vscode/settings.json
new file mode 100644
index 000000000..cc43ff1bb
--- /dev/null
+++ b/packages/e2e/.vscode/settings.json
@@ -0,0 +1,4 @@
+{
+ "json.schemaDownload.enable": true,
+ "typescript.tsdk": "../../node_modules/typescript/lib"
+}
diff --git a/packages/e2e/Dockerfile b/packages/e2e/Dockerfile
index a98e4c6e4..5498d0ab8 100644
--- a/packages/e2e/Dockerfile
+++ b/packages/e2e/Dockerfile
@@ -1,8 +1,6 @@
-
-FROM shardlabs/starknet-devnet:0.4.6
+FROM shardlabs/starknet-devnet:0.6.3
RUN addgroup -S localuser \
&& adduser -S localuser -G localuser
-
USER localuser
-COPY ./packages/extension/e2e/network-setup /network-setup
-ENTRYPOINT [ "starknet-devnet", "--host", "0.0.0.0", "--port", "5050", "--seed", "0", "--lite-mode", "--load-path", "/network-setup/dump.pkl" ]
\ No newline at end of file
+COPY ./packages/e2e/extension/network-setup /network-setup
+ENTRYPOINT [ "starknet-devnet", "--host", "0.0.0.0", "--port", "5050", "--seed", "0", "--lite-mode" ]
\ No newline at end of file
diff --git a/packages/e2e/extension/network-setup/Dockerfile b/packages/e2e/extension/network-setup/Dockerfile
index db57732eb..728e9bee5 100644
--- a/packages/e2e/extension/network-setup/Dockerfile
+++ b/packages/e2e/extension/network-setup/Dockerfile
@@ -1,9 +1,9 @@
-FROM shardlabs/starknet-devnet:0.4.6
+FROM shardlabs/starknet-devnet:0.6.3
RUN addgroup -S localuser \
&& adduser -S localuser -G localuser
USER localuser
COPY ./dump.pkl ./dump.pkl
-ENTRYPOINT [ "starknet-devnet", "--host", "0.0.0.0", "--port", "5050", "--seed", "0", "--lite-mode", "--load-path", "./dump.pkl" ]
\ No newline at end of file
+ENTRYPOINT [ "starknet-devnet", "--host", "0.0.0.0", "--port", "5050", "--seed", "0", "--lite-mode" ]
\ No newline at end of file
diff --git a/packages/e2e/extension/playwright.config.ts b/packages/e2e/extension/playwright.config.ts
new file mode 100644
index 000000000..360c9bb54
--- /dev/null
+++ b/packages/e2e/extension/playwright.config.ts
@@ -0,0 +1,35 @@
+import type { PlaywrightTestConfig } from "@playwright/test"
+import config from "./src/config"
+
+const isCI = Boolean(process.env.CI)
+
+const playwrightConfig: PlaywrightTestConfig = {
+ projects: [
+ {
+ name: "ArgentX",
+ use: {
+ trace: "on-first-retry",
+ viewport: { width: 360, height: 600 },
+ actionTimeout: 60 * 1000, // 1 minute
+ permissions: ["clipboard-read", "clipboard-write"],
+ },
+ timeout: 5 * 60e3, // 5 minutes
+ expect: { timeout: 30 * 1000 }, // 30 seconds
+ testDir: "./src/specs",
+ testMatch: /\.spec.ts$/,
+ retries: isCI ? 1 : 0,
+ outputDir: config.artifactsDir,
+ },
+ ],
+ workers: 1,
+ reportSlowTests: {
+ threshold: 1 * 60e3, // 1 minute
+ max: 5,
+ },
+ reporter: isCI ? [["github"], ["blob"]] : "list",
+ forbidOnly: isCI,
+ outputDir: config.artifactsDir,
+ preserveOutput: isCI ? "failures-only" : "never",
+}
+
+export default playwrightConfig
diff --git a/packages/e2e/extension/src/config.ts b/packages/e2e/extension/src/config.ts
index 313bb9730..45e6b3cf1 100644
--- a/packages/e2e/extension/src/config.ts
+++ b/packages/e2e/extension/src/config.ts
@@ -5,30 +5,15 @@ export default {
artifactsDir: path.resolve(__dirname, "../../artifacts/playwright"),
reportsDir: path.resolve(__dirname, "../../artifacts/reports"),
distDir: path.join(__dirname, "../../../extension/dist/"),
-
- wallets: [
- {
- // NOTE: Seed phrase is here intentionally and is used only for local testing. DO NOT use for any other purpose
- seed: "dove luxury shield hill chronic radio used barely rifle brick author bounce",
- accounts: [
- // 0.9992 ETH, deployed
- "0x0027A5C4b2Fe3D2F2623A9B9d91b73b53fBefba45087e572C22A27005a602B74",
- // 2 ETH, not deployed
- "0x03eC198B36781Bbb352ffa55c3E3b48F74C469da3495b6Bc211D87d61B9fDF7b",
- ],
- },
- {
- // NOTE: Seed phrase is here intentionally and is used only for local testing. DO NOT use for any other purpose
- seed: "muffin abandon fancy enhance neglect fit team biology loyal traffic ocean wash",
- accounts: [
- // 1 ETH, not deployed
- "0x035508Aaf6C124D348686F31ca9981568F0c0d29b563a2Ecb045aA8C81334057",
- ],
- },
- {
- // NOTE: Seed phrase is here intentionally and is used only for local testing. DO NOT use for any other purpose
- seed: "slam water student cotton chalk okay auto police frown smart vague salon",
- // 32 accounts
- },
- ],
+ testNetSeed1: process.env.E2E_TESTNET_SEED1, //wallet with 32 deployed accounts
+ testNetSeed2: process.env.E2E_TESTNET_SEED2, //wallet with 1 deployed account
+ testNetSeed3: process.env.E2E_TESTNET_SEED3, //wallet with 1 deployed account
+ destinationAddress: process.env.E2E_SENDER_ADDRESS, //used as transfers destination
+ senderAddr: process.env.E2E_SENDER_ADDRESS,
+ senderSeed: process.env.E2E_SENDER_SEED,
+ senderKey: process.env.E2E_SENDER_PRIVATEKEY,
+ account1Seed2: process.env.E2E_ACCOUNT_1_SEED2,
+ account1Seed3: process.env.E2E_ACCOUNT_1_SEED3,
+ starknetTestNetUrl: process.env.STARKNET_TESTNET_URL,
+ starkscanTestNetUrl: process.env.STARKSCAN_TESTNET_URL,
}
diff --git a/packages/e2e/extension/src/fixtures.ts b/packages/e2e/extension/src/fixtures.ts
index 3b8f07e6b..721a9cea3 100644
--- a/packages/e2e/extension/src/fixtures.ts
+++ b/packages/e2e/extension/src/fixtures.ts
@@ -1,6 +1,9 @@
+import { ChromiumBrowserContext } from "@playwright/test"
+
import type ExtensionPage from "./page-objects/ExtensionPage"
export interface TestExtensions {
extension: ExtensionPage
secondExtension: ExtensionPage
+ browserContext: ChromiumBrowserContext
}
diff --git a/packages/e2e/extension/src/languages/ILanguage.ts b/packages/e2e/extension/src/languages/ILanguage.ts
index 780d66051..21f21beb4 100644
--- a/packages/e2e/extension/src/languages/ILanguage.ts
+++ b/packages/e2e/extension/src/languages/ILanguage.ts
@@ -17,6 +17,7 @@ export interface ILanguage {
create: string
cancel: string
privacyStatement: string
+ reviewSend: string
}
account: {
noAccounts: string
@@ -31,6 +32,9 @@ export interface ILanguage {
pendingTransactions: string
recipientAddress: string
saveAddress: string
+ confirmTheSeedPhrase: string
+ showAccountRecovery: string
+ wrongPassword: string
}
wallet: {
//first screen
@@ -70,6 +74,12 @@ export interface ILanguage {
hiddenAccounts: string
delete: string
copy: string
+ copied: string
+ confirmRecovery: string
+ revealSeedPhrase: string
+ beforeYouContinue: string
+ seedWarning: string
+ deployAccount: string
}
developerSettings: {
manageNetworks: string
@@ -83,5 +93,11 @@ export interface ILanguage {
addressRequired: string
removeAddress: string
delete: string
+ addressBook: string
+ }
+ dapps: {
+ connect: string
+ reject: string
+ resetAll: string
}
}
diff --git a/packages/e2e/extension/src/languages/en/index.ts b/packages/e2e/extension/src/languages/en/index.ts
index 8afb708eb..8bae6b2ef 100644
--- a/packages/e2e/extension/src/languages/en/index.ts
+++ b/packages/e2e/extension/src/languages/en/index.ts
@@ -18,20 +18,25 @@ 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/",
+ reviewSend: "Review send",
},
account: {
noAccounts: "You have no accounts on ",
createAccount: "Create account",
addFunds: "Add funds",
- fundsFromStarkNet: "From another StarkNet account",
+ fundsFromStarkNet: "From another Starknet wallet",
fullAccountAddress: "Full account address",
send: "Send",
export: "Export",
- accountRecovery: "Set up account recovery",
+ accountRecovery: "Save your recovery phrase",
+ showAccountRecovery: "Show recovery phrase",
saveTheRecoveryPhrase: "Save the recovery phrase",
+ confirmTheSeedPhrase:
+ "I have saved my recovery phrase and understand I should never share it with anyone else",
pendingTransactions: "Pending transactions",
recipientAddress: "Recipient's address",
saveAddress: "Save address",
+ wrongPassword: "Incorrect password",
},
wallet: {
//first screen
@@ -44,7 +49,7 @@ const texts = {
desc2:
"StarkNet is in Alpha and may experience technical issues or introduce breaking changes from time to time. Please accept this before continuing.",
lossOfFunds:
- "I understand that StarkNet may introduce changes that make my existing account unusable and force to create new ones.",
+ "I understand that StarkNet will introduce changes (e.g. Cairo 1.0) that will affect my existing account(s) (e.g. rendering unusable) if I do not complete account upgrades.",
alphaVersion:
"I understand that StarkNet may experience performance issues and my transactions may fail for various reasons.",
//third screen
@@ -63,7 +68,7 @@ const texts = {
settings: {
addresBook: "Address book",
connectedDapps: "Connected dapps",
- showRecoveryPhase: "Show recovery phrase",
+ showRecoveryPhase: "Recovery phrase",
developerSettings: "Developer settings",
privacy: "Privacy",
hideAccount: "Hide account",
@@ -74,6 +79,14 @@ const texts = {
hiddenAccounts: "Hidden accounts",
delete: "Delete",
copy: "Copy",
+ copied: "Copied",
+ confirmRecovery:
+ "I have saved my recovery phrase and understand I should never share it with anyone else",
+ revealSeedPhrase: "Click to reveal recovery phrase",
+ beforeYouContinue: "Before you continue...",
+ seedWarning:
+ "Please save your recovery phrase. This is the only way you will be able to recover your Argent X accounts",
+ deployAccount: "Deploy account",
},
developerSettings: {
manageNetworks: "Manage networks",
@@ -87,6 +100,12 @@ const texts = {
addressRequired: "Address is required",
removeAddress: "Remove from address book",
delete: "Delete",
+ addressBook: "Address book",
+ },
+ dapps: {
+ connect: "Connect",
+ reject: "Reject",
+ resetAll: "Reset all dapp connections",
},
}
diff --git a/packages/e2e/extension/src/page-objects/Account.ts b/packages/e2e/extension/src/page-objects/Account.ts
index 54d7062d1..711ddd7a1 100644
--- a/packages/e2e/extension/src/page-objects/Account.ts
+++ b/packages/e2e/extension/src/page-objects/Account.ts
@@ -14,6 +14,9 @@ export default class Account extends Navigation {
constructor(page: Page) {
super(page)
}
+ accountName1 = "Account 1"
+ accountName2 = "Account 2"
+
get noAccountBanner() {
return this.page.locator(`div h5:text-is("${lang.account.noAccounts}")`)
}
@@ -36,10 +39,20 @@ export default class Account extends Navigation {
)
}
+ get accountAddressFromAssetsView() {
+ return this.page.locator('[data-testid="account-tokens"] button').first()
+ }
+
get send() {
return this.page.locator(`button:text-is("${lang.account.send}")`)
}
+ get deployAccount() {
+ return this.page.locator(
+ `button :text-is("${lang.settings.deployAccount}")`,
+ )
+ }
+
token(tkn: TokenName) {
return this.page.locator(`button :text-is('${tkn}')`)
}
@@ -65,11 +78,11 @@ export default class Account extends Navigation {
}
get sendMax() {
- return this.page.locator('button:text-is("MAX")')
+ return this.page.locator('button:text-is("Max")')
}
- get recepientAddress() {
- return this.page.locator('[name="recipient"]')
+ get recipientAddressQuery() {
+ return this.page.locator('[name="query"]')
}
account(accountName: string) {
@@ -80,14 +93,33 @@ export default class Account extends Navigation {
return this.page.locator('[data-testid="tokenBalance"]')
}
- currentBalance(tkn: "ETH") {
- return this.page.locator(` //button//h6[contains(text(), '${tkn}')]`)
+ currentBalance(tkn: "Ethereum") {
+ return this.page.locator(
+ ` //button//h6[contains(text(), '${tkn}')]/following::p`,
+ )
+ }
+
+ currentBalanceDevNet(tkn: "ETH") {
+ return this.page.locator(`//button//h6[contains(text(), '${tkn}')]`)
}
get accountName() {
return this.page.locator('[data-testid="account-tokens"] h2')
}
+ async addAccountMainnet({ firstAccount = true }: { firstAccount?: boolean }) {
+ if (firstAccount) {
+ await this.createAccount.click()
+ } else {
+ await this.accountListSelector.click()
+ await this.addANewccountFromAccountList.click()
+ }
+ await this.addStandardAccountFromNewAccountScreen.click()
+
+ await this.account("").last().click()
+ await expect(this.accountListSelector).toBeVisible()
+ }
+
async addAccount({ firstAccount = true }: { firstAccount?: boolean }) {
if (firstAccount) {
await this.createAccount.click()
@@ -119,7 +151,7 @@ export default class Account extends Navigation {
if (currentAccount != accountName) {
await this.selectAccount(accountName)
}
- await expect(this.accountName).toHaveText(accountName)
+ await expect(this.accountListSelector).toHaveText(accountName)
}
async assets(accountName: string) {
@@ -136,12 +168,14 @@ export default class Account extends Navigation {
}
return assetsList
}
-
- async ensureAsset(accountName: string, name: "ETH", value: string) {
+ ////*[text() = 'Ethereum']/following-sibling::div
+ async ensureAsset(accountName: string, name: "Ethereum", value: string) {
await this.ensureSelectedAccount(accountName)
await expect(
- this.page.locator(`button :text("${value} ${name}")`),
- ).toBeVisible()
+ this.page.locator(
+ `//*[text() = '${name}']/following-sibling::div/p[text() = '${value}']`,
+ ),
+ ).toBeVisible({ timeout: 90000 })
}
async getTotalFeeValue() {
@@ -157,27 +191,40 @@ export default class Account extends Navigation {
}
async transfer({
originAccountName,
- recepientAddress,
+ recipientAddress,
tokenName,
amount,
+ fillRecipientAddress = "paste",
+ submit = true,
}: {
originAccountName: string
- recepientAddress: string
+ recipientAddress: string
tokenName: TokenName
amount: number | "MAX"
+ fillRecipientAddress?: "typing" | "paste"
+ submit?: boolean
}) {
await this.ensureSelectedAccount(originAccountName)
await this.token(tokenName).click()
- await this.send.last().click()
+ fillRecipientAddress === "paste"
+ ? await this.recipientAddressQuery.fill(recipientAddress)
+ : await this.recipientAddressQuery.type(recipientAddress)
+ if (recipientAddress.endsWith("stark")) {
+ await this.page.click(`button:has-text("${recipientAddress}")`)
+ } else {
+ await this.recipientAddressQuery.focus()
+ await this.page.keyboard.press("Enter")
+ }
if (amount === "MAX") {
+ await expect(this.balance).toBeVisible()
+ await expect(this.sendMax).toBeVisible()
await this.sendMax.click()
} else {
await this.amount.fill(amount.toString())
}
- await this.recepientAddress.fill(recepientAddress)
- await this.next.click()
- await this.approve.click()
+ await this.reviewSend.click()
+ submit ?? (await this.approve.click())
}
async ensureTokenBalance({
@@ -207,7 +254,19 @@ export default class Account extends Navigation {
get setUpAccountRecovery() {
return this.page.locator(
- `button :text-is("${lang.account.accountRecovery}")`,
+ `button:text-is("${lang.account.accountRecovery}")`,
+ )
+ }
+
+ get showAccountRecovery() {
+ return this.page.locator(
+ `button:text-is("${lang.account.showAccountRecovery}")`,
+ )
+ }
+
+ get confirmTheSeedPhrase() {
+ return this.page.locator(
+ `p:text-is("${lang.account.confirmTheSeedPhrase}")`,
)
}
@@ -228,8 +287,12 @@ export default class Account extends Navigation {
return this.page.locator(`button:text-is("${lang.account.saveAddress}")`)
}
- get contact() {
- return this.page.locator("div h5")
+ get copyAddress() {
+ return this.page.locator('[data-testid="account-tokens"] button').first()
+ }
+
+ contact(label: string) {
+ return this.page.locator(`div h6:text-is("${label}")`)
}
get dappsBanner() {
@@ -239,4 +302,49 @@ export default class Account extends Navigation {
get dappsBannerClose() {
return this.page.locator('[title="Dappland"] svg')
}
+
+ async saveRecoveryPhrase() {
+ const nextModal = await this.next.isVisible({ timeout: 60 })
+ if (nextModal) {
+ await Promise.all([
+ expect(
+ this.page.locator(
+ `h3:has-text("${lang.settings.beforeYouContinue}")`,
+ ),
+ ).toBeVisible(),
+ expect(
+ this.page.locator(`p:has-text("${lang.settings.seedWarning}")`),
+ ).toBeVisible(),
+ ])
+ await this.next.click()
+ }
+ await this.page
+ .locator(`span:has-text("${lang.settings.revealSeedPhrase}")`)
+ .click()
+ const pos = Array.from({ length: 12 }, (_, i) => i + 1)
+ const seed = await Promise.all(
+ pos.map(async (index) => {
+ return this.page
+ .locator(`//*[normalize-space() = '${index}']/parent::*`)
+ .textContent()
+ .then((text) => text?.replace(/[0-9]/g, ""))
+ }),
+ ).then((result) => result.join(" "))
+
+ await Promise.all([
+ this.page.locator(`button:has-text("${lang.settings.copy}")`).click(),
+ expect(
+ this.page.locator(`button:has-text("${lang.settings.copied}")`),
+ ).toBeVisible(),
+ ])
+ await this.page
+ .locator(`p:has-text("${lang.settings.confirmRecovery}")`)
+ .click()
+ await this.done.click()
+ const seedPhraseCopied = await this.page.evaluate(
+ `navigator.clipboard.readText();`,
+ )
+ expect(seed).toBe(seedPhraseCopied)
+ return seedPhraseCopied
+ }
}
diff --git a/packages/e2e/extension/src/page-objects/Activity.ts b/packages/e2e/extension/src/page-objects/Activity.ts
index 9160360f5..978590eaa 100644
--- a/packages/e2e/extension/src/page-objects/Activity.ts
+++ b/packages/e2e/extension/src/page-objects/Activity.ts
@@ -24,7 +24,7 @@ export default class Activity extends Navigation {
checkActivity(nbr: number) {
return Promise.all([
- this.menuPendingTransationsIndicator.click(),
+ this.menuPendingTransactionsIndicator.click(),
this.ensurePendingTransactions(nbr),
])
}
diff --git a/packages/e2e/extension/src/page-objects/AddressBook.ts b/packages/e2e/extension/src/page-objects/AddressBook.ts
index 96074f5a3..999968ad8 100644
--- a/packages/e2e/extension/src/page-objects/AddressBook.ts
+++ b/packages/e2e/extension/src/page-objects/AddressBook.ts
@@ -32,33 +32,39 @@ export default class AddressBook extends Navigation {
return this.page.locator(`button:text-is("${lang.common.cancel}")`)
}
- networkOption(name: "Localhost 5050" | "Testnet" | "Testnet 2" | "Mainnet") {
- return this.page.locator(`div[aria-disabled="false"]:text-is("${name}")`)
+ networkOption(name: "Localhost 5050" | "Testnet" | "Mainnet") {
+ return this.page.locator(`button[role="menuitem"]:text-is("${name}")`)
}
get nameRequired() {
return this.page.locator(
- `//input[@name="name"]/following::p[contains(text(), '${lang.address.nameRequired}')]`,
+ `//input[@name="name"]/following::label[contains(text(), '${lang.address.nameRequired}')]`,
)
}
get addressRequired() {
return this.page.locator(
- `//textarea[@name="address"]/following::p[contains(text(), '${lang.address.addressRequired}')]`,
+ `//textarea[@name="address"]/following::label[contains(text(), '${lang.address.addressRequired}')]`,
)
}
- addressByname(name: string) {
+ addressByName(name: string) {
return this.page.locator(
`//button/following::*[contains(text(),'${name}')]`,
)
}
get deleteAddress() {
- return this.page.locator(`button:text-is("${lang.address.removeAddress}")`)
+ return this.page.locator(
+ `button[aria-label="${lang.address.removeAddress}"]`,
+ )
}
get delete() {
return this.page.locator(`button:text-is("${lang.address.delete}")`)
}
+
+ get addressBook() {
+ return this.page.locator(`button:text-is("${lang.address.addressBook}")`)
+ }
}
diff --git a/packages/e2e/extension/src/page-objects/Dapps.ts b/packages/e2e/extension/src/page-objects/Dapps.ts
new file mode 100644
index 000000000..74465d681
--- /dev/null
+++ b/packages/e2e/extension/src/page-objects/Dapps.ts
@@ -0,0 +1,53 @@
+import { ChromiumBrowserContext, Page, expect } from "@playwright/test"
+
+import { lang } from "../languages"
+import Navigation from "./Navigation"
+
+export default class Dapps extends Navigation {
+ constructor(page: Page) {
+ super(page)
+ }
+
+ connected(url: string) {
+ return this.page.locator(
+ `//button/*[contains(text(),'${url.slice(0, 30)}')]`,
+ )
+ }
+
+ disconnect(url: string) {
+ return this.page.locator(
+ `//button/*[contains(text(),'${url.slice(0, 30)}')]/following::button[1]`,
+ )
+ }
+
+ resetAll() {
+ return this.page.locator(`button:text-is("${lang.dapps.resetAll}")`)
+ }
+
+ get accept() {
+ return this.page.locator(`button:text-is("${lang.dapps.connect}")`)
+ }
+
+ get reject() {
+ return this.page.locator(`button:text-is("${lang.dapps.reject}")`)
+ }
+
+ async requestConnectionFromDapp(
+ browserContext: ChromiumBrowserContext,
+ url: string,
+ ) {
+ //open dapp page
+ const dapp = await browserContext.newPage()
+ await dapp.setViewportSize({ width: 1080, height: 720 })
+ await dapp.goto("chrome://inspect/#extensions")
+ await dapp.waitForTimeout(5000)
+ await dapp.goto(url)
+
+ await dapp
+ .locator('div :text-matches("Connect Wallet", "i")')
+ .first()
+ .click()
+ await expect(dapp.locator("text=Argent X")).toBeVisible()
+ await dapp.locator("text=Argent X").click()
+ }
+}
diff --git a/packages/e2e/extension/src/page-objects/DeveloperSettings.ts b/packages/e2e/extension/src/page-objects/DeveloperSettings.ts
index 4acc8de98..23978b04f 100644
--- a/packages/e2e/extension/src/page-objects/DeveloperSettings.ts
+++ b/packages/e2e/extension/src/page-objects/DeveloperSettings.ts
@@ -42,8 +42,8 @@ export default class DeveloperSettings {
return this.page.locator('[name="chainId"]')
}
- get baseUrl() {
- return this.page.locator('[name="baseUrl"]')
+ get sequencerUrl() {
+ return this.page.locator('[name="sequencerUrl"]')
}
get create() {
diff --git a/packages/e2e/extension/src/page-objects/ExtensionPage.ts b/packages/e2e/extension/src/page-objects/ExtensionPage.ts
index 6230edd1a..e0cf8a868 100644
--- a/packages/e2e/extension/src/page-objects/ExtensionPage.ts
+++ b/packages/e2e/extension/src/page-objects/ExtensionPage.ts
@@ -1,14 +1,17 @@
-import type { Page } from "@playwright/test"
+import { expect, type Page } from "@playwright/test"
import Messages from "../utils/Messages"
import Account from "./Account"
import Activity from "./Activity"
import AddressBook from "./AddressBook"
+import Dapps from "./Dapps"
import DeveloperSettings from "./DeveloperSettings"
import Navigation from "./Navigation"
import Network from "./Network"
import Settings from "./Settings"
import Wallet from "./Wallet"
+import config from "../config"
+import { balanceEther, transferEth, AccountsToSetup } from "../utils/account"
export default class ExtensionPage {
page: Page
@@ -21,6 +24,7 @@ export default class ExtensionPage {
navigation: Navigation
developerSettings: DeveloperSettings
addressBook: AddressBook
+ dapps: Dapps
constructor(page: Page, private extensionUrl: string) {
this.page = page
this.wallet = new Wallet(page)
@@ -33,6 +37,7 @@ export default class ExtensionPage {
this.navigation = new Navigation(page)
this.developerSettings = new DeveloperSettings(page)
this.addressBook = new AddressBook(page)
+ this.dapps = new Dapps(page)
}
async open() {
@@ -51,7 +56,98 @@ export default class ExtensionPage {
await this.page.keyboard.press(`${key}+KeyV`)
}
+ async pasteSeed() {
+ await this.page.locator('[data-testid="seed-input-0"]').focus()
+ await this.paste()
+ }
+
async setClipBoardContent(text: string) {
await this.page.evaluate(`navigator.clipboard.writeText('${text}')`)
}
+
+ async recoverWallet(seed: string, password?: string) {
+ await this.wallet.restoreExistingWallet.click()
+ await this.setClipBoardContent(seed)
+ await this.pasteSeed()
+ await this.navigation.continue.click()
+
+ await this.wallet.password.fill(password ?? config.password)
+ await this.wallet.repeatPassword.fill(password ?? config.password)
+
+ await this.navigation.continue.click()
+ await expect(this.wallet.finish.first()).toBeVisible({
+ timeout: 180000,
+ })
+
+ await this.open()
+ await expect(this.network.networkSelector).toBeVisible()
+ }
+
+ getClipboard() {
+ return this.page.evaluate(`navigator.clipboard.readText()`)
+ }
+
+ async deployAccountByName(accountName: string) {
+ await this.navigation.showSettings.click()
+ await this.page.locator(`text=${accountName}`).click()
+ await this.account.deployAccount.click()
+ await this.navigation.confirm.click()
+ await this.navigation.back.click()
+ await this.navigation.close.click()
+ }
+
+ async setupWallet({
+ accountsToSetup,
+ }: {
+ accountsToSetup: AccountsToSetup[]
+ }) {
+ await this.wallet.newWalletOnboarding()
+ await this.open()
+ await this.account.accountAddressFromAssetsView.click()
+ const seed = await this.account
+ .saveRecoveryPhrase()
+ .then((adr) => String(adr))
+ const accountAddresses: string[] = []
+
+ for (const [accIndex, acc] of accountsToSetup.entries()) {
+ console.log(accIndex, acc)
+ if (accIndex !== 0) {
+ await this.account.addAccount({ firstAccount: false })
+ }
+ await this.account.copyAddress.click()
+ const accountAddress = await this.getClipboard().then((adr) =>
+ String(adr),
+ )
+ expect(accountAddress).toMatch(/^0x0/)
+ accountAddresses.push(accountAddress)
+
+ if (acc.initialBalance > 0) {
+ await transferEth(
+ `${acc.initialBalance * Math.pow(10, 18)}`, // amount Ethereum has 18 decimals
+ accountAddress, // reciever wallet address
+ )
+ await this.account.ensureAsset(
+ `Account ${accIndex + 1}`,
+ "Ethereum",
+ `${acc.initialBalance} ETH`,
+ )
+ if (acc.deploy) {
+ await this.deployAccountByName(`Account ${accIndex + 1}`)
+ }
+ }
+ }
+ console.log(accountAddresses.length, accountAddresses, seed)
+ return { accountAddresses, seed }
+ }
+
+ async validateTx(address: string) {
+ const initialBalance = await balanceEther(address)
+ await this.navigation.approve.click()
+ await this.activity.checkActivity(1)
+ await expect(
+ this.navigation.menuPendingTransactionsIndicator,
+ ).not.toBeVisible({ timeout: 60000 })
+ const finalBalance = await balanceEther(address)
+ expect(parseFloat(finalBalance)).toBeGreaterThan(parseFloat(initialBalance))
+ }
}
diff --git a/packages/e2e/extension/src/page-objects/Navigation.ts b/packages/e2e/extension/src/page-objects/Navigation.ts
index 29570bffc..bbe871dd8 100644
--- a/packages/e2e/extension/src/page-objects/Navigation.ts
+++ b/packages/e2e/extension/src/page-objects/Navigation.ts
@@ -20,10 +20,18 @@ export default class Navigation {
return this.page.locator(`button:text-is("${lang.common.confirm}")`)
}
+ get confirm() {
+ return this.page.locator(`button:text-is("${lang.common.confirm}")`)
+ }
+
get next() {
return this.page.locator(`button:text-is("${lang.common.next}")`)
}
+ get reviewSend() {
+ return this.page.locator(`button:text-is("${lang.common.reviewSend}")`)
+ }
+
get done() {
return this.page.locator(`button:text-is("${lang.common.done}")`)
}
@@ -51,7 +59,7 @@ export default class Navigation {
}
get lockWallet() {
- return this.page.locator(`button:text-is("${lang.common.lockWallet}")`)
+ return this.page.locator(`[aria-label="${lang.common.lockWallet}"]`)
}
get reset() {
@@ -62,7 +70,7 @@ export default class Navigation {
return this.page.locator(`button:text-is("${lang.common.confirmReset}")`)
}
- get menuPendingTransationsIndicator() {
+ get menuPendingTransactionsIndicator() {
return this.page.locator('[aria-label="Pending transactions"]')
}
diff --git a/packages/e2e/extension/src/page-objects/Network.ts b/packages/e2e/extension/src/page-objects/Network.ts
index a6db6e9a2..0a761f319 100644
--- a/packages/e2e/extension/src/page-objects/Network.ts
+++ b/packages/e2e/extension/src/page-objects/Network.ts
@@ -1,10 +1,29 @@
import { Page, expect } from "@playwright/test"
-type NetworkName =
- | "Localhost 5050"
- | "Testnet"
- | "Testnet 2"
- | "Mainnet"
- | "My Network"
+type NetworkName = "Localhost 5050" | "Testnet" | "Mainnet" | "My Network"
+export function getDefaultNetwork() {
+ const argentXEnv = process.env.ARGENT_X_ENVIRONMENT
+
+ if (!argentXEnv) {
+ throw new Error("ARGENT_X_ENVIRONMENT not set")
+ }
+ let defaultNetworkId: string
+ switch (argentXEnv.toLowerCase()) {
+ case "prod":
+ case "staging":
+ defaultNetworkId = "mainnet-alpha"
+ break
+
+ case "hydrogen":
+ case "test":
+ defaultNetworkId = "goerli-alpha"
+ break
+
+ default:
+ throw new Error(`Unknown ARGENTX_ENVIRONMENT: ${argentXEnv}`)
+ }
+
+ return defaultNetworkId
+}
export default class Network {
constructor(private page: Page) {}
get networkSelector() {
@@ -27,4 +46,16 @@ export default class Network {
.allInnerTexts()
return networks.map((net) => expect(availableNetworks).toContain(net))
}
+
+ getDefaultNetworkName() {
+ const defaultNetworkId = getDefaultNetwork()
+ switch (defaultNetworkId.toLowerCase()) {
+ case "mainnet-alpha":
+ return "Mainnet"
+ case "goerli-alpha":
+ return "Testnet"
+ default:
+ throw new Error(`Unknown ARGENTX_Network: ${defaultNetworkId}`)
+ }
+ }
}
diff --git a/packages/e2e/extension/src/specs/accountSettings.spec.ts b/packages/e2e/extension/src/specs/accountSettings.spec.ts
index 25e776a17..cd2301817 100644
--- a/packages/e2e/extension/src/specs/accountSettings.spec.ts
+++ b/packages/e2e/extension/src/specs/accountSettings.spec.ts
@@ -2,14 +2,17 @@ import { expect } from "@playwright/test"
import config from "../config"
import test from "../test"
+import { lang } from "../languages"
test.describe("Account settings", () => {
test("User should be able to edit account name", async ({ extension }) => {
await extension.wallet.newWalletOnboarding()
await extension.open()
await expect(extension.network.networkSelector).toBeVisible()
- await extension.network.selectNetwork("Localhost 5050")
- const [accountName1] = await extension.account.addAccount({})
+ await extension.network.selectNetwork("Testnet")
+ const [accountName1] = await extension.account.addAccount({
+ firstAccount: false,
+ })
await extension.navigation.showSettings.click()
await extension.settings.account(accountName1!).click()
@@ -27,8 +30,10 @@ test.describe("Account settings", () => {
await extension.wallet.newWalletOnboarding()
await extension.open()
await expect(extension.network.networkSelector).toBeVisible()
- await extension.network.selectNetwork("Localhost 5050")
- const [accountName1] = await extension.account.addAccount({})
+ await extension.network.selectNetwork("Testnet")
+ const [accountName1] = await extension.account.addAccount({
+ firstAccount: false,
+ })
await extension.navigation.showSettings.click()
await extension.settings.account(accountName1!).click()
@@ -52,41 +57,40 @@ test.describe("Account settings", () => {
await extension.wallet.newWalletOnboarding()
await extension.open()
await expect(extension.network.networkSelector).toBeVisible()
- await extension.network.selectNetwork("Testnet 2")
- const [accountName1] = await extension.account.addAccount({})
+ await extension.network.selectNetwork("Testnet")
+ const [accountName2] = await extension.account.addAccount({
+ firstAccount: false,
+ })
await extension.navigation.showSettings.click()
- await extension.settings.account(accountName1!).click()
+ await extension.settings.account(accountName2!).click()
await extension.settings.hideAccount.click()
await extension.settings.confirmHide.click()
- await expect(extension.account.account(accountName1!)).toBeHidden()
+ await expect(extension.account.account(accountName2!)).toBeHidden()
await extension.settings.hiddenAccounts.click()
- await extension.settings.unhideAccount(accountName1!).click()
+ await extension.settings.unhideAccount(accountName2!).click()
+ await extension.navigation.back.click()
await expect(extension.settings.hiddenAccounts).toBeHidden()
- await expect(extension.account.account(accountName1!)).toBeVisible()
+ await expect(extension.account.account(accountName2!)).toBeVisible()
})
- test("User should be able to delete an account on local network", async ({
+ test("User should be able unlock wallet using password", async ({
extension,
}) => {
await extension.wallet.newWalletOnboarding()
await extension.open()
await expect(extension.network.networkSelector).toBeVisible()
- await extension.network.selectNetwork("Localhost 5050")
- const [accountName1] = await extension.account.addAccount({})
-
await extension.navigation.showSettings.click()
- await extension.settings.account(accountName1!).click()
- await extension.settings.deleteAccount.click()
- await extension.settings.confirmDelete.click()
+ await extension.navigation.lockWallet.click()
- await expect(extension.account.account(accountName1!)).toBeHidden()
- await expect(extension.settings.hiddenAccounts).toBeHidden()
+ await extension.account.password.fill(config.password)
+ await extension.navigation.unlock.click()
+ await expect(extension.network.networkSelector).toBeVisible()
})
- test("User should be able unlock wallet using password", async ({
+ test("User should not be able unlock wallet using wrong password", async ({
extension,
}) => {
await extension.wallet.newWalletOnboarding()
@@ -95,8 +99,11 @@ test.describe("Account settings", () => {
await extension.navigation.showSettings.click()
await extension.navigation.lockWallet.click()
- await extension.account.password.fill(config.password)
+ await extension.account.password.fill("wrongpassword123!")
await extension.navigation.unlock.click()
- await expect(extension.network.networkSelector).toBeVisible()
+ await expect(
+ extension.page.locator(`label:text-is("${lang.account.wrongPassword}")`),
+ ).toBeVisible()
+ await expect(extension.account.password).toBeVisible()
})
})
diff --git a/packages/e2e/extension/src/specs/addressBook.spec.ts b/packages/e2e/extension/src/specs/addressBook.spec.ts
index c2935a67f..dca2eecad 100644
--- a/packages/e2e/extension/src/specs/addressBook.spec.ts
+++ b/packages/e2e/extension/src/specs/addressBook.spec.ts
@@ -5,11 +5,10 @@ import test from "../test"
test.describe("Address Book", () => {
test("Add, update, use and delete address", async ({ extension }) => {
- await extension.wallet.newWalletOnboarding()
- await extension.open()
- await expect(extension.network.networkSelector).toBeVisible()
- await extension.network.selectNetwork("Localhost 5050")
- await extension.account.addAccount({})
+ await extension.setupWallet({
+ accountsToSetup: [{ initialBalance: 0.002 }],
+ })
+
await extension.navigation.showSettings.click()
await extension.settings.addressBook.click()
//create
@@ -21,66 +20,89 @@ test.describe("Address Book", () => {
await extension.addressBook.save.click()
await expect(extension.addressBook.nameRequired).not.toBeVisible()
await expect(extension.addressBook.addressRequired).toBeVisible()
- await extension.addressBook.address.fill(config.wallets[0].accounts![0])
+ await extension.addressBook.address.fill(config.account1Seed2!)
await expect(extension.addressBook.nameRequired).not.toBeVisible()
await expect(extension.addressBook.addressRequired).not.toBeVisible()
await extension.addressBook.network.click()
- await extension.addressBook.networkOption("Localhost 5050").click()
+ await extension.addressBook.networkOption("Testnet").click()
await extension.addressBook.save.click()
// update
- await extension.addressBook.addressByname("My first address").click()
+ await extension.addressBook.addressByName("My first address").click()
await extension.addressBook.name.fill("New name")
await extension.addressBook.save.click()
- await expect(extension.addressBook.addressByname("New name")).toBeVisible()
+ await expect(extension.addressBook.addressByName("New name")).toBeVisible()
await extension.navigation.back.click()
await extension.navigation.close.click()
//transfer to address
await extension.account.token("Ethereum").click()
- await extension.account.send.click()
- await extension.account.recipientAddress.click()
- await extension.addressBook.addressByname("New name").click()
+ await extension.addressBook.addressBook.click()
+ await extension.addressBook.addressByName("New name").click()
await extension.account.sendMax.click()
- await extension.navigation.next.click()
- await extension.navigation.approve.click()
- await extension.activity.checkActivity(1)
+ await extension.navigation.reviewSend.click()
+
+ await extension.validateTx(config.account1Seed2!)
//delete address
await extension.navigation.menuTokens.click()
await extension.navigation.showSettings.click()
await extension.settings.addressBook.click()
- await extension.addressBook.addressByname("New name").click()
+ await extension.addressBook.addressByName("New name").click()
await extension.addressBook.deleteAddress.click()
await extension.addressBook.delete.click()
await expect(
- extension.addressBook.addressByname("New name"),
+ extension.addressBook.addressByName("New name"),
).not.toBeVisible()
})
- test("Add address from send window", async ({ extension }) => {
- await extension.wallet.newWalletOnboarding()
- await extension.open()
+ test("Add address after typing", async ({ extension }) => {
+ await extension.setupWallet({
+ accountsToSetup: [{ initialBalance: 0.002 }],
+ })
+
await expect(extension.network.networkSelector).toBeVisible()
- await extension.network.selectNetwork("Localhost 5050")
- await extension.account.addAccount({})
+ await extension.network.selectNetwork("Testnet")
await extension.account.token("Ethereum").click()
- await extension.account.send.click()
- await extension.account.recepientAddress.fill(
- config.wallets[0].accounts![0],
+ await extension.account.recipientAddressQuery.type(config.account1Seed2!)
+ await extension.addressBook.add.click()
+ await expect(extension.addressBook.address).toHaveText(
+ config.account1Seed2!,
)
+ await extension.addressBook.name.fill("My address")
+ await extension.addressBook.save.click()
+ await extension.account.contact("My address").click()
+
+ await extension.account.sendMax.click()
+ await extension.navigation.reviewSend.click()
+
+ await extension.validateTx(config.account1Seed2!)
+ })
+
+ test("Add address from send window", async ({ extension }) => {
+ await extension.setupWallet({
+ accountsToSetup: [{ initialBalance: 0.002 }],
+ })
+
+ await expect(extension.network.networkSelector).toBeVisible()
+ await extension.network.selectNetwork("Testnet")
+
+ await extension.account.token("Ethereum").click()
+ await extension.setClipBoardContent(config.account1Seed2!)
+ await extension.account.recipientAddressQuery.focus()
+ await extension.paste()
+
await extension.account.saveAddress.click()
await expect(extension.addressBook.address).toHaveText(
- config.wallets[0].accounts![0],
+ config.account1Seed2!,
)
await extension.addressBook.name.fill("My address")
await extension.addressBook.save.click()
- await expect(extension.account.contact).toHaveText("My address")
await extension.account.sendMax.click()
- await extension.navigation.next.click()
- await extension.navigation.approve.click()
- await extension.activity.checkActivity(1)
+ await extension.navigation.reviewSend.click()
+
+ await extension.validateTx(config.account1Seed2!)
})
})
diff --git a/packages/e2e/extension/src/specs/dapps.spec.ts b/packages/e2e/extension/src/specs/dapps.spec.ts
new file mode 100644
index 000000000..d2701d920
--- /dev/null
+++ b/packages/e2e/extension/src/specs/dapps.spec.ts
@@ -0,0 +1,91 @@
+import { expect } from "@playwright/test"
+
+import test from "../test"
+
+const aspectUrl = "https://testnet.aspect.co"
+const testDappUrl = "https://dapp-argentlabs.vercel.app/"
+
+test.describe("Dapps", () => {
+ test("connect from aspect", async ({ extension, browserContext }) => {
+ //setup wallet
+ await extension.wallet.newWalletOnboarding()
+ await extension.open()
+ await extension.dapps.requestConnectionFromDapp(browserContext, aspectUrl)
+ //accept connection from ArgentX
+ await extension.dapps.accept.click()
+ //check connect dapps
+ await extension.navigation.showSettings.click()
+ await extension.settings.connectedDapps.click()
+ await expect(extension.dapps.connected(aspectUrl)).toBeVisible()
+ //disconnect dapp from ArgentX
+ await extension.dapps.disconnect(aspectUrl).click()
+ await expect(extension.dapps.connected(aspectUrl)).toBeHidden()
+ })
+
+ test("connect from testDapp", async ({ extension, browserContext }) => {
+ //setup wallet
+ await extension.wallet.newWalletOnboarding()
+ await extension.open()
+ await extension.dapps.requestConnectionFromDapp(browserContext, testDappUrl)
+ //accept connection from ArgentX
+ await extension.dapps.accept.click()
+ //check connect dapps
+ await extension.navigation.showSettings.click()
+ await extension.settings.connectedDapps.click()
+ await expect(extension.dapps.connected(testDappUrl)).toBeVisible()
+ //disconnect dapp from ArgentX
+ await extension.dapps.disconnect(testDappUrl).click()
+ await expect(extension.dapps.connected(testDappUrl)).toBeHidden()
+ })
+
+ test("reset all connections", async ({ extension, browserContext }) => {
+ //setup wallet
+ await extension.wallet.newWalletOnboarding()
+ await extension.open()
+ await extension.dapps.requestConnectionFromDapp(browserContext, aspectUrl)
+ //accept connection from ArgentX
+ await extension.dapps.accept.click()
+ await extension.dapps.requestConnectionFromDapp(browserContext, testDappUrl)
+ //accept connection from ArgentX
+ await extension.dapps.accept.click()
+ await extension.navigation.showSettings.click()
+ await extension.settings.connectedDapps.click()
+ await Promise.all([
+ expect(extension.dapps.connected(testDappUrl)).toBeVisible(),
+ expect(extension.dapps.connected(aspectUrl)).toBeVisible(),
+ ])
+
+ await extension.dapps.resetAll().click()
+ await Promise.all([
+ expect(extension.dapps.connected(testDappUrl)).toBeHidden(),
+ expect(extension.dapps.connected(aspectUrl)).toBeHidden(),
+ ])
+ })
+
+ test("disconnect only one connected dapp", async ({
+ extension,
+ browserContext,
+ }) => {
+ //setup wallet
+ await extension.wallet.newWalletOnboarding()
+ await extension.open()
+ await extension.dapps.requestConnectionFromDapp(browserContext, aspectUrl)
+ //accept connection from ArgentX
+ await extension.dapps.accept.click()
+ await extension.dapps.requestConnectionFromDapp(browserContext, testDappUrl)
+ //accept connection from ArgentX
+ await extension.dapps.accept.click()
+ await extension.navigation.showSettings.click()
+ await extension.settings.connectedDapps.click()
+ await Promise.all([
+ expect(extension.dapps.connected(testDappUrl)).toBeVisible(),
+ expect(extension.dapps.connected(aspectUrl)).toBeVisible(),
+ ])
+
+ await extension.dapps.disconnect(testDappUrl).click()
+ await Promise.all([
+ expect(extension.dapps.connected(testDappUrl)).toBeHidden(),
+ expect(extension.dapps.connected(aspectUrl)).toBeVisible(),
+ ])
+ })
+})
diff --git a/packages/e2e/extension/src/specs/dappsBanner.spec.ts b/packages/e2e/extension/src/specs/dappsBanner.spec.ts
index 905bbcde1..b4af268ce 100644
--- a/packages/e2e/extension/src/specs/dappsBanner.spec.ts
+++ b/packages/e2e/extension/src/specs/dappsBanner.spec.ts
@@ -1,6 +1,5 @@
import { expect } from "@playwright/test"
-import config from "../config"
import test from "../test"
test.describe("Banner", () => {
@@ -8,11 +7,6 @@ test.describe("Banner", () => {
await extension.wallet.newWalletOnboarding()
await extension.open()
await expect(extension.network.networkSelector).toBeVisible()
- await extension.account.setUpAccountRecovery.click()
- await extension.account.saveTheRecoveryPhrase.click()
- await extension.navigation.continue.click()
- await extension.navigation.yes.click()
- await expect(extension.account.setUpAccountRecovery).toBeHidden()
await expect(extension.account.dappsBanner).toBeVisible()
let href = await extension.account.dappsBanner.getAttribute("href")
expect(href).toContain("https://www.dappland.com")
@@ -28,11 +22,6 @@ test.describe("Banner", () => {
await extension.wallet.newWalletOnboarding()
await extension.open()
await expect(extension.network.networkSelector).toBeVisible()
- await extension.account.setUpAccountRecovery.click()
- await extension.account.saveTheRecoveryPhrase.click()
- await extension.navigation.continue.click()
- await extension.navigation.yes.click()
- await expect(extension.account.setUpAccountRecovery).toBeHidden()
await expect(extension.account.dappsBanner).toBeVisible()
await extension.account.dappsBannerClose.click()
await expect(extension.account.dappsBanner).toBeHidden()
@@ -41,23 +30,16 @@ test.describe("Banner", () => {
test("dapps banner shoud be visible after account recovery", async ({
extension,
}) => {
- await extension.open()
- await extension.wallet.restoreExistingWallet.click()
- await extension.setClipBoardContent(config.wallets[1].seed)
- await extension.paste()
- await extension.navigation.continue.click()
-
- await extension.wallet.password.fill(config.password)
- await extension.wallet.repeatPassword.fill(config.password)
-
- await extension.navigation.continue.click()
-
- await expect(extension.wallet.finish.first()).toBeVisible({
- timeout: 180000,
+ const { seed } = await extension.setupWallet({
+ accountsToSetup: [
+ {
+ initialBalance: 0,
+ },
+ ],
})
- await extension.open()
- await expect(extension.network.networkSelector).toBeVisible()
+ await extension.resetExtension()
+ await extension.recoverWallet(seed)
await expect(extension.account.dappsBanner).toBeVisible()
})
})
diff --git a/packages/e2e/extension/src/specs/network.spec.ts b/packages/e2e/extension/src/specs/network.spec.ts
index 73ce362cd..461625ac8 100644
--- a/packages/e2e/extension/src/specs/network.spec.ts
+++ b/packages/e2e/extension/src/specs/network.spec.ts
@@ -8,9 +8,8 @@ test.describe("Network", () => {
await extension.open()
await expect(extension.network.networkSelector).toBeVisible()
await extension.network.ensureAvailableNetworks([
- "Mainnet\nhttps://alpha-mainnet.starknet.io",
- "Testnet\nhttps://alpha4.starknet.io",
- "Testnet 2\nhttps://alpha4-2.starknet.io",
+ "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",
])
})
@@ -28,7 +27,9 @@ 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.baseUrl.fill("https://alpha4.starknet.io")
+ await extension.developerSettings.sequencerUrl.fill(
+ "https://alpha4.starknet.io",
+ )
await extension.navigation.create.click()
await expect(
@@ -80,7 +81,9 @@ 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.baseUrl.fill("https://alpha4.starknet.io")
+ await extension.developerSettings.sequencerUrl.fill(
+ "https://alpha4.starknet.io",
+ )
await extension.navigation.create.click()
await expect(
@@ -92,7 +95,7 @@ test.describe("Network", () => {
// add account
await extension.network.selectNetwork("My Network")
- await extension.account.addAccount({})
+ await extension.account.addAccount({ firstAccount: true })
await extension.network.selectNetwork("Testnet")
// try to restore networks
diff --git a/packages/e2e/extension/src/specs/receiveFunds.spec.ts b/packages/e2e/extension/src/specs/receiveFunds.spec.ts
deleted file mode 100644
index 928314a0e..000000000
--- a/packages/e2e/extension/src/specs/receiveFunds.spec.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { expect } from "@playwright/test"
-
-import test from "../test"
-
-test.describe("Receive funds", () => {
- test("Account balance should be updated after receiving funds", async ({
- extension,
- secondExtension,
- }) => {
- //setup wallet 1
- await extension.wallet.newWalletOnboarding()
- await extension.open()
- await expect(extension.network.networkSelector).toBeVisible()
- await extension.network.selectNetwork("Localhost 5050")
- const [accountName1, accountAddress1] = await extension.account.addAccount(
- {},
- )
-
- if (!accountName1 || !accountAddress1) {
- throw new Error("Invalid account info")
- }
- await extension.account.ensureAsset(accountName1, "ETH", "1.0")
-
- //setup wallet 2
- await secondExtension.wallet.newWalletOnboarding()
- await secondExtension.open()
- await expect(secondExtension.network.networkSelector).toBeVisible()
- await secondExtension.network.selectNetwork("Localhost 5050")
- const [accountName2, accountAddress2] =
- await secondExtension.account.addAccount({})
-
- if (!accountName2 || !accountAddress2) {
- throw new Error("Invalid account info")
- }
- await secondExtension.account.ensureAsset(accountName1, "ETH", "1.0")
- await extension.account.transfer({
- originAccountName: accountName1,
- recepientAddress: accountAddress2,
- tokenName: "Ethereum",
- amount: 0.5,
- })
- await extension.activity.checkActivity(1)
- await extension.navigation.menuTokens.click()
- await expect(
- extension.navigation.menuPendingTransationsIndicator,
- ).not.toBeVisible()
- await expect(extension.account.currentBalance("ETH")).toContainText(
- "0.4988",
- )
- await extension.account.token("Ethereum").click()
- await expect(extension.account.balance).toContainText("0.4988")
-
- await secondExtension.account.token("Ethereum").click()
- await secondExtension.account.back.click()
- await expect(secondExtension.account.currentBalance("ETH")).toContainText(
- "1.5",
- )
- })
-})
diff --git a/packages/e2e/extension/src/specs/recovery.spec.ts b/packages/e2e/extension/src/specs/recovery.spec.ts
index 8bd31f0fe..feead1738 100644
--- a/packages/e2e/extension/src/specs/recovery.spec.ts
+++ b/packages/e2e/extension/src/specs/recovery.spec.ts
@@ -7,22 +7,17 @@ test.describe("Recovery Wallet", () => {
test("User should be able to recover wallet using seed phrase", async ({
extension,
}) => {
- await extension.open()
- await extension.wallet.restoreExistingWallet.click()
- await extension.setClipBoardContent(config.wallets[1].seed)
- await extension.paste()
- await extension.navigation.continue.click()
-
- await extension.wallet.password.fill(config.password)
- await extension.wallet.repeatPassword.fill(config.password)
-
- await extension.navigation.continue.click()
- await expect(extension.wallet.finish.first()).toBeVisible({
- timeout: 180000,
+ const { seed } = await extension.setupWallet({
+ accountsToSetup: [
+ {
+ initialBalance: 0.0005,
+ deploy: true,
+ },
+ ],
})
- await extension.open()
- await expect(extension.network.networkSelector).toBeVisible()
+ await extension.resetExtension()
+ await extension.recoverWallet(seed)
})
test("Set up account recovery banner should not be visible after user copy phrase", async ({
@@ -31,10 +26,12 @@ test.describe("Recovery Wallet", () => {
await extension.wallet.newWalletOnboarding()
await extension.open()
await expect(extension.network.networkSelector).toBeVisible()
- await extension.account.setUpAccountRecovery.click()
- await extension.account.saveTheRecoveryPhrase.click()
- await extension.navigation.continue.click()
- await extension.navigation.yes.click()
+ await extension.network.selectNetwork("Mainnet")
+ await extension.account.addAccountMainnet({ firstAccount: true })
+ await expect(extension.account.showAccountRecovery).toBeVisible()
+ await extension.account.showAccountRecovery.click()
+ await extension.account.confirmTheSeedPhrase.click()
+ await extension.navigation.done.click()
await expect(extension.account.setUpAccountRecovery).toBeHidden()
})
@@ -42,25 +39,40 @@ test.describe("Recovery Wallet", () => {
extension,
}) => {
await extension.open()
- await extension.wallet.restoreExistingWallet.click()
- await extension.setClipBoardContent(config.wallets[2].seed)
- await extension.paste()
- await extension.navigation.continue.click()
-
- await extension.wallet.password.fill(config.password)
- await extension.wallet.repeatPassword.fill(config.password)
+ await extension.recoverWallet(config.testNetSeed1!)
+ await expect(extension.network.networkSelector).toBeVisible()
+ await extension.network.selectNetwork("Testnet")
+ await extension.account.selectAccount("Account 33")
+ await expect(extension.account.currentBalance("Ethereum")).toContainText(
+ "0.0000097 ETH",
+ )
+ })
- await extension.navigation.continue.click()
- await expect(extension.wallet.finish.first()).toBeVisible({
- timeout: 180000,
- })
+ test("Copy phrase from account view when creating new wallet", async ({
+ extension,
+ }) => {
+ await extension.wallet.newWalletOnboarding()
+ await extension.open()
+ await expect(extension.network.networkSelector).toBeVisible()
+ await extension.account.accountAddressFromAssetsView.click()
+ await extension.account.saveRecoveryPhrase()
+ await extension.account.copyAddress.click()
+ const accountAddress = await extension.getClipboard()
+ expect(accountAddress).toMatch(/^0x0/)
+ })
+ test("Save your recovery phrase banner", async ({ extension }) => {
+ await extension.wallet.newWalletOnboarding()
await extension.open()
await expect(extension.network.networkSelector).toBeVisible()
- await extension.network.selectNetwork("Localhost 5050")
- await extension.account.selectAccount("Account 32")
- await expect(extension.account.currentBalance("ETH")).toContainText(
- "0.9991 ETH",
- )
+ await extension.network.selectNetwork("Mainnet")
+ await extension.account.addAccountMainnet({ firstAccount: true })
+
+ await extension.account.showAccountRecovery.click()
+ await extension.account.saveRecoveryPhrase()
+ await extension.account.copyAddress.click()
+ const accountAddress = await extension.getClipboard()
+ expect(accountAddress).toMatch(/^0x0/)
+ await expect(extension.account.showAccountRecovery).toBeHidden()
})
})
diff --git a/packages/e2e/extension/src/specs/sendFunds.spec.ts b/packages/e2e/extension/src/specs/sendFunds.spec.ts
new file mode 100644
index 000000000..4b49ab77d
--- /dev/null
+++ b/packages/e2e/extension/src/specs/sendFunds.spec.ts
@@ -0,0 +1,141 @@
+import { expect } from "@playwright/test"
+
+import config from "../config"
+import test from "../test"
+
+test.describe("Send funds", () => {
+ test("send MAX funds to other self account", async ({ extension }) => {
+ const { accountAddresses } = await extension.setupWallet({
+ accountsToSetup: [{ initialBalance: 0.01 }, { initialBalance: 0 }],
+ })
+ await extension.account.transfer({
+ originAccountName: extension.account.accountName1,
+ recipientAddress: accountAddresses[1],
+ tokenName: "Ethereum",
+ amount: "MAX",
+ submit: false,
+ })
+
+ await extension.validateTx(accountAddresses[1])
+ await extension.navigation.menuTokens.click()
+
+ //ensure that balance is updated
+ await expect(extension.account.currentBalance("Ethereum")).toContainText(
+ "0.00",
+ )
+ let balance = await extension.account.currentBalance("Ethereum").innerText()
+ expect(parseFloat(balance)).toBeLessThan(0.01)
+
+ await extension.account.ensureSelectedAccount(
+ extension.account.accountName2,
+ )
+ await expect(extension.account.currentBalance("Ethereum")).toContainText(
+ "0.01",
+ )
+ balance = await extension.account.currentBalance("Ethereum").innerText()
+ expect(parseFloat(balance)).toBeGreaterThan(0.0001)
+ })
+
+ test("send MAX funds to other wallet/account", async ({ extension }) => {
+ await extension.setupWallet({
+ accountsToSetup: [{ initialBalance: 0.002 }],
+ })
+
+ await extension.account.transfer({
+ originAccountName: extension.account.accountName1,
+ recipientAddress: config.destinationAddress!,
+ tokenName: "Ethereum",
+ amount: "MAX",
+ submit: false,
+ })
+
+ await extension.validateTx(config.destinationAddress!)
+ await extension.navigation.menuTokens.click()
+
+ //ensure that balance is updated
+ await expect(extension.account.currentBalance("Ethereum")).toContainText(
+ "0.000",
+ )
+ const balance = await extension.account
+ .currentBalance("Ethereum")
+ .innerText()
+ expect(parseFloat(balance)).toBeLessThan(0.002)
+ })
+
+ test("send partial funds to other self account", async ({ extension }) => {
+ const { accountAddresses } = await extension.setupWallet({
+ accountsToSetup: [{ initialBalance: 0.01 }, { initialBalance: 0 }],
+ })
+ await extension.account.transfer({
+ originAccountName: extension.account.accountName1,
+ recipientAddress: accountAddresses[1],
+ tokenName: "Ethereum",
+ amount: 0.005,
+ submit: false,
+ })
+ await extension.validateTx(accountAddresses[1])
+ await extension.navigation.menuTokens.click()
+
+ //ensure that balance is updated
+ await expect(extension.account.currentBalance("Ethereum")).toContainText(
+ "0.00",
+ )
+ const balance = await extension.account
+ .currentBalance("Ethereum")
+ .innerText()
+ expect(parseFloat(balance)).toBeLessThan(0.01)
+
+ await extension.account.ensureSelectedAccount(
+ extension.account.accountName2,
+ )
+ await expect(extension.account.currentBalance("Ethereum")).toContainText(
+ "0.005",
+ )
+ })
+
+ test("send partial funds to other wallet/account", async ({ extension }) => {
+ await extension.setupWallet({ accountsToSetup: [{ initialBalance: 0.01 }] })
+ await extension.account.transfer({
+ originAccountName: extension.account.accountName1,
+ recipientAddress: config.destinationAddress!,
+ tokenName: "Ethereum",
+ amount: 0.005,
+ fillRecipientAddress: "typing",
+ submit: false,
+ })
+ await extension.validateTx(config.destinationAddress!)
+ await extension.navigation.menuTokens.click()
+
+ //ensure that balance is updated
+ await expect(extension.account.currentBalance("Ethereum")).toContainText(
+ "0.00",
+ )
+ const balance = await extension.account
+ .currentBalance("Ethereum")
+ .innerText()
+ expect(parseFloat(balance)).toBeLessThan(0.01)
+
+ //send back remaining funds
+ await extension.account.transfer({
+ originAccountName: extension.account.accountName1,
+ recipientAddress: config.destinationAddress!,
+ tokenName: "Ethereum",
+ amount: "MAX",
+ })
+ })
+
+ test("User should be able to send funds to starknet id", async ({
+ extension,
+ }) => {
+ await extension.setupWallet({ accountsToSetup: [{ initialBalance: 0.01 }] })
+
+ await extension.account.transfer({
+ originAccountName: extension.account.accountName1,
+ recipientAddress: "e2e-test.stark",
+ tokenName: "Ethereum",
+ amount: "MAX",
+ submit: false,
+ })
+ await extension.validateTx(config.account1Seed3!)
+ })
+})
diff --git a/packages/e2e/extension/src/specs/sendFundsMax.spec.ts b/packages/e2e/extension/src/specs/sendFundsMax.spec.ts
deleted file mode 100644
index 7e4eca6d9..000000000
--- a/packages/e2e/extension/src/specs/sendFundsMax.spec.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import { expect } from "@playwright/test"
-
-import test from "../test"
-
-test.describe("Send MAX funds", () => {
- const otherAccount =
- "0x02c786C7b4708b476a3a7c012922e6C3a161096F71EC694D61b590dbD4051Faf"
- const setupWallet = async (extension: any) => {
- await extension.wallet.newWalletOnboarding()
- await extension.open()
- await expect(extension.network.networkSelector).toBeVisible()
- await extension.network.selectNetwork("Localhost 5050")
- const [accountName1, accountAddress1] = await extension.account.addAccount(
- {},
- )
- const [accountName2, accountAddress2] = await extension.account.addAccount({
- firstAccount: false,
- })
- if (!accountName1 || !accountName2 || !accountAddress2) {
- throw new Error("Invalid account names")
- }
- await extension.account.ensureAsset(accountName1, "ETH", "1.0")
- await extension.account.ensureAsset(accountName2, "ETH", "1.0")
-
- return { accountName1, accountAddress1, accountName2, accountAddress2 }
- }
-
- test("send MAX funds to other self account", async ({ extension }) => {
- const { accountName1, accountName2, accountAddress2 } = await setupWallet(
- extension,
- )
- await extension.account.transfer({
- originAccountName: accountName1,
- recepientAddress: accountAddress2,
- tokenName: "Ethereum",
- amount: "MAX",
- })
- await extension.activity.checkActivity(1)
- await extension.navigation.menuTokens.click()
- await expect(
- extension.navigation.menuPendingTransationsIndicator,
- ).not.toBeVisible()
- await expect(extension.account.currentBalance("ETH")).toContainText(
- "0.0023",
- )
-
- await extension.account.token("Ethereum").click()
- await expect(extension.account.balance).toContainText("0.0023")
- await extension.account.back.click()
- await extension.account.ensureSelectedAccount(accountName2)
- await extension.account.token("Ethereum").click()
- await expect(extension.account.balance).toContainText("1.9965")
- await extension.account.back.click()
- await expect(extension.account.currentBalance("ETH")).toContainText("1.9")
- })
-
- test("send MAX funds to other wallet/account", async ({ extension }) => {
- const { accountName1 } = await setupWallet(extension)
-
- await extension.account.transfer({
- originAccountName: accountName1,
- recepientAddress: otherAccount,
- tokenName: "Ethereum",
- amount: "MAX",
- })
- await extension.activity.checkActivity(1)
- await extension.navigation.menuTokens.click()
- await expect(
- extension.navigation.menuPendingTransationsIndicator,
- ).not.toBeVisible()
- await expect(extension.account.currentBalance("ETH")).toContainText(
- "0.0023",
- )
-
- await extension.account.token("Ethereum").click()
- await expect(extension.account.balance).toContainText("0.0023")
- })
-})
diff --git a/packages/e2e/extension/src/specs/sendFundsPartial.spec.ts b/packages/e2e/extension/src/specs/sendFundsPartial.spec.ts
deleted file mode 100644
index 855986b07..000000000
--- a/packages/e2e/extension/src/specs/sendFundsPartial.spec.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import { expect } from "@playwright/test"
-
-import test from "../test"
-
-test.describe("Send partial funds", () => {
- const otherAccount =
- "0x02c786C7b4708b476a3a7c012922e6C3a161096F71EC694D61b590dbD4051Faf"
- const setupWallet = async (extension: any) => {
- await extension.wallet.newWalletOnboarding()
- await extension.open()
- await expect(extension.network.networkSelector).toBeVisible()
- await extension.network.selectNetwork("Localhost 5050")
- const [accountName1, accountAddress1] = await extension.account.addAccount(
- {},
- )
- const [accountName2, accountAddress2] = await extension.account.addAccount({
- firstAccount: false,
- })
- if (!accountName1 || !accountName2 || !accountAddress2) {
- throw new Error("Invalid account names")
- }
- await extension.account.ensureAsset(accountName1, "ETH", "1.0")
- await extension.account.ensureAsset(accountName2, "ETH", "1.0")
-
- return { accountName1, accountAddress1, accountName2, accountAddress2 }
- }
-
- test("send partial funds to other self account", async ({ extension }) => {
- const { accountName1, accountName2, accountAddress2 } = await setupWallet(
- extension,
- )
- await extension.account.transfer({
- originAccountName: accountName1,
- recepientAddress: accountAddress2,
- tokenName: "Ethereum",
- amount: 0.5,
- })
- await extension.activity.checkActivity(1)
- await extension.navigation.menuTokens.click()
- await expect(
- extension.navigation.menuPendingTransationsIndicator,
- ).not.toBeVisible()
-
- await expect(extension.account.currentBalance("ETH")).toContainText(
- "0.4988",
- )
- await extension.account.token("Ethereum").click()
- await expect(extension.account.balance).toContainText("0.4988")
- await extension.account.back.click()
- await extension.account.ensureSelectedAccount(accountName2)
- await extension.account.token("Ethereum").click()
- await expect(extension.account.balance).toContainText("1.5")
- await extension.account.back.click()
- await expect(extension.account.currentBalance("ETH")).toContainText("1.5")
- })
-
- test("send partial funds to other wallet/account", async ({ extension }) => {
- const { accountName1 } = await setupWallet(extension)
- await extension.account.ensureAsset(accountName1, "ETH", "1.0")
- await extension.account.transfer({
- originAccountName: accountName1,
- recepientAddress: otherAccount,
- tokenName: "Ethereum",
- amount: 0.5,
- })
- await extension.activity.checkActivity(1)
- await extension.navigation.menuTokens.click()
- await expect(
- extension.navigation.menuPendingTransationsIndicator,
- ).not.toBeVisible()
- await expect(extension.account.currentBalance("ETH")).toContainText(
- "0.4988",
- )
- await extension.account.token("Ethereum").click()
- await expect(extension.account.balance).toContainText("0.4988")
- })
-})
diff --git a/packages/e2e/extension/src/specs/welcome.spec.ts b/packages/e2e/extension/src/specs/welcome.spec.ts
index de7ac03cf..8a3281582 100644
--- a/packages/e2e/extension/src/specs/welcome.spec.ts
+++ b/packages/e2e/extension/src/specs/welcome.spec.ts
@@ -36,5 +36,8 @@ test.describe("Welcome screen", () => {
await extension.wallet.newWalletOnboarding()
await extension.open()
await expect(extension.network.networkSelector).toBeVisible()
+ await expect(extension.network.networkSelector).toHaveText(
+ extension.network.getDefaultNetworkName(),
+ )
})
})
diff --git a/packages/e2e/extension/src/test.ts b/packages/e2e/extension/src/test.ts
index 979454025..8d511ce9b 100644
--- a/packages/e2e/extension/src/test.ts
+++ b/packages/e2e/extension/src/test.ts
@@ -1,6 +1,8 @@
import * as fs from "fs"
import path from "path"
+import dotenv from "dotenv"
+dotenv.config()
import {
ChromiumBrowserContext,
Page,
@@ -8,13 +10,15 @@ import {
chromium,
test as testBase,
} from "@playwright/test"
+import { v4 as uuid } from "uuid"
import config from "./config"
import type { TestExtensions } from "./fixtures"
import ExtensionPage from "./page-objects/ExtensionPage"
+const isCI = Boolean(process.env.CI)
const isExtensionURL = (url: string) => url.startsWith("chrome-extension://")
-
+let browserCtx: ChromiumBrowserContext
const closePages = async (browserContext: ChromiumBrowserContext) => {
const pages = browserContext?.pages() || []
for (const page of pages) {
@@ -54,17 +58,18 @@ const keepArtifacts = async (testInfo: TestInfo, page: Page) => {
}
}
}
-const initBrowserWithExtension = async (testInfo: TestInfo) => {
- const userDataDir = `/tmp/test-user-data-${Math.round(
- new Date().getTime() / 1000,
- )}-${(Math.random() + 1).toString(36).substring(7)}-${testInfo.workerIndex}`
- const browserContext = (await chromium.launchPersistentContext(userDataDir, {
+
+const createBrowserContext = () => {
+ const userDataDir = `/tmp/test-user-data-${uuid()}`
+ return chromium.launchPersistentContext(userDataDir, {
headless: false,
args: [
+ `${isCI ? "--headless=new" : ""}`,
"--disable-dev-shm-usage",
"--ipc=host",
`--disable-extensions-except=${config.distDir}`,
`--load-extension=${config.distDir}`,
+ "--disable-gp",
],
recordVideo: {
dir: config.artifactsDir,
@@ -73,8 +78,11 @@ const initBrowserWithExtension = async (testInfo: TestInfo) => {
height: 600,
},
},
- })) as ChromiumBrowserContext
+ })
+}
+const initBrowserWithExtension = async (testInfo: TestInfo) => {
+ const browserContext = await createBrowserContext()
// save video
browserContext.on("page", async (page) => {
page.on("load", async (page) => {
@@ -156,14 +164,23 @@ function createExtension() {
const extension = new ExtensionPage(page, extensionURL)
await keepArtifacts(testInfo, page)
await closePages(browserContext)
+ browserCtx = browserContext
await use(extension)
await browserContext.close()
}
}
+
+function getContext() {
+ return async ({}, use: any, _testInfo: TestInfo) => {
+ await use(browserCtx)
+ }
+}
+
let pageId = 0
const test = testBase.extend({
extension: createExtension(),
secondExtension: createExtension(),
+ browserContext: getContext(),
})
export default test
diff --git a/packages/e2e/extension/src/utils/account.ts b/packages/e2e/extension/src/utils/account.ts
new file mode 100644
index 000000000..9c805ea9f
--- /dev/null
+++ b/packages/e2e/extension/src/utils/account.ts
@@ -0,0 +1,102 @@
+import { Account, SequencerProvider, constants, uint256 } from "starknet"
+import { bigDecimal } from "@argent/shared"
+import { Multicall } from "@argent/x-multicall"
+import config from "../config"
+
+export interface AccountsToSetup {
+ initialBalance: number
+ deploy?: boolean
+}
+
+const provider = new SequencerProvider({
+ network: constants.NetworkName.SN_GOERLI,
+})
+const tnkETH =
+ "0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7" // address of ETH
+const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
+
+const maxRetries = 4
+
+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) {
+ const account = new Account(
+ provider,
+ config.senderAddr!,
+ config.senderKey!,
+ "1",
+ )
+ const initialBalance = await balanceEther(account.address)
+ const initialBalanceFormatted = parseFloat(initialBalance) * Math.pow(10, 18)
+ const { low, high } = uint256.bnToUint256(amount)
+
+ if (initialBalanceFormatted < parseInt(amount)) {
+ throw `Failed to tranfer: Not enought balance ${initialBalanceFormatted} < ${amount}`
+ }
+ let placeTXAttempt = 0
+ while (placeTXAttempt < maxRetries) {
+ try {
+ placeTXAttempt++
+ const tx = await account.execute({
+ contractAddress: tnkETH,
+ entrypoint: "transfer",
+ calldata: [to, low, high],
+ })
+ let failed = true
+ let txSuccessAttempt = 0
+ let txStatusResponse
+ while (failed && txSuccessAttempt < maxRetries) {
+ txSuccessAttempt++
+ const txStatus = await getTransaction(tx.transaction_hash)
+ txStatusResponse = await txStatus.json()
+ if (txStatusResponse.execution_status === "REJECTED") {
+ console.error(
+ `Failed to place TX: ${config.starkscanTestNetUrl}/tx/${tx.transaction_hash}, execution_status: ${txStatusResponse.execution_status}, status: ${txStatusResponse.status}`,
+ )
+ txSuccessAttempt = maxRetries
+ } else if (
+ txStatusResponse.execution_status !== "SUCCEEDED" &&
+ txStatusResponse.status !== "ACCEPTED_ON_L2"
+ ) {
+ console.log(
+ `TX not processed: hash: ${tx.transaction_hash}, execution_status: ${txStatusResponse.execution_status}, status: ${txStatusResponse.status}`,
+ )
+ await sleep(5000)
+ } else {
+ failed = false
+ }
+ }
+ if (failed) {
+ throw `Failed to place TX: ${config.starkscanTestNetUrl}/tx/${tx.transaction_hash}, execution_status: ${txStatusResponse.execution_status}, status: ${txStatusResponse.status}`
+ }
+
+ console.log(
+ `[Successful TX] ${config.starkscanTestNetUrl}/tx/${tx.transaction_hash}`,
+ )
+ return tx.transaction_hash
+ } catch (e) {
+ //for debug only
+ console.log("Exception: ", e)
+ }
+ console.warn("Transfer failed, going to try again ")
+ }
+}
+
+export async function balanceEther(accountAddress: string) {
+ const balanceOfCall = {
+ contractAddress: tnkETH,
+ entrypoint: "balanceOf",
+ calldata: [accountAddress],
+ }
+
+ const multicall = new Multicall(provider)
+ const response = await multicall.call(balanceOfCall)
+ const [low, high] = response
+ const balance = bigDecimal.formatEther(uint256.uint256ToBN({ low, high }))
+ return balance
+}
diff --git a/packages/e2e/package.json b/packages/e2e/package.json
index c32477a8f..ea85a1d27 100644
--- a/packages/e2e/package.json
+++ b/packages/e2e/package.json
@@ -1,15 +1,21 @@
{
"name": "@argent-x/e2e",
"private": true,
- "version": "6.3.0",
+ "version": "6.3.2",
"main": "index.js",
"license": "MIT",
"devDependencies": {
- "@playwright/test": "^1.32.0",
- "@types/node": "^18.15.11"
+ "@argent/shared": "workspace:^",
+ "@argent/x-multicall": "workspace:^",
+ "@playwright/test": "^1.37.1",
+ "@types/node": "^20.5.7",
+ "@types/uuid": "^9.0.3",
+ "dotenv": "^16.3.1",
+ "starknet": "5.18.0",
+ "uuid": "^9.0.0"
},
"scripts": {
- "test:e2e:extension": "playwright test ./extension",
- "test:e2e": "yarn run test:e2e:extension"
+ "test:extension": "pnpm playwright test --config=./extension",
+ "test:webwallet": "pnpm playwright test --config=./webwallet"
}
}
diff --git a/packages/e2e/playwright.config.ts b/packages/e2e/playwright.config.ts
deleted file mode 100644
index 220201962..000000000
--- a/packages/e2e/playwright.config.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import path from "path"
-
-import type { PlaywrightTestConfig } from "@playwright/test"
-
-import config from "./extension/src/config"
-
-const isCI = Boolean(process.env.CI)
-
-const playwrightConfig: PlaywrightTestConfig = {
- projects: [
- {
- name: "chromium",
- },
- ],
- workers: 1,
- timeout: 5 * 60e3, // 5 minutes
- reportSlowTests: {
- threshold: 1 * 60e3, // 1 minute
- max: 5,
- },
- expect: { timeout: 90 * 1000 }, // 90 seconds
- reporter: isCI
- ? [
- ["github"],
- [
- "json",
- { outputFile: path.join(config.reportsDir, "extension.json") },
- ],
- ["list"],
- [
- "html",
- {
- open: "never",
- outputFolder: path.join(config.reportsDir, "playwright-report"),
- },
- ],
- ]
- : "list",
- forbidOnly: isCI,
- testDir: "./extension/src/specs",
- testMatch: /\.spec.ts$/,
- retries: isCI ? 2 : 0,
- use: {
- trace: "on-first-retry",
- viewport: { width: 360, height: 600 },
- actionTimeout: 60 * 1000, // 1 minute
- permissions: ["clipboard-read", "clipboard-write"],
- },
- outputDir: config.artifactsDir,
- preserveOutput: isCI ? "failures-only" : "never",
-}
-
-export default playwrightConfig
diff --git a/packages/e2e/tsconfig.json b/packages/e2e/tsconfig.json
index 826af4096..6a764dd01 100644
--- a/packages/e2e/tsconfig.json
+++ b/packages/e2e/tsconfig.json
@@ -1,8 +1,8 @@
{
"compilerOptions": {
"target": "Esnext",
+ "module": "ESNext",
"moduleResolution": "node",
- "module": "commonjs",
"allowSyntheticDefaultImports": true,
"strict": true,
"noImplicitAny": true,
@@ -13,7 +13,8 @@
"inlineSources": true,
"inlineSourceMap": true,
"composite": true,
- "types": ["node"]
+ "types": ["node"],
+ "noEmit": true
},
"include": ["**/src"],
"exclude": ["node_modules"]
diff --git a/packages/e2e/webwallet/playwright.config.ts b/packages/e2e/webwallet/playwright.config.ts
new file mode 100644
index 000000000..d4d2d962c
--- /dev/null
+++ b/packages/e2e/webwallet/playwright.config.ts
@@ -0,0 +1,44 @@
+import type { PlaywrightTestConfig } from "@playwright/test"
+import config from "./src/config"
+
+const isCI = Boolean(process.env.CI)
+
+const playwrightConfig: PlaywrightTestConfig = {
+ projects: [
+ {
+ name: "WebWallet - Chrome",
+ use: {
+ browserName: "chromium",
+ },
+ },
+ {
+ name: "WebWallet - Firefox",
+ use: {
+ browserName: "firefox",
+ },
+ },
+ {
+ name: "WebWallet - WebKit",
+ use: {
+ browserName: "webkit",
+ },
+ },
+ ],
+ expect: {
+ timeout: 20 * 1000, // 20 seconds
+ },
+ timeout: 1 * 60e3, // 1 minutes
+ retries: isCI ? 2 : 0,
+ workers: 1,
+ reportSlowTests: {
+ threshold: 2 * 60e3, // 2 minute
+ max: 5,
+ },
+ reporter: isCI ? [["github"], ["blob"]] : "list",
+
+ forbidOnly: isCI,
+ outputDir: config.artifactsDir,
+ preserveOutput: isCI ? "failures-only" : "never",
+}
+
+export default playwrightConfig
diff --git a/packages/e2e/webwallet/src/config.ts b/packages/e2e/webwallet/src/config.ts
new file mode 100644
index 000000000..78ace2245
--- /dev/null
+++ b/packages/e2e/webwallet/src/config.ts
@@ -0,0 +1,12 @@
+import path from "path"
+
+export default {
+ validLogin: {
+ email: "testuser@mail.com",
+ pin: "1111111",
+ password: "myNewPass12!",
+ },
+ url: "http://localhost:3005",
+ artifactsDir: path.resolve(__dirname, "../../artifacts/playwright"),
+ reportsDir: path.resolve(__dirname, "../../artifacts/reports"),
+}
diff --git a/packages/e2e/webwallet/src/fixtures.ts b/packages/e2e/webwallet/src/fixtures.ts
new file mode 100644
index 000000000..6a7f1c148
--- /dev/null
+++ b/packages/e2e/webwallet/src/fixtures.ts
@@ -0,0 +1,5 @@
+import type WebWalletPage from "./page-objects/WebWalletPage"
+
+export interface TestPages {
+ webWallet: WebWalletPage
+}
diff --git a/packages/e2e/webwallet/src/page-objects/Login.ts b/packages/e2e/webwallet/src/page-objects/Login.ts
new file mode 100644
index 000000000..b2f0ae1df
--- /dev/null
+++ b/packages/e2e/webwallet/src/page-objects/Login.ts
@@ -0,0 +1,66 @@
+import { Page, expect } from "@playwright/test"
+
+import config from "../config"
+import Navigation from "./Navigation"
+
+interface ICredentials {
+ email: string
+ pin: string
+ password: string
+}
+
+export default class Login extends Navigation {
+ constructor(page: Page) {
+ super(page)
+ }
+
+ get email() {
+ return this.page.locator("input[name=email]")
+ }
+
+ get pinInput() {
+ return this.page.locator('[id^="pin-input"]')
+ }
+
+ get password() {
+ return this.page.locator("input[name=password]")
+ }
+
+ get wrongPassword() {
+ return this.page.locator(
+ '//input[@name="password"][@aria-invalid="true"]/following::label[contains(text(), "Wrong password")]',
+ )
+ }
+
+ get forgetPassword() {
+ return this.page.locator(
+ '//a[@href="/password"]//label[contains(text(), "Forgotten your password?")]',
+ )
+ }
+
+ get differentAccount() {
+ return this.page.locator('p:text-is("Use a different account")')
+ }
+
+ async fillPin(pin: string) {
+ await Promise.all([
+ this.page.waitForURL(`${config.url}/pin`),
+ this.continue.click(),
+ ])
+ await this.pinInput.first().click()
+ await this.pinInput.first().fill(pin)
+ }
+
+ async success(credentials: ICredentials = config.validLogin) {
+ await this.email.fill(credentials.email)
+ await this.fillPin(credentials.pin)
+ await this.password.fill(credentials.password)
+ await expect(this.forgetPassword).toBeVisible()
+ await expect(this.differentAccount).toBeVisible()
+ await Promise.all([
+ this.page.waitForURL(`${config.url}/dashboard`),
+ this.continue.click(),
+ ])
+ await expect(this.lock).toBeVisible()
+ }
+}
diff --git a/packages/e2e/webwallet/src/page-objects/Navigation.ts b/packages/e2e/webwallet/src/page-objects/Navigation.ts
new file mode 100644
index 000000000..3937ea355
--- /dev/null
+++ b/packages/e2e/webwallet/src/page-objects/Navigation.ts
@@ -0,0 +1,40 @@
+import type { Page } from "@playwright/test"
+
+export default class Navigation {
+ page: Page
+ constructor(page: Page) {
+ this.page = page
+ }
+
+ get continue() {
+ return this.page.locator(`button:text-is("Continue")`)
+ }
+
+ get assets() {
+ return this.page.getByRole("link", { name: "Assets" })
+ }
+
+ get addFunds() {
+ return this.page.getByRole("link", { name: "Add funds" })
+ }
+
+ get send() {
+ return this.page.getByRole("link", { name: "Send" })
+ }
+
+ get authorizedDapps() {
+ return this.page.getByRole("link", { name: "Authorized dapps" })
+ }
+
+ get changePassword() {
+ return this.page.getByRole("link", { name: "Change password" })
+ }
+
+ get lock() {
+ return this.page.getByRole("heading", { name: "Lock" })
+ }
+
+ get switchTheme() {
+ return this.page.getByRole("button", { name: "Switch theme" })
+ }
+}
diff --git a/packages/e2e/webwallet/src/page-objects/WebWalletPage.ts b/packages/e2e/webwallet/src/page-objects/WebWalletPage.ts
new file mode 100644
index 000000000..3ddf03b3b
--- /dev/null
+++ b/packages/e2e/webwallet/src/page-objects/WebWalletPage.ts
@@ -0,0 +1,21 @@
+import type { Page } from "@playwright/test"
+
+import config from "../config"
+import Login from "./Login"
+import Navigation from "./Navigation"
+
+export default class WebWalletPage {
+ page: Page
+ login: Login
+ navigation: Navigation
+
+ constructor(page: Page) {
+ this.page = page
+ this.login = new Login(page)
+ this.navigation = new Navigation(page)
+ }
+
+ open() {
+ return this.page.goto(config.url)
+ }
+}
diff --git a/packages/e2e/webwallet/src/specs/login.spec.ts b/packages/e2e/webwallet/src/specs/login.spec.ts
new file mode 100644
index 000000000..d994d05a9
--- /dev/null
+++ b/packages/e2e/webwallet/src/specs/login.spec.ts
@@ -0,0 +1,18 @@
+import { expect } from "@playwright/test"
+
+import config from "../config"
+import test from "../test"
+
+test.describe(`Login page`, () => {
+ test.skip("can log in", async ({ webWallet }) => {
+ await webWallet.login.success()
+ })
+
+ test.skip("wrong password", async ({ webWallet }) => {
+ await webWallet.login.email.fill(config.validLogin.email)
+ await webWallet.login.fillPin(config.validLogin.pin)
+ await webWallet.login.password.fill("VeryFake123!")
+ await webWallet.login.continue.click()
+ await expect(webWallet.login.wrongPassword).toBeVisible()
+ })
+})
diff --git a/packages/e2e/webwallet/src/test.ts b/packages/e2e/webwallet/src/test.ts
new file mode 100644
index 000000000..b6753f254
--- /dev/null
+++ b/packages/e2e/webwallet/src/test.ts
@@ -0,0 +1,135 @@
+import * as fs from "fs"
+import path from "path"
+
+import { Browser, Page, TestInfo, test as testBase } from "@playwright/test"
+
+import config from "./config"
+import { TestPages } from "./fixtures"
+import WebWalletPage from "./page-objects/WebWalletPage"
+
+const keepArtifacts = async (testInfo: TestInfo, page: Page) => {
+ if (
+ testInfo.config.preserveOutput === "always" ||
+ (testInfo.config.preserveOutput === "failures-only" &&
+ testInfo.status !== "passed")
+ ) {
+ //save HTML
+ const folder = testInfo.title.replace(/\s+/g, "_").replace(/\W/g, "")
+ const filename = `${testInfo.retry}-${testInfo.status}-${pageId}-${testInfo.workerIndex}.html`
+ try {
+ const htmlContent = await page.content()
+ await fs.promises
+ .mkdir(path.resolve(config.artifactsDir, folder), { recursive: true })
+ .catch((error) => {
+ console.error(error)
+ })
+ await fs.promises
+ .writeFile(
+ path.resolve(config.artifactsDir, folder, filename),
+ htmlContent,
+ )
+ .catch((error) => {
+ console.error(error)
+ })
+ } catch (error) {
+ console.error("Error while saving HTML content", error)
+ }
+ }
+}
+let pageId = 0
+
+async function createContext({
+ browser,
+ baseURL,
+ name,
+ testInfo,
+}: {
+ browser: Browser
+ baseURL: string
+ name: string
+ testInfo: TestInfo
+}) {
+ const context = await browser.newContext({
+ ignoreHTTPSErrors: true,
+ acceptDownloads: true,
+ recordVideo: process.env.CI
+ ? {
+ dir: config.artifactsDir,
+ size: {
+ width: 1366,
+ height: 768,
+ },
+ }
+ : undefined,
+ baseURL,
+ viewport: { width: 1366, height: 768 },
+ })
+ context.on("page", async (page) => {
+ page.on("load", async (page) => {
+ try {
+ await page.title()
+ } catch (err) {
+ console.warn(err)
+ }
+ })
+
+ page.on("close", async (page) => {
+ if (
+ testInfo.config.preserveOutput === "always" ||
+ (testInfo.config.preserveOutput === "failures-only" &&
+ testInfo.status === "failed") ||
+ testInfo.status === "timedOut"
+ ) {
+ const folder = testInfo.title.replace(/\s+/g, "_").replace(/\W/g, "")
+ const filename = `${testInfo.retry}-${name}-${
+ testInfo.status
+ }-${pageId++}-${testInfo.workerIndex}.webm`
+
+ await page
+ .video()
+ ?.saveAs(path.resolve(config.artifactsDir, folder, filename))
+ .catch((error) => {
+ console.error(error)
+ })
+ }
+ page
+ .video()
+ ?.delete()
+ .catch((error) => {
+ console.error(error)
+ })
+ })
+ })
+
+ await context.addInitScript("window.PLAYWRIGHT = true;")
+ return context
+}
+
+function createPage() {
+ return async (
+ { browser }: { browser: Browser },
+ use: any,
+ testInfo: TestInfo,
+ ) => {
+ const url = config.url
+
+ const context = await createContext({
+ browser,
+ testInfo,
+ name: "WebWallet",
+ baseURL: url,
+ })
+ const page = await context.newPage()
+
+ const webWalletPage = new WebWalletPage(page)
+ await webWalletPage.open()
+ await keepArtifacts(testInfo, page)
+ await use(webWalletPage)
+ await context.close()
+ }
+}
+const test = testBase.extend({
+ webWallet: createPage(),
+})
+
+export default test
diff --git a/packages/eslint-plugin-local/.eslintrc.js b/packages/eslint-plugin-local/.eslintrc.js
new file mode 100644
index 000000000..acb0897e1
--- /dev/null
+++ b/packages/eslint-plugin-local/.eslintrc.js
@@ -0,0 +1,31 @@
+module.exports = {
+ env: {
+ node: true,
+ },
+ extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
+ parser: "@typescript-eslint/parser",
+ parserOptions: {
+ ecmaVersion: "latest",
+ sourceType: "module",
+ project: "./tsconfig.json",
+ tsconfigRootDir: __dirname,
+ },
+ ignorePatterns: ["**/dist/**", "**/node_modules/**"],
+ plugins: ["@typescript-eslint"],
+ rules: {
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/no-extra-semi": "off",
+ "@typescript-eslint/no-unused-vars": [
+ "warn",
+ {
+ vars: "all",
+ ignoreRestSiblings: true,
+ argsIgnorePattern: "^_",
+ },
+ ],
+ "@typescript-eslint/no-non-null-assertion": "error",
+ curly: "error",
+ "@typescript-eslint/no-misused-promises": "warn",
+ "@typescript-eslint/no-floating-promises": "warn",
+ },
+}
diff --git a/packages/eslint-plugin-local/package.json b/packages/eslint-plugin-local/package.json
new file mode 100644
index 000000000..ff1f0077a
--- /dev/null
+++ b/packages/eslint-plugin-local/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "@argent/eslint-plugin-local",
+ "version": "6.3.1",
+ "license": "MIT",
+ "private": true,
+ "files": [
+ "dist"
+ ],
+ "main": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "devDependencies": {
+ "@types/eslint": "^8.7.0",
+ "@typescript-eslint/experimental-utils": "^5.59.7",
+ "eslint": "^8.7.0",
+ "minimatch": "^9.0.1",
+ "typescript": "^5.0.4"
+ },
+ "scripts": {
+ "dev": "tsc --build --watch",
+ "build": "tsc --build",
+ "lint": "eslint . --cache --ext .ts",
+ "setup": "yarn build"
+ }
+}
diff --git a/packages/eslint-plugin-local/src/code-import-patterns.ts b/packages/eslint-plugin-local/src/code-import-patterns.ts
new file mode 100644
index 000000000..fb31562ad
--- /dev/null
+++ b/packages/eslint-plugin-local/src/code-import-patterns.ts
@@ -0,0 +1,91 @@
+import * as path from "path"
+
+import type { TSESTree } from "@typescript-eslint/experimental-utils"
+import * as eslint from "eslint"
+import { minimatch } from "minimatch"
+
+const REPO_ROOT = path.resolve(__dirname, "../../../")
+
+interface Option {
+ target: string
+ disallow: string[]
+ message?: string
+}
+
+/**
+ * eslint rule to check imports are allowed against specific patterns
+ *
+ * @example
+ * ```json
+ * "local/code-import-patterns": [
+ * "warn",
+ * {
+ * target: "packages/extension/src/ui/**",
+ * disallow: ["packages/extension/src/background/**"],
+ * message: "import background from ui is disallowed",
+ * },
+ * ]
+ * ```
+ */
+
+const rule: eslint.Rule.RuleModule = {
+ meta: {
+ type: "problem",
+ messages: {
+ badImport: "{{message}}",
+ },
+ docs: {
+ description: "Disallow imports e.g. UI code from background process",
+ recommended: true,
+ },
+ },
+
+ create: (context) => {
+ const options: Option[] = context.options
+ const relativeFilename = path.relative(REPO_ROOT, context.getFilename())
+
+ for (const option of options) {
+ if (minimatch(relativeFilename, option.target)) {
+ return {
+ ImportDeclaration: (declarationNode) => {
+ const node = (declarationNode).source
+ if (node.type === "Literal" && typeof node.value === "string") {
+ checkImport(context, option, node)
+ }
+ },
+ }
+ }
+ }
+
+ return {}
+ },
+}
+
+function checkImport(
+ context: eslint.Rule.RuleContext,
+ option: Option,
+ node: TSESTree.StringLiteral,
+) {
+ const importPath = node.value
+ const sourceFileBase = path.dirname(context.getFilename())
+ const fullImportPath = path.resolve(sourceFileBase, importPath)
+ const rootBasedImportPath = path.relative(REPO_ROOT, fullImportPath)
+
+ for (const pattern of option.disallow) {
+ if (minimatch(rootBasedImportPath, pattern)) {
+ const defaultMessage = `import ${option.disallow.join(" or ")} from ${
+ option.target
+ } is disallowed`
+ context.report({
+ loc: node.loc,
+ messageId: "badImport",
+ data: {
+ message: option.message || defaultMessage,
+ },
+ })
+ return
+ }
+ }
+}
+
+export default rule
diff --git a/packages/eslint-plugin-local/src/index.ts b/packages/eslint-plugin-local/src/index.ts
new file mode 100644
index 000000000..ec2af33a6
--- /dev/null
+++ b/packages/eslint-plugin-local/src/index.ts
@@ -0,0 +1,5 @@
+import codeImportPatterns from "./code-import-patterns"
+
+exports.rules = {
+ "code-import-patterns": codeImportPatterns,
+}
diff --git a/packages/eslint-plugin-local/tsconfig.json b/packages/eslint-plugin-local/tsconfig.json
new file mode 100644
index 000000000..4a755d8d6
--- /dev/null
+++ b/packages/eslint-plugin-local/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "ts-node/node16/tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "declaration": true
+ },
+ "include": ["src", ".eslintrc.js"]
+}
diff --git a/packages/extension/.env.example b/packages/extension/.env.example
index 04c5ff7d3..4c1143a7e 100644
--- a/packages/extension/.env.example
+++ b/packages/extension/.env.example
@@ -2,8 +2,8 @@ SEGMENT_WRITE_KEY=
SENTRY_AUTH_TOKEN=
RAMP_API_KEY=
ARGENT_API_BASE_URL=
-ARGENT_TRANSACTION_REVIEW_API_BASE_URL=
ARGENT_X_STATUS_URL=
+ARGENT_X_ENVIRONMENT=
# 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
@@ -12,10 +12,16 @@ ARGENT_X_STATUS_URL=
#FEATURE_BANXA=
#FEATURE_LAYERSWAP=
#FEATURE_PRIVACY_SETTINGS=
-#ARGENT_EXPLORER_BASE_URL=
#FEATURE_EXPERIMENTAL_SETTINGS=
-#FEATURE_ARGENT_SHIELD=
+#FEATURE_BETA_FEATURES=
#ARGENT_SHIELD_NETWORK_ID=
#FEATURE_VERIFIED_DAPPS=
#FEATURE_MULTISIG=
-#ARGENT_MULTISIG_BASE_URL=
+#FEATURE_NEW_SEND=
+#MULTICALL_MAX_BATCH_SIZE=
+#FEATURE_HIDE_DEPRECATED_ACCOUNTS=
+
+FAST=20
+MEDIUM=60
+SLOW=60*5
+VERY_SLOW=24*60*60
\ No newline at end of file
diff --git a/packages/extension/.eslintrc.base.js b/packages/extension/.eslintrc.base.js
new file mode 100644
index 000000000..00d27c22c
--- /dev/null
+++ b/packages/extension/.eslintrc.base.js
@@ -0,0 +1,55 @@
+module.exports = {
+ settings: {
+ react: {
+ version: "detect",
+ },
+ },
+ env: {
+ browser: true,
+ es2021: true,
+ node: true,
+ },
+ extends: [
+ "eslint:recommended",
+ "plugin:react/recommended",
+ "plugin:@typescript-eslint/recommended",
+ ],
+ parser: "@typescript-eslint/parser",
+ parserOptions: {
+ ecmaFeatures: {
+ jsx: true,
+ },
+ ecmaVersion: "latest",
+ sourceType: "module",
+ project: "./tsconfig.json",
+ tsconfigRootDir: __dirname,
+ },
+ ignorePatterns: [
+ ".eslintrc.base.js",
+ "**/dist/**",
+ "**/node_modules/**",
+ "vite.config.ts",
+ "webpack.config.js",
+ ],
+ plugins: ["react", "react-hooks", "@typescript-eslint"],
+ rules: {
+ "react/jsx-no-target-blank": "off",
+ "react/react-in-jsx-scope": "off",
+ "react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
+ "react-hooks/exhaustive-deps": "warn", // Checks effect dependencies
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/no-extra-semi": "off",
+ "@typescript-eslint/no-unused-vars": [
+ "warn",
+ {
+ vars: "all",
+ ignoreRestSiblings: true,
+ argsIgnorePattern: "^_",
+ },
+ ],
+ "@typescript-eslint/no-non-null-assertion": "error",
+ curly: "error",
+ "@typescript-eslint/no-misused-promises": "warn",
+ "@typescript-eslint/no-floating-promises": "warn",
+ },
+}
diff --git a/packages/extension/.eslintrc.js b/packages/extension/.eslintrc.js
index ff07fe963..5d148b59f 100644
--- a/packages/extension/.eslintrc.js
+++ b/packages/extension/.eslintrc.js
@@ -1,53 +1,7 @@
module.exports = {
- settings: {
- react: {
- version: "detect",
- },
- },
- env: {
- browser: true,
- es2021: true,
- node: true,
- },
- extends: [
- "eslint:recommended",
- "plugin:react/recommended",
- "plugin:@typescript-eslint/recommended",
- ],
- parser: "@typescript-eslint/parser",
- parserOptions: {
- ecmaFeatures: {
- jsx: true,
- },
- ecmaVersion: "latest",
- sourceType: "module",
- project: "./tsconfig.json",
- tsconfigRootDir: __dirname,
- },
- ignorePatterns: [
- "**/dist/**",
- "**/node_modules/**",
- "vite.config.ts",
- "webpack.config.js",
- ],
- plugins: ["react", "react-hooks", "@typescript-eslint"],
+ extends: [".eslintrc.base.js", "prettier"],
+ plugins: ["@argent/eslint-plugin-local"],
rules: {
- "react/jsx-no-target-blank": "off",
- "react/react-in-jsx-scope": "off",
- "react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
- "react-hooks/exhaustive-deps": "warn", // Checks effect dependencies
- "@typescript-eslint/no-explicit-any": "off",
- "@typescript-eslint/no-extra-semi": "off",
- "@typescript-eslint/no-unused-vars": [
- "warn",
- {
- vars: "all",
- ignoreRestSiblings: true,
- argsIgnorePattern: "^_",
- },
- ],
- "@typescript-eslint/no-non-null-assertion": "error",
- curly: "error",
"@typescript-eslint/no-restricted-imports": [
"error",
{
@@ -60,7 +14,26 @@ module.exports = {
],
},
],
- "@typescript-eslint/no-misused-promises": "warn",
- "@typescript-eslint/no-floating-promises": "warn",
+ "@argent/local/code-import-patterns": [
+ "warn",
+ {
+ target: "packages/extension/src/ui/**",
+ disallow: ["packages/extension/src/background/**"],
+ message: "import background from ui is disallowed",
+ },
+ {
+ target: "packages/extension/src/background/**",
+ disallow: ["packages/extension/src/ui/**"],
+ message: "import ui from background is disallowed",
+ },
+ {
+ target: "packages/extension/src/shared/**",
+ disallow: [
+ "packages/extension/src/ui/**",
+ "packages/extension/src/background/**",
+ ],
+ message: "import ui or background from shared is disallowed",
+ },
+ ],
},
}
diff --git a/packages/extension/.vscode/settings.json b/packages/extension/.vscode/settings.json
new file mode 100644
index 000000000..cc43ff1bb
--- /dev/null
+++ b/packages/extension/.vscode/settings.json
@@ -0,0 +1,4 @@
+{
+ "json.schemaDownload.enable": true,
+ "typescript.tsdk": "../../node_modules/typescript/lib"
+}
diff --git a/packages/extension/CHANGELOG.md b/packages/extension/CHANGELOG.md
new file mode 100644
index 000000000..91d0af921
--- /dev/null
+++ b/packages/extension/CHANGELOG.md
@@ -0,0 +1,152 @@
+# @argent-x/extension
+
+## 6.9.0
+
+### Minor Changes
+
+- 281140dc5: Release
+
+### Patch Changes
+
+- Updated dependencies [5cfc58de7]
+ - @argent/x-multicall@6.4.1
+
+## 6.8.6
+
+### Minor Changes
+
+- a636b7a9b: Release
+
+## 6.8.5
+
+### Patch Changes
+
+- 4cd0c97de: hotfix
+
+## 6.8.4
+
+### Patch Changes
+
+- e9cad5187: laggy ui hotfix
+
+## 6.8.3
+
+### Patch Changes
+
+- 965162852: hotfix network view
+
+## 6.8.2
+
+### Patch Changes
+
+- 9c26e2df8: hotfix network update
+
+## 6.8.1
+
+### Patch Changes
+
+- 833909902: Release
+
+## 6.8.0
+
+### Minor Changes
+
+- 5cfe05efd: Release
+
+## 6.7.4
+
+### Patch Changes
+
+- eaf315917: hotfix reenabling tx v0
+
+## 6.7.3
+
+### Patch Changes
+
+- 52b514811: Release
+
+## 6.7.2
+
+### Patch Changes
+
+- 8e3690df8: Release
+
+## 6.7.1
+
+### Patch Changes
+
+- 3bc9cc325: Release
+
+## 6.7.0
+
+### Minor Changes
+
+- 66aca3030: Release
+
+## 6.6.1
+
+### Patch Changes
+
+- 9a9f6467c: hotfix unlock
+
+## 6.6.0
+
+### Minor Changes
+
+- cd61ea2e1: Release
+
+## 6.5.0
+
+### Minor Changes
+
+- 3fb3228f5: Hotfix Firefox Extension
+
+## 6.4.7
+
+### Patch Changes
+
+- 6c016a06e: bump extension to trigger npm packages release
+
+## 6.4.6
+
+### Patch Changes
+
+- e7699b475: Fix darkmode
+
+## 6.4.5
+
+### Patch Changes
+
+- 6977fa180: release npm packages
+- Updated dependencies [712829411]
+ - @argent/shared@6.3.3
+
+## 6.4.4
+
+### Patch Changes
+
+- 93ae3ba6e: hotfix extension
+
+## 6.4.3
+
+### Patch Changes
+
+- 3dbcf15cc: hotfix release
+
+## 6.4.2
+
+### Patch Changes
+
+- d8c50c78a: extension hotfix
+
+## 6.4.1
+
+### Patch Changes
+
+- 1c6a83c03: Hotfix release for recovery issues
+
+## 6.4.0
+
+### Minor Changes
+
+- 6c7115e7: Trigger extension release
diff --git a/packages/extension/manifest/v2.json b/packages/extension/manifest/v2.json
index 78d56a968..bde8b036d 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.4.0",
+ "version": "5.9.0",
"manifest_version": 2,
"browser_action": {
"default_icon": {
diff --git a/packages/extension/manifest/v3.json b/packages/extension/manifest/v3.json
index 8091866fb..3e636a09a 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.4.0",
+ "version": "5.9.0",
"manifest_version": 3,
"action": {
"default_icon": {
diff --git a/packages/extension/package.json b/packages/extension/package.json
index 62b81e668..0a2d980ca 100644
--- a/packages/extension/package.json
+++ b/packages/extension/package.json
@@ -1,16 +1,17 @@
{
"name": "@argent-x/extension",
- "version": "6.3.0",
+ "version": "6.9.0",
"main": "index.js",
"private": true,
"license": "MIT",
"devDependencies": {
- "@sentry/webpack-plugin": "^1.18.9",
- "@svgr/webpack": "^6.0.0",
- "@testing-library/jest-dom": "^5.16.5",
+ "@argent/eslint-plugin-local": "^6.3.0",
+ "@svgr/webpack": "^8.0.1",
+ "@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^14.0.0",
"@types/async-retry": "^1.4.5",
- "@types/chrome": "^0.0.218",
+ "@types/chrome": "^0.0.246",
+ "@types/fs-extra": "^11.0.1",
"@types/lodash-es": "^4.17.6",
"@types/object-hash": "^3.0.2",
"@types/react": "^18.0.0",
@@ -21,67 +22,66 @@
"@types/styled-components": "^5.1.25",
"@types/url-join": "^4.0.1",
"@types/ws": "^8.5.3",
- "@typescript-eslint/eslint-plugin": "^5.10.1",
- "@typescript-eslint/parser": "^5.10.1",
- "@vitejs/plugin-react": "^3.0.0",
- "@vitest/coverage-istanbul": "^0.31.0",
+ "@typescript-eslint/eslint-plugin": "^6.0.0",
+ "@typescript-eslint/parser": "^6.0.0",
+ "@vitejs/plugin-react-swc": "^3.3.1",
+ "@vitest/coverage-istanbul": "^0.34.0",
+ "@vitest/coverage-v8": "^0.34.0",
"chokidar": "^3.5.2",
- "concurrently": "^7.2.2",
+ "concurrently": "^8.0.1",
"copy-webpack-plugin": "^11.0.0",
- "cross-fetch": "^3.1.5",
+ "cross-fetch": "^4.0.0",
+ "dotenv": "^16.1.4",
"dotenv-webpack": "^8.0.0",
- "esbuild-loader": "^3.0.1",
+ "esbuild-loader": "^4.0.0",
"eslint": "^8.7.0",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.6.0",
- "eslint-webpack-plugin": "^4.0.0",
"fetch-intercept": "^2.4.0",
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^8.0.0",
+ "fs-extra": "^11.1.1",
+ "happy-dom": "^12.0.0",
"html-webpack-plugin": "^5.5.0",
"isomorphic-fetch": "^3.0.0",
"jsdom": "^22.0.0",
- "mitt": "^3.0.0",
+ "minimatch": "^9.0.1",
"msw": "^1.0.0",
"raw-loader": "^4.0.2",
- "type-fest": "^3.9.0",
- "typescript": "^4.9.4",
+ "ts-node": "^10.9.1",
+ "type-fest": "^4.0.0",
+ "typescript": "^5.0.4",
"typescript-styled-plugin": "^0.18.2",
"url-loader": "^4.1.1",
- "vite": "^4.0.3",
- "vitest": "^0.29.2",
+ "vite": "^4.3.8",
+ "vitest": "^0.33.0",
"wait-for-expect": "^3.0.2",
- "webpack": "^5.62.1",
- "webpack-cli": "^5.0.1",
+ "webpack": "^5.88.0",
+ "webpack-cli": "^5.1.1",
"ws": "^8.8.1"
},
"scripts": {
"build": "NODE_ENV=production webpack",
- "build:sourcemaps": "GEN_SOURCE_MAPS=true yarn build",
+ "build:sourcemaps": "GEN_SOURCE_MAPS=true pnpm build",
"start": "webpack",
- "dev": "concurrently -k -r \"webpack --color --watch\" \"yarn dev:hot-reload-server\"",
- "dev:ui": "SHOW_DEV_UI=true yarn dev",
+ "dev": "concurrently -k -r \"webpack --color --watch\" \"pnpm run dev:hot-reload-server\"",
+ "dev:ui": "SHOW_DEV_UI=true pnpm dev",
"dev:hot-reload-server": "ts-node ./scripts/hot-reload-server.ts",
"lint": "eslint . --cache --ext .ts,.tsx",
"test": "vitest run",
"test:watch": "vitest",
"test:ci": "vitest run --coverage",
- "version": "yarn run change-to-release-branch && yarn run commit-and-tag-version-changes && yarn run push-release-branch",
- "change-to-release-branch": "git checkout -b release/v$npm_package_version",
- "sync-manifest-version": "concurrently \"yarn sync-manifest-version:v2\" \"yarn sync-manifest-version:v3\"",
- "sync-manifest-version:v2": "node -p \"JSON.stringify({...require('./manifest/v2.json'), version: '$npm_package_version'}, null, 2)\" > ./manifest/v2.temp.json && prettier --write ./manifest/v2.temp.json && mv ./manifest/v2.temp.json ./manifest/v2.json",
- "sync-manifest-version:v3": "node -p \"JSON.stringify({...require('./manifest/v3.json'), version: '$npm_package_version'}, null, 2)\" > ./manifest/v3.temp.json && prettier --write ./manifest/v3.temp.json && mv ./manifest/v3.temp.json ./manifest/v3.json",
- "commit-and-tag-version-changes": "git add --update && git commit -m v$npm_package_version && git tag -a v$npm_package_version -m \"v$npm_package_version\"",
- "push-release-branch": "git push --set-upstream origin release/v$npm_package_version --follow-tags"
+ "export": "ts-node ./scripts/export.ts"
},
"dependencies": {
- "@argent/guardian": "^6.3.0",
- "@argent/stack-router": "^6.3.0",
- "@argent/ui": "^6.3.0",
- "@argent/x-multicall": "^6.3.0",
- "@argent/x-sessions": "^6.3.0",
- "@argent/x-swap": "^6.3.0",
- "@argent/x-window": "^6.3.0",
+ "@argent/guardian": "^6.3.1",
+ "@argent/shared": "^6.3.3",
+ "@argent/stack-router": "^6.3.1",
+ "@argent/ui": "^6.3.1",
+ "@argent/x-multicall": "^6.4.1",
+ "@argent/x-sessions": "^6.3.1",
+ "@argent/x-swap": "^6.3.1",
+ "@argent/x-window": "^6.3.1",
"@chakra-ui/icons": "^2.0.15",
"@chakra-ui/react": "^2.6.1",
"@extend-chrome/messages": "^1.2.2",
@@ -90,46 +90,49 @@
"@mui/icons-material": "^5.3.1",
"@mui/material": "^5.1.0",
"@mui/styled-engine-sc": "^5.10.3",
- "@noble/hashes": "^1.1.3",
- "@scure/bip39": "^1.2.0",
+ "@noble/curves": "^1.0.0",
+ "@noble/hashes": "^1.3.1",
+ "@scure/bip32": "^1.3.1",
+ "@scure/bip39": "^1.2.1",
"@sentry/react": "^7.6.0",
"@sentry/tracing": "^7.6.0",
"@tippyjs/react": "^4.2.6",
- "@trpc/client": "^10.20.0",
- "@trpc/server": "^10.20.0",
- "@vitest/coverage-istanbul": "^0.31.0",
+ "@trpc/client": "^10.28.0",
+ "@trpc/server": "^10.31.0",
+ "@vitest/coverage-istanbul": "^0.34.0",
"async-retry": "^1.3.3",
- "bignumber.js": "^9.0.2",
"buffer": "^6.0.3",
- "colord": "^2.9.2",
+ "colord": "^2.9.3",
"dexie": "^3.2.2",
"dexie-react-hooks": "^1.1.1",
+ "emittery": "^1.0.1",
+ "eslint-config-prettier": "^9.0.0",
+ "eslint-plugin-prettier": "^5.0.0",
"ethers": "^5.5.1",
"jose": "^4.3.6",
"jotai": "^2.0.4",
"lodash-es": "^4.17.21",
"micro-starknet": "^0.2.3",
- "nanoid": "^4.0.0",
+ "nanoid": "^5.0.0",
"object-hash": "^3.0.0",
"qr-code-styling": "^1.6.0-rc.1",
"react": "^18.0.0",
"react-copy-to-clipboard": "^5.0.4",
"react-dom": "^18.0.0",
"react-dropzone": "^14.0.0",
- "react-hook-form": "^7.33.0",
+ "react-hook-form": "^7.44.2",
"react-measure": "^2.5.2",
"react-router-dom": "^6.0.1",
"react-select": "^5.4.0",
"react-textarea-autosize": "^8.3.4",
- "semver": "^7.3.7",
- "starknet": "^4.21.0",
- "starknet3": "npm:starknet@3.18.2",
- "starknet4": "npm:starknet@4.4.0",
- "starknet5": "npm:starknet@5.7.0",
+ "semver": "^7.5.2",
+ "starknet": "5.18.0",
+ "starknet4": "npm:starknet@4.22.0",
+ "starknet4-deprecated": "npm:starknet@4.4.0",
"styled-components": "^5.3.5",
"styled-normalize": "^8.0.7",
"swr": "^1.3.0",
- "trpc-extension": "^1.1.0",
+ "trpc-browser": "^1.3.1",
"url-join": "^5.0.0",
"webextension-polyfill": "^0.10.0",
"yup": "^1.0.0-beta.4",
diff --git a/packages/extension/scripts/export.ts b/packages/extension/scripts/export.ts
new file mode 100644
index 000000000..35df97c5c
--- /dev/null
+++ b/packages/extension/scripts/export.ts
@@ -0,0 +1,110 @@
+import fs from "fs-extra"
+import { minimatch } from "minimatch"
+import path from "path"
+import dotenv from "dotenv"
+import childProcess from "child_process"
+
+const readme = `> This repo has been exported and configured to allow building from source without access to the main git repo
+
+Install \`pnpm\` - see https://pnpm.io/installation
+
+\`\`\`bash
+pnpm run setup # setup dependencies
+pnpm build # run build process for all packages
+\`\`\`
+`
+
+const manifestVersion = getManifestVersion()
+
+const source = path.resolve(__dirname, "../../..")
+const destination = path.resolve(
+ source,
+ `../argent-x-exported-v${manifestVersion}`,
+)
+
+const dotenvSource = path.resolve(__dirname, "../.env")
+const dotenvDestination = path.resolve(destination, "packages/extension/.env")
+
+const readmeDestination = path.resolve(destination, "Readme.md")
+
+const exclude = [
+ ".git**",
+ "**/.next",
+ "**/.env",
+ "**/dist**",
+ "**/.eslintcache",
+ "**.log**",
+ "**/node_modules**",
+ "packages/dapp**",
+ "packages/get-starknet**",
+ "packages/starknet-react-webwallet-connector**",
+ "packages/web**",
+]
+
+function getCommitHash() {
+ const hash = childProcess.execSync("git rev-parse HEAD")
+ return hash.toString().trim()
+}
+
+function getManifestVersion() {
+ const pkgRaw = fs.readFileSync(
+ path.resolve(__dirname, "../manifest/v2.json"),
+ "utf8",
+ )
+ const pkg = JSON.parse(pkgRaw)
+ return pkg.version
+}
+
+async function preflightCheck() {
+ console.log("🚨 CHECK: is the source .env file configured for production?")
+ console.log(` ${dotenvSource}\n`)
+ const dotEnvRaw = await fs.readFile(dotenvSource, "utf-8")
+ const dotEnvParsed = dotenv.parse(dotEnvRaw)
+ if (dotEnvParsed.ARGENT_X_ENVIRONMENT !== "prod") {
+ console.log(
+ `.env error - expected ARGENT_X_ENVIRONMENT to be "prod" but got "${dotEnvParsed.ARGENT_X_ENVIRONMENT}"`,
+ )
+ process.exit(1)
+ }
+}
+
+async function exportFiles() {
+ await fs.emptyDir(destination)
+ await fs.copy(source, destination, {
+ filter: (path) => {
+ const repoPath = path.substring(source.length + 1)
+ if (!repoPath.length) {
+ return true
+ }
+ const match = exclude.some((pattern) => minimatch(repoPath, pattern))
+ return !match
+ },
+ })
+}
+
+async function exportEnv() {
+ const dotEnvRaw = await fs.readFile(dotenvSource, "utf-8")
+ const filteredComments = dotEnvRaw.replace(/^#.*\n?/gm, "")
+ const filteredEmpty = filteredComments.replace(/\n+/gm, "\n")
+ const commitHash = getCommitHash()
+ const withCommitHash = `${filteredEmpty}\nCOMMIT_HASH_OVERRIDE=${commitHash}\n`
+ await fs.writeFile(dotenvDestination, withCommitHash)
+ console.log("👀 CHECK: Exported .env:\n")
+ console.log(withCommitHash)
+}
+
+async function exportReadme() {
+ await fs.writeFile(readmeDestination, readme)
+}
+
+async function exportForEndUserBuild() {
+ await preflightCheck()
+ await exportFiles()
+ await exportReadme()
+ await exportEnv()
+ console.log("✅ Buildable source and Readme exported to", destination)
+}
+
+;(async () => {
+ await exportForEndUserBuild()
+})()
diff --git a/packages/extension/src/assets/default-tokens.json b/packages/extension/src/assets/default-tokens.json
index 8c50ef6c2..ffaab4bc9 100644
--- a/packages/extension/src/assets/default-tokens.json
+++ b/packages/extension/src/assets/default-tokens.json
@@ -5,7 +5,7 @@
"symbol": "ETH",
"decimals": "18",
"network": "mainnet-alpha",
- "image": "https://dv3jj1unlp2jl.cloudfront.net/128/color/eth.png",
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/eth.png",
"showAlways": true
},
{
@@ -14,7 +14,7 @@
"symbol": "ETH",
"decimals": "18",
"network": "goerli-alpha",
- "image": "https://dv3jj1unlp2jl.cloudfront.net/128/color/eth.png",
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/eth.png",
"showAlways": true
},
{
@@ -23,16 +23,7 @@
"symbol": "ETH",
"decimals": "18",
"network": "integration",
- "image": "https://dv3jj1unlp2jl.cloudfront.net/128/color/eth.png",
- "showAlways": true
- },
- {
- "address": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
- "name": "Ether",
- "symbol": "ETH",
- "decimals": "18",
- "network": "goerli-alpha-2",
- "image": "https://dv3jj1unlp2jl.cloudfront.net/128/color/eth.png",
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/eth.png",
"showAlways": true
},
{
@@ -41,7 +32,7 @@
"symbol": "ETH",
"decimals": "18",
"network": "localhost",
- "image": "https://dv3jj1unlp2jl.cloudfront.net/128/color/eth.png",
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/eth.png",
"showAlways": true
},
{
@@ -50,7 +41,7 @@
"symbol": "DAI",
"decimals": "18",
"network": "mainnet-alpha",
- "image": "https://dv3jj1unlp2jl.cloudfront.net/128/color/dai.png"
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/dai.png"
},
{
"address": "0x03e85bfbb8e2a42b7bead9e88e9a1b19dbccf661471061807292120462396ec9",
@@ -58,7 +49,7 @@
"symbol": "DAI",
"decimals": "18",
"network": "goerli-alpha",
- "image": "https://dv3jj1unlp2jl.cloudfront.net/128/color/dai.png"
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/dai.png"
},
{
"address": "0x03fe2b97c1fd336e750087d68b9b867997fd64a2661ff3ca5a7c771641e8e7ac",
@@ -66,7 +57,7 @@
"symbol": "WBTC",
"decimals": "8",
"network": "mainnet-alpha",
- "image": "https://dv3jj1unlp2jl.cloudfront.net/128/color/wbtc.png"
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/wbtc.png"
},
{
"address": "0x12d537dc323c439dc65c976fad242d5610d27cfb5f31689a0a319b8be7f3d56",
@@ -74,7 +65,7 @@
"symbol": "WBTC",
"decimals": "8",
"network": "goerli-alpha",
- "image": "https://dv3jj1unlp2jl.cloudfront.net/128/color/wbtc.png"
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/wbtc.png"
},
{
"address": "0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8",
@@ -82,7 +73,7 @@
"symbol": "USDC",
"decimals": "6",
"network": "mainnet-alpha",
- "image": "https://dv3jj1unlp2jl.cloudfront.net/128/color/usdc.png"
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/usdc.png"
},
{
"address": "0x005a643907b9a4bc6a55e9069c4fd5fd1f5c79a22470690f75556c4736e34426",
@@ -90,7 +81,7 @@
"symbol": "USDC",
"decimals": "6",
"network": "goerli-alpha",
- "image": "https://dv3jj1unlp2jl.cloudfront.net/128/color/usdc.png"
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/usdc.png"
},
{
"address": "0x068f5c6a61780768455de69077e07e89787839bf8166decfbf92b645209c0fb8",
@@ -98,7 +89,7 @@
"symbol": "USDT",
"decimals": "6",
"network": "mainnet-alpha",
- "image": "https://dv3jj1unlp2jl.cloudfront.net/128/color/usdt.png"
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/usdt.png"
},
{
"address": "0x386e8d061177f19b3b485c20e31137e6f6bc497cc635ccdfcab96fadf5add6a",
@@ -106,35 +97,30 @@
"symbol": "USDT",
"decimals": "6",
"network": "goerli-alpha",
- "image": "https://dv3jj1unlp2jl.cloudfront.net/128/color/usdt.png"
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/usdt.png"
},
{
- "address": "0x07394cbe418daa16e42b87ba67372d4ab4a5df0b05c6e554d158458ce245bc10",
- "name": "Test Token",
- "symbol": "TEST",
+ "address": "0x0148f970a06fc95ee0682140e23a980351be8fad26168c5f0465e63940c46514",
+ "name": "Angle agEUR",
+ "symbol": "agEUR",
"decimals": "18",
- "network": "goerli-alpha"
+ "network": "mainnet-alpha",
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/ageur.png"
},
{
- "address": "0x06a09ccb1caaecf3d9683efe335a667b2169a409d19c589ba1eb771cd210af75",
- "name": "Test Token",
- "symbol": "TEST",
+ "address": "0x05ef98e7dc5865f49bcec200df508d27669b3ef7f9d2439decd127350359f291",
+ "name": "Wrapped Lido ETH",
+ "symbol": "wstETH",
"decimals": "18",
- "network": "mainnet-alpha"
- },
- {
- "address": "0x07a39a50bf689e9430fc81fba0f4d46e245e1657e77455548ed7e32c808cfc10",
- "name": "SelfService",
- "symbol": "SLF",
- "decimals": "6",
- "network": "goerli-alpha"
+ "network": "mainnet-alpha",
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/steth.png"
},
{
- "address": "0x0148f970a06fc95ee0682140e23a980351be8fad26168c5f0465e63940c46514",
- "name": "Angle agEUR",
- "symbol": "agEUR",
+ "address": "0x0319111a5037cbec2b3e638cc34a3474e2d2608299f3e62866e9cc683208c610",
+ "name": "Rocket Pool ETH",
+ "symbol": "rETH",
"decimals": "18",
"network": "mainnet-alpha",
- "image": "https://dv3jj1unlp2jl.cloudfront.net/128/color/ageur.png"
+ "iconUrl": "https://dv3jj1unlp2jl.cloudfront.net/128/color/reth.png"
}
]
diff --git a/packages/extension/src/background/__new/procedures/account/create.ts b/packages/extension/src/background/__new/procedures/account/create.ts
index e07f32aac..bd2084c5e 100644
--- a/packages/extension/src/background/__new/procedures/account/create.ts
+++ b/packages/extension/src/background/__new/procedures/account/create.ts
@@ -9,7 +9,11 @@ import { extensionOnlyProcedure } from "../permissions"
const createAccountInputSchema = z.object({
networkId: z.string(),
- type: z.union([z.literal("standard"), z.literal("multisig")]),
+ type: z.union([
+ z.literal("standard"),
+ z.literal("multisig"),
+ z.literal("standardCairo0"),
+ ]),
})
export const createAccountProcedure = extensionOnlyProcedure
diff --git a/packages/extension/src/background/__new/procedures/account/deploy.ts b/packages/extension/src/background/__new/procedures/account/deploy.ts
index 7d3609bcf..55bfdffa6 100644
--- a/packages/extension/src/background/__new/procedures/account/deploy.ts
+++ b/packages/extension/src/background/__new/procedures/account/deploy.ts
@@ -6,9 +6,16 @@ import { extensionOnlyProcedure } from "../permissions"
export const deployAccountProcedure = extensionOnlyProcedure
.use(openSessionMiddleware)
.input(baseWalletAccountSchema)
- .mutation(async ({ input: data, ctx: { services } }) => {
- await deployAccountAction({
- account: data,
- actionQueue: services.actionQueue,
- })
- })
+ .mutation(
+ async ({
+ input,
+ ctx: {
+ services: { actionService },
+ },
+ }) => {
+ await deployAccountAction({
+ account: input,
+ actionService,
+ })
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/account/index.ts b/packages/extension/src/background/__new/procedures/account/index.ts
index cae8998de..9f6044d1b 100644
--- a/packages/extension/src/background/__new/procedures/account/index.ts
+++ b/packages/extension/src/background/__new/procedures/account/index.ts
@@ -1,10 +1,12 @@
import { router } from "../../trpc"
import { createAccountProcedure } from "./create"
import { deployAccountProcedure } from "./deploy"
+import { selectAccountProcedure } from "./select"
import { upgradeAccountProcedure } from "./upgrade"
export const accountRouter = router({
create: createAccountProcedure,
deploy: deployAccountProcedure,
upgrade: upgradeAccountProcedure,
+ select: selectAccountProcedure,
})
diff --git a/packages/extension/src/background/__new/procedures/account/select.ts b/packages/extension/src/background/__new/procedures/account/select.ts
new file mode 100644
index 000000000..1722317f3
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/account/select.ts
@@ -0,0 +1,21 @@
+import { baseWalletAccountSchema } from "../../../../shared/wallet.model"
+import { openSessionMiddleware } from "../../middleware/session"
+import { extensionOnlyProcedure } from "../permissions"
+import { respond } from "../../../respond"
+
+export const selectAccountProcedure = extensionOnlyProcedure
+ .use(openSessionMiddleware)
+ .input(baseWalletAccountSchema.nullable())
+ .mutation(
+ async ({
+ input: baseWalletAccount,
+ ctx: {
+ services: { wallet },
+ },
+ }) => {
+ const account = await wallet.selectAccount(baseWalletAccount)
+ if (account) {
+ void respond({ type: "CONNECT_ACCOUNT_RES", data: account })
+ }
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/account/upgrade.ts b/packages/extension/src/background/__new/procedures/account/upgrade.ts
index a656e2acc..da9f7ec67 100644
--- a/packages/extension/src/background/__new/procedures/account/upgrade.ts
+++ b/packages/extension/src/background/__new/procedures/account/upgrade.ts
@@ -19,15 +19,15 @@ export const upgradeAccountProcedure = extensionOnlyProcedure
async ({
input: { account, targetImplementationType },
ctx: {
- services: { wallet, actionQueue },
+ services: { wallet, actionService },
},
}) => {
// TODO ⬇ should be a service
await upgradeAccount({
account,
wallet,
+ actionService,
targetImplementationType,
- actionQueue,
})
},
)
diff --git a/packages/extension/src/background/__new/procedures/accountMessaging/cancelEscape.ts b/packages/extension/src/background/__new/procedures/accountMessaging/cancelEscape.ts
new file mode 100644
index 000000000..3a4d371f6
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/accountMessaging/cancelEscape.ts
@@ -0,0 +1,55 @@
+import { z } from "zod"
+
+import { extensionOnlyProcedure } from "../permissions"
+import { baseWalletAccountSchema } from "../../../../shared/wallet.model"
+import { Account } from "starknet"
+import { getEntryPointSafe } from "../../../../shared/utils/transactions"
+import { AccountMessagingError } from "../../../../shared/errors/accountMessaging"
+
+const cancelEscapeSchema = z.object({
+ account: baseWalletAccountSchema,
+})
+
+export const cancelEscapeProcedure = extensionOnlyProcedure
+ .input(cancelEscapeSchema)
+ .mutation(
+ async ({
+ input: { account },
+ ctx: {
+ services: { actionService, wallet },
+ },
+ }) => {
+ try {
+ const starknetAccount =
+ (await wallet.getSelectedStarknetAccount()) as Account
+ await actionService.add(
+ {
+ type: "TRANSACTION",
+ payload: {
+ transactions: {
+ contractAddress: account.address,
+ entrypoint: getEntryPointSafe(
+ "cancelEscape",
+ starknetAccount.cairoVersion,
+ ),
+ calldata: [],
+ },
+ meta: {
+ isCancelEscape: true,
+ title: "Cancel escape",
+ type: "INVOKE",
+ },
+ },
+ },
+ {
+ origin,
+ },
+ )
+ } catch (error) {
+ throw new AccountMessagingError({
+ options: { error },
+ code: "ESCAPE_CANCELLATION_FAILED",
+ })
+ }
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/accountMessaging/changeGuardian.ts b/packages/extension/src/background/__new/procedures/accountMessaging/changeGuardian.ts
new file mode 100644
index 000000000..dd8915d38
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/accountMessaging/changeGuardian.ts
@@ -0,0 +1,61 @@
+import { z } from "zod"
+
+import { extensionOnlyProcedure } from "../permissions"
+import { baseWalletAccountSchema } from "../../../../shared/wallet.model"
+import { constants, num, Account } from "starknet"
+import { getEntryPointSafe } from "../../../../shared/utils/transactions"
+import { AccountMessagingError } from "../../../../shared/errors/accountMessaging"
+
+const changeGuardianSchema = z.object({
+ guardian: z.string(),
+ account: baseWalletAccountSchema,
+})
+
+export const changeGuardianProcedure = extensionOnlyProcedure
+ .input(changeGuardianSchema)
+ .mutation(
+ async ({
+ input: { account, guardian },
+ ctx: {
+ services: { actionService, wallet },
+ },
+ }) => {
+ try {
+ const newGuardian = num.hexToDecimalString(guardian)
+ const starknetAccount =
+ (await wallet.getSelectedStarknetAccount()) as Account // Old accounts are not supported
+
+ await actionService.add(
+ {
+ type: "TRANSACTION",
+ payload: {
+ transactions: {
+ contractAddress: account.address,
+ entrypoint: getEntryPointSafe(
+ "changeGuardian",
+ starknetAccount.cairoVersion,
+ ),
+ calldata: [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",
+ },
+ },
+ },
+ {
+ origin,
+ },
+ )
+ } catch (error) {
+ throw new AccountMessagingError({
+ options: { error },
+ code: "CHANGE_GUARDIAN_FAILED",
+ })
+ }
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/accountMessaging/escapeAndChangeGuardian.ts b/packages/extension/src/background/__new/procedures/accountMessaging/escapeAndChangeGuardian.ts
new file mode 100644
index 000000000..54d76ff48
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/accountMessaging/escapeAndChangeGuardian.ts
@@ -0,0 +1,110 @@
+import { z } from "zod"
+
+import { extensionOnlyProcedure } from "../permissions"
+import { baseWalletAccountSchema } from "../../../../shared/wallet.model"
+import { isEqualAddress } from "../../../../ui/services/addresses"
+import { constants, num, Account } from "starknet"
+import { getEntryPointSafe } from "../../../../shared/utils/transactions"
+import { AccountMessagingError } from "../../../../shared/errors/accountMessaging"
+import { AccountError } from "../../../../shared/errors/account"
+
+const escapeAndChangeGuardianSchema = z.object({
+ account: baseWalletAccountSchema,
+})
+
+export const escapeAndChangeGuardianProcedure = extensionOnlyProcedure
+ .input(escapeAndChangeGuardianSchema)
+ .mutation(
+ async ({
+ input: { account },
+ ctx: {
+ services: { actionService, wallet },
+ },
+ }) => {
+ try {
+ /**
+ * This is a two-stage process
+ *
+ * 1. call escapeGuardian with current signer key as new guardian key
+ * 2. changeGuardian to ZERO, signed twice by same signer key (like 2/2 multisig with same key)
+ */
+
+ const selectedAccount = await wallet.getAccount(account)
+ const starknetAccount =
+ (await wallet.getSelectedStarknetAccount()) as Account // Old accounts are not supported
+
+ if (!selectedAccount) {
+ throw new AccountError({
+ code: "NOT_FOUND",
+ })
+ }
+
+ const { publicKey } = await wallet.getPublicKey(account)
+
+ if (
+ selectedAccount.guardian &&
+ isEqualAddress(selectedAccount.guardian, publicKey)
+ ) {
+ /**
+ * Account already used `escapeGuardian` to change guardian to this account publicKey
+ * Call `changeGuardian` to ZERO
+ */
+
+ await actionService.add(
+ {
+ type: "TRANSACTION",
+ payload: {
+ transactions: {
+ contractAddress: account.address,
+ entrypoint: getEntryPointSafe(
+ "changeGuardian",
+ starknetAccount.cairoVersion,
+ ),
+ calldata: [num.hexToDecimalString(constants.ZERO.toString())],
+ },
+ meta: {
+ isChangeGuardian: true,
+ title: "Change account guardian",
+ type: "INVOKE",
+ },
+ },
+ },
+ {
+ origin,
+ },
+ )
+ } else {
+ /**
+ * Call `escapeGuardian` to change guardian to this account publicKey
+ */
+ await actionService.add(
+ {
+ type: "TRANSACTION",
+ payload: {
+ transactions: {
+ contractAddress: account.address,
+ entrypoint: "escapeGuardian",
+ calldata: [num.hexToDecimalString(publicKey)],
+ },
+ meta: {
+ isChangeGuardian: true,
+ title: "Escape account guardian",
+ type: "INVOKE",
+ },
+ },
+ },
+ {
+ origin,
+ },
+ )
+ }
+ } catch (error) {
+ throw new AccountMessagingError({
+ options: {
+ error,
+ },
+ code: "ESCAPE_AND_CHANGE_GUARDIAN_FAILED",
+ })
+ }
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/accountMessaging/getEncryptedPrivateKey.ts b/packages/extension/src/background/__new/procedures/accountMessaging/getEncryptedPrivateKey.ts
new file mode 100644
index 000000000..9e10ab0e9
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/accountMessaging/getEncryptedPrivateKey.ts
@@ -0,0 +1,42 @@
+import { z } from "zod"
+
+import { extensionOnlyProcedure } from "../permissions"
+import { baseWalletAccountSchema } from "../../../../shared/wallet.model"
+import { encryptForUi } from "../../../crypto"
+import { AccountMessagingError } from "../../../../shared/errors/accountMessaging"
+import { SessionError } from "../../../../shared/errors/session"
+
+const getEncryptedPrivateKeySchema = z.object({
+ encryptedSecret: z.string(),
+ account: baseWalletAccountSchema,
+})
+
+export const getEncryptedPrivateKeyProcedure = extensionOnlyProcedure
+ .input(getEncryptedPrivateKeySchema)
+ .output(z.string())
+ .mutation(
+ async ({
+ input: { account, encryptedSecret },
+ ctx: {
+ services: { wallet, messagingKeys },
+ },
+ }) => {
+ if (!(await wallet.isSessionOpen())) {
+ throw new SessionError({
+ code: "NO_OPEN_SESSION",
+ })
+ }
+ try {
+ return await encryptForUi(
+ await wallet.getPrivateKey(account),
+ encryptedSecret,
+ messagingKeys.privateKey,
+ )
+ } catch (e) {
+ throw new AccountMessagingError({
+ options: { error: e },
+ code: "GET_ENCRYPTED_KEY_FAILED",
+ })
+ }
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/accountMessaging/getEncryptedSeedPhrase.ts b/packages/extension/src/background/__new/procedures/accountMessaging/getEncryptedSeedPhrase.ts
new file mode 100644
index 000000000..16b8e40fd
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/accountMessaging/getEncryptedSeedPhrase.ts
@@ -0,0 +1,41 @@
+import { z } from "zod"
+
+import { extensionOnlyProcedure } from "../permissions"
+import { encryptForUi } from "../../../crypto"
+import { AccountMessagingError } from "../../../../shared/errors/accountMessaging"
+import { SessionError } from "../../../../shared/errors/session"
+
+const getEncryptedSeedPhraseSchema = z.object({
+ encryptedSecret: z.string(),
+})
+
+export const getEncryptedSeedPhraseProcedure = extensionOnlyProcedure
+ .input(getEncryptedSeedPhraseSchema)
+ .output(z.string())
+ .mutation(
+ async ({
+ input: { encryptedSecret },
+ ctx: {
+ services: { wallet, messagingKeys },
+ },
+ }) => {
+ if (!(await wallet.isSessionOpen())) {
+ throw new SessionError({
+ code: "NO_OPEN_SESSION",
+ })
+ }
+
+ try {
+ return await encryptForUi(
+ await wallet.getSeedPhrase(),
+ encryptedSecret,
+ messagingKeys.privateKey,
+ )
+ } catch (e) {
+ throw new AccountMessagingError({
+ options: { error: e },
+ code: "GET_SEEDPHRASE_FAILED",
+ })
+ }
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/accountMessaging/getNextPublicKey.ts b/packages/extension/src/background/__new/procedures/accountMessaging/getNextPublicKey.ts
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/extension/src/background/__new/procedures/accountMessaging/getNextPublicKeyForMultisig.ts b/packages/extension/src/background/__new/procedures/accountMessaging/getNextPublicKeyForMultisig.ts
new file mode 100644
index 000000000..b0efb4bbe
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/accountMessaging/getNextPublicKeyForMultisig.ts
@@ -0,0 +1,31 @@
+import { z } from "zod"
+
+import { extensionOnlyProcedure } from "../permissions"
+import { PubKeyError } from "../../../../shared/errors/pubKey"
+
+const getNextPublicKeyForMultisigSchema = z.object({
+ networkId: z.string(),
+})
+
+export const getNextPublicKeyForMultisigProcedure = extensionOnlyProcedure
+ .input(getNextPublicKeyForMultisigSchema)
+ .output(z.string())
+ .mutation(
+ async ({
+ input: { networkId },
+ ctx: {
+ services: { wallet },
+ },
+ }) => {
+ try {
+ const { publicKey } = await wallet.getNextPublicKeyForMultisig(
+ networkId,
+ )
+ return publicKey
+ } catch (error) {
+ throw new PubKeyError({
+ code: "FAILED_NEXT_PUB_KEY_GENERATION",
+ })
+ }
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/accountMessaging/getPublicKey.ts b/packages/extension/src/background/__new/procedures/accountMessaging/getPublicKey.ts
new file mode 100644
index 000000000..2d0d94cae
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/accountMessaging/getPublicKey.ts
@@ -0,0 +1,31 @@
+import { z } from "zod"
+
+import { extensionOnlyProcedure } from "../permissions"
+import { baseWalletAccountSchema } from "../../../../shared/wallet.model"
+import { AccountMessagingError } from "../../../../shared/errors/accountMessaging"
+
+const getPublicKeySchema = z.object({
+ account: z.optional(baseWalletAccountSchema),
+})
+
+export const getPublicKeyProcedure = extensionOnlyProcedure
+ .input(getPublicKeySchema)
+ .output(z.string())
+ .query(
+ async ({
+ input: { account },
+ ctx: {
+ services: { wallet },
+ },
+ }) => {
+ try {
+ const { publicKey } = await wallet.getPublicKey(account)
+ return publicKey
+ } catch (error) {
+ throw new AccountMessagingError({
+ options: { error },
+ code: "GET_PUBLIC_KEY_FAILED",
+ })
+ }
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/accountMessaging/getPublicKeysBufferForMultisig.ts b/packages/extension/src/background/__new/procedures/accountMessaging/getPublicKeysBufferForMultisig.ts
new file mode 100644
index 000000000..2f6da5b9c
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/accountMessaging/getPublicKeysBufferForMultisig.ts
@@ -0,0 +1,33 @@
+import { z } from "zod"
+
+import { extensionOnlyProcedure } from "../permissions"
+import { PubKeyError } from "../../../../shared/errors/pubKey"
+
+const getPublicKeysBufferForMultisigSchema = z.object({
+ start: z.number(),
+ buffer: z.number(),
+})
+
+export const getPublicKeysBufferForMultisigProcedure = extensionOnlyProcedure
+ .input(getPublicKeysBufferForMultisigSchema)
+ .output(z.array(z.string()))
+ .query(
+ async ({
+ input: { start, buffer },
+ ctx: {
+ services: { wallet },
+ },
+ }) => {
+ try {
+ const pubKeys = await wallet.getPublicKeysBufferForMultisig(
+ start,
+ buffer,
+ )
+ return pubKeys
+ } catch (error) {
+ throw new PubKeyError({
+ code: "FAILED_BUFFER_GENERATION",
+ })
+ }
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/accountMessaging/index.ts b/packages/extension/src/background/__new/procedures/accountMessaging/index.ts
new file mode 100644
index 000000000..7d0906624
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/accountMessaging/index.ts
@@ -0,0 +1,22 @@
+import { router } from "../../trpc"
+import { getEncryptedPrivateKeyProcedure } from "./getEncryptedPrivateKey"
+import { getEncryptedSeedPhraseProcedure } from "./getEncryptedSeedPhrase"
+import { changeGuardianProcedure } from "./changeGuardian"
+import { cancelEscapeProcedure } from "./cancelEscape"
+import { triggerEscapeGuardianProcedure } from "./triggerEscapeGuardian"
+import { escapeAndChangeGuardianProcedure } from "./escapeAndChangeGuardian"
+import { getPublicKeyProcedure } from "./getPublicKey"
+import { getNextPublicKeyForMultisigProcedure } from "./getNextPublicKeyForMultisig"
+import { getPublicKeysBufferForMultisigProcedure } from "./getPublicKeysBufferForMultisig"
+
+export const accountMessagingRouter = router({
+ getEncryptedPrivateKey: getEncryptedPrivateKeyProcedure,
+ getEncryptedSeedPhrase: getEncryptedSeedPhraseProcedure,
+ changeGuardian: changeGuardianProcedure,
+ cancelEscape: cancelEscapeProcedure,
+ triggerEscapeGuardian: triggerEscapeGuardianProcedure,
+ escapeAndChangeGuardian: escapeAndChangeGuardianProcedure,
+ getPublicKey: getPublicKeyProcedure,
+ getNextPublicKeyForMultisig: getNextPublicKeyForMultisigProcedure,
+ getPublicKeysBufferForMultisig: getPublicKeysBufferForMultisigProcedure,
+})
diff --git a/packages/extension/src/background/__new/procedures/accountMessaging/triggerEscapeGuardian.ts b/packages/extension/src/background/__new/procedures/accountMessaging/triggerEscapeGuardian.ts
new file mode 100644
index 000000000..3b2574a4d
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/accountMessaging/triggerEscapeGuardian.ts
@@ -0,0 +1,48 @@
+import { z } from "zod"
+
+import { extensionOnlyProcedure } from "../permissions"
+import { baseWalletAccountSchema } from "../../../../shared/wallet.model"
+import { AccountMessagingError } from "../../../../shared/errors/accountMessaging"
+
+const triggerEscapeGuardianSchema = z.object({
+ account: baseWalletAccountSchema,
+})
+
+export const triggerEscapeGuardianProcedure = extensionOnlyProcedure
+ .input(triggerEscapeGuardianSchema)
+ .mutation(
+ async ({
+ input: { account },
+ ctx: {
+ services: { actionService },
+ },
+ }) => {
+ try {
+ await actionService.add(
+ {
+ type: "TRANSACTION",
+ payload: {
+ transactions: {
+ contractAddress: account.address,
+ entrypoint: "triggerEscapeGuardian",
+ calldata: [],
+ },
+ meta: {
+ isCancelEscape: true,
+ title: "Trigger escape guardian",
+ type: "INVOKE",
+ },
+ },
+ },
+ {
+ origin,
+ },
+ )
+ } catch (error) {
+ throw new AccountMessagingError({
+ options: { error },
+ code: "TRIGGER_ESCAPE_FAILED",
+ })
+ }
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/action/approve.ts b/packages/extension/src/background/__new/procedures/action/approve.ts
new file mode 100644
index 000000000..bc4a3068e
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/action/approve.ts
@@ -0,0 +1,18 @@
+import { z } from "zod"
+
+import {
+ actionHashSchema,
+ actionQueueItemSchema,
+} from "../../../../shared/actionQueue/schema"
+import { openSessionMiddleware } from "../../middleware/session"
+import { extensionOnlyProcedure } from "../permissions"
+
+const approveActionSchema = z.union([actionQueueItemSchema, actionHashSchema])
+
+export const approveActionProcedure = extensionOnlyProcedure
+ .use(openSessionMiddleware)
+ .input(approveActionSchema)
+ .mutation(async ({ input, ctx: { services } }) => {
+ const { actionService } = services
+ return actionService.approve(input)
+ })
diff --git a/packages/extension/src/background/__new/procedures/action/approveAndWait.ts b/packages/extension/src/background/__new/procedures/action/approveAndWait.ts
new file mode 100644
index 000000000..84719a495
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/action/approveAndWait.ts
@@ -0,0 +1,18 @@
+import { z } from "zod"
+
+import {
+ actionHashSchema,
+ actionQueueItemSchema,
+} from "../../../../shared/actionQueue/schema"
+import { openSessionMiddleware } from "../../middleware/session"
+import { extensionOnlyProcedure } from "../permissions"
+
+const approveActionSchema = z.union([actionQueueItemSchema, actionHashSchema])
+
+export const approveAndWaitActionProcedure = extensionOnlyProcedure
+ .use(openSessionMiddleware)
+ .input(approveActionSchema)
+ .mutation(async ({ input, ctx: { services } }) => {
+ const { actionService } = services
+ return actionService.approveAndWait(input)
+ })
diff --git a/packages/extension/src/background/__new/procedures/action/index.ts b/packages/extension/src/background/__new/procedures/action/index.ts
new file mode 100644
index 000000000..3a3deca7a
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/action/index.ts
@@ -0,0 +1,12 @@
+import { router } from "../../trpc"
+import { approveActionProcedure } from "./approve"
+import { approveAndWaitActionProcedure } from "./approveAndWait"
+import { rejectActionProcedure } from "./reject"
+import { rejectAllActionsProcedure } from "./rejectAll"
+
+export const actionRouter = router({
+ approve: approveActionProcedure,
+ approveAndWait: approveAndWaitActionProcedure,
+ reject: rejectActionProcedure,
+ rejectAll: rejectAllActionsProcedure,
+})
diff --git a/packages/extension/src/background/__new/procedures/action/reject.ts b/packages/extension/src/background/__new/procedures/action/reject.ts
new file mode 100644
index 000000000..4d8fa6b6f
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/action/reject.ts
@@ -0,0 +1,18 @@
+import { z } from "zod"
+
+import { actionHashSchema } from "../../../../shared/actionQueue/schema"
+import { openSessionMiddleware } from "../../middleware/session"
+import { extensionOnlyProcedure } from "../permissions"
+
+const rejectActionSchema = z.union([
+ actionHashSchema,
+ z.array(actionHashSchema),
+])
+
+export const rejectActionProcedure = extensionOnlyProcedure
+ .use(openSessionMiddleware)
+ .input(rejectActionSchema)
+ .mutation(async ({ input, ctx: { services } }) => {
+ const { actionService } = services
+ return actionService.reject(input)
+ })
diff --git a/packages/extension/src/background/__new/procedures/action/rejectAll.ts b/packages/extension/src/background/__new/procedures/action/rejectAll.ts
new file mode 100644
index 000000000..546fa2bf8
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/action/rejectAll.ts
@@ -0,0 +1,9 @@
+import { openSessionMiddleware } from "../../middleware/session"
+import { extensionOnlyProcedure } from "../permissions"
+
+export const rejectAllActionsProcedure = extensionOnlyProcedure
+ .use(openSessionMiddleware)
+ .mutation(async ({ ctx: { services } }) => {
+ const { actionService } = services
+ return actionService.rejectAll()
+ })
diff --git a/packages/extension/src/background/__new/procedures/addressBook/add.ts b/packages/extension/src/background/__new/procedures/addressBook/add.ts
new file mode 100644
index 000000000..314360d95
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/addressBook/add.ts
@@ -0,0 +1,17 @@
+import { z } from "zod"
+
+import {
+ addressBookContactNoIdSchema,
+ addressBookContactSchema,
+} from "../../../../shared/addressBook/schema"
+import { addressBookService } from "../../../../shared/addressBook/service"
+import { openSessionMiddleware } from "../../middleware/session"
+import { extensionOnlyProcedure } from "../permissions"
+
+export const addAddressBookContactProcedure = extensionOnlyProcedure
+ .use(openSessionMiddleware)
+ .input(z.union([addressBookContactNoIdSchema, addressBookContactSchema]))
+ .output(addressBookContactSchema)
+ .mutation(async ({ input }) => {
+ return addressBookService.add(input)
+ })
diff --git a/packages/extension/src/background/__new/procedures/addressBook/index.ts b/packages/extension/src/background/__new/procedures/addressBook/index.ts
new file mode 100644
index 000000000..b55204376
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/addressBook/index.ts
@@ -0,0 +1,10 @@
+import { router } from "../../trpc"
+import { addAddressBookContactProcedure } from "./add"
+import { removeAddressBookContactProcedure } from "./remove"
+import { updateAddressBookContactProcedure } from "./update"
+
+export const addressBookRouter = router({
+ add: addAddressBookContactProcedure,
+ update: updateAddressBookContactProcedure,
+ remove: removeAddressBookContactProcedure,
+})
diff --git a/packages/extension/src/background/__new/procedures/addressBook/remove.ts b/packages/extension/src/background/__new/procedures/addressBook/remove.ts
new file mode 100644
index 000000000..b25922c3d
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/addressBook/remove.ts
@@ -0,0 +1,12 @@
+import { addressBookContactSchema } from "../../../../shared/addressBook/schema"
+import { addressBookService } from "../../../../shared/addressBook/service"
+import { openSessionMiddleware } from "../../middleware/session"
+import { extensionOnlyProcedure } from "../permissions"
+
+export const removeAddressBookContactProcedure = extensionOnlyProcedure
+ .use(openSessionMiddleware)
+ .input(addressBookContactSchema)
+ .output(addressBookContactSchema)
+ .mutation(async ({ input }) => {
+ return addressBookService.remove(input)
+ })
diff --git a/packages/extension/src/background/__new/procedures/addressBook/update.ts b/packages/extension/src/background/__new/procedures/addressBook/update.ts
new file mode 100644
index 000000000..e5c570756
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/addressBook/update.ts
@@ -0,0 +1,12 @@
+import { addressBookContactSchema } from "../../../../shared/addressBook/schema"
+import { addressBookService } from "../../../../shared/addressBook/service"
+import { openSessionMiddleware } from "../../middleware/session"
+import { extensionOnlyProcedure } from "../permissions"
+
+export const updateAddressBookContactProcedure = extensionOnlyProcedure
+ .use(openSessionMiddleware)
+ .input(addressBookContactSchema)
+ .output(addressBookContactSchema)
+ .mutation(async ({ input }) => {
+ return addressBookService.update(input)
+ })
diff --git a/packages/extension/src/background/__new/procedures/argentAccount/addAccount.ts b/packages/extension/src/background/__new/procedures/argentAccount/addAccount.ts
new file mode 100644
index 000000000..742cc0122
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/argentAccount/addAccount.ts
@@ -0,0 +1,17 @@
+import { z } from "zod"
+
+import { openSessionMiddleware } from "../../middleware/session"
+import { extensionOnlyProcedure } from "../permissions"
+
+export const addAccountProcedure = extensionOnlyProcedure
+ .use(openSessionMiddleware)
+ .output(z.string())
+ .mutation(
+ async ({
+ ctx: {
+ services: { argentAccountService },
+ },
+ }) => {
+ return await argentAccountService.addAccount()
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/argentAccount/confirmEmail.ts b/packages/extension/src/background/__new/procedures/argentAccount/confirmEmail.ts
new file mode 100644
index 000000000..260e442d1
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/argentAccount/confirmEmail.ts
@@ -0,0 +1,22 @@
+import { z } from "zod"
+
+import { openSessionMiddleware } from "../../middleware/session"
+import { extensionOnlyProcedure } from "../permissions"
+
+const confirmEmailSchema = z.object({
+ code: z.string(),
+})
+
+export const confirmEmailProcedure = extensionOnlyProcedure
+ .use(openSessionMiddleware)
+ .input(confirmEmailSchema)
+ .mutation(
+ async ({
+ input: { code },
+ ctx: {
+ services: { argentAccountService },
+ },
+ }) => {
+ return await argentAccountService.confirmEmail(code)
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/argentAccount/getPreferences.ts b/packages/extension/src/background/__new/procedures/argentAccount/getPreferences.ts
new file mode 100644
index 000000000..ff8102594
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/argentAccount/getPreferences.ts
@@ -0,0 +1,17 @@
+import { openSessionMiddleware } from "../../middleware/session"
+import { extensionOnlyProcedure } from "../permissions"
+import { preferencesEndpointPayload } from "./updatePreferences.model"
+
+export const getPreferencesProcedure = extensionOnlyProcedure
+ .use(openSessionMiddleware)
+ .output(preferencesEndpointPayload.optional())
+ .query(
+ async ({
+ ctx: {
+ services: { argentAccountService },
+ },
+ }) => {
+ const response = await argentAccountService.getPreferences()
+ return response
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/argentAccount/index.ts b/packages/extension/src/background/__new/procedures/argentAccount/index.ts
new file mode 100644
index 000000000..b0e8c851e
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/argentAccount/index.ts
@@ -0,0 +1,20 @@
+import { router } from "../../trpc"
+import { addAccountProcedure } from "./addAccount"
+import { requestEmailProcedure } from "./requestEmail"
+import { confirmEmailProcedure } from "./confirmEmail"
+import { validateAccountProcedure } from "./validateAccount"
+import { isTokenExpiredProcedure } from "./isTokenExpired"
+import { logoutProcedure } from "./logout"
+import { updatePreferencesProcedure } from "./updatePreferences"
+import { getPreferencesProcedure } from "./getPreferences"
+
+export const argentAccountRouter = router({
+ addAccount: addAccountProcedure,
+ validateAccount: validateAccountProcedure,
+ requestEmail: requestEmailProcedure,
+ confirmEmail: confirmEmailProcedure,
+ isTokenExpired: isTokenExpiredProcedure,
+ logout: logoutProcedure,
+ updatePreferences: updatePreferencesProcedure,
+ getPreferences: getPreferencesProcedure,
+})
diff --git a/packages/extension/src/background/__new/procedures/argentAccount/isTokenExpired.ts b/packages/extension/src/background/__new/procedures/argentAccount/isTokenExpired.ts
new file mode 100644
index 000000000..7798ca1b3
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/argentAccount/isTokenExpired.ts
@@ -0,0 +1,17 @@
+import { z } from "zod"
+
+import { openSessionMiddleware } from "../../middleware/session"
+import { extensionOnlyProcedure } from "../permissions"
+
+export const isTokenExpiredProcedure = extensionOnlyProcedure
+ .use(openSessionMiddleware)
+ .output(z.boolean())
+ .query(
+ async ({
+ ctx: {
+ services: { argentAccountService },
+ },
+ }) => {
+ return await argentAccountService.isTokenExpired()
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/argentAccount/logout.ts b/packages/extension/src/background/__new/procedures/argentAccount/logout.ts
new file mode 100644
index 000000000..e55a59a5f
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/argentAccount/logout.ts
@@ -0,0 +1,14 @@
+import { z } from "zod"
+
+import { extensionOnlyProcedure } from "../permissions"
+import { resetDevice } from "../../../../shared/shield/jwt"
+
+export const logoutProcedure = extensionOnlyProcedure
+ .output(z.void())
+ .mutation(async () => {
+ try {
+ await resetDevice()
+ } catch (error) {
+ throw new Error("Error while logging out", { cause: error })
+ }
+ })
diff --git a/packages/extension/src/background/__new/procedures/argentAccount/requestEmail.ts b/packages/extension/src/background/__new/procedures/argentAccount/requestEmail.ts
new file mode 100644
index 000000000..a3ca64135
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/argentAccount/requestEmail.ts
@@ -0,0 +1,22 @@
+import { z } from "zod"
+
+import { openSessionMiddleware } from "../../middleware/session"
+import { extensionOnlyProcedure } from "../permissions"
+
+const requestEmailSchema = z.object({
+ email: z.string(),
+})
+
+export const requestEmailProcedure = extensionOnlyProcedure
+ .use(openSessionMiddleware)
+ .input(requestEmailSchema)
+ .mutation(
+ async ({
+ input: { email },
+ ctx: {
+ services: { argentAccountService },
+ },
+ }) => {
+ return await argentAccountService.requestEmail(email)
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/argentAccount/updatePreferences.model.ts b/packages/extension/src/background/__new/procedures/argentAccount/updatePreferences.model.ts
new file mode 100644
index 000000000..be639cae1
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/argentAccount/updatePreferences.model.ts
@@ -0,0 +1,14 @@
+import { z } from "zod"
+
+export const preferencesSchema = z.object({
+ value: z.string(),
+ platform: z.enum(["ios", "argentx", "android"]).nullable(),
+})
+
+export type Preferences = z.infer
+
+export const preferencesEndpointPayload = z.object({
+ preferences: z.record(preferencesSchema),
+})
+
+export type PreferencesPayload = z.infer
diff --git a/packages/extension/src/background/__new/procedures/argentAccount/updatePreferences.ts b/packages/extension/src/background/__new/procedures/argentAccount/updatePreferences.ts
new file mode 100644
index 000000000..51d961106
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/argentAccount/updatePreferences.ts
@@ -0,0 +1,19 @@
+import { openSessionMiddleware } from "../../middleware/session"
+import { extensionOnlyProcedure } from "../permissions"
+import { preferencesEndpointPayload } from "./updatePreferences.model"
+
+export const updatePreferencesProcedure = extensionOnlyProcedure
+ .use(openSessionMiddleware)
+ .input(preferencesEndpointPayload)
+ .output(preferencesEndpointPayload.optional())
+ .mutation(
+ async ({
+ input: data,
+ ctx: {
+ services: { argentAccountService },
+ },
+ }) => {
+ const response = await argentAccountService.updatePreferences(data)
+ return response
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/argentAccount/validateAccount.ts b/packages/extension/src/background/__new/procedures/argentAccount/validateAccount.ts
new file mode 100644
index 000000000..ae5f3ab16
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/argentAccount/validateAccount.ts
@@ -0,0 +1,14 @@
+import { openSessionMiddleware } from "../../middleware/session"
+import { extensionOnlyProcedure } from "../permissions"
+
+export const validateAccountProcedure = extensionOnlyProcedure
+ .use(openSessionMiddleware)
+ .mutation(
+ async ({
+ ctx: {
+ services: { argentAccountService },
+ },
+ }) => {
+ return await argentAccountService.validateAccount()
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/multisig/addAccount.ts b/packages/extension/src/background/__new/procedures/multisig/addAccount.ts
new file mode 100644
index 000000000..205f08b08
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/multisig/addAccount.ts
@@ -0,0 +1,17 @@
+import { openSessionMiddleware } from "../../middleware/session"
+import { extensionOnlyProcedure } from "../permissions"
+import { addAccountSchema } from "../../../../shared/multisig/multisig.model"
+
+export const addAccountProcedure = extensionOnlyProcedure
+ .use(openSessionMiddleware)
+ .input(addAccountSchema)
+ .mutation(
+ async ({
+ input,
+ ctx: {
+ services: { multisigService },
+ },
+ }) => {
+ return await multisigService.addAccount(input)
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/multisig/addOwner.ts b/packages/extension/src/background/__new/procedures/multisig/addOwner.ts
new file mode 100644
index 000000000..23d877ed8
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/multisig/addOwner.ts
@@ -0,0 +1,17 @@
+import { openSessionMiddleware } from "../../middleware/session"
+import { extensionOnlyProcedure } from "../permissions"
+import { addOwnerMultisigSchema } from "../../../../shared/multisig/multisig.model"
+
+export const addOwnerProcedure = extensionOnlyProcedure
+ .use(openSessionMiddleware)
+ .input(addOwnerMultisigSchema)
+ .mutation(
+ async ({
+ input,
+ ctx: {
+ services: { multisigService },
+ },
+ }) => {
+ return await multisigService.addOwner(input)
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/multisig/addPendingAccount.ts b/packages/extension/src/background/__new/procedures/multisig/addPendingAccount.ts
new file mode 100644
index 000000000..f94a253dd
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/multisig/addPendingAccount.ts
@@ -0,0 +1,22 @@
+import { z } from "zod"
+
+import { openSessionMiddleware } from "../../middleware/session"
+import { extensionOnlyProcedure } from "../permissions"
+
+const addPendingAccountSchema = z.object({
+ networkId: z.string(),
+})
+
+export const addPendingAccountProcedure = extensionOnlyProcedure
+ .use(openSessionMiddleware)
+ .input(addPendingAccountSchema)
+ .mutation(
+ async ({
+ input: { networkId },
+ ctx: {
+ services: { multisigService },
+ },
+ }) => {
+ return await multisigService.addPendingAccount(networkId)
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/multisig/addTransactionSignature.ts b/packages/extension/src/background/__new/procedures/multisig/addTransactionSignature.ts
new file mode 100644
index 000000000..5f917c13a
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/multisig/addTransactionSignature.ts
@@ -0,0 +1,22 @@
+import { z } from "zod"
+
+import { openSessionMiddleware } from "../../middleware/session"
+import { extensionOnlyProcedure } from "../permissions"
+
+const addTransactionSignatureSchema = z.object({
+ requestId: z.string(),
+})
+
+export const addTransactionSignatureProcedure = extensionOnlyProcedure
+ .use(openSessionMiddleware)
+ .input(addTransactionSignatureSchema)
+ .mutation(
+ async ({
+ input: { requestId },
+ ctx: {
+ services: { multisigService },
+ },
+ }) => {
+ return await multisigService.addTransactionSignature(requestId)
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/multisig/deploy.ts b/packages/extension/src/background/__new/procedures/multisig/deploy.ts
new file mode 100644
index 000000000..5775481f8
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/multisig/deploy.ts
@@ -0,0 +1,17 @@
+import { openSessionMiddleware } from "../../middleware/session"
+import { extensionOnlyProcedure } from "../permissions"
+import { baseWalletAccountSchema } from "../../../../shared/wallet.model"
+
+export const deployProcedure = extensionOnlyProcedure
+ .use(openSessionMiddleware)
+ .input(baseWalletAccountSchema)
+ .mutation(
+ async ({
+ input,
+ ctx: {
+ services: { multisigService },
+ },
+ }) => {
+ return await multisigService.deploy(input)
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/multisig/index.ts b/packages/extension/src/background/__new/procedures/multisig/index.ts
new file mode 100644
index 000000000..12761464f
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/multisig/index.ts
@@ -0,0 +1,20 @@
+import { router } from "../../trpc"
+import { addAccountProcedure } from "./addAccount"
+import { addOwnerProcedure } from "./addOwner"
+import { removeOwnerProcedure } from "./removeOwner"
+import { updateThresholdProcedure } from "./updateThreshold"
+import { deployProcedure } from "./deploy"
+import { addPendingAccountProcedure } from "./addPendingAccount"
+import { addTransactionSignatureProcedure } from "./addTransactionSignature"
+import { replaceOwnerProcedure } from "./replaceOwner"
+
+export const multisigRouter = router({
+ addAccount: addAccountProcedure,
+ addPendingAccount: addPendingAccountProcedure,
+ addOwner: addOwnerProcedure,
+ removeOwner: removeOwnerProcedure,
+ replaceOwner: replaceOwnerProcedure,
+ updateThreshold: updateThresholdProcedure,
+ deploy: deployProcedure,
+ addTransactionSignature: addTransactionSignatureProcedure,
+})
diff --git a/packages/extension/src/background/__new/procedures/multisig/removeOwner.ts b/packages/extension/src/background/__new/procedures/multisig/removeOwner.ts
new file mode 100644
index 000000000..8c2f29881
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/multisig/removeOwner.ts
@@ -0,0 +1,17 @@
+import { openSessionMiddleware } from "../../middleware/session"
+import { extensionOnlyProcedure } from "../permissions"
+import { removeOwnerMultisigSchema } from "../../../../shared/multisig/multisig.model"
+
+export const removeOwnerProcedure = extensionOnlyProcedure
+ .use(openSessionMiddleware)
+ .input(removeOwnerMultisigSchema)
+ .mutation(
+ async ({
+ input,
+ ctx: {
+ services: { multisigService },
+ },
+ }) => {
+ return await multisigService.removeOwner(input)
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/multisig/replaceOwner.ts b/packages/extension/src/background/__new/procedures/multisig/replaceOwner.ts
new file mode 100644
index 000000000..52744e297
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/multisig/replaceOwner.ts
@@ -0,0 +1,17 @@
+import { openSessionMiddleware } from "../../middleware/session"
+import { extensionOnlyProcedure } from "../permissions"
+import { replaceOwnerMultisigSchema } from "../../../../shared/multisig/multisig.model"
+
+export const replaceOwnerProcedure = extensionOnlyProcedure
+ .use(openSessionMiddleware)
+ .input(replaceOwnerMultisigSchema)
+ .mutation(
+ async ({
+ input,
+ ctx: {
+ services: { multisigService },
+ },
+ }) => {
+ return await multisigService.replaceOwner(input)
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/multisig/updateThreshold.ts b/packages/extension/src/background/__new/procedures/multisig/updateThreshold.ts
new file mode 100644
index 000000000..9a4e5a505
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/multisig/updateThreshold.ts
@@ -0,0 +1,17 @@
+import { openSessionMiddleware } from "../../middleware/session"
+import { extensionOnlyProcedure } from "../permissions"
+import { updateMultisigThresholdSchema } from "../../../../shared/multisig/multisig.model"
+
+export const updateThresholdProcedure = extensionOnlyProcedure
+ .use(openSessionMiddleware)
+ .input(updateMultisigThresholdSchema)
+ .mutation(
+ async ({
+ input,
+ ctx: {
+ services: { multisigService },
+ },
+ }) => {
+ return await multisigService.updateThreshold(input)
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/network/add.ts b/packages/extension/src/background/__new/procedures/network/add.ts
deleted file mode 100644
index e8a845c67..000000000
--- a/packages/extension/src/background/__new/procedures/network/add.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { addNetwork, networkSchema } from "../../../../shared/network"
-import { openSessionMiddleware } from "../../middleware/session"
-import { extensionOnlyProcedure } from "../permissions"
-
-export const addNetworkProcedure = extensionOnlyProcedure
- .use(openSessionMiddleware)
- .input(networkSchema)
- .mutation(async ({ input }) => {
- await addNetwork(input)
- return true
- })
diff --git a/packages/extension/src/background/__new/procedures/network/index.ts b/packages/extension/src/background/__new/procedures/network/index.ts
deleted file mode 100644
index 84b83e578..000000000
--- a/packages/extension/src/background/__new/procedures/network/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { router } from "../../trpc"
-import { addNetworkProcedure } from "./add"
-
-export const networkRouter = router({
- add: addNetworkProcedure,
-})
diff --git a/packages/extension/src/background/__new/procedures/recovery/recoverSeedphrase.ts b/packages/extension/src/background/__new/procedures/recovery/recoverSeedphrase.ts
index 07da90069..be27d8031 100644
--- a/packages/extension/src/background/__new/procedures/recovery/recoverSeedphrase.ts
+++ b/packages/extension/src/background/__new/procedures/recovery/recoverSeedphrase.ts
@@ -1,7 +1,6 @@
import { compactDecrypt } from "jose"
import { z } from "zod"
-import { accountService } from "../../../../shared/account/service"
import { bytesToUft8 } from "../../../../shared/utils/encode"
import { getMessagingKeys } from "../../../keys/messagingKeys"
import { extensionOnlyProcedure } from "../permissions"
@@ -10,12 +9,8 @@ const recoverSeedphraseSchema = z.object({
jwe: z.string(),
})
-const recoverSeedphraseResponseSchema = z.object({
- isSuccess: z.boolean(),
-})
export const recoverSeedphraseProcedure = extensionOnlyProcedure
.input(recoverSeedphraseSchema)
- .output(recoverSeedphraseResponseSchema)
.mutation(
async ({
input: { jwe },
@@ -35,7 +30,7 @@ export const recoverSeedphraseProcedure = extensionOnlyProcedure
} = JSON.parse(bytesToUft8(plaintext))
await wallet.restoreSeedPhrase(seedPhrase, newPassword)
- void transactionTracker.loadHistory(await accountService.get())
+ void transactionTracker.loadHistory()
return { isSuccess: true }
},
)
diff --git a/packages/extension/src/background/__new/procedures/session/checkPassword.ts b/packages/extension/src/background/__new/procedures/session/checkPassword.ts
new file mode 100644
index 000000000..8fec02090
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/session/checkPassword.ts
@@ -0,0 +1,36 @@
+import { z } from "zod"
+import { extensionOnlyProcedure } from "../permissions"
+import { compactDecrypt } from "jose"
+import { BUGGY_bytesToUft8 } from "../../../../shared/utils/encode"
+import { SessionError } from "../../../../shared/errors/session"
+
+const checkPasswordSchema = z.string()
+
+export const checkPasswordProcedure = extensionOnlyProcedure
+ .input(checkPasswordSchema)
+ .output(z.boolean())
+ .mutation(
+ async ({
+ input: encryptedPassword,
+ ctx: {
+ services: { wallet, messagingKeys },
+ },
+ }) => {
+ try {
+ const { plaintext } = await compactDecrypt(
+ encryptedPassword,
+ messagingKeys.privateKey,
+ )
+ const password = BUGGY_bytesToUft8(plaintext)
+ if (await wallet.checkPassword(password)) {
+ return true
+ }
+ return false
+ } catch (error) {
+ throw new SessionError({
+ code: "PASSWORD_CHECK_FAILED",
+ options: { error },
+ })
+ }
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/session/index.ts b/packages/extension/src/background/__new/procedures/session/index.ts
new file mode 100644
index 000000000..22280991c
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/session/index.ts
@@ -0,0 +1,12 @@
+import { router } from "../../trpc"
+import { checkPasswordProcedure } from "./checkPassword"
+import { isPasswordSetProcedure } from "./isPasswordSet"
+import { startProcedure } from "./start"
+import { stopProcedure } from "./stop"
+
+export const sessionRouter = router({
+ start: startProcedure,
+ stop: stopProcedure,
+ checkPassword: checkPasswordProcedure,
+ isPasswordSet: isPasswordSetProcedure,
+})
diff --git a/packages/extension/src/background/__new/procedures/session/isPasswordSet.ts b/packages/extension/src/background/__new/procedures/session/isPasswordSet.ts
new file mode 100644
index 000000000..fe1a62f91
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/session/isPasswordSet.ts
@@ -0,0 +1,14 @@
+import { z } from "zod"
+import { extensionOnlyProcedure } from "../permissions"
+
+export const isPasswordSetProcedure = extensionOnlyProcedure
+ .output(z.boolean())
+ .query(
+ async ({
+ ctx: {
+ services: { wallet },
+ },
+ }) => {
+ return wallet.isSessionOpen()
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/session/start.ts b/packages/extension/src/background/__new/procedures/session/start.ts
new file mode 100644
index 000000000..a1736a00e
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/session/start.ts
@@ -0,0 +1,73 @@
+import { z } from "zod"
+
+import { extensionOnlyProcedure } from "../permissions"
+import { compactDecrypt } from "jose"
+import { BUGGY_bytesToUft8, bytesToUft8 } from "../../../../shared/utils/encode"
+import { respond } from "../../../respond"
+import { walletAccountSchema } from "../../../../shared/wallet.model"
+import { SessionError } from "../../../../shared/errors/session"
+
+const startSchema = z.string()
+
+export const startProcedure = extensionOnlyProcedure
+ .input(startSchema)
+ .output(walletAccountSchema.optional())
+ .mutation(
+ async ({
+ input: encryptedPassword,
+ ctx: {
+ services: { wallet, messagingKeys },
+ },
+ }) => {
+ try {
+ const { plaintext } = await compactDecrypt(
+ encryptedPassword,
+ messagingKeys.privateKey,
+ )
+
+ const [BUGGY_sessionPassword, sessionPassword] = [
+ BUGGY_bytesToUft8(plaintext),
+ bytesToUft8(plaintext),
+ ]
+
+ const progressCallback = (percent: number) =>
+ void respond({ type: "LOADING_PROGRESS", data: percent })
+
+ try {
+ // Check for the buggy bytesToUtf8 first because most users will have the buggy version.
+ const result = await wallet.startSession(
+ BUGGY_sessionPassword,
+ progressCallback,
+ )
+
+ if (!result) {
+ throw new SessionError({
+ code: "START_SESSION_FAILED_BUGGY",
+ })
+ }
+ } catch (error) {
+ // For users who have superscript characters in their password, the buggy version of bytesToUtf8 will fail.
+ // That's why we try the correct version here.
+ const result = await wallet.startSession(
+ sessionPassword,
+ progressCallback,
+ )
+
+ if (!result) {
+ throw new SessionError({
+ code: "START_SESSION_FAILED",
+ options: { error },
+ })
+ }
+ }
+
+ const selectedAccount = await wallet.getSelectedAccount()
+ return selectedAccount
+ } catch (error) {
+ throw new SessionError({
+ code: "START_SESSION_FAILED",
+ options: { error },
+ })
+ }
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/session/stop.ts b/packages/extension/src/background/__new/procedures/session/stop.ts
new file mode 100644
index 000000000..38ee0814e
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/session/stop.ts
@@ -0,0 +1,13 @@
+import { extensionOnlyProcedure } from "../permissions"
+import { respond } from "../../../respond"
+
+export const stopProcedure = extensionOnlyProcedure.mutation(
+ async ({
+ ctx: {
+ services: { wallet },
+ },
+ }) => {
+ void wallet.lock()
+ return respond({ type: "DISCONNECT_ACCOUNT" })
+ },
+)
diff --git a/packages/extension/src/background/__new/procedures/tokens/addToken.ts b/packages/extension/src/background/__new/procedures/tokens/addToken.ts
new file mode 100644
index 000000000..c8e397ff7
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/tokens/addToken.ts
@@ -0,0 +1,10 @@
+import { extensionOnlyProcedure } from "../permissions"
+import { TokenSchema } from "../../../../shared/token/__new/types/token.model"
+import { tokenService } from "../../../../shared/token/__new/service"
+
+export const addTokenProcedure = extensionOnlyProcedure
+ .input(TokenSchema)
+ .mutation(async ({ input: token }) => {
+ // tokens that are added from the UI should always be shown with custom flag, even if the balance is 0
+ return await tokenService.addToken({ ...token, custom: true })
+ })
diff --git a/packages/extension/src/background/__new/procedures/tokens/fetchAccountBalance.ts b/packages/extension/src/background/__new/procedures/tokens/fetchAccountBalance.ts
new file mode 100644
index 000000000..7ed30b4da
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/tokens/fetchAccountBalance.ts
@@ -0,0 +1,36 @@
+import { addressSchema } from "@argent/shared"
+import { z } from "zod"
+import { extensionOnlyProcedure } from "../permissions"
+import { tokenService } from "../../../../shared/token/__new/service"
+import { equalToken } from "../../../../shared/token/__new/utils"
+import { TokenError } from "../../../../shared/errors/token"
+
+const fetchTokenBalanceSchema = z.object({
+ tokenAddress: addressSchema,
+ accountAddress: addressSchema,
+ networkId: z.string(),
+})
+
+export const fetchTokenBalanceProcedure = extensionOnlyProcedure
+ .input(fetchTokenBalanceSchema)
+ .output(z.string())
+ .mutation(async ({ input: { tokenAddress, accountAddress, networkId } }) => {
+ const account = { address: accountAddress, networkId }
+ const baseToken = { address: tokenAddress, networkId }
+ const [token] = await tokenService.getTokens((t) =>
+ equalToken(t, baseToken),
+ )
+
+ if (!token) {
+ throw new TokenError({
+ code: "TOKEN_NOT_FOUND",
+ message: `Token ${tokenAddress} not found`,
+ })
+ }
+ const [tokenBalance] = await tokenService.fetchTokenBalancesFromOnChain(
+ account,
+ token,
+ )
+
+ return tokenBalance.balance
+ })
diff --git a/packages/extension/src/background/__new/procedures/tokens/fetchDetails.ts b/packages/extension/src/background/__new/procedures/tokens/fetchDetails.ts
new file mode 100644
index 000000000..b040893a2
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/tokens/fetchDetails.ts
@@ -0,0 +1,13 @@
+import { extensionOnlyProcedure } from "../permissions"
+import {
+ BaseTokenSchema,
+ RequestTokenSchema,
+} from "../../../../shared/token/__new/types/token.model"
+import { tokenService } from "../../../../shared/token/__new/service"
+
+export const fetchDetailsProcedure = extensionOnlyProcedure
+ .input(BaseTokenSchema)
+ .output(RequestTokenSchema)
+ .query(async ({ input: baseToken }) => {
+ return await tokenService.fetchTokenDetails(baseToken)
+ })
diff --git a/packages/extension/src/background/__new/procedures/tokens/getAccountBalance.ts b/packages/extension/src/background/__new/procedures/tokens/getAccountBalance.ts
new file mode 100644
index 000000000..d8221f324
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/tokens/getAccountBalance.ts
@@ -0,0 +1,34 @@
+import { addressSchema } from "@argent/shared"
+import { z } from "zod"
+import { extensionOnlyProcedure } from "../permissions"
+import { tokenService } from "../../../../shared/token/__new/service"
+import { equalToken } from "../../../../shared/token/__new/utils"
+import { BaseTokenWithBalanceSchema } from "../../../../shared/token/__new/types/tokenBalance.model"
+import { TokenError } from "../../../../shared/errors/token"
+
+const getAccountBalanceSchema = z.object({
+ tokenAddress: addressSchema,
+ accountAddress: addressSchema,
+ networkId: z.string(),
+})
+
+export const getAccountBalanceProcedure = extensionOnlyProcedure
+ .input(getAccountBalanceSchema)
+ .output(BaseTokenWithBalanceSchema)
+ .query(async ({ input: { tokenAddress, accountAddress, networkId } }) => {
+ const [token] = await tokenService.getTokens((t) =>
+ equalToken(t, { address: tokenAddress, networkId }),
+ )
+ if (!token) {
+ throw new TokenError({
+ code: "TOKEN_NOT_FOUND",
+ message: `Token ${tokenAddress} not found`,
+ })
+ }
+ const account = { address: accountAddress, networkId }
+ const [tokenBalance] = await tokenService.getTokenBalancesForAccount(
+ account,
+ [token],
+ )
+ return tokenBalance
+ })
diff --git a/packages/extension/src/background/__new/procedures/tokens/getAllTokenBalances.ts b/packages/extension/src/background/__new/procedures/tokens/getAllTokenBalances.ts
new file mode 100644
index 000000000..a327d554e
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/tokens/getAllTokenBalances.ts
@@ -0,0 +1,30 @@
+import { addressSchema } from "@argent/shared"
+
+import { z } from "zod"
+import { extensionOnlyProcedure } from "../permissions"
+import { tokenService } from "../../../../shared/token/__new/service"
+import { equalToken } from "../../../../shared/token/__new/utils"
+import { BaseTokenWithBalanceSchema } from "../../../../shared/token/__new/types/tokenBalance.model"
+
+const getAllTokenBalancesSchema = z.object({
+ tokenAddresses: z.array(addressSchema),
+ accountAddress: addressSchema,
+ networkId: z.string(),
+})
+
+export const getAllTokenBalancesProcedure = extensionOnlyProcedure
+ .input(getAllTokenBalancesSchema)
+ .output(z.array(BaseTokenWithBalanceSchema))
+ .query(async ({ input: { tokenAddresses, accountAddress, networkId } }) => {
+ const tokens = await tokenService.getTokens((t) =>
+ tokenAddresses.some((tokenAddress) =>
+ equalToken(t, { address: tokenAddress, networkId }),
+ ),
+ )
+ const account = { address: accountAddress, networkId }
+ const tokenBalances = await tokenService.getTokenBalancesForAccount(
+ account,
+ tokens,
+ )
+ return tokenBalances
+ })
diff --git a/packages/extension/src/background/__new/procedures/tokens/getCurrencyValueForTokens.ts b/packages/extension/src/background/__new/procedures/tokens/getCurrencyValueForTokens.ts
new file mode 100644
index 000000000..cd578d6be
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/tokens/getCurrencyValueForTokens.ts
@@ -0,0 +1,15 @@
+import { z } from "zod"
+import { BaseTokenWithBalanceSchema } from "../../../../shared/token/__new/types/tokenBalance.model"
+import { TokenWithBalanceAndPriceSchema } from "../../../../shared/token/__new/types/tokenPrice.model"
+import { extensionOnlyProcedure } from "../permissions"
+import { tokenService } from "../../../../shared/token/__new/service"
+
+export const getCurrencyValueForTokensProcedure = extensionOnlyProcedure
+ .input(z.array(BaseTokenWithBalanceSchema))
+ .output(z.array(TokenWithBalanceAndPriceSchema))
+ .query(async ({ input: tokenWithBalances }) => {
+ const tokensWithBalancesAndPrice =
+ await tokenService.getCurrencyValueForTokens(tokenWithBalances)
+
+ return tokensWithBalancesAndPrice
+ })
diff --git a/packages/extension/src/background/__new/procedures/tokens/index.ts b/packages/extension/src/background/__new/procedures/tokens/index.ts
new file mode 100644
index 000000000..a031eb8cf
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/tokens/index.ts
@@ -0,0 +1,18 @@
+import { router } from "../../trpc"
+import { addTokenProcedure } from "./addToken"
+import { fetchTokenBalanceProcedure } from "./fetchAccountBalance"
+import { fetchDetailsProcedure } from "./fetchDetails"
+import { getAccountBalanceProcedure } from "./getAccountBalance"
+import { getAllTokenBalancesProcedure } from "./getAllTokenBalances"
+import { getCurrencyValueForTokensProcedure } from "./getCurrencyValueForTokens"
+import { removeTokenProcedure } from "./removeToken"
+
+export const tokensRouter = router({
+ addToken: addTokenProcedure,
+ removeToken: removeTokenProcedure,
+ fetchDetails: fetchDetailsProcedure,
+ fetchTokenBalance: fetchTokenBalanceProcedure,
+ getAccountBalance: getAccountBalanceProcedure,
+ getAllTokenBalances: getAllTokenBalancesProcedure,
+ getCurrencyValueForTokens: getCurrencyValueForTokensProcedure,
+})
diff --git a/packages/extension/src/background/__new/procedures/tokens/removeToken.ts b/packages/extension/src/background/__new/procedures/tokens/removeToken.ts
new file mode 100644
index 000000000..c704eebdb
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/tokens/removeToken.ts
@@ -0,0 +1,9 @@
+import { extensionOnlyProcedure } from "../permissions"
+import { BaseTokenSchema } from "../../../../shared/token/__new/types/token.model"
+import { tokenService } from "../../../../shared/token/__new/service"
+
+export const removeTokenProcedure = extensionOnlyProcedure
+ .input(BaseTokenSchema)
+ .mutation(async ({ input: baseToken }) => {
+ return await tokenService.removeToken(baseToken)
+ })
diff --git a/packages/extension/src/background/__new/procedures/transfer/index.ts b/packages/extension/src/background/__new/procedures/transfer/index.ts
new file mode 100644
index 000000000..1abeaa62d
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/transfer/index.ts
@@ -0,0 +1,6 @@
+import { router } from "../../trpc"
+import { sendProcedure } from "./send"
+
+export const transferRouter = router({
+ send: sendProcedure,
+})
diff --git a/packages/extension/src/background/__new/procedures/transfer/send.ts b/packages/extension/src/background/__new/procedures/transfer/send.ts
new file mode 100644
index 000000000..fde4c0854
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/transfer/send.ts
@@ -0,0 +1,33 @@
+import { addressSchema } from "@argent/shared"
+import { z } from "zod"
+
+import { extensionOnlyProcedure } from "../permissions"
+
+const sendSchema = z.object({
+ transactions: z.object({
+ contractAddress: addressSchema,
+ entrypoint: z.string(),
+ calldata: z.string().array(),
+ }),
+})
+
+export const sendProcedure = extensionOnlyProcedure
+ .input(sendSchema)
+ .output(z.string())
+ .mutation(
+ async ({
+ input: { transactions },
+ ctx: {
+ services: { actionService },
+ },
+ }) => {
+ const { meta } = await actionService.add({
+ type: "TRANSACTION",
+ payload: {
+ transactions,
+ },
+ })
+
+ return meta.hash
+ },
+ )
diff --git a/packages/extension/src/background/__new/procedures/udc/getConstructorParams.ts b/packages/extension/src/background/__new/procedures/udc/getConstructorParams.ts
new file mode 100644
index 000000000..d6658071f
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/udc/getConstructorParams.ts
@@ -0,0 +1,51 @@
+import { LegacyContractClass } from "starknet"
+import { z } from "zod"
+
+import { getProvider } from "../../../../shared/network"
+import { networkService } from "../../../../shared/network/service"
+import { extensionOnlyProcedure } from "../permissions"
+import { UdcError } from "../../../../shared/errors/udc"
+
+const getConstructorParamsSchema = z.object({
+ networkId: z.string(),
+ classHash: z.string(),
+})
+
+const basicContractClassSchema = z.object({
+ abi: z.array(z.any()),
+})
+
+export const getConstructorParamsProcedure = extensionOnlyProcedure
+ .input(getConstructorParamsSchema)
+ .output(
+ z.custom((item) => {
+ return basicContractClassSchema.parse(item)
+ }),
+ )
+ .query(async ({ input: { networkId, classHash } }) => {
+ const network = await networkService.getById(networkId)
+ const provider = getProvider(network)
+
+ try {
+ if (!("getClassByHash" in provider)) {
+ throw new UdcError({
+ code: "FETCH_CONTRACT_CONTRUCTOR_PARAMS",
+ })
+ }
+
+ const contract = await provider.getClassByHash(classHash)
+
+ if ("sierra_program" in contract) {
+ throw new UdcError({
+ code: "CAIRO_1_NOT_SUPPORTED",
+ })
+ }
+
+ return contract
+ } catch (error) {
+ throw new UdcError({
+ options: { error },
+ code: "FETCH_CONTRACT_CONTRUCTOR_PARAMS",
+ })
+ }
+ })
diff --git a/packages/extension/src/background/__new/procedures/udc/index.ts b/packages/extension/src/background/__new/procedures/udc/index.ts
new file mode 100644
index 000000000..60bd6b88c
--- /dev/null
+++ b/packages/extension/src/background/__new/procedures/udc/index.ts
@@ -0,0 +1,7 @@
+import { router } from "../../trpc"
+
+import { getConstructorParamsProcedure } from "./getConstructorParams"
+
+export const udcRouter = router({
+ getConstructorParams: getConstructorParamsProcedure,
+})
diff --git a/packages/extension/src/background/__new/router.ts b/packages/extension/src/background/__new/router.ts
index c81f6bb13..a2507f13e 100644
--- a/packages/extension/src/background/__new/router.ts
+++ b/packages/extension/src/background/__new/router.ts
@@ -1,19 +1,36 @@
-import { createChromeHandler } from "trpc-extension/adapter"
+import { createChromeHandler } from "trpc-browser/adapter"
-import { globalActionQueueStore } from "../../shared/actionQueue/store"
-import { ActionItem } from "../../shared/actionQueue/types"
-import { getQueue } from "../actionQueue"
-import { transactionTracker } from "../transactions/tracking"
+import { getMessagingKeys } from "../keys/messagingKeys"
+import { transactionTrackerWorker } from "../transactions/service/worker"
import { walletSingleton } from "../walletSingleton"
import { accountRouter } from "./procedures/account"
-import { networkRouter } from "./procedures/network"
+import { accountMessagingRouter } from "./procedures/accountMessaging"
+import { actionRouter } from "./procedures/action"
+import { addressBookRouter } from "./procedures/addressBook"
+import { argentAccountRouter } from "./procedures/argentAccount"
+import { multisigRouter } from "./procedures/multisig"
import { recoveryRouter } from "./procedures/recovery"
+import { sessionRouter } from "./procedures/session"
+import { tokensRouter } from "./procedures/tokens"
+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 { router } from "./trpc"
const appRouter = router({
account: accountRouter,
- network: networkRouter,
+ accountMessaging: accountMessagingRouter,
+ action: actionRouter,
+ addressBook: addressBookRouter,
recovery: recoveryRouter,
+ tokens: tokensRouter,
+ transfer: transferRouter,
+ argentAccount: argentAccountRouter,
+ multisig: multisigRouter,
+ session: sessionRouter,
+ udc: udcRouter,
})
export type AppRouter = typeof appRouter
@@ -25,8 +42,11 @@ createChromeHandler({
services: {
// services can be shared accross requests, as we usually only handle one user at a time
wallet: walletSingleton, // wallet "service" is obviously way too big and should be split up
- actionQueue: await getQueue(globalActionQueueStore),
- transactionTracker,
+ transactionTracker: transactionTrackerWorker,
+ actionService: backgroundActionService,
+ messagingKeys: await getMessagingKeys(),
+ argentAccountService: backgroundArgentAccountService,
+ multisigService: backgroundMultisigService,
},
}),
})
diff --git a/packages/extension/src/background/__new/services/action/background.ts b/packages/extension/src/background/__new/services/action/background.ts
new file mode 100644
index 000000000..d88522d74
--- /dev/null
+++ b/packages/extension/src/background/__new/services/action/background.ts
@@ -0,0 +1,155 @@
+import { isObject, isString } from "lodash-es"
+
+import { IActionQueue } from "../../../../shared/actionQueue/queue/interface"
+import type {
+ ActionHash,
+ ActionQueueItemMeta,
+} from "../../../../shared/actionQueue/schema"
+import type {
+ ActionItem,
+ ExtQueueItem,
+ ExtensionActionItem,
+} from "../../../../shared/actionQueue/types"
+import { MessageType } from "../../../../shared/messages"
+import {
+ handleActionApproval,
+ handleActionRejection,
+} from "../../../actionHandlers"
+import type { Respond } from "../../../respond"
+import { Wallet } from "../../../wallet"
+import type { IBackgroundActionService } from "./interface"
+import { ActionError } from "../../../../shared/errors/action"
+
+const getResultData = (resultMessage?: MessageType) => {
+ if (resultMessage && "data" in resultMessage) {
+ return resultMessage.data
+ }
+}
+
+const getResultDataError = (resultMessage?: MessageType) => {
+ const data = getResultData(resultMessage)
+ if (isObject(data) && "error" in data) {
+ return isString(data.error) ? data.error : "Unknown error"
+ }
+}
+
+export default class BackgroundActionService
+ implements IBackgroundActionService
+{
+ constructor(
+ private queue: IActionQueue,
+ private wallet: Wallet,
+ private respond: Respond,
+ ) {}
+
+ async approve(input: ExtensionActionItem | ActionHash) {
+ const actionHash = isString(input) ? input : input.meta.hash
+ const action = await this.queue.get(actionHash)
+ await this.queue.updateMeta(actionHash, {
+ startedApproving: Date.now(),
+ errorApproving: undefined,
+ })
+ if (!action) {
+ throw new ActionError({ code: "NOT_FOUND" })
+ }
+ /**
+ * Don't await handleActionApproval, this allows for existing patterns to use 'waitForMessage' after calling await clientActionService.approve(...)
+ */
+ handleActionApproval(action, this.wallet)
+ .then((resultMessage) => {
+ const error = getResultDataError(resultMessage)
+ if (error) {
+ void this.queue.updateMeta(actionHash, {
+ startedApproving: undefined,
+ errorApproving: error,
+ })
+ } else {
+ void this.queue.remove(actionHash)
+ }
+ if (resultMessage) {
+ void this.respond(resultMessage)
+ }
+ })
+ .catch((e) => {
+ /** handleActionApproval catches exceptions internally so this should never happen */
+ throw e
+ })
+ }
+
+ async approveAndWait(input: ExtensionActionItem | ActionHash) {
+ const actionHash = isString(input) ? input : input.meta.hash
+ const action = await this.queue.get(actionHash)
+ if (!action) {
+ throw new ActionError({ code: "NOT_FOUND" })
+ }
+ await this.queue.updateMeta(actionHash, {
+ startedApproving: Date.now(),
+ errorApproving: undefined,
+ })
+ const resultMessage = await handleActionApproval(action, this.wallet)
+ const error = getResultDataError(resultMessage)
+ if (error) {
+ await this.queue.updateMeta(actionHash, {
+ startedApproving: undefined,
+ errorApproving: error,
+ })
+ } else {
+ await this.queue.remove(actionHash)
+ }
+ if (resultMessage) {
+ try {
+ await this.respond(resultMessage)
+ } catch (e) {
+ console.warn("Error sending response", e)
+ throw e
+ }
+ }
+ /** return just the data like waitForMessage() */
+ const data = getResultData(resultMessage)
+ return data
+ }
+
+ async reject(input: string | string[]) {
+ const actionHashes = isString(input) ? [input] : input
+ return this.rejectAllActionHashes(actionHashes)
+ }
+
+ async rejectAll() {
+ const actions = await this.queue.getAll()
+ const actionHashes = actions.map((action) => action.meta.hash)
+ return this.rejectAllActionHashes(actionHashes)
+ }
+
+ async rejectAllActionHashes(actionHashes: ActionHash[]) {
+ for (const actionHash of actionHashes) {
+ const action = await this.queue.remove(actionHash)
+ if (!action) {
+ throw new ActionError({ code: "NOT_FOUND" })
+ }
+ /**
+ * Don't await handleActionRejection, this allows for existing patterns to use 'waitForMessage' after calling await clientActionService.reject(...)
+ */
+ handleActionRejection(action)
+ .then((resultMessage) => {
+ if (resultMessage) {
+ void this.respond(resultMessage)
+ }
+ })
+ .catch((e) => {
+ /** handleActionRejection catches exceptions internally so this should never happen */
+ throw e
+ })
+ }
+ }
+
+ async add(
+ action: T,
+ meta?: Partial,
+ ): Promise> {
+ return this.queue.add(action, meta)
+ }
+
+ async remove(actionHash: string): Promise | null> {
+ return this.queue.remove(actionHash)
+ }
+}
diff --git a/packages/extension/src/background/__new/services/action/index.ts b/packages/extension/src/background/__new/services/action/index.ts
new file mode 100644
index 000000000..4ec7fa3a1
--- /dev/null
+++ b/packages/extension/src/background/__new/services/action/index.ts
@@ -0,0 +1,10 @@
+import { actionQueue } from "../../../../shared/actionQueue"
+import { respond } from "../../../respond"
+import { walletSingleton } from "../../../walletSingleton"
+import BackgroundActionService from "./background"
+
+export const backgroundActionService = new BackgroundActionService(
+ actionQueue,
+ walletSingleton,
+ respond,
+)
diff --git a/packages/extension/src/background/__new/services/action/interface.ts b/packages/extension/src/background/__new/services/action/interface.ts
new file mode 100644
index 000000000..aee69b794
--- /dev/null
+++ b/packages/extension/src/background/__new/services/action/interface.ts
@@ -0,0 +1,17 @@
+import {
+ ActionHash,
+ ActionQueueItemMeta,
+} from "../../../../shared/actionQueue/schema"
+import type { IActionService } from "../../../../shared/actionQueue/service/interface"
+import type {
+ ActionItem,
+ ExtQueueItem,
+} from "../../../../shared/actionQueue/types"
+
+export interface IBackgroundActionService extends IActionService {
+ add(
+ action: T,
+ meta?: Partial,
+ ): Promise>
+ remove(actionHash: ActionHash): Promise | null>
+}
diff --git a/packages/extension/src/background/__new/services/analytics/index.ts b/packages/extension/src/background/__new/services/analytics/index.ts
new file mode 100644
index 000000000..7f4dadaa5
--- /dev/null
+++ b/packages/extension/src/background/__new/services/analytics/index.ts
@@ -0,0 +1,8 @@
+import { activeStore } from "../../../../shared/analytics"
+import { backgroundUIService } from "../ui"
+import { AnalyticsWorker } from "./worker"
+
+export const analyticsWorker = new AnalyticsWorker(
+ activeStore,
+ backgroundUIService,
+)
diff --git a/packages/extension/src/background/__new/services/analytics/worker.ts b/packages/extension/src/background/__new/services/analytics/worker.ts
new file mode 100644
index 000000000..d52deff79
--- /dev/null
+++ b/packages/extension/src/background/__new/services/analytics/worker.ts
@@ -0,0 +1,17 @@
+import type { IActiveStore } from "../../../../shared/analytics"
+import type { IBackgroundUIService } from "../ui/interface"
+import { Opened } from "../ui/interface"
+
+export class AnalyticsWorker {
+ constructor(
+ private readonly activeStore: IActiveStore,
+ private readonly backgroundUIService: IBackgroundUIService,
+ ) {
+ this.backgroundUIService.emitter.on(Opened, (opened) => {
+ if (!opened) {
+ /** Extension was closed */
+ this.activeStore.getState().update("lastClosed")
+ }
+ })
+ }
+}
diff --git a/packages/extension/src/background/__new/services/argentAccount/implementation.ts b/packages/extension/src/background/__new/services/argentAccount/implementation.ts
new file mode 100644
index 000000000..3f06c064b
--- /dev/null
+++ b/packages/extension/src/background/__new/services/argentAccount/implementation.ts
@@ -0,0 +1,206 @@
+import { IArgentAccountServiceBackground } from "../../../../shared/argentAccount/service/interface"
+import { ARGENT_SHIELD_NETWORK_ID } from "../../../../shared/shield/constants"
+import {
+ addBackendAccount,
+ emailVerificationStatusErrorSchema,
+ getBackendAccounts,
+ isTokenExpired,
+ register,
+ requestEmailAuthentication,
+ verifyEmail,
+} from "../../../../shared/shield/backend/account"
+import { encode, num } from "starknet"
+import { keccak, pedersen, sign, Signature } from "micro-starknet"
+import { stringToBytes } from "@scure/base"
+import { Wallet } from "../../../wallet"
+import { accountService } from "../../../../shared/account/service"
+import {
+ getNetworkSelector,
+ withGuardianSelector,
+} from "../../../../shared/account/selectors"
+import { validateEmailForAccounts } from "../../../../shared/shield/validation/validateAccount"
+import {
+ PreferencesPayload,
+ preferencesEndpointPayload,
+} from "../../procedures/argentAccount/updatePreferences.model"
+import { IHttpService } from "@argent/shared"
+import urlJoin from "url-join"
+import { ARGENT_ACCOUNT_URL } from "../../../../shared/api/constants"
+import { generateJwt } from "../../../../shared/shield/jwt"
+import { BaseError } from "../../../../shared/errors/baseError"
+
+export default class BackgroundArgentAccountService
+ implements IArgentAccountServiceBackground
+{
+ constructor(private wallet: Wallet, private httpService: IHttpService) {}
+
+ async addAccount() {
+ if (!ARGENT_SHIELD_NETWORK_ID) {
+ /** should never happen */
+ throw new BaseError({
+ message: "ARGENT_SHIELD_NETWORK_ID is not defined",
+ })
+ }
+ const selectedAccount = await this.wallet.getSelectedAccount()
+ if (!selectedAccount) {
+ throw new BaseError({ message: "No account selected" })
+ }
+
+ /** Check if this account already exists in backend */
+ const backendAccounts = await getBackendAccounts()
+
+ const existingAccount = backendAccounts.find(
+ (x) =>
+ num.hexToDecimalString(x.address) ===
+ num.hexToDecimalString(selectedAccount.address),
+ )
+
+ let guardianAddress: string | undefined
+
+ if (existingAccount) {
+ guardianAddress = existingAccount.guardianAddresses[0]
+ } else {
+ /** Add account to backend */
+ const keyPair = await this.wallet.getKeyPairByDerivationPath(
+ selectedAccount?.signer.derivationPath,
+ )
+ const privateKey = keyPair.getPrivate()
+ const publicKey = keyPair.pubKey
+ const privateKeyHex = num.toHex(privateKey)
+
+ const deploySignature = sign(
+ pedersen(keccak(stringToBytes("utf8", "starknet")), publicKey),
+ privateKeyHex,
+ )
+
+ const { r, s } = Signature.fromDER(deploySignature.toDERHex())
+ const response = await addBackendAccount(
+ publicKey,
+ selectedAccount.address,
+ [
+ encode.addHexPrefix(r.toString(16)),
+ encode.addHexPrefix(s.toString(16)),
+ ],
+ )
+ guardianAddress = response.guardianAddress
+ }
+ if (!guardianAddress) {
+ throw new BaseError({
+ message: "Unable to add account",
+ })
+ }
+ return guardianAddress
+ }
+
+ async requestEmail(email: string) {
+ try {
+ await requestEmailAuthentication(email)
+ } catch (error) {
+ throw new BaseError({
+ message: "Error while requesting email for argent account",
+ options: {
+ error,
+ context: { email },
+ },
+ })
+ }
+ }
+
+ async confirmEmail(code: string) {
+ try {
+ const { userRegistrationStatus } = await verifyEmail(code)
+
+ if (userRegistrationStatus === "notRegistered") {
+ await register()
+ }
+ } catch (error) {
+ let message = "Error while confirming email for argent account"
+ const emailVerificationStatusError =
+ emailVerificationStatusErrorSchema.safeParse(error)
+ if (emailVerificationStatusError.success) {
+ message = emailVerificationStatusError.data.responseJson.status
+ }
+ throw new BaseError({
+ message,
+ options: {
+ error,
+ context: { code },
+ },
+ })
+ }
+ }
+
+ async validateAccount() {
+ if (!ARGENT_SHIELD_NETWORK_ID) {
+ /** should never happen */
+ throw new BaseError({
+ message: "ARGENT_SHIELD_NETWORK_ID is not defined",
+ })
+ }
+
+ /** Check if account is valid for current wallet */
+ const selectedAccount = await this.wallet.getSelectedAccount()
+ const starknetAccount = await this.wallet.getSelectedStarknetAccount()
+
+ if (!starknetAccount || !selectedAccount) {
+ throw new BaseError({ message: "no accounts" })
+ }
+
+ /** Get current account state */
+
+ const localAccounts = await accountService.get(
+ getNetworkSelector(ARGENT_SHIELD_NETWORK_ID),
+ )
+ const localAccountsWithGuardian = await accountService.get(
+ withGuardianSelector,
+ )
+ const backendAccounts = await getBackendAccounts()
+
+ /** Validate email against account state */
+
+ validateEmailForAccounts({
+ localAccounts,
+ localAccountsWithGuardian,
+ backendAccounts,
+ })
+ }
+
+ async isTokenExpired() {
+ return await isTokenExpired()
+ }
+
+ async updatePreferences(preferences: PreferencesPayload) {
+ if (!ARGENT_ACCOUNT_URL) {
+ throw new Error("ARGENT_ACCOUNT_URL is not defined")
+ }
+ // TODO move this one layer above in next ticket
+ const jwt = await generateJwt()
+ return await this.httpService.post(
+ urlJoin(ARGENT_ACCOUNT_URL, "/preferences"),
+ {
+ body: JSON.stringify(preferences),
+ headers: {
+ Authorization: `Bearer ${jwt}`,
+ "Content-Type": "application/json",
+ },
+ },
+ preferencesEndpointPayload,
+ )
+ }
+
+ async getPreferences() {
+ if (!ARGENT_ACCOUNT_URL) {
+ throw new Error("ARGENT_ACCOUNT_URL is not defined")
+ }
+ const jwt = await generateJwt()
+ return await this.httpService.get(
+ urlJoin(ARGENT_ACCOUNT_URL, "/preferences"),
+ {
+ headers: {
+ Authorization: `Bearer ${jwt}`,
+ "Content-Type": "application/json",
+ },
+ },
+ )
+ }
+}
diff --git a/packages/extension/src/background/__new/services/argentAccount/index.ts b/packages/extension/src/background/__new/services/argentAccount/index.ts
new file mode 100644
index 000000000..7b9771313
--- /dev/null
+++ b/packages/extension/src/background/__new/services/argentAccount/index.ts
@@ -0,0 +1,6 @@
+import BackgroundArgentAccountService from "./implementation"
+import { walletSingleton } from "../../../walletSingleton"
+import { httpService } from "../http/singleton"
+
+export const backgroundArgentAccountService =
+ new BackgroundArgentAccountService(walletSingleton, httpService)
diff --git a/packages/extension/src/background/__new/services/http/singleton.ts b/packages/extension/src/background/__new/services/http/singleton.ts
new file mode 100644
index 000000000..6324f80ad
--- /dev/null
+++ b/packages/extension/src/background/__new/services/http/singleton.ts
@@ -0,0 +1,3 @@
+import { HTTPService } from "@argent/shared"
+
+export const httpService = new HTTPService()
diff --git a/packages/extension/src/background/__new/services/multisig/implementation.ts b/packages/extension/src/background/__new/services/multisig/implementation.ts
new file mode 100644
index 000000000..c5d936cf0
--- /dev/null
+++ b/packages/extension/src/background/__new/services/multisig/implementation.ts
@@ -0,0 +1,212 @@
+import {
+ AddAccountResponse,
+ IMultisigService,
+} from "../../../../shared/multisig/service/messaging/interface"
+import { tryToMintFeeToken } from "../../../../shared/devnet/mintFeeToken"
+import { analytics } from "../../../analytics"
+import { getMultisigAccounts } from "../../../../shared/multisig/utils/baseMultisig"
+import {
+ AddAccountPayload,
+ AddOwnerMultisigPayload,
+ RemoveOwnerMultisigPayload,
+ ReplaceOwnerMultisigPayload,
+ UpdateMultisigThresholdPayload,
+} from "../../../../shared/multisig/multisig.model"
+import { Wallet } from "../../../wallet"
+import { CallData } from "starknet"
+import { IBackgroundActionService } from "../action/interface"
+import { BaseWalletAccount } from "../../../../shared/wallet.model"
+import {
+ MultisigEntryPointType,
+ MultisigTransactionType,
+ PendingMultisig,
+} from "../../../../shared/multisig/types"
+import { MultisigAccount } from "../../../../shared/multisig/account"
+import { decodeBase58, decodeBase58Array } from "@argent/shared"
+import { AccountError } from "../../../../shared/errors/account"
+import { getMultisigPendingTransaction } from "../../../../shared/multisig/pendingTransactionsStore"
+import { MultisigError } from "../../../../shared/errors/multisig"
+
+export default class BackgroundMultisigService implements IMultisigService {
+ constructor(
+ private wallet: Wallet,
+ private actionService: IBackgroundActionService,
+ ) {}
+ async addAccount(payload: AddAccountPayload): Promise {
+ const { networkId, signers, threshold, creator, publicKey, updatedAt } =
+ payload
+ try {
+ const account = await this.wallet.newAccount(networkId, "multisig", {
+ signers,
+ threshold,
+ creator,
+ publicKey,
+ updatedAt,
+ })
+ await tryToMintFeeToken(account)
+
+ void analytics.track("createAccount", {
+ status: "success",
+ networkId,
+ type: "multisig",
+ })
+
+ const accounts = await getMultisigAccounts()
+
+ return {
+ account,
+ accounts,
+ }
+ } catch (error) {
+ void analytics.track("createAccount", {
+ status: "failure",
+ networkId: networkId,
+ type: "multisig",
+ errorMessage: `${error}`,
+ })
+
+ throw error
+ }
+ }
+
+ async addOwner(payload: AddOwnerMultisigPayload): Promise {
+ const { address, signersToAdd, newThreshold } = payload
+
+ const signersPayload = {
+ entrypoint: MultisigEntryPointType.ADD_SIGNERS,
+ calldata: CallData.compile({
+ 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,
+ },
+ },
+ })
+ }
+
+ async removeOwner(payload: RemoveOwnerMultisigPayload): Promise {
+ const { address, signerToRemove, newThreshold } = payload
+
+ const signersToRemove = [decodeBase58(signerToRemove)]
+
+ const signersPayload = {
+ entrypoint: MultisigEntryPointType.REMOVE_SIGNERS,
+ calldata: CallData.compile({
+ 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,
+ },
+ },
+ })
+ }
+
+ async replaceOwner(payload: ReplaceOwnerMultisigPayload): Promise {
+ const { signerToRemove, signerToAdd, address } = payload
+
+ const decodedSignerToRemove = decodeBase58(signerToRemove)
+ const decodedSignerToAdd = decodeBase58(signerToAdd)
+
+ const signersPayload = {
+ entrypoint: MultisigEntryPointType.REPLACE_SIGNER,
+ calldata: CallData.compile({
+ 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,
+ },
+ },
+ })
+ }
+
+ async addPendingAccount(networkId: string): Promise {
+ return await this.wallet.newPendingMultisig(networkId)
+ }
+
+ async addTransactionSignature(requestId: string): Promise {
+ const selectedAccount = await this.wallet.getSelectedAccount()
+
+ if (!selectedAccount) {
+ throw new AccountError({ code: "NOT_SELECTED" })
+ }
+
+ const multisigStarknetAccount = await this.wallet.getStarknetAccount(
+ selectedAccount,
+ )
+
+ if (!MultisigAccount.isMultisig(multisigStarknetAccount)) {
+ throw new AccountError({ code: "NOT_MULTISIG" })
+ }
+
+ const transactionToSign = await getMultisigPendingTransaction(requestId)
+
+ if (!transactionToSign) {
+ throw new MultisigError({
+ code: "PENDING_MULTISIG_TRANSACTION_NOT_FOUND",
+ message: `Pending Multisig transaction ${requestId} not found`,
+ })
+ }
+
+ const { transaction_hash } =
+ await multisigStarknetAccount.addRequestSignature(transactionToSign)
+
+ return transaction_hash
+ }
+
+ async deploy(account: BaseWalletAccount): Promise {
+ await this.actionService.add({
+ type: "DEPLOY_MULTISIG_ACTION",
+ payload: account,
+ })
+ }
+
+ async updateThreshold(
+ payload: UpdateMultisigThresholdPayload,
+ ): Promise {
+ const { address, newThreshold } = payload
+
+ const thresholdPayload = {
+ entrypoint: MultisigEntryPointType.CHANGE_THRESHOLD,
+ calldata: [newThreshold.toString()],
+ contractAddress: address,
+ }
+
+ await this.actionService.add({
+ type: "TRANSACTION",
+ payload: {
+ transactions: thresholdPayload,
+ meta: {
+ title: "Change threshold",
+ type: MultisigTransactionType.MULTISIG_CHANGE_THRESHOLD,
+ },
+ },
+ })
+ }
+}
diff --git a/packages/extension/src/background/__new/services/multisig/index.ts b/packages/extension/src/background/__new/services/multisig/index.ts
new file mode 100644
index 000000000..7ca35d7a3
--- /dev/null
+++ b/packages/extension/src/background/__new/services/multisig/index.ts
@@ -0,0 +1,8 @@
+import BackgroundMultisigService from "./implementation"
+import { walletSingleton } from "../../../walletSingleton"
+import { backgroundActionService } from "../action"
+
+export const backgroundMultisigService = new BackgroundMultisigService(
+ walletSingleton,
+ backgroundActionService,
+)
diff --git a/packages/extension/src/background/__new/services/network/background.test.ts b/packages/extension/src/background/__new/services/network/background.test.ts
new file mode 100644
index 000000000..5734c5e18
--- /dev/null
+++ b/packages/extension/src/background/__new/services/network/background.test.ts
@@ -0,0 +1,59 @@
+import { describe, expect, test, vi } from "vitest"
+
+import { Network } from "../../../../shared/network"
+import { defaultReadonlyNetworks } from "../../../../shared/network/defaults"
+import { networkSelector } from "../../../../shared/network/selectors"
+import { networksEqual } from "../../../../shared/network/store"
+import { InMemoryRepository } from "../../../../shared/storage/__new/__test__/inmemoryImplementations"
+import BackgroundNetworkService from "./background"
+import { NetworkWithStatus } from "../../../../shared/network/type"
+
+describe("BackgroundNetworkService", () => {
+ const makeService = () => {
+ const networkRepo = new InMemoryRepository({
+ namespace: "core:allNetworks",
+ compare: networksEqual,
+ })
+ const networkWithStatusRepo = new InMemoryRepository({
+ namespace: "core:allNetworkStatus",
+ compare: networksEqual,
+ })
+ const getNetworkStatuses = vi.fn()
+ const backgroundNetworkService = new BackgroundNetworkService(
+ networkRepo,
+ networkWithStatusRepo,
+ defaultReadonlyNetworks,
+ getNetworkStatuses,
+ )
+ return {
+ backgroundNetworkService,
+ networkWithStatusRepo,
+ getNetworkStatuses,
+ }
+ }
+ test("updateStatuses", async () => {
+ const {
+ backgroundNetworkService,
+ networkWithStatusRepo,
+ getNetworkStatuses,
+ } = makeService()
+
+ getNetworkStatuses.mockResolvedValueOnce({
+ "mainnet-alpha": "ok",
+ "goerli-alpha": "degraded",
+ })
+
+ await backgroundNetworkService.updateStatuses()
+ expect(getNetworkStatuses).toHaveBeenCalled()
+
+ const [ok] = await networkWithStatusRepo.get(
+ networkSelector("mainnet-alpha"),
+ )
+ expect(ok).toHaveProperty("status", "ok")
+
+ const [degraded] = await networkWithStatusRepo.get(
+ networkSelector("goerli-alpha"),
+ )
+ expect(degraded).toHaveProperty("status", "degraded")
+ })
+})
diff --git a/packages/extension/src/background/__new/services/network/background.ts b/packages/extension/src/background/__new/services/network/background.ts
new file mode 100644
index 000000000..995d43771
--- /dev/null
+++ b/packages/extension/src/background/__new/services/network/background.ts
@@ -0,0 +1,38 @@
+import { uniqWith } from "lodash-es"
+
+import { Network } from "../../../../shared/network"
+import { INetworkRepo, networksEqual } from "../../../../shared/network/store"
+import { GetNetworkStatusesFn, IBackgroundNetworkService } from "./interface"
+import { INetworkWithStatusRepo } from "../../../../shared/network/statusStore"
+
+export default class BackgroundNetworkService
+ implements IBackgroundNetworkService
+{
+ constructor(
+ private readonly networkRepo: INetworkRepo,
+ private readonly networkWithStatusRepo: INetworkWithStatusRepo,
+ readonly defaultNetworks: Network[],
+ private readonly getNetworkStatuses: GetNetworkStatusesFn,
+ ) {}
+
+ private async loadNetworks() {
+ const allNetworks = uniqWith(
+ [...(await this.networkRepo.get()), ...this.defaultNetworks],
+ networksEqual,
+ )
+ return allNetworks
+ }
+
+ async updateStatuses() {
+ const networks = await this.loadNetworks()
+ const networkStatuses = await this.getNetworkStatuses(networks)
+ const networkWithUpdatedStatuses = networks.map((network) => {
+ return {
+ id: network.id,
+ status: networkStatuses[network.id] ?? "unknown",
+ }
+ })
+
+ await this.networkWithStatusRepo.upsert(networkWithUpdatedStatuses)
+ }
+}
diff --git a/packages/extension/src/background/__new/services/network/index.ts b/packages/extension/src/background/__new/services/network/index.ts
new file mode 100644
index 000000000..c2f9d585f
--- /dev/null
+++ b/packages/extension/src/background/__new/services/network/index.ts
@@ -0,0 +1,21 @@
+import { defaultNetworks } from "../../../../shared/network"
+import { networkStatusRepo } from "../../../../shared/network/statusStore"
+import { networkRepo } from "../../../../shared/network/store"
+import { chromeScheduleService } from "../../../../shared/schedule"
+import { backgroundUIService } from "../ui"
+import BackgroundNetworkService from "./background"
+import { getNetworkStatuses } from "./status"
+import { NetworkWorker } from "./worker"
+
+export const backgroundNetworkService = new BackgroundNetworkService(
+ networkRepo,
+ networkStatusRepo,
+ defaultNetworks,
+ getNetworkStatuses,
+)
+
+export const networkWorker = new NetworkWorker(
+ backgroundNetworkService,
+ backgroundUIService,
+ chromeScheduleService,
+)
diff --git a/packages/extension/src/background/__new/services/network/interface.ts b/packages/extension/src/background/__new/services/network/interface.ts
new file mode 100644
index 000000000..7c68df6a9
--- /dev/null
+++ b/packages/extension/src/background/__new/services/network/interface.ts
@@ -0,0 +1,9 @@
+import { Network, NetworkStatus } from "../../../../shared/network"
+
+export interface IBackgroundNetworkService {
+ updateStatuses(): Promise
+}
+
+export type GetNetworkStatusesFn = (
+ networks: Network[],
+) => Promise>
diff --git a/packages/extension/src/background/__new/services/network/status.ts b/packages/extension/src/background/__new/services/network/status.ts
new file mode 100644
index 000000000..749a31023
--- /dev/null
+++ b/packages/extension/src/background/__new/services/network/status.ts
@@ -0,0 +1,75 @@
+import urljoin from "url-join"
+
+import { Network, NetworkStatus } from "../../../../shared/network"
+import { fetchWithTimeout } from "../../../utils/fetchWithTimeout"
+import { z } from "zod"
+import { NetworkError } from "../../../../shared/errors/network"
+
+const checklyNetworkNames = {
+ "Goerli - Contract call": "goerli-alpha",
+ "Goerli 2 - Contract call": "goerli-alpha-2",
+ "Mainnet - Contract call": "mainnet-alpha",
+ "Integration - Goerli - Get state update": "integration",
+}
+const checklySchema = z.object({
+ results: z.array(
+ z.object({
+ id: z.string(),
+ name: z.string(),
+ status: z.object({
+ hasErrors: z.boolean(),
+ hasFailures: z.boolean(),
+ isDegraded: z.boolean(),
+ }),
+ }),
+ ),
+})
+
+export const getNetworkStatuses = async () => {
+ try {
+ const response = await fetchWithTimeout(
+ urljoin(
+ "https://api.checklyhq.com/v1/status-page/4054/statuses?page=1&limit=15",
+ ),
+ { timeout: 5000, method: "GET" },
+ )
+ const data = await response.json()
+ const parsedData = checklySchema.safeParse(data)
+ if (!parsedData.success) {
+ throw new NetworkError({
+ message: "NETWORK_STATUS_RESPONSE_PARSING_FAILED",
+ })
+ }
+ const networkStatuses: Record = {}
+ parsedData.data.results.forEach((result) => {
+ let status: NetworkStatus = "unknown"
+ if (result.status.hasErrors) {
+ status = "error"
+ } else if (result.status.hasFailures) {
+ status = "error"
+ } else if (result.status.isDegraded) {
+ status = "degraded"
+ } else if (result.status.isDegraded === false) {
+ status = "ok"
+ }
+ if (result.name in checklyNetworkNames) {
+ const key = result.name as keyof typeof checklyNetworkNames
+ networkStatuses[checklyNetworkNames[key]] = status
+ }
+ })
+
+ return networkStatuses
+ } catch (error) {
+ console.warn({ error })
+ const networkStatuses: Record = {}
+ Object.values(checklyNetworkNames).map((value) => {
+ networkStatuses[value] = "unknown"
+ })
+ // Gracefully returning unknown statuses
+ return networkStatuses
+ throw new NetworkError({
+ message: "NETWORK_STATUS_RESPONSE_PARSING_FAILED",
+ options: { error },
+ })
+ }
+}
diff --git a/packages/extension/src/background/__new/services/network/worker.ts b/packages/extension/src/background/__new/services/network/worker.ts
new file mode 100644
index 000000000..663283a4b
--- /dev/null
+++ b/packages/extension/src/background/__new/services/network/worker.ts
@@ -0,0 +1,56 @@
+import { IScheduleService } from "../../../../shared/schedule/interface"
+import { IBackgroundUIService, Opened } from "../ui/interface"
+import { IBackgroundNetworkService } from "./interface"
+import { RefreshInterval } from "../../../../shared/config"
+
+const TASK_ID = "NetworkWorker.updateStatuses"
+const REFRESH_PERIOD_MINUTES = Math.floor(RefreshInterval.MEDIUM / 60)
+
+export class NetworkWorker {
+ private isUpdating = false
+ private lastUpdatedTimestamp = 0
+
+ constructor(
+ private readonly backgroundNetworkService: IBackgroundNetworkService,
+ private readonly backgroundUIService: IBackgroundUIService,
+ private readonly scheduleService: IScheduleService,
+ ) {
+ void this.scheduleService.registerImplementation({
+ id: TASK_ID,
+ callback: this.updateNetworkStatuses.bind(this),
+ })
+
+ this.backgroundUIService.emitter.on(Opened, this.onOpened.bind(this))
+ }
+
+ async updateNetworkStatuses() {
+ if (this.isUpdating) {
+ return
+ }
+ this.isUpdating = true
+ this.lastUpdatedTimestamp = Date.now()
+ await this.backgroundNetworkService.updateStatuses()
+ this.isUpdating = false
+ }
+
+ onOpened(opened: boolean) {
+ if (opened) {
+ const currentTimestamp = Date.now()
+ const differenceInMilliseconds =
+ currentTimestamp - this.lastUpdatedTimestamp
+ const differenceInMinutes = differenceInMilliseconds / (1000 * 60)
+
+ if (differenceInMinutes > REFRESH_PERIOD_MINUTES) {
+ void this.updateNetworkStatuses()
+ }
+
+ void this.scheduleService.every(RefreshInterval.MEDIUM, {
+ id: TASK_ID,
+ })
+ } else {
+ void this.scheduleService.delete({
+ id: TASK_ID,
+ })
+ }
+ }
+}
diff --git a/packages/extension/src/background/__new/services/onboarding/implementation.test.ts b/packages/extension/src/background/__new/services/onboarding/implementation.test.ts
index 4187cb4be..82f6ccd0c 100644
--- a/packages/extension/src/background/__new/services/onboarding/implementation.test.ts
+++ b/packages/extension/src/background/__new/services/onboarding/implementation.test.ts
@@ -10,8 +10,6 @@ describe("OnboardingService", () => {
closePopup: vi.fn(),
createTab: vi.fn(),
focusTab: vi.fn(),
- getPopup: vi.fn(),
- getTab: vi.fn(),
hasPopup: vi.fn(),
hasTab: vi.fn(),
setDefaultPopup: vi.fn(),
diff --git a/packages/extension/src/background/__new/services/onboarding/implementation.ts b/packages/extension/src/background/__new/services/onboarding/implementation.ts
index 269261de2..6072b1673 100644
--- a/packages/extension/src/background/__new/services/onboarding/implementation.ts
+++ b/packages/extension/src/background/__new/services/onboarding/implementation.ts
@@ -1,11 +1,23 @@
import type { IUIService } from "../../../../shared/__new/services/ui/interface"
import type { KeyValueStorage } from "../../../../shared/storage"
+import { DeepPick } from "../../../../shared/types/deepPick"
import type { WalletStorageProps } from "../../../../shared/wallet/walletStore"
import type { IOnboardingService } from "./interface"
+type MinimalUIService = DeepPick<
+ IUIService,
+ | "closePopup"
+ | "createTab"
+ | "focusTab"
+ | "hasPopup"
+ | "hasTab"
+ | "setDefaultPopup"
+ | "unsetDefaultPopup"
+>
+
export default class OnboardingService implements IOnboardingService {
constructor(
- private uiService: IUIService,
+ private uiService: MinimalUIService,
private walletStore: KeyValueStorage,
) {}
diff --git a/packages/extension/src/background/__new/services/ui/background.test.ts b/packages/extension/src/background/__new/services/ui/background.test.ts
new file mode 100644
index 000000000..34ab603f6
--- /dev/null
+++ b/packages/extension/src/background/__new/services/ui/background.test.ts
@@ -0,0 +1,86 @@
+import { describe, expect, test, vi } from "vitest"
+
+import BackgroundUIService from "./background"
+import { Opened } from "./interface"
+
+describe("BackgroundUIService", () => {
+ const makeService = () => {
+ const browser = {
+ runtime: {
+ connect: vi.fn(),
+ onConnect: {
+ addListener: vi.fn(),
+ },
+ onDisconnect: {
+ addListener: vi.fn(),
+ },
+ },
+ }
+ const uiService = {
+ connectId: "test-connect-id",
+ hasTab: vi.fn(),
+ }
+ const emitter = {
+ anyEvent: vi.fn(),
+ bindMethods: vi.fn(),
+ clearListeners: vi.fn(),
+ debug: vi.fn(),
+ emit: vi.fn(),
+ emitSerial: vi.fn(),
+ events: vi.fn(),
+ listenerCount: vi.fn(),
+ off: vi.fn(),
+ offAny: vi.fn(),
+ on: vi.fn(),
+ onAny: vi.fn(),
+ once: vi.fn(),
+ }
+ const sessionService = {
+ locked: false,
+ emitter,
+ }
+
+ const backgroundUIService = new BackgroundUIService(
+ emitter,
+ browser,
+ uiService,
+ sessionService,
+ )
+ return {
+ backgroundUIService,
+ uiService,
+ browser,
+ emitter,
+ }
+ }
+ test("open / close lifecycle", async () => {
+ const { backgroundUIService, uiService, emitter } = makeService()
+ const port = {
+ name: uiService.connectId,
+ onDisconnect: {
+ addListener: vi.fn(),
+ },
+ }
+
+ /** open */
+ backgroundUIService.onConnectPort(port)
+ expect(backgroundUIService.opened).toBeTruthy()
+ expect(port.onDisconnect.addListener).toHaveBeenCalled()
+ expect(emitter.emit).toHaveBeenCalledWith(Opened, true)
+
+ /** should not fire again */
+ emitter.emit.mockReset()
+ backgroundUIService.onConnectPort(port)
+ expect(emitter.emit).not.toHaveBeenCalled()
+
+ /** close */
+ await backgroundUIService.onDisconnectPort()
+ expect(backgroundUIService.opened).toBeFalsy()
+ expect(emitter.emit).toHaveBeenCalledWith(Opened, false)
+
+ /** should not fire again */
+ emitter.emit.mockReset()
+ await backgroundUIService.onDisconnectPort()
+ expect(emitter.emit).not.toHaveBeenCalled()
+ })
+})
diff --git a/packages/extension/src/background/__new/services/ui/background.ts b/packages/extension/src/background/__new/services/ui/background.ts
new file mode 100644
index 000000000..1651e2bc1
--- /dev/null
+++ b/packages/extension/src/background/__new/services/ui/background.ts
@@ -0,0 +1,100 @@
+import Emittery from "emittery"
+import browser from "webextension-polyfill"
+
+import { IUIService } from "../../../../shared/__new/services/ui/interface"
+import { DeepPick } from "../../../../shared/types/deepPick"
+import { openUi } from "../../../openUi"
+import { Locked } from "../../../wallet/session/interface"
+import type { WalletSessionService } from "../../../wallet/session/session.service"
+import type { Events, IBackgroundUIService } from "./interface"
+import { Opened } from "./interface"
+
+type MinimalBrowser = DeepPick<
+ typeof chrome,
+ "runtime.connect" | "runtime.onConnect.addListener"
+>
+
+type MinimalIUIService = Pick
+
+type MinimalPort = DeepPick<
+ browser.runtime.Port,
+ "name" | "onDisconnect.addListener"
+>
+
+type MinimalIWalletSessionService = Pick<
+ WalletSessionService,
+ "emitter" | "locked"
+>
+
+export default class BackgroundUIService implements IBackgroundUIService {
+ private _opened = false
+
+ constructor(
+ readonly emitter: Emittery,
+ private browser: MinimalBrowser,
+ private uiService: MinimalIUIService,
+ private sessionService: MinimalIWalletSessionService,
+ ) {
+ this.initListeners()
+ }
+
+ /*
+ * There is no usable 'close' event on an extension
+ *
+ * instead we open a message port to the extension and simply listen for it to be disconnected
+ * as a side-effect of the extension being closed
+ */
+
+ private initListeners() {
+ this.browser.runtime.onConnect.addListener(this.onConnectPort.bind(this))
+ }
+
+ /** listen for the port connection from the UI, then detect disconnection */
+ onConnectPort(port: MinimalPort) {
+ if (port.name !== this.uiService.connectId) {
+ return
+ }
+ this.opened = true
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
+ port.onDisconnect.addListener(this.onDisconnectPort.bind(this))
+ }
+
+ async onDisconnectPort() {
+ /** An instance of the UI was closed */
+ const hasTab = await this.uiService.hasTab()
+ if (!hasTab) {
+ /** There are no more instances left open */
+ this.opened = false
+ }
+ }
+
+ get opened() {
+ return this._opened
+ }
+
+ private set opened(opened: boolean) {
+ if (this._opened === opened) {
+ return
+ }
+ this._opened = opened
+ void this.emitter.emit(Opened, this.opened)
+ }
+
+ async openUiAndUnlock() {
+ if (!this.opened) {
+ await openUi()
+ /** wait for Opened state to update */
+ await this.emitter.once(Opened)
+ }
+ if (!this.sessionService.locked) {
+ return true
+ }
+ /** wait for change in either Locked or Opened state */
+ await Promise.race([
+ this.emitter.once(Opened),
+ this.sessionService.emitter.once(Locked),
+ ])
+ const unlocked = !this.sessionService.locked
+ return unlocked
+ }
+}
diff --git a/packages/extension/src/background/__new/services/ui/index.ts b/packages/extension/src/background/__new/services/ui/index.ts
new file mode 100644
index 000000000..a96c28eee
--- /dev/null
+++ b/packages/extension/src/background/__new/services/ui/index.ts
@@ -0,0 +1,18 @@
+import Emittery from "emittery"
+import browser from "webextension-polyfill"
+
+import { uiService } from "../../../../shared/__new/services/ui"
+import { sessionService } from "../../../walletSingleton"
+import BackgroundUIService from "./background"
+import type { Events } from "./interface"
+
+export { Opened } from "./interface"
+
+const emitter = new Emittery()
+
+export const backgroundUIService = new BackgroundUIService(
+ emitter,
+ browser,
+ uiService,
+ sessionService,
+)
diff --git a/packages/extension/src/background/__new/services/ui/interface.ts b/packages/extension/src/background/__new/services/ui/interface.ts
new file mode 100644
index 000000000..53519d7d7
--- /dev/null
+++ b/packages/extension/src/background/__new/services/ui/interface.ts
@@ -0,0 +1,24 @@
+import Emittery from "emittery"
+
+export const Opened = Symbol("Opened")
+
+export type Events = {
+ /**
+ * Fired when UI state changes to/from having any open windows or tabs
+ */
+ [Opened]: boolean
+}
+
+export interface IBackgroundUIService {
+ readonly emitter: Emittery
+ /**
+ * Flag for if there are one or more UI windows or tabs open currently
+ */
+ readonly opened: boolean
+ /**
+ * Opens ui
+ * returns true if already unlocked, or if the user proceeded to unlock the wallet
+ * returns false if the wallet was locked and the user closed the wallet
+ */
+ openUiAndUnlock(): Promise
+}
diff --git a/packages/extension/src/background/__new/trpc.ts b/packages/extension/src/background/__new/trpc.ts
index 019c09568..1ca4b937a 100644
--- a/packages/extension/src/background/__new/trpc.ts
+++ b/packages/extension/src/background/__new/trpc.ts
@@ -1,22 +1,61 @@
import { initTRPC } from "@trpc/server"
-import { ActionItem } from "../../shared/actionQueue/types"
-import { Queue } from "../actionQueue"
-import { TransactionTracker } from "../transactions/tracking"
+import type { IArgentAccountServiceBackground } from "../../shared/argentAccount/service/interface"
+import { BaseError } from "../../shared/errors/baseError"
+import type { IMultisigService } from "../../shared/multisig/service/messaging/interface"
+import { MessagingKeys } from "../keys/messagingKeys"
+import { TransactionTrackerWorker } from "../transactions/service/starknet.service"
import { Wallet } from "../wallet"
+import type { IBackgroundActionService } from "./services/action/interface"
interface Context {
sender?: chrome.runtime.MessageSender
services: {
wallet: Wallet
- actionQueue: Queue
- transactionTracker: TransactionTracker
+ transactionTracker: TransactionTrackerWorker
+ actionService: IBackgroundActionService
+ messagingKeys: MessagingKeys
+ argentAccountService: IArgentAccountServiceBackground
+ multisigService: IMultisigService
}
}
const t = initTRPC.context().create({
isServer: false,
allowOutsideOfServer: true,
+ errorFormatter: (opts) => {
+ const { shape, error } = opts
+ const { cause } = error
+
+ if (cause instanceof BaseError) {
+ return {
+ ...shape,
+ data: {
+ ...shape.data,
+ code: cause.code,
+ name: cause.name,
+ message: cause.message,
+ context: cause.context,
+ },
+ }
+ } else if (cause?.cause instanceof BaseError) {
+ // The production build is nesting the error in another cause
+ const nestedCause = cause.cause
+
+ return {
+ ...shape,
+ data: {
+ ...shape.data,
+ code: nestedCause.code,
+ name: nestedCause.name,
+ message: nestedCause.message,
+ context: nestedCause.context,
+ },
+ }
+ }
+
+ return shape
+ },
})
export const router = t.router
diff --git a/packages/extension/src/background/accountDeploy.ts b/packages/extension/src/background/accountDeploy.ts
index 916e5c4cf..e17cc420c 100644
--- a/packages/extension/src/background/accountDeploy.ts
+++ b/packages/extension/src/background/accountDeploy.ts
@@ -1,35 +1,28 @@
-import { ActionItem } from "../shared/actionQueue/types"
+import { BlockNumber, num } from "starknet"
import { BaseWalletAccount, WalletAccount } from "../shared/wallet.model"
-import { Queue } from "./actionQueue"
+import { IBackgroundActionService } from "./__new/services/action/interface"
export interface IDeployAccount {
account: BaseWalletAccount
- actionQueue: Queue
+ actionService: IBackgroundActionService
}
export const deployAccountAction = async ({
- actionQueue,
account,
+ actionService,
}: IDeployAccount) => {
- await actionQueue.push({
+ await actionService.add({
type: "DEPLOY_ACCOUNT_ACTION",
payload: account,
})
}
-export const deployMultisigAction = async ({
- actionQueue,
- account,
-}: IDeployAccount) => {
- await actionQueue.push({
- type: "DEPLOY_MULTISIG_ACTION",
- payload: account,
- })
-}
-
export const isAccountDeployed = async (
account: WalletAccount,
- getClassAt: (address: string, blockIdentifier?: unknown) => Promise,
+ getClassAt: (
+ address: string,
+ blockIdentifier?: BlockNumber | num.BigNumberish, // from starknet.js due to missing export
+ ) => Promise,
) => {
if (!account.needsDeploy) {
return true
@@ -38,6 +31,7 @@ export const isAccountDeployed = async (
await getClassAt(account.address)
return true
} catch (e) {
+ console.error(e)
return false
}
}
diff --git a/packages/extension/src/background/accountDeployAction.ts b/packages/extension/src/background/accountDeployAction.ts
index 04f6e025f..5a4f1aade 100644
--- a/packages/extension/src/background/accountDeployAction.ts
+++ b/packages/extension/src/background/accountDeployAction.ts
@@ -1,8 +1,8 @@
import { ExtQueueItem } from "../shared/actionQueue/types"
import { BaseWalletAccount } from "../shared/wallet.model"
-import { BackgroundService } from "./background"
import { addTransaction } from "./transactions/store"
import { checkTransactionHash } from "./transactions/transactionExecution"
+import { Wallet } from "./wallet"
type DeployAccountAction = ExtQueueItem<{
type: "DEPLOY_ACCOUNT_ACTION"
@@ -11,14 +11,14 @@ type DeployAccountAction = ExtQueueItem<{
export const accountDeployAction = async (
{ payload: baseAccount }: DeployAccountAction,
- { wallet }: BackgroundService,
+ wallet: Wallet,
) => {
if (!(await wallet.isSessionOpen())) {
throw Error("you need an open session")
}
const selectedAccount = await wallet.getAccount(baseAccount)
- const accountNeedsDeploy = selectedAccount.needsDeploy
+ const accountNeedsDeploy = selectedAccount?.needsDeploy
if (!accountNeedsDeploy) {
throw Error("Account already deployed")
diff --git a/packages/extension/src/background/accountMessaging.ts b/packages/extension/src/background/accountMessaging.ts
index c0c18c3ec..6258824df 100644
--- a/packages/extension/src/background/accountMessaging.ts
+++ b/packages/extension/src/background/accountMessaging.ts
@@ -1,301 +1,14 @@
-import { constants, number } from "starknet"
-
-import { accountService } from "../shared/account/service"
import { AccountMessage } from "../shared/messages/AccountMessage"
-import { isEqualAddress } from "../ui/services/addresses"
-import { upgradeAccount } from "./accountUpgrade"
-import { sendMessageToUi } from "./activeTabs"
import { HandleMessage, UnhandledMessage } from "./background"
-import { encryptForUi } from "./crypto"
-import { addTransaction } from "./transactions/store"
export const handleAccountMessage: HandleMessage = async ({
msg,
- background: { wallet, actionQueue },
- messagingKeys: { privateKey },
+ background: { actionService },
}) => {
switch (msg.type) {
- case "GET_SELECTED_ACCOUNT": {
- const selectedAccount = await wallet.getSelectedAccount()
- return sendMessageToUi({
- type: "GET_SELECTED_ACCOUNT_RES",
- data: selectedAccount,
- })
- }
-
- case "UPGRADE_ACCOUNT": {
- try {
- await upgradeAccount({
- account: msg.data.wallet,
- wallet,
- actionQueue,
- targetImplementationType: msg.data.targetImplementationType,
- })
- return sendMessageToUi({ type: "UPGRADE_ACCOUNT_RES" })
- } catch {
- return sendMessageToUi({ type: "UPGRADE_ACCOUNT_REJ" })
- }
- }
-
- case "REDEPLOY_ACCOUNT": {
- try {
- const account = msg.data
- const fullAccount = await wallet.getAccount(account)
- const { txHash } = await wallet.redeployAccount(fullAccount)
- void addTransaction({
- hash: txHash,
- account: fullAccount,
- meta: { title: "Redeploy wallet", type: "DEPLOY_ACCOUNT" },
- })
- return sendMessageToUi({
- type: "REDEPLOY_ACCOUNT_RES",
- data: {
- txHash,
- address: account.address,
- },
- })
- } catch {
- return sendMessageToUi({ type: "REDEPLOY_ACCOUNT_REJ" })
- }
- }
-
- case "DELETE_ACCOUNT": {
- try {
- await accountService.remove(msg.data)
- return sendMessageToUi({ type: "DELETE_ACCOUNT_RES" })
- } catch {
- return sendMessageToUi({ type: "DELETE_ACCOUNT_REJ" })
- }
- }
-
- case "GET_ENCRYPTED_PRIVATE_KEY": {
- if (!(await wallet.isSessionOpen())) {
- throw Error("you need an open session")
- }
-
- const encryptedPrivateKey = await encryptForUi(
- await wallet.getPrivateKey(msg.data.account),
- msg.data.encryptedSecret,
- privateKey,
- )
-
- return sendMessageToUi({
- type: "GET_ENCRYPTED_PRIVATE_KEY_RES",
- data: { encryptedPrivateKey },
- })
- }
-
- case "GET_PUBLIC_KEY": {
- const { publicKey, account } = await wallet.getPublicKey(msg.data)
-
- return sendMessageToUi({
- type: "GET_PUBLIC_KEY_RES",
- data: { publicKey, account },
- })
- }
-
- case "GET_ENCRYPTED_SEED_PHRASE": {
- if (!(await wallet.isSessionOpen())) {
- throw Error("you need an open session")
- }
-
- const encryptedSeedPhrase = await encryptForUi(
- await wallet.getSeedPhrase(),
- msg.data.encryptedSecret,
- privateKey,
- )
-
- return sendMessageToUi({
- type: "GET_ENCRYPTED_SEED_PHRASE_RES",
- data: { encryptedSeedPhrase },
- })
- }
-
- case "GET_NEXT_PUBLIC_KEY": {
- try {
- const { publicKey } = await wallet.getNextPublicKey(msg.data.networkId)
-
- return sendMessageToUi({
- type: "GET_NEXT_PUBLIC_KEY_RES",
- data: { publicKey },
- })
- } catch (e) {
- console.error(e)
- return sendMessageToUi({
- type: "GET_NEXT_PUBLIC_KEY_REJ",
- })
- }
- }
-
+ // TODO: refactor after we refactor actionHandlers.ts
case "DEPLOY_ACCOUNT_ACTION_FAILED": {
- return await actionQueue.remove(msg.data.actionHash)
- }
-
- case "ACCOUNT_CHANGE_GUARDIAN": {
- try {
- const { account, guardian } = msg.data
-
- const newGuardian = number.hexToDecimalString(guardian)
-
- await actionQueue.push({
- type: "TRANSACTION",
- payload: {
- transactions: {
- contractAddress: account.address,
- entrypoint: "changeGuardian",
- calldata: [newGuardian],
- },
- meta: {
- isChangeGuardian: true,
- title: "Change account guardian",
- type: number.toBN(newGuardian).isZero() // if guardian is 0, it's a remove guardian action
- ? "REMOVE_ARGENT_SHIELD"
- : "ADD_ARGENT_SHIELD",
- },
- },
- })
- return sendMessageToUi({
- type: "ACCOUNT_CHANGE_GUARDIAN_RES",
- })
- } catch (error) {
- return sendMessageToUi({
- type: "ACCOUNT_CHANGE_GUARDIAN_REJ",
- data: `${error}`,
- })
- }
- }
-
- case "ACCOUNT_CANCEL_ESCAPE": {
- try {
- const { account } = msg.data
- await actionQueue.push({
- type: "TRANSACTION",
- payload: {
- transactions: {
- contractAddress: account.address,
- entrypoint: "cancelEscape",
- calldata: [],
- },
- meta: {
- isCancelEscape: true,
- title: "Cancel escape",
- type: "INVOKE_FUNCTION",
- },
- },
- })
- return sendMessageToUi({
- type: "ACCOUNT_CANCEL_ESCAPE_RES",
- })
- } catch (error) {
- return sendMessageToUi({
- type: "ACCOUNT_CANCEL_ESCAPE_REJ",
- data: `${error}`,
- })
- }
- }
-
- case "ACCOUNT_TRIGGER_ESCAPE_GUARDIAN": {
- try {
- const { account } = msg.data
- await actionQueue.push({
- type: "TRANSACTION",
- payload: {
- transactions: {
- contractAddress: account.address,
- entrypoint: "triggerEscapeGuardian",
- calldata: [],
- },
- meta: {
- isCancelEscape: true,
- title: "Trigger escape guardian",
- type: "INVOKE_FUNCTION",
- },
- },
- })
- return sendMessageToUi({
- type: "ACCOUNT_TRIGGER_ESCAPE_GUARDIAN_RES",
- })
- } catch (error) {
- return sendMessageToUi({
- type: "ACCOUNT_TRIGGER_ESCAPE_GUARDIAN_REJ",
- data: `${error}`,
- })
- }
- }
-
- case "ACCOUNT_ESCAPE_AND_CHANGE_GUARDIAN": {
- try {
- const { account } = msg.data
- /**
- * This is a two-stage process
- *
- * 1. call escapeGuardian with current signer key as new guardian key
- * 2. changeGuardian to ZERO, signed twice by same signer key (like 2/2 multisig with same key)
- */
-
- const selectedAccount = await wallet.getAccount(account)
- if (!selectedAccount) {
- throw Error("no account selected")
- }
-
- const { publicKey } = await wallet.getPublicKey(account)
-
- if (
- selectedAccount.guardian &&
- isEqualAddress(selectedAccount.guardian, publicKey)
- ) {
- /**
- * Account already used `escapeGuardian` to change guardian to this account publicKey
- * Call `changeGuardian` to ZERO
- */
-
- await actionQueue.push({
- type: "TRANSACTION",
- payload: {
- transactions: {
- contractAddress: account.address,
- entrypoint: "changeGuardian",
- calldata: [
- number.hexToDecimalString(constants.ZERO.toString()),
- ],
- },
- meta: {
- isChangeGuardian: true,
- title: "Change account guardian",
- type: "INVOKE_FUNCTION",
- },
- },
- })
- } else {
- /**
- * Call `escapeGuardian` to change guardian to this account publicKey
- */
- await actionQueue.push({
- type: "TRANSACTION",
- payload: {
- transactions: {
- contractAddress: account.address,
- entrypoint: "escapeGuardian",
- calldata: [number.hexToDecimalString(publicKey)],
- },
- meta: {
- isChangeGuardian: true,
- title: "Escape account guardian",
- type: "INVOKE_FUNCTION",
- },
- },
- })
- }
-
- return sendMessageToUi({
- type: "ACCOUNT_ESCAPE_AND_CHANGE_GUARDIAN_RES",
- })
- } catch (error) {
- return sendMessageToUi({
- type: "ACCOUNT_ESCAPE_AND_CHANGE_GUARDIAN_REJ",
- data: `${error}`,
- })
- }
+ return await actionService.remove(msg.data.actionHash)
}
}
diff --git a/packages/extension/src/background/accountUpgrade.ts b/packages/extension/src/background/accountUpgrade.ts
index 1e9150062..de8604c16 100644
--- a/packages/extension/src/background/accountUpgrade.ts
+++ b/packages/extension/src/background/accountUpgrade.ts
@@ -1,30 +1,34 @@
-import { stark } from "starknet"
+import { CallData } from "starknet"
-import { ActionItem } from "../shared/actionQueue/types"
-import { getNetwork } from "../shared/network"
+import { networkService } from "../shared/network/service"
import { ArgentAccountType, BaseWalletAccount } from "../shared/wallet.model"
-import { Queue } from "./actionQueue"
+import { IBackgroundActionService } from "./__new/services/action/interface"
import { Wallet } from "./wallet"
+import { isAccountV5 } from "../shared/utils/accountv4"
+import { AccountError } from "../shared/errors/account"
export interface IUpgradeAccount {
account: BaseWalletAccount
wallet: Wallet
- actionQueue: Queue
+ actionService: IBackgroundActionService
targetImplementationType?: ArgentAccountType
}
export const upgradeAccount = async ({
account,
wallet,
- actionQueue,
+ actionService,
targetImplementationType,
}: IUpgradeAccount) => {
const fullAccount = await wallet.getAccount(account)
+ if (!fullAccount) {
+ throw new AccountError({ code: "NOT_FOUND" })
+ }
const starknetAccount = await wallet.getStarknetAccount(account)
const accountType = targetImplementationType ?? fullAccount.type
- const { accountClassHash: newImplementation } = await getNetwork(
+ const { accountClassHash: newImplementation } = await networkService.getById(
fullAccount.network.id,
)
@@ -32,19 +36,23 @@ export const upgradeAccount = async ({
throw "Cannot upgrade account without a new contract implementation"
}
+ const accountTypeWithCairo0Check =
+ accountType === "standardCairo0" ? "standard" : accountType
const implementationClassHash =
- newImplementation[accountType] ?? newImplementation.standard
+ newImplementation[accountTypeWithCairo0Check] ?? newImplementation.standard
- const calldata = stark.compileCalldata({
- implementation: implementationClassHash,
- })
+ if (!isAccountV5(starknetAccount)) {
+ throw new AccountError({ code: "UPGRADE_NOT_SUPPORTED" })
+ }
- if ("estimateAccountDeployFee" in starknetAccount) {
+ const upgradeCalldata = {
+ implementation: implementationClassHash,
// new starknet accounts have a new upgrade interface to allow for transactions right after upgrade
- calldata.push("0")
+ calldata: [0],
}
- await actionQueue.push({
+ const calldata = CallData.compile(upgradeCalldata)
+ await actionService.add({
type: "TRANSACTION",
payload: {
transactions: {
diff --git a/packages/extension/src/background/actionHandlers.ts b/packages/extension/src/background/actionHandlers.ts
index 79efcd132..d6176aa10 100644
--- a/packages/extension/src/background/actionHandlers.ts
+++ b/packages/extension/src/background/actionHandlers.ts
@@ -1,29 +1,31 @@
+import { stark } from "starknet"
+
import { accountService } from "../shared/account/service"
-import { ActionItem, ExtQueueItem } from "../shared/actionQueue/types"
+import { ExtensionActionItem } from "../shared/actionQueue/types"
import { MessageType } from "../shared/messages"
-import { addNetwork, getNetworks } from "../shared/network"
+import { networkService } from "../shared/network/service"
import { preAuthorize } from "../shared/preAuthorizations"
import { isEqualWalletAddress } from "../shared/wallet.service"
import { assertNever } from "../ui/services/assertNever"
import { accountDeployAction } from "./accountDeployAction"
import { analytics } from "./analytics"
-import { BackgroundService } from "./background"
-import { multisigDeployAction } from "./multisigDeployAction"
+import { multisigDeployAction } from "./multisig/multisigDeployAction"
import { openUi } from "./openUi"
import { executeTransactionAction } from "./transactions/transactionExecution"
import { udcDeclareContract, udcDeployContract } from "./udcAction"
+import { Wallet } from "./wallet"
export const handleActionApproval = async (
- action: ExtQueueItem,
- background: BackgroundService,
+ action: ExtensionActionItem,
+ wallet: Wallet,
): Promise => {
- const { wallet } = background
const actionHash = action.meta.hash
+ const selectedAccount = await wallet.getSelectedAccount()
+ const networkId = selectedAccount?.networkId || "unknown"
switch (action.type) {
case "CONNECT_DAPP": {
const { host } = action.payload
- const selectedAccount = await wallet.getSelectedAccount()
if (!selectedAccount) {
void openUi()
@@ -32,7 +34,7 @@ export const handleActionApproval = async (
void analytics.track("preauthorizeDapp", {
host,
- networkId: selectedAccount.networkId,
+ networkId,
})
await preAuthorize(selectedAccount, host)
@@ -41,14 +43,32 @@ export const handleActionApproval = async (
}
case "TRANSACTION": {
+ const host = action.meta.origin
try {
- const response = await executeTransactionAction(action, background)
+ 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: unknown) {
+ } catch (error) {
+ void analytics.track("sentTransaction", {
+ success: false,
+ networkId,
+ host,
+ })
+
return {
type: "TRANSACTION_FAILED",
data: { actionHash, error: `${error}` },
@@ -58,7 +78,11 @@ export const handleActionApproval = async (
case "DEPLOY_ACCOUNT_ACTION": {
try {
- const txHash = await accountDeployAction(action, background)
+ void analytics.track("signedTransaction", {
+ networkId,
+ })
+
+ const txHash = await accountDeployAction(action, wallet)
void analytics.track("deployAccount", {
status: "success",
@@ -66,16 +90,26 @@ export const handleActionApproval = async (
networkId: action.payload.networkId,
})
+ void analytics.track("sentTransaction", {
+ success: true,
+ networkId,
+ })
+
return {
type: "DEPLOY_ACCOUNT_ACTION_SUBMITTED",
data: { txHash, actionHash },
}
- } catch (exception: unknown) {
+ } catch (exception) {
let error = `${exception}`
if (error.includes("403")) {
error = `${error}\n\nA 403 error means there's already something running on the selected port. On macOS, AirPlay is using port 5000 by default, so please try running your node on another port and changing the port in Argent X settings.`
}
+ void analytics.track("sentTransaction", {
+ success: false,
+ networkId,
+ })
+
void analytics.track("deployAccount", {
status: "failure",
networkId: action.payload.networkId,
@@ -91,7 +125,11 @@ export const handleActionApproval = async (
case "DEPLOY_MULTISIG_ACTION": {
try {
- const txHash = await multisigDeployAction(action, background)
+ void analytics.track("signedTransaction", {
+ networkId,
+ })
+
+ const txHash = await multisigDeployAction(action, wallet)
void analytics.track("deployMultisig", {
status: "success",
@@ -99,26 +137,29 @@ export const handleActionApproval = async (
networkId: action.payload.networkId,
})
- return {
- type: "DEPLOY_MULTISIG_ACTION_SUBMITTED",
- data: { txHash, actionHash },
- }
- } catch (exception: unknown) {
+ void analytics.track("sentTransaction", {
+ success: true,
+ networkId,
+ })
+ break
+ } catch (exception) {
let error = `${exception}`
+ console.error(error)
if (error.includes("403")) {
error = `${error}\n\nA 403 error means there's already something running on the selected port. On macOS, AirPlay is using port 5000 by default, so please try running your node on another port and changing the port in Argent X settings.`
}
+ void analytics.track("sentTransaction", {
+ success: false,
+ networkId,
+ })
+
void analytics.track("deployMultisig", {
status: "failure",
networkId: action.payload.networkId,
errorMessage: `${error}`,
})
-
- return {
- type: "DEPLOY_MULTISIG_ACTION_FAILED",
- data: { actionHash, error: `${error}` },
- }
+ break
}
}
@@ -128,13 +169,19 @@ export const handleActionApproval = async (
throw Error("you need an open session")
}
const starknetAccount = await wallet.getSelectedStarknetAccount()
+ const selectedAccount = await wallet.getSelectedAccount()
const signature = await starknetAccount.signMessage(typedData)
+ const formattedSignature = stark.signatureToDecimalArray(signature)
+
+ await analytics.track("signedMessage", {
+ networkId: selectedAccount?.networkId || "unknown",
+ })
return {
type: "SIGNATURE_SUCCESS",
data: {
- signature,
+ signature: formattedSignature,
actionHash,
},
}
@@ -147,13 +194,26 @@ export const handleActionApproval = async (
}
}
- case "REQUEST_SWITCH_CUSTOM_NETWORK": {
+ case "REQUEST_ADD_CUSTOM_NETWORK": {
try {
- const networks = await getNetworks()
+ await networkService.add(action.payload)
+ return {
+ type: "APPROVE_REQUEST_ADD_CUSTOM_NETWORK",
+ data: { actionHash },
+ }
+ } catch (error) {
+ return {
+ type: "REJECT_REQUEST_ADD_CUSTOM_NETWORK",
+ data: { actionHash },
+ }
+ }
+ }
+ case "REQUEST_SWITCH_CUSTOM_NETWORK": {
+ try {
const { chainId } = action.payload
- const network = networks.find((n) => n.chainId === chainId)
+ const network = await networkService.getByChainId(chainId)
if (!network) {
throw Error(`Network with chainId ${chainId} not found`)
@@ -197,21 +257,32 @@ export const handleActionApproval = async (
case "DECLARE_CONTRACT_ACTION": {
try {
- const { classHash, txHash } = await udcDeclareContract(
- action,
- background,
- )
+ void analytics.track("signedDeclareTransaction", {
+ networkId,
+ })
+
+ const { classHash, txHash } = await udcDeclareContract(action, wallet)
+
+ void analytics.track("sentTransaction", {
+ success: true,
+ networkId,
+ })
return {
type: "DECLARE_CONTRACT_ACTION_SUBMITTED",
data: { txHash, actionHash, classHash },
}
- } catch (exception: unknown) {
+ } catch (exception) {
let error = `${exception}`
if (error.includes("403")) {
error = `${error}\n\nA 403 error means there's already something running on the selected port. On macOS, AirPlay is using port 5000 by default, so please try running your node on another port and changing the port in Argent X settings.`
}
+ void analytics.track("sentTransaction", {
+ success: false,
+ networkId,
+ })
+
return {
type: "DECLARE_CONTRACT_ACTION_FAILED",
data: { actionHash, error: `${error}` },
@@ -221,11 +292,20 @@ export const handleActionApproval = async (
case "DEPLOY_CONTRACT_ACTION": {
try {
+ void analytics.track("signedDeployTransaction", {
+ networkId,
+ })
+
const { txHash, contractAddress } = await udcDeployContract(
action,
- background,
+ wallet,
)
+ void analytics.track("sentTransaction", {
+ success: true,
+ networkId,
+ })
+
return {
type: "DEPLOY_CONTRACT_ACTION_SUBMITTED",
data: {
@@ -234,12 +314,17 @@ export const handleActionApproval = async (
actionHash,
},
}
- } catch (exception: unknown) {
+ } catch (exception) {
let error = `${exception}`
if (error.includes("403")) {
error = `${error}\n\nA 403 error means there's already something running on the selected port. On macOS, AirPlay is using port 5000 by default, so please try running your node on another port and changing the port in Argent X settings.`
}
+ void analytics.track("sentTransaction", {
+ success: false,
+ networkId,
+ })
+
return {
type: "DEPLOY_CONTRACT_ACTION_FAILED",
data: { actionHash, error: `${error}` },
@@ -253,8 +338,7 @@ export const handleActionApproval = async (
}
export const handleActionRejection = async (
- action: ExtQueueItem,
- _: BackgroundService,
+ action: ExtensionActionItem,
): Promise => {
const actionHash = action.meta.hash
@@ -284,10 +368,7 @@ export const handleActionRejection = async (
}
case "DEPLOY_MULTISIG_ACTION": {
- return {
- type: "DEPLOY_MULTISIG_ACTION_FAILED",
- data: { actionHash },
- }
+ break
}
case "SIGN": {
@@ -304,6 +385,13 @@ export const handleActionRejection = async (
}
}
+ case "REQUEST_ADD_CUSTOM_NETWORK": {
+ return {
+ type: "REJECT_REQUEST_ADD_CUSTOM_NETWORK",
+ data: { actionHash },
+ }
+ }
+
case "REQUEST_SWITCH_CUSTOM_NETWORK": {
return {
type: "REJECT_REQUEST_SWITCH_CUSTOM_NETWORK",
diff --git a/packages/extension/src/background/actionMessaging.ts b/packages/extension/src/background/actionMessaging.ts
index 585dc22ac..dd4ebed55 100644
--- a/packages/extension/src/background/actionMessaging.ts
+++ b/packages/extension/src/background/actionMessaging.ts
@@ -1,62 +1,24 @@
import { ActionMessage } from "../shared/messages/ActionMessage"
-import { handleActionApproval, handleActionRejection } from "./actionHandlers"
-import { sendMessageToUi } from "./activeTabs"
import { UnhandledMessage } from "./background"
import { HandleMessage } from "./background"
export const handleActionMessage: HandleMessage = async ({
msg,
- background,
+ origin,
+ background: { actionService },
respond,
}) => {
- const { actionQueue } = background
-
switch (msg.type) {
- case "GET_ACTIONS": {
- const actions = await actionQueue.getAll()
- return sendMessageToUi({
- type: "GET_ACTIONS_RES",
- data: actions,
- })
- }
-
- case "APPROVE_ACTION": {
- const { actionHash } = msg.data
- const action = await actionQueue.remove(actionHash)
- if (!action) {
- throw new Error("Action not found")
- }
- const resultMessage = await handleActionApproval(action, background)
-
- if (resultMessage) {
- respond(resultMessage)
- }
- return
- }
-
- case "REJECT_ACTION": {
- const payload = msg.data.actionHash
-
- const actionHashes = Array.isArray(payload) ? payload : [payload]
-
- for (const actionHash of actionHashes) {
- const action = await actionQueue.remove(actionHash)
- if (!action) {
- throw new Error("Action not found")
- }
- const resultMessage = await handleActionRejection(action, background)
- if (resultMessage) {
- respond(resultMessage)
- }
- }
- return
- }
-
case "SIGN_MESSAGE": {
- const { meta } = await actionQueue.push({
- type: "SIGN",
- payload: msg.data,
- })
+ const { meta } = await actionService.add(
+ {
+ type: "SIGN",
+ payload: msg.data,
+ },
+ {
+ origin,
+ },
+ )
return respond({
type: "SIGN_MESSAGE_RES",
@@ -67,7 +29,7 @@ export const handleActionMessage: HandleMessage = async ({
}
case "SIGNATURE_FAILURE": {
- return await actionQueue.remove(msg.data.actionHash)
+ return await actionService.remove(msg.data.actionHash)
}
}
diff --git a/packages/extension/src/background/actionQueue.ts b/packages/extension/src/background/actionQueue.ts
deleted file mode 100644
index f8c730a20..000000000
--- a/packages/extension/src/background/actionQueue.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { partition } from "lodash-es"
-import oHash from "object-hash"
-
-import { ExtQueueItem } from "../shared/actionQueue/types"
-import type { IArrayStorage } from "../shared/storage/array"
-
-function objectHash(obj: object | null) {
- return oHash(obj, { unorderedArrays: true })
-}
-
-type AllObjects = Record
-
-export interface Queue {
- getAll: () => Promise[]>
- push: (item: T, expires?: number) => Promise>
- remove: (hash: string) => Promise | null>
-}
-
-export async function getQueue(
- storage: IArrayStorage>,
-): Promise> {
- async function getAll(): Promise[]> {
- const allInQueue = await storage.get()
- const [notExpiredQueue, expiredItems] = partition(
- allInQueue,
- (item) => item.meta.expires > Date.now(),
- )
-
- // set queue to storage if it has changed
- if (expiredItems.length) {
- await storage.remove(expiredItems)
- }
-
- return notExpiredQueue
- }
-
- async function push(
- item: T,
- expires: number = 5 * 60 * 60 * 1000, // 5 hours
- ): Promise> {
- const hash = objectHash(item)
- const newItem = {
- ...item,
- meta: {
- hash,
- expires: Date.now() + expires,
- },
- }
-
- await storage.unshift(newItem)
-
- return newItem
- }
-
- async function remove(hash: string): Promise | null> {
- const [item] = await storage.remove((item) => item.meta.hash === hash)
- return item ?? null
- }
-
- return {
- getAll,
- push,
- remove,
- }
-}
diff --git a/packages/extension/src/background/activeTabs.ts b/packages/extension/src/background/activeTabs.ts
index 4400935e8..3db2636d8 100644
--- a/packages/extension/src/background/activeTabs.ts
+++ b/packages/extension/src/background/activeTabs.ts
@@ -1,52 +1,52 @@
import browser from "webextension-polyfill"
import { MessageType, sendMessage } from "../shared/messages"
-import { ArrayStorage } from "../shared/storage"
+import { UniqueSet } from "./utils/uniqueSet"
interface Tab {
id: number
host: string
port: browser.runtime.Port
}
-const activeTabs = new ArrayStorage([], {
- namespace: "core:activeTabs",
- areaName: "session",
- compare(a, b) {
- return a.id === b.id
- },
-})
+
+// do not store the port in any storage, like ArrayStorage.
+// it is not serializable and will cause errors
+// ports also get closed when the background worker is reloaded, and they should automatically reconnect
+const activeTabs = new UniqueSet((t) => t.id)
browser.tabs.onRemoved.addListener(removeTab)
export async function addTab(tab: Tab) {
if (tab.id !== undefined) {
- return activeTabs.push(tab)
+ return activeTabs.add(tab)
}
}
export function removeTab(tabId?: number) {
- return activeTabs.remove((tab) => tab.id === tabId)
+ if (tabId === undefined) {
+ return false
+ }
+ return activeTabs.delete(tabId)
}
export async function hasTab(tabId?: number) {
if (tabId === undefined) {
return false
}
- const [hit] = await activeTabs.get((tab) => tab.id === tabId)
- return Boolean(hit)
+ return activeTabs.has(tabId)
}
-export async function getTabIdsOfHost(host: string) {
- const hits = await activeTabs.get((tab) => tab.host === host)
- return hits ? hits.map((tab) => tab.id) : []
+async function getTabsOfHost(host: string) {
+ const allTabs = activeTabs.getAll()
+ const hits = allTabs.filter((tab) => tab.host === host)
+ return hits
}
export async function sendMessageToHost(
message: MessageType,
host: string,
): Promise {
- const tabIds = await getTabIdsOfHost(host)
- const tabs = await activeTabs.get((tab) => tabIds.includes(tab.id))
+ const tabs = await getTabsOfHost(host)
for (const tab of tabs) {
try {
@@ -60,7 +60,7 @@ export async function sendMessageToHost(
export async function sendMessageToActiveTabs(
message: MessageType,
): Promise {
- const tabs = await activeTabs.get()
+ const tabs = activeTabs.getAll()
for (const tab of tabs) {
try {
diff --git a/packages/extension/src/background/background.ts b/packages/extension/src/background/background.ts
index ec8350b1d..6794900ad 100644
--- a/packages/extension/src/background/background.ts
+++ b/packages/extension/src/background/background.ts
@@ -1,16 +1,15 @@
import browser from "webextension-polyfill"
-import { ActionItem } from "../shared/actionQueue/types"
-import { MessageType } from "../shared/messages"
-import { Queue } from "./actionQueue"
-import { MessagingKeys } from "./keys/messagingKeys"
-import { TransactionTracker } from "./transactions/tracking"
+import { IBackgroundActionService } from "./__new/services/action/interface"
+import type { MessagingKeys } from "./keys/messagingKeys"
+import type { Respond } from "./respond"
import { Wallet } from "./wallet"
+import { TransactionTrackerWorker } from "./transactions/service/starknet.service"
export interface BackgroundService {
wallet: Wallet
- transactionTracker: TransactionTracker
- actionQueue: Queue
+ transactionTrackerWorker: TransactionTrackerWorker
+ actionService: IBackgroundActionService
}
export class UnhandledMessage extends Error {
@@ -23,10 +22,11 @@ export class UnhandledMessage extends Error {
interface HandlerParams {
msg: T
sender: browser.runtime.MessageSender
+ origin: string
port?: browser.runtime.Port
background: BackgroundService
messagingKeys: MessagingKeys
- respond: (msg: MessageType) => Promise
+ respond: Respond
}
export type HandleMessage = (params: HandlerParams) => Promise
diff --git a/packages/extension/src/background/devnet/declareAccounts.ts b/packages/extension/src/background/devnet/declareAccounts.ts
index d55af0a75..bdbbf1794 100644
--- a/packages/extension/src/background/devnet/declareAccounts.ts
+++ b/packages/extension/src/background/devnet/declareAccounts.ts
@@ -1,14 +1,14 @@
import { memoize } from "lodash-es"
-import { Account, AccountInterface, ec } from "starknet"
-import { hash } from "starknet5"
+import { Account, AccountInterface, hash } from "starknet"
import urlJoin from "url-join"
import { Network, getProvider } from "../../shared/network"
-import { LoadContracts } from "../accounts"
+import { LoadContracts } from "../wallet/loadContracts"
import {
ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES,
PROXY_CONTRACT_CLASS_HASHES,
-} from "../wallet"
+} from "../wallet/starknet.constants"
+import { getNetworkUrl } from "../../shared/network/utils"
interface PreDeployedAccount {
address: string
@@ -20,8 +20,10 @@ export const getPreDeployedAccount = async (
index = 0,
): Promise => {
try {
+ const networkUrl = getNetworkUrl(network)
+
const preDeployedAccounts = await fetch(
- urlJoin(network.baseUrl, "predeployed_accounts"),
+ urlJoin(networkUrl, "predeployed_accounts"),
).then((x) => x.json() as Promise)
const preDeployedAccount = preDeployedAccounts[index]
@@ -30,8 +32,13 @@ export const getPreDeployedAccount = async (
}
const provider = getProvider(network)
- const keypair = ec.getKeyPair(preDeployedAccount.private_key)
- return new Account(provider, preDeployedAccount.address, keypair)
+
+ return new Account(
+ provider,
+ preDeployedAccount.address,
+ preDeployedAccount.private_key,
+ "0", // Devnet is currently supporting only cairo 0
+ )
} catch (e) {
console.warn(`Failed to get pre-deployed account: ${e}`)
return null
@@ -69,7 +76,9 @@ export const declareContracts = memoize(
contract: proxyContract,
})
- await deployAccount.waitForTransaction(proxy.transaction_hash, 1e3)
+ await deployAccount.waitForTransaction(proxy.transaction_hash, {
+ retryInterval: 1e3,
+ })
proxyClassHash = proxy.class_hash
}
@@ -80,7 +89,9 @@ export const declareContracts = memoize(
contract: accountContract,
})
- await deployAccount.waitForTransaction(account.transaction_hash, 1e3)
+ await deployAccount.waitForTransaction(account.transaction_hash, {
+ retryInterval: 1e3,
+ })
accountClassHash = account.class_hash
}
@@ -90,7 +101,7 @@ export const declareContracts = memoize(
accountClassHash: accountClassHash ?? computedAccountClassHash,
}
},
- (network) => `${network.baseUrl}`,
+ (network) => `${network.sequencerUrl}`,
)
export const checkIfClassIsDeclared = async (
diff --git a/packages/extension/src/background/download.ts b/packages/extension/src/background/download.ts
deleted file mode 100644
index 1c860e588..000000000
--- a/packages/extension/src/background/download.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import browser from "webextension-polyfill"
-
-export async function downloadFile(data: { url: string; filename: string }) {
- browser.downloads.download(data)
-}
diff --git a/packages/extension/src/background/index.ts b/packages/extension/src/background/index.ts
index ebb8e4b51..ba91148fb 100644
--- a/packages/extension/src/background/index.ts
+++ b/packages/extension/src/background/index.ts
@@ -1,324 +1,26 @@
import "./__new/router"
+import "./transactions/service/worker"
+import "./migrations"
-import { StarknetMethodArgumentsSchemas } from "@argent/x-window"
-import browser from "webextension-polyfill"
-
-import { accountService } from "../shared/account/service"
-import { globalActionQueueStore } from "../shared/actionQueue/store"
-import { ActionItem } from "../shared/actionQueue/types"
-import { MessageType, messageStream } from "../shared/messages"
-import { multisigTracker } from "../shared/multisig/tracking"
-import {
- isPreAuthorized,
- migratePreAuthorizations,
-} from "../shared/preAuthorizations"
-import { delay } from "../shared/utils/delay"
-import { migrateWallet } from "../shared/wallet/storeMigration"
-import { handleAccountMessage } from "./accountMessaging"
-import { handleActionMessage } from "./actionMessaging"
-import { getQueue } from "./actionQueue"
-import {
- hasTab,
- sendMessageToActiveTabs,
- sendMessageToActiveTabsAndUi,
- sendMessageToUi,
-} from "./activeTabs"
-import {
- BackgroundService,
- HandleMessage,
- UnhandledMessage,
-} from "./background"
-import { getMessagingKeys } from "./keys/messagingKeys"
-import { handleMiscellaneousMessage } from "./miscellaneousMessaging"
-import { handleMultisigMessage } from "./multisigMessaging"
-import { networkService } from "./network/network.service"
-import { handleNetworkMessage } from "./networkMessaging"
-import { initOnboarding } from "./onboarding"
-import {
- getOriginFromSender,
- handlePreAuthorizationMessage,
-} from "./preAuthorizationMessaging"
-import { handleSessionMessage } from "./sessionMessaging"
-import { handleShieldMessage } from "./shieldMessaging"
-import { handleTokenMessaging } from "./tokenMessaging"
+import { messageStream } from "../shared/messages"
+import { initWorkers } from "./workers"
import { initBadgeText } from "./transactions/badgeText"
-import { transactionTracker } from "./transactions/tracking"
-import { handleTransactionMessage } from "./transactions/transactionMessaging"
-import { handleUdcMessaging } from "./udcMessaging"
-import { walletSingleton } from "./walletSingleton"
-
-const DEFAULT_POLLING_INTERVAL = 15
-const LOCAL_POLLING_INTERVAL = 5
-
-const enum ALARM_NAMES {
- TRANSACTION_TRACKER_HISTORY = "core:transactionTracker:history",
- TRANSACTION_TRACKER_UPDATE = "core:transactionTracker:update",
- MULTISIG_ACCOUNT_UPDATE = "core:multisig:updateDataForAccounts",
- MULTISIG_PENDING_UPDATE = "core:multisig:updateDataForPendingMultisig",
- MULTISIG_TRANSACTION_TRACKER = "core:multisig:transactionTracker",
- NETWORK_STATUS_TRACKER = "core:networkStatusTracker:update",
-}
-
-browser.alarms.create(ALARM_NAMES.TRANSACTION_TRACKER_HISTORY, {
- periodInMinutes: 5, // fetch history transactions every 5 minutes from voyager
-})
-browser.alarms.create(ALARM_NAMES.TRANSACTION_TRACKER_UPDATE, {
- periodInMinutes: 1, // fetch transaction updates of existing transactions every minute from onchain
-})
-browser.alarms.create(ALARM_NAMES.MULTISIG_ACCOUNT_UPDATE, {
- periodInMinutes: 5, // fetch multisig updates of existing multisigs every 5 minutes from backend
-})
-browser.alarms.create(ALARM_NAMES.MULTISIG_PENDING_UPDATE, {
- periodInMinutes: 3, // fetch pending multisig updates of existing multisigs every 3 minutes from backend
-})
-browser.alarms.create(ALARM_NAMES.MULTISIG_TRANSACTION_TRACKER, {
- periodInMinutes: 2, // fetch transaction updates of existing multisig every 2 minutes from backend
-})
-
-// eslint-disable-next-line @typescript-eslint/no-misused-promises
-browser.alarms.onAlarm.addListener(async (alarm) => {
- switch (alarm.name) {
- case ALARM_NAMES.TRANSACTION_TRACKER_HISTORY: {
- console.info("~> fetching transaction history")
- await transactionTracker.loadHistory(await accountService.get())
- break
- }
-
- case ALARM_NAMES.MULTISIG_ACCOUNT_UPDATE: {
- console.info("~> fetching multisig account updates")
- await multisigTracker.updateDataForAccounts()
- break
- }
-
- case ALARM_NAMES.MULTISIG_PENDING_UPDATE: {
- console.info("~> fetching pending multisig account updates")
- await multisigTracker.updateDataForPendingMultisig()
- break
- }
-
- case ALARM_NAMES.MULTISIG_TRANSACTION_TRACKER: {
- console.info("~> fetching multisig transaction updates")
- await multisigTracker.updateTransactions()
- break
- }
-
- case ALARM_NAMES.TRANSACTION_TRACKER_UPDATE: {
- console.info("~> fetching transaction updates")
- let inFlightTransactions = await transactionTracker.update()
- // the config below will run transaction updates 4x per minute, if there are in-flight transactions
- // By default it will update on second 0, 15, 30 and 45 but by updating WAIT_TIME we can change the number of executions
- const maxExecutionTimeInMs = 60000 // 1 minute max execution time
- let transactionPollingIntervalInS = DEFAULT_POLLING_INTERVAL
- const startTime = Date.now()
-
- while (
- inFlightTransactions.length > 0 &&
- Date.now() - startTime < maxExecutionTimeInMs
- ) {
- const localTransaction = inFlightTransactions.find(
- (tx) => tx.account.networkId === "localhost",
- )
- if (localTransaction) {
- transactionPollingIntervalInS = LOCAL_POLLING_INTERVAL
- } else {
- transactionPollingIntervalInS = DEFAULT_POLLING_INTERVAL
- }
- console.info(
- `~> waiting ${transactionPollingIntervalInS}s for transaction updates`,
- )
- await delay(transactionPollingIntervalInS * 1000)
- console.info(
- "~> fetching transaction updates as pending transactions were detected",
- )
- inFlightTransactions = await transactionTracker.update()
- }
- break
- }
-
- case ALARM_NAMES.NETWORK_STATUS_TRACKER: {
- await networkService.updateStatuses()
- break
- }
-
- default:
- break
- }
-})
+import { transactionTrackerWorker } from "./transactions/service/worker"
+import { handleMessage } from "./messageHandling/handle"
+import { addMessageListeners } from "./messageHandling/addMessageListeners"
// badge shown on extension icon
-
initBadgeText()
-// runs on startup
-
-const handlers = [
- handleAccountMessage,
- handleActionMessage,
- handleMiscellaneousMessage,
- handleNetworkMessage,
- handlePreAuthorizationMessage,
- handleSessionMessage,
- handleTransactionMessage,
- handleTokenMessaging,
- handleUdcMessaging,
- handleShieldMessage,
- handleMultisigMessage,
-] as Array>
-
-accountService
- .get()
- .then((x) => transactionTracker.loadHistory(x))
+// load transaction history
+transactionTrackerWorker
+ .loadHistory()
.catch(() => console.warn("failed to load transaction history"))
-const safeMessages: MessageType["type"][] = [
- "IS_PREAUTHORIZED",
- "CONNECT_DAPP",
- "DISCONNECT_ACCOUNT",
- "OPEN_UI",
- // answers
- "EXECUTE_TRANSACTION_RES",
- "TRANSACTION_SUBMITTED",
- "TRANSACTION_FAILED",
- "SIGN_MESSAGE_RES",
- "SIGNATURE_SUCCESS",
- "SIGNATURE_FAILURE",
- "IS_PREAUTHORIZED_RES",
- "REQUEST_TOKEN_RES",
- "APPROVE_REQUEST_TOKEN",
- "REJECT_REQUEST_TOKEN",
- "REQUEST_ADD_CUSTOM_NETWORK_RES",
- "APPROVE_REQUEST_ADD_CUSTOM_NETWORK",
- "REJECT_REQUEST_ADD_CUSTOM_NETWORK",
- "REQUEST_SWITCH_CUSTOM_NETWORK_RES",
- "APPROVE_REQUEST_SWITCH_CUSTOM_NETWORK",
- "REJECT_REQUEST_SWITCH_CUSTOM_NETWORK",
- "CONNECT_DAPP_RES",
- "CONNECT_ACCOUNT_RES",
- "REJECT_PREAUTHORIZATION",
- "REQUEST_DECLARE_CONTRACT_RES",
- "DECLARE_CONTRACT_ACTION_FAILED",
- "DECLARE_CONTRACT_ACTION_SUBMITTED",
-]
-
-const safeIfPreauthorizedMessages: MessageType["type"][] = [
- "EXECUTE_TRANSACTION",
- "SIGN_MESSAGE",
- "REQUEST_TOKEN",
- "REQUEST_SWITCH_CUSTOM_NETWORK",
- "REQUEST_DECLARE_CONTRACT",
-]
-
-const handleMessage = async (
- [msg, sender]: [MessageType, browser.runtime.MessageSender],
- port?: browser.runtime.Port,
-) => {
- await Promise.all([migrateWallet(), migratePreAuthorizations()]) // do migrations before handling messages
-
- const messagingKeys = await getMessagingKeys()
-
- const actionQueue = await getQueue(globalActionQueueStore)
-
- const background: BackgroundService = {
- wallet: walletSingleton,
- transactionTracker,
- actionQueue,
- }
-
- const extensionUrl = browser.extension.getURL("")
- const safeOrigin = extensionUrl.replace(/\/$/, "")
- const origin = getOriginFromSender(sender)
- const isSafeOrigin = Boolean(origin === safeOrigin)
-
- const currentAccount = await walletSingleton.getSelectedAccount()
- const senderIsPreauthorized =
- !!currentAccount && (await isPreAuthorized(currentAccount, origin))
-
- if (
- !isSafeOrigin && // allow all messages from the extension itself
- !safeMessages.includes(msg.type) && // allow messages that are needed to get into preauthorization state
- !(senderIsPreauthorized && safeIfPreauthorizedMessages.includes(msg.type)) // allow additional messages if sender is preauthorized
- ) {
- console.warn(
- `received message of type ${msg.type} from ${origin} but it is not allowed`,
- )
- return // this return must not be removed
- }
-
- // forward UI messages to rest of the tabs
- if (isSafeOrigin) {
- if (await hasTab(sender.tab?.id)) {
- await sendMessageToActiveTabs(msg)
- }
- }
-
- const respond = async (msg: MessageType) => {
- if (safeMessages.includes(msg.type)) {
- await sendMessageToActiveTabsAndUi(msg)
- } else {
- await sendMessageToUi(msg)
- }
- }
-
- for (const handleMessage of handlers) {
- try {
- await handleMessage({
- msg,
- sender,
- background,
- messagingKeys,
- port,
- respond,
- })
- } catch (error) {
- if (error instanceof UnhandledMessage) {
- continue
- }
- throw error
- }
- break
- }
-}
-
-browser.runtime.onConnect.addListener((port) => {
- // eslint-disable-next-line @typescript-eslint/no-misused-promises
- port.onMessage.addListener(async (msg: MessageType, port) => {
- const sender = port.sender
- if (sender) {
- switch (msg.type) {
- case "EXECUTE_TRANSACTION": {
- const [transactions, abis, transactionsDetail] =
- await StarknetMethodArgumentsSchemas.execute.parseAsync([
- msg.data.transactions,
- msg.data.abis,
- msg.data.transactionsDetail,
- ])
- return handleMessage(
- [
- { ...msg, data: { transactions, abis, transactionsDetail } },
- sender,
- ],
- port,
- )
- }
-
- case "SIGN_MESSAGE": {
- const [message] =
- await StarknetMethodArgumentsSchemas.signMessage.parseAsync([
- msg.data,
- ])
- return handleMessage([{ ...msg, data: message }, sender], port)
- }
-
- default:
- return handleMessage([msg, sender], port)
- }
- }
- })
-})
+addMessageListeners()
// eslint-disable-next-line @typescript-eslint/no-misused-promises
messageStream.subscribe(handleMessage)
-// open onboarding flow on initial install
-
-initOnboarding()
+// start workers
+initWorkers()
diff --git a/packages/extension/src/background/keys/keyDerivation.ts b/packages/extension/src/background/keys/keyDerivation.ts
index 48bef3907..21d49152a 100644
--- a/packages/extension/src/background/keys/keyDerivation.ts
+++ b/packages/extension/src/background/keys/keyDerivation.ts
@@ -1,23 +1,93 @@
-import { BigNumber, BigNumberish, utils } from "ethers"
-import { isNumber } from "lodash-es"
-import { KeyPair, ec, number } from "starknet"
+import { Hex, bytesToHex, hexToBytes } from "@noble/curves/abstract/utils"
+import { sha256 } from "@noble/hashes/sha256"
+import { HDKey } from "@scure/bip32"
+import { isFunction, isNumber } from "lodash-es"
+import { encode, num } from "starknet"
+import { getStarkKey, grindKey as microGrindKey } from "micro-starknet"
+
+const { addHexPrefix } = encode
+
+export interface KeyPair {
+ pubKey: string
+ getPrivate: () => string
+}
+
+export interface KeyPairWithIndex extends KeyPair {
+ index: number
+}
+
+export interface PublicKeyWithIndex {
+ pubKey: string
+ index: number
+}
export function getStarkPair(
indexOrPath: T,
- secret: BigNumberish,
+ secret: string,
...[baseDerivationPath]: T extends string ? [] : [string]
): KeyPair {
- const masterNode = utils.HDNode.fromSeed(BigNumber.from(secret).toHexString())
+ const hex = encode.removeHexPrefix(num.toHex(secret))
+
+ // Bytes must be a multiple of 2 and default is multiple of 8
+ // sanitizeHex should not be used because of leading 0x
+ const sanitized = encode.sanitizeBytes(hex, 2)
+
+ const masterNode = HDKey.fromMasterSeed(hexToBytes(sanitized))
// baseDerivationPath will never be undefined because of the extends statement below,
// but somehow TS doesnt get this. As this will be removed in the near future I didnt bother
const path: string = isNumber(indexOrPath)
? getPathForIndex(indexOrPath, baseDerivationPath ?? "")
: indexOrPath
- const childNode = masterNode.derivePath(path)
+ const childNode = masterNode.derive(path)
+
+ if (!childNode.privateKey) {
+ throw "childNode.privateKey is undefined"
+ }
+
const groundKey = grindKey(childNode.privateKey)
- const starkPair = ec.getKeyPair(groundKey)
- return starkPair
+
+ return {
+ pubKey: encode.sanitizeHex(getStarkKey(groundKey)),
+ getPrivate: () => encode.sanitizeHex(groundKey),
+ }
+}
+
+/**
+ * Grinds a private key to a valid StarkNet private key
+ * @param privateKey
+ * @returns Unsantized hex string
+ */
+export function grindKey(privateKey: Hex): string {
+ return addHexPrefix(microGrindKey(privateKey))
+}
+
+export function generateStarkKeyPairs(
+ secret: string,
+ start: number,
+ numberOfPairs: number,
+ baseDerivationPath: string,
+): KeyPairWithIndex[] {
+ const keyPairs: KeyPairWithIndex[] = []
+ for (let index = start; index < start + numberOfPairs; index++) {
+ keyPairs.push({ ...getStarkPair(index, secret, baseDerivationPath), index })
+ }
+ return keyPairs
+}
+
+export function generatePublicKeys(
+ secret: string,
+ start: number,
+ numberOfPairs: number,
+ baseDerivationPath: string,
+): PublicKeyWithIndex[] {
+ const keyPairs = generateStarkKeyPairs(
+ secret,
+ start,
+ numberOfPairs,
+ baseDerivationPath,
+ )
+ return keyPairs.map(({ pubKey, index }) => ({ pubKey, index }))
}
export function getPathForIndex(
@@ -47,41 +117,15 @@ export function getNextPathIndex(
return paths.length
}
-// inspired/copied from https://github.com/authereum/starkware-monorepo/blob/51c5df19e7f98399a2f7e63d564210d761d138d1/packages/starkware-crypto/src/keyDerivation.ts#L85
-export function grindKey(keySeed: string): string {
- const keyValueLimit = ec.ec.n
- if (!keyValueLimit) {
- return keySeed
- }
- const sha256EcMaxDigest = number.toBN(
- "1 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000",
- 16,
- )
- const maxAllowedVal = sha256EcMaxDigest.sub(
- sha256EcMaxDigest.mod(keyValueLimit),
+export function pathHash(name: string): number {
+ const bigHash = BigInt.asUintN(
+ 31,
+ BigInt(addHexPrefix(bytesToHex(sha256(name)))),
)
- // Make sure the produced key is devided by the Stark EC order,
- // and falls within the range [0, maxAllowedVal).
- let i = 0
- let key
- do {
- key = hashKeyWithIndex(keySeed, i)
- i++
- } while (!key.lt(maxAllowedVal))
-
- return "0x" + key.umod(keyValueLimit).toString("hex")
+ return Number(bigHash)
}
-function hashKeyWithIndex(key: string, index: number) {
- const payload = utils.concat([utils.arrayify(key), utils.arrayify(index)])
- const hash = utils.sha256(payload)
- return number.toBN(hash)
-}
-
-export function pathHash(name: string): number {
- return number
- .toBN(utils.sha256(utils.toUtf8Bytes(name)))
- .maskn(31)
- .toNumber()
+export function isKeyPair(val: any): val is KeyPair {
+ return val && val.pubKey && isFunction(val.getPrivate)
}
diff --git a/packages/extension/src/background/messageHandling/addMessageListeners.ts b/packages/extension/src/background/messageHandling/addMessageListeners.ts
new file mode 100644
index 000000000..640273ecb
--- /dev/null
+++ b/packages/extension/src/background/messageHandling/addMessageListeners.ts
@@ -0,0 +1,43 @@
+import browser from "webextension-polyfill"
+import { StarknetMethodArgumentsSchemas } from "@argent/x-window"
+import { MessageType } from "../../shared/messages"
+import { handleMessage } from "./handle"
+
+export const addMessageListeners = () => {
+ browser.runtime.onConnect.addListener((port) => {
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
+ port.onMessage.addListener(async (msg: MessageType, port) => {
+ const sender = port.sender
+ if (sender) {
+ switch (msg.type) {
+ case "EXECUTE_TRANSACTION": {
+ const [transactions, abis, transactionsDetail] =
+ await StarknetMethodArgumentsSchemas.execute.parseAsync([
+ msg.data.transactions,
+ msg.data.abis,
+ msg.data.transactionsDetail,
+ ])
+ return handleMessage(
+ [
+ { ...msg, data: { transactions, abis, transactionsDetail } },
+ sender,
+ ],
+ port,
+ )
+ }
+
+ case "SIGN_MESSAGE": {
+ const [message] =
+ await StarknetMethodArgumentsSchemas.signMessage.parseAsync([
+ msg.data,
+ ])
+ return handleMessage([{ ...msg, data: message }, sender], port)
+ }
+
+ default:
+ return handleMessage([msg, sender], port)
+ }
+ }
+ })
+ })
+}
diff --git a/packages/extension/src/background/messageHandling/handle.ts b/packages/extension/src/background/messageHandling/handle.ts
new file mode 100644
index 000000000..d3170a001
--- /dev/null
+++ b/packages/extension/src/background/messageHandling/handle.ts
@@ -0,0 +1,104 @@
+import { MessageType } from "../../shared/messages"
+import {
+ migratePreAuthorizations,
+ isPreAuthorized,
+} from "../../shared/preAuthorizations"
+import { migrateWallet } from "../../shared/wallet/storeMigration"
+import { backgroundActionService } from "../__new/services/action"
+import { handleAccountMessage } from "../accountMessaging"
+import { handleActionMessage } from "../actionMessaging"
+import { hasTab, sendMessageToActiveTabs } from "../activeTabs"
+import {
+ BackgroundService,
+ HandleMessage,
+ UnhandledMessage,
+} from "../background"
+import { getMessagingKeys } from "../keys/messagingKeys"
+import { handleMiscellaneousMessage } from "../miscellaneousMessaging"
+import { handleNetworkMessage } from "../networkMessaging"
+import {
+ getOriginFromSender,
+ handlePreAuthorizationMessage,
+} from "../preAuthorizationMessaging"
+import { respond } from "../respond"
+import { handleTokenMessaging } from "../tokenMessaging"
+import { transactionTrackerWorker } from "../transactions/service/worker"
+import { handleTransactionMessage } from "../transactions/transactionMessaging"
+import { handleUdcMessaging } from "../udcMessaging"
+import { walletSingleton } from "../walletSingleton"
+import { safeMessages, safeIfPreauthorizedMessages } from "./messages"
+import browser from "webextension-polyfill"
+
+const handlers = [
+ handleAccountMessage,
+ handleActionMessage,
+ handleMiscellaneousMessage,
+ handleNetworkMessage,
+ handlePreAuthorizationMessage,
+ handleTransactionMessage,
+ handleTokenMessaging,
+ handleUdcMessaging,
+] as Array>
+
+export const handleMessage = async (
+ [msg, sender]: [MessageType, browser.runtime.MessageSender],
+ port?: browser.runtime.Port,
+) => {
+ await Promise.all([migrateWallet(), migratePreAuthorizations()]) // do migrations before handling messages
+
+ const messagingKeys = await getMessagingKeys()
+
+ /** TODO: refactor into background service singleton pattern */
+ const background: BackgroundService = {
+ wallet: walletSingleton,
+ transactionTrackerWorker: transactionTrackerWorker,
+ actionService: backgroundActionService,
+ }
+
+ const extensionUrl = browser.extension.getURL("")
+ const safeOrigin = extensionUrl.replace(/\/$/, "")
+ const origin = getOriginFromSender(sender)
+ const isSafeOrigin = Boolean(origin === safeOrigin)
+
+ const currentAccount = await walletSingleton.getSelectedAccount()
+ const senderIsPreauthorized =
+ !!currentAccount && (await isPreAuthorized(currentAccount, origin))
+
+ if (
+ !isSafeOrigin && // allow all messages from the extension itself
+ !safeMessages.includes(msg.type) && // allow messages that are needed to get into preauthorization state
+ !(senderIsPreauthorized && safeIfPreauthorizedMessages.includes(msg.type)) // allow additional messages if sender is preauthorized
+ ) {
+ console.warn(
+ `received message of type ${msg.type} from ${origin} but it is not allowed`,
+ )
+ return // this return must not be removed
+ }
+
+ // forward UI messages to rest of the tabs
+ if (isSafeOrigin) {
+ if (await hasTab(sender.tab?.id)) {
+ await sendMessageToActiveTabs(msg)
+ }
+ }
+
+ for (const handleMessage of handlers) {
+ try {
+ await handleMessage({
+ msg,
+ sender,
+ origin,
+ background,
+ messagingKeys,
+ port,
+ respond,
+ })
+ } catch (error) {
+ if (error instanceof UnhandledMessage) {
+ continue
+ }
+ throw error
+ }
+ break
+ }
+}
diff --git a/packages/extension/src/background/messageHandling/messages.ts b/packages/extension/src/background/messageHandling/messages.ts
new file mode 100644
index 000000000..1db62915d
--- /dev/null
+++ b/packages/extension/src/background/messageHandling/messages.ts
@@ -0,0 +1,40 @@
+import { MessageType } from "../../shared/messages"
+
+export const safeMessages: MessageType["type"][] = [
+ "IS_PREAUTHORIZED",
+ "CONNECT_DAPP",
+ "DISCONNECT_ACCOUNT",
+ "OPEN_UI",
+ // answers
+ "EXECUTE_TRANSACTION_RES",
+ "TRANSACTION_SUBMITTED",
+ "TRANSACTION_FAILED",
+ "SIGN_MESSAGE_RES",
+ "SIGNATURE_SUCCESS",
+ "SIGNATURE_FAILURE",
+ "IS_PREAUTHORIZED_RES",
+ "REQUEST_TOKEN_RES",
+ "APPROVE_REQUEST_TOKEN",
+ "REJECT_REQUEST_TOKEN",
+ "REQUEST_ADD_CUSTOM_NETWORK_RES",
+ "APPROVE_REQUEST_ADD_CUSTOM_NETWORK",
+ "REJECT_REQUEST_ADD_CUSTOM_NETWORK",
+ "REQUEST_SWITCH_CUSTOM_NETWORK_RES",
+ "APPROVE_REQUEST_SWITCH_CUSTOM_NETWORK",
+ "REJECT_REQUEST_SWITCH_CUSTOM_NETWORK",
+ "CONNECT_DAPP_RES",
+ "CONNECT_ACCOUNT_RES",
+ "REJECT_PREAUTHORIZATION",
+ "REQUEST_DECLARE_CONTRACT_RES",
+ "DECLARE_CONTRACT_ACTION_FAILED",
+ "DECLARE_CONTRACT_ACTION_SUBMITTED",
+]
+
+export const safeIfPreauthorizedMessages: MessageType["type"][] = [
+ "EXECUTE_TRANSACTION",
+ "SIGN_MESSAGE",
+ "REQUEST_TOKEN",
+ "REQUEST_ADD_CUSTOM_NETWORK",
+ "REQUEST_SWITCH_CUSTOM_NETWORK",
+ "REQUEST_DECLARE_CONTRACT",
+]
diff --git a/packages/extension/src/background/messagingDocs.md b/packages/extension/src/background/messagingDocs.md
index 676c651a9..31b7841ed 100644
--- a/packages/extension/src/background/messagingDocs.md
+++ b/packages/extension/src/background/messagingDocs.md
@@ -1,147 +1,70 @@
# Messaging
-Direct communication from frontend and background service is not allowed and it's done through `messages`.
+Direct communication from frontend and background service is not allowed and it's done through `messages`. We use [trpc](https://trpc.io/) alongside the [trpc-chrome](https://github.com/jlalmes/trpc-chrome) dependency to make these messages type-safe and keep the architecture clean and modular.
-The React component will call a function defined into a `service` first (located in `/src/ui/services`)
+## Mutations / writes
```js
const Component = () => {
-useEffect(() => {
- serviceFunction(argument)
-}, [])
-
-const onClick = () => {
- serviceFunction2(argument)
-}
+ const onClick = () => {
+ someService.doSomethingInTheBackground(argument)
+ }
-return
+ return
}
```
-The service called will then send the messages to communicate with the background service with the use of `sendMessage`.
-
-The service will then need to wait for the response (that's another message) using `waitForMessage`.
+We usually recommend using trpc `mutations` to cover this message flow.
```js
-const serviceFunction = async () => {
- sendMessage({type: "A_MESSAGE", data: { param: "param"}})
+import { messageClient } from "...."
+class SomeService implements ISomeService {
+ async doSomethingInTheBackground() {
try {
- await Promise.race([
- waitForMessage("A_MESSAGE_RES"), // background OK response
- waitForMessage("A_MESSAGE_REJ").then(() => { // background FAILURE response
- throw new Error("Rejected")
- }),
- ])
- } catch {
- throw Error("Could not declare contract")
+ messageClient.someBackgroundService.doSomething.mutate()
+ } catch (e) {
+ throw Error(`doSomethingInTheBackground failed with: ${e}`)
+ }
}
}
```
-These messages will be then managed by messaging handlers. When a new handler is added, in order to be handled, it need to be added to the `messageStream` in `/src/background/index.ts`
+These messages will be then managed by the correct router in the background. `router.ts` is where the different routers are implemented. We have one router for each service and we try to inject dependencies into trpc rather than import them in the `procedures`.
-When the service send a message, the handler will will check the `type` of the message and, if there is a match, it will execute the related code.
+Once routed, this message lands with its payload in the correct procedure, in this case we would have `doSomethingProcedure` that is mapped to `doSomething`.
-- Handlers are located in `/src/background/aMessaging.ts`.
-- Message types: `/src/shared/messages`
-- Action queue types `/src/shared/actionQueue`
+#### Make sure to preserve separation of concerns between the UI and the background. Utils that can be used by both should be in shared, everything else should live in the right folder.
-```js
-export const handleAMessaging: HandleMessage = async ({
- msg,
- background,
- respond,
-}) => {
- const { actionQueue, wallet } = background
- const { type } = msg
+Each procedure then acts in a similar way to a middleware, doing the following:
- switch (type) {
- case "A_MESSAGE": {
- const { data } = msg
- const { address, networkId, classHash, contract } = data
- await wallet.selectAccount({ address, networkId })
+- Input and output validation with zod
+- Dependency injection
+- Service calls
- /* This is not mandatory, depends if an action is needed */
- const action = await actionQueue.push({
- type: "ACTION_TYPE",
- payload: {},
- })
+Use background services in the procedures rather than having the logic there directly
+```js
+export const doSomethingProcedure = extensionOnlyProcedure
+ .input(myInputValidationSchema)
+ .ouput(myOutputValidationSchema)
+ .mutation(
+ async ({
+ input,
+ ctx: {
+ // these are the injected services if necessary
+ services
+ }
+ }) => {
try {
- await doSomething()
- } catch {
- return respond({
- type: "A_MESSAGE_REJ",
- data: {
- actionHash: action.meta.hash,
- },
- })
+ const result = await backgroundService.doSomething(input)
+ return result
+ } catch (e) {
+ throw new Error(
+ `Something failed in doSomethingProcedure: ${e}`
+ )
}
-
- return respond({
- type: "A_MESSAGE_RES",
- data: {
- actionHash: action.meta.hash,
- },
- })
}
- }
-
- throw new UnhandledMessage()
-}
+ )
```
-Actions messages are handled in the same way and defined in `/src/background/actionHandlers.ts`.
-
-Action message types are defined into `/src/shared/actionQueue/types.ts`
-
-### messaging flows
-
-Basic messaging flow
-
-```mermaid
-
-sequenceDiagram
-Frontend->> Service: request method
-
-Service ->> Background: sendMessage({ type, payload})
-Service ->> Service: Promise.race([ waitForMessage(msgOK), waitForMessage(msgFail) ])
-
-Background ->> Background: execute
-
-Background ->> Service: respond({type, payload})
-
-Service ->> Service: resolve Promise.race
-
-Service ->> Frontend: send data (or empty)
-
-
-```
-
-action queue types `/src/shared/actionQueue`
-
-With action queue flow
-
-```mermaid
-
-sequenceDiagram
-Frontend->> Service: request method
-
-Service ->> Background: sendMessage({ type, payload})
-Service ->> Service: Promise.race([ waitForMessage(msgOK), waitForMessage(msgFail) ])
-
-Background ->> Background: push actionQueue
-Note right of Background: action is saved into store and used by AppRoute
-
-Background ->> Service: respond({type, data: actionHash})
-Service ->> Service: resolve Promise.race
-Service ->> Frontend: send data (or empty)
-
-AppRoute ->> ActionScreen: display actionHash screen (based on type)
-
-ActionScreen ->> ActionScreen: user perform action
-ActionScreen ->> Background-ActionHandlers: perform action with OK/KO result
-Background-ActionHandlers ->> ActionScreen: updateApp state or reroute based on result
-
-
-```
+#### ⚠️ Important: the messaging system should be used primarily to mutate data. When reading reading data, the general rule of thumb is to first rely on the shared storage between background and UI, and only if necessary rely on trpc `queries`.
diff --git a/packages/extension/src/background/migrations/index.ts b/packages/extension/src/background/migrations/index.ts
new file mode 100644
index 000000000..175aafcad
--- /dev/null
+++ b/packages/extension/src/background/migrations/index.ts
@@ -0,0 +1,63 @@
+import { walletSessionServiceEmitter } from "../walletSingleton"
+import { Locked } from "../wallet/session/interface"
+import { runRemoveTestnet2Migration } from "./network/removeTestnet2"
+import { KeyValueStorage } from "../../shared/storage"
+import { runRemoveTestnet2Accounts, runV581Migration } from "./wallet"
+import { runV59TokenMigration } from "./token/v5.9"
+
+enum WalletMigrations {
+ v581 = "wallet:v581",
+ removeTestnet2Accounts = "wallet:removeTestnet2Accounts",
+}
+
+enum NetworkMigrations {
+ removeTestnet2 = "network:removeTestnet2",
+}
+
+enum TokenMigrations {
+ v59 = "token:v59",
+}
+
+const migrationsStore = new KeyValueStorage(
+ {
+ [WalletMigrations.v581]: false,
+ [WalletMigrations.removeTestnet2Accounts]: false,
+ [NetworkMigrations.removeTestnet2]: false,
+ [TokenMigrations.v59]: false,
+ },
+ "core:migrations",
+)
+
+export const migrationListener = walletSessionServiceEmitter.on(
+ Locked,
+ async (locked) => {
+ 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 removeTestnet2Accounts = await migrationsStore.get(
+ WalletMigrations.removeTestnet2Accounts,
+ )
+ const v59Migration = await migrationsStore.get(TokenMigrations.v59)
+ if (!v581Migration) {
+ await runV581Migration()
+ await migrationsStore.set(WalletMigrations.v581, true)
+ }
+ if (!removeTestnet2Accounts) {
+ await runRemoveTestnet2Accounts()
+ await migrationsStore.set(WalletMigrations.removeTestnet2Accounts, true)
+ }
+ if (!networkMigration) {
+ await runRemoveTestnet2Migration()
+ await migrationsStore.set(NetworkMigrations.removeTestnet2, true)
+ }
+
+ if (!v59Migration) {
+ await runV59TokenMigration()
+ await migrationsStore.set(TokenMigrations.v59, true)
+ }
+ }
+ },
+)
diff --git a/packages/extension/src/background/migrations/network/removeTestnet2.ts b/packages/extension/src/background/migrations/network/removeTestnet2.ts
new file mode 100644
index 000000000..ccf2660d2
--- /dev/null
+++ b/packages/extension/src/background/migrations/network/removeTestnet2.ts
@@ -0,0 +1,5 @@
+import { networkService } from "../../../shared/network/service"
+
+export async function runRemoveTestnet2Migration() {
+ await networkService.restoreDefaults()
+}
diff --git a/packages/extension/src/background/migrations/token/v5.9.ts b/packages/extension/src/background/migrations/token/v5.9.ts
new file mode 100644
index 000000000..2c5caf415
--- /dev/null
+++ b/packages/extension/src/background/migrations/token/v5.9.ts
@@ -0,0 +1,37 @@
+import { isEmpty } from "lodash-es"
+import { tokenService } from "../../../shared/token/__new/service"
+import { Token } from "../../../shared/token/__new/types/token.model"
+import {
+ equalToken,
+ parsedDefaultTokens,
+} from "../../../shared/token/__new/utils"
+import { tokenStore } from "../../../shared/token/__deprecated/storage"
+
+export async function runV59TokenMigration() {
+ const oldTokens = await tokenStore.get()
+
+ // Only migrate tokens that are not default tokens
+ const tokensToMigrate: Token[] = oldTokens
+ .filter(
+ (token) =>
+ !parsedDefaultTokens.some((defaultToken) =>
+ equalToken(token, defaultToken),
+ ),
+ )
+ .map((token) => ({
+ name: token.name,
+ address: token.address,
+ networkId: token.networkId,
+ symbol: token.symbol,
+ decimals: token.decimals,
+ custom: true,
+ iconUrl: token.image,
+ showAlways: token.showAlways,
+ }))
+
+ if (isEmpty(tokensToMigrate)) {
+ return
+ }
+
+ return await tokenService.addToken(tokensToMigrate)
+}
diff --git a/packages/extension/src/background/migrations/wallet/index.ts b/packages/extension/src/background/migrations/wallet/index.ts
new file mode 100644
index 000000000..92635abd6
--- /dev/null
+++ b/packages/extension/src/background/migrations/wallet/index.ts
@@ -0,0 +1,17 @@
+import { cryptoStarknetService } from "../../walletSingleton"
+import { accountRepo } from "../../../shared/account/store"
+import { walletStore } from "../../../shared/wallet/walletStore"
+import { determineMigrationNeededV581, migrateAccountsV581 } from "./v5.8.1"
+import { migrateTestnet2Accounts } from "./testnet2Accounts"
+
+export async function runV581Migration() {
+ const accountsToMigrate = await determineMigrationNeededV581(
+ cryptoStarknetService,
+ accountRepo,
+ )
+ await migrateAccountsV581(accountsToMigrate, accountRepo, walletStore)
+}
+
+export async function runRemoveTestnet2Accounts() {
+ await migrateTestnet2Accounts(accountRepo, walletStore)
+}
diff --git a/packages/extension/src/background/migrations/wallet/testnet2Accounts.ts b/packages/extension/src/background/migrations/wallet/testnet2Accounts.ts
new file mode 100644
index 000000000..949fa0015
--- /dev/null
+++ b/packages/extension/src/background/migrations/wallet/testnet2Accounts.ts
@@ -0,0 +1,39 @@
+import {
+ IObjectStore,
+ IRepository,
+} from "../../../shared/storage/__new/interface"
+import { WalletAccount } from "../../../shared/wallet.model"
+import { WalletStorageProps } from "../../wallet/account/shared.service"
+
+export async function getTestnet2Accounts(
+ walletStore: IRepository,
+): Promise {
+ const accounts = await walletStore.get()
+
+ return accounts.filter((account) => account.networkId === "goerli-alpha-2")
+}
+
+export async function migrateTestnet2Accounts(
+ walletStore: IRepository,
+ store: IObjectStore,
+) {
+ const testnet2Accounts = await getTestnet2Accounts(walletStore)
+ // update accounts to hide the testnet2 ones
+ await walletStore.remove(testnet2Accounts)
+
+ // if selected account is in list of testnet2 accounts, select another one
+ const { selected } = await store.get()
+ if (selected?.networkId === "goerli-alpha-2") {
+ const [firstValidAccount] = await walletStore.get(
+ (account) => !account.hidden && account.networkId !== "goerli-alpha-2",
+ )
+ await store.set({
+ selected: firstValidAccount
+ ? {
+ address: firstValidAccount.address,
+ networkId: firstValidAccount.networkId,
+ }
+ : null,
+ })
+ }
+}
diff --git a/packages/extension/src/background/migrations/wallet/v5.8.1.test.ts b/packages/extension/src/background/migrations/wallet/v5.8.1.test.ts
new file mode 100644
index 000000000..f20f6c95a
--- /dev/null
+++ b/packages/extension/src/background/migrations/wallet/v5.8.1.test.ts
@@ -0,0 +1,73 @@
+import { describe, expect, it } from "vitest"
+import { determineMigrationNeededV581 } from "./v5.8.1"
+import {
+ cryptoStarknetServiceMock,
+ getWalletStoreMock,
+} from "../../wallet/test.utils"
+
+describe("v5.8.1", () => {
+ it("should detect falsey accounts for migration", async () => {
+ const result = await determineMigrationNeededV581(
+ cryptoStarknetServiceMock,
+ getWalletStoreMock({
+ get: vi.fn(() =>
+ Promise.resolve([
+ {
+ address:
+ "0x27391f566beb47c08442bf54748855f8ada5a645b2bc7eb2de1bcfc3849a0e5",
+ name: "Account 1",
+ needsDeploy: true,
+ networkId: "mainnet-alpha",
+ signer: {
+ derivationPath: "m/44'/9004'/0'/0/0",
+ type: "local_secret",
+ },
+ type: "standard",
+ },
+ {
+ address:
+ "0x27391f566beb47c08442bf54748855f8ada5a645b2bc7eb2de1bcfc3849a0e5",
+ name: "Account 1",
+ needsDeploy: false,
+ networkId: "goerli-alpha",
+ signer: {
+ derivationPath: "m/44'/9004'/0'/0/0",
+ type: "local_secret",
+ },
+ type: "standard",
+ },
+ {
+ address:
+ "0x522623bbfd34bada76d76f2d31ad294d2767a5890bf76d7aa0b8272df367ee5",
+ name: "Account 2",
+ needsDeploy: true,
+ networkId: "goerli-alpha",
+ signer: {
+ derivationPath: "m/44'/9004'/0'/0/1",
+ type: "local_secret",
+ },
+ type: "standard",
+ },
+ ]),
+ ),
+ }),
+ )
+
+ // this is the only falsey account, as it was created with v5.8.1
+ expect(result).toMatchInlineSnapshot(`
+ [
+ {
+ "address": "0x522623bbfd34bada76d76f2d31ad294d2767a5890bf76d7aa0b8272df367ee5",
+ "name": "Account 2",
+ "needsDeploy": true,
+ "networkId": "goerli-alpha",
+ "signer": {
+ "derivationPath": "m/44'/9004'/0'/0/1",
+ "type": "local_secret",
+ },
+ "type": "standard",
+ },
+ ]
+ `)
+ })
+})
diff --git a/packages/extension/src/background/migrations/wallet/v5.8.1.ts b/packages/extension/src/background/migrations/wallet/v5.8.1.ts
new file mode 100644
index 000000000..1716b1caa
--- /dev/null
+++ b/packages/extension/src/background/migrations/wallet/v5.8.1.ts
@@ -0,0 +1,76 @@
+import { isEqualAddress } from "@argent/shared"
+import { STANDARD_CAIRO_0_ACCOUNT_CLASS_HASH } from "../../../shared/network/constants"
+import {
+ IObjectStore,
+ IRepository,
+} from "../../../shared/storage/__new/interface"
+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"
+
+export async function determineMigrationNeededV581(
+ cryptoStarknetService: WalletCryptoStarknetService,
+ walletStore: IRepository,
+): Promise {
+ const accounts = await walletStore.get()
+
+ const accountNeedsToMigrate: [WalletAccount, boolean][] = await Promise.all(
+ accounts.map(async (account) => {
+ const { pubKey } = await cryptoStarknetService.getKeyPairByDerivationPath(
+ account.signer.derivationPath,
+ )
+ const falseyAccountAddress =
+ cryptoStarknetService.getCairo1AccountContractAddress(
+ STANDARD_CAIRO_0_ACCOUNT_CLASS_HASH,
+ pubKey,
+ )
+
+ return [account, isEqualAddress(falseyAccountAddress, account.address)]
+ }),
+ )
+
+ const accountsToMigrate = accountNeedsToMigrate
+ .filter(([, needsMigrate]) => needsMigrate)
+ .map(([account]) => account)
+
+ return accountsToMigrate
+}
+
+export async function migrateAccountsV581(
+ falseyAccounts: WalletAccount[],
+ walletStore: IRepository,
+ store: IObjectStore,
+) {
+ const isFalseyAccount = (account?: BaseWalletAccount) =>
+ falseyAccounts.some((f) => accountsEqual(f, account))
+
+ // update accounts to hide the falsey ones
+ await walletStore.upsert((account) =>
+ account.map((x) =>
+ isFalseyAccount(x)
+ ? {
+ ...x,
+ hidden: true,
+ showBlockingDeprecated: true,
+ }
+ : x,
+ ),
+ )
+
+ // if selected account is in list of falsey accounts, select another one
+ const { selected } = await store.get()
+ if (isFalseyAccount(selected ?? undefined)) {
+ const [firstValidAccount] = await walletStore.get(
+ (account) => !account.hidden,
+ )
+ await store.set({
+ selected: firstValidAccount
+ ? {
+ address: firstValidAccount.address,
+ networkId: firstValidAccount.networkId,
+ }
+ : null,
+ })
+ }
+}
diff --git a/packages/extension/src/background/multisigDeployAction.ts b/packages/extension/src/background/multisig/multisigDeployAction.ts
similarity index 71%
rename from packages/extension/src/background/multisigDeployAction.ts
rename to packages/extension/src/background/multisig/multisigDeployAction.ts
index 1765722ae..316b29757 100644
--- a/packages/extension/src/background/multisigDeployAction.ts
+++ b/packages/extension/src/background/multisig/multisigDeployAction.ts
@@ -1,11 +1,11 @@
-import { number } from "starknet"
+import { num } from "starknet"
-import { ExtQueueItem } from "../shared/actionQueue/types"
-import { BaseWalletAccount } from "../shared/wallet.model"
-import { BackgroundService } from "./background"
-import { addTransaction } from "./transactions/store"
-import { checkTransactionHash } from "./transactions/transactionExecution"
-import { argentMaxFee } from "./utils/argentMaxFee"
+import { ExtQueueItem } from "../../shared/actionQueue/types"
+import { BaseWalletAccount } from "../../shared/wallet.model"
+import { addTransaction } from "../transactions/store"
+import { checkTransactionHash } from "../transactions/transactionExecution"
+import { argentMaxFee } from "../utils/argentMaxFee"
+import { Wallet } from "../wallet"
type DeployMultisigAction = ExtQueueItem<{
type: "DEPLOY_MULTISIG_ACTION"
@@ -14,7 +14,7 @@ type DeployMultisigAction = ExtQueueItem<{
export const multisigDeployAction = async (
{ payload: baseAccount }: DeployMultisigAction,
- { wallet }: BackgroundService,
+ wallet: Wallet,
) => {
if (!(await wallet.isSessionOpen())) {
throw Error("you need an open session")
@@ -36,7 +36,7 @@ export const multisigDeployAction = async (
maxFee = argentMaxFee(suggestedMaxFee)
} catch (error) {
- const fallbackPrice = number.toBN(10e14)
+ const fallbackPrice = num.toBigInt(10e14)
maxFee = argentMaxFee(fallbackPrice)
}
diff --git a/packages/extension/src/background/multisigMessaging.ts b/packages/extension/src/background/multisigMessaging.ts
deleted file mode 100644
index 78ebd19aa..000000000
--- a/packages/extension/src/background/multisigMessaging.ts
+++ /dev/null
@@ -1,260 +0,0 @@
-import { utils } from "ethers"
-import { stark } from "starknet"
-
-import { tryToMintFeeToken } from "../shared/devnet/mintFeeToken"
-import { MultisigMessage } from "../shared/messages/MultisigMessage"
-import { MultisigAccount } from "../shared/multisig/account"
-import { getMultisigAccounts } from "../shared/multisig/utils/baseMultisig"
-import { deployMultisigAction } from "./accountDeploy"
-import { sendMessageToUi } from "./activeTabs"
-import { analytics } from "./analytics"
-import { HandleMessage, UnhandledMessage } from "./background"
-
-export const handleMultisigMessage: HandleMessage = async ({
- msg,
- background: { wallet, actionQueue },
-}) => {
- switch (msg.type) {
- case "NEW_MULTISIG_ACCOUNT": {
- if (!(await wallet.isSessionOpen())) {
- throw Error("you need an open session")
- }
-
- const { networkId, signers, threshold, creator, publicKey } = msg.data
- try {
- const account = await wallet.newAccount(networkId, "multisig", {
- signers,
- threshold,
- creator,
- publicKey,
- })
- tryToMintFeeToken(account)
-
- analytics.track("createAccount", {
- status: "success",
- networkId,
- type: "multisig",
- })
-
- const accounts = await getMultisigAccounts()
-
- return sendMessageToUi({
- type: "NEW_MULTISIG_ACCOUNT_RES",
- data: {
- account,
- accounts,
- },
- })
- } catch (exception) {
- const error = `${exception}`
-
- analytics.track("createAccount", {
- status: "failure",
- networkId: networkId,
- type: "multisig",
- errorMessage: error,
- })
-
- return sendMessageToUi({
- type: "NEW_MULTISIG_ACCOUNT_REJ",
- data: { error },
- })
- }
- }
-
- case "DEPLOY_MULTISIG": {
- try {
- await deployMultisigAction({
- account: msg.data,
- actionQueue,
- })
-
- return sendMessageToUi({ type: "DEPLOY_MULTISIG_RES" })
- } catch (e) {
- return sendMessageToUi({ type: "DEPLOY_MULTISIG_REJ" })
- }
- }
-
- case "NEW_PENDING_MULTISIG": {
- if (!(await wallet.isSessionOpen())) {
- throw Error("you need an open session")
- }
-
- const { networkId } = msg.data
- try {
- const pendingMultisig = await wallet.newPendingMultisig(networkId)
-
- // TODO: Add tracking
- // analytics.track("createAccount", {
- // status: "success",
- // networkId,
- // type: "multisig",
- // })
-
- return sendMessageToUi({
- type: "NEW_PENDING_MULTISIG_RES",
- data: pendingMultisig,
- })
- } catch (exception) {
- const error = `${exception}`
-
- // TODO: Add tracking
-
- // analytics.track("createAccount", {
- // status: "failure",
- // networkId: networkId,
- // type: "multisig",
- // errorMessage: error,
- // })
-
- return sendMessageToUi({
- type: "NEW_PENDING_MULTISIG_REJ",
- data: { error },
- })
- }
- }
-
- case "ADD_MULTISIG_OWNERS": {
- try {
- const { address, signersToAdd, newThreshold } = msg.data
-
- const signersPayload = {
- entrypoint: "addSigners",
- calldata: stark.compileCalldata({
- new_threshold: newThreshold.toString(),
- signers_to_add: signersToAdd.map((signer) =>
- utils.hexlify(utils.base58.decode(signer)),
- ),
- }),
- contractAddress: address,
- }
-
- await actionQueue.push({
- type: "TRANSACTION",
- payload: {
- transactions: signersPayload,
- meta: {
- title: "Add multisig owners",
- type: "MULTISIG_ADD_SIGNERS",
- },
- },
- })
-
- return sendMessageToUi({
- type: "ADD_MULTISIG_OWNERS_RES",
- })
- } catch (e) {
- return sendMessageToUi({
- type: "ADD_MULTISIG_OWNERS_REJ",
- data: { error: `${e}` },
- })
- }
- }
-
- case "REMOVE_MULTISIG_OWNER": {
- try {
- const { address, signerToRemove, newThreshold } = msg.data
-
- const signersToRemove = [
- utils.hexlify(utils.base58.decode(signerToRemove)),
- ]
-
- const signersPayload = {
- entrypoint: "removeSigners",
- calldata: stark.compileCalldata({
- new_threshold: newThreshold.toString(),
- signers_to_remove: signersToRemove,
- }),
- contractAddress: address,
- }
-
- await actionQueue.push({
- type: "TRANSACTION",
- payload: {
- transactions: signersPayload,
- meta: {
- title: "Remove multisig owner",
- type: "MULTISIG_REMOVE_SIGNER",
- },
- },
- })
-
- return sendMessageToUi({
- type: "REMOVE_MULTISIG_OWNER_RES",
- })
- } catch (e) {
- return sendMessageToUi({
- type: "REMOVE_MULTISIG_OWNER_REJ",
- data: { error: `${e}` },
- })
- }
- }
- case "UPDATE_MULTISIG_THRESHOLD": {
- try {
- const { address, newThreshold } = msg.data
-
- const thresholdPayload = {
- entrypoint: "changeThreshold",
- calldata: [newThreshold.toString()],
- contractAddress: address,
- }
-
- await actionQueue.push({
- type: "TRANSACTION",
- payload: {
- transactions: thresholdPayload,
- meta: {
- title: "Set confirmations threshold",
- type: "MULTISIG_UPDATE_THRESHOLD",
- },
- },
- })
- return sendMessageToUi({
- type: "UPDATE_MULTISIG_THRESHOLD_RES",
- })
- } catch (e) {
- return sendMessageToUi({
- type: "UPDATE_MULTISIG_THRESHOLD_REJ",
- data: { error: `${e}` },
- })
- }
- }
-
- case "ADD_MULTISIG_TRANSACTION_SIGNATURE": {
- try {
- const { requestId } = msg.data
-
- const selectedAccount = await wallet.getSelectedAccount()
-
- if (!selectedAccount) {
- throw Error("No account selected")
- }
-
- const multisigStarknetAccount = await wallet.getStarknetAccount(
- selectedAccount,
- )
-
- if (!MultisigAccount.isMultisig(multisigStarknetAccount)) {
- throw Error("Selected account is not a multisig account")
- }
-
- const { transaction_hash } =
- await multisigStarknetAccount.addRequestSignature(requestId)
-
- return sendMessageToUi({
- type: "ADD_MULTISIG_TRANSACTION_SIGNATURE_RES",
- data: {
- txHash: transaction_hash,
- },
- })
- } catch (e) {
- return sendMessageToUi({
- type: "ADD_MULTISIG_TRANSACTION_SIGNATURE_REJ",
- data: { error: `${e}` },
- })
- }
- }
- }
-
- throw new UnhandledMessage()
-}
diff --git a/packages/extension/src/background/network/network.service.ts b/packages/extension/src/background/network/network.service.ts
deleted file mode 100644
index 536b254f0..000000000
--- a/packages/extension/src/background/network/network.service.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { uniqWith } from "lodash-es"
-
-import { Network, defaultNetworks } from "../../shared/network"
-import { allNetworksStore, equalNetwork } from "../../shared/network/storage"
-import { getNetworkStatuses } from "./networkStatus.worker"
-
-export interface NetworkService {
- updateStatuses: () => Promise
- loadNetworks: () => Promise
-}
-
-export const networkService: NetworkService = {
- async loadNetworks() {
- const allNetworks = uniqWith(
- [...(await allNetworksStore.get()), ...defaultNetworks],
- equalNetwork,
- )
- return allNetworks
- },
- async updateStatuses() {
- const networks = await this.loadNetworks()
- const networkStatuses = await getNetworkStatuses(networks)
- const networkWithUpdatedStatuses = networks.map((network) => {
- return {
- ...network,
- status: networkStatuses[network.id] ?? "unknown",
- }
- })
- await allNetworksStore.push(networkWithUpdatedStatuses)
- },
-}
diff --git a/packages/extension/src/background/network/networkStatus.worker.ts b/packages/extension/src/background/network/networkStatus.worker.ts
deleted file mode 100644
index 4ebd51888..000000000
--- a/packages/extension/src/background/network/networkStatus.worker.ts
+++ /dev/null
@@ -1,143 +0,0 @@
-import urljoin from "url-join"
-
-import { Network, NetworkStatus } from "../../shared/network"
-import { KeyValueStorage } from "../../shared/storage"
-import { createStaleWhileRevalidateCache } from "../swr"
-import { fetchWithTimeout } from "../utils/fetchWithTimeout"
-
-type SwrCacheKey = string
-
-const swrStorage = new KeyValueStorage>(
- {},
- "cache:swr",
-)
-
-// see: https://github.com/jperasmus/stale-while-revalidate-cache#configuration
-const swr = createStaleWhileRevalidateCache({
- storage: swrStorage, // can be any object with getItem and setItem methods
- minTimeToStale: 60e3, // 1 minute
- maxTimeToLive: 30 * 60e3, // 30 minutes
-})
-
-const getChecklyNetworkStatus = async (
- network: Network,
-): Promise =>
- swr(`${network.id}-checkly-network-status`, async () => {
- if (network.id !== "goerli-alpha") {
- // checkly is only available on goerli-alpha
- return "unknown"
- }
- try {
- const response = await fetchWithTimeout(
- `https://starknet-status.vercel.app/api/simple-status`,
- { timeout: 8000, method: "GET" },
- )
- const { status }: { status: NetworkStatus } = await response.json()
-
- return (response.status === 200 && status) || "error"
- } catch {
- return "error"
- }
- })
-
-function determineStatusByRequestStatusCode(statusCode: number): NetworkStatus {
- if (statusCode === 200) {
- return "ok"
- }
- if (statusCode === 429) {
- return "degraded"
- }
- return "error"
-}
-
-const getFeederGatewayNetworkStatus = async (
- network: Network,
-): Promise =>
- swr(`${network.id}-feeder-gateway-network-status`, async () => {
- // fetch https://alpha-mainnet.starknet.io/feeder_gateway/is_alive and check the response
- try {
- const response = await fetchWithTimeout(
- urljoin(network.baseUrl, "feeder_gateway/is_alive"),
- { timeout: 5000, method: "GET" },
- )
-
- return determineStatusByRequestStatusCode(response.status)
- } catch {
- return "error"
- }
- })
-
-const getGatewayNetworkStatus = async (
- network: Network,
-): Promise =>
- swr(`${network.id}-gateway-network-status`, async () => {
- // fetch https://alpha-mainnet.starknet.io/gateway/is_alive and check the response
- try {
- const response = await fetchWithTimeout(
- urljoin(network.baseUrl, "gateway/is_alive"),
- { timeout: 5000, method: "GET" },
- )
-
- return determineStatusByRequestStatusCode(response.status)
- } catch {
- return "error"
- }
- })
-
-export const getDevnetStatus = async (
- network: Network,
-): Promise =>
- swr(`${network.id}-devnet-network-status`, async () => {
- // fetch http://localhost:5050/is_alive and check the response
- try {
- const response = await fetchWithTimeout(
- urljoin(network.baseUrl, "is_alive"),
- { timeout: 5000, method: "GET" },
- )
-
- const status = determineStatusByRequestStatusCode(response.status)
-
- return status
- } catch {
- return "error"
- }
- })
-
-export const getNetworkStatus = async (
- network: Network,
-): Promise => {
- // return ok if all of the above are ok
- // return degraded if any of the above are degraded
- // return error if any of the above are error
- // return unknown if all of the above are unknown
-
- const isDevnet = await getDevnetStatus(network)
- if (isDevnet === "ok") {
- return isDevnet
- }
-
- const statuses = await Promise.all([
- getChecklyNetworkStatus(network),
- getFeederGatewayNetworkStatus(network),
- getGatewayNetworkStatus(network),
- ])
-
- const degraded = statuses.some((s) => s === "degraded")
- const error = statuses.some((s) => s === "error")
- const unknown = statuses.every((s) => s === "unknown")
-
- return unknown ? "unknown" : error ? "error" : degraded ? "degraded" : "ok"
-}
-
-export const getNetworkStatuses = async (
- networks: Network[],
-): Promise>> => {
- const statuses = await Promise.all(
- networks.map((network) => getNetworkStatus(network)),
- )
-
- return networks.reduce(
- (acc, network, i) => ({ ...acc, [network.id]: statuses[i] }),
- {},
- )
-}
diff --git a/packages/extension/src/background/networkMessaging.ts b/packages/extension/src/background/networkMessaging.ts
index 7038aa16a..1238e2c77 100644
--- a/packages/extension/src/background/networkMessaging.ts
+++ b/packages/extension/src/background/networkMessaging.ts
@@ -1,25 +1,48 @@
-import { number, shortString } from "starknet"
+import { num, shortString } from "starknet"
import { NetworkMessage } from "../shared/messages/NetworkMessage"
-import { getNetworkByChainId } from "../shared/network"
+import { networkService } from "../shared/network/service"
import { UnhandledMessage } from "./background"
import { HandleMessage } from "./background"
export const handleNetworkMessage: HandleMessage = async ({
msg,
- background: { actionQueue },
+ origin,
+ background: { actionService },
respond,
}) => {
switch (msg.type) {
- case "REQUEST_SWITCH_CUSTOM_NETWORK": {
- const { chainId } = msg.data
+ case "REQUEST_ADD_CUSTOM_NETWORK": {
+ const exists = await networkService.getByChainId(msg.data.chainId)
- const isHexChainId = number.isHex(chainId)
+ if (exists) {
+ return respond({
+ type: "REQUEST_ADD_CUSTOM_NETWORK_REJ",
+ data: {
+ error: `Network with chainId ${msg.data.chainId} already exists`,
+ },
+ })
+ }
+
+ const { meta } = await actionService.add({
+ type: "REQUEST_ADD_CUSTOM_NETWORK",
+ payload: msg.data,
+ })
+
+ return respond({
+ type: "REQUEST_ADD_CUSTOM_NETWORK_RES",
+ data: {
+ actionHash: meta.hash,
+ },
+ })
+ }
- const decodedChainId = shortString.decodeShortString(chainId)
+ case "REQUEST_SWITCH_CUSTOM_NETWORK": {
+ const { chainId } = msg.data
+ const isHexChainId = num.isHex(chainId)
- const network = await getNetworkByChainId(
- isHexChainId ? decodedChainId : chainId,
+ const network = await networkService.getByChainId(
+ isHexChainId ? shortString.decodeShortString(chainId) : chainId,
)
if (!network) {
@@ -31,10 +54,15 @@ export const handleNetworkMessage: HandleMessage = async ({
})
}
- const { meta } = await actionQueue.push({
- type: "REQUEST_SWITCH_CUSTOM_NETWORK",
- payload: network,
- })
+ const { meta } = await actionService.add(
+ {
+ type: "REQUEST_SWITCH_CUSTOM_NETWORK",
+ payload: network,
+ },
+ {
+ origin,
+ },
+ )
return respond({
type: "REQUEST_SWITCH_CUSTOM_NETWORK_RES",
@@ -46,7 +74,7 @@ export const handleNetworkMessage: HandleMessage = async ({
case "REJECT_REQUEST_ADD_CUSTOM_NETWORK":
case "REJECT_REQUEST_SWITCH_CUSTOM_NETWORK": {
- return await actionQueue.remove(msg.data.actionHash)
+ return await actionService.remove(msg.data.actionHash)
}
}
diff --git a/packages/extension/src/background/nonce.ts b/packages/extension/src/background/nonce.ts
index 8470c984d..d5880e065 100644
--- a/packages/extension/src/background/nonce.ts
+++ b/packages/extension/src/background/nonce.ts
@@ -1,9 +1,10 @@
-import { number } from "starknet"
+import { num, Account } from "starknet"
import { KeyValueStorage } from "../shared/storage"
import { BaseWalletAccount, WalletAccount } from "../shared/wallet.model"
import { getAccountIdentifier } from "../shared/wallet.service"
-import { Wallet } from "./wallet"
+
+import { Account as AccountV4__deprecated } from "starknet4-deprecated"
const nonceStore = new KeyValueStorage>(
{},
@@ -15,31 +16,30 @@ const nonceStore = new KeyValueStorage>(
export async function getNonce(
account: WalletAccount,
- wallet: Wallet,
+ starknetAccount: Account | AccountV4__deprecated,
): Promise {
- const starknetAccount = await wallet.getStarknetAccount(account)
const storageAddress = getAccountIdentifier(account)
const result = await starknetAccount.getNonce()
- const nonceBn = number.toBN(result)
+ const nonceBn = num.toBigInt(result)
const storedNonce = await nonceStore.get(storageAddress)
if (account.type === "multisig") {
// If the account is a multisig, we don't want to store the nonce
- return number.toHex(nonceBn)
+ return num.toHex(nonceBn)
}
// If there's no nonce stored or the fetched nonce is bigger than the stored one, store the fetched nonce
- if (!storedNonce || nonceBn.gt(number.toBN(storedNonce))) {
- await nonceStore.set(storageAddress, number.toHex(nonceBn))
+ if (!storedNonce || nonceBn > num.toBigInt(storedNonce)) {
+ await nonceStore.set(storageAddress, num.toHex(nonceBn))
}
// If the stored nonce is greater than the fetched nonce, use the stored nonce
- if (storedNonce && number.toBN(storedNonce).gt(nonceBn)) {
- return number.toHex(number.toBN(storedNonce))
+ if (storedNonce && num.toBigInt(storedNonce) > nonceBn) {
+ return num.toHex(storedNonce)
}
// else return the fetched nonce
- return number.toHex(nonceBn)
+ return num.toHex(nonceBn)
}
export async function increaseStoredNonce(
@@ -48,9 +48,9 @@ export async function increaseStoredNonce(
const storageAddress = getAccountIdentifier(account)
const storedNonce = await nonceStore.get(storageAddress)
if (storedNonce) {
- nonceStore.set(
+ await nonceStore.set(
storageAddress,
- number.toHex(number.toBN(storedNonce).add(number.toBN(1))),
+ num.toHex(num.toBigInt(storedNonce) + num.toBigInt(1)),
)
}
}
diff --git a/packages/extension/src/background/onboarding.ts b/packages/extension/src/background/onboarding.ts
deleted file mode 100644
index b0ffee015..000000000
--- a/packages/extension/src/background/onboarding.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { onboardingWorker } from "./__new/services/onboarding"
-
-/** TODO: refactor: remove this facade */
-export function initOnboarding() {
- return {
- onboardingWorker,
- }
-}
diff --git a/packages/extension/src/background/preAuthorizationMessaging.ts b/packages/extension/src/background/preAuthorizationMessaging.ts
index eb09adfc8..c671ed548 100644
--- a/packages/extension/src/background/preAuthorizationMessaging.ts
+++ b/packages/extension/src/background/preAuthorizationMessaging.ts
@@ -1,12 +1,13 @@
import { difference } from "lodash-es"
import browser from "webextension-polyfill"
+import { uiService } from "../shared/__new/services/ui"
import { PreAuthorisationMessage } from "../shared/messages/PreAuthorisationMessage"
import { isPreAuthorized, preAuthorizeStore } from "../shared/preAuthorizations"
+import { Opened, backgroundUIService } from "./__new/services/ui"
import { addTab, sendMessageToHost } from "./activeTabs"
import { UnhandledMessage } from "./background"
import { HandleMessage } from "./background"
-import { openUi } from "./openUi"
export function getOriginFromSender(
sender: browser.runtime.MessageSender,
@@ -34,12 +35,12 @@ export const handlePreAuthorizationMessage: HandleMessage<
> = async ({
msg,
sender,
+ origin,
port,
- background: { wallet, actionQueue },
+ background: { wallet, actionService },
respond,
}) => {
async function addSenderTab() {
- const origin = getOriginFromSender(sender)
if (sender.tab?.id && port) {
await addTab({
id: sender.tab?.id,
@@ -51,30 +52,66 @@ export const handlePreAuthorizationMessage: HandleMessage<
switch (msg.type) {
case "CONNECT_DAPP": {
- const selectedAccount = await wallet.getSelectedAccount()
+ let selectedAccount = await wallet.getSelectedAccount()
+ let didOpenProgramatically = false
+
if (!selectedAccount) {
- openUi()
- return
+ didOpenProgramatically = true
+ const openAndUnlocked = await backgroundUIService.openUiAndUnlock()
+ selectedAccount = await wallet.getSelectedAccount()
+ if (!openAndUnlocked || !selectedAccount) {
+ return respond({
+ type: "REJECT_PREAUTHORIZATION",
+ })
+ }
}
- const origin = getOriginFromSender(sender)
+
const isAuthorized = await isPreAuthorized(selectedAccount, origin)
await addSenderTab()
if (!isAuthorized) {
- await actionQueue.push({
- type: "CONNECT_DAPP",
- payload: { host: origin },
+ /** Prompt user to connect to dapp */
+ const action = await actionService.add(
+ {
+ type: "CONNECT_DAPP",
+ payload: { host: origin },
+ },
+ {
+ origin,
+ },
+ )
+ didOpenProgramatically = true
+ const openAndUnlocked = await backgroundUIService.openUiAndUnlock()
+ if (!openAndUnlocked) {
+ return respond({
+ type: "REJECT_PREAUTHORIZATION",
+ })
+ }
+ /** Special case for CONNECT_DAPP - treat closing extension as rejection, in order to cleanly reset dapp */
+ void backgroundUIService.emitter.once(Opened).then(() => {
+ if (!backgroundUIService.opened) {
+ void actionService.reject(action.meta.hash)
+ }
})
}
if (isAuthorized && selectedAccount?.address) {
+ if (didOpenProgramatically) {
+ /** user unlocked, close the ui */
+ if (await uiService.hasFloatingWindow()) {
+ await uiService.closeFloatingWindow()
+ }
+ if (uiService.hasPopup()) {
+ uiService.closePopup()
+ }
+ }
return respond({
type: "CONNECT_DAPP_RES",
data: selectedAccount,
})
}
- return openUi()
+ return
}
case "IS_PREAUTHORIZED": {
@@ -85,7 +122,6 @@ export const handlePreAuthorizationMessage: HandleMessage<
return respond({ type: "IS_PREAUTHORIZED_RES", data: false })
}
- const origin = getOriginFromSender(sender)
const valid = await isPreAuthorized(selectedAccount, origin)
return respond({ type: "IS_PREAUTHORIZED_RES", data: valid })
diff --git a/packages/extension/src/background/respond.ts b/packages/extension/src/background/respond.ts
new file mode 100644
index 000000000..5a8afe673
--- /dev/null
+++ b/packages/extension/src/background/respond.ts
@@ -0,0 +1,14 @@
+import { MessageType } from "../shared/messages"
+import { sendMessageToActiveTabsAndUi, sendMessageToUi } from "./activeTabs"
+import { safeMessages } from "./messageHandling/messages"
+
+/** TODO: refactor */
+export const respond = async (msg: MessageType) => {
+ if (safeMessages.includes(msg.type)) {
+ await sendMessageToActiveTabsAndUi(msg)
+ } else {
+ await sendMessageToUi(msg)
+ }
+}
+
+export type Respond = typeof respond
diff --git a/packages/extension/src/background/schema/backup.schema.ts b/packages/extension/src/background/schema/backup.schema.ts
index 17255721a..cf099b6a1 100644
--- a/packages/extension/src/background/schema/backup.schema.ts
+++ b/packages/extension/src/background/schema/backup.schema.ts
@@ -1,57 +1,60 @@
-import { array, number, object, string } from "yup"
+import { z } from "zod"
-const cryptoValidation = object()
- .default(undefined)
- .optional()
- .shape({
- cipher: string().required(),
- ciphertext: string().required(),
- kdf: string().required(),
- mac: string().required(),
- cipherparams: object().required().shape({
- iv: string().required(),
- }),
- kdfparams: object().required().shape({
- salt: string().required(),
- n: number().required(),
- dklen: number().required(),
- p: number().required(),
- r: number().required(),
+const cryptoValidation = z.object({
+ cipher: z.string(),
+ ciphertext: z.string(),
+ kdf: z.string(),
+ mac: z.string(),
+ cipherparams: z.object({
+ iv: z.string(),
+ }),
+ kdfparams: z.object({
+ salt: z.string(),
+ n: z.number(),
+ dklen: z.number(),
+ p: z.number(),
+ r: z.number(),
+ }),
+})
+
+const schema = z
+ .object({
+ // standard backup/keystore file
+ address: z.string(),
+ version: z.number().int(),
+ // one of both must be there
+ crypto: cryptoValidation.optional(),
+ Crypto: cryptoValidation.optional(),
+ // ethers.js additions
+ "x-ethers": z.object({
+ mnemonicCounter: z.string(),
+ mnemonicCiphertext: z.string(),
+ path: z.string(),
}),
+ // argent additions
+ argent: z
+ .object({
+ version: z.number().int(),
+ accounts: z
+ .array(
+ z.object({
+ address: z.string().optional(),
+ network: z.string().optional(),
+ signer: z
+ .object({
+ type: z.string(),
+ derivationPath: z.string(),
+ })
+ .optional(),
+ }),
+ )
+ .optional(),
+ })
+ .optional(),
+ })
+ .refine((data) => !data.crypto !== !data.Crypto, {
+ message: "Just one of crypto or Crypto must be present",
+ path: ["crypto"],
})
-export default object({
- // standard backup/keystore file
- address: string().required(),
- version: number().integer().required(),
- // one of both must be there
- crypto: cryptoValidation,
- Crypto: cryptoValidation,
- // ethers.js additions
- "x-ethers": object().required().shape({
- mnemonicCounter: string().required(),
- mnemonicCiphertext: string().required(),
- path: string().required(),
- }),
- // argent additions
- argent: object()
- .default(undefined)
- .optional()
- .shape({
- version: number().integer().min(1).max(1).required(),
- accounts: array().of(
- object()
- .optional()
- .shape({
- address: string().required(),
- network: string().required(),
- signer: object().required().shape({
- type: string().required(),
- derivationPath: string().required(),
- }),
- }),
- ),
- }),
-}).test("crypto", "just one of crypto or Crypto must be present", (val) => {
- return !val.crypto !== !val.Crypto
-})
+export default schema
diff --git a/packages/extension/src/background/sessionMessaging.ts b/packages/extension/src/background/sessionMessaging.ts
deleted file mode 100644
index 9096a75e9..000000000
--- a/packages/extension/src/background/sessionMessaging.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import { compactDecrypt } from "jose"
-
-import { SessionMessage } from "../shared/messages/SessionMessage"
-import { bytesToUft8 } from "../shared/utils/encode"
-import { sendMessageToUi } from "./activeTabs"
-import { UnhandledMessage } from "./background"
-import { HandleMessage } from "./background"
-
-export const handleSessionMessage: HandleMessage = async ({
- msg,
- background: { wallet },
- messagingKeys: { privateKey },
- respond,
-}) => {
- switch (msg.type) {
- case "START_SESSION": {
- const { secure, body } = msg.data
- if (secure !== true) {
- throw Error("session can only be started with encryption")
- }
- const { plaintext } = await compactDecrypt(body, privateKey)
- const sessionPassword = bytesToUft8(plaintext)
- const result = await wallet.startSession(sessionPassword, (percent) => {
- respond({ type: "LOADING_PROGRESS", data: percent })
- })
- if (result) {
- const selectedAccount = await wallet.getSelectedAccount()
- return respond({
- type: "START_SESSION_RES",
- data: selectedAccount,
- })
- }
- return respond({ type: "START_SESSION_REJ" })
- }
-
- case "CHECK_PASSWORD": {
- const { body } = msg.data
- const { plaintext } = await compactDecrypt(body, privateKey)
- const password = bytesToUft8(plaintext)
- if (await wallet.checkPassword(password)) {
- return sendMessageToUi({ type: "CHECK_PASSWORD_RES" })
- }
- return sendMessageToUi({ type: "CHECK_PASSWORD_REJ" })
- }
-
- case "HAS_SESSION": {
- return respond({
- type: "HAS_SESSION_RES",
- data: await wallet.isSessionOpen(),
- })
- }
-
- case "STOP_SESSION": {
- await wallet.lock()
- return respond({ type: "DISCONNECT_ACCOUNT" })
- }
-
- case "IS_INITIALIZED": {
- const initialized = await wallet.isInitialized()
- return respond({
- type: "IS_INITIALIZED_RES",
- data: { initialized },
- })
- }
- }
-
- throw new UnhandledMessage()
-}
diff --git a/packages/extension/src/background/shieldMessaging.ts b/packages/extension/src/background/shieldMessaging.ts
deleted file mode 100644
index 79272226c..000000000
--- a/packages/extension/src/background/shieldMessaging.ts
+++ /dev/null
@@ -1,192 +0,0 @@
-import { stringToBytes } from "@scure/base"
-import { Signature, keccak, pedersen, sign } from "micro-starknet"
-import { ec, encode, number } from "starknet"
-
-import {
- getNetworkSelector,
- withGuardianSelector,
-} from "../shared/account/selectors"
-import { accountService } from "../shared/account/service"
-import { ShieldMessage } from "../shared/messages/ShieldMessage"
-import {
- addBackendAccount,
- getBackendAccounts,
- isTokenExpired,
- register,
- requestEmailAuthentication,
- verifyEmail,
-} from "../shared/shield/backend/account"
-import {
- ARGENT_SHIELD_ENABLED,
- ARGENT_SHIELD_NETWORK_ID,
-} from "../shared/shield/constants"
-import { validateEmailForAccounts } from "../shared/shield/validation"
-import { sendMessageToUi } from "./activeTabs"
-import { UnhandledMessage } from "./background"
-import { HandleMessage } from "./background"
-
-export const handleShieldMessage: HandleMessage = async ({
- msg,
- background: { wallet },
-}) => {
- switch (msg.type) {
- case "SHIELD_VALIDATE_ACCOUNT": {
- if (!ARGENT_SHIELD_ENABLED) {
- /** should never happen */
- throw new Error("Argent Shield is not enabled")
- }
-
- if (!ARGENT_SHIELD_NETWORK_ID) {
- /** should never happen */
- throw new Error("ARGENT_SHIELD_NETWORK_ID is not defined")
- }
-
- /** Check if account is valid for current wallet */
- try {
- const selectedAccount = await wallet.getSelectedAccount()
- const starknetAccount = await wallet.getSelectedStarknetAccount()
-
- if (!starknetAccount || !selectedAccount) {
- throw Error("no accounts")
- }
-
- /** Get current account state */
-
- const localAccounts = await accountService.get(
- getNetworkSelector(ARGENT_SHIELD_NETWORK_ID),
- )
- const localAccountsWithGuardian = await accountService.get(
- withGuardianSelector,
- )
- const backendAccounts = await getBackendAccounts()
-
- /** Validate email against account state */
-
- validateEmailForAccounts({
- localAccounts,
- localAccountsWithGuardian,
- backendAccounts,
- })
-
- return sendMessageToUi({
- type: "SHIELD_VALIDATE_ACCOUNT_RES",
- })
- } catch (error) {
- return sendMessageToUi({
- type: "SHIELD_VALIDATE_ACCOUNT_REJ",
- data: `${error}`,
- })
- }
- }
- case "SHIELD_ADD_ACCOUNT": {
- if (!ARGENT_SHIELD_ENABLED) {
- /** should never happen */
- throw new Error("Argent Shield is not enabled")
- }
-
- if (!ARGENT_SHIELD_NETWORK_ID) {
- /** should never happen */
- throw new Error("ARGENT_SHIELD_NETWORK_ID is not defined")
- }
- try {
- const selectedAccount = await wallet.getSelectedAccount()
- if (!selectedAccount) {
- throw Error("no account selected")
- }
-
- /** Check if this account already exists in backend */
- const backendAccounts = await getBackendAccounts()
-
- const existingAccount = backendAccounts.find(
- (x) =>
- number.hexToDecimalString(x.address) ===
- number.hexToDecimalString(selectedAccount.address),
- )
-
- let guardianAddress: string | undefined
-
- if (existingAccount) {
- guardianAddress = existingAccount.guardianAddresses[0]
- } else {
- /** Add account to backend */
- const keyPair = await wallet.getKeyPairByDerivationPath(
- selectedAccount?.signer.derivationPath,
- )
- const privateKey = keyPair.getPrivate()
- const publicKey = ec.getStarkKey(keyPair)
- const privateKeyHex = encode.addHexPrefix(privateKey.toString(16))
-
- const deploySignature = sign(
- pedersen(keccak(stringToBytes("utf8", "starknet")), publicKey),
- privateKeyHex,
- )
-
- const { r, s } = Signature.fromDER(deploySignature.toDERHex())
- const response = await addBackendAccount(
- publicKey,
- selectedAccount.address,
- [
- encode.addHexPrefix(r.toString(16)),
- encode.addHexPrefix(s.toString(16)),
- ],
- )
- guardianAddress = response.guardianAddress
- }
- if (!guardianAddress) {
- throw new Error("Unable to add account")
- }
- return sendMessageToUi({
- type: "SHIELD_ADD_ACCOUNT_RES",
- data: {
- guardianAddress,
- },
- })
- } catch (error) {
- return sendMessageToUi({
- type: "SHIELD_ADD_ACCOUNT_REJ",
- data: `${error}`,
- })
- }
- }
- case "SHIELD_REQUEST_EMAIL": {
- const email = msg.data
- try {
- await requestEmailAuthentication(email)
- return sendMessageToUi({
- type: "SHIELD_REQUEST_EMAIL_RES",
- })
- } catch (error) {
- return sendMessageToUi({
- type: "SHIELD_REQUEST_EMAIL_REJ",
- data: `${error}`,
- })
- }
- }
- case "SHIELD_CONFIRM_EMAIL": {
- const code = msg.data
- try {
- const { userRegistrationStatus } = await verifyEmail(code)
-
- if (userRegistrationStatus === "notRegistered") {
- await register()
- }
- return sendMessageToUi({
- type: "SHIELD_CONFIRM_EMAIL_RES",
- })
- } catch (e) {
- return sendMessageToUi({
- type: "SHIELD_CONFIRM_EMAIL_REJ",
- data: JSON.stringify(e),
- })
- }
- }
- case "SHIELD_IS_TOKEN_EXPIRED": {
- const data = await isTokenExpired()
- return sendMessageToUi({
- type: "SHIELD_IS_TOKEN_EXPIRED_RES",
- data,
- })
- }
- }
- throw new UnhandledMessage()
-}
diff --git a/packages/extension/src/background/swr/helpers.ts b/packages/extension/src/background/swr/helpers.ts
deleted file mode 100644
index ef5c7a61d..000000000
--- a/packages/extension/src/background/swr/helpers.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { isFunction, isObjectLike, isPlainObject } from "lodash-es"
-
-import { Config } from "./types"
-
-const identity = (value: unknown) => value
-
-export function parseConfig(config: Config) {
- if (!isPlainObject(config)) {
- throw new Error("Config is required")
- }
-
- const storage = config.storage
-
- if (
- !isObjectLike(storage) ||
- !isFunction(storage.get) ||
- !isFunction(storage.set)
- ) {
- throw new Error(
- 'Storage is required and should satisfy the Config["storage"] type',
- )
- }
-
- const minTimeToStale = config.minTimeToStale || 0
- const maxTimeToLive =
- Math.min(config.maxTimeToLive ?? 0, Number.MAX_SAFE_INTEGER) || Infinity
- const serialize = isFunction(config.serialize) ? config.serialize : identity
- const deserialize = isFunction(config.deserialize)
- ? config.deserialize
- : identity
-
- if (minTimeToStale >= maxTimeToLive) {
- throw new Error("minTimeToStale must be less than maxTimeToLive")
- }
-
- return {
- storage,
- minTimeToStale,
- maxTimeToLive,
- serialize,
- deserialize,
- }
-}
diff --git a/packages/extension/src/background/swr/index.ts b/packages/extension/src/background/swr/index.ts
deleted file mode 100644
index e7cecf73a..000000000
--- a/packages/extension/src/background/swr/index.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import { isFunction, isNil } from "lodash-es"
-
-import { parseConfig } from "./helpers"
-import { Config, StaleWhileRevalidateCache } from "./types"
-
-// modified from https://github.com/jperasmus/stale-while-revalidate-cache
-
-export function createStaleWhileRevalidateCache(
- config: Config,
-): StaleWhileRevalidateCache {
- const { storage, minTimeToStale, maxTimeToLive, serialize, deserialize } =
- parseConfig(config)
-
- async function staleWhileRevalidate(
- cacheKey: string | (() => string),
- fn: () => TReturnValue,
- ): Promise {
- const key = String(isFunction(cacheKey) ? cacheKey() : cacheKey)
- const timeKey = `${key}_time`
-
- async function retrieveCachedValue() {
- try {
- const [cachedValue, cachedTime] = await Promise.all([
- storage.get(key),
- storage.get(timeKey),
- ])
-
- let deserializedCachedValue = deserialize(cachedValue)
-
- if (isNil(deserializedCachedValue)) {
- return { cachedValue: null, cachedAge: 0 }
- }
-
- const now = Date.now()
- const cachedAge = now - Number(cachedTime)
-
- if (cachedAge > maxTimeToLive) {
- deserializedCachedValue = null
- }
-
- return { cachedValue: deserializedCachedValue, cachedAge }
- } catch {
- return { cachedValue: null, cachedAge: 0 }
- }
- }
-
- async function persistValue(result: TReturnValue) {
- try {
- await Promise.all([
- storage.set(key, serialize(result)),
- storage.set(timeKey, Date.now().toString()),
- ])
- } catch {
- // Ignore
- }
- }
-
- async function revalidate() {
- const result = await fn()
-
- // Intentionally persisting asynchronously and not blocking since there is
- // in any case a chance for a race condition to occur when using an external
- // persistence store, like Redis, with multiple consumers. The impact is low.
- persistValue(result)
-
- return result
- }
-
- const { cachedValue, cachedAge } = await retrieveCachedValue()
-
- if (!isNil(cachedValue)) {
- if (cachedAge >= minTimeToStale) {
- // Non-blocking so that revalidation runs while stale cache data is returned
- // Error handled in `revalidate` by emitting an event, so only need a no-op here
- revalidate().catch(() => {
- /* no-op */
- })
- }
-
- return cachedValue as TReturnValue
- }
-
- return revalidate()
- }
-
- return staleWhileRevalidate
-}
diff --git a/packages/extension/src/background/swr/types.ts b/packages/extension/src/background/swr/types.ts
deleted file mode 100644
index 0d7a4c5c7..000000000
--- a/packages/extension/src/background/swr/types.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { IKeyValueStorage } from "../../shared/storage/keyvalue"
-
-export type StaleWhileRevalidateCache = (
- cacheKey: string | (() => string),
- fn: () => TReturnValue,
-) => Promise
-
-export interface Config {
- minTimeToStale?: number
- maxTimeToLive?: number
- storage: IKeyValueStorage
- serialize?: (value: unknown) => unknown
- deserialize?: (value: unknown) => unknown
-}
diff --git a/packages/extension/src/background/tokenMessaging.ts b/packages/extension/src/background/tokenMessaging.ts
index 78306ef32..71ab85d1b 100644
--- a/packages/extension/src/background/tokenMessaging.ts
+++ b/packages/extension/src/background/tokenMessaging.ts
@@ -1,27 +1,34 @@
import { TokenMessage } from "../shared/messages/TokenMessage"
import { defaultNetwork } from "../shared/network"
-import { hasToken } from "../shared/token/storage"
+import { tokenService } from "../shared/token/__new/service"
import { HandleMessage, UnhandledMessage } from "./background"
export const handleTokenMessaging: HandleMessage = async ({
msg,
- background: { actionQueue, wallet },
+ origin,
+ background: { wallet, actionService },
respond,
}) => {
switch (msg.type) {
case "REQUEST_TOKEN": {
const selectedAccount = await wallet.getSelectedAccount()
- const exists = await hasToken({
+ const token = await tokenService.getToken({
+ address: msg.data.address,
networkId:
selectedAccount?.networkId ?? msg.data.networkId ?? defaultNetwork.id,
- ...msg.data,
})
+ const exists = Boolean(token)
if (!exists) {
- const { meta } = await actionQueue.push({
- type: "REQUEST_TOKEN",
- payload: msg.data,
- })
+ const { meta } = await actionService.add(
+ {
+ type: "REQUEST_TOKEN",
+ payload: msg.data,
+ },
+ {
+ origin,
+ },
+ )
return respond({
type: "REQUEST_TOKEN_RES",
@@ -38,7 +45,7 @@ export const handleTokenMessaging: HandleMessage = async ({
}
case "REJECT_REQUEST_TOKEN": {
- return await actionQueue.remove(msg.data.actionHash)
+ return await actionService.remove(msg.data.actionHash)
}
}
diff --git a/packages/extension/src/background/transactions/badgeText.ts b/packages/extension/src/background/transactions/badgeText.ts
index a677743d1..f88ef1607 100644
--- a/packages/extension/src/background/transactions/badgeText.ts
+++ b/packages/extension/src/background/transactions/badgeText.ts
@@ -14,15 +14,17 @@ import {
BaseWalletAccount,
MultisigWalletAccount,
} from "../../shared/wallet.model"
-import { accountsEqual } from "../../shared/wallet.service"
+import { accountsEqual } from "../../shared/utils/accountsEqual"
import { old_walletStore } from "../../shared/wallet/walletStore"
import { transactionsStore } from "./store"
+import { TransactionExecutionStatus, TransactionFinalityStatus } from "starknet"
// selects transactions that are pending and match the provided account
export const pendingAccountTransactionsSelector = memoize(
(account: BaseWalletAccount) => (transaction: Transaction) =>
- transaction.status === "RECEIVED" &&
+ transaction.finalityStatus === TransactionFinalityStatus.RECEIVED &&
+ transaction.executionStatus !== TransactionExecutionStatus.REJECTED && // Rejected transactions have finality status RECEIVED
!transaction.meta?.isDeployAccount &&
accountsEqual(account, transaction.account),
)
@@ -30,12 +32,7 @@ export const pendingAccountTransactionsSelector = memoize(
export const multisigPendingTransactionSelector = memoize(
(multisig: MultisigWalletAccount) =>
(transaction: MultisigPendingTransaction) => {
- const transactionAccount = {
- address: transaction.address,
- networkId: transaction.networkId,
- }
-
- return accountsEqual(multisig, transactionAccount) && transaction.notify
+ return accountsEqual(multisig, transaction.account) && transaction.notify
},
)
diff --git a/packages/extension/src/background/transactions/checkTransactionHash.ts b/packages/extension/src/background/transactions/checkTransactionHash.ts
new file mode 100644
index 000000000..3403570e6
--- /dev/null
+++ b/packages/extension/src/background/transactions/checkTransactionHash.ts
@@ -0,0 +1,25 @@
+import { constants, num } from "starknet"
+import { WalletAccount } from "../../shared/wallet.model"
+import { TransactionError } from "../../shared/errors/transaction"
+
+export const checkTransactionHash = (
+ transactionHash?: num.BigNumberish,
+ account?: WalletAccount,
+): boolean => {
+ try {
+ if (!transactionHash) {
+ throw new TransactionError({
+ code: "NO_TRANSACTION_HASH",
+ })
+ }
+ const bn = num.toBigInt(transactionHash)
+ if (bn <= constants.ZERO && account?.type !== "multisig") {
+ throw new TransactionError({
+ code: "INVALID_TRANSACTION_HASH_RANGE",
+ })
+ }
+ return true
+ } catch {
+ return false
+ }
+}
diff --git a/packages/extension/src/background/transactions/determineUpdates.ts b/packages/extension/src/background/transactions/determineUpdates.ts
index 11366ddee..4a5a72293 100644
--- a/packages/extension/src/background/transactions/determineUpdates.ts
+++ b/packages/extension/src/background/transactions/determineUpdates.ts
@@ -8,6 +8,6 @@ export function getTransactionsStatusUpdate(
(newTransaction) =>
oldTransactions.find((oldTransaction) =>
compareTransactions(oldTransaction, newTransaction),
- )?.status !== newTransaction.status,
+ )?.finalityStatus !== newTransaction.finalityStatus,
)
}
diff --git a/packages/extension/src/background/transactions/fees/multisigFeeEstimation.ts b/packages/extension/src/background/transactions/fees/multisigFeeEstimation.ts
deleted file mode 100644
index 20c796d94..000000000
--- a/packages/extension/src/background/transactions/fees/multisigFeeEstimation.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { AllowArray, Call, number } from "starknet"
-import { Account as Accountv5, ec } from "starknet5"
-
-import { getProviderv5 } from "../../../shared/network/provider"
-import { WalletAccount } from "../../../shared/wallet.model"
-
-export const getEstimatedFeeForMultisigTx = async (
- selectedAccount: WalletAccount,
- transactions: AllowArray,
- nonce?: number.BigNumberish,
-) => {
- const providerV5 = getProviderv5(selectedAccount.network)
-
- const accountv5 = new Accountv5(
- providerV5,
- selectedAccount.address,
- ec.starkCurve.utils.randomPrivateKey(), // Random private key works cuz we skipValidation is true
- )
-
- const { suggestedMaxFee, overall_fee } = await accountv5.estimateInvokeFee(
- transactions,
- {
- nonce,
- skipValidate: true,
- },
- )
-
- return {
- overall_fee: number.toBN(overall_fee.toString()),
- suggestedMaxFee: number.toBN(suggestedMaxFee.toString()),
- }
-}
diff --git a/packages/extension/src/background/transactions/fees/store.ts b/packages/extension/src/background/transactions/fees/store.ts
deleted file mode 100644
index e872b18c5..000000000
--- a/packages/extension/src/background/transactions/fees/store.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { isEqual } from "lodash-es"
-import { Call } from "starknet5"
-
-import { ArrayStorage } from "../../../shared/storage"
-
-type EstimatedFees = {
- amount: string
- suggestedMaxFee: string
- accountDeploymentFee?: string
- maxADFee?: string
- transactions: Call | Call[]
-}
-
-type EstimatedFeesEnriched = EstimatedFees & {
- timestamp: number
-}
-
-export const estimatedFeesStore = new ArrayStorage([], {
- namespace: "core:estimatedFees",
- areaName: "session",
-})
-
-const timestampInSeconds = (): number => Math.floor(Date.now() / 1000)
-
-export const addEstimatedFees = (estimatedFees: EstimatedFees) => {
- const newEstimatedFees = {
- ...estimatedFees,
- timestamp: timestampInSeconds(),
- }
- return estimatedFeesStore.push(newEstimatedFees)
-}
-
-export const getEstimatedFees = async (
- transactions: Call | Call[],
-): Promise => {
- const fees = await estimatedFeesStore.get((value) =>
- isEqual(value.transactions, transactions),
- )
- const FIFTEEN_SECONDS = 15
- const feesExist = fees.length > 0
- const areFeesOutdated =
- feesExist &&
- fees[0].timestamp + FIFTEEN_SECONDS < Math.floor(Date.now() / 1000)
-
- if (feesExist && !areFeesOutdated) {
- return fees[0]
- }
- return null
-}
diff --git a/packages/extension/src/background/transactions/onupdate/declareContract.ts b/packages/extension/src/background/transactions/onupdate/declareContract.ts
index 91ac3ed6b..c70dc2335 100644
--- a/packages/extension/src/background/transactions/onupdate/declareContract.ts
+++ b/packages/extension/src/background/transactions/onupdate/declareContract.ts
@@ -6,7 +6,7 @@ export const handleDeclareContractTransaction: TransactionUpdateListener =
async (transactions) => {
for (const transaction of transactions) {
if (transaction.meta?.type === UdcTransactionType.DECLARE_CONTRACT) {
- if (transaction.status !== "REJECTED") {
+ if (transaction.executionStatus !== "REJECTED") {
await declaredTransactionsStore.push(transaction)
} else {
await declaredTransactionsStore.remove(transaction)
diff --git a/packages/extension/src/background/transactions/onupdate/deployAccount.ts b/packages/extension/src/background/transactions/onupdate/deployAccount.ts
index cd53326aa..89a186df3 100644
--- a/packages/extension/src/background/transactions/onupdate/deployAccount.ts
+++ b/packages/extension/src/background/transactions/onupdate/deployAccount.ts
@@ -8,7 +8,7 @@ export const handleDeployAccountTransaction: TransactionUpdateListener = async (
const deployAccountTxns = transactions.filter(
(transaction) =>
transaction.meta?.isDeployAccount &&
- SUCCESS_STATUSES.includes(transaction.status),
+ SUCCESS_STATUSES.includes(transaction.finalityStatus),
)
if (deployAccountTxns.length > 0) {
await updateAccountDetails(
diff --git a/packages/extension/src/background/transactions/onupdate/nonce.ts b/packages/extension/src/background/transactions/onupdate/nonce.ts
index 4a5cc42e2..b38438735 100644
--- a/packages/extension/src/background/transactions/onupdate/nonce.ts
+++ b/packages/extension/src/background/transactions/onupdate/nonce.ts
@@ -6,7 +6,7 @@ export const checkResetStoredNonce: TransactionUpdateListener = async (
) => {
for (const transaction of transactions) {
// on error remove stored (increased) nonce
- if (transaction.account && transaction.status === "REJECTED") {
+ if (transaction.account && transaction.executionStatus === "REJECTED") {
await resetStoredNonce(transaction.account)
}
}
diff --git a/packages/extension/src/background/transactions/onupdate/notifications.ts b/packages/extension/src/background/transactions/onupdate/notifications.ts
index 1a48fc33e..706891e02 100644
--- a/packages/extension/src/background/transactions/onupdate/notifications.ts
+++ b/packages/extension/src/background/transactions/onupdate/notifications.ts
@@ -4,22 +4,25 @@ import {
addToAlreadyShown,
hasShownNotification,
sendTransactionNotification,
-} from "../../notification"
+} from "../../../shared/notification"
import { TransactionUpdateListener } from "./type"
export const notifyAboutCompletedTransactions: TransactionUpdateListener =
async (transactions) => {
for (const transaction of transactions) {
- const { hash, status, meta, account } = transaction
+ const { hash, finalityStatus, executionStatus, meta, account } =
+ transaction
if (
- (SUCCESS_STATUSES.includes(status) || FAILED_STATUS.includes(status)) &&
+ (SUCCESS_STATUSES.includes(finalityStatus) ||
+ (executionStatus && FAILED_STATUS.includes(executionStatus))) &&
!(await hasShownNotification(hash))
) {
- addToAlreadyShown(hash)
+ void addToAlreadyShown(hash)
if (!account.hidden && !meta?.isDeployAccount) {
await decrementTransactionsBeforeReview()
- sendTransactionNotification(hash, status, meta)
+ finalityStatus &&
+ sendTransactionNotification(hash, finalityStatus, meta)
}
}
}
diff --git a/packages/extension/src/background/transactions/onupdate/upgrade.ts b/packages/extension/src/background/transactions/onupdate/upgrade.ts
index 17ccd41cd..e81ea7729 100644
--- a/packages/extension/src/background/transactions/onupdate/upgrade.ts
+++ b/packages/extension/src/background/transactions/onupdate/upgrade.ts
@@ -9,7 +9,7 @@ export const handleUpgradeTransaction: TransactionUpdateListener = async (
)
if (upgrades.length > 0) {
await updateAccountDetails(
- "type",
+ "implementation",
upgrades.map((transaction) => transaction.account),
)
}
diff --git a/packages/extension/src/background/transactions/service/base.test.ts b/packages/extension/src/background/transactions/service/base.test.ts
new file mode 100644
index 000000000..4a6bcd048
--- /dev/null
+++ b/packages/extension/src/background/transactions/service/base.test.ts
@@ -0,0 +1,90 @@
+import { BaseTransaction } from "../../../shared/transactions/interface"
+import { BaseTransactionTrackingService } from "./base"
+
+class TestTransactionService extends BaseTransactionTrackingService<
+ BaseTransaction,
+ string
+> {}
+
+describe("BaseTransactionTrackingService", () => {
+ let service: TestTransactionService
+ const transaction1: BaseTransaction = { hash: "0x1", networkId: "net1" }
+ const transaction2: BaseTransaction = { hash: "0x2", networkId: "net2" }
+
+ beforeEach(() => {
+ const toIdentifier = (tx: BaseTransaction) => `${tx.networkId}-${tx.hash}`
+ service = new TestTransactionService("default", toIdentifier)
+ })
+
+ test("add transaction", async () => {
+ await service.add(transaction1)
+ const status = await service.get(transaction1)
+ expect(status).toBe("default")
+ })
+
+ test("get transaction", async () => {
+ await service.add(transaction1)
+ const status = await service.get(transaction1)
+ expect(status).toBe("default")
+
+ await expect(service.get(transaction2)).rejects.toThrow(
+ "Transaction [object Object] not tracked",
+ )
+ })
+
+ test("subscribe to transaction updates", async () => {
+ const callback = vi.fn()
+ const unsubscribe = await service.subscribe(callback)
+
+ await service.add(transaction1)
+ expect(callback).toBeCalledWith({
+ transaction: transaction1,
+ status: "default",
+ })
+
+ unsubscribe()
+
+ await service.add(transaction2)
+ expect(callback).not.toBeCalledWith({
+ transaction: transaction2,
+ status: "default",
+ })
+ })
+
+ test("callbacks are notified on transaction add", async () => {
+ const callback1 = vi.fn()
+ const callback2 = vi.fn()
+
+ await service.subscribe(callback1)
+ await service.subscribe(callback2)
+
+ await service.add(transaction1)
+
+ expect(callback1).toBeCalledWith({
+ transaction: transaction1,
+ status: "default",
+ })
+ expect(callback2).toBeCalledWith({
+ transaction: transaction1,
+ status: "default",
+ })
+ })
+
+ test("subscribe after transaction added", async () => {
+ const callback = vi.fn()
+
+ await service.add(transaction1)
+ await service.subscribe(callback)
+
+ expect(callback).not.toBeCalledWith({
+ transaction: transaction1,
+ status: "default",
+ })
+
+ await service.add(transaction2)
+ expect(callback).toBeCalledWith({
+ transaction: transaction2,
+ status: "default",
+ })
+ })
+})
diff --git a/packages/extension/src/background/transactions/service/base.ts b/packages/extension/src/background/transactions/service/base.ts
new file mode 100644
index 000000000..0a64d9634
--- /dev/null
+++ b/packages/extension/src/background/transactions/service/base.ts
@@ -0,0 +1,58 @@
+import { BaseTransaction } from "../../../shared/transactions/interface"
+
+type TxStatusUpdate = {
+ transaction: H
+ status: S
+}
+
+// TransactionTrackingService used to track inflight transactions
+export interface TransactionTrackingService {
+ add: (transaction: H) => Promise
+ get: (transaction: H) => Promise
+ subscribe: (
+ callback: (ctx: TxStatusUpdate) => void,
+ ) => Promise<() => void>
+}
+
+export abstract class BaseTransactionTrackingService<
+ T extends BaseTransaction,
+ S,
+> implements TransactionTrackingService
+{
+ protected callbacks = new Set<(ctx: TxStatusUpdate) => void>()
+ protected identifierToStatuses = new Map()
+
+ constructor(
+ protected defaultStatus: S,
+ protected toIdentifier: (tx: T) => string,
+ ) {}
+
+ protected notifySubscribers(transaction: T, status: S) {
+ this.callbacks.forEach((callback) => {
+ callback({ transaction, status })
+ })
+ }
+
+ async add(transaction: T): Promise {
+ const identifier = this.toIdentifier(transaction)
+ this.identifierToStatuses.set(identifier, this.defaultStatus)
+ this.notifySubscribers(transaction, this.defaultStatus)
+ }
+ async get(transaction: T): Promise {
+ const identifier = this.toIdentifier(transaction)
+ const status = this.identifierToStatuses.get(identifier)
+
+ if (!status) {
+ throw new Error(`Transaction ${transaction} not tracked`)
+ }
+ return status
+ }
+ async subscribe(
+ callback: (ctx: TxStatusUpdate) => void,
+ ): Promise<() => void> {
+ this.callbacks.add(callback)
+ return () => {
+ this.callbacks.delete(callback)
+ }
+ }
+}
diff --git a/packages/extension/src/background/transactions/service/starknet.service.ts b/packages/extension/src/background/transactions/service/starknet.service.ts
new file mode 100644
index 000000000..03d1a9137
--- /dev/null
+++ b/packages/extension/src/background/transactions/service/starknet.service.ts
@@ -0,0 +1,186 @@
+import {
+ BaseTransactionTrackingService,
+ TransactionTrackingService,
+} from "./base"
+
+import { IScheduleService } from "../../../shared/schedule/interface"
+import { IChainService } from "../../../shared/chain/service/interface"
+import {
+ BaseTransaction,
+ TransactionStatus,
+} from "../../../shared/transactions/interface"
+import {
+ getTransactionIdentifier,
+ identifierToBaseTransaction,
+} from "../../../shared/transactions/utils"
+import { IRepository } from "../../../shared/storage/__new/interface"
+import {
+ Transaction,
+ getInFlightTransactions,
+} from "../../../shared/transactions"
+import uniqWith from "lodash-es/uniqWith"
+import { accountsEqual } from "../../../shared/utils/accountsEqual"
+import { getTransactionHistory } from "../sources/voyager"
+import { getTransactionsUpdate } from "../sources/onchain"
+import { transactionsStore } from "../store"
+import { accountService } from "../../../shared/account/service"
+import { delay } from "../../../shared/utils/delay"
+import { TransactionFinalityStatus } from "starknet"
+import { RefreshInterval } from "../../../shared/config"
+
+function isFinalStatus(status: TransactionStatus): boolean {
+ return status.status === "confirmed" || status.status === "failed"
+}
+
+const DEFAULT_POLLING_INTERVAL = 15
+const LOCAL_POLLING_INTERVAL = 5
+
+export class TransactionTrackerWorker
+ extends BaseTransactionTrackingService
+ implements TransactionTrackingService
+{
+ constructor(
+ private readonly schedulingService: IScheduleService<
+ "starknetTransactionTracker" | "loadHistory" | "trackTransactionsUpdates"
+ >,
+ private readonly chainService: IChainService,
+ private readonly transactionsRepo: IRepository,
+ ) {
+ super({ status: "pending" }, getTransactionIdentifier)
+
+ void this.schedulingService.registerImplementation({
+ id: "starknetTransactionTracker",
+ callback: this.update.bind(this),
+ })
+
+ void this.schedulingService.registerImplementation({
+ id: "loadHistory",
+ callback: this.loadHistory.bind(this),
+ })
+
+ void this.schedulingService.registerImplementation({
+ id: "trackTransactionsUpdates",
+ callback: this.trackTransactionsUpdates.bind(this),
+ })
+
+ void this.schedulingService.every(RefreshInterval.FAST, {
+ id: "starknetTransactionTracker",
+ })
+ void this.schedulingService.every(RefreshInterval.SLOW, {
+ id: "loadHistory",
+ })
+ void this.schedulingService.every(RefreshInterval.MEDIUM, {
+ id: "trackTransactionsUpdates",
+ })
+ this.subscribeToRepoChange()
+ }
+
+ async trackTransactionsUpdates() {
+ // the config below will run transaction updates 4x per minute, if there are in-flight transactions
+ // By default it will update on second 0, 15, 30 and 45 but by updating WAIT_TIME we can change the number of executions
+ const maxExecutionTimeInMs = 60000 // 1 minute max execution time
+ let transactionPollingIntervalInS = DEFAULT_POLLING_INTERVAL
+ const startTime = Date.now()
+ let inFlightTransactions = await this.syncTransactionRepo()
+ while (
+ inFlightTransactions.length > 0 &&
+ Date.now() - startTime < maxExecutionTimeInMs
+ ) {
+ const localTransaction = inFlightTransactions.find(
+ (tx) => tx.account.networkId === "localhost",
+ )
+ if (localTransaction) {
+ transactionPollingIntervalInS = LOCAL_POLLING_INTERVAL
+ } else {
+ transactionPollingIntervalInS = DEFAULT_POLLING_INTERVAL
+ }
+ console.info(
+ `~> waiting ${transactionPollingIntervalInS}s for transaction updates`,
+ )
+ await delay(transactionPollingIntervalInS * 1000)
+ console.info(
+ "~> fetching transaction updates as pending transactions were detected",
+ )
+ inFlightTransactions = await this.syncTransactionRepo()
+ }
+ }
+
+ protected async update() {
+ const oldTransactionStatuses = Object.fromEntries(
+ this.identifierToStatuses.entries(),
+ )
+ const baseTransactions = [...this.identifierToStatuses.keys()].map(
+ identifierToBaseTransaction,
+ )
+ const updatedTransactions = await Promise.all(
+ baseTransactions.map(async (tx) => {
+ return this.chainService.getTransactionStatus(tx)
+ }),
+ )
+
+ for (const tx of updatedTransactions) {
+ // if status has changed compared to previous update, notify subscribers
+ const oldStatus = oldTransactionStatuses[super.toIdentifier(tx)]
+ if (oldStatus?.status !== tx.status.status) {
+ this.callbacks.forEach((callback) => {
+ callback({ transaction: tx, status: tx.status })
+ })
+ }
+
+ // remove transaction from tracking if it's final
+ if (isFinalStatus(tx.status)) {
+ this.identifierToStatuses.delete(super.toIdentifier(tx))
+ }
+ }
+ }
+
+ async loadHistory() {
+ const accountsToPopulate = await accountService.get()
+ const allTransactions = await this.transactionsRepo.get()
+ const uniqAccounts = uniqWith(accountsToPopulate, accountsEqual)
+ const historyTransactions = await getTransactionHistory(
+ uniqAccounts,
+ allTransactions,
+ )
+ await this.transactionsRepo.upsert(historyTransactions)
+ }
+ 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,
+ )
+ await this.transactionsRepo.upsert(updatedTransactions)
+ return getInFlightTransactions(allTransactions)
+ }
+
+ private subscribeToRepoChange() {
+ const FAST_UPDATE_INTERVAL = 5 * 1000 // 5 seconds
+ // Not sure why I cannot use the repo instead of the store here
+ transactionsStore.subscribe((_, changeset) => {
+ const oldTransactions = changeset.oldValue?.map((t) => t.hash) ?? []
+ const newAddedTransactions =
+ changeset.newValue
+ ?.filter(({ hash }) => !oldTransactions.includes(hash))
+ .filter(
+ ({ finalityStatus }) =>
+ finalityStatus === TransactionFinalityStatus.RECEIVED,
+ ) ?? []
+
+ if (newAddedTransactions.length > 0) {
+ setTimeout(() => {
+ const syncTxRepo = async () => {
+ const inFlightTransactions = await this.syncTransactionRepo()
+ // only update again if there are still in-flight transactions
+ if (inFlightTransactions.length > 0) {
+ setTimeout(() => {
+ void this.syncTransactionRepo()
+ }, FAST_UPDATE_INTERVAL)
+ }
+ }
+ void syncTxRepo()
+ }, FAST_UPDATE_INTERVAL)
+ }
+ })
+ }
+}
diff --git a/packages/extension/src/background/transactions/service/worker.ts b/packages/extension/src/background/transactions/service/worker.ts
new file mode 100644
index 000000000..886b6f42f
--- /dev/null
+++ b/packages/extension/src/background/transactions/service/worker.ts
@@ -0,0 +1,10 @@
+import { starknetChainService } from "../../../shared/chain/service"
+import { chromeScheduleService } from "../../../shared/schedule"
+import { transactionsRepo } from "../store"
+import { TransactionTrackerWorker } from "./starknet.service"
+
+export const transactionTrackerWorker = new TransactionTrackerWorker(
+ chromeScheduleService,
+ starknetChainService,
+ transactionsRepo,
+)
diff --git a/packages/extension/src/background/transactions/sources/onchain.ts b/packages/extension/src/background/transactions/sources/onchain.ts
index 04198a2f5..b2cb34844 100644
--- a/packages/extension/src/background/transactions/sources/onchain.ts
+++ b/packages/extension/src/background/transactions/sources/onchain.ts
@@ -1,3 +1,4 @@
+import { TransactionExecutionStatus, TransactionFinalityStatus } from "starknet"
import { getProvider } from "../../../shared/network"
import {
Transaction,
@@ -13,19 +14,52 @@ export async function getTransactionsUpdate(transactions: Transaction[]) {
const fetchedTransactions = await Promise.allSettled(
transactionsToCheck.map(async (transaction) => {
const provider = getProvider(transaction.account.network)
- const status = await provider.getTransactionStatus(transaction.hash)
- return {
- ...transaction,
- status: status.tx_status,
- failureReason: status.tx_failure_reason,
+ const tx = await provider.getTransactionReceipt(transaction.hash)
+
+ let updatedTransaction: Transaction
+
+ // Handle Reverted transaction
+ if ("revert_reason" in tx) {
+ updatedTransaction = {
+ ...transaction,
+ finalityStatus:
+ tx.finality_status ||
+ tx.status ||
+ TransactionFinalityStatus.NOT_RECEIVED, // For backward compatibility on mainnet
+ revertReason: tx.revert_reason,
+ }
+
+ // Handle Rejected transaction
+ } else if ("transaction_failure_reason" in tx) {
+ updatedTransaction = {
+ ...transaction,
+ finalityStatus: tx.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,
+ }
}
+
+ return updatedTransaction
}),
)
const updatedTransactions = fetchedTransactions.reduce(
(acc, transaction) => {
if (transaction.status === "fulfilled") {
- acc.push(transaction.value)
+ acc.push({
+ ...transaction.value,
+ finalityStatus:
+ transaction.value.finalityStatus ??
+ TransactionFinalityStatus.RECEIVED,
+ executionStatus: transaction.value.executionStatus,
+ })
}
return acc
},
diff --git a/packages/extension/src/background/transactions/sources/voyager.model.ts b/packages/extension/src/background/transactions/sources/voyager.model.ts
new file mode 100644
index 000000000..29b138155
--- /dev/null
+++ b/packages/extension/src/background/transactions/sources/voyager.model.ts
@@ -0,0 +1,11 @@
+import { TransactionExecutionStatus, TransactionFinalityStatus } from "starknet"
+
+export interface VoyagerTransaction {
+ blockId: string
+ blockNumber: number
+ hash: string
+ index: number
+ timestamp: number
+ type: string
+ status: TransactionFinalityStatus | TransactionExecutionStatus
+}
diff --git a/packages/extension/src/background/transactions/sources/voyager.ts b/packages/extension/src/background/transactions/sources/voyager.ts
index 3eb373dea..956475ab8 100644
--- a/packages/extension/src/background/transactions/sources/voyager.ts
+++ b/packages/extension/src/background/transactions/sources/voyager.ts
@@ -1,5 +1,3 @@
-import { Status } from "starknet"
-
import { ARGENT_EXPLORER_BASE_URL } from "../../../shared/api/constants"
import { argentApiNetworkForNetwork } from "../../../shared/api/fetcher"
import { Network } from "../../../shared/network"
@@ -9,16 +7,7 @@ import { WalletAccount } from "../../../shared/wallet.model"
import { stripAddressZeroPadding } from "../../../ui/features/accounts/accounts.service"
import { fetchWithTimeout } from "../../utils/fetchWithTimeout"
import { mapVoyagerTransactionToTransaction } from "../transformers"
-
-export interface VoyagerTransaction {
- blockId: string
- blockNumber: number
- hash: string
- index: number
- timestamp: number
- type: string
- status: Status
-}
+import { VoyagerTransaction } from "./voyager.model"
export const fetchVoyagerTransactions = async (
address: string,
diff --git a/packages/extension/src/background/transactions/store.ts b/packages/extension/src/background/transactions/store.ts
index 3d93c9bd2..353968c69 100644
--- a/packages/extension/src/background/transactions/store.ts
+++ b/packages/extension/src/background/transactions/store.ts
@@ -9,29 +9,41 @@ import {
compareTransactions,
} from "../../shared/transactions"
import { runAddedOrUpdatedHandlers, runChangedStatusHandlers } from "./onupdate"
-import { checkTransactionHash } from "./transactionExecution"
+import { checkTransactionHash } from "./checkTransactionHash"
+import { adaptArrayStorage } from "../../shared/storage/__new/repository"
+import { TransactionExecutionStatus, TransactionFinalityStatus } from "starknet"
+import { IRepository } from "../../shared/storage/__new/interface"
+/**
+ * @deprecated use `transactionsRepo` instead
+ */
export const transactionsStore = new ArrayStorage([], {
namespace: "core:transactions",
areaName: "local",
compare: compareTransactions,
})
+export const transactionsRepo: IRepository =
+ adaptArrayStorage(transactionsStore)
+
const timestampInSeconds = (): number => Math.floor(Date.now() / 1000)
-export const addTransaction = async (
+export const addTransaction = (
transaction: TransactionRequest,
- status?: ExtendedTransactionStatus,
+ finalityStatus?: ExtendedTransactionStatus,
+ executionStatus?: TransactionExecutionStatus,
) => {
// sanity checks
if (!checkTransactionHash(transaction.hash)) {
return // dont throw
}
- const defaultStatus: ExtendedTransactionStatus = "RECEIVED"
+ const defaultStatus: ExtendedTransactionStatus =
+ TransactionFinalityStatus.RECEIVED
- const newTransaction = {
- status: status ?? defaultStatus,
+ const newTransaction: Transaction = {
+ finalityStatus: finalityStatus ?? defaultStatus,
+ executionStatus,
timestamp: timestampInSeconds(),
...transaction,
}
@@ -54,7 +66,11 @@ const equalTransactionWithStatus = (
a: Transaction,
b: Transaction,
): boolean => {
- return compareTransactions(a, b) && a.status === b.status
+ return (
+ compareTransactions(a, b) &&
+ a.finalityStatus === b.finalityStatus &&
+ a.executionStatus === b.executionStatus
+ )
}
transactionsStore.subscribe((_, changeSet) => {
@@ -71,7 +87,11 @@ transactionsStore.subscribe((_, changeSet) => {
const oldTransaction = changeSet.oldValue?.find(
(oldTransaction) => oldTransaction.hash === newTransaction.hash,
)
- if (oldTransaction && oldTransaction.status !== newTransaction.status) {
+ if (
+ oldTransaction &&
+ (oldTransaction.finalityStatus !== newTransaction.finalityStatus ||
+ oldTransaction.executionStatus !== newTransaction.executionStatus)
+ ) {
return newTransaction
}
return []
diff --git a/packages/extension/src/background/transactions/tracking.ts b/packages/extension/src/background/transactions/tracking.ts
deleted file mode 100644
index 36358e79c..000000000
--- a/packages/extension/src/background/transactions/tracking.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { uniqWith } from "lodash-es"
-
-import { Transaction, getInFlightTransactions } from "../../shared/transactions"
-import { WalletAccount } from "../../shared/wallet.model"
-import { accountsEqual } from "../../shared/wallet.service"
-import { getTransactionsUpdate } from "./sources/onchain"
-import { getTransactionHistory } from "./sources/voyager"
-import { transactionsStore } from "./store"
-
-export interface TransactionTracker {
- loadHistory: (accountsToPopulate: WalletAccount[]) => Promise
- update: () => Promise
-}
-
-export const transactionTracker: TransactionTracker = {
- async loadHistory(accountsToPopulate: WalletAccount[]) {
- const allTransactions = await transactionsStore.get()
- const uniqAccounts = uniqWith(accountsToPopulate, accountsEqual)
- const historyTransactions = await getTransactionHistory(
- uniqAccounts,
- allTransactions,
- )
- return transactionsStore.push(historyTransactions)
- },
- async update() {
- const allTransactions = await transactionsStore.get()
- const updatedTransactions = await getTransactionsUpdate(
- // is smart enough to filter for just the pending transactions, as the rest needs no update
- allTransactions,
- )
- await transactionsStore.push(updatedTransactions)
- return getInFlightTransactions(allTransactions)
- },
-}
diff --git a/packages/extension/src/background/transactions/transactionAdapter.ts b/packages/extension/src/background/transactions/transactionAdapter.ts
new file mode 100644
index 000000000..c9816a4d5
--- /dev/null
+++ b/packages/extension/src/background/transactions/transactionAdapter.ts
@@ -0,0 +1,11 @@
+import { CallData, Call } from "starknet"
+import { Call as Callv4 } from "starknet4"
+
+export function transactionCallsAdapter(transactions: Call | Call[]): Callv4[] {
+ const calls = Array.isArray(transactions) ? transactions : [transactions]
+
+ return calls.map((tx) => ({
+ ...tx,
+ calldata: CallData.toCalldata(tx.calldata),
+ }))
+}
diff --git a/packages/extension/src/background/transactions/transactionExecution.ts b/packages/extension/src/background/transactions/transactionExecution.ts
index 1592e46b2..b8ce5771e 100644
--- a/packages/extension/src/background/transactions/transactionExecution.ts
+++ b/packages/extension/src/background/transactions/transactionExecution.ts
@@ -1,42 +1,45 @@
-import { BigNumber } from "ethers"
import {
- Account,
Call,
EstimateFee,
- TransactionBulk,
constants,
- number,
+ num,
stark,
+ TransactionFinalityStatus,
+ TransactionExecutionStatus,
} from "starknet"
-
import {
ExtQueueItem,
TransactionActionPayload,
} from "../../shared/actionQueue/types"
import { getL1GasPrice } from "../../shared/ethersUtils"
import { AllowArray } from "../../shared/storage/types"
-import { nameTransaction } from "../../shared/transactions"
+import {
+ ExtendedTransactionStatus,
+ TransactionRequest,
+ nameTransaction,
+} from "../../shared/transactions"
import { WalletAccount } from "../../shared/wallet.model"
-import { accountsEqual } from "../../shared/wallet.service"
+import { accountsEqual } from "../../shared/utils/accountsEqual"
import { isAccountDeployed } from "../accountDeploy"
import { analytics } from "../analytics"
-import { BackgroundService } from "../background"
import { getNonce, increaseStoredNonce, resetStoredNonce } from "../nonce"
import { argentMaxFee } from "../utils/argentMaxFee"
-import { getEstimatedFeeForMultisigTx } from "./fees/multisigFeeEstimation"
-import { getEstimatedFees } from "./fees/store"
+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"
export const checkTransactionHash = (
- transactionHash?: number.BigNumberish,
+ transactionHash?: num.BigNumberish,
account?: WalletAccount,
): boolean => {
try {
if (!transactionHash) {
throw Error("transactionHash not defined")
}
- const bn = number.toBN(transactionHash)
- if (bn.lte(constants.ZERO) && account?.type !== "multisig") {
+ const bn = num.toBigInt(transactionHash)
+ if (bn <= constants.ZERO && account?.type !== "multisig") {
throw Error("transactionHash needs to be >0")
}
return true
@@ -52,13 +55,24 @@ type TransactionAction = ExtQueueItem<{
export const executeTransactionAction = async (
action: TransactionAction,
- { wallet }: BackgroundService,
+ wallet: Wallet,
) => {
const { transactions, abis, transactionsDetail, meta = {} } = action.payload
const allTransactions = await transactionsStore.get()
const preComputedFees = await getEstimatedFees(transactions)
- analytics.track("executeTransaction", {
+ if (!preComputedFees) {
+ throw Error("PreComputedFees not defined")
+ }
+
+ const suggestedMaxFee =
+ transactionsDetail?.maxFee ?? preComputedFees.suggestedMaxFee
+ const suggestedMaxADFee = preComputedFees.maxADFee ?? "0"
+
+ const maxFee = argentMaxFee(suggestedMaxFee)
+ const maxADFee = argentMaxFee(suggestedMaxADFee)
+
+ void analytics.track("executeTransaction", {
usesCachedFees: Boolean(preComputedFees),
})
@@ -66,68 +80,46 @@ export const executeTransactionAction = async (
throw Error("you need an open session")
}
const selectedAccount = await wallet.getSelectedAccount()
+
if (!selectedAccount) {
throw Error("no accounts")
}
+ const multisig =
+ selectedAccount.type === "multisig"
+ ? await getMultisigAccountFromBaseWallet(selectedAccount)
+ : undefined
+
const pendingAccountTransactions = allTransactions.filter(
(tx) =>
- tx.status === "RECEIVED" && accountsEqual(tx.account, selectedAccount),
+ tx.finalityStatus === TransactionFinalityStatus.RECEIVED &&
+ tx.executionStatus !== TransactionExecutionStatus.REJECTED && // Rejected transactions have finality status RECEIVED
+ accountsEqual(tx.account, selectedAccount),
)
const hasUpgradePending = pendingAccountTransactions.some(
(tx) => tx.meta?.isUpgrade,
)
- const accountNeedsDeploy = selectedAccount.needsDeploy
-
const starknetAccount = await wallet.getStarknetAccount(
selectedAccount,
hasUpgradePending,
)
+ const accountNeedsDeploy = !(await isAccountDeployed(
+ selectedAccount,
+ starknetAccount.getClassAt.bind(starknetAccount),
+ ))
+
// if nonce doesnt get provided by the UI, we can use the stored nonce to allow transaction queueing
const nonceWasProvidedByUI = transactionsDetail?.nonce !== undefined // nonce can be a number of 0 therefore we need to check for undefined
const nonce = accountNeedsDeploy
- ? number.toHex(number.toBN(1))
+ ? num.toHex(1)
: nonceWasProvidedByUI
- ? number.toHex(number.toBN(transactionsDetail?.nonce || 0))
- : await getNonce(selectedAccount, wallet)
-
- let maxFee = preComputedFees?.suggestedMaxFee ?? "0"
- let maxADFee = preComputedFees?.maxADFee ?? "0"
+ ? num.toHex(transactionsDetail?.nonce || 0)
+ : await getNonce(selectedAccount, starknetAccount)
- if (
- selectedAccount.needsDeploy &&
- !(await isAccountDeployed(selectedAccount, starknetAccount.getClassAt))
- ) {
- if ("estimateFeeBulk" in starknetAccount) {
- const deployAccountPayload =
- selectedAccount.type === "multisig"
- ? await wallet.getMultisigDeploymentPayload(selectedAccount)
- : await wallet.getAccountDeploymentPayload(selectedAccount)
-
- const bulkTransactions: TransactionBulk = [
- {
- type: "DEPLOY_ACCOUNT",
- payload: deployAccountPayload,
- },
- {
- type: "INVOKE_FUNCTION",
- payload: transactions,
- },
- ]
- const estimateFeeBulk = await starknetAccount.estimateFeeBulk(
- bulkTransactions,
- )
-
- maxADFee =
- preComputedFees?.maxADFee ??
- argentMaxFee(estimateFeeBulk[0].suggestedMaxFee)
- maxFee =
- preComputedFees?.suggestedMaxFee ??
- argentMaxFee(estimateFeeBulk[1].suggestedMaxFee)
- }
+ if (accountNeedsDeploy) {
const { account, txHash } = await wallet.deployAccount(selectedAccount, {
maxFee: maxADFee,
})
@@ -137,7 +129,7 @@ export const executeTransactionAction = async (
)
}
- analytics.track("deployAccount", {
+ void analytics.track("deployAccount", {
status: "success",
trigger: "transaction",
networkId: account.networkId,
@@ -153,39 +145,17 @@ export const executeTransactionAction = async (
type: "DEPLOY_ACCOUNT",
},
})
- } else if (selectedAccount.type === "multisig") {
- const { suggestedMaxFee } = await getEstimatedFeeForMultisigTx(
- selectedAccount,
- transactions,
- nonce,
- )
-
- maxFee = argentMaxFee(suggestedMaxFee)
- } else {
- if (hasUpgradePending && !preComputedFees?.suggestedMaxFee) {
- const oldStarknetAccount = await wallet.getStarknetAccount(
- selectedAccount,
- false,
- )
- // Use old starknet account to calculate the max fee if upgrade is in progress
- const { suggestedMaxFee } = await oldStarknetAccount.estimateFee(
- transactions,
- )
- maxFee = argentMaxFee(suggestedMaxFee)
- } else if (!preComputedFees?.suggestedMaxFee) {
- // estimate fee with onchain nonce even tho transaction nonce may be different
- const { suggestedMaxFee } = await starknetAccount.estimateFee(
- transactions,
- )
-
- maxFee = argentMaxFee(suggestedMaxFee)
- }
}
const acc =
- selectedAccount.type === "multisig" && starknetAccount instanceof Account // Multisig uses latest account interface
- ? wallet.getStarknetAccountOfType(starknetAccount, "multisig")
+ selectedAccount.type !== "standard"
+ ? wallet.getStarknetAccountOfType(starknetAccount, selectedAccount.type)
: starknetAccount
+
+ if (!isAccountV5(acc)) {
+ throw new Error("Old Accounts are not supported anymore")
+ }
+
const transaction = await acc.execute(transactions, abis, {
...transactionsDetail,
nonce,
@@ -193,28 +163,34 @@ export const executeTransactionAction = async (
})
if (!checkTransactionHash(transaction.transaction_hash, selectedAccount)) {
- throw Error("Transaction could not get added to the sequencer")
+ throw new Error("Transaction could not get added to the sequencer")
}
const title = nameTransaction(transactions)
- // TODO: Remove this conditional as we now fallback to computed transactionHash for multisig
- // So we can always add the transaction to the queue. The added transaction will have
- // status "NOT_RECEIVED" until all the owners have signed the transaction
- if (selectedAccount.type !== "multisig") {
- await addTransaction({
- hash: transaction.transaction_hash,
- account: selectedAccount,
- meta: {
- ...meta,
- title,
- transactions,
- type: "INVOKE_FUNCTION",
- },
- })
+ const finalityStatus: ExtendedTransactionStatus =
+ multisig && multisig.threshold > 1
+ ? TransactionFinalityStatus.NOT_RECEIVED
+ : TransactionFinalityStatus.RECEIVED
+
+ const tx: TransactionRequest = {
+ hash: transaction.transaction_hash,
+ account: selectedAccount,
+ meta: {
+ ...meta,
+ title,
+ transactions,
+ type: meta.type ?? "INVOKE",
+ },
}
- if (!nonceWasProvidedByUI && selectedAccount.type !== "multisig") {
+ // Add transaction with finality status NOT_RECEIVED for multisig transactions with threshold > 1
+ await addTransaction(tx, finalityStatus)
+
+ if (
+ !nonceWasProvidedByUI &&
+ finalityStatus === TransactionFinalityStatus.RECEIVED
+ ) {
await increaseStoredNonce(selectedAccount)
}
@@ -229,7 +205,7 @@ export const calculateEstimateFeeFromL1Gas = async (
account: WalletAccount,
transactions: AllowArray,
): Promise => {
- const fallbackPrice = number.toBN(10e14)
+ const fallbackPrice = num.toBigInt(10e14)
try {
if (account.networkId === "localhost") {
console.log("Using fallback gas price for localhost")
@@ -242,12 +218,12 @@ export const calculateEstimateFeeFromL1Gas = async (
const l1GasPrice = await getL1GasPrice(account.networkId)
const callsLen = Array.isArray(transactions) ? transactions.length : 1
- const multiplier = BigNumber.from(3744)
+ const multiplier = BigInt(3744)
const price = l1GasPrice.mul(callsLen).mul(multiplier).toString()
return {
- overall_fee: number.toBN(price),
+ overall_fee: num.toBigInt(price),
suggestedMaxFee: stark.estimatedFeeToMaxFee(price),
}
} catch {
diff --git a/packages/extension/src/background/transactions/transactionMessaging.ts b/packages/extension/src/background/transactions/transactionMessaging.ts
index 2d029fada..a28f388d2 100644
--- a/packages/extension/src/background/transactions/transactionMessaging.ts
+++ b/packages/extension/src/background/transactions/transactionMessaging.ts
@@ -1,28 +1,44 @@
import {
- Account,
- InvocationsSignerDetails,
- TransactionBulk,
+ CallData,
+ Invocations,
+ TransactionType,
hash,
- number,
+ num,
stark,
+ transaction,
} from "starknet"
import { TransactionMessage } from "../../shared/messages/TransactionMessage"
+import {
+ SimulateDeployAccountRequest,
+ SimulateInvokeRequest,
+} from "../../shared/transactionSimulation/types"
+import { getErrorObject } from "../../shared/utils/error"
import { isAccountDeployed } from "../accountDeploy"
import { HandleMessage, UnhandledMessage } from "../background"
+import { isAccountV5 } from "../../shared/utils/accountv4"
import { argentMaxFee } from "../utils/argentMaxFee"
-import { getEstimatedFeeForMultisigTx } from "./fees/multisigFeeEstimation"
-import { addEstimatedFees } from "./fees/store"
+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"
export const handleTransactionMessage: HandleMessage<
TransactionMessage
-> = async ({ msg, background: { wallet, actionQueue }, respond: respond }) => {
+> = async ({ msg, origin, background: { wallet, actionService }, respond }) => {
switch (msg.type) {
case "EXECUTE_TRANSACTION": {
- const { meta } = await actionQueue.push({
- type: "TRANSACTION",
- payload: msg.data,
- })
+ const { meta } = await actionService.add(
+ {
+ type: "TRANSACTION",
+ payload: msg.data,
+ },
+ {
+ origin,
+ },
+ )
return respond({
type: "EXECUTE_TRANSACTION_RES",
data: { actionHash: meta.hash },
@@ -31,71 +47,74 @@ export const handleTransactionMessage: HandleMessage<
case "ESTIMATE_TRANSACTION_FEE": {
const selectedAccount = await wallet.getSelectedAccount()
- const starknetAccount = await wallet.getSelectedStarknetAccount()
const transactions = msg.data
+ const oldAccountTransactions = transactionCallsAdapter(transactions)
if (!selectedAccount) {
- throw Error("no accounts")
+ throw new AccountError({ code: "NOT_FOUND" })
}
+
+ const starknetAccount = await wallet.getSelectedStarknetAccount()
+
try {
let txFee = "0",
maxTxFee = "0",
accountDeploymentFee: string | undefined,
maxADFee: string | undefined
- if (
- selectedAccount.needsDeploy &&
- !(await isAccountDeployed(
- selectedAccount,
- starknetAccount.getClassAt,
- ))
- ) {
+ const isDeployed = await isAccountDeployed(
+ selectedAccount,
+ starknetAccount.getClassAt.bind(starknetAccount),
+ )
+
+ if (!isDeployed) {
if ("estimateFeeBulk" in starknetAccount) {
- const bulkTransactions: TransactionBulk = [
+ const bulkTransactions: Invocations = [
{
- type: "DEPLOY_ACCOUNT",
+ type: TransactionType.DEPLOY_ACCOUNT,
payload: await wallet.getAccountDeploymentPayload(
selectedAccount,
),
},
{
- type: "INVOKE_FUNCTION",
+ type: TransactionType.INVOKE,
payload: transactions,
},
]
const estimateFeeBulk = await starknetAccount.estimateFeeBulk(
bulkTransactions,
+ { skipValidate: true },
)
- accountDeploymentFee = number.toHex(estimateFeeBulk[0].overall_fee)
- txFee = number.toHex(estimateFeeBulk[1].overall_fee)
+ accountDeploymentFee = num.toHex(estimateFeeBulk[0].overall_fee)
+ txFee = num.toHex(estimateFeeBulk[1].overall_fee)
maxADFee = argentMaxFee(estimateFeeBulk[0].suggestedMaxFee)
maxTxFee = argentMaxFee(estimateFeeBulk[1].suggestedMaxFee)
}
- } else if (selectedAccount.type === "multisig") {
- const { overall_fee, suggestedMaxFee } =
- await getEstimatedFeeForMultisigTx(selectedAccount, transactions)
- txFee = number.toHex(overall_fee)
- maxTxFee = number.toHex(suggestedMaxFee)
} else {
- const { overall_fee, suggestedMaxFee } =
- await starknetAccount.estimateFee(transactions)
+ const { overall_fee, suggestedMaxFee } = isAccountV5(starknetAccount)
+ ? await starknetAccount.estimateFee(transactions, {
+ skipValidate: true,
+ })
+ : await starknetAccount.estimateFee(oldAccountTransactions)
- txFee = number.toHex(overall_fee)
- maxTxFee = number.toHex(suggestedMaxFee) // Here, maxFee = estimatedFee * 1.5x
+ txFee = num.toHex(overall_fee)
+ maxTxFee = num.toHex(suggestedMaxFee) // Here, maxFee = estimatedFee * 1.5x
}
const suggestedMaxFee = argentMaxFee(maxTxFee)
- addEstimatedFees({
- amount: txFee,
- suggestedMaxFee,
- accountDeploymentFee,
- maxADFee,
+ await addEstimatedFees(
+ {
+ amount: txFee,
+ suggestedMaxFee,
+ accountDeploymentFee,
+ maxADFee,
+ },
transactions,
- })
+ )
return respond({
type: "ESTIMATE_TRANSACTION_FEE_RES",
data: {
@@ -106,14 +125,12 @@ export const handleTransactionMessage: HandleMessage<
},
})
} catch (error) {
- console.error(error)
+ const errorObject = getErrorObject(error, false)
+ console.error("ESTIMATE_TRANSACTION_FEE_REJ", error, errorObject)
return respond({
type: "ESTIMATE_TRANSACTION_FEE_REJ",
data: {
- error:
- (error as any)?.message?.toString?.() ??
- (error as any)?.toString?.() ??
- "Unkown error",
+ error: errorObject,
},
})
}
@@ -133,14 +150,14 @@ export const handleTransactionMessage: HandleMessage<
const { overall_fee, suggestedMaxFee } =
await wallet.getAccountDeploymentFee(account)
- const maxADFee = number.toHex(
+ const maxADFee = num.toHex(
stark.estimatedFeeToMaxFee(suggestedMaxFee, 1), // This adds the 3x overhead. i.e: suggestedMaxFee = maxFee * 2x = estimatedFee * 3x
)
return respond({
type: "ESTIMATE_ACCOUNT_DEPLOYMENT_FEE_RES",
data: {
- amount: number.toHex(overall_fee),
+ amount: num.toHex(overall_fee),
maxADFee,
},
})
@@ -148,36 +165,34 @@ export const handleTransactionMessage: HandleMessage<
// FIXME: This is a temporary fix for the case where the user has a multisig account.
// Once starknet 0.11 is released, we can remove this.
if (account.type === "multisig") {
- const fallbackPrice = number.toBN(10e14)
+ const fallbackPrice = num.toBigInt(10e14)
return respond({
type: "ESTIMATE_ACCOUNT_DEPLOYMENT_FEE_RES",
data: {
- amount: number.toHex(fallbackPrice),
+ amount: num.toHex(fallbackPrice),
maxADFee: argentMaxFee(fallbackPrice),
},
})
}
- console.error(error)
+ const errorObject = getErrorObject(error, false)
+ console.error("ESTIMATE_ACCOUNT_DEPLOYMENT_FEE_REJ", error, errorObject)
return respond({
type: "ESTIMATE_ACCOUNT_DEPLOYMENT_FEE_REJ",
data: {
- error:
- (error as any)?.message?.toString?.() ??
- (error as any)?.toString?.() ??
- "Unkown error",
+ error: errorObject,
},
})
}
}
case "ESTIMATE_DECLARE_CONTRACT_FEE": {
- const { classHash, contract, ...restData } = msg.data
+ const { address, networkId, ...rest } = msg.data
const selectedAccount = await wallet.getSelectedAccount()
const selectedStarknetAccount =
- "address" in restData
- ? await wallet.getStarknetAccount(restData)
+ address && networkId
+ ? await wallet.getStarknetAccount({ address, networkId })
: await wallet.getSelectedStarknetAccount()
if (!selectedStarknetAccount) {
@@ -194,7 +209,7 @@ export const handleTransactionMessage: HandleMessage<
selectedAccount?.needsDeploy &&
!(await isAccountDeployed(
selectedAccount,
- selectedStarknetAccount.getClassAt,
+ selectedStarknetAccount.getClassAt.bind(selectedStarknetAccount),
))
) {
if ("estimateFeeBulk" in selectedStarknetAccount) {
@@ -202,38 +217,38 @@ export const handleTransactionMessage: HandleMessage<
selectedAccount.type === "multisig"
? await wallet.getMultisigDeploymentPayload(selectedAccount)
: await wallet.getAccountDeploymentPayload(selectedAccount)
- const bulkTransactions: TransactionBulk = [
+ const bulkTransactions: Invocations = [
{
- type: "DEPLOY_ACCOUNT",
+ type: TransactionType.DEPLOY_ACCOUNT,
payload: deployPayload,
},
{
- type: "DECLARE",
+ type: TransactionType.DECLARE,
payload: {
- classHash,
- contract,
+ ...rest,
},
},
]
const estimateFeeBulk =
- await selectedStarknetAccount.estimateFeeBulk(bulkTransactions)
+ await selectedStarknetAccount.estimateFeeBulk(bulkTransactions, {
+ skipValidate: true,
+ })
- accountDeploymentFee = number.toHex(estimateFeeBulk[0].overall_fee)
- txFee = number.toHex(estimateFeeBulk[1].overall_fee)
+ accountDeploymentFee = num.toHex(estimateFeeBulk[0].overall_fee)
+ txFee = num.toHex(estimateFeeBulk[1].overall_fee)
maxADFee = argentMaxFee(estimateFeeBulk[0].suggestedMaxFee)
- maxTxFee = estimateFeeBulk[1].suggestedMaxFee
+ maxTxFee = estimateFeeBulk[1].suggestedMaxFee.toString()
}
} else {
if ("estimateDeclareFee" in selectedStarknetAccount) {
const { overall_fee, suggestedMaxFee } =
await selectedStarknetAccount.estimateDeclareFee({
- classHash,
- contract,
+ ...rest,
})
- txFee = number.toHex(overall_fee)
- maxTxFee = number.toHex(suggestedMaxFee)
+ txFee = num.toHex(overall_fee)
+ maxTxFee = num.toHex(suggestedMaxFee)
} else {
throw Error("estimateDeclareFee not supported")
}
@@ -284,19 +299,19 @@ export const handleTransactionMessage: HandleMessage<
selectedAccount?.needsDeploy &&
!(await isAccountDeployed(
selectedAccount,
- selectedStarknetAccount.getClassAt,
+ selectedStarknetAccount.getClassAt.bind(selectedStarknetAccount),
))
) {
if ("estimateFeeBulk" in selectedStarknetAccount) {
- const bulkTransactions: TransactionBulk = [
+ const bulkTransactions: Invocations = [
{
- type: "DEPLOY_ACCOUNT",
+ type: TransactionType.DEPLOY_ACCOUNT,
payload: await wallet.getAccountDeploymentPayload(
selectedAccount,
),
},
{
- type: "DEPLOY",
+ type: TransactionType.DEPLOY,
payload: {
classHash,
salt,
@@ -309,11 +324,11 @@ export const handleTransactionMessage: HandleMessage<
const estimateFeeBulk =
await selectedStarknetAccount.estimateFeeBulk(bulkTransactions)
- accountDeploymentFee = number.toHex(estimateFeeBulk[0].overall_fee)
- txFee = number.toHex(estimateFeeBulk[1].overall_fee)
+ accountDeploymentFee = num.toHex(estimateFeeBulk[0].overall_fee)
+ txFee = num.toHex(estimateFeeBulk[1].overall_fee)
maxADFee = argentMaxFee(estimateFeeBulk[0].suggestedMaxFee)
- maxTxFee = estimateFeeBulk[1].suggestedMaxFee
+ maxTxFee = estimateFeeBulk[1].suggestedMaxFee.toString()
}
} else {
if ("estimateDeployFee" in selectedStarknetAccount) {
@@ -324,8 +339,8 @@ export const handleTransactionMessage: HandleMessage<
unique,
constructorCalldata,
})
- txFee = number.toHex(overall_fee)
- maxTxFee = number.toHex(suggestedMaxFee)
+ txFee = num.toHex(overall_fee)
+ maxTxFee = num.toHex(suggestedMaxFee)
} else {
throw Error("estimateDeployFee not supported")
}
@@ -361,64 +376,81 @@ export const handleTransactionMessage: HandleMessage<
try {
const selectedAccount = await wallet.getSelectedAccount()
- const starknetAccount =
- (await wallet.getSelectedStarknetAccount()) as Account // Old accounts are not supported
-
if (!selectedAccount) {
- throw Error("no accounts")
+ throw new AccountError({ code: "NOT_FOUND" })
}
+ const starknetAccount = await wallet.getSelectedStarknetAccount()
- const nonce = await starknetAccount.getNonce()
-
- const chainId = starknetAccount.chainId
+ if (!isAccountV5(starknetAccount)) {
+ // Old accounts are not supported
+ return respond({
+ type: "SIMULATE_TRANSACTION_INVOCATION_RES",
+ data: null,
+ })
+ }
- const version = number.toHex(hash.feeTransactionVersion)
+ let nonce
- const signerDetails: InvocationsSignerDetails = {
- walletAddress: starknetAccount.address,
- nonce,
- maxFee: 0,
- version,
- chainId,
+ try {
+ nonce = await starknetAccount.getNonce()
+ } catch {
+ nonce = "0"
}
- // TODO: Use this when Simulate Transaction allows multiple transaction types
- // const signerDetailsWithZeroNonce = {
- // ...signerDetails,
- // nonce: 0,
- // }
+ const chainId = await starknetAccount.getChainId()
+
+ const version = num.toHex(hash.feeTransactionVersion)
- // const accountDeployPayload = await wallet.getAccountDeploymentPayload(
- // selectedAccount,
- // )
+ const calldata = transaction.getExecuteCalldata(
+ transactions,
+ starknetAccount.cairoVersion,
+ )
- // const accountDeployInvocation =
- // await starknetAccount.buildAccountDeployPayload(
- // accountDeployPayload,
- // signerDetailsWithZeroNonce,
- // )
+ let accountDeployTransaction: SimulateDeployAccountRequest | null = null
- const { contractAddress, calldata, signature } =
- await starknetAccount.buildInvocation(transactions, signerDetails)
+ const isDeployed = await isAccountDeployed(
+ selectedAccount,
+ starknetAccount.getClassAt.bind(starknetAccount),
+ )
- const invocation = {
- type: "INVOKE_FUNCTION" as const,
- contract_address: contractAddress,
+ const invokeTransactions: SimulateInvokeRequest = {
+ type: TransactionType.INVOKE,
+ sender_address: selectedAccount.address,
calldata,
- signature,
- nonce,
+ signature: [],
+ nonce: isDeployed ? num.toHex(nonce) : num.toHex(1),
version,
}
+ if (!isDeployed) {
+ const accountDeployPayload = await wallet.getAccountDeploymentPayload(
+ selectedAccount,
+ )
+
+ accountDeployTransaction = {
+ type: TransactionType.DEPLOY_ACCOUNT,
+ calldata: CallData.toCalldata(
+ accountDeployPayload.constructorCalldata,
+ ),
+ classHash: num.toHex(accountDeployPayload.classHash),
+ salt: num.toHex(accountDeployPayload.addressSalt || 0),
+ nonce: num.toHex(0),
+ version: num.toHex(version),
+ signature: [],
+ }
+ }
+
return respond({
type: "SIMULATE_TRANSACTION_INVOCATION_RES",
data: {
- invocation,
+ transactions: accountDeployTransaction
+ ? [accountDeployTransaction, invokeTransactions]
+ : [invokeTransactions],
chainId,
},
})
} catch (error) {
- console.log(error)
+ console.error("SIMULATE_TRANSACTION_INVOCATION_REJ", error)
return respond({
type: "SIMULATE_TRANSACTION_INVOCATION_REJ",
data: {
@@ -431,42 +463,117 @@ export const handleTransactionMessage: HandleMessage<
}
}
- case "TRANSACTION_FAILED": {
- return await actionQueue.remove(msg.data.actionHash)
- }
+ case "SIMULATE_TRANSACTIONS": {
+ const transactions = Array.isArray(msg.data) ? msg.data : [msg.data]
- case "SIMULATE_TRANSACTION_FALLBACK": {
- const selectedAccount = await wallet.getSelectedAccount()
- const starknetAccount =
- (await wallet.getSelectedStarknetAccount()) as Account // Old accounts are not supported
+ try {
+ const selectedAccount = await wallet.getSelectedAccount()
+ if (!selectedAccount) {
+ throw new AccountError({ code: "NOT_FOUND" })
+ }
+ const starknetAccount = await wallet.getSelectedStarknetAccount()
- if (!selectedAccount) {
- throw Error("no accounts")
- }
+ if (!isAccountV5(starknetAccount)) {
+ // Old accounts are not supported
+ return respond({
+ type: "SIMULATE_TRANSACTION_INVOCATION_RES",
+ data: null,
+ })
+ }
- const nonce = await starknetAccount.getNonce()
+ let nonce
- try {
- const simulated = await starknetAccount.simulateTransaction(msg.data, {
- nonce,
+ try {
+ nonce = await starknetAccount.getNonce()
+ } catch {
+ nonce = "0"
+ }
+
+ const chainId = await starknetAccount.getChainId()
+
+ const version = num.toHex(hash.feeTransactionVersion)
+
+ const calldata = transaction.getExecuteCalldata(
+ transactions,
+ starknetAccount.cairoVersion,
+ )
+
+ let accountDeployTransaction: SimulateDeployAccountRequest | null = null
+
+ const isDeployed = await isAccountDeployed(
+ selectedAccount,
+ starknetAccount.getClassAt.bind(starknetAccount),
+ )
+
+ const invokeTransactions: SimulateInvokeRequest = {
+ type: TransactionType.INVOKE,
+ sender_address: selectedAccount.address,
+ calldata,
+ signature: [],
+ nonce: isDeployed ? num.toHex(nonce) : num.toHex(1),
+ version,
+ }
+
+ if (!isDeployed) {
+ const accountDeployPayload = await wallet.getAccountDeploymentPayload(
+ selectedAccount,
+ )
+
+ accountDeployTransaction = {
+ type: TransactionType.DEPLOY_ACCOUNT,
+ calldata: CallData.toCalldata(
+ accountDeployPayload.constructorCalldata,
+ ),
+ classHash: num.toHex(accountDeployPayload.classHash),
+ salt: num.toHex(accountDeployPayload.addressSalt || 0),
+ nonce: num.toHex(0),
+ version: num.toHex(version),
+ signature: [],
+ }
+ }
+
+ const invocations = accountDeployTransaction
+ ? [accountDeployTransaction, invokeTransactions]
+ : [invokeTransactions]
+
+ const result = await fetchTransactionBulkSimulation({
+ invocations,
+ chainId,
})
+ const estimatedFee = getEstimatedFeeFromSimulation(result)
+
+ let simulationWithFees = null
+
+ if (result) {
+ await addEstimatedFees(estimatedFee, transactions)
+ simulationWithFees = {
+ simulation: result,
+ feeEstimation: estimatedFee,
+ }
+ }
+
return respond({
- type: "SIMULATE_TRANSACTION_FALLBACK_RES",
- data: simulated,
+ type: "SIMULATE_TRANSACTIONS_RES",
+ data: simulationWithFees,
})
} catch (error) {
+ console.error("SIMULATE_TRANSACTIONS_REJ", error)
return respond({
- type: "SIMULATE_TRANSACTION_FALLBACK_REJ",
+ type: "SIMULATE_TRANSACTIONS_REJ",
data: {
- error:
- (error as any)?.message?.toString() ??
- (error as any)?.toString() ??
- "Unkown error",
+ error: new TransactionError({
+ code: "SIMULATION_ERROR",
+ message: `${error}`,
+ }),
},
})
}
}
+
+ case "TRANSACTION_FAILED": {
+ return await actionService.remove(msg.data.actionHash)
+ }
}
throw new UnhandledMessage()
}
diff --git a/packages/extension/src/background/transactions/transformers.ts b/packages/extension/src/background/transactions/transformers.ts
index 71cacccdf..3ab1dfb02 100644
--- a/packages/extension/src/background/transactions/transformers.ts
+++ b/packages/extension/src/background/transactions/transformers.ts
@@ -1,6 +1,6 @@
import { Transaction } from "../../shared/transactions"
import { WalletAccount } from "../../shared/wallet.model"
-import { VoyagerTransaction } from "./sources/voyager"
+import { VoyagerTransaction } from "./sources/voyager.model"
export const mapVoyagerTransactionToTransaction = (
transaction: VoyagerTransaction,
@@ -10,6 +10,6 @@ export const mapVoyagerTransactionToTransaction = (
hash: transaction.hash,
account,
meta,
- status: transaction.status,
+ finalityStatus: transaction.status as any,
timestamp: transaction.timestamp,
})
diff --git a/packages/extension/src/background/udcAction.ts b/packages/extension/src/background/udcAction.ts
index 69ff7e326..40bd437b7 100644
--- a/packages/extension/src/background/udcAction.ts
+++ b/packages/extension/src/background/udcAction.ts
@@ -1,20 +1,24 @@
import {
+ CallData,
DeclareContractPayload,
- TransactionBulk,
+ Invocations,
+ TransactionType,
UniversalDeployerContractPayload,
constants,
- number,
- stark,
+ num,
} from "starknet"
import { ExtQueueItem } from "../shared/actionQueue/types"
import { isAccountDeployed } from "./accountDeploy"
import { analytics } from "./analytics"
-import { BackgroundService } from "./background"
import { getNonce, increaseStoredNonce } from "./nonce"
import { addTransaction } from "./transactions/store"
import { checkTransactionHash } from "./transactions/transactionExecution"
import { argentMaxFee } from "./utils/argentMaxFee"
+import { Wallet } from "./wallet"
+import { AccountError } from "../shared/errors/account"
+import { WalletError } from "../shared/errors/wallet"
+import { UdcError } from "../shared/errors/udc"
const { UDC } = constants
@@ -35,14 +39,14 @@ export enum UdcTransactionType {
export const udcDeclareContract = async (
{ payload }: DeclareContractAction,
- { wallet }: BackgroundService,
+ wallet: Wallet,
) => {
if (!(await wallet.isSessionOpen())) {
- throw Error("you need an open session")
+ throw new WalletError({ code: "NO_SESSION_OPEN" })
}
const selectedAccount = await wallet.getSelectedAccount()
if (!selectedAccount) {
- throw new Error("No account selected")
+ throw new AccountError({ code: "NOT_SELECTED" })
}
const starknetAccount = await wallet.getStarknetAccount({
@@ -54,29 +58,30 @@ export const udcDeclareContract = async (
let maxDeclareFee = "0"
const declareNonce = selectedAccount.needsDeploy
- ? number.toHex(number.toBN(1))
- : await getNonce(selectedAccount, wallet)
+ ? num.toHex(1)
+ : await getNonce(selectedAccount, starknetAccount)
if (
selectedAccount.needsDeploy &&
- !(await isAccountDeployed(selectedAccount, starknetAccount.getClassAt))
+ !(await isAccountDeployed(
+ selectedAccount,
+ starknetAccount.getClassAt.bind(starknetAccount),
+ ))
) {
if ("estimateFeeBulk" in starknetAccount) {
- const bulkTransactions: TransactionBulk = [
+ const bulkTransactions: Invocations = [
{
- type: "DEPLOY_ACCOUNT",
+ type: TransactionType.DEPLOY_ACCOUNT,
payload: await wallet.getAccountDeploymentPayload(selectedAccount),
},
{
- type: "DECLARE",
- payload: {
- classHash: payload.classHash,
- contract: payload.contract,
- },
+ type: TransactionType.DECLARE,
+ payload,
},
]
const estimateFeeBulk = await starknetAccount.estimateFeeBulk(
bulkTransactions,
+ { skipValidate: true },
)
maxADFee = argentMaxFee(estimateFeeBulk[0].suggestedMaxFee)
@@ -90,9 +95,7 @@ export const udcDeclareContract = async (
)
if (!checkTransactionHash(accountDeployTxHash)) {
- throw Error(
- "Deploy Account Transaction could not get added to the sequencer",
- )
+ throw new UdcError({ code: "DEPLOY_TX_NOT_ADDED" })
}
analytics.track("deployAccount", {
@@ -107,18 +110,15 @@ export const udcDeclareContract = async (
meta: {
title: "Activate Account",
isDeployAccount: true,
- type: "DEPLOY_ACCOUNT",
+ type: TransactionType.DEPLOY_ACCOUNT,
},
})
} else {
if ("getSuggestedMaxFee" in starknetAccount) {
const suggestedMaxFee = await starknetAccount.getSuggestedMaxFee(
{
- type: "DECLARE",
- payload: {
- classHash: payload.classHash,
- contract: payload.contract,
- },
+ type: TransactionType.DECLARE,
+ payload,
},
{
nonce: declareNonce,
@@ -126,29 +126,19 @@ export const udcDeclareContract = async (
)
maxDeclareFee = argentMaxFee(suggestedMaxFee)
} else {
- throw Error("Account does not support Starknet Declare Fee")
+ throw new UdcError({ code: "NO_STARKNET_DECLARE_FEE" })
}
}
- if ("declare" in starknetAccount) {
- const { classHash, contract } = payload
-
+ if ("declareIfNot" in starknetAccount) {
const { transaction_hash: declareTxHash, class_hash: deployedClassHash } =
- await starknetAccount.declare(
- {
- classHash,
- contract,
- },
- {
- nonce: declareNonce,
- maxFee: maxDeclareFee,
- },
- )
+ await starknetAccount.declareIfNot(payload, {
+ nonce: declareNonce,
+ maxFee: maxDeclareFee,
+ })
if (!checkTransactionHash(declareTxHash)) {
- throw Error(
- "Deploy Account Transaction could not be added to the sequencer",
- )
+ throw new UdcError({ code: "DEPLOY_TX_NOT_ADDED" })
}
await increaseStoredNonce(selectedAccount)
@@ -158,7 +148,7 @@ export const udcDeclareContract = async (
account: selectedAccount,
meta: {
title: "Contract declared",
- subTitle: classHash.toString(),
+ subTitle: payload.classHash || payload.compiledClassHash,
type: UdcTransactionType.DECLARE_CONTRACT,
transactions: {
contractAddress: UDC.ADDRESS,
@@ -169,21 +159,20 @@ export const udcDeclareContract = async (
return { txHash: declareTxHash, classHash: deployedClassHash }
}
-
- throw Error("Account does not support Starknet declare")
+ throw new UdcError({ code: "NO_STARKNET_DECLARE" })
}
export const udcDeployContract = async (
{ payload }: DeployContractAction,
- { wallet }: BackgroundService,
+ wallet: Wallet,
) => {
if (!(await wallet.isSessionOpen())) {
- throw Error("you need an open session")
+ throw new WalletError({ code: "NO_SESSION_OPEN" })
}
const selectedAccount = await wallet.getSelectedAccount()
if (!selectedAccount) {
- throw new Error("No account selected")
+ throw new AccountError({ code: "NOT_SELECTED" })
}
const starknetAccount = await wallet.getStarknetAccount({
@@ -195,21 +184,24 @@ export const udcDeployContract = async (
let maxDeployFee = "0"
const deployNonce = selectedAccount.needsDeploy
- ? number.toHex(number.toBN(1))
- : await getNonce(selectedAccount, wallet)
+ ? num.toHex(num.toBigInt(1))
+ : await getNonce(selectedAccount, starknetAccount)
if (
selectedAccount.needsDeploy &&
- !(await isAccountDeployed(selectedAccount, starknetAccount.getClassAt))
+ !(await isAccountDeployed(
+ selectedAccount,
+ starknetAccount.getClassAt.bind(starknetAccount),
+ ))
) {
if ("estimateFeeBulk" in starknetAccount) {
- const bulkTransactions: TransactionBulk = [
+ const bulkTransactions: Invocations = [
{
- type: "DEPLOY_ACCOUNT",
+ type: TransactionType.DEPLOY_ACCOUNT,
payload: await wallet.getAccountDeploymentPayload(selectedAccount),
},
{
- type: "DEPLOY",
+ type: TransactionType.DEPLOY,
payload: {
classHash: payload.classHash,
constructorCalldata: payload.constructorCalldata,
@@ -220,6 +212,7 @@ export const udcDeployContract = async (
]
const estimateFeeBulk = await starknetAccount.estimateFeeBulk(
bulkTransactions,
+ { skipValidate: true },
)
maxADFee = argentMaxFee(estimateFeeBulk[0].suggestedMaxFee)
@@ -233,9 +226,7 @@ export const udcDeployContract = async (
)
if (!checkTransactionHash(accountDeployTxHash)) {
- throw Error(
- "Deploy Account Transaction could not get added to the sequencer",
- )
+ throw new UdcError({ code: "DEPLOY_TX_NOT_ADDED" })
}
analytics.track("deployAccount", {
@@ -257,7 +248,7 @@ export const udcDeployContract = async (
if ("getSuggestedMaxFee" in starknetAccount) {
const suggestedMaxFee = await starknetAccount.getSuggestedMaxFee(
{
- type: "DEPLOY",
+ type: TransactionType.DEPLOY,
payload: {
classHash: payload.classHash,
constructorCalldata: payload.constructorCalldata,
@@ -271,7 +262,7 @@ export const udcDeployContract = async (
)
maxDeployFee = argentMaxFee(suggestedMaxFee)
} else {
- throw Error("Account does not support Starknet Deploy Fee")
+ throw new UdcError({ code: "NO_STARKNET_DECLARE_FEE" })
}
}
@@ -279,9 +270,7 @@ export const udcDeployContract = async (
const { classHash, salt, unique, constructorCalldata } = payload
// make sure contract hashes can be calculated before submitting onchain
- const compiledConstructorCallData = stark.compileCalldata(
- constructorCalldata || [],
- )
+ const compiledConstructorCallData = CallData.toCalldata(constructorCalldata)
// submit onchain
const { transaction_hash: deployTxHash, contract_address } =
@@ -299,9 +288,7 @@ export const udcDeployContract = async (
)
if (!checkTransactionHash(deployTxHash)) {
- throw Error(
- "Deploy Account Transaction could not be added to the sequencer",
- )
+ throw new UdcError({ code: "DEPLOY_TX_NOT_ADDED" })
}
const contractAddress = contract_address[0]
@@ -326,5 +313,5 @@ export const udcDeployContract = async (
return { txHash: deployTxHash, contractAddress }
}
- throw Error("Account does not support Starknet declare")
+ throw new UdcError({ code: "NO_STARKNET_DECLARE" })
}
diff --git a/packages/extension/src/background/udcMessaging.ts b/packages/extension/src/background/udcMessaging.ts
index f80b724b5..3d6ece6ad 100644
--- a/packages/extension/src/background/udcMessaging.ts
+++ b/packages/extension/src/background/udcMessaging.ts
@@ -1,31 +1,36 @@
import { UdcMessage } from "../shared/messages/UdcMessage"
-import { getNetwork } from "../shared/network"
-import { getProvider } from "../shared/network/provider"
+
import { HandleMessage, UnhandledMessage } from "./background"
export const handleUdcMessaging: HandleMessage = async ({
msg,
- background: { actionQueue, wallet },
+ origin,
+ background: { wallet, actionService },
respond,
}) => {
switch (msg.type) {
+ // TODO: refactor after we have a plan for inpage
case "REQUEST_DECLARE_CONTRACT": {
const { data } = msg
- const { classHash, contract, ...restData } = data
- if ("address" in restData && "networkId" in restData) {
+ const { address, networkId, ...rest } = data
+ if (address && networkId) {
await wallet.selectAccount({
- address: restData.address,
- networkId: restData.networkId,
+ address,
+ networkId,
})
}
- const action = await actionQueue.push({
- type: "DECLARE_CONTRACT_ACTION",
- payload: {
- classHash,
- contract,
+ const action = await actionService.add(
+ {
+ type: "DECLARE_CONTRACT_ACTION",
+ payload: {
+ ...rest,
+ },
},
- })
+ {
+ origin,
+ },
+ )
return respond({
type: "REQUEST_DECLARE_CONTRACT_RES",
@@ -35,35 +40,7 @@ export const handleUdcMessaging: HandleMessage = async ({
})
}
- case "FETCH_CONSTRUCTOR_PARAMS": {
- const {
- data: { networkId, classHash },
- } = msg
-
- const network = await getNetwork(networkId)
- const provider = getProvider(network)
-
- try {
- if ("getClassByHash" in provider) {
- const contract = await provider.getClassByHash(classHash)
- return respond({
- type: "FETCH_CONSTRUCTOR_PARAMS_RES",
- data: {
- contract,
- },
- })
- }
- } catch (error) {
- return respond({
- type: "FETCH_CONSTRUCTOR_PARAMS_REJ",
- data: {
- error: `${error}`,
- },
- })
- }
- return
- }
-
+ // TODO: refactor after refactoring actionHandlers
case "REQUEST_DEPLOY_CONTRACT": {
const { data } = msg
const {
@@ -76,15 +53,20 @@ export const handleUdcMessaging: HandleMessage = async ({
} = data
await wallet.selectAccount({ address, networkId })
- const action = await actionQueue.push({
- type: "DEPLOY_CONTRACT_ACTION",
- payload: {
- classHash: classHash.toString(),
- constructorCalldata,
- salt,
- unique,
+ const action = await actionService.add(
+ {
+ type: "DEPLOY_CONTRACT_ACTION",
+ payload: {
+ classHash: classHash.toString(),
+ constructorCalldata,
+ salt,
+ unique,
+ },
},
- })
+ {
+ origin,
+ },
+ )
return respond({
type: "REQUEST_DEPLOY_CONTRACT_RES",
diff --git a/packages/extension/src/background/utils/argentMaxFee.ts b/packages/extension/src/background/utils/argentMaxFee.ts
index 3e40f76e1..291379aec 100644
--- a/packages/extension/src/background/utils/argentMaxFee.ts
+++ b/packages/extension/src/background/utils/argentMaxFee.ts
@@ -1,4 +1,4 @@
-import { number, stark } from "starknet"
+import { num, stark } from "starknet"
/**
*
@@ -8,5 +8,5 @@ import { number, stark } from "starknet"
* @returns argentMaxFee: fee calculated by argent x overhead. argentMaxFee = suggestedMaxFee * 2 = overall_fee * 3
*
* */
-export const argentMaxFee = (suggestedMaxFee: number.BigNumberish) =>
- number.toHex(stark.estimatedFeeToMaxFee(suggestedMaxFee, 1))
+export const argentMaxFee = (suggestedMaxFee: num.BigNumberish) =>
+ num.toHex(stark.estimatedFeeToMaxFee(suggestedMaxFee, 1))
diff --git a/packages/extension/src/background/utils/uniqueSet.test.ts b/packages/extension/src/background/utils/uniqueSet.test.ts
new file mode 100644
index 000000000..f351a303b
--- /dev/null
+++ b/packages/extension/src/background/utils/uniqueSet.test.ts
@@ -0,0 +1,67 @@
+import { describe, beforeEach, expect } from "vitest"
+import { UniqueSet } from "./uniqueSet"
+
+// Define a simple object structure for testing
+type TestObject = {
+ id: string
+ name: string
+}
+
+describe("UniqueSet", () => {
+ let uniqueSet: UniqueSet
+
+ beforeEach(() => {
+ // Initialize the UniqueSet with a function to get the identifier
+ uniqueSet = new UniqueSet((a: TestObject) => a.id)
+ })
+
+ test("add() and has()", async () => {
+ const item: TestObject = { id: "1", name: "test" }
+ uniqueSet.add(item)
+
+ expect(uniqueSet.has("1")).toBe(true)
+ expect(uniqueSet.has("2")).toBe(false)
+ })
+
+ test("get()", async () => {
+ const item: TestObject = { id: "1", name: "test" }
+ uniqueSet.add(item)
+
+ expect(uniqueSet.get("1")).toBe(item)
+ expect(uniqueSet.get("2")).toBe(undefined)
+ })
+
+ test("getAll()", async () => {
+ const item1: TestObject = { id: "1", name: "test1" }
+ const item2: TestObject = { id: "2", name: "test2" }
+ uniqueSet.add(item1)
+ uniqueSet.add(item2)
+
+ const values = uniqueSet.getAll()
+
+ expect(values.length).toBe(2)
+ expect(values.includes(item1)).toBe(true)
+ expect(values.includes(item2)).toBe(true)
+ })
+
+ test("delete()", async () => {
+ const item: TestObject = { id: "1", name: "test" }
+ uniqueSet.add(item)
+
+ expect(uniqueSet.delete("1")).toBe(true)
+ expect(uniqueSet.delete("2")).toBe(false)
+ expect(uniqueSet.has("1")).toBe(false)
+ expect(uniqueSet.has("2")).toBe(false)
+ })
+
+ test("add() updates existing items", async () => {
+ const item1: TestObject = { id: "1", name: "test1" }
+ const item2: TestObject = { id: "1", name: "test2" } // same id, different name
+ uniqueSet.add(item1)
+ uniqueSet.add(item2)
+
+ const retrievedItem = uniqueSet.get("1")
+
+ expect(retrievedItem).toBe(item2)
+ })
+})
diff --git a/packages/extension/src/background/utils/uniqueSet.ts b/packages/extension/src/background/utils/uniqueSet.ts
new file mode 100644
index 000000000..4d5e33de2
--- /dev/null
+++ b/packages/extension/src/background/utils/uniqueSet.ts
@@ -0,0 +1,25 @@
+export class UniqueSet {
+ protected innerSet = new Map()
+
+ constructor(private readonly getIdentifier: (a: T) => K) {}
+
+ get(key: K) {
+ return this.innerSet.get(key)
+ }
+
+ getAll() {
+ return Array.from(this.innerSet.values())
+ }
+
+ add(value: T) {
+ this.innerSet.set(this.getIdentifier(value), value)
+ }
+
+ has(key: K) {
+ return this.innerSet.has(key)
+ }
+
+ delete(key: K) {
+ return this.innerSet.delete(key)
+ }
+}
diff --git a/packages/extension/src/background/wallet.ts b/packages/extension/src/background/wallet.ts
deleted file mode 100644
index 18b9dff23..000000000
--- a/packages/extension/src/background/wallet.ts
+++ /dev/null
@@ -1,1283 +0,0 @@
-import { ethers, utils } from "ethers"
-import { ProgressCallback } from "ethers/lib/utils"
-import {
- find,
- isEmpty,
- memoize,
- noop,
- partition,
- throttle,
- union,
-} from "lodash-es"
-import {
- Account,
- DeployAccountContractPayload,
- DeployAccountContractTransaction,
- EstimateFee,
- InvocationsDetails,
- KeyPair,
- SignerInterface,
- ec,
- hash,
- number,
- stark,
-} from "starknet"
-import { Account as Accountv4 } from "starknet4"
-import browser from "webextension-polyfill"
-
-import { updateAccountsWithNames } from "./../shared/account/details/updateAccountsWithNames"
-import { sortByDerivationPath } from "./../shared/utils/accountsMultisigSort"
-import {
- ArgentAccountType,
- BaseMultisigWalletAccount,
- CreateAccountType,
- CreateWalletAccount,
- MultisigData,
- MultisigWalletAccount,
-} from "./../shared/wallet.model"
-import { getAccountEscapeFromChain } from "../shared/account/details/getAccountEscapeFromChain"
-import { getAccountGuardiansFromChain } from "../shared/account/details/getAccountGuardiansFromChain"
-import { getAccountTypesFromChain } from "../shared/account/details/getAccountTypesFromChain"
-import {
- DetailFetchers,
- getAndMergeAccountDetails,
-} from "../shared/account/details/getAndMergeAccountDetails"
-import { withHiddenSelector } from "../shared/account/selectors"
-import { getMulticallForNetwork } from "../shared/multicall"
-import { MultisigAccount } from "../shared/multisig/account"
-import { fetchMultisigDataForSigner } from "../shared/multisig/multisig.service"
-import { MultisigSigner } from "../shared/multisig/signer"
-import { PendingMultisig } from "../shared/multisig/types"
-import { getMultisigAccountFromBaseWallet } from "../shared/multisig/utils/baseMultisig"
-import {
- Network,
- defaultNetwork,
- defaultNetworks,
- getProvider,
-} from "../shared/network"
-import { getProviderv4 } from "../shared/network/provider"
-import { cosignerSign } from "../shared/shield/backend/account"
-import { ARGENT_SHIELD_ENABLED } from "../shared/shield/constants"
-import { GuardianSelfSigner } from "../shared/shield/GuardianSelfSigner"
-import { GuardianSignerArgentX } from "../shared/shield/GuardianSignerArgentX"
-import {
- IArrayStorage,
- IKeyValueStorage,
- IObjectStorage,
- ObjectStorage,
-} from "../shared/storage"
-import { BaseWalletAccount, WalletAccount } from "../shared/wallet.model"
-import {
- accountsEqual,
- baseDerivationPath,
- getAccountIdentifier,
-} from "../shared/wallet.service"
-import { isEqualAddress } from "../ui/services/addresses"
-import { LoadContracts } from "./accounts"
-import {
- declareContracts,
- getPreDeployedAccount,
-} from "./devnet/declareAccounts"
-import {
- getNextPathIndex,
- getPathForIndex,
- getStarkPair,
-} from "./keys/keyDerivation"
-import { getNonce, increaseStoredNonce } from "./nonce"
-import backupSchema from "./schema/backup.schema"
-
-const { calculateContractAddressFromHash, getSelectorFromName } = hash
-
-const isDev = process.env.NODE_ENV === "development"
-const isTest = process.env.NODE_ENV === "test"
-const isDevOrTest = isDev || isTest
-const SCRYPT_N = isDevOrTest ? 64 : 262144 // 131072 is the default value used by ethers
-
-const CURRENT_BACKUP_VERSION = 1
-export const SESSION_DURATION = isDev ? 24 * 60 * 60 : 30 * 60 // 30 mins in prod, 24 hours in dev
-
-const CHECK_OFFSET = 10
-
-export const PROXY_CONTRACT_CLASS_HASHES = [
- "0x25ec026985a3bf9d0cc1fe17326b245dfdc3ff89b8fde106542a3ea56c5a918",
-]
-export const ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES = [
- "0x33434ad846cdd5f23eb73ff09fe6fddd568284a0fb7d1be20ee482f044dabe2",
- "0x1a7820094feaf82d53f53f214b81292d717e7bb9a92bb2488092cd306f3993f",
- "0x3e327de1c40540b98d05cbcb13552008e36f0ec8d61d46956d2f9752c294328",
- "0x7e28fb0161d10d1cf7fe1f13e7ca57bce062731a3bd04494dfd2d0412699727",
-]
-
-export interface WalletSession {
- secret: string
- password: string
-}
-
-export interface WalletStorageProps {
- backup?: string
- selected?: BaseWalletAccount | null
- discoveredOnce?: boolean
-}
-
-export const sessionStore = new ObjectStorage(null, {
- namespace: "core:wallet:session",
- areaName: "session",
-})
-
-const isNonceManagedOnAccountContract = memoize(
- async (account: Accountv4, _: 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 type GetNetwork = (networkId: string) => Promise
-
-export class Wallet {
- constructor(
- private readonly store: IKeyValueStorage,
- private readonly walletStore: IArrayStorage,
- private readonly sessionStore: IObjectStorage,
- private readonly multisigStore: IArrayStorage,
- private readonly pendingMultisigStore: IArrayStorage,
- private readonly loadContracts: LoadContracts,
- private readonly getNetwork: GetNetwork,
- ) {}
-
- public async isInitialized(): Promise {
- return Boolean(await this.store.get("backup"))
- }
-
- public async isSessionOpen(): Promise {
- return (await this.sessionStore.get()) !== null
- }
-
- private async generateNewLocalSecret(
- password: string,
- progressCallback?: ProgressCallback,
- ) {
- if (await this.isInitialized()) {
- return
- }
-
- const ethersWallet = ethers.Wallet.createRandom()
- const encryptedBackup = await ethersWallet.encrypt(
- password,
- { scrypt: { N: SCRYPT_N } },
- progressCallback,
- )
-
- await this.store.set("discoveredOnce", true)
- await this.store.set("backup", encryptedBackup)
- return this.setSession(ethersWallet.privateKey, password)
- }
-
- public async getSeedPhrase(): Promise {
- const session = await this.sessionStore.get()
- const backup = await this.store.get("backup")
-
- if (!(await this.isSessionOpen()) || !session || !backup) {
- throw new Error("Session is not open")
- }
-
- const wallet = await ethers.Wallet.fromEncryptedJson(
- backup,
- session.password,
- )
-
- return wallet.mnemonic.phrase
- }
-
- public async restoreSeedPhrase(seedPhrase: string, newPassword: string) {
- const session = await this.sessionStore.get()
- if ((await this.isInitialized()) || session) {
- throw new Error("Wallet is already initialized")
- }
- const ethersWallet = ethers.Wallet.fromMnemonic(seedPhrase)
- const encryptedBackup = await ethersWallet.encrypt(newPassword, {
- scrypt: { N: SCRYPT_N },
- })
-
- await this.importBackup(encryptedBackup)
- await this.setSession(ethersWallet.privateKey, newPassword)
- const accounts = await this.discoverAccounts()
- if (accounts.length === 0) {
- this.newAccount(defaultNetwork.id)
- }
- }
-
- public async discoverAccounts() {
- const session = await this.sessionStore.get()
- if (!session?.secret) {
- throw new Error("Wallet is not initialized")
- }
- const wallet = new ethers.Wallet(session?.secret)
-
- const networks = defaultNetworks.map((network) => network.id)
-
- const accountsResults = await Promise.all(
- networks.map(async (networkId) => {
- const network = await this.getNetwork(networkId)
- if (!network) {
- throw new Error(`Network ${networkId} not found`)
- }
- return this.restoreAccountsFromWallet(wallet.privateKey, network)
- }),
- )
- const accounts = accountsResults.flatMap((x) => x)
-
- await this.walletStore.push(accounts)
- this.store.set("discoveredOnce", true)
- return accounts
- }
-
- private async getAccountClassHashForNetwork(
- network: Network,
- accountType: ArgentAccountType,
- ): Promise {
- if (network.accountClassHash && network.accountClassHash.standard) {
- return (
- network.accountClassHash[accountType] ??
- network.accountClassHash.standard
- )
- }
-
- const deployerAccount = await getPreDeployedAccount(network)
- if (deployerAccount) {
- const { accountClassHash } = await declareContracts(
- network,
- deployerAccount,
- this.loadContracts,
- )
-
- return accountClassHash
- }
-
- return ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES[0]
- }
-
- private async restoreAccountsFromWallet(
- secret: string,
- network: Network,
- offset: number = CHECK_OFFSET,
- ): Promise {
- const provider = getProvider(network)
-
- const accounts: WalletAccount[] = []
-
- const standardAccountClassHash = await this.getAccountClassHashForNetwork(
- network,
- "standard",
- )
-
- // This will be a standard account hash if multisig is not supported on the network
- // It will be handled by the union function below
- const multisigAccountClassHash = await this.getAccountClassHashForNetwork(
- network,
- "multisig",
- )
-
- const accountClassHashes = union(ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES, [
- standardAccountClassHash,
- multisigAccountClassHash,
- ])
- const proxyClassHashes = PROXY_CONTRACT_CLASS_HASHES
-
- if (!accountClassHashes?.length) {
- console.error(`No known account class hashes for network ${network.id}`)
- return accounts
- }
-
- const proxyClassHashAndAccountClassHash2DMap = proxyClassHashes.flatMap(
- (contractHash) =>
- accountClassHashes.map(
- (implementation) => [contractHash, implementation] as const,
- ),
- )
-
- const promises = proxyClassHashAndAccountClassHash2DMap.map(
- async ([contractClassHash, accountClassHash]) => {
- let lastHit = 0
- let lastCheck = 0
-
- while (lastHit + offset > lastCheck) {
- const starkPair = getStarkPair(lastCheck, secret, baseDerivationPath)
- const starkPub = ec.getStarkKey(starkPair)
-
- const address = calculateContractAddressFromHash(
- starkPub,
- contractClassHash,
- stark.compileCalldata({
- implementation: accountClassHash,
- selector: getSelectorFromName("initialize"),
- calldata: stark.compileCalldata({
- signer: starkPub,
- guardian: "0",
- }),
- }),
- 0,
- )
-
- const account: WalletAccount = {
- name: "Unnamed Account",
- address,
- networkId: network.id,
- network,
- signer: {
- type: "local_secret",
- derivationPath: getPathForIndex(lastCheck, baseDerivationPath),
- },
- type: "standard",
- needsDeploy: false, // Only deployed accounts will be recovered
- }
-
- const code = await provider.getCode(address)
-
- if (code.bytecode.length > 0) {
- lastHit = lastCheck
- accounts.push(account) // add a standard account
- } else if (
- isEqualAddress(accountClassHash, multisigAccountClassHash) // this is required to ensure multisig accounts are only checked on networks that support them
- ) {
- // If it's not a standard account, check if the signer is a part of a Multisig
- const multisigData = await fetchMultisigDataForSigner({
- signer: starkPub,
- network,
- })
-
- // If the signer is not a part of multisig, the api doesn't throw an error
- // but returns an empty content array
- if (!isEmpty(multisigData.content)) {
- lastHit = lastCheck
- const {
- address: multisigAddress,
- creator,
- signers,
- threshold,
- } = multisigData.content[0]
-
- accounts.push({
- ...account,
- type: "multisig",
- address: multisigAddress,
- }) // add a multisig account
-
- await this.multisigStore.push({
- address: multisigAddress,
- networkId: network.id,
- signers,
- threshold,
- creator,
- publicKey: starkPub,
- })
- }
- }
-
- ++lastCheck
- }
- },
- )
-
- await Promise.allSettled(promises)
-
- try {
- const accountDetailFetchers: DetailFetchers[] = [getAccountTypesFromChain]
-
- if (ARGENT_SHIELD_ENABLED) {
- accountDetailFetchers.push(getAccountEscapeFromChain)
- accountDetailFetchers.push(getAccountGuardiansFromChain)
- }
-
- const accountsWithDetails = await getAndMergeAccountDetails(
- accounts,
- accountDetailFetchers,
- )
-
- const accountDetailsWithNames =
- updateAccountsWithNames(accountsWithDetails)
-
- await this.walletStore.push(accountDetailsWithNames)
-
- return accountDetailsWithNames
- } catch (error) {
- console.error(
- "Error getting account types or guardians from chain",
- error,
- )
- throw new Error(JSON.stringify(error, Object.getOwnPropertyNames(error)))
- }
- }
-
- public async startSession(
- password: string,
- progressCallback?: ProgressCallback,
- ): Promise {
- // session has already started
- const session = await this.sessionStore.get()
- if (session) {
- return true
- }
-
- const throttledProgressCallback = throttle(progressCallback ?? noop, 50, {
- leading: true,
- trailing: true,
- })
-
- // wallet is not initialized: let's initialise it
- if (!(await this.isInitialized())) {
- await this.generateNewLocalSecret(password, throttledProgressCallback)
- return true
- }
-
- const backup = await this.store.get("backup")
-
- if (!backup) {
- throw new Error("Backup is not found")
- }
-
- try {
- const wallet = await ethers.Wallet.fromEncryptedJson(
- backup,
- password,
- throttledProgressCallback,
- )
-
- await this.setSession(wallet.privateKey, password)
-
- // if we have not yet discovered accounts, do it now. This only applies to wallets which got restored from a backup file, as we could not restore all accounts from onchain yet as the backup was locked until now.
- const discoveredOnce = await this.store.get("discoveredOnce")
- if (!discoveredOnce) {
- await this.discoverAccounts()
- }
-
- return true
- } catch {
- return false
- }
- }
-
- public async checkPassword(password: string): Promise {
- const session = await this.sessionStore.get()
- return session?.password === password
- }
-
- public async discoverAccountsForNetwork(
- network?: Network,
- offset: number = CHECK_OFFSET,
- ) {
- const session = await this.sessionStore.get()
- if (!this.isSessionOpen() || !session?.secret) {
- throw new Error("Session is not open")
- }
- const wallet = new ethers.Wallet(session?.secret)
-
- if (!network?.accountClassHash) {
- // silent fail if no account implementation is defined for this network
- return
- }
- const accounts = await this.restoreAccountsFromWallet(
- wallet.privateKey,
- network,
- offset,
- )
-
- await this.walletStore.push(accounts)
- }
-
- public async getDefaultAccountName(
- networkId: string,
- type: CreateAccountType,
- ): Promise {
- const accounts = await this.walletStore.get(withHiddenSelector)
- const pendingMultisigs = await this.pendingMultisigStore.get()
-
- const networkAccounts = accounts.filter(
- (account) => account.networkId === networkId,
- )
-
- const [multisigs, standards] = partition(
- networkAccounts,
- (account) => account.type === "multisig",
- )
-
- const allMultisigs = [...multisigs, ...pendingMultisigs]
-
- const defaultAccountName =
- type === "multisig"
- ? `Multisig ${allMultisigs.length + 1}`
- : `Account ${standards.length + 1}`
-
- return defaultAccountName
- }
-
- public async newAccount(
- networkId: string,
- type: CreateAccountType = "standard", // Should not be able to create plugin accounts. Default to argent account
- multisigPayload?: MultisigData,
- ): Promise {
- const session = await this.sessionStore.get()
- if (!this.isSessionOpen() || !session) {
- throw Error("no open session")
- }
-
- const network = await this.getNetwork(networkId)
-
- const accounts = await this.walletStore.get(withHiddenSelector)
- const pendingMultisigs = await this.pendingMultisigStore.get()
-
- const accountsOrPendingMultisigs = [...accounts, ...pendingMultisigs]
-
- const currentPaths = accountsOrPendingMultisigs
- .filter(
- (account) =>
- account.signer.type === "local_secret" &&
- account.networkId === networkId,
- )
- .map((account) => account.signer.derivationPath)
-
- const index = getNextPathIndex(currentPaths, baseDerivationPath)
-
- let payload
-
- if (type === "multisig" && multisigPayload) {
- payload = await this.getDeployContractPayloadForMultisig({
- index,
- networkId,
- ...multisigPayload,
- })
- } else {
- payload = await this.getDeployContractPayloadForAccountIndex(
- index,
- networkId,
- )
- }
- const proxyClassHash = PROXY_CONTRACT_CLASS_HASHES[0]
-
- const proxyAddress = calculateContractAddressFromHash(
- addressSalt,
- proxyClassHash,
- constructorCalldata,
- 0,
- )
-
- const defaultAccountName = await this.getDefaultAccountName(networkId, type)
-
- const account: CreateWalletAccount = {
- name: defaultAccountName,
- network,
- networkId: network.id,
- address: proxyAddress,
- signer: {
- type: "local_secret" as const,
- derivationPath: getPathForIndex(index, baseDerivationPath),
- },
- type,
- needsDeploy: true,
- }
-
- await this.walletStore.push([account])
-
- if (type === "multisig" && multisigPayload) {
- await this.multisigStore.push({
- address: account.address,
- networkId: account.networkId,
- signers: multisigPayload.signers,
- threshold: multisigPayload.threshold,
- creator: multisigPayload.creator,
- publicKey: multisigPayload.publicKey,
- })
- }
-
- await this.selectAccount(account)
-
- return account
- }
-
- public async deployAccount(
- walletAccount: WalletAccount,
- transactionDetails?: InvocationsDetails | undefined,
- ): Promise<{ account: WalletAccount; txHash: string }> {
- const starknetAccount = await this.getStarknetAccount(walletAccount)
-
- if (!("deployAccount" in starknetAccount)) {
- throw Error("Cannot deploy old accounts")
- }
-
- let deployAccountPayload: DeployAccountContractPayload
-
- if (walletAccount.type === "multisig") {
- deployAccountPayload = await this.getMultisigDeploymentPayload(
- walletAccount,
- )
- } else {
- deployAccountPayload = await this.getAccountDeploymentPayload(
- walletAccount,
- )
- }
-
- const { transaction_hash } = await starknetAccount.deployAccount(
- deployAccountPayload,
- transactionDetails,
- )
-
- await this.selectAccount(walletAccount)
-
- return { account: walletAccount, txHash: transaction_hash }
- }
-
- public async getAccountDeploymentFee(
- walletAccount: WalletAccount,
- ): Promise {
- const starknetAccount = await this.getStarknetAccount(walletAccount)
-
- if (!("deployAccount" in starknetAccount)) {
- throw Error("Cannot estimate fee to deploy old accounts")
- }
-
- const deployAccountPayload =
- walletAccount.type === "multisig"
- ? await this.getMultisigDeploymentPayload(walletAccount)
- : await this.getAccountDeploymentPayload(walletAccount)
-
- return starknetAccount.estimateAccountDeployFee(deployAccountPayload)
- }
-
- public async redeployAccount(account: WalletAccount) {
- if (!this.isSessionOpen()) {
- throw Error("no open session")
- }
-
- const nonce = await getNonce(account, this)
-
- const deployTransaction = await this.deployAccount(account, { nonce })
-
- await increaseStoredNonce(account)
-
- return { account, txHash: deployTransaction.txHash }
- }
-
- /** Get the Account Deployment Payload
- * Use it in the deployAccount and getAccountDeploymentFee methods
- * @param {WalletAccount} walletAccount
- */
- public async getAccountDeploymentPayload(
- walletAccount: WalletAccount,
- ): Promise> {
- const starkPair = await this.getKeyPairByDerivationPath(
- walletAccount.signer.derivationPath,
- )
-
- const starkPub = ec.getStarkKey(starkPair)
-
- const accountClassHash = await this.getAccountClassHashForNetwork(
- walletAccount.network,
- walletAccount.type,
- )
-
- const constructorCallData = {
- implementation: accountClassHash,
- selector: getSelectorFromName("initialize"),
- calldata: stark.compileCalldata({ signer: starkPub, guardian: "0" }),
- }
-
- const deployAccountPayload = {
- classHash: PROXY_CONTRACT_CLASS_HASHES[0],
- contractAddress: walletAccount.address,
- constructorCalldata: stark.compileCalldata(constructorCallData),
- addressSalt: starkPub,
- }
-
- const calculatedAccountAddress = calculateContractAddressFromHash(
- deployAccountPayload.addressSalt,
- deployAccountPayload.classHash,
- deployAccountPayload.constructorCalldata,
- 0,
- )
-
- if (isEqualAddress(walletAccount.address, calculatedAccountAddress)) {
- return deployAccountPayload
- }
-
- console.warn("Calculated address does not match account address")
-
- const oldCalldata = stark.compileCalldata({
- ...constructorCallData,
- implementation:
- "0x1a7820094feaf82d53f53f214b81292d717e7bb9a92bb2488092cd306f3993f", // old implementation, ask @janek why
- })
-
- const oldCalculatedAddress = calculateContractAddressFromHash(
- deployAccountPayload.addressSalt,
- deployAccountPayload.classHash,
- oldCalldata,
- 0,
- )
-
- if (isEqualAddress(oldCalculatedAddress, walletAccount.address)) {
- console.warn("Address matches old implementation")
- deployAccountPayload.constructorCalldata = oldCalldata
- } else {
- throw new Error("Calculated address does not match account address")
- }
-
- return deployAccountPayload
- }
-
- public async getMultisigDeploymentPayload(
- walletAccount: WalletAccount,
- ): Promise> {
- const multisigAccount = await getMultisigAccountFromBaseWallet(
- walletAccount,
- )
-
- if (!multisigAccount) {
- throw new Error("This multisig account does not exist")
- }
-
- const starkPair = await this.getKeyPairByDerivationPath(
- multisigAccount.signer.derivationPath,
- )
-
- const starkPub = ec.getStarkKey(starkPair)
-
- const accountClassHash = await this.getAccountClassHashForNetwork(
- multisigAccount.network,
- "multisig", // make sure to always use the multisig implementation
- )
-
- const constructorCallData = {
- implementation: accountClassHash,
- selector: getSelectorFromName("initialize"),
- calldata: stark.compileCalldata({
- threshold: multisigAccount.threshold.toString(),
- signers: multisigAccount.signers,
- }),
- }
-
- const deployMultisigPayload = {
- classHash: PROXY_CONTRACT_CLASS_HASHES[0],
- contractAddress: multisigAccount.address,
- constructorCalldata: stark.compileCalldata(constructorCallData),
- addressSalt: starkPub,
- }
-
- // Mostly we don't need to calculate the address,
- // but we do it here just to make sure the address is correct
- const calculatedMultisigAddress = calculateContractAddressFromHash(
- deployMultisigPayload.addressSalt,
- deployMultisigPayload.classHash,
- deployMultisigPayload.constructorCalldata,
- 0,
- )
-
- if (!isEqualAddress(calculatedMultisigAddress, multisigAccount.address)) {
- throw new Error("Calculated address does not match multisig address")
- }
-
- return deployMultisigPayload
- }
-
- public async getDeployContractPayloadForAccountIndex(
- index: number,
- networkId: string,
- ): Promise, "signature">> {
- const hasSession = await this.isSessionOpen()
- const session = await this.sessionStore.get()
- const initialised = await this.isInitialized()
-
- if (!initialised) {
- throw Error("wallet is not initialized")
- }
- if (!hasSession || !session) {
- throw Error("no open session")
- }
-
- const network = await this.getNetwork(networkId)
- const starkPair = getStarkPair(index, session?.secret, baseDerivationPath)
- const starkPub = ec.getStarkKey(starkPair)
-
- const accountClassHash = await this.getAccountClassHashForNetwork(
- network,
- "standard",
- )
-
- const payload = {
- classHash: accountClassHash,
- constructorCalldata: stark.compileCalldata({
- implementation: accountClassHash,
- selector: getSelectorFromName("initialize"),
- calldata: stark.compileCalldata({ signer: starkPub, guardian: "0" }),
- }),
- addressSalt: starkPub,
- }
-
- return payload
- }
-
- public async getDeployContractPayloadForMultisig({
- signers,
- threshold,
- index,
- networkId,
- }: {
- threshold: number
- signers: string[]
- index: number
- networkId: string
- }): Promise> {
- const hasSession = await this.isSessionOpen()
- const session = await this.sessionStore.get()
- const initialised = await this.isInitialized()
-
- if (!initialised) {
- throw Error("wallet is not initialized")
- }
- if (!hasSession || !session) {
- throw Error("no open session")
- }
-
- const network = await this.getNetwork(networkId)
- const starkPair = getStarkPair(index, session?.secret, baseDerivationPath)
- const starkPub = ec.getStarkKey(starkPair)
-
- const accountClassHash = await this.getAccountClassHashForNetwork(
- network,
- "multisig",
- )
-
- const payload = {
- classHash: accountClassHash,
- constructorCalldata: stark.compileCalldata({
- implementation: accountClassHash,
- selector: getSelectorFromName("initialize"),
- calldata: stark.compileCalldata({
- threshold: threshold.toString(),
- signers,
- }),
- }),
- addressSalt: starkPub,
- signature: starkPair.getPrivate(),
- }
-
- return payload
- }
-
- public async getAccount(selector: BaseWalletAccount): Promise {
- const [hit] = await this.walletStore.get((account) =>
- accountsEqual(account, selector),
- )
- if (!hit) {
- throw Error("account not found")
- }
- return hit
- }
-
- public async getMultisigAccount(
- selector: BaseWalletAccount,
- ): Promise {
- const [walletAccount] = await this.walletStore.get(
- (account) =>
- accountsEqual(account, selector) && account.type === "multisig",
- )
- if (!walletAccount) {
- throw Error("multisig wallet account not found")
- }
-
- const [multisigBaseWalletAccount] = await this.multisigStore.get(
- (account) => accountsEqual(account, selector),
- )
-
- if (!multisigBaseWalletAccount) {
- throw Error("multisig base wallet account not found")
- }
-
- return {
- ...walletAccount,
- ...multisigBaseWalletAccount,
- type: "multisig",
- }
- }
-
- public async getKeyPairByDerivationPath(derivationPath: string) {
- const session = await this.sessionStore.get()
- if (!session?.secret) {
- throw Error("session is not open")
- }
- return getStarkPair(derivationPath, session.secret)
- }
-
- public async getSignerForAccount(
- account: WalletAccount,
- ): Promise {
- const keyPair = await this.getKeyPairByDerivationPath(
- account.signer.derivationPath,
- )
-
- if (ARGENT_SHIELD_ENABLED && account.guardian) {
- const publicKey = ec.getStarkKey(keyPair)
- if (isEqualAddress(account.guardian, publicKey)) {
- /** Account guardian is the same as local signer */
- return new GuardianSelfSigner(keyPair)
- }
- return new GuardianSignerArgentX(keyPair, cosignerSign)
- }
-
- // Return Multisig Signer if account is multisig
- if (account.type === "multisig") {
- return new MultisigSigner(keyPair)
- }
-
- return keyPair
- }
-
- public getStarknetAccountOfType(account: Account, type: ArgentAccountType) {
- if (type === "multisig") {
- return MultisigAccount.fromAccount(account)
- }
- return account
- }
-
- public async getStarknetAccount(
- selector: BaseWalletAccount,
- useLatest = false,
- ): Promise {
- if (!(await this.isSessionOpen())) {
- throw Error("no open session")
- }
- const account = await this.getAccount(selector)
- if (!account) {
- throw Error("account not found")
- }
-
- const provider = getProvider(
- account.network && account.network.baseUrl
- ? account.network
- : await this.getNetwork(selector.networkId),
- )
-
- const signer = await this.getSignerForAccount(account)
-
- if (account.needsDeploy || useLatest) {
- const starknetAccount = new Account(provider, account.address, signer)
-
- return this.getStarknetAccountOfType(starknetAccount, account.type)
- }
-
- const providerV4 = getProviderv4(
- account.network && account.network.baseUrl
- ? account.network
- : await this.getNetwork(selector.networkId),
- )
-
- const oldAccount = new Accountv4(providerV4, account.address, signer)
-
- const isOldAccount = await isNonceManagedOnAccountContract(
- oldAccount,
- account,
- )
-
- const starknetAccount = new Account(provider, account.address, signer)
-
- return isOldAccount
- ? oldAccount
- : this.getStarknetAccountOfType(starknetAccount, account.type)
- }
-
- public async getCurrentImplementation(
- account: WalletAccount,
- ): Promise {
- const multicall = getMulticallForNetwork(account.network)
-
- const [implementation] = await multicall.call({
- contractAddress: account.address,
- entrypoint: "get_implementation",
- })
-
- return stark.makeAddress(number.toHex(number.toBN(implementation)))
- }
-
- public async getSelectedStarknetAccount(): Promise {
- if (!this.isSessionOpen()) {
- throw Error("no open session")
- }
-
- const account = await this.getSelectedAccount()
- if (!account) {
- throw new Error("no selected account")
- }
-
- return this.getStarknetAccount(account)
- }
-
- public async getCalculatedMultisigAddress(
- baseMultisigAccount: BaseMultisigWalletAccount,
- ): Promise {
- const multisigAccount = await getMultisigAccountFromBaseWallet(
- baseMultisigAccount,
- )
-
- if (!multisigAccount) {
- throw new Error("This multisig account does not exist")
- }
-
- const starkPair = await this.getKeyPairByDerivationPath(
- multisigAccount.signer.derivationPath,
- )
-
- const starkPub = ec.getStarkKey(starkPair)
-
- const accountClassHash = await this.getAccountClassHashForNetwork(
- multisigAccount.network,
- "multisig", // make sure to always use the multisig implementation
- )
-
- const decodedSigners = baseMultisigAccount.signers.map((signer) =>
- utils.hexlify(utils.base58.decode(signer)),
- )
-
- const constructorCallData = {
- implementation: accountClassHash,
- selector: getSelectorFromName("initialize"),
- calldata: stark.compileCalldata({
- threshold: baseMultisigAccount.threshold.toString(),
- signers: decodedSigners,
- }),
- }
-
- const deployMultisigPayload = {
- classHash: PROXY_CONTRACT_CLASS_HASHES[0],
- constructorCalldata: stark.compileCalldata(constructorCallData),
- addressSalt: starkPub,
- }
-
- return calculateContractAddressFromHash(
- deployMultisigPayload.addressSalt,
- deployMultisigPayload.classHash,
- deployMultisigPayload.constructorCalldata,
- 0,
- )
- }
-
- public async getSelectedAccount(): Promise {
- if (!this.isSessionOpen()) {
- return
- }
- const accounts = await this.walletStore.get()
- const selectedAccount = await this.store.get("selected")
- const defaultAccount =
- accounts.find((account) => account.networkId === defaultNetwork.id) ??
- accounts[0]
- if (!selectedAccount) {
- return defaultAccount
- }
- const account = find(accounts, (account) =>
- accountsEqual(selectedAccount, account),
- )
- return account ?? defaultAccount
- }
-
- public async selectAccount(accountIdentifier?: BaseWalletAccount) {
- if (!accountIdentifier) {
- await this.store.set("selected", null) // Set null instead of undefinded
- return
- }
-
- const accounts = await this.walletStore.get()
- const account = find(accounts, (account) =>
- accountsEqual(account, accountIdentifier),
- )
-
- if (!account) {
- throw Error("account not found")
- }
-
- const { address, networkId } = account // makes sure to strip away unused properties
- await this.store.set("selected", { address, networkId })
- return account
- }
-
- public async lock() {
- await this.sessionStore.set(this.sessionStore.defaults)
- }
-
- public async exportBackup(): Promise<{ url: string; filename: string }> {
- const backup = await this.store.get("backup")
-
- if (!backup) {
- throw Error("no local backup")
- }
- const blob = new Blob([backup], {
- type: "application/json",
- })
- const url = URL.createObjectURL(blob)
- const filename = "argent-x-backup.json"
- return { url, filename }
- }
-
- public async getPrivateKey(
- baseWalletAccount: BaseWalletAccount,
- ): Promise {
- const session = await this.sessionStore.get()
- if (!this.isSessionOpen() || !session?.secret) {
- throw new Error("Session is not open")
- }
-
- const account = await this.getAccount(baseWalletAccount)
-
- if (!account) {
- throw new Error("no selected account")
- }
-
- const starkPair = getStarkPair(
- account.signer.derivationPath,
- session.secret,
- )
-
- return starkPair.getPrivate().toString()
- }
-
- public async getPublicKey(
- baseAccount?: BaseWalletAccount,
- ): Promise<{ publicKey: string; account: BaseWalletAccount }> {
- const account = baseAccount
- ? await this.getAccount(baseAccount)
- : await this.getSelectedAccount()
-
- if (!account) {
- throw new Error("no selected account")
- }
-
- const starkPair = await this.getKeyPairByDerivationPath(
- account.signer.derivationPath,
- )
-
- const starkPub = ec.getStarkKey(starkPair)
-
- return { publicKey: starkPub, account }
- }
-
- /**
- * Given networkId, returns the next public key that will be used for a new account
- * @param networkId
- * @returns Public key
- */
- public async getNextPublicKey(
- networkId: string,
- ): Promise<{ derivationPath: string; publicKey: string }> {
- const session = await this.sessionStore.get()
-
- if (!session?.secret) {
- throw Error("session is not open")
- }
-
- const accounts = await this.walletStore.get(withHiddenSelector)
- const pendingMultisigs = await this.pendingMultisigStore.get()
-
- const accountsOrPendingMultisigs = [...accounts, ...pendingMultisigs]
-
- const currentPaths = accountsOrPendingMultisigs
- .filter(
- (account) =>
- account.signer.type === "local_secret" &&
- account.networkId === networkId,
- )
- .sort(sortByDerivationPath)
- .map((account) => account.signer.derivationPath)
-
- const index = getNextPathIndex(currentPaths, baseDerivationPath)
-
- const path = getPathForIndex(index, baseDerivationPath)
- const starkPair = getStarkPair(index, session?.secret, baseDerivationPath)
-
- return {
- derivationPath: path,
- publicKey: ec.getStarkKey(starkPair),
- }
- }
-
- public async newPendingMultisig(networkId: string): Promise {
- const { derivationPath, publicKey } = await this.getNextPublicKey(networkId)
-
- const name = await this.getDefaultAccountName(networkId, "multisig")
-
- const pendingMultisig: PendingMultisig = {
- name,
- networkId,
- signer: {
- type: "local_secret",
- derivationPath,
- },
- publicKey,
- type: "multisig",
- }
-
- await this.pendingMultisigStore.push(pendingMultisig)
-
- return pendingMultisig
- }
-
- public static validateBackup(backupString: string): boolean {
- try {
- const backup = JSON.parse(backupString)
- return backupSchema.isValidSync(backup)
- } catch {
- return false
- }
- }
-
- private async setSession(secret: string, password: string) {
- await this.sessionStore.set({ secret, password })
-
- browser.alarms.onAlarm.addListener(async (alarm) => {
- if (alarm.name === "session_timeout") {
- return this.lock()
- }
- })
-
- const alarm = await browser.alarms.get("session_timeout")
- if (alarm?.name !== "session_timeout") {
- browser.alarms.create("session_timeout", {
- delayInMinutes: SESSION_DURATION,
- })
- }
- }
-
- public async importBackup(backup: string): Promise {
- if (!Wallet.validateBackup(backup)) {
- throw new Error("invalid backup file in local storage")
- }
-
- const backupJson = JSON.parse(backup)
- if (backupJson.argent?.version !== CURRENT_BACKUP_VERSION) {
- // in the future, backup file migration will happen here
- }
-
- await this.store.set("backup", backup)
-
- const accounts: WalletAccount[] = await Promise.all(
- (backupJson.argent?.accounts ?? []).map(async (account: any) => {
- const network = await this.getNetwork(account.network)
- return {
- ...account,
- network,
- networkId: network.id,
- }
- }),
- )
-
- if (accounts.length > 0) {
- await this.walletStore.push(accounts)
- }
- }
-}
diff --git a/packages/extension/src/background/wallet/account/shared.service.test.ts b/packages/extension/src/background/wallet/account/shared.service.test.ts
new file mode 100644
index 000000000..75671b920
--- /dev/null
+++ b/packages/extension/src/background/wallet/account/shared.service.test.ts
@@ -0,0 +1,340 @@
+import {
+ WalletAccountSharedService,
+ WalletSession,
+ WalletStorageProps,
+} from "./shared.service"
+
+import { BaseMultisigWalletAccount } from "../../../shared/wallet.model"
+import { PendingMultisig } from "../../../shared/multisig/types"
+
+import { WalletAccount } from "../../../shared/wallet.model"
+import {
+ getMultisigStoreMock,
+ getPendingMultisigStoreMock,
+ getSessionStoreMock,
+ getStoreMock,
+ getWalletStoreMock,
+} from "../test.utils"
+import {
+ IObjectStore,
+ IRepository,
+} from "../../../shared/storage/__new/interface"
+
+describe("WalletAccountSharedService", () => {
+ let service: WalletAccountSharedService
+ let storeMock: IObjectStore
+ let walletStoreMock: IRepository
+ let sessionStoreMock: IObjectStore
+ let multisigStoreMock: IRepository
+ let pendingMultisigStoreMock: IRepository
+
+ beforeEach(() => {
+ vi.resetAllMocks()
+ })
+
+ describe("getAccount", () => {
+ it("should return the account when found", async () => {
+ const accountMock = {
+ address: "address",
+ networkId: "networkId",
+ } as WalletAccount
+ const accountsMock = [accountMock]
+ storeMock = getStoreMock()
+ walletStoreMock = getWalletStoreMock({
+ get: vi.fn(() => Promise.resolve(accountsMock)),
+ })
+ sessionStoreMock = getSessionStoreMock()
+ multisigStoreMock = getMultisigStoreMock()
+ pendingMultisigStoreMock = getPendingMultisigStoreMock()
+
+ service = new WalletAccountSharedService(
+ storeMock,
+ walletStoreMock,
+ sessionStoreMock,
+ multisigStoreMock,
+ pendingMultisigStoreMock,
+ )
+
+ const result = await service.getAccount(accountMock)
+
+ expect(walletStoreMock.get).toHaveBeenCalledWith(expect.any(Function))
+ expect(result).toEqual(accountMock)
+ })
+
+ it("should throw an error when account not found", async () => {
+ const accountMock = {
+ address: "address",
+ networkId: "networkId",
+ } as WalletAccount
+ const accountsMock = [] as WalletAccount[]
+ storeMock = getStoreMock()
+ walletStoreMock = getWalletStoreMock({
+ get: vi.fn(() => Promise.resolve(accountsMock)),
+ })
+ sessionStoreMock = getSessionStoreMock()
+ multisigStoreMock = getMultisigStoreMock()
+ pendingMultisigStoreMock = getPendingMultisigStoreMock()
+
+ service = new WalletAccountSharedService(
+ storeMock,
+ walletStoreMock,
+ sessionStoreMock,
+ multisigStoreMock,
+ pendingMultisigStoreMock,
+ )
+
+ await expect(service.getAccount(accountMock)).rejects.toThrow(
+ "Account not found",
+ )
+ })
+ })
+
+ describe("getSelectedAccount", () => {
+ it("should return the selected account when session is set and selected account exists", async () => {
+ const sessionMock = { secret: "secret", password: "password" }
+ const accountsMock = [
+ { address: "address1", networkId: "networkId1" },
+ { address: "address2", networkId: "networkId2" },
+ ] as WalletAccount[]
+
+ storeMock = getStoreMock({
+ set: vi.fn(),
+ subscribe: vi.fn(),
+ namespace: "",
+ get: vi.fn(() =>
+ Promise.resolve({
+ selected: accountsMock[0],
+ }),
+ ),
+ })
+ walletStoreMock = getWalletStoreMock({
+ get: vi.fn(() => Promise.resolve(accountsMock)),
+ })
+ sessionStoreMock = getSessionStoreMock({
+ get: vi.fn(() => Promise.resolve(sessionMock)),
+ })
+ multisigStoreMock = getMultisigStoreMock()
+ pendingMultisigStoreMock = getPendingMultisigStoreMock()
+
+ service = new WalletAccountSharedService(
+ storeMock,
+ walletStoreMock,
+ sessionStoreMock,
+ multisigStoreMock,
+ pendingMultisigStoreMock,
+ )
+
+ const result = await service.getSelectedAccount()
+
+ expect(sessionStoreMock.get).toHaveBeenCalled()
+ expect(walletStoreMock.get).toHaveBeenCalled()
+ expect(storeMock.get).toHaveBeenCalled()
+ expect(result).toEqual(accountsMock[0])
+ })
+
+ it("should return undefined when session is not set", async () => {
+ storeMock = getStoreMock({
+ set: vi.fn(),
+ subscribe: vi.fn(),
+ namespace: "",
+ get: vi.fn(() =>
+ Promise.resolve({
+ selected: undefined,
+ }),
+ ),
+ })
+
+ walletStoreMock = getWalletStoreMock({
+ get: vi.fn(() => Promise.resolve([])),
+ })
+ sessionStoreMock = getSessionStoreMock()
+ multisigStoreMock = getMultisigStoreMock()
+ pendingMultisigStoreMock = getPendingMultisigStoreMock()
+
+ service = new WalletAccountSharedService(
+ storeMock,
+ walletStoreMock,
+ sessionStoreMock,
+ multisigStoreMock,
+ pendingMultisigStoreMock,
+ )
+
+ const result = await service.getSelectedAccount()
+
+ expect(sessionStoreMock.get).toHaveBeenCalled()
+ expect(result).toBeUndefined()
+ })
+ })
+
+ describe("selectAccount", () => {
+ it("should set selected account to null when no account identifier is provided", async () => {
+ storeMock = getStoreMock()
+ walletStoreMock = getWalletStoreMock({
+ get: vi.fn(() => Promise.resolve([])),
+ })
+ sessionStoreMock = getSessionStoreMock()
+ multisigStoreMock = getMultisigStoreMock()
+ pendingMultisigStoreMock = getPendingMultisigStoreMock()
+
+ service = new WalletAccountSharedService(
+ storeMock,
+ walletStoreMock,
+ sessionStoreMock,
+ multisigStoreMock,
+ pendingMultisigStoreMock,
+ )
+
+ await service.selectAccount()
+
+ expect(storeMock.set).toHaveBeenCalledWith({ selected: null })
+ })
+
+ it("should throw an error when the selected account is not found", async () => {
+ const accountIdentifierMock = {
+ address: "address",
+ networkId: "networkId",
+ }
+ const accountsMock = [
+ { address: "address1", networkId: "networkId1" },
+ { address: "address2", networkId: "networkId2" },
+ ] as WalletAccount[]
+ storeMock = getStoreMock()
+ walletStoreMock = getWalletStoreMock({
+ get: vi.fn(() => Promise.resolve(accountsMock)),
+ })
+ sessionStoreMock = getSessionStoreMock()
+ multisigStoreMock = getMultisigStoreMock()
+ pendingMultisigStoreMock = getPendingMultisigStoreMock()
+
+ service = new WalletAccountSharedService(
+ storeMock,
+ walletStoreMock,
+ sessionStoreMock,
+ multisigStoreMock,
+ pendingMultisigStoreMock,
+ )
+
+ await expect(
+ service.selectAccount(accountIdentifierMock),
+ ).rejects.toThrow("Account not found")
+ })
+
+ it("should set selected account and return it when a valid account identifier is provided", async () => {
+ const accountIdentifierMock = {
+ address: "0x2",
+ networkId: "networkId2",
+ }
+ const accountsMock = [
+ { address: "0x1", networkId: "networkId1" },
+ { address: "0x2", networkId: "networkId2" },
+ ] as WalletAccount[]
+ storeMock = getStoreMock()
+ walletStoreMock = getWalletStoreMock({
+ get: vi.fn(() => Promise.resolve(accountsMock)),
+ })
+ sessionStoreMock = getSessionStoreMock()
+ multisigStoreMock = getMultisigStoreMock()
+ pendingMultisigStoreMock = getPendingMultisigStoreMock()
+
+ service = new WalletAccountSharedService(
+ storeMock,
+ walletStoreMock,
+ sessionStoreMock,
+ multisigStoreMock,
+ pendingMultisigStoreMock,
+ )
+
+ const result = await service.selectAccount(accountIdentifierMock)
+
+ expect(walletStoreMock.get).toHaveBeenCalled()
+
+ expect(storeMock.set).toHaveBeenCalledWith({
+ selected: {
+ address: accountsMock[1].address,
+ networkId: accountsMock[1].networkId,
+ },
+ })
+ expect(result).toEqual(accountsMock[1])
+ })
+ })
+
+ describe("getMultisigAccount", () => {
+ it("should return the multisig wallet account when found", async () => {
+ const accountIdentifierMock = {
+ address: "address",
+ networkId: "networkId",
+ }
+ const walletAccountMock = {
+ address: "address",
+ networkId: "networkId",
+ type: "multisig",
+ } as WalletAccount
+ const multisigBaseWalletAccountMock = {
+ baseWalletAccount: "baseWalletAccount",
+ } as unknown as BaseMultisigWalletAccount
+ const expectedMultisigAccountMock = {
+ ...walletAccountMock,
+ ...multisigBaseWalletAccountMock,
+ type: "multisig",
+ }
+
+ storeMock = getStoreMock()
+ walletStoreMock = getWalletStoreMock({
+ get: vi.fn(() => Promise.resolve([walletAccountMock])),
+ })
+ sessionStoreMock = getSessionStoreMock()
+ multisigStoreMock = getMultisigStoreMock({
+ get: vi.fn(() => Promise.resolve([multisigBaseWalletAccountMock])),
+ })
+ pendingMultisigStoreMock = getPendingMultisigStoreMock()
+
+ service = new WalletAccountSharedService(
+ storeMock,
+ walletStoreMock,
+ sessionStoreMock,
+ multisigStoreMock,
+ pendingMultisigStoreMock,
+ )
+
+ const result = await service.getMultisigAccount(accountIdentifierMock)
+
+ expect(walletStoreMock.get).toHaveBeenCalledWith(expect.any(Function))
+ expect(multisigStoreMock.get).toHaveBeenCalledWith(expect.any(Function))
+ expect(result).toEqual(expectedMultisigAccountMock)
+ })
+
+ it("should throw an error when multisig wallet account not found", async () => {
+ const accountIdentifierMock = {
+ address: "address",
+ networkId: "networkId",
+ }
+ const walletAccountMock = {
+ address: "address",
+ networkId: "networkId",
+ type: "standard",
+ } as WalletAccount
+
+ storeMock = getStoreMock()
+ walletStoreMock = getWalletStoreMock({
+ get: vi.fn(() => Promise.resolve([walletAccountMock])),
+ })
+ sessionStoreMock = getSessionStoreMock()
+ multisigStoreMock = getMultisigStoreMock({
+ get: vi.fn(() => Promise.resolve([])),
+ })
+ pendingMultisigStoreMock = getPendingMultisigStoreMock()
+
+ service = new WalletAccountSharedService(
+ storeMock,
+ walletStoreMock,
+ sessionStoreMock,
+ multisigStoreMock,
+ pendingMultisigStoreMock,
+ )
+
+ await expect(
+ service.getMultisigAccount(accountIdentifierMock),
+ ).rejects.toThrow("Multisig base wallet account not found")
+ })
+ })
+})
diff --git a/packages/extension/src/background/wallet/account/shared.service.ts b/packages/extension/src/background/wallet/account/shared.service.ts
new file mode 100644
index 000000000..2e4f7eff8
--- /dev/null
+++ b/packages/extension/src/background/wallet/account/shared.service.ts
@@ -0,0 +1,191 @@
+import { find, partition } from "lodash-es"
+
+import {
+ BaseMultisigWalletAccount,
+ CreateAccountType,
+ MultisigWalletAccount,
+} from "../../../shared/wallet.model"
+import { withHiddenSelector } from "../../../shared/account/selectors"
+import { PendingMultisig } from "../../../shared/multisig/types"
+import { Network, defaultNetwork } from "../../../shared/network"
+import { BaseWalletAccount, WalletAccount } from "../../../shared/wallet.model"
+import {
+ MULTISIG_DERIVATION_PATH,
+ STANDARD_DERIVATION_PATH,
+} from "../../../shared/wallet.service"
+import {
+ IObjectStore,
+ IRepository,
+} from "../../../shared/storage/__new/interface"
+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"
+
+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,
+ public readonly walletStore: IRepository,
+ public readonly sessionStore: IObjectStore,
+ public readonly multisigStore: IRepository,
+ public readonly pendingMultisigStore: IRepository,
+ ) {}
+
+ public async getDefaultAccountName(
+ networkId: string,
+ type: CreateAccountType,
+ ): Promise {
+ const accounts = await this.walletStore.get(withHiddenSelector)
+ const pendingMultisigs = await this.pendingMultisigStore.get()
+
+ const networkAccounts = accounts.filter(
+ (account) => account.networkId === networkId,
+ )
+
+ const [multisigs, standards] = partition(
+ networkAccounts,
+ (account) => account.type === "multisig",
+ )
+
+ const allMultisigs = [...multisigs, ...pendingMultisigs]
+
+ const defaultAccountName =
+ type === "multisig"
+ ? `Multisig ${allMultisigs.length + 1}`
+ : `Account ${standards.length + 1}`
+
+ return defaultAccountName
+ }
+
+ public getDefaultStandardAccount(
+ index: number,
+ address: string,
+ network: Network,
+ needsDeploy: boolean,
+ ): WalletAccount {
+ return {
+ name: `Account ${index + 1}`,
+ address,
+ network,
+ networkId: network.id,
+ type: "standard",
+ signer: {
+ derivationPath: getPathForIndex(index, STANDARD_DERIVATION_PATH),
+ type: "local_secret",
+ },
+ needsDeploy,
+ }
+ }
+
+ public getDefaultMultisigAccount(
+ index: number,
+ address: string,
+ network: Network,
+ needsDeploy: boolean,
+ ): WalletAccount {
+ return {
+ name: `Multisig ${index + 1}`,
+ address,
+ networkId: network.id,
+ network,
+ type: "multisig",
+ signer: {
+ derivationPath: getPathForIndex(index, MULTISIG_DERIVATION_PATH),
+ type: "local_secret",
+ },
+ classHash: MULTISIG_ACCOUNT_CLASS_HASH,
+ cairoVersion: "1",
+ needsDeploy,
+ }
+ }
+
+ // TODO rewrite using views, move out of service and rename to accountView
+ public async getAccount(
+ selector: BaseWalletAccount,
+ ): Promise {
+ const [hit] = await this.walletStore.get((account) =>
+ accountsEqual(account, selector),
+ )
+ if (!hit) {
+ throw new AccountError({ code: "NOT_FOUND" })
+ }
+ return hit
+ }
+
+ public async getSelectedAccount(): Promise {
+ // Replace with session service once instantiated
+ if ((await this.sessionStore.get()) === null) {
+ return
+ }
+ const accounts = await this.walletStore.get()
+ const selectedAccount = (await this.store.get()).selected
+ const defaultAccount =
+ accounts.find((account) => account.networkId === defaultNetwork.id) ??
+ accounts[0]
+ if (!selectedAccount) {
+ return defaultAccount
+ }
+ const account = find(accounts, (account) =>
+ accountsEqual(selectedAccount, account),
+ )
+ return account ?? defaultAccount
+ }
+
+ public async selectAccount(accountIdentifier?: BaseWalletAccount | null) {
+ if (!accountIdentifier) {
+ // handles undefined and null
+ await this.store.set({ selected: null }) // Set null instead of undefinded
+ return
+ }
+
+ const accounts = await this.walletStore.get()
+ const account = find(accounts, (account) =>
+ accountsEqual(account, accountIdentifier),
+ )
+
+ if (!account) {
+ throw new AccountError({ code: "NOT_FOUND" })
+ }
+
+ const { address, networkId } = account // makes sure to strip away unused properties
+ await this.store.set({ selected: { address, networkId } })
+ return account
+ }
+
+ public async getMultisigAccount(
+ selector: BaseWalletAccount,
+ ): Promise {
+ const [walletAccount] = await this.walletStore.get(
+ (account) =>
+ accountsEqual(account, selector) && account.type === "multisig",
+ )
+ if (!walletAccount) {
+ throw new AccountError({ code: "MULTISIG_NOT_FOUND" })
+ }
+
+ const [multisigBaseWalletAccount] = await this.multisigStore.get(
+ (account) => accountsEqual(account, selector),
+ )
+
+ if (!multisigBaseWalletAccount) {
+ throw new AccountError({ code: "MULTISIG_BASE_NOT_FOUND" })
+ }
+
+ return {
+ ...walletAccount,
+ ...multisigBaseWalletAccount,
+ type: "multisig",
+ }
+ }
+}
diff --git a/packages/extension/src/background/wallet/account/starknet.service.test.ts b/packages/extension/src/background/wallet/account/starknet.service.test.ts
new file mode 100644
index 000000000..3a98a82e5
--- /dev/null
+++ b/packages/extension/src/background/wallet/account/starknet.service.test.ts
@@ -0,0 +1,138 @@
+import { WalletAccountStarknetService } from "./starknet.service"
+import { WalletSessionService } from "../session/session.service"
+import { WalletAccountSharedService } from "./shared.service"
+import { WalletCryptoStarknetService } from "../crypto/starknet.service"
+import { MultisigAccount } from "../../../shared/multisig/account"
+import {
+ accountSharedServiceMock,
+ accountStarknetServiceMock,
+ cryptoStarknetServiceMock,
+ sessionServiceMock,
+} from "../test.utils"
+import { Account } from "starknet"
+import { grindKey } from "../../keys/keyDerivation"
+import { MultisigSigner } from "../../../shared/multisig/signer"
+
+// Mock dependencies
+vi.mock("../session/session.service")
+vi.mock("./shared.service")
+vi.mock("../crypto/starknet.service")
+
+describe("AccountStarknetService", () => {
+ let accountStarknetService: WalletAccountStarknetService
+ let sessionService: WalletSessionService
+ let accountSharedService: WalletAccountSharedService
+ let cryptoStarknetService: WalletCryptoStarknetService
+
+ beforeEach(() => {
+ sessionService = sessionServiceMock
+ accountSharedService = accountSharedServiceMock
+ cryptoStarknetService = cryptoStarknetServiceMock
+ accountStarknetService = accountStarknetServiceMock
+ })
+
+ describe("getStarknetAccount", () => {
+ it("should throw an error if no session is open", async () => {
+ vi.spyOn(sessionService, "isSessionOpen").mockResolvedValue(false)
+
+ await expect(
+ accountStarknetService.getStarknetAccount({
+ address: "0x0",
+ networkId: "net1",
+ }),
+ ).rejects.toThrow("no open session")
+ })
+
+ it("should throw an error if account is not found", async () => {
+ vi.spyOn(sessionService, "isSessionOpen").mockResolvedValue(true)
+ vi.spyOn(accountSharedService, "getAccount").mockResolvedValue(null)
+
+ await expect(
+ accountStarknetService.getStarknetAccount({
+ address: "0x0",
+ networkId: "net1",
+ }),
+ ).rejects.toThrow("Account not found")
+ })
+ })
+
+ describe("getSelectedStarknetAccount", () => {
+ it("should throw an error if no session is open", async () => {
+ vi.spyOn(sessionService, "isSessionOpen").mockResolvedValue(false)
+
+ await expect(
+ accountStarknetService.getSelectedStarknetAccount(),
+ ).rejects.toThrow("no open session")
+ })
+
+ it("should throw an error if no selected account", async () => {
+ vi.spyOn(sessionService, "isSessionOpen").mockResolvedValue(true)
+ vi.spyOn(accountSharedService, "getSelectedAccount").mockResolvedValue(
+ undefined,
+ )
+
+ await expect(
+ accountStarknetService.getSelectedStarknetAccount(),
+ ).rejects.toThrow("no selected account")
+ })
+ })
+
+ describe("newPendingMultisig", () => {
+ it("should create a new pending multisig", async () => {
+ const mockPublicKey = "mockPublicKey"
+ const mockNetworkId = "mockNetworkId"
+ const mockDerivationPath = "mockDerivationPath"
+ vi.spyOn(
+ cryptoStarknetService,
+ "getNextPublicKeyForMultisig",
+ ).mockResolvedValue({
+ index: 0,
+ publicKey: mockPublicKey,
+ derivationPath: mockDerivationPath,
+ })
+ const pushSpy = vi.spyOn(
+ accountStarknetService["pendingMultisigStore"],
+ "upsert",
+ )
+
+ const result = await accountStarknetService.newPendingMultisig(
+ mockNetworkId,
+ )
+
+ expect(result).toEqual(
+ expect.objectContaining({
+ name: "Multisig 1",
+ networkId: mockNetworkId,
+ signer: {
+ type: "local_secret",
+ derivationPath: mockDerivationPath,
+ },
+ publicKey: mockPublicKey,
+ type: "multisig",
+ }),
+ )
+ expect(pushSpy).toHaveBeenCalledWith(result)
+ })
+ })
+
+ describe("getStarknetAccountOfType", () => {
+ it('should return MultisigAccount if type is "multisig"', () => {
+ const signer = new MultisigSigner(grindKey("0x1"))
+ const mockAccount = new Account({}, "0x0", signer)
+ const result = accountStarknetService.getStarknetAccountOfType(
+ mockAccount,
+ "multisig",
+ )
+ expect(result).toBeInstanceOf(MultisigAccount)
+ })
+
+ it('should return Account if type is not "multisig"', () => {
+ const mockAccount = new Account({}, "0x0", grindKey("0x1"))
+ const result = accountStarknetService.getStarknetAccountOfType(
+ mockAccount,
+ "standard",
+ )
+ expect(result).toBeInstanceOf(Account)
+ })
+ })
+})
diff --git a/packages/extension/src/background/wallet/account/starknet.service.ts b/packages/extension/src/background/wallet/account/starknet.service.ts
new file mode 100644
index 000000000..c0b6f7924
--- /dev/null
+++ b/packages/extension/src/background/wallet/account/starknet.service.ts
@@ -0,0 +1,196 @@
+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"
+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"
+import { WalletAccountSharedService } from "./shared.service"
+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,
+ private readonly networkService: Pick,
+ private readonly sessionService: WalletSessionService,
+ private readonly accountSharedService: WalletAccountSharedService,
+ private readonly cryptoStarknetService: WalletCryptoStarknetService,
+ private readonly multisigBackendService: IMultisigBackendService,
+ ) {}
+
+ public async getStarknetAccount(
+ selector: BaseWalletAccount,
+ useLatest = false,
+ ): Promise {
+ if (!(await this.sessionService.isSessionOpen())) {
+ throw new SessionError({ code: "NO_OPEN_SESSION" })
+ }
+ const account = await this.accountSharedService.getAccount(selector)
+ if (!account) {
+ throw new AccountError({ code: "NOT_FOUND" })
+ }
+
+ const provider = getProvider(
+ account.network && account.network.sequencerUrl
+ ? account.network
+ : await this.networkService.getById(selector.networkId),
+ )
+
+ const signer = await this.cryptoStarknetService.getSignerForAccount(account)
+
+ const pkOrSigner = isKeyPair(signer) ? signer.getPrivate() : signer
+
+ if (account.needsDeploy) {
+ const cairoVersion =
+ await this.cryptoStarknetService.getUndeployedAccountCairoVersion(
+ selector,
+ )
+
+ const starknetAccount = new Account(
+ provider,
+ account.address,
+ pkOrSigner,
+ cairoVersion,
+ )
+
+ return this.getStarknetAccountOfType(starknetAccount, account.type)
+ }
+
+ if (useLatest) {
+ const starknetAccount = new Account(
+ provider,
+ account.address,
+ pkOrSigner,
+ "1",
+ )
+
+ return this.getStarknetAccountOfType(starknetAccount, account.type)
+ }
+
+ const providerV4 = getProviderv4__deprecated(
+ account.network && account.network.sequencerUrl
+ ? account.network
+ : await this.networkService.getById(selector.networkId),
+ )
+
+ const oldAccount = new AccountV4__deprecated(
+ providerV4,
+ account.address,
+ isKeyPair(signer)
+ ? ec__deprecated.getKeyPair(signer.getPrivate())
+ : signer,
+ )
+
+ const isOldAccount = await isNonceManagedOnAccountContract(
+ oldAccount,
+ account,
+ )
+
+ // Keep the fallback here as we don't want to block the users
+ // if the worker has not updated the account yet
+ const accountCairoVersion =
+ account.cairoVersion ??
+ (await getAccountCairoVersion(
+ account.address,
+ account.network,
+ account.type,
+ ))
+
+ if (!accountCairoVersion) {
+ throw new AccountError({
+ code: "DEPLOYED_ACCOUNT_CAIRO_VERSION_NOT_FOUND",
+ })
+ }
+
+ const starknetAccount = new Account(
+ provider,
+ account.address,
+ pkOrSigner,
+ accountCairoVersion,
+ )
+
+ return isOldAccount
+ ? oldAccount
+ : this.getStarknetAccountOfType(starknetAccount, account.type)
+ }
+
+ public async getSelectedStarknetAccount(): Promise<
+ Account | AccountV4__deprecated
+ > {
+ if (!(await this.sessionService.isSessionOpen())) {
+ throw Error("no open session")
+ }
+
+ const account = await this.accountSharedService.getSelectedAccount()
+ if (!account) {
+ throw new Error("no selected account")
+ }
+
+ return this.getStarknetAccount(account)
+ }
+
+ public async newPendingMultisig(networkId: string): Promise {
+ const { index, derivationPath, publicKey } =
+ await this.cryptoStarknetService.getNextPublicKeyForMultisig(networkId)
+
+ const pendingMultisig: PendingMultisig = {
+ name: `Multisig ${index + 1}`,
+ networkId,
+ signer: {
+ type: "local_secret",
+ derivationPath,
+ },
+ publicKey,
+ type: "multisig",
+ }
+
+ await this.pendingMultisigStore.upsert(pendingMultisig)
+
+ return pendingMultisig
+ }
+
+ public getStarknetAccountOfType(
+ account: Account | AccountV4__deprecated,
+ type: ArgentAccountType,
+ ) {
+ if (type === "multisig") {
+ return MultisigAccount.fromAccount(account, this.multisigBackendService)
+ }
+ return account
+ }
+}
diff --git a/packages/extension/src/background/wallet/backup/backup.service.ts b/packages/extension/src/background/wallet/backup/backup.service.ts
new file mode 100644
index 000000000..ccc4f5f62
--- /dev/null
+++ b/packages/extension/src/background/wallet/backup/backup.service.ts
@@ -0,0 +1,74 @@
+import { INetworkService } from "../../../shared/network/service/interface"
+import {
+ IObjectStore,
+ IRepository,
+} from "../../../shared/storage/__new/interface"
+import { BaseWalletAccount, WalletAccount } from "../../../shared/wallet.model"
+import backupSchema from "../../schema/backup.schema"
+import { WalletError } from "../../../shared/errors/wallet"
+
+const CURRENT_BACKUP_VERSION = 1
+
+export interface WalletSession {
+ secret: string
+ password: string
+}
+
+export interface WalletStorageProps {
+ backup?: string
+ selected?: BaseWalletAccount | null
+ discoveredOnce?: boolean
+}
+
+export class WalletBackupService {
+ constructor(
+ public readonly store: IObjectStore,
+ public readonly walletStore: IRepository,
+ private readonly networkService: Pick,
+ ) {}
+
+ public async getBackup() {
+ return (await this.store.get()).backup
+ }
+
+ public async isInitialized(): Promise {
+ return Boolean((await this.store.get()).backup)
+ }
+
+ public static validateBackup(backupString: string): boolean {
+ try {
+ const backup = JSON.parse(backupString)
+ return backupSchema.safeParse(backup).success
+ } catch {
+ return false
+ }
+ }
+
+ public async importBackup(backup: string): Promise {
+ if (!WalletBackupService.validateBackup(backup)) {
+ throw new WalletError({ code: "INVALID_BACKUP_FILE" })
+ }
+
+ const backupJson = JSON.parse(backup)
+ if (backupJson.argent?.version !== CURRENT_BACKUP_VERSION) {
+ // in the future, backup file migration will happen here
+ }
+
+ await this.store.set({ backup })
+
+ const accounts: WalletAccount[] = await Promise.all(
+ (backupJson.argent?.accounts ?? []).map(async (account: any) => {
+ const network = await this.networkService.getById(account.network)
+ return {
+ ...account,
+ network,
+ networkId: network.id,
+ }
+ }),
+ )
+
+ if (accounts.length > 0) {
+ await this.walletStore.upsert(accounts)
+ }
+ }
+}
diff --git a/packages/extension/src/background/wallet/crypto/shared.service.ts b/packages/extension/src/background/wallet/crypto/shared.service.ts
new file mode 100644
index 000000000..1ee046ece
--- /dev/null
+++ b/packages/extension/src/background/wallet/crypto/shared.service.ts
@@ -0,0 +1,60 @@
+import { WalletBackupService } from "../backup/backup.service"
+
+import { ethers } from "ethers"
+import { defaultNetwork } from "../../../shared/network"
+import { WalletRecoverySharedService } from "../recovery/shared.service"
+import { WalletSessionService } from "../session/session.service"
+import type { WalletSession } from "../session/walletSession.model"
+import { IWalletDeploymentService } from "../deployment/interface"
+import { IObjectStore } from "../../../shared/storage/__new/interface"
+import { WalletError } from "../../../shared/errors/wallet"
+
+export class WalletCryptoSharedService {
+ constructor(
+ private readonly sessionStore: IObjectStore,
+ private readonly sessionService: WalletSessionService,
+ private readonly backupService: WalletBackupService,
+ private readonly recoverySharedService: WalletRecoverySharedService,
+ private readonly deploymentChainService: IWalletDeploymentService,
+ private SCRYPT_N: number,
+ ) {}
+
+ public async restoreSeedPhrase(seedPhrase: string, newPassword: string) {
+ const session = await this.sessionStore.get()
+ if ((await this.backupService.isInitialized()) || session) {
+ throw new WalletError({ code: "ALREADY_INITIALIZED" })
+ }
+ const ethersWallet = ethers.Wallet.fromMnemonic(seedPhrase)
+ const encryptedBackup = await ethersWallet.encrypt(newPassword, {
+ scrypt: { N: this.SCRYPT_N },
+ })
+
+ await this.backupService.importBackup(encryptedBackup)
+ await this.sessionService.setSession(ethersWallet.privateKey, newPassword)
+ const accounts = await this.recoverySharedService.discoverAccounts()
+
+ const hasAccountsOnDefaultNetwork = accounts.some(
+ (account) => account.networkId === defaultNetwork.id,
+ )
+
+ if (!hasAccountsOnDefaultNetwork) {
+ void this.deploymentChainService.newAccount(defaultNetwork.id)
+ }
+ }
+
+ public async getSeedPhrase(): Promise {
+ const session = await this.sessionStore.get()
+ const backup = await this.backupService.getBackup()
+
+ if (!(await this.sessionService.isSessionOpen()) || !session || !backup) {
+ throw new Error("Session is not open")
+ }
+
+ const wallet = await ethers.Wallet.fromEncryptedJson(
+ backup,
+ session.password,
+ )
+
+ return wallet.mnemonic.phrase
+ }
+}
diff --git a/packages/extension/src/background/wallet/crypto/starknet.service.ts b/packages/extension/src/background/wallet/crypto/starknet.service.ts
new file mode 100644
index 000000000..ac1e01f7b
--- /dev/null
+++ b/packages/extension/src/background/wallet/crypto/starknet.service.ts
@@ -0,0 +1,402 @@
+import { isEqualAddress } from "@argent/shared"
+import { CairoVersion, CallData, hash } from "starknet"
+import { withHiddenSelector } from "../../../shared/account/selectors"
+import { MultisigSigner } from "../../../shared/multisig/signer"
+import { PendingMultisig } from "../../../shared/multisig/types"
+import { GuardianSelfSigner } from "../../../shared/shield/GuardianSelfSigner"
+import { GuardianSignerArgentX } from "../../../shared/shield/GuardianSignerArgentX"
+import { cosignerSign } from "../../../shared/shield/backend/account"
+import {
+ WalletAccount,
+ BaseWalletAccount,
+ BaseMultisigWalletAccount,
+ ArgentAccountType,
+} from "../../../shared/wallet.model"
+import {
+ getStarkPair,
+ getNextPathIndex,
+ getPathForIndex,
+ generatePublicKeys,
+} from "../../keys/keyDerivation"
+
+import { WalletAccountSharedService } from "../account/shared.service"
+import { getMultisigAccountFromBaseWallet } from "../../../shared/multisig/utils/baseMultisig"
+import type { WalletSession } from "../session/walletSession.model"
+import { Network } from "../../../shared/network"
+import {
+ getPreDeployedAccount,
+ declareContracts,
+} from "../../devnet/declareAccounts"
+import { LoadContracts } from "../loadContracts"
+import {
+ IObjectStore,
+ IRepository,
+} from "../../../shared/storage/__new/interface"
+import {
+ ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES,
+ PROXY_CONTRACT_CLASS_HASHES,
+} from "../starknet.constants"
+import { decodeBase58Array } from "@argent/shared"
+import { MULTISIG_DERIVATION_PATH } from "../../../shared/wallet.service"
+import { sortMultisigByDerivationPath } from "../../../shared/utils/accountsMultisigSort"
+import { SessionError } from "../../../shared/errors/session"
+import { getAccountCairoVersion } from "../../../shared/utils/argentAccountVersion"
+import { AccountError } from "../../../shared/errors/account"
+import { STANDARD_CAIRO_0_ACCOUNT_CLASS_HASH } from "../../../shared/network/constants"
+const { getSelectorFromName, calculateContractAddressFromHash } = hash
+
+export class WalletCryptoStarknetService {
+ constructor(
+ private readonly walletStore: IRepository,
+ private readonly sessionStore: IObjectStore,
+ private readonly pendingMultisigStore: IRepository,
+ private readonly accountSharedService: WalletAccountSharedService,
+ private readonly loadContracts: LoadContracts,
+ ) {}
+
+ public async getKeyPairByDerivationPath(derivationPath: string) {
+ const session = await this.sessionStore.get()
+ if (!session?.secret) {
+ throw new SessionError({ code: "NO_OPEN_SESSION" })
+ }
+ return getStarkPair(derivationPath, session.secret)
+ }
+
+ public async getSignerForAccount(account: WalletAccount) {
+ const keyPair = await this.getKeyPairByDerivationPath(
+ account.signer.derivationPath,
+ )
+
+ const publicKey = keyPair.pubKey
+ const pk = keyPair.getPrivate()
+
+ // Keep the fallback here as we don't want to block the users
+ // if the worker has not updated the account yet
+ let cairoVersion =
+ account.cairoVersion ??
+ (await getAccountCairoVersion(
+ account.address,
+ account.network,
+ account.type,
+ ))
+
+ if (!cairoVersion && account.needsDeploy) {
+ cairoVersion = await this.getUndeployedAccountCairoVersion(account)
+ }
+
+ if (!cairoVersion) {
+ throw new AccountError({
+ code: "DEPLOYED_ACCOUNT_CAIRO_VERSION_NOT_FOUND",
+ })
+ }
+
+ if (account.guardian) {
+ if (isEqualAddress(account.guardian, publicKey)) {
+ /** Account guardian is the same as local signer */
+ return new GuardianSelfSigner(pk)
+ }
+ return new GuardianSignerArgentX(pk, cosignerSign, cairoVersion)
+ }
+
+ // Return Multisig Signer if account is multisig
+ if (account.type === "multisig") {
+ return new MultisigSigner(pk)
+ }
+
+ return keyPair
+ }
+
+ public async getPrivateKey(
+ baseWalletAccount: BaseWalletAccount,
+ ): Promise {
+ const session = await this.sessionStore.get()
+ if (session === null || !session?.secret) {
+ throw new SessionError({ code: "NO_OPEN_SESSION" })
+ }
+
+ const account = await this.accountSharedService.getAccount(
+ baseWalletAccount,
+ )
+
+ if (!account) {
+ throw new AccountError({ code: "NOT_SELECTED" })
+ }
+
+ const starkPair = getStarkPair(
+ account.signer.derivationPath,
+ session.secret,
+ )
+
+ return starkPair.getPrivate().toString()
+ }
+
+ public async getPublicKey(
+ baseAccount?: BaseWalletAccount,
+ ): Promise<{ publicKey: string; account: BaseWalletAccount }> {
+ const account = baseAccount
+ ? await this.accountSharedService.getAccount(baseAccount)
+ : await this.accountSharedService.getSelectedAccount()
+
+ if (!account) {
+ throw new AccountError({ code: "NOT_SELECTED" })
+ }
+
+ const starkPair = await this.getKeyPairByDerivationPath(
+ account.signer.derivationPath,
+ )
+
+ const starkPub = starkPair.pubKey
+
+ return { publicKey: starkPub, account }
+ }
+
+ /**
+ * Given networkId, returns the next public key that will be used for a new account
+ * @param networkId
+ * @returns Public key
+ */
+ public async getNextPublicKeyForMultisig(
+ networkId: string,
+ ): Promise<{ index: number; derivationPath: string; publicKey: string }> {
+ const session = await this.sessionStore.get()
+
+ if (!session?.secret) {
+ throw new SessionError({ code: "NO_OPEN_SESSION" })
+ }
+
+ const accounts = await this.walletStore.get(withHiddenSelector)
+
+ const multisigs = accounts.filter((account) => account.type === "multisig")
+
+ const pendingMultisigs = await this.pendingMultisigStore.get()
+
+ const multisigsOrPendingMultisigs = [...multisigs, ...pendingMultisigs]
+
+ const currentPaths = multisigsOrPendingMultisigs
+ .filter(
+ (account) =>
+ account.signer.type === "local_secret" &&
+ account.signer.derivationPath.startsWith(MULTISIG_DERIVATION_PATH) && // just to be sure
+ account.networkId === networkId,
+ )
+ .sort(sortMultisigByDerivationPath)
+ .map((account) => account.signer.derivationPath)
+
+ const index = getNextPathIndex(currentPaths, MULTISIG_DERIVATION_PATH)
+
+ const path = getPathForIndex(index, MULTISIG_DERIVATION_PATH)
+ const starkPair = getStarkPair(
+ index,
+ session?.secret,
+ MULTISIG_DERIVATION_PATH,
+ )
+
+ return {
+ index,
+ derivationPath: path,
+ publicKey: starkPair.pubKey,
+ }
+ }
+
+ /**
+ * Given start and buffer, returns an array of public keys
+ * @param start Start index
+ * @param buffer Number of public keys to return
+ * @returns String array of public keys
+ */
+ public async getPublicKeysBufferForMultisig(
+ start: number,
+ buffer: number,
+ ): Promise {
+ const session = await this.sessionStore.get()
+
+ if (!session?.secret) {
+ throw new SessionError({ code: "NO_OPEN_SESSION" })
+ }
+
+ const keys = generatePublicKeys(
+ session.secret,
+ start,
+ buffer,
+ MULTISIG_DERIVATION_PATH,
+ )
+
+ return keys.map(({ pubKey }) => pubKey)
+ }
+
+ async getAccountClassHashForNetwork(
+ network: Network,
+ accountType: ArgentAccountType,
+ ): Promise {
+ if (network.accountClassHash && network.accountClassHash.standard) {
+ return (
+ network.accountClassHash[accountType] ??
+ network.accountClassHash.standard
+ )
+ }
+
+ const deployerAccount = await getPreDeployedAccount(network)
+ if (deployerAccount) {
+ const { accountClassHash } = await declareContracts(
+ network,
+ deployerAccount,
+ this.loadContracts,
+ )
+
+ return accountClassHash
+ }
+
+ return ARGENT_ACCOUNT_CONTRACT_CLASS_HASHES[0]
+ }
+
+ public getCairo0AccountContractAddress(
+ accountClassHash: string,
+ pubKey: string,
+ ): string {
+ const constructorCallData = {
+ implementation: accountClassHash,
+ selector: getSelectorFromName("initialize"),
+ calldata: CallData.compile({
+ signer: pubKey,
+ guardian: "0",
+ }),
+ }
+
+ const deployAccountPayload = {
+ classHash: PROXY_CONTRACT_CLASS_HASHES[0],
+ constructorCalldata: CallData.compile(constructorCallData),
+ addressSalt: pubKey,
+ }
+
+ return calculateContractAddressFromHash(
+ deployAccountPayload.addressSalt,
+ deployAccountPayload.classHash,
+ deployAccountPayload.constructorCalldata,
+ 0,
+ )
+ }
+
+ public getCairo1AccountContractAddress(
+ accountClassHash: string,
+ pubKey: string,
+ ) {
+ const deployAccountPayload = {
+ classHash: accountClassHash,
+ constructorCalldata: CallData.compile({
+ signer: pubKey,
+ guardian: "0",
+ }),
+ addressSalt: pubKey,
+ }
+
+ return calculateContractAddressFromHash(
+ deployAccountPayload.addressSalt,
+ deployAccountPayload.classHash,
+ deployAccountPayload.constructorCalldata,
+ 0,
+ )
+ }
+
+ public async getUndeployedAccountCairoVersion(
+ baseAccount: BaseWalletAccount,
+ ): Promise {
+ const account = await this.accountSharedService.getAccount(baseAccount)
+
+ if (!account) {
+ throw new AccountError({ code: "NOT_FOUND" })
+ }
+
+ if (!account.needsDeploy) {
+ throw new AccountError({
+ code: "ACCOUNT_ALREADY_DEPLOYED",
+ message:
+ "Account is already deployed. Please use getAccountCairoVersion function to get the Cairo Version of the account",
+ })
+ }
+
+ if (account.type === "multisig") {
+ return "1" // multisig is always Cairo 1
+ }
+
+ const accountClassHash =
+ account.classHash ??
+ (await this.getAccountClassHashForNetwork(account.network, account.type))
+
+ const { publicKey } = await this.getPublicKey(account)
+
+ const cairo1Address = this.getCairo1AccountContractAddress(
+ accountClassHash,
+ publicKey,
+ )
+
+ if (isEqualAddress(account.address, cairo1Address)) {
+ console.log("Undeployed Account is a Cairo 1 account")
+ return "1"
+ }
+
+ const cairo0Address = this.getCairo0AccountContractAddress(
+ STANDARD_CAIRO_0_ACCOUNT_CLASS_HASH, // last Cairo 0 implementation
+ publicKey,
+ )
+
+ if (isEqualAddress(account.address, cairo0Address)) {
+ console.log("Undeployed Account is a Cairo 0 account")
+ return "0"
+ }
+
+ // We don't check for bad class hash 0x01a7820094feaf82d53f53f214b81292d717e7bb9a92bb2488092cd306f3993f
+ // because it's deprecated, so we should not have any account with this class hash that should need this function
+ throw new AccountError({
+ code: "UNDEPLOYED_ACCOUNT_CAIRO_VERSION_NOT_FOUND",
+ })
+ }
+
+ public async getCalculatedMultisigAddress(
+ baseMultisigAccount: BaseMultisigWalletAccount,
+ ): Promise {
+ const multisigAccount = await getMultisigAccountFromBaseWallet(
+ baseMultisigAccount,
+ )
+
+ if (!multisigAccount) {
+ throw new AccountError({ code: "MULTISIG_NOT_FOUND" })
+ }
+
+ const starkPair = await this.getKeyPairByDerivationPath(
+ multisigAccount.signer.derivationPath,
+ )
+
+ const starkPub = starkPair.pubKey
+
+ const accountClassHash =
+ multisigAccount.classHash ??
+ (await this.getAccountClassHashForNetwork(
+ multisigAccount.network,
+ "multisig", // make sure to always use the multisig implementation
+ ))
+
+ const decodedSigners = decodeBase58Array(multisigAccount.signers)
+
+ const constructorCallData = {
+ implementation: accountClassHash,
+ selector: getSelectorFromName("initialize"),
+ calldata: CallData.compile({
+ threshold: baseMultisigAccount.threshold.toString(),
+ signers: decodedSigners,
+ }),
+ }
+
+ const deployMultisigPayload = {
+ classHash: PROXY_CONTRACT_CLASS_HASHES[0],
+ constructorCalldata: CallData.compile(constructorCallData),
+ addressSalt: starkPub,
+ }
+
+ return calculateContractAddressFromHash(
+ deployMultisigPayload.addressSalt,
+ deployMultisigPayload.classHash,
+ deployMultisigPayload.constructorCalldata,
+ 0,
+ )
+ }
+}
diff --git a/packages/extension/src/background/wallet/deployment/interface.ts b/packages/extension/src/background/wallet/deployment/interface.ts
new file mode 100644
index 000000000..23cd05055
--- /dev/null
+++ b/packages/extension/src/background/wallet/deployment/interface.ts
@@ -0,0 +1,35 @@
+import {
+ DeployAccountContractPayload as StarknetDeployAccountContractPayload,
+ EstimateFee as StarknetEstimateFee,
+ InvocationsDetails as StarknetInvocationDetails,
+} from "starknet"
+import {
+ CreateAccountType,
+ CreateWalletAccount,
+ MultisigData,
+ WalletAccount,
+} from "../../../shared/wallet.model"
+
+// Extend to support multichain
+type InvocationsDetails = StarknetInvocationDetails
+type EstimateFee = StarknetEstimateFee
+type DeployAccountContractPayload = StarknetDeployAccountContractPayload
+
+export interface IWalletDeploymentService {
+ deployAccount(
+ walletAccount: WalletAccount,
+ transactionDetails?: InvocationsDetails | undefined,
+ ): Promise<{ account: WalletAccount; txHash: string }>
+ getAccountDeploymentFee(walletAccount: WalletAccount): Promise
+ redeployAccount(
+ account: WalletAccount,
+ ): Promise<{ account: WalletAccount; txHash: string }>
+ getAccountDeploymentPayload(
+ walletAccount: WalletAccount,
+ ): Promise>
+ newAccount(
+ networkId: string,
+ type?: CreateAccountType, // Should not be able to create plugin accounts. Default to argent account
+ multisigPayload?: MultisigData,
+ ): Promise
+}
diff --git a/packages/extension/src/background/wallet/deployment/starknet.service.ts b/packages/extension/src/background/wallet/deployment/starknet.service.ts
new file mode 100644
index 000000000..110750d3b
--- /dev/null
+++ b/packages/extension/src/background/wallet/deployment/starknet.service.ts
@@ -0,0 +1,551 @@
+import {
+ addressSchema,
+ isContractDeployed,
+ isEqualAddress,
+} from "@argent/shared"
+import {
+ CallData,
+ DeployAccountContractPayload,
+ DeployAccountContractTransaction,
+ EstimateFee,
+ InvocationsDetails,
+ hash,
+} from "starknet"
+
+import { withHiddenSelector } from "../../../shared/account/selectors"
+import { PendingMultisig } from "../../../shared/multisig/types"
+import { getMultisigAccountFromBaseWallet } from "../../../shared/multisig/utils/baseMultisig"
+import { getProvider } from "../../../shared/network/provider"
+import { INetworkService } from "../../../shared/network/service/interface"
+import {
+ IObjectStore,
+ IRepository,
+} from "../../../shared/storage/__new/interface"
+import {
+ BaseMultisigWalletAccount,
+ CreateAccountType,
+ CreateWalletAccount,
+ MultisigData,
+ WalletAccount,
+} from "../../../shared/wallet.model"
+
+import { WalletAccountSharedService } from "../account/shared.service"
+
+import {
+ MULTISIG_DERIVATION_PATH,
+ STANDARD_DERIVATION_PATH,
+} from "../../../shared/wallet.service"
+import {
+ getNextPathIndex,
+ getPathForIndex,
+ 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"
+import { WalletSessionService } from "../session/session.service"
+import type { WalletSession } from "../session/walletSession.model"
+import { PROXY_CONTRACT_CLASS_HASHES } from "../starknet.constants"
+import { IWalletDeploymentService } from "./interface"
+import { 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"
+
+const { getSelectorFromName, calculateContractAddressFromHash } = hash
+
+export class WalletDeploymentStarknetService
+ implements IWalletDeploymentService
+{
+ constructor(
+ private readonly walletStore: IRepository,
+ private readonly multisigStore: IRepository,
+ private readonly pendingMultisigStore: IRepository,
+ private readonly sessionService: WalletSessionService,
+ public readonly sessionStore: IObjectStore,
+ private readonly accountSharedService: WalletAccountSharedService,
+ private readonly accountStarknetService: WalletAccountStarknetService,
+ private readonly cryptoStarknetService: WalletCryptoStarknetService,
+ private readonly backupService: WalletBackupService,
+ private readonly networkService: Pick,
+ ) {}
+
+ public async deployAccount(
+ walletAccount: WalletAccount,
+ transactionDetails?: InvocationsDetails | undefined,
+ ): Promise<{ account: WalletAccount; txHash: string }> {
+ const starknetAccount =
+ await this.accountStarknetService.getStarknetAccount(walletAccount)
+
+ if (!("deployAccount" in starknetAccount)) {
+ 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,
+ )
+ }
+
+ if (!isAccountV5(starknetAccount)) {
+ throw new AccountError({ code: "CANNOT_DEPLOY_OLD_ACCOUNTS" })
+ }
+
+ const { transaction_hash } = await starknetAccount.deployAccount(
+ deployAccountPayload,
+ transactionDetails,
+ )
+
+ await this.accountSharedService.selectAccount(walletAccount)
+
+ return { account: walletAccount, txHash: transaction_hash }
+ }
+
+ public async getAccountDeploymentFee(
+ walletAccount: WalletAccount,
+ ): Promise {
+ const starknetAccount =
+ await this.accountStarknetService.getStarknetAccount(walletAccount)
+
+ if (!("deployAccount" in starknetAccount)) {
+ throw new AccountError({ code: "CANNOT_ESTIMATE_DEPLOY_OLD_ACCOUNTS" })
+ }
+
+ const deployAccountPayload =
+ walletAccount.type === "multisig"
+ ? await this.getMultisigDeploymentPayload(walletAccount)
+ : await this.getAccountDeploymentPayload(walletAccount)
+ if (!isAccountV5(starknetAccount)) {
+ throw new AccountError({
+ code: "CANNOT_ESTIMATE_FEE_OLD_ACCOUNTS_DEPLOYMENT",
+ })
+ }
+ return starknetAccount.estimateAccountDeployFee(deployAccountPayload)
+ }
+
+ public async redeployAccount(account: WalletAccount) {
+ if (!(await this.sessionService.isSessionOpen())) {
+ throw new SessionError({ code: "NO_OPEN_SESSION" })
+ }
+ const starknetAccount =
+ await this.accountStarknetService.getStarknetAccount({
+ address: account.address,
+ networkId: account.networkId,
+ })
+ const nonce = await getNonce(account, starknetAccount)
+
+ const deployTransaction = await this.deployAccount(account, { nonce })
+
+ await increaseStoredNonce(account)
+
+ return { account, txHash: deployTransaction.txHash }
+ }
+
+ /** Get the Account Deployment Payload
+ * Use it in the deployAccount and getAccountDeploymentFee methods
+ * @param {WalletAccount} walletAccount
+ */
+ public async getAccountDeploymentPayload(
+ walletAccount: WalletAccount,
+ ): Promise> {
+ const starkPair =
+ await this.cryptoStarknetService.getKeyPairByDerivationPath(
+ walletAccount.signer.derivationPath,
+ )
+
+ const starkPub = starkPair.pubKey
+
+ // Try to get the account class hash from walletAccount if it exists
+ // If it doesn't exist, get it from the network object
+ const accountClassHash =
+ walletAccount.classHash ??
+ (await this.cryptoStarknetService.getAccountClassHashForNetwork(
+ walletAccount.network,
+ walletAccount.type,
+ ))
+
+ const constructorCallData = {
+ implementation: accountClassHash,
+ selector: getSelectorFromName("initialize"),
+ calldata: CallData.compile({ signer: starkPub, guardian: "0" }),
+ }
+
+ const deployAccountPayloadCairo0 = {
+ classHash: PROXY_CONTRACT_CLASS_HASHES[0],
+ contractAddress: walletAccount.address,
+ constructorCalldata: CallData.compile(constructorCallData),
+ addressSalt: starkPub,
+ }
+
+ const deployAccountPayloadCairo1 = {
+ classHash: accountClassHash,
+ contractAddress: walletAccount.address,
+ constructorCalldata: CallData.compile({
+ signer: starkPub,
+ guardian: "0",
+ }),
+ addressSalt: starkPub,
+ }
+
+ let deployAccountPayload
+
+ if (walletAccount.type === "standardCairo0") {
+ deployAccountPayload = deployAccountPayloadCairo0
+ } else {
+ deployAccountPayload = deployAccountPayloadCairo1
+ }
+
+ const calculatedAccountAddress = calculateContractAddressFromHash(
+ deployAccountPayload.addressSalt,
+ deployAccountPayload.classHash,
+ deployAccountPayload.constructorCalldata,
+ 0,
+ )
+
+ if (isEqualAddress(walletAccount.address, calculatedAccountAddress)) {
+ return deployAccountPayload
+ }
+
+ // Warn if the account was created using Cairo 0 implementation and the address does not match
+ console.warn(
+ "Calculated address does not match Cairo 1 account address. Trying Cairo 0 implementation",
+ )
+
+ const cairo0Calldata = CallData.compile({
+ ...constructorCallData,
+ implementation: STANDARD_CAIRO_0_ACCOUNT_CLASS_HASH, // last Cairo 0 implementation
+ })
+
+ // Try to deploy using Cairo 0 implementation
+ const cairo0CalculatedAccountAddress = calculateContractAddressFromHash(
+ deployAccountPayloadCairo0.addressSalt,
+ deployAccountPayloadCairo0.classHash,
+ cairo0Calldata,
+ 0,
+ )
+
+ if (isEqualAddress(walletAccount.address, cairo0CalculatedAccountAddress)) {
+ console.warn("Address matches Cairo 0 implementation")
+ deployAccountPayloadCairo0.constructorCalldata = cairo0Calldata
+ return deployAccountPayloadCairo0
+ }
+
+ console.warn(
+ "Calculated address does not match Cairo 0 account address. Trying old implementation",
+ )
+
+ // In the end, try to deploy using the old implementation
+ const oldCalldata = CallData.compile({
+ ...constructorCallData,
+ implementation:
+ "0x1a7820094feaf82d53f53f214b81292d717e7bb9a92bb2488092cd306f3993f", // old implementation, ask @janek why
+ })
+
+ const oldCalculatedAddress = calculateContractAddressFromHash(
+ deployAccountPayload.addressSalt,
+ deployAccountPayload.classHash,
+ oldCalldata,
+ 0,
+ )
+
+ if (isEqualAddress(oldCalculatedAddress, walletAccount.address)) {
+ console.warn("Address matches old implementation")
+ deployAccountPayload.constructorCalldata = oldCalldata
+ } else {
+ throw new AccountError({ code: "CALCULATED_ADDRESS_NO_MATCH" })
+ }
+
+ return deployAccountPayload
+ }
+
+ public async getMultisigDeploymentPayload(
+ walletAccount: WalletAccount,
+ ): Promise> {
+ const multisigAccount = await getMultisigAccountFromBaseWallet(
+ walletAccount,
+ )
+
+ if (!multisigAccount) {
+ throw new AccountError({ code: "MULTISIG_NOT_FOUND" })
+ }
+
+ const { address, network, signer, threshold, signers } = multisigAccount
+
+ const starkPair =
+ await this.cryptoStarknetService.getKeyPairByDerivationPath(
+ signer.derivationPath,
+ )
+
+ const starkPub = starkPair.pubKey
+
+ const accountClassHash =
+ multisigAccount.classHash ??
+ (await this.cryptoStarknetService.getAccountClassHashForNetwork(
+ network,
+ "multisig", // make sure to always use the multisig implementation
+ ))
+
+ const deployMultisigPayload = {
+ classHash: accountClassHash,
+ contractAddress: address,
+ constructorCalldata: CallData.compile({
+ threshold, // Initial threshold
+ signers, // Initial signers
+ }),
+ addressSalt: starkPub,
+ }
+
+ // Mostly we don't need to calculate the address,
+ // but we do it here just to make sure the address is correct
+ const calculatedMultisigAddress = calculateContractAddressFromHash(
+ deployMultisigPayload.addressSalt,
+ deployMultisigPayload.classHash,
+ deployMultisigPayload.constructorCalldata,
+ 0,
+ )
+
+ if (!isEqualAddress(calculatedMultisigAddress, address)) {
+ throw new AccountError({ code: "CALCULATED_ADDRESS_NO_MATCH" })
+ }
+
+ return deployMultisigPayload
+ }
+
+ // TODO: remove this once testing of cairo 1 is done
+ public async getDeployContractPayloadForAccountIndexCairo0(
+ index: number,
+ networkId: string,
+ ): Promise, "signature">> {
+ const hasSession = await this.sessionService.isSessionOpen()
+ const session = await this.sessionStore.get()
+ const initialised = await this.backupService.isInitialized()
+
+ if (!initialised) {
+ throw new WalletError({ code: "NOT_INITIALIZED" })
+ }
+ if (!hasSession || !session) {
+ throw new SessionError({ code: "NO_OPEN_SESSION" })
+ }
+
+ const network = await this.networkService.getById(networkId)
+ const { pubKey } = getStarkPair(
+ index,
+ session?.secret,
+ STANDARD_DERIVATION_PATH,
+ )
+
+ const accountClassHash =
+ await this.cryptoStarknetService.getAccountClassHashForNetwork(
+ network,
+ "standardCairo0",
+ )
+
+ const payload = {
+ classHash: PROXY_CONTRACT_CLASS_HASHES[0],
+ constructorCalldata: CallData.compile({
+ implementation: accountClassHash,
+ selector: getSelectorFromName("initialize"),
+ calldata: CallData.compile({ signer: pubKey, guardian: "0" }),
+ }),
+ addressSalt: pubKey,
+ }
+
+ return payload
+ }
+
+ public async getDeployContractPayloadForAccountIndex(
+ index: number,
+ networkId: string,
+ ): Promise, "signature">> {
+ const hasSession = await this.sessionService.isSessionOpen()
+ const session = await this.sessionStore.get()
+ const initialised = await this.backupService.isInitialized()
+
+ if (!initialised) {
+ throw new WalletError({ code: "NOT_INITIALIZED" })
+ }
+ if (!hasSession || !session) {
+ throw new SessionError({ code: "NO_OPEN_SESSION" })
+ }
+
+ const network = await this.networkService.getById(networkId)
+ const { pubKey } = getStarkPair(
+ index,
+ session?.secret,
+ STANDARD_DERIVATION_PATH,
+ )
+
+ const accountClassHash =
+ await this.cryptoStarknetService.getAccountClassHashForNetwork(
+ network,
+ "standard",
+ )
+
+ const payload = {
+ classHash: accountClassHash,
+ constructorCalldata: CallData.compile({
+ signer: pubKey,
+ guardian: "0",
+ }),
+ addressSalt: pubKey,
+ }
+
+ return payload
+ }
+
+ public async getDeployContractPayloadForMultisig({
+ signers,
+ threshold,
+ index,
+ networkId,
+ }: {
+ threshold: number
+ signers: string[]
+ index: number
+ networkId: string
+ }): Promise, "signature">> {
+ const hasSession = await this.sessionService.isSessionOpen()
+ const session = await this.sessionStore.get()
+ const initialised = await this.backupService.isInitialized()
+
+ if (!initialised) {
+ throw new WalletError({ code: "NOT_INITIALIZED" })
+ }
+ if (!hasSession || !session) {
+ throw new SessionError({ code: "NO_OPEN_SESSION" })
+ }
+
+ const network = await this.networkService.getById(networkId)
+ const { pubKey } = getStarkPair(
+ index,
+ session?.secret,
+ MULTISIG_DERIVATION_PATH,
+ )
+
+ const accountClassHash =
+ await this.cryptoStarknetService.getAccountClassHashForNetwork(
+ network,
+ "multisig",
+ )
+
+ const payload = {
+ classHash: accountClassHash,
+ constructorCalldata: CallData.compile({
+ threshold, // Initial threshold
+ signers, // Initial signers
+ }),
+ addressSalt: pubKey,
+ }
+
+ return payload
+ }
+
+ public async newAccount(
+ networkId: string,
+ type: CreateAccountType = "standard", // Should not be able to create plugin accounts. Default to argent account
+ multisigPayload?: MultisigData,
+ ): Promise {
+ const session = await this.sessionStore.get()
+ if (!(await this.sessionService.isSessionOpen()) || !session) {
+ throw new SessionError({ code: "NO_OPEN_SESSION" })
+ }
+
+ const network = await this.networkService.getById(networkId)
+
+ const accounts = await this.walletStore.get(withHiddenSelector)
+
+ const pendingMultisigs = await this.pendingMultisigStore.get()
+
+ const accountsOrPendingMultisigs = [...accounts, ...pendingMultisigs]
+
+ const baseDerivationPath =
+ type === "multisig" ? MULTISIG_DERIVATION_PATH : STANDARD_DERIVATION_PATH
+
+ const currentPaths = accountsOrPendingMultisigs
+ .filter(
+ (account) =>
+ account.signer.type === "local_secret" &&
+ account.signer.derivationPath.startsWith(baseDerivationPath) && // filters out invalid account types
+ account.networkId === networkId,
+ )
+ .map((account) => account.signer.derivationPath)
+
+ const index = getNextPathIndex(currentPaths, baseDerivationPath)
+
+ let payload: Omit, "signature">
+
+ if (type === "multisig" && multisigPayload) {
+ payload = await this.getDeployContractPayloadForMultisig({
+ index,
+ networkId,
+ ...multisigPayload,
+ })
+ } else if (type === "standardCairo0") {
+ payload = await this.getDeployContractPayloadForAccountIndexCairo0(
+ index,
+ networkId,
+ )
+ } else {
+ payload = await this.getDeployContractPayloadForAccountIndex(
+ index,
+ networkId,
+ )
+ }
+
+ const accountAddress = calculateContractAddressFromHash(
+ payload.addressSalt,
+ payload.classHash,
+ payload.constructorCalldata,
+ 0,
+ )
+
+ const defaultAccountName =
+ type === "multisig" ? `Multisig ${index + 1}` : `Account ${index + 1}`
+
+ const isDeployed = await isContractDeployed(
+ getProvider(network),
+ accountAddress,
+ )
+
+ const account: CreateWalletAccount = {
+ name: defaultAccountName,
+ network,
+ networkId: network.id,
+ address: accountAddress,
+ signer: {
+ type: "local_secret" as const,
+ derivationPath: getPathForIndex(index, baseDerivationPath),
+ },
+ type,
+ classHash: addressSchema.parse(payload.classHash), // This is only true for new Cairo 1 accounts. For Cairo 0, this is the proxy contract class hash
+ cairoVersion: "1",
+ needsDeploy: !isDeployed,
+ }
+
+ await this.walletStore.upsert([account])
+
+ if (type === "multisig" && multisigPayload) {
+ await this.multisigStore.upsert({
+ address: account.address,
+ networkId: account.networkId,
+ signers: multisigPayload.signers,
+ threshold: multisigPayload.threshold,
+ creator: multisigPayload.creator,
+ publicKey: multisigPayload.publicKey,
+ updatedAt: Date.now(),
+ })
+ }
+
+ await this.accountSharedService.selectAccount(account)
+
+ return account
+ }
+}
diff --git a/packages/extension/src/background/wallet/index.ts b/packages/extension/src/background/wallet/index.ts
new file mode 100644
index 000000000..ea669705b
--- /dev/null
+++ b/packages/extension/src/background/wallet/index.ts
@@ -0,0 +1,246 @@
+import { Account, InvocationsDetails } from "starknet"
+import { Account as Account4__deprecated } from "starknet4-deprecated"
+
+import {
+ ArgentAccountType,
+ BaseMultisigWalletAccount,
+ CreateAccountType,
+ MultisigData,
+} from "../../shared/wallet.model"
+import { PendingMultisig } from "../../shared/multisig/types"
+import { Network } from "../../shared/network"
+import { BaseWalletAccount, WalletAccount } from "../../shared/wallet.model"
+import { WalletAccountSharedService } from "./account/shared.service"
+import { WalletAccountStarknetService } from "./account/starknet.service"
+import { WalletBackupService } from "./backup/backup.service"
+import { WalletCryptoSharedService } from "./crypto/shared.service"
+import { WalletCryptoStarknetService } from "./crypto/starknet.service"
+import { WalletDeploymentStarknetService } from "./deployment/starknet.service"
+import { WalletRecoverySharedService } from "./recovery/shared.service"
+import { WalletSessionService } from "./session/session.service"
+import { WalletRecoveryStarknetService } from "./recovery/starknet.service"
+import { ProgressCallback } from "ethers/lib/utils"
+
+export class Wallet {
+ constructor(
+ private readonly walletAccountSharedService: WalletAccountSharedService,
+ private readonly walletAccountStarknetService: WalletAccountStarknetService,
+ private readonly walletBackupService: WalletBackupService,
+ private readonly walletCryptoSharedService: WalletCryptoSharedService,
+ private readonly walletCryptoStarknetService: WalletCryptoStarknetService,
+ private readonly walletDeploymentStarknetService: WalletDeploymentStarknetService,
+ private readonly walletRecoverySharedService: WalletRecoverySharedService,
+ private readonly walletRecoveryStarknetService: WalletRecoveryStarknetService,
+ private readonly walletSessionService: WalletSessionService,
+ ) {}
+
+ // WalletAccountSharedService
+ public async getDefaultAccountName(
+ networkId: string,
+ type: CreateAccountType,
+ ) {
+ return this.walletAccountSharedService.getDefaultAccountName(
+ networkId,
+ type,
+ )
+ }
+ public async getAccount(
+ selector: BaseWalletAccount,
+ ): Promise {
+ return this.walletAccountSharedService.getAccount(selector)
+ }
+ public async getSelectedAccount(): Promise {
+ return this.walletAccountSharedService.getSelectedAccount()
+ }
+ public async selectAccount(accountIdentifier?: BaseWalletAccount | null) {
+ return this.walletAccountSharedService.selectAccount(accountIdentifier)
+ }
+ public async getMultisigAccount(selector: BaseWalletAccount) {
+ return this.walletAccountSharedService.getMultisigAccount(selector)
+ }
+
+ // WalletAccountStarknetService
+ public async getStarknetAccount(
+ selector: BaseWalletAccount,
+ useLatest = false,
+ ) {
+ return this.walletAccountStarknetService.getStarknetAccount(
+ selector,
+ useLatest,
+ )
+ }
+ public async getSelectedStarknetAccount() {
+ return this.walletAccountStarknetService.getSelectedStarknetAccount()
+ }
+ public async newPendingMultisig(networkId: string): Promise {
+ return this.walletAccountStarknetService.newPendingMultisig(networkId)
+ }
+ public getStarknetAccountOfType(
+ account: Account | Account4__deprecated,
+ type: ArgentAccountType,
+ ) {
+ return this.walletAccountStarknetService.getStarknetAccountOfType(
+ account,
+ type,
+ )
+ }
+
+ // WalletBackupService
+ public async getBackup() {
+ return this.walletBackupService.getBackup()
+ }
+ public async isInitialized() {
+ return this.walletBackupService.isInitialized()
+ }
+ public async importBackup(backup: string) {
+ return this.walletBackupService.importBackup(backup)
+ }
+
+ // WalletCryptoSharedService
+ public async restoreSeedPhrase(seedPhrase: string, newPassword: string) {
+ return this.walletCryptoSharedService.restoreSeedPhrase(
+ seedPhrase,
+ newPassword,
+ )
+ }
+ public async getSeedPhrase(): Promise {
+ return this.walletCryptoSharedService.getSeedPhrase()
+ }
+
+ // WalletCryptoStarknetService
+ public async getKeyPairByDerivationPath(derivationPath: string) {
+ return this.walletCryptoStarknetService.getKeyPairByDerivationPath(
+ derivationPath,
+ )
+ }
+ public async getSignerForAccount(account: WalletAccount) {
+ return this.walletCryptoStarknetService.getSignerForAccount(account)
+ }
+ public async getPrivateKey(
+ baseWalletAccount: BaseWalletAccount,
+ ): Promise {
+ return this.walletCryptoStarknetService.getPrivateKey(baseWalletAccount)
+ }
+ public async getPublicKey(baseAccount?: BaseWalletAccount) {
+ return this.walletCryptoStarknetService.getPublicKey(baseAccount)
+ }
+ public async getNextPublicKeyForMultisig(networkId: string) {
+ return this.walletCryptoStarknetService.getNextPublicKeyForMultisig(
+ networkId,
+ )
+ }
+ public async getPublicKeysBufferForMultisig(start: number, buffer: number) {
+ return this.walletCryptoStarknetService.getPublicKeysBufferForMultisig(
+ start,
+ buffer,
+ )
+ }
+ public async getUndeployedAccountCairoVersion(
+ baseAccount: BaseWalletAccount,
+ ) {
+ return this.walletCryptoStarknetService.getUndeployedAccountCairoVersion(
+ baseAccount,
+ )
+ }
+
+ public async getCalculatedMultisigAddress(
+ baseMultisigAccount: BaseMultisigWalletAccount,
+ ) {
+ return this.walletCryptoStarknetService.getCalculatedMultisigAddress(
+ baseMultisigAccount,
+ )
+ }
+ async getAccountClassHashForNetwork(
+ network: Network,
+ accountType: ArgentAccountType,
+ ): Promise {
+ return this.walletCryptoStarknetService.getAccountClassHashForNetwork(
+ network,
+ accountType,
+ )
+ }
+
+ // WalletDeploymentStarknetService
+ public async deployAccount(
+ walletAccount: WalletAccount,
+ transactionDetails?: InvocationsDetails | undefined,
+ ) {
+ return this.walletDeploymentStarknetService.deployAccount(
+ walletAccount,
+ transactionDetails,
+ )
+ }
+ public async getAccountDeploymentFee(walletAccount: WalletAccount) {
+ return this.walletDeploymentStarknetService.getAccountDeploymentFee(
+ walletAccount,
+ )
+ }
+ public async redeployAccount(account: WalletAccount) {
+ return this.walletDeploymentStarknetService.redeployAccount(account)
+ }
+ public async getAccountDeploymentPayload(walletAccount: WalletAccount) {
+ return this.walletDeploymentStarknetService.getAccountDeploymentPayload(
+ walletAccount,
+ )
+ }
+ public async getMultisigDeploymentPayload(walletAccount: WalletAccount) {
+ return this.walletDeploymentStarknetService.getMultisigDeploymentPayload(
+ walletAccount,
+ )
+ }
+ public async getDeployContractPayloadForAccountIndex(
+ index: number,
+ networkId: string,
+ ) {
+ return this.walletDeploymentStarknetService.getDeployContractPayloadForAccountIndex(
+ index,
+ networkId,
+ )
+ }
+ public async getDeployContractPayloadForMultisig(props: {
+ threshold: number
+ signers: string[]
+ index: number
+ networkId: string
+ }) {
+ return this.walletDeploymentStarknetService.getDeployContractPayloadForMultisig(
+ props,
+ )
+ }
+ public async newAccount(
+ networkId: string,
+ type: CreateAccountType = "standard",
+ multisigPayload?: MultisigData,
+ ) {
+ return this.walletDeploymentStarknetService.newAccount(
+ networkId,
+ type,
+ multisigPayload,
+ )
+ }
+
+ // WalletRecoverySharedService
+ public async discoverAccounts() {
+ return this.walletRecoverySharedService.discoverAccounts()
+ }
+
+ // WalletSessionService
+ public async isSessionOpen() {
+ return this.walletSessionService.isSessionOpen()
+ }
+ public async startSession(
+ password: string,
+ progressCallback?: ProgressCallback,
+ ) {
+ return this.walletSessionService.startSession(password, progressCallback)
+ }
+ public async checkPassword(password: string): Promise {
+ return this.walletSessionService.checkPassword(password)
+ }
+ public async lock() {
+ return this.walletSessionService.lock()
+ }
+ public async setSession(secret: string, password: string) {
+ return this.walletSessionService.setSession(secret, password)
+ }
+}
diff --git a/packages/extension/src/background/accounts.ts b/packages/extension/src/background/wallet/loadContracts.ts
similarity index 72%
rename from packages/extension/src/background/accounts.ts
rename to packages/extension/src/background/wallet/loadContracts.ts
index 67fdb3970..1e7ed3e4c 100644
--- a/packages/extension/src/background/accounts.ts
+++ b/packages/extension/src/background/wallet/loadContracts.ts
@@ -1,5 +1,5 @@
-import ArgentAccountCompiledContractUrl from "../contracts/ArgentAccount.txt"
-import ProxyCompiledContractUrl from "../contracts/Proxy.txt"
+import ArgentAccountCompiledContractUrl from "../../contracts/ArgentAccount.txt"
+import ProxyCompiledContractUrl from "../../contracts/Proxy.txt"
export type LoadContracts = (
derivationPathBase?: string,
diff --git a/packages/extension/src/background/wallet/recovery/interface.ts b/packages/extension/src/background/wallet/recovery/interface.ts
new file mode 100644
index 000000000..7cfbdfec2
--- /dev/null
+++ b/packages/extension/src/background/wallet/recovery/interface.ts
@@ -0,0 +1,15 @@
+import { Network } from "../../../shared/network"
+import { BaseWalletAccount, WalletAccount } from "../../../shared/wallet.model"
+
+export interface IWalletRecoveryService {
+ restoreAccountsFromWallet(
+ secret: string,
+ network: Network,
+ ): Promise
+}
+
+export const Recovered = Symbol("Recovered")
+
+export type Events = {
+ [Recovered]: BaseWalletAccount[]
+}
diff --git a/packages/extension/src/background/wallet/recovery/shared.service.test.ts b/packages/extension/src/background/wallet/recovery/shared.service.test.ts
new file mode 100644
index 000000000..c00849aad
--- /dev/null
+++ b/packages/extension/src/background/wallet/recovery/shared.service.test.ts
@@ -0,0 +1,115 @@
+import { bytesToHex, hexToBytes } from "@noble/hashes/utils"
+import { HDKey } from "@scure/bip32"
+import { generateMnemonic, mnemonicToSeedSync } from "@scure/bip39"
+import { wordlist } from "@scure/bip39/wordlists/english"
+import { grindKey } from "micro-starknet"
+import { encode } from "starknet"
+import { Mock } from "vitest"
+
+import { defaultNetworks } from "../../../shared/network"
+import {
+ IObjectStore,
+ IRepository,
+} from "../../../shared/storage/__new/interface"
+import { WalletAccount } from "../../../shared/wallet.model"
+import { WalletSession, WalletStorageProps } from "../account/shared.service"
+import {
+ emitterMock,
+ getSessionStoreMock,
+ getStoreMock,
+ getWalletStoreMock,
+} from "../test.utils"
+import { WalletRecoverySharedService } from "./shared.service"
+import { WalletRecoveryStarknetService } from "./starknet.service"
+import { WalletError } from "../../../shared/errors/wallet"
+
+vi.mock("ethers", async () => {
+ const actual = await vi.importActual("ethers")
+
+ return {
+ ...(actual as object),
+ Wallet: vi.fn().mockReturnValue({ privateKey: "abc" }),
+ }
+})
+
+describe("WalletRecoverySharedService", () => {
+ let service: WalletRecoverySharedService
+ let storeMock: IObjectStore
+ let walletStoreMock: IRepository
+ let sessionStoreMock: IObjectStore
+ let chainRecoveryServiceMock: WalletRecoveryStarknetService
+ let networkServiceMock: { getById: Mock }
+ beforeEach(() => {
+ vi.clearAllMocks()
+ networkServiceMock = {
+ getById: vi.fn(),
+ }
+ })
+
+ it("should throw an error when session secret is not defined", async () => {
+ storeMock = getStoreMock()
+ walletStoreMock = getWalletStoreMock()
+ sessionStoreMock = getSessionStoreMock()
+
+ service = new WalletRecoverySharedService(
+ emitterMock,
+ storeMock,
+ walletStoreMock,
+ sessionStoreMock,
+ networkServiceMock,
+ chainRecoveryServiceMock,
+ )
+
+ await expect(service.discoverAccounts()).rejects.toThrow(
+ new WalletError({ code: "NOT_INITIALIZED" }),
+ )
+ })
+
+ it("should discover accounts", async () => {
+ const mnemonic = generateMnemonic(wordlist)
+ const seed = mnemonicToSeedSync(mnemonic)
+ const preGrindPrivateKey =
+ HDKey.fromMasterSeed(seed).derive("m/44'/9004'/0'/0/0").privateKey
+
+ if (!preGrindPrivateKey) {
+ throw new Error("Could not generate private key")
+ }
+
+ const grindedKey = grindKey(preGrindPrivateKey)
+ const paddedKey = grindedKey.padStart(64, "0")
+ const privateKey = hexToBytes(encode.removeHexPrefix(paddedKey))
+ const mockSession = {
+ secret: encode.addHexPrefix(bytesToHex(privateKey)),
+ password: "password",
+ }
+
+ storeMock = getStoreMock()
+ walletStoreMock = getWalletStoreMock()
+ sessionStoreMock = getSessionStoreMock({
+ get: vi.fn(() => Promise.resolve(mockSession)),
+ })
+ const mockAccount = { name: "mockAccount" }
+ networkServiceMock = {
+ getById: vi.fn().mockResolvedValue({ networkId: "networkId" }),
+ }
+ chainRecoveryServiceMock = {
+ restoreAccountsFromWallet: vi.fn(() =>
+ Promise.resolve([mockAccount] as WalletAccount[]),
+ ),
+ } as unknown as WalletRecoveryStarknetService
+ service = new WalletRecoverySharedService(
+ emitterMock,
+ storeMock,
+ walletStoreMock,
+ sessionStoreMock,
+ networkServiceMock,
+ chainRecoveryServiceMock,
+ )
+ const result = await service.discoverAccounts()
+ const mockAccounts = new Array(defaultNetworks.length).fill(mockAccount)
+
+ expect(result).toEqual(mockAccounts)
+ expect(walletStoreMock.upsert).toHaveBeenCalledWith(mockAccounts)
+ expect(storeMock.set).toHaveBeenCalledWith({ discoveredOnce: true })
+ })
+})
diff --git a/packages/extension/src/background/wallet/recovery/shared.service.ts b/packages/extension/src/background/wallet/recovery/shared.service.ts
new file mode 100644
index 000000000..de91d3f9f
--- /dev/null
+++ b/packages/extension/src/background/wallet/recovery/shared.service.ts
@@ -0,0 +1,53 @@
+import { ethers } from "ethers"
+
+import { defaultNetworks } from "../../../shared/network"
+import { INetworkService } from "../../../shared/network/service/interface"
+import {
+ IObjectStore,
+ 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"
+
+export class WalletRecoverySharedService {
+ constructor(
+ readonly emitter: Emittery,
+ public readonly store: IObjectStore,
+ private readonly walletStore: IRepository,
+ public readonly sessionStore: IObjectStore,
+ private readonly networkService: Pick