From 8629116972dbb3e9166fe9c033bc5f0ebe9540ee Mon Sep 17 00:00:00 2001 From: Joe Pegler Date: Mon, 13 Jan 2025 17:21:35 +0000 Subject: [PATCH 1/6] feat: bic-333_add_mee --- .env.example | 5 +- .github/workflows/funded-tests.yml | 31 + .github/workflows/unit-tests.yml | 2 +- .gitignore | 5 +- README.md | 4 +- biome.json | 3 +- package.json | 1 + scripts/fetch:tokenMap.ts | 209 +++ .../account/toNexusAccount.addresses.test.ts | 74 +- src/sdk/account/toNexusAccount.test.ts | 4 +- src/sdk/account/toNexusAccount.ts | 88 +- src/sdk/account/utils/Utils.ts | 8 + .../account/utils/explorer/explorer.test.ts | 66 + src/sdk/account/utils/explorer/explorer.ts | 61 + .../account/utils/getCounterFactualAddress.ts | 112 +- src/sdk/account/utils/getFactoryData.ts | 83 ++ .../account/utils/getMultichainContract.ts | 209 +++ .../utils/toMultiChainNexusAccount.test.ts | 117 ++ .../account/utils/toMultiChainNexusAccount.ts | 76 + .../clients/createBicoBundlerClient.test.ts | 2 +- src/sdk/clients/createBundlerClient.test.ts | 2 +- src/sdk/clients/createHttpClient.test.ts | 45 + src/sdk/clients/createHttpClient.ts | 87 ++ src/sdk/clients/createMeeClient.test.ts | 247 ++++ src/sdk/clients/createMeeClient.ts | 49 + .../clients/createSmartAccountClient.test.ts | 8 +- src/sdk/clients/createSmartAccountClient.ts | 10 +- .../erc7579/erc7579.decorators.test.ts | 2 +- .../decorators/erc7579/installModule.ts | 4 +- .../clients/decorators/mee/execute.test.ts | 78 + src/sdk/clients/decorators/mee/execute.ts | 30 + .../decorators/mee/executeQuote.test.ts | 75 + .../clients/decorators/mee/executeQuote.ts | 28 + .../mee/executeSignedFusionQuote.ts | 47 + .../decorators/mee/executeSignedQuote.test.ts | 78 + .../decorators/mee/executeSignedQuote.ts | 36 + .../clients/decorators/mee/getQuote.test.ts | 81 ++ src/sdk/clients/decorators/mee/getQuote.ts | 291 ++++ src/sdk/clients/decorators/mee/index.ts | 181 +++ .../decorators/mee/signFusionQuote.test.ts | 114 ++ .../clients/decorators/mee/signFusionQuote.ts | 105 ++ .../clients/decorators/mee/signQuote.test.ts | 67 + src/sdk/clients/decorators/mee/signQuote.ts | 69 + .../mee/waitForSupertransactionReceipt.ts | 113 ++ .../smartAccount/account.decorators.test.ts | 2 +- .../smartAccount/debugUserOperation.ts | 5 +- .../tokenPaymaster/getTokenPaymasterQuotes.ts | 3 +- src/sdk/constants/abi/AccountFactory.ts | 323 +++++ src/sdk/constants/abi/K1ValidatorFactory.ts | 36 + .../constants}/abi/NexusBootstrapAbi.ts | 0 src/sdk/constants/index.ts | 9 +- .../constants/tokens/__AUTO_GENERATED__.ts | 1254 +++++++++++++++++ src/sdk/constants/tokens/index.ts | 1 + src/sdk/constants/tokens/tokens.test.ts | 64 + .../modules/k1Validator/toK1Validator.test.ts | 2 +- .../decorators/ownables.decorators.test.ts | 2 +- .../toOwnableValidator.dx.test.ts | 2 +- .../toOwnableValidator.executor.test.ts | 2 +- .../toOwnableValidator.test.ts | 2 +- .../smartSessions.decorators.test.ts | 2 +- ...oSmartSessionValidator.enable.mode.test.ts | 5 +- src/sdk/modules/utils/Types.ts | 12 + src/test/README.md | 2 +- src/test/__contracts/abi/index.ts | 1 - src/test/globalSetup.ts | 12 +- src/test/testSetup.ts | 23 +- src/test/testUtils.ts | 17 +- 67 files changed, 4630 insertions(+), 158 deletions(-) create mode 100644 .github/workflows/funded-tests.yml create mode 100644 scripts/fetch:tokenMap.ts create mode 100644 src/sdk/account/utils/explorer/explorer.test.ts create mode 100644 src/sdk/account/utils/explorer/explorer.ts create mode 100644 src/sdk/account/utils/getFactoryData.ts create mode 100644 src/sdk/account/utils/getMultichainContract.ts create mode 100644 src/sdk/account/utils/toMultiChainNexusAccount.test.ts create mode 100644 src/sdk/account/utils/toMultiChainNexusAccount.ts create mode 100644 src/sdk/clients/createHttpClient.test.ts create mode 100644 src/sdk/clients/createHttpClient.ts create mode 100644 src/sdk/clients/createMeeClient.test.ts create mode 100644 src/sdk/clients/createMeeClient.ts create mode 100644 src/sdk/clients/decorators/mee/execute.test.ts create mode 100644 src/sdk/clients/decorators/mee/execute.ts create mode 100644 src/sdk/clients/decorators/mee/executeQuote.test.ts create mode 100644 src/sdk/clients/decorators/mee/executeQuote.ts create mode 100644 src/sdk/clients/decorators/mee/executeSignedFusionQuote.ts create mode 100644 src/sdk/clients/decorators/mee/executeSignedQuote.test.ts create mode 100644 src/sdk/clients/decorators/mee/executeSignedQuote.ts create mode 100644 src/sdk/clients/decorators/mee/getQuote.test.ts create mode 100644 src/sdk/clients/decorators/mee/getQuote.ts create mode 100644 src/sdk/clients/decorators/mee/index.ts create mode 100644 src/sdk/clients/decorators/mee/signFusionQuote.test.ts create mode 100644 src/sdk/clients/decorators/mee/signFusionQuote.ts create mode 100644 src/sdk/clients/decorators/mee/signQuote.test.ts create mode 100644 src/sdk/clients/decorators/mee/signQuote.ts create mode 100644 src/sdk/clients/decorators/mee/waitForSupertransactionReceipt.ts create mode 100644 src/sdk/constants/abi/AccountFactory.ts create mode 100644 src/sdk/constants/abi/K1ValidatorFactory.ts rename src/{test/__contracts => sdk/constants}/abi/NexusBootstrapAbi.ts (100%) create mode 100644 src/sdk/constants/tokens/__AUTO_GENERATED__.ts create mode 100644 src/sdk/constants/tokens/index.ts create mode 100644 src/sdk/constants/tokens/tokens.test.ts diff --git a/.env.example b/.env.example index 2c074f1c..be9ffdef 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,6 @@ PRIVATE_KEY= CHAIN_ID=84532 -ALT_CHAIN_ID=11155111 +MAINNET_CHAIN_ID=11155111 RPC_URL= BUNDLER_URL= BICONOMY_SDK_DEBUG=false @@ -8,4 +8,5 @@ PAYMASTER_URL= PIMLICO_API_KEY= TENDERLY_API_KEY= TENDERLY_ACCOUNT_SLUG= -TENDERLY_PROJECT_SLUG= \ No newline at end of file +TENDERLY_PROJECT_SLUG= +RUN_PAID_TESTS=false \ No newline at end of file diff --git a/.github/workflows/funded-tests.yml b/.github/workflows/funded-tests.yml new file mode 100644 index 00000000..bc44ad32 --- /dev/null +++ b/.github/workflows/funded-tests.yml @@ -0,0 +1,31 @@ +name: paid-tests +on: + workflow_dispatch: + pull_request_review: + types: [submitted] +jobs: + paid-tests: + name: paid-tests + permissions: write-all + runs-on: ubuntu-latest + concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-paid-tests + cancel-in-progress: true + steps: + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - uses: actions/checkout@v4 + + - name: Install dependencies + uses: ./.github/actions/install-dependencies + + - name: Run the tests + run: bun run test + env: + PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} + CHAIN_ID: ${{ secrets.CHAIN_ID }} + CI: true + RUN_PAID_TESTS: true + diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 83d440bf..25bfd1b7 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -29,5 +29,5 @@ jobs: PAYMASTER_URL: ${{ secrets.PAYMASTER_URL }} BUNDLER_URL: ${{ secrets.BUNDLER_URL }} CHAIN_ID: 84532 - ALT_CHAIN_ID: 11155420 + MAINNET_CHAIN_ID: 11155420 CI: true diff --git a/.gitignore b/.gitignore index 3a8fedd3..1e02182e 100644 --- a/.gitignore +++ b/.gitignore @@ -177,4 +177,7 @@ dist docs -sessionStorageData \ No newline at end of file +sessionStorageData + +# Data from scraping scripts +.data diff --git a/README.md b/README.md index e171d2fb..82ff0abc 100644 --- a/README.md +++ b/README.md @@ -67,8 +67,8 @@ bun install --frozen-lockfile # Run all tests bun run test -# Run tests for a specific module -bun run test -t=smartSessions +# Run tests for a specific subset of tests (by test description) +bun run test -t=mee ``` For detailed information about the testing framework, network configurations, and debugging guidelines, please refer to our [Testing Documentation](./src/test/README.md). diff --git a/biome.json b/biome.json index 26269399..10f647f5 100644 --- a/biome.json +++ b/biome.json @@ -28,7 +28,8 @@ "noExplicitAny": "warn" }, "style": { - "noUnusedTemplateLiteral": "warn" + "noUnusedTemplateLiteral": "warn", + "noNonNullAssertion": "off" } } }, diff --git a/package.json b/package.json index e30b042a..ab924b7b 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "test:watch": "bun run test dev", "playground": "RUN_PLAYGROUND=true vitest -c ./src/test/vitest.config.ts -t=playground", "playground:watch": "RUN_PLAYGROUND=true bun run test -t=playground --watch", + "fetch:tokenMap": "bun run scripts/fetch:tokenMap.ts && bun run lint:fix", "size": "size-limit", "docs": "typedoc --tsconfig ./tsconfig/tsconfig.esm.json", "docs:deploy": "bun run docs && gh-pages -d docs", diff --git a/scripts/fetch:tokenMap.ts b/scripts/fetch:tokenMap.ts new file mode 100644 index 00000000..923837d1 --- /dev/null +++ b/scripts/fetch:tokenMap.ts @@ -0,0 +1,209 @@ +import fs from "node:fs" +import path from "node:path" +import { getAddress, isHex } from "viem" +import { baseSepolia } from "viem/chains" +import coinDataFromJson from "../.data/coinData.json" +import coinIdsFromJson from "../.data/coinIds.json" +import networkIdMap from "../.data/networkIdMap.json" + +// biome-ignore lint/suspicious/noExplicitAny: +async function writeJsonToFile(data: any, filename: string) { + // Create directory path if it doesn't exist + const dirname = path.dirname(filename) + fs.mkdirSync(dirname, { recursive: true }) + fs.writeFileSync(filename, JSON.stringify(data, null, 2)) +} + +async function writeTsToFile(data: string, filename: string) { + // Create directory path if it doesn't exist + const dirname = path.dirname(filename) + fs.mkdirSync(dirname, { recursive: true }) + fs.writeFileSync(filename, data) +} + +async function getErc20CoinsByMarketCap(limit = 200): Promise { + if (coinIdsFromJson.length > 0) return coinIdsFromJson + + const COINS_TO_OMIT_FROM_COIN_DATA = coinDataFromJson + .map((coin) => coin?.id) + .filter(Boolean) + + const COINS_TO_OMIT = ["ethereum", ...COINS_TO_OMIT_FROM_COIN_DATA] + const fetchResponse = await fetch( + `https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&category=ethereum-ecosystem&order=market_cap_desc&per_page=${limit}&page=1&sparkline=false`, + { method: "GET", headers: { accept: "application/json" } } + ) + const coinResponse = await fetchResponse.json() + const coinIds = coinResponse + .filter((coin) => !COINS_TO_OMIT.includes(coin.id)) + .map((coin) => coin.id) + writeJsonToFile(coinIds, path.join(__dirname, "../.data/coinIds.json")) + return coinIds +} + +type Coin = { + id: string + symbol: string + name: string + platforms: Record +} +async function getCoinDataById(coinIds_: string[]): Promise { + const COINS_TO_OMIT = coinDataFromJson.map((coin) => coin?.id).filter(Boolean) + const coinsToFetch = coinIds_.filter( + (coinId) => !COINS_TO_OMIT.includes(coinId) + ) + const coinsAlreadyFetched = coinDataFromJson.filter((coin) => coin?.id) + + const coinResponses = [] + // Run promises in sequence + for (const coinId of coinsToFetch) { + try { + const coinResponse = await fetch( + `https://api.coingecko.com/api/v3/coins/${coinId}`, + { + method: "GET", + headers: { accept: "application/json" } + } + ) + await new Promise((resolve) => setTimeout(resolve, 3000)) // Sleep for 3 seconds to avoid rate limiting + // @ts-ignore + coinResponses.push(await coinResponse.json()) + } catch (error) { + // Likely rate limited. Skip this coin and continue + console.error(`Error fetching coin data for ${coinId}:`, error) + } + } + + const coinData = coinResponses.map(({ id, symbol, name, platforms }) => ({ + id, + symbol, + name, + platforms + })) + + const newCoinData = [...coinsAlreadyFetched, ...coinData].filter( + (v) => Object.keys(v ?? {}).length > 0 + ) + + writeJsonToFile(newCoinData, path.join(__dirname, "../.data/coinData.json")) + + // @ts-ignore + return newCoinData +} + +type Networks = Record +async function getNetworkIds(): Promise { + if (Object.keys(networkIdMap).length > 0) return networkIdMap + const fetchResponse = await fetch( + "https://api.coingecko.com/api/v3/asset_platforms", + { method: "GET", headers: { accept: "application/json" } } + ) + const networks = await fetchResponse.json() + const newNetworkIdMap = networks.reduce((acc, network) => { + const networkId = Number(network?.chain_identifier ?? 0) + const networkName = network?.id + if (!!networkId && !!networkName) { + acc[networkName] = networkId + } + return acc + }, {}) + writeJsonToFile( + newNetworkIdMap, + path.join(__dirname, "../.data/networkIdMap.json") + ) + return newNetworkIdMap +} + +type FinalisedCoin = { + id: string + symbol: string + name: string + networks: Record +} + +function sanitiseCoins(networks: Networks, coinData: Coin[]): FinalisedCoin[] { + const HARDCODE = { + usdc: { + [baseSepolia.id]: "0x036CbD53842c5426634e7929541eC2318f3dCF7e" + } + } + + // @ts-ignore + return coinData + .filter(Boolean) + .map(({ id, symbol, name, platforms }) => { + const networkNames = Object.keys(platforms ?? {}) + const sanitisedNetworks = networkNames.reduce( + (acc, platform) => { + const networkId = networks[platform] + const address = platforms[platform] + if (networkId && isHex(address)) acc[networkId] = getAddress(address) + return acc + }, + (HARDCODE[symbol] ?? {}) as Record + ) + + if ( + !id || + !symbol || + !name || + !platforms || + Object.keys(sanitisedNetworks ?? {}).length <= 1 + ) { + return undefined + } + return { + id, + symbol, + name, + networks: sanitisedNetworks + } + }) + .filter(Boolean) +} + +async function generateTokenConstants(coins: FinalisedCoin[]) { + const warning = + "// N.B. This file is auto-generated by the fetch:tokenMap.ts script. Do not edit it manually. \n// Instead, edit the script and run it again, or hardcode your new tokens in the index file that imports this file" + + const startOfFile = `import { getMultichainContract } from "../../account/utils/getMultichainContract";\nimport { erc20Abi } from "viem"\n\n` + + const tokenConstants = coins + .map((coin) => { + const { symbol, networks } = coin + + const safeId = symbol + .replace(/[^a-zA-Z0-9]/g, "_") // Replace any non-alphanumeric chars with underscore + .replace(/^[0-9]/, "_$&") // If starts with number, prefix with underscore + .toUpperCase() + + return `export const mc${safeId} = getMultichainContract({ + abi: erc20Abi, + deployments: [${Object.entries(networks) + .map(([networkId, address]) => `['${address}', ${networkId}]`) + .join(",\n ")}] + })` + }) + .join("\n\n") + + writeTsToFile( + `${warning}\n\n${startOfFile}\n\n${tokenConstants}`, + path.join(__dirname, "../src/sdk/constants/tokens/__AUTO_GENERATED__.ts") + ) +} + +async function main() { + const coinIds = await getErc20CoinsByMarketCap(200) + const coinData = await getCoinDataById(coinIds) + console.log("coinData.length", coinData.length) + + const networks = await getNetworkIds() + const coins = sanitiseCoins(networks, coinData) + console.log("coins.length", coins.length) + + await writeJsonToFile(coins, path.join(__dirname, "../.data/coins.json")) + + await generateTokenConstants(coins) +} + +main().catch((err) => console.error(err)) diff --git a/src/sdk/account/toNexusAccount.addresses.test.ts b/src/sdk/account/toNexusAccount.addresses.test.ts index ffc0dd4f..0b3d3ab4 100644 --- a/src/sdk/account/toNexusAccount.addresses.test.ts +++ b/src/sdk/account/toNexusAccount.addresses.test.ts @@ -2,11 +2,16 @@ import { http, type Address, type Chain, + Hex, type LocalAccount, type PublicClient, type WalletClient, - createWalletClient + createWalletClient, + isHex, + pad, + toHex } from "viem" +import { privateKeyToAccount } from "viem/accounts" import { base, baseSepolia } from "viem/chains" import { afterAll, beforeAll, describe, expect, test } from "vitest" import { toNetwork } from "../../test/testSetup" @@ -22,12 +27,15 @@ import { createSmartAccountClient } from "../clients/createSmartAccountClient" import { + MEE_VALIDATOR_ADDRESS, + NEXUS_ACCOUNT_FACTORY, RHINESTONE_ATTESTER_ADDRESS, + TEMP_MEE_ATTESTER_ADDR, TEST_ADDRESS_K1_VALIDATOR_ADDRESS, TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS } from "../constants" -import type { NexusAccount } from "./toNexusAccount" -import { getCounterFactualAddress } from "./utils" +import { type NexusAccount, toNexusAccount } from "./toNexusAccount" +import { getK1CounterFactualAddress } from "./utils" describe("nexus.account.addresses", async () => { let network: NetworkConfig @@ -44,7 +52,7 @@ describe("nexus.account.addresses", async () => { let walletClient: WalletClient beforeAll(async () => { - network = await toNetwork() + network = await toNetwork("BESPOKE_ANVIL_NETWORK_FORKING_BASE_SEPOLIA") chain = network.chain bundlerUrl = network.bundlerUrl @@ -63,7 +71,7 @@ describe("nexus.account.addresses", async () => { chain, transport: http(), bundlerTransport: http(bundlerUrl), - k1ValidatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, + validatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS }) @@ -75,15 +83,15 @@ describe("nexus.account.addresses", async () => { test("should check account address", async () => { nexusAccountAddress = await nexusClient.account.getCounterFactualAddress() - const counterfactualAddressFromHelper = await getCounterFactualAddress( - testClient as unknown as PublicClient, - eoaAccount.address, - true, - 0n, - [RHINESTONE_ATTESTER_ADDRESS], - 1, - TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS - ) + const counterfactualAddressFromHelper = await getK1CounterFactualAddress({ + publicClient: testClient as unknown as PublicClient, + signerAddress: eoaAccount.address, + isTestnet: true, + index: 0n, + attesters: [RHINESTONE_ATTESTER_ADDRESS], + threshold: 1, + factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS + }) const gottenAddress = await nexusClient.account.getAddress() expect(counterfactualAddressFromHelper).toBe(nexusAccountAddress) expect(nexusAccount.address).toBe(nexusAccountAddress) @@ -93,15 +101,15 @@ describe("nexus.account.addresses", async () => { test("should check addresses after fund and deploy", async () => { await fundAndDeployClients(testClient, [nexusClient]) - const counterfactualAddressFromHelper = await getCounterFactualAddress( - testClient as unknown as PublicClient, - eoaAccount.address, - true, - 0n, - [RHINESTONE_ATTESTER_ADDRESS], - 1, - TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS - ) + const counterfactualAddressFromHelper = await getK1CounterFactualAddress({ + publicClient: testClient as unknown as PublicClient, + signerAddress: eoaAccount.address, + isTestnet: true, + index: 0n, + attesters: [RHINESTONE_ATTESTER_ADDRESS], + threshold: 1, + factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS + }) const gottenAddress = await nexusClient.account.getAddress() expect(counterfactualAddressFromHelper).toBe(nexusAccountAddress) expect(nexusAccount.address).toBe(nexusAccountAddress) @@ -148,4 +156,24 @@ describe("nexus.account.addresses", async () => { expect(testnetAddress).not.toBe(mainnetAddress) }) + + test("should test a mee account", async () => { + const eoaAccount = privateKeyToAccount(`0x${process.env.PRIVATE_KEY}`) + + const meeAccount = await toNexusAccount({ + signer: eoaAccount, + chain: baseSepolia, + transport: http(), + validatorAddress: MEE_VALIDATOR_ADDRESS, + factoryAddress: NEXUS_ACCOUNT_FACTORY, + attesters: [TEMP_MEE_ATTESTER_ADDR] + }) + + const meeAddress = await meeAccount.getAddress() + const meeCounterfactualAddress = await meeAccount.getCounterFactualAddress() + + expect(isHex(meeAddress)).toBe(true) + expect(isHex(meeCounterfactualAddress)).toBe(true) + expect(meeAddress).toBe(meeCounterfactualAddress) + }) }) diff --git a/src/sdk/account/toNexusAccount.test.ts b/src/sdk/account/toNexusAccount.test.ts index e5041203..952b475b 100644 --- a/src/sdk/account/toNexusAccount.test.ts +++ b/src/sdk/account/toNexusAccount.test.ts @@ -97,7 +97,7 @@ describe("nexus.account", async () => { chain, transport: http(), bundlerTransport: http(bundlerUrl), - k1ValidatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, + validatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS }) @@ -275,9 +275,7 @@ describe("nexus.account", async () => { expect(factoryArgs.factory).toBe(undefined) expect(factoryArgs.factoryData).toBe(undefined) } else { - // biome-ignore lint/style/noNonNullAssertion: expect(isAddress(factoryArgs.factory!)).toBe(true) - // biome-ignore lint/style/noNonNullAssertion: expect(isHex(factoryArgs.factoryData!)).toBe(true) } diff --git a/src/sdk/account/toNexusAccount.ts b/src/sdk/account/toNexusAccount.ts index a10624c4..70f90574 100644 --- a/src/sdk/account/toNexusAccount.ts +++ b/src/sdk/account/toNexusAccount.ts @@ -1,4 +1,3 @@ -// viem import { type AbiParameter, type Account, @@ -48,12 +47,17 @@ import { ENTRY_POINT_ADDRESS, MAINNET_ADDRESS_K1_VALIDATOR_ADDRESS, MAINNET_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS, - MOCK_ATTESTER_ADDRESS, + MEE_VALIDATOR_ADDRESS, + NEXUS_BOOTSTRAP_ADDRESS, + REGISTRY_ADDRESS, RHINESTONE_ATTESTER_ADDRESS } from "../constants" // Constants import { EntrypointAbi } from "../constants/abi" -import { getCounterFactualAddress as getCounterFactualAddress_ } from "./utils/getCounterFactualAddress" +import { + getK1CounterFactualAddress, + getMeeCounterFactualAddress +} from "./utils/getCounterFactualAddress" // Modules import { toK1Validator } from "../modules/k1Validator/toK1Validator" @@ -77,6 +81,8 @@ import { isNullOrUndefined, typeToString } from "./utils/Utils" +import { getK1FactoryData } from "./utils/getFactoryData" +import { getMeeFactoryData } from "./utils/getFactoryData" import { type EthereumProvider, type Signer, toSigner } from "./utils/toSigner" /** @@ -101,13 +107,17 @@ export type ToNexusSmartAccountParameters = { /** Optional factory address */ factoryAddress?: Address /** Optional K1 validator address */ - k1ValidatorAddress?: Address + validatorAddress?: Address /** Optional account address override */ accountAddress?: Address /** Attester addresses to apply to the account */ attesters?: Address[] /** Optional attestors threshold for the account */ attesterThreshold?: number + /** Optional boot strap address */ + bootStrapAddress?: Address + /** Optional registry address */ + registryAddress?: Address } & Prettify< Pick< ClientConfig, @@ -145,7 +155,7 @@ export type NexusSmartAccountImplementation = SmartAccountImplementation< getModule: () => Module factoryData: Hex factoryAddress: Address - k1ValidatorAddress: Address + validatorAddress: Address attesters: Address[] signer: Signer publicClient: PublicClient @@ -180,13 +190,17 @@ export const toNexusAccount = async ( index = 0n, module: module_, factoryAddress = MAINNET_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS, - k1ValidatorAddress = MAINNET_ADDRESS_K1_VALIDATOR_ADDRESS, + validatorAddress = MAINNET_ADDRESS_K1_VALIDATOR_ADDRESS, key = "nexus account", name = "Nexus Account", attesters: attesters_ = [RHINESTONE_ATTESTER_ADDRESS], - attesterThreshold = 1 + attesterThreshold = 1, + bootStrapAddress = NEXUS_BOOTSTRAP_ADDRESS, + registryAddress = REGISTRY_ADDRESS } = parameters + const useMeeAccount = addressEquals(validatorAddress, MEE_VALIDATOR_ADDRESS) + // @ts-ignore const signer = await toSigner({ signer: _signer }) @@ -197,14 +211,13 @@ export const toNexusAccount = async ( key, name }).extend(publicActions) + const signerAddress = walletClient.account.address const publicClient = createPublicClient({ chain, transport }) - const signerAddress = walletClient.account.address - const entryPointContract = getContract({ address: ENTRY_POINT_ADDRESS, abi: EntrypointAbi, @@ -216,14 +229,26 @@ export const toNexusAccount = async ( // Review: // Todo: attesters can be added here to do one time setup upon deployment. - chain?.testnet && attesters_.push(MOCK_ATTESTER_ADDRESS) - const factoryData = encodeFunctionData({ - abi: parseAbi([ - "function createAccount(address eoaOwner, uint256 index, address[] attesters, uint8 threshold) external returns (address)" - ]), - functionName: "createAccount", - args: [signerAddress, index, attesters_, attesterThreshold] - }) + // chain?.testnet && attesters_.push(MOCK_ATTESTER_ADDRESS) + + const factoryData = useMeeAccount + ? await getMeeFactoryData({ + signerAddress, + index, + publicClient, + walletClient, + bootStrapAddress, + registryAddress, + attesters: attesters_, + attesterThreshold, + validatorAddress + }) + : await getK1FactoryData({ + signerAddress, + index, + attesters: attesters_, + attesterThreshold + }) /** * @description Gets the init code for the account @@ -252,15 +277,22 @@ export const toNexusAccount = async ( } } - const addressFromFactory = await getCounterFactualAddress_( - publicClient, - signerAddress, - false, - index, - attesters_, - attesterThreshold, - factoryAddress - ) + const addressFromFactory = useMeeAccount + ? await getMeeCounterFactualAddress({ + publicClient, + signerAddress, + index, + factoryAddress + }) + : await getK1CounterFactualAddress({ + publicClient, + signerAddress, + isTestnet: chain.testnet, + index, + attesters: attesters_, + threshold: attesterThreshold, + factoryAddress + }) if (!addressEquals(addressFromFactory, zeroAddress)) { _accountAddress = addressFromFactory @@ -273,7 +305,7 @@ export const toNexusAccount = async ( let module = module_ ?? toK1Validator({ - address: k1ValidatorAddress, + address: validatorAddress, accountAddress: await getCounterFactualAddress(), initData: signerAddress, deInitData: "0x", @@ -570,7 +602,7 @@ export const toNexusAccount = async ( getModule: () => module, factoryData, factoryAddress, - k1ValidatorAddress, + validatorAddress, signer, walletClient, publicClient, diff --git a/src/sdk/account/utils/Utils.ts b/src/sdk/account/utils/Utils.ts index 92000d5e..a8be599c 100644 --- a/src/sdk/account/utils/Utils.ts +++ b/src/sdk/account/utils/Utils.ts @@ -359,6 +359,14 @@ export const getAccountDomainStructFields = async ( ]) } +export const inProduction = () => { + try { + return process?.env?.environment === "production" + } catch (e) { + return true + } +} + export const playgroundTrue = () => { try { return process?.env?.RUN_PLAYGROUND === "true" diff --git a/src/sdk/account/utils/explorer/explorer.test.ts b/src/sdk/account/utils/explorer/explorer.test.ts new file mode 100644 index 00000000..fb140e3a --- /dev/null +++ b/src/sdk/account/utils/explorer/explorer.test.ts @@ -0,0 +1,66 @@ +import type { Address, Chain, LocalAccount } from "viem" +import { base, baseSepolia } from "viem/chains" +import { afterAll, beforeAll, describe, expect, test } from "vitest" +import { toNetwork } from "../../../../test/testSetup" +import type { NetworkConfig } from "../../../../test/testUtils" +import { + type MeeClient, + createMeeClient +} from "../../../clients/createMeeClient" +import { + type MultichainSmartAccount, + toMultichainNexusAccount +} from "../toMultiChainNexusAccount" +import { getExplorerTxLink, getJiffyScanLink, getMeeScanLink } from "./explorer" + +describe("explorer", () => { + let network: NetworkConfig + let eoaAccount: LocalAccount + let paymentChain: Chain + let paymentToken: Address + let mcNexusMainnet: MultichainSmartAccount + let meeClient: MeeClient + + beforeAll(async () => { + network = await toNetwork("MAINNET_FROM_ENV_VARS") + + paymentChain = network.chain + paymentToken = network.paymentToken! + eoaAccount = network.account! + + mcNexusMainnet = await toMultichainNexusAccount({ + chains: [base, paymentChain], + signer: eoaAccount + }) + + meeClient = createMeeClient({ account: mcNexusMainnet }) + }) + + test("should get a meescan url", () => { + const hash = "0x123" + const url = getMeeScanLink(hash) + expect(url).toEqual(`https://meescan.biconomy.io/details/${hash}`) + }) + + test("should get a jiffyscan url", () => { + const hash = "0x123" + const url = getJiffyScanLink(hash) + expect(url).toEqual(`https://v2.jiffyscan.xyz/tx/${hash}`) + }) + + test("should get a url for a baseSepolia tx", () => { + const hash = "0x123" + const url = getExplorerTxLink(hash, baseSepolia) + expect(url).toEqual(`${baseSepolia.blockExplorers?.default.url}/tx/${hash}`) + }) + test("should get a url for a baseSepolia tx by chainId (number)", () => { + const hash = "0x123" + const url = getExplorerTxLink(hash, baseSepolia.id) + expect(url).toEqual(`${baseSepolia.blockExplorers?.default.url}/tx/${hash}`) + }) + test("should get a url for a baseSepolia tx by chainId (string)", () => { + const hash = "0x123" + const url = getExplorerTxLink(hash, String(baseSepolia.id)) + expect(url).toEqual(`${baseSepolia.blockExplorers?.default.url}/tx/${hash}`) + }) +}) diff --git a/src/sdk/account/utils/explorer/explorer.ts b/src/sdk/account/utils/explorer/explorer.ts new file mode 100644 index 00000000..3f450389 --- /dev/null +++ b/src/sdk/account/utils/explorer/explorer.ts @@ -0,0 +1,61 @@ +import type { Chain, Hex } from "viem" +import type { Url } from "../../../clients/createHttpClient" +import { getChain } from "../getChain" + +/** + * Get the explorer tx link + * @param hash - The transaction hash + * @param chain - The chain + * @returns The explorer tx link + * + * @example + * ```ts + * const hash = "0x123" + * const chain = optimism + * const url = getExplorerTxLink(hash, chain) + * console.log(url) // https://meescan.biconomy.io/details/0x123 + * ``` + */ +export const getExplorerTxLink = ( + hash: Hex, + chain_: Chain | number | string +): Url => { + const chain: Chain = + typeof chain_ === "number" || typeof chain_ === "string" + ? getChain(Number(chain_)) + : chain_ + + return `${chain.blockExplorers?.default.url}/tx/${hash}` as Url +} + +/** + * Get the jiffyscan tx link + * @param hash - The transaction hash + * @returns The jiffyscan tx link + * + * @example + * ```ts + * const hash = "0x123" + * const url = getJiffyScanLink(hash) + * console.log(url) // https://jiffyscan.com/tx/0x123 + * ``` + */ +export const getJiffyScanLink = (userOpHash: Hex): Url => { + return `https://v2.jiffyscan.xyz/tx/${userOpHash}` as Url +} + +/** + * Get the meescan tx link + * @param hash - The transaction hash + * @returns The meescan tx link + * + * @example + * ```ts + * const hash = "0x123" + * const url = getMeeScanLink(hash) + * console.log(url) // https://meescan.biconomy.io/details/0x123 + * ``` + */ +export const getMeeScanLink = (hash: Hex): Url => { + return `https://meescan.biconomy.io/details/${hash}` as Url +} diff --git a/src/sdk/account/utils/getCounterFactualAddress.ts b/src/sdk/account/utils/getCounterFactualAddress.ts index 7405dc44..f6a03b7a 100644 --- a/src/sdk/account/utils/getCounterFactualAddress.ts +++ b/src/sdk/account/utils/getCounterFactualAddress.ts @@ -1,10 +1,13 @@ -import type { Address } from "viem" +import { type Address, pad, toHex } from "viem" import type { PublicClient } from "viem" import { MAINNET_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS, MOCK_ATTESTER_ADDRESS, + NEXUS_BOOTSTRAP_ADDRESS, RHINESTONE_ATTESTER_ADDRESS } from "../../constants" +import { AccountFactoryAbi } from "../../constants/abi/AccountFactory" +import { K1ValidatorFactoryAbi } from "../../constants/abi/K1ValidatorFactory" /** * Get the counterfactual address of a signer @@ -23,58 +26,73 @@ import { * const counterFactualAddress = await getCounterFactualAddress(publicClient, signerAddress) * ``` */ -export const getCounterFactualAddress = async ( - publicClient: PublicClient, - signerAddress: Address, - isTestnet = false, - index = 0n, - attesters = [RHINESTONE_ATTESTER_ADDRESS], - threshold = 1, - factoryAddress = MAINNET_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS -) => { + +type K1CounterFactualAddressParams = { + /** The public client to use for the read contract */ + publicClient: PublicClient + /** The address of the signer */ + signerAddress: Address + /** Whether the network is testnet */ + isTestnet?: boolean + /** The index of the account */ + index?: bigint + /** The attesters to use */ + attesters?: Address[] + /** The threshold of the attesters */ + threshold?: number + /** The factory address to use. Defaults to the mainnet factory address */ + factoryAddress?: Address +} +export const getK1CounterFactualAddress = async ( + params: K1CounterFactualAddressParams +): Promise
=> { + const { + publicClient, + signerAddress, + isTestnet = false, + index = 0n, + attesters = [RHINESTONE_ATTESTER_ADDRESS], + threshold = 1, + factoryAddress = MAINNET_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS + } = params + if (isTestnet) { attesters.push(MOCK_ATTESTER_ADDRESS) } - return await publicClient.readContract({ address: factoryAddress, - abi: [ - { - inputs: [ - { - internalType: "address", - name: "eoaOwner", - type: "address" - }, - { - internalType: "uint256", - name: "index", - type: "uint256" - }, - { - internalType: "address[]", - name: "attesters", - type: "address[]" - }, - { - internalType: "uint8", - name: "threshold", - type: "uint8" - } - ], - name: "computeAccountAddress", - outputs: [ - { - internalType: "address payable", - name: "expectedAddress", - type: "address" - } - ], - stateMutability: "view", - type: "function" - } - ], + abi: K1ValidatorFactoryAbi, functionName: "computeAccountAddress", args: [signerAddress, index, attesters, threshold] }) } + +type MeeCounterFactualAddressParams = { + /** The public client to use for the read contract */ + publicClient: PublicClient + /** The address of the signer */ + signerAddress: Address + /** The salt for the account */ + index: bigint + /** The factory address to use. Defaults to the mainnet factory address */ + factoryAddress?: Address +} +export const getMeeCounterFactualAddress = async ( + params: MeeCounterFactualAddressParams +) => { + console.log("getMeeCounterFactualAddress", params) + + const salt = pad(toHex(params.index), { size: 32 }) + const { + publicClient, + signerAddress, + factoryAddress = NEXUS_BOOTSTRAP_ADDRESS + } = params + + return await publicClient.readContract({ + address: factoryAddress, + abi: AccountFactoryAbi, + functionName: "computeAccountAddress", + args: [signerAddress, salt] + }) +} diff --git a/src/sdk/account/utils/getFactoryData.ts b/src/sdk/account/utils/getFactoryData.ts new file mode 100644 index 00000000..b81e8e8d --- /dev/null +++ b/src/sdk/account/utils/getFactoryData.ts @@ -0,0 +1,83 @@ +import { + type Address, + type Hex, + type PublicClient, + type WalletClient, + encodeFunctionData, + getContract, + pad, + parseAbi, + toHex +} from "viem" +import { NexusBootstrapAbi } from "../../constants/abi/NexusBootstrapAbi" + +export type GetK1FactoryDataParams = { + signerAddress: Address + index: bigint + attesters: Address[] + attesterThreshold: number +} +export const getK1FactoryData = async ({ + signerAddress, + index, + attesters, + attesterThreshold +}: GetK1FactoryDataParams): Promise => + encodeFunctionData({ + abi: parseAbi([ + "function createAccount(address eoaOwner, uint256 index, address[] attesters, uint8 threshold) external returns (address)" + ]), + functionName: "createAccount", + args: [signerAddress, index, attesters, attesterThreshold] + }) + +export type GetMeeFactoryDataParams = GetK1FactoryDataParams & { + validatorAddress: Address + registryAddress: Address + publicClient: PublicClient + walletClient: WalletClient + bootStrapAddress: Address +} +export const getMeeFactoryData = async ({ + validatorAddress, + attesters, + registryAddress, + attesterThreshold, + publicClient, + walletClient, + bootStrapAddress, + signerAddress, + index +}: GetMeeFactoryDataParams): Promise => { + const nexusBootstrap = getContract({ + address: bootStrapAddress, + abi: NexusBootstrapAbi, + client: { + public: publicClient, + wallet: walletClient + } + }) + + const initData = + await nexusBootstrap.read.getInitNexusWithSingleValidatorCalldata([ + { + module: validatorAddress, + data: signerAddress + }, + registryAddress, + attesters, + attesterThreshold + ]) + + const salt = pad(toHex(index), { size: 32 }) + + const factoryData = encodeFunctionData({ + abi: parseAbi([ + "function createAccount(bytes initData, bytes32 salt) external returns (address)" + ]), + functionName: "createAccount", + args: [initData, salt] + }) + + return factoryData +} diff --git a/src/sdk/account/utils/getMultichainContract.ts b/src/sdk/account/utils/getMultichainContract.ts new file mode 100644 index 00000000..f1df0a3b --- /dev/null +++ b/src/sdk/account/utils/getMultichainContract.ts @@ -0,0 +1,209 @@ +import type { + AbiParametersToPrimitiveTypes, + ExtractAbiFunction, + ExtractAbiFunctionNames +} from "abitype" +import type { + Abi, + Address, + Chain, + ContractFunctionArgs, + ContractFunctionName, + ContractFunctionReturnType, + EncodeFunctionDataParameters, + PublicClient, + Transport +} from "viem" +import { encodeFunctionData } from "viem" +import type { + AbstractCall, + Instruction +} from "../../clients/decorators/mee/getQuote" +import type { MultichainSmartAccount } from "./toMultiChainNexusAccount" +/** + * Contract instance capable of encoding transactions across multiple chains + * @template TAbi - The contract ABI type + */ +export type MultichainContract = { + abi: TAbi + deployments: Map + on: (chainId: number) => ChainSpecificContract + addressOn: (chainId: number) => Address + read: < + TFunctionName extends ContractFunctionName + >(params: { + onChains: Chain[] + functionName: TFunctionName + args: ContractFunctionArgs + account: MultichainSmartAccount + }) => Promise< + Array<{ + chainId: number + result: ContractFunctionReturnType + }> + > +} + +export type ChainSpecificContract = { + [TFunctionName in ExtractAbiFunctionNames]: (params: { + args: AbiParametersToPrimitiveTypes< + ExtractAbiFunction["inputs"] + > + gasLimit: bigint + value?: bigint + }) => Instruction +} + +function createChainSpecificContract( + abi: TAbi, + chainId: number, + address: Address +): ChainSpecificContract { + return new Proxy({} as ChainSpecificContract, { + get: (_: ChainSpecificContract, prop: string) => { + if (!abi.some((item) => item.type === "function" && item.name === prop)) { + throw new Error(`Function ${prop} not found in ABI`) + } + + return ({ + args, + gasLimit, + value = 0n + }: { + // biome-ignore lint/suspicious/noExplicitAny: + args: any[] // This will be typed by the ChainSpecificContract type + gasLimit: bigint + value?: bigint + }) => { + const params: EncodeFunctionDataParameters = { + abi, + functionName: prop, + args + } + const data = encodeFunctionData(params) + + const call: AbstractCall = { + to: address, + gasLimit, + value, + data + } + + return { + calls: [call], + chainId + } + } + } + }) +} + +/** + * Creates a contract instance that can encode function calls across multiple chains + * @template TAbi The contract ABI type + * @example + * const mcUSDC = getMultichainContract({ + * abi: erc20ABI, + * deployments: [ + * ['0x...', optimism.id], + * ['0x...', base.id], + * // Other chains + * ] + * }); + * + * const transferOp = usdc.on(optimism.id).transfer({ + * args: ['0x...', 100n], + * gasLimit: 100000n + * }); + */ +export function getMultichainContract(config: { + abi: TAbi + deployments: [Address, number][] +}): MultichainContract { + const deployments = new Map( + config.deployments.map((deployment) => { + const [address, chainId] = deployment + return [chainId, address] + }) + ) + return { + abi: config.abi, + deployments, + on: (chainId: number) => { + const address = deployments.get(chainId) + if (!address) { + throw new Error(`No deployment found for chain ${chainId}`) + } + + return createChainSpecificContract(config.abi, chainId, address) + }, + addressOn: (chainId: number) => { + const address = deployments.get(chainId) + if (!address) { + throw new Error(`No deployment found for chain ${chainId}`) + } + return address + }, + read: async < + TFunctionName extends ContractFunctionName + >(params: { + onChains: Chain[] + functionName: TFunctionName + args: ContractFunctionArgs + account: MultichainSmartAccount + }): Promise< + Array<{ + chainId: number + result: ContractFunctionReturnType + }> + > => { + const abiFunction = config.abi.find( + ( + item + ): item is Extract< + TAbi[number], + { type: "function"; stateMutability: "view" | "pure" } + > => + item.type === "function" && + item.name === params.functionName && + (item.stateMutability === "view" || item.stateMutability === "pure") + ) + + if (!abiFunction) { + throw new Error( + `Function ${params.functionName} not found in ABI or is not a read function` + ) + } + + const results = await Promise.all( + params.onChains.map(async (chain) => { + const address = deployments.get(chain.id) + if (!address) { + throw new Error(`No deployment found for chain ${chain.id}`) + } + + const deployment = params.account.deploymentOn(chain.id) + const client = deployment.client as PublicClient + + const result = await client.readContract({ + address, + abi: config.abi, + functionName: params.functionName, + args: params.args + }) + + return { + chainId: chain.id, + result: result as ContractFunctionReturnType< + TAbi, + "pure" | "view", + TFunctionName + > + } + }) + ) + + return results + } + } +} diff --git a/src/sdk/account/utils/toMultiChainNexusAccount.test.ts b/src/sdk/account/utils/toMultiChainNexusAccount.test.ts new file mode 100644 index 00000000..0f90aa5c --- /dev/null +++ b/src/sdk/account/utils/toMultiChainNexusAccount.test.ts @@ -0,0 +1,117 @@ +import { + http, + type Chain, + type PrivateKeyAccount, + isAddress, + isHex +} from "viem" +import { base, optimism, optimismSepolia } from "viem/chains" +import { baseSepolia } from "viem/chains" +import { beforeAll, describe, expect, test } from "vitest" +import { toNetwork } from "../../../test/testSetup" +import type { NetworkConfig } from "../../../test/testUtils" +import { MEE_VALIDATOR_ADDRESS, TEMP_MEE_ATTESTER_ADDR } from "../../constants" +import { NEXUS_ACCOUNT_FACTORY } from "../../constants" +import { mcUSDC } from "../../constants/tokens" +import { toNexusAccount } from "../toNexusAccount" +import { + type MultichainSmartAccount, + toMultichainNexusAccount +} from "./toMultiChainNexusAccount" + +describe("mee.toMultiChainNexusAccount", async () => { + let network: NetworkConfig + let chain: Chain + let bundlerUrl: string + + let eoaAccount: PrivateKeyAccount + let mcNexusTestnet: MultichainSmartAccount + let mcNexusMainnet: MultichainSmartAccount + + beforeAll(async () => { + network = await toNetwork("TESTNET_FROM_ENV_VARS") + + chain = network.chain + bundlerUrl = network.bundlerUrl + eoaAccount = network.account! + }) + + test("should create multichain account with correct parameters", async () => { + mcNexusTestnet = await toMultichainNexusAccount({ + signer: eoaAccount, + chains: [baseSepolia, optimismSepolia] + }) + + mcNexusMainnet = await toMultichainNexusAccount({ + signer: eoaAccount, + chains: [base, optimism] + }) + + // Verify the structure of the returned object + expect(mcNexusMainnet).toHaveProperty("deployments") + expect(mcNexusMainnet).toHaveProperty("signer") + expect(mcNexusMainnet).toHaveProperty("deploymentOn") + expect(mcNexusMainnet.signer).toBe(eoaAccount) + expect(mcNexusMainnet.deployments).toHaveLength(2) + + expect(mcNexusTestnet.deployments).toHaveLength(2) + expect(mcNexusTestnet.signer).toBe(eoaAccount) + expect(mcNexusTestnet.deployments).toHaveLength(2) + }) + + test("should return correct deployment for specific chain", async () => { + const deployment = mcNexusTestnet.deploymentOn(baseSepolia.id) + expect(deployment).toBeDefined() + expect(deployment.client.chain?.id).toBe(baseSepolia.id) + }) + + test("should throw error for non-existent chain deployment", async () => { + expect(() => mcNexusTestnet.deploymentOn(999)).toThrow( + "No account deployment for chainId: 999" + ) + }) + + test("should handle empty chains array", async () => { + const multiChainAccount = await toMultichainNexusAccount({ + signer: eoaAccount, + chains: [] + }) + expect(multiChainAccount.deployments).toHaveLength(0) + }) + + test("should have configured accounts correctly", async () => { + expect(mcNexusMainnet.deployments.length).toEqual(2) + expect(mcNexusTestnet.deployments.length).toEqual(2) + expect(mcNexusTestnet.deploymentOn(baseSepolia.id).address).toEqual( + mcNexusTestnet.deploymentOn(baseSepolia.id).address + ) + }) + + test("should sign message using MEE Compliant Nexus Account", async () => { + const nexus = await toNexusAccount({ + chain: baseSepolia, + signer: eoaAccount, + transport: http(), + validatorAddress: MEE_VALIDATOR_ADDRESS, + factoryAddress: NEXUS_ACCOUNT_FACTORY, + attesters: [TEMP_MEE_ATTESTER_ADDR] + }) + + expect(isAddress(nexus.address)).toBeTruthy() + + const signed = await nexus.signMessage({ message: { raw: "0xABC" } }) + expect(isHex(signed)).toBeTruthy() + }) + + test("should read usdc balance on mainnet", async () => { + const readAddress = mcNexusMainnet.deploymentOn(optimism.id).address + const usdcBalanceOnChains = await mcUSDC.read({ + account: mcNexusMainnet, + functionName: "balanceOf", + args: [readAddress], + onChains: [base, optimism] + }) + + expect(usdcBalanceOnChains.length).toEqual(2) + }) +}) diff --git a/src/sdk/account/utils/toMultiChainNexusAccount.ts b/src/sdk/account/utils/toMultiChainNexusAccount.ts new file mode 100644 index 00000000..7afc5d62 --- /dev/null +++ b/src/sdk/account/utils/toMultiChainNexusAccount.ts @@ -0,0 +1,76 @@ +import { http, type Chain } from "viem" +import { + MEE_VALIDATOR_ADDRESS, + NEXUS_ACCOUNT_FACTORY, + TEMP_MEE_ATTESTER_ADDR +} from "../../constants" +import type { MinimalMEESmartAccount } from "../../modules/utils/Types" +import { toNexusAccount } from "../toNexusAccount" +import type { Signer } from "./toSigner" + +/** + * Parameters required to create a multichain Nexus account + */ +export type MultichainNexusParams = { + /** The signer instance used for account creation */ + signer: Signer + /** Array of chains where the account will be deployed */ + chains: Chain[] +} + +/** + * Represents a smart account deployed across multiple chains + */ +export type MultichainSmartAccount = { + /** Array of minimal MEE smart account instances across different chains */ + deployments: MinimalMEESmartAccount[] + /** The signer associated with this multichain account */ + signer: Signer + /** + * Function to retrieve deployment information for a specific chain + * @param chainId - The ID of the chain to query + * @returns The smart account deployment for the specified chain + * @throws Error if no deployment exists for the specified chain + */ + deploymentOn: (chainId: number) => MinimalMEESmartAccount +} + +/** + * Creates a multichain Nexus account across specified chains + * @param parameters - Configuration parameters for multichain account creation + * @returns Promise resolving to a MultichainSmartAccount instance + */ +export async function toMultichainNexusAccount( + parameters: MultichainNexusParams +): Promise { + const { signer, chains } = parameters + + const deployments = await Promise.all( + chains.map((chain) => + toNexusAccount({ + chain, + signer, + transport: http(), + validatorAddress: MEE_VALIDATOR_ADDRESS, + factoryAddress: NEXUS_ACCOUNT_FACTORY, + attesters: [TEMP_MEE_ATTESTER_ADDR] + }) + ) + ) + + const deploymentOn = (chainId: number) => { + const deployment = deployments.find( + (dep) => dep.client.chain?.id === chainId + ) + if (!deployment) { + throw Error(`No account deployment for chainId: ${chainId}`) + } + return deployment + } + + return { + deployments, + signer, + deploymentOn + } +} diff --git a/src/sdk/clients/createBicoBundlerClient.test.ts b/src/sdk/clients/createBicoBundlerClient.test.ts index b64cb143..bed11bef 100644 --- a/src/sdk/clients/createBicoBundlerClient.test.ts +++ b/src/sdk/clients/createBicoBundlerClient.test.ts @@ -43,7 +43,7 @@ describe("bico.bundler", async () => { signer: eoaAccount, chain, transport: http(), - k1ValidatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, + validatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS }) diff --git a/src/sdk/clients/createBundlerClient.test.ts b/src/sdk/clients/createBundlerClient.test.ts index f35a0599..b64c5f1a 100644 --- a/src/sdk/clients/createBundlerClient.test.ts +++ b/src/sdk/clients/createBundlerClient.test.ts @@ -39,7 +39,7 @@ describe.each(COMPETITORS)( chain, transport: http(), // You can omit this outside of a testing context - k1ValidatorAddress: MAINNET_ADDRESS_K1_VALIDATOR_ADDRESS, + validatorAddress: MAINNET_ADDRESS_K1_VALIDATOR_ADDRESS, factoryAddress: MAINNET_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS }) diff --git a/src/sdk/clients/createHttpClient.test.ts b/src/sdk/clients/createHttpClient.test.ts new file mode 100644 index 00000000..a427bac1 --- /dev/null +++ b/src/sdk/clients/createHttpClient.test.ts @@ -0,0 +1,45 @@ +import type { Address, Chain, LocalAccount } from "viem" +import { base } from "viem/chains" +import { beforeAll, describe, expect, test } from "vitest" +import { toNetwork } from "../../test/testSetup" +import type { NetworkConfig } from "../../test/testUtils" +import { + type MultichainSmartAccount, + toMultichainNexusAccount +} from "../account/utils/toMultiChainNexusAccount" +import createHttpClient from "./createHttpClient" +import { type MeeClient, createMeeClient } from "./createMeeClient" + +describe("mee:createHttp Client", async () => { + let network: NetworkConfig + let eoaAccount: LocalAccount + let paymentChain: Chain + let paymentToken: Address + let mcNexusMainnet: MultichainSmartAccount + let meeClient: MeeClient + + beforeAll(async () => { + network = await toNetwork("MAINNET_FROM_ENV_VARS") + + paymentChain = network.chain + paymentToken = network.paymentToken! + eoaAccount = network.account! + + mcNexusMainnet = await toMultichainNexusAccount({ + chains: [base, paymentChain], + signer: eoaAccount + }) + + meeClient = createMeeClient({ account: mcNexusMainnet }) + }) + + test("should instantiate a client", async () => { + const httpClient = createHttpClient("http://google.com") + + expect(httpClient).toBeDefined() + expect(httpClient.request).toBeDefined() + expect(Object.keys(httpClient)).toContain("request") + expect(Object.keys(httpClient)).not.toContain("account") + expect(Object.keys(httpClient)).not.toContain("getQuote") + }) +}) diff --git a/src/sdk/clients/createHttpClient.ts b/src/sdk/clients/createHttpClient.ts new file mode 100644 index 00000000..eb11daa9 --- /dev/null +++ b/src/sdk/clients/createHttpClient.ts @@ -0,0 +1,87 @@ +import type { Prettify } from "viem" +import type { AnyData } from "../modules" + +/** + * Parameters for initializing a Http client + */ +export type Url = `https://${string}` | `http://${string}` + +/** + * Parameters for making requests to the Http node + */ +type RequestParams = { + /** API endpoint path */ + path: string + /** HTTP method to use. Defaults to "POST" */ + method?: "GET" | "POST" + /** Optional request body */ + body?: object +} + +/** + * Base interface for the Http client + */ +export type HttpClient = { + /** Makes HTTP requests to the Http node */ + request: (params: RequestParams) => Promise + /** + * Extends the client with additional functionality + * @param fn - Function that adds new properties/methods to the base client + * @returns Extended client with both base and new functionality + */ + extend: < + const client extends Extended, + const extendedHttpClient extends HttpClient + >( + fn: (base: extendedHttpClient) => client + ) => client & extendedHttpClient +} + +type Extended = Prettify< + // disallow redefining base properties + { [_ in keyof HttpClient]?: undefined } & { + [key: string]: unknown + } +> + +/** + * Creates a new Http client instance + * @param params - Configuration parameters for the client + * @returns A base Http client instance that can be extended with additional functionality + */ +export const createHttpClient = (url: Url): HttpClient => { + const request = async (params: RequestParams) => { + const { path, method = "POST", body } = params + const result = await fetch(`${url}/${path}`, { + method, + headers: { + "Content-Type": "application/json" + }, + ...(body ? { body: JSON.stringify(body) } : {}) + }) + + if (!result.ok) { + throw new Error(result.statusText) + } + + return (await result.json()) as T + } + + const client = { + request + } + + function extend(base: typeof client) { + type ExtendFn = (base: typeof client) => unknown + return (extendFn: ExtendFn) => { + const extended = extendFn(base) as Extended + for (const key in client) delete extended[key] + const combined = { ...base, ...extended } + return Object.assign(combined, { extend: extend(combined as AnyData) }) + } + } + + return Object.assign(client, { extend: extend(client) as AnyData }) +} + +export default createHttpClient diff --git a/src/sdk/clients/createMeeClient.test.ts b/src/sdk/clients/createMeeClient.test.ts new file mode 100644 index 00000000..178e3e62 --- /dev/null +++ b/src/sdk/clients/createMeeClient.test.ts @@ -0,0 +1,247 @@ +import { + type Address, + type Chain, + type LocalAccount, + erc20Abi, + isHex +} from "viem" +import { base } from "viem/chains" +import { beforeAll, describe, expect, inject, test } from "vitest" +import { toNetwork } from "../../test/testSetup" +import type { NetworkConfig } from "../../test/testUtils" +import { + type MultichainSmartAccount, + toMultichainNexusAccount +} from "../account/utils/toMultiChainNexusAccount" +import { type MeeClient, createMeeClient } from "./createMeeClient" +import type { Instruction } from "./decorators/mee" + +const { runPaidTests } = inject("settings") + +describe("mee:createMeeClient", async () => { + let network: NetworkConfig + let eoaAccount: LocalAccount + let paymentChain: Chain + let paymentToken: Address + let mcNexusMainnet: MultichainSmartAccount + let meeClient: MeeClient + + beforeAll(async () => { + network = await toNetwork("MAINNET_FROM_ENV_VARS") + + paymentChain = network.chain + paymentToken = network.paymentToken! + eoaAccount = network.account! + + mcNexusMainnet = await toMultichainNexusAccount({ + chains: [base, paymentChain], + signer: eoaAccount + }) + + meeClient = createMeeClient({ account: mcNexusMainnet }) + }) + + test("should instantiate a client", async () => { + const meeClient = createMeeClient({ account: mcNexusMainnet }) + expect(meeClient).toBeDefined() + expect(meeClient.request).toBeDefined() + expect(Object.keys(meeClient)).toContain("request") + expect(Object.keys(meeClient)).toContain("account") + expect(Object.keys(meeClient)).toContain("getQuote") + }) + + test("should extend meeClient with decorators", () => { + expect(meeClient).toBeDefined() + expect(meeClient.getQuote).toBeDefined() + expect(meeClient.request).toBeDefined() + expect(meeClient.account).toBeDefined() + expect(meeClient.getQuote).toBeDefined() + expect(meeClient.signQuote).toBeDefined() + expect(meeClient.executeSignedQuote).toBeDefined() + }) + + test("should get a quote", async () => { + const meeClient = createMeeClient({ account: mcNexusMainnet }) + + const quote = await meeClient.getQuote({ + instructions: [], + feeToken: { + address: paymentToken, + chainId: paymentChain.id + } + }) + + expect(quote).toBeDefined() + expect(quote.paymentInfo.sender).toEqual( + mcNexusMainnet.deploymentOn(paymentChain.id).address + ) + expect(quote.paymentInfo.token).toEqual(paymentToken) + expect(+quote.paymentInfo.chainId).toEqual(paymentChain.id) + }) + + test("should sign a quote", async () => { + const quote = await meeClient.getQuote({ + instructions: [ + { + calls: [ + { + to: "0x0000000000000000000000000000000000000000", + gasLimit: 50000n, + value: 0n + } + ], + chainId: 8453 + } + ], + feeToken: { + address: paymentToken, + chainId: paymentChain.id + } + }) + + const signedQuote = await meeClient.signQuote({ quote }) + + expect(signedQuote).toBeDefined() + expect(isHex(signedQuote.signature)).toEqual(true) + }) + + test + .runIf(runPaidTests) + .skip("should execute a quote by getting it, signing it, and then executing the signed quote", async () => { + const quote = await meeClient.getQuote({ + instructions: [ + { + calls: [ + { + to: "0x0000000000000000000000000000000000000000", + gasLimit: 50000n, + value: 0n + } + ], + chainId: 8453 + } + ], + feeToken: { + address: paymentToken, + chainId: paymentChain.id + } + }) + + const signedQuote = await meeClient.signQuote({ quote }) + const executeeQuote = await meeClient.executeSignedQuote({ signedQuote }) + + expect(executeeQuote).toBeDefined() + expect(executeeQuote.hash).toBeDefined() + expect(isHex(executeeQuote.hash)).toEqual(true) + }) + + test("should demo the devEx of preparing instructions", async () => { + // These can be any 'Instruction', or any helper method that resolves to a 'ResolvedInstruction', + // including 'requireErc20Balance'. They all are resolved in the 'getQuote' method under the hood. + const preparedInstructions: Instruction[] = [ + { + calls: [ + { + to: "0x0000000000000000000000000000000000000000", + gasLimit: 50000n, + value: 0n + } + ], + chainId: 8453 + }, + () => ({ + calls: [ + { + to: "0x0000000000000000000000000000000000000000", + gasLimit: 50000n, + value: 0n + } + ], + chainId: 8453 + }), + Promise.resolve({ + calls: [ + { + to: "0x0000000000000000000000000000000000000000", + gasLimit: 50000n, + value: 0n + } + ], + chainId: 8453 + }), + () => [ + { + calls: [ + { + to: "0x0000000000000000000000000000000000000000", + gasLimit: 50000n, + value: 0n + } + ], + chainId: 8453 + }, + { + calls: [ + { + to: "0x0000000000000000000000000000000000000000", + gasLimit: 50000n, + value: 0n + } + ], + chainId: 8453 + } + ] + ] + + expect(preparedInstructions).toBeDefined() + + const quote = await meeClient.getQuote({ + instructions: preparedInstructions, + feeToken: { + address: paymentToken, + chainId: paymentChain.id + } + }) + + expect(quote.userOps.length).toEqual(6) + expect(quote).toBeDefined() + expect(quote.paymentInfo.sender).toEqual( + mcNexusMainnet.deploymentOn(paymentChain.id).address + ) + expect(quote.paymentInfo.token).toEqual(paymentToken) + expect(+quote.paymentInfo.chainId).toEqual(paymentChain.id) + }) + + test.runIf(runPaidTests)( + "should execute a quote with a single call, and wait for the receipt", + async () => { + console.time("execute:hashTimer") + console.time("execute:receiptTimer") + const { hash } = await meeClient.execute({ + instructions: [ + { + calls: [ + { + to: "0x0000000000000000000000000000000000000000", + gasLimit: 50000n, + value: 0n + } + ], + chainId: 8453 + } + ], + feeToken: { + address: paymentToken, + chainId: paymentChain.id + } + }) + + expect(hash).toBeDefined() + console.timeEnd("execute:hashTimer") + const receipt = await meeClient.waitForSupertransactionReceipt({ hash }) + console.timeEnd("execute:receiptTimer") + expect(receipt).toBeDefined() + console.log(receipt.explorerLinks) + } + ) +}) diff --git a/src/sdk/clients/createMeeClient.ts b/src/sdk/clients/createMeeClient.ts new file mode 100644 index 00000000..35ae284d --- /dev/null +++ b/src/sdk/clients/createMeeClient.ts @@ -0,0 +1,49 @@ +import type { Prettify } from "viem" +import { inProduction } from "../account/utils/Utils" +import type { MultichainSmartAccount } from "../account/utils/toMultiChainNexusAccount" +import createHttpClient, { type HttpClient, type Url } from "./createHttpClient" +import { meeActions } from "./decorators/mee" + +/** + * Default URL for the MEE node service + */ +const DEFAULT_MEE_NODE_URL = "https://mee-node.biconomy.io" + +/** + * Parameters for creating a Mee client + */ +export type CreateMeeClientParams = { + /** URL for the MEE node service */ + url?: Url + /** Polling interval for the Mee client */ + pollingInterval?: number + /** Account to use for the Mee client */ + account: MultichainSmartAccount +} + +export type BaseMeeClient = Prettify< + HttpClient & { + pollingInterval: number + account: MultichainSmartAccount + } +> + +export type MeeClient = ReturnType + +export const createMeeClient = (params: CreateMeeClientParams) => { + inProduction() && + console.warn(` +--------------------------- READ ---------------------------------------------- + You are using the Developer Preview of the Biconomy MEE. The underlying + contracts are still being audited, and the multichain tokens exported from + this package are yet to be verified. +-------------------------------------------------------------------------------`) + const { url = DEFAULT_MEE_NODE_URL, pollingInterval = 1000, account } = params + const httpClient = createHttpClient(url) + const baseMeeClient = Object.assign(httpClient, { + pollingInterval, + account + }) + + return baseMeeClient.extend(meeActions) +} diff --git a/src/sdk/clients/createSmartAccountClient.test.ts b/src/sdk/clients/createSmartAccountClient.test.ts index 1f3d6dc3..81911acc 100644 --- a/src/sdk/clients/createSmartAccountClient.test.ts +++ b/src/sdk/clients/createSmartAccountClient.test.ts @@ -73,7 +73,7 @@ describe("nexus.client", async () => { chain, transport: http(), bundlerTransport: http(bundlerUrl), - k1ValidatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, + validatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS }) nexusAccountAddress = await nexusClient.account.getCounterFactualAddress() @@ -276,7 +276,7 @@ describe("nexus.client", async () => { chain, transport: http(), bundlerTransport: http(bundlerUrl), - k1ValidatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, + validatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS }) @@ -285,7 +285,7 @@ describe("nexus.client", async () => { chain, transport: http(), bundlerTransport: http(bundlerUrl), - k1ValidatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, + validatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS }) @@ -302,7 +302,7 @@ describe("nexus.client", async () => { chain, transport: http(), bundlerTransport: http(bundlerUrl), - k1ValidatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, + validatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS }) diff --git a/src/sdk/clients/createSmartAccountClient.ts b/src/sdk/clients/createSmartAccountClient.ts index c02144aa..773078fd 100644 --- a/src/sdk/clients/createSmartAccountClient.ts +++ b/src/sdk/clients/createSmartAccountClient.ts @@ -165,13 +165,17 @@ export type SmartAccountClientConfig< /** Factory address of the account. */ factoryAddress?: Address /** Owner module */ - k1ValidatorAddress?: Address + validatorAddress?: Address /** Account address */ accountAddress?: Address /** Attesters */ attesters?: ToNexusSmartAccountParameters["attesters"] /** Threshold */ attesterThreshold?: ToNexusSmartAccountParameters["attesterThreshold"] + /** Boot strap address */ + bootStrapAddress?: Address + /** Registry address */ + registryAddress?: Address } > @@ -206,7 +210,7 @@ export async function createSmartAccountClient( name = "Nexus Client", module, factoryAddress = MAINNET_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS, - k1ValidatorAddress = MAINNET_ADDRESS_K1_VALIDATOR_ADDRESS, + validatorAddress = MAINNET_ADDRESS_K1_VALIDATOR_ADDRESS, bundlerTransport, transport, accountAddress, @@ -228,7 +232,7 @@ export async function createSmartAccountClient( index, module, factoryAddress, - k1ValidatorAddress, + validatorAddress, attesters, attesterThreshold })) diff --git a/src/sdk/clients/decorators/erc7579/erc7579.decorators.test.ts b/src/sdk/clients/decorators/erc7579/erc7579.decorators.test.ts index 639e7227..dc72b016 100644 --- a/src/sdk/clients/decorators/erc7579/erc7579.decorators.test.ts +++ b/src/sdk/clients/decorators/erc7579/erc7579.decorators.test.ts @@ -54,7 +54,7 @@ describe("erc7579.decorators", async () => { chain, transport: http(), bundlerTransport: http(bundlerUrl), - k1ValidatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, + validatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS }) diff --git a/src/sdk/clients/decorators/erc7579/installModule.ts b/src/sdk/clients/decorators/erc7579/installModule.ts index 5a0123b3..ac2c91aa 100644 --- a/src/sdk/clients/decorators/erc7579/installModule.ts +++ b/src/sdk/clients/decorators/erc7579/installModule.ts @@ -111,9 +111,9 @@ export async function installModule< if (addressEquals(address, SMART_SESSIONS_ADDRESS)) { const nexusAccount = account as NexusAccount - if (nexusAccount?.k1ValidatorAddress) { + if (nexusAccount?.validatorAddress) { calls.push({ - to: nexusAccount.k1ValidatorAddress, + to: nexusAccount.validatorAddress, value: BigInt(0), data: encodeFunctionData({ abi: [ diff --git a/src/sdk/clients/decorators/mee/execute.test.ts b/src/sdk/clients/decorators/mee/execute.test.ts new file mode 100644 index 00000000..76d6d651 --- /dev/null +++ b/src/sdk/clients/decorators/mee/execute.test.ts @@ -0,0 +1,78 @@ +import type { Address, Chain, Hex, LocalAccount } from "viem" +import { base } from "viem/chains" +import { afterAll, beforeAll, describe, expect, test, vi } from "vitest" +import { toNetwork } from "../../../../test/testSetup" +import type { NetworkConfig } from "../../../../test/testUtils" +import { + type MultichainSmartAccount, + toMultichainNexusAccount +} from "../../../account/utils/toMultiChainNexusAccount" +import { type MeeClient, createMeeClient } from "../../createMeeClient" +import { execute } from "./execute" +import type { Instruction } from "./getQuote" + +vi.mock("./execute") + +describe("mee:execute", () => { + let network: NetworkConfig + let eoaAccount: LocalAccount + let paymentChain: Chain + let paymentToken: Address + let mcNexusMainnet: MultichainSmartAccount + let meeClient: MeeClient + + beforeAll(async () => { + network = await toNetwork("MAINNET_FROM_ENV_VARS") + + paymentChain = network.chain + paymentToken = network.paymentToken! + eoaAccount = network.account! + + mcNexusMainnet = await toMultichainNexusAccount({ + chains: [base, paymentChain], + signer: eoaAccount + }) + + meeClient = createMeeClient({ account: mcNexusMainnet }) + }) + + test("should execute a quote using execute", async () => { + const instructions: Instruction[] = [ + { + calls: [ + { + to: "0x0000000000000000000000000000000000000000", + gasLimit: 50000n, + value: 0n + } + ], + chainId: 8453 + } + ] + + expect(instructions).toBeDefined() + + // Mock the execute function + const mockExecuteResponse = { hash: "0x123" as Hex } + // Mock implementation for this specific test + vi.mocked(execute).mockResolvedValue(mockExecuteResponse) + + const { hash } = await execute(meeClient, { + instructions: instructions, + feeToken: { + address: paymentToken, + chainId: paymentChain.id + } + }) + + expect(hash).toEqual(mockExecuteResponse.hash) + + expect(execute).toHaveBeenCalledWith(meeClient, { + instructions: instructions, + feeToken: { + address: paymentToken, + chainId: paymentChain.id + } + }) + }) +}) diff --git a/src/sdk/clients/decorators/mee/execute.ts b/src/sdk/clients/decorators/mee/execute.ts new file mode 100644 index 00000000..6c97cb93 --- /dev/null +++ b/src/sdk/clients/decorators/mee/execute.ts @@ -0,0 +1,30 @@ +import type { BaseMeeClient } from "../../createMeeClient" +import { + type ExecuteSignedQuotePayload, + executeSignedQuote +} from "./executeSignedQuote" +import getQuote, { type GetQuoteParams } from "./getQuote" +import { signQuote } from "./signQuote" + +/** + * Executes a quote by fetching it, signing it, and then executing the signed quote. + * @param client - The Mee client to use + * @param params - The parameters for signing the quote + * @returns The hash of the executed transaction + * @example + * const hash = await execute(client, { + * instructions: [ + * ... + * ] + * }) + */ +export const execute = async ( + client: BaseMeeClient, + params: GetQuoteParams +): Promise => { + const quote = await getQuote(client, params) + const signedQuote = await signQuote(client, { quote }) + return executeSignedQuote(client, { signedQuote }) +} + +export default execute diff --git a/src/sdk/clients/decorators/mee/executeQuote.test.ts b/src/sdk/clients/decorators/mee/executeQuote.test.ts new file mode 100644 index 00000000..47a3db1e --- /dev/null +++ b/src/sdk/clients/decorators/mee/executeQuote.test.ts @@ -0,0 +1,75 @@ +import type { Address, Chain, Hex, LocalAccount } from "viem" +import { base } from "viem/chains" +import { afterAll, beforeAll, describe, expect, test, vi } from "vitest" +import { toNetwork } from "../../../../test/testSetup" +import type { NetworkConfig } from "../../../../test/testUtils" +import { + type MultichainSmartAccount, + toMultichainNexusAccount +} from "../../../account/utils/toMultiChainNexusAccount" +import { type MeeClient, createMeeClient } from "../../createMeeClient" +import executeQuote from "./executeQuote" +import type { ExecuteSignedQuotePayload } from "./executeSignedQuote" +import { type Instruction, getQuote } from "./getQuote" + +vi.mock("./executeQuote") + +describe("mee:executeQuote", () => { + let network: NetworkConfig + let eoaAccount: LocalAccount + let paymentChain: Chain + let paymentToken: Address + let mcNexusMainnet: MultichainSmartAccount + let meeClient: MeeClient + + beforeAll(async () => { + network = await toNetwork("MAINNET_FROM_ENV_VARS") + + paymentChain = network.chain + paymentToken = network.paymentToken! + eoaAccount = network.account! + + mcNexusMainnet = await toMultichainNexusAccount({ + chains: [base, paymentChain], + signer: eoaAccount + }) + + meeClient = createMeeClient({ account: mcNexusMainnet }) + }) + + test("should execute a quote using", async () => { + const instructions: Instruction[] = [ + { + calls: [ + { + to: "0x0000000000000000000000000000000000000000", + gasLimit: 50000n, + value: 0n + } + ], + chainId: 8453 + } + ] + + expect(instructions).toBeDefined() + + // Mock the execute function + const mockExecuteQuoteResponse: ExecuteSignedQuotePayload = { + hash: "0x123" as Hex + } + // Mock implementation for this specific test + vi.mocked(executeQuote).mockResolvedValue(mockExecuteQuoteResponse) + + const quote = await getQuote(meeClient, { + instructions: instructions, + feeToken: { + address: paymentToken, + chainId: paymentChain.id + } + }) + + const executedQuote = await executeQuote(meeClient, { quote }) + + expect(executedQuote).toEqual(mockExecuteQuoteResponse) + }) +}) diff --git a/src/sdk/clients/decorators/mee/executeQuote.ts b/src/sdk/clients/decorators/mee/executeQuote.ts new file mode 100644 index 00000000..78eb7e41 --- /dev/null +++ b/src/sdk/clients/decorators/mee/executeQuote.ts @@ -0,0 +1,28 @@ +import type { BaseMeeClient } from "../../createMeeClient" +import { + type ExecuteSignedQuotePayload, + executeSignedQuote +} from "./executeSignedQuote" +import { type SignQuoteParams, signQuote } from "./signQuote" + +/** + * Executes a quote by signing it and then executing the signed quote. + * @param client - The Mee client to use + * @param params - The parameters for signing the quote + * @returns The hash of the executed transaction + * @example + * const hash = await executeQuote(client, { + * quote: { + * ... + * } + * }) + */ +export const executeQuote = async ( + client: BaseMeeClient, + params: SignQuoteParams +): Promise => { + const signedQuote = await signQuote(client, params) + return executeSignedQuote(client, { signedQuote }) +} + +export default executeQuote diff --git a/src/sdk/clients/decorators/mee/executeSignedFusionQuote.ts b/src/sdk/clients/decorators/mee/executeSignedFusionQuote.ts new file mode 100644 index 00000000..6c755222 --- /dev/null +++ b/src/sdk/clients/decorators/mee/executeSignedFusionQuote.ts @@ -0,0 +1,47 @@ +import type { Hex, TransactionReceipt } from "viem" +import type { BaseMeeClient } from "../../createMeeClient" +import type { ExecuteSignedQuotePayload } from "./executeSignedQuote" +import type { SignFusionQuotePayload } from "./signFusionQuote" + +export type ExecuteSignedFusionQuoteParams = { + /** Quote to be executed */ + signedFusionQuote: SignFusionQuotePayload +} + +export type ExecuteSignedFusionQuotePayload = { + /** Hash of the executed transaction */ + hash: Hex + /** Transaction receipt */ + receipt: TransactionReceipt +} + +/** + * Executes a signed quote. + * @param client - The Mee client to use + * @param params - The parameters for executing the signed quote + * @returns The hash of the executed transaction + * @example + * const hash = await executeSignedFusionQuote(client, { + * signedFusionQuote: { + * ... + * } + * }) + */ +export const executeSignedFusionQuote = async ( + client: BaseMeeClient, + params: ExecuteSignedFusionQuoteParams +): Promise => { + const { receipt, ...signedFusionQuote } = params.signedFusionQuote + + const { hash } = await client.request({ + path: "v1/exec", + body: signedFusionQuote + }) + + return { + hash, + receipt + } +} + +export default executeSignedFusionQuote diff --git a/src/sdk/clients/decorators/mee/executeSignedQuote.test.ts b/src/sdk/clients/decorators/mee/executeSignedQuote.test.ts new file mode 100644 index 00000000..06458656 --- /dev/null +++ b/src/sdk/clients/decorators/mee/executeSignedQuote.test.ts @@ -0,0 +1,78 @@ +import type { Address, Chain, Hex, LocalAccount } from "viem" +import { base } from "viem/chains" +import { afterAll, beforeAll, describe, expect, test, vi } from "vitest" +import { toNetwork } from "../../../../test/testSetup" +import type { NetworkConfig } from "../../../../test/testUtils" +import type { MultichainSmartAccount } from "../../../account/utils/toMultiChainNexusAccount" +import { toMultichainNexusAccount } from "../../../account/utils/toMultiChainNexusAccount" +import { type MeeClient, createMeeClient } from "../../createMeeClient" +import { executeSignedQuote } from "./executeSignedQuote" +import type { Instruction } from "./getQuote" +import { signQuote } from "./signQuote" +vi.mock("./executeSignedQuote") + +describe("mee:executeSignedQuote", () => { + let network: NetworkConfig + let eoaAccount: LocalAccount + let paymentChain: Chain + let paymentToken: Address + let mcNexusMainnet: MultichainSmartAccount + let meeClient: MeeClient + + beforeAll(async () => { + network = await toNetwork("MAINNET_FROM_ENV_VARS") + + paymentChain = network.chain + paymentToken = network.paymentToken! + eoaAccount = network.account! + + mcNexusMainnet = await toMultichainNexusAccount({ + chains: [base, paymentChain], + signer: eoaAccount + }) + + meeClient = createMeeClient({ account: mcNexusMainnet }) + }) + + test("should execute a quote using executeSignedQuote", async () => { + const instructions: Instruction[] = [ + { + calls: [ + { + to: "0x0000000000000000000000000000000000000000", + gasLimit: 50000n, + value: 0n + } + ], + chainId: 8453 + } + ] + + expect(instructions).toBeDefined() + + // Mock the executeSignedQuote function + const mockExecuteResponse = { hash: "0x123" as Hex } + // Mock implementation for this specific test + vi.mocked(executeSignedQuote).mockResolvedValue(mockExecuteResponse) + + const quote = await meeClient.getQuote({ + instructions, + feeToken: { + address: paymentToken, + chainId: paymentChain.id + } + }) + + const signedQuote = await signQuote(meeClient, { quote }) + + const { hash } = await executeSignedQuote(meeClient, { + signedQuote + }) + + expect(hash).toEqual(mockExecuteResponse.hash) + + expect(executeSignedQuote).toHaveBeenCalledWith(meeClient, { + signedQuote + }) + }) +}) diff --git a/src/sdk/clients/decorators/mee/executeSignedQuote.ts b/src/sdk/clients/decorators/mee/executeSignedQuote.ts new file mode 100644 index 00000000..35b722e9 --- /dev/null +++ b/src/sdk/clients/decorators/mee/executeSignedQuote.ts @@ -0,0 +1,36 @@ +import type { Hex } from "viem" +import type { BaseMeeClient } from "../../createMeeClient" +import type { SignQuotePayload } from "./signQuote" + +export type ExecuteSignedQuoteParams = { + /** Quote to be executed */ + signedQuote: SignQuotePayload +} + +export type ExecuteSignedQuotePayload = { + /** Hash of the executed transaction */ + hash: Hex +} + +/** + * Executes a signed quote. + * @param client - The Mee client to use + * @param params - The parameters for executing the signed quote + * @returns The hash of the executed transaction + * @example + * const hash = await executeSignedQuote(client, { + * signedQuote: { + * ... + * } + * }) + */ +export const executeSignedQuote = async ( + client: BaseMeeClient, + params: ExecuteSignedQuoteParams +): Promise => + client.request({ + path: "v1/exec", + body: params.signedQuote + }) + +export default executeSignedQuote diff --git a/src/sdk/clients/decorators/mee/getQuote.test.ts b/src/sdk/clients/decorators/mee/getQuote.test.ts new file mode 100644 index 00000000..3c13b6f4 --- /dev/null +++ b/src/sdk/clients/decorators/mee/getQuote.test.ts @@ -0,0 +1,81 @@ +import type { Address, Chain, LocalAccount, erc20Abi } from "viem" +import { base } from "viem/chains" +import { beforeAll, describe, expect, test } from "vitest" +import { toNetwork } from "../../../../test/testSetup" +import type { NetworkConfig } from "../../../../test/testUtils" +import type { MultichainSmartAccount } from "../../../account/utils/toMultiChainNexusAccount" +import { toMultichainNexusAccount } from "../../../account/utils/toMultiChainNexusAccount" +import { type MeeClient, createMeeClient } from "../../createMeeClient" +import { type Instruction, getQuote } from "./getQuote" + +describe("mee:getQuote", () => { + let network: NetworkConfig + let eoaAccount: LocalAccount + let paymentChain: Chain + let paymentToken: Address + let mcNexusMainnet: MultichainSmartAccount + let meeClient: MeeClient + + beforeAll(async () => { + network = await toNetwork("MAINNET_FROM_ENV_VARS") + + paymentChain = network.chain + paymentToken = network.paymentToken! + eoaAccount = network.account! + + mcNexusMainnet = await toMultichainNexusAccount({ + chains: [base, paymentChain], + signer: eoaAccount + }) + + meeClient = createMeeClient({ account: mcNexusMainnet }) + }) + + test("should resolve instructions", async () => { + const instructions: Instruction[] = [ + { + calls: [ + { + to: "0x0000000000000000000000000000000000000000", + gasLimit: 50000n, + value: 0n + } + ], + chainId: 8453 + }, + () => ({ + calls: [ + { + to: "0x0000000000000000000000000000000000000000", + gasLimit: 50000n, + value: 0n + } + ], + chainId: 8453 + }), + Promise.resolve({ + calls: [ + { + to: "0x0000000000000000000000000000000000000000", + gasLimit: 50000n, + value: 0n + } + ], + chainId: 8453 + }) + ] + + expect(instructions).toBeDefined() + expect(instructions.length).toEqual(3) + + const quote = await getQuote(meeClient, { + instructions: instructions, + feeToken: { + address: paymentToken, + chainId: paymentChain.id + } + }) + + expect(quote).toBeDefined() + }) +}) diff --git a/src/sdk/clients/decorators/mee/getQuote.ts b/src/sdk/clients/decorators/mee/getQuote.ts new file mode 100644 index 00000000..38dae91f --- /dev/null +++ b/src/sdk/clients/decorators/mee/getQuote.ts @@ -0,0 +1,291 @@ +import type { Address, Hex, OneOf } from "viem" +import type { MultichainSmartAccount } from "../../../account/utils/toMultiChainNexusAccount" +import type { AnyData } from "../../../modules/utils/Types" +import type { BaseMeeClient } from "../../createMeeClient" + +/** + * Represents an abstract call to be executed in the transaction + */ +export type AbstractCall = { + /** Address of the contract to call */ + to: Address + /** Gas limit for the call execution */ + gasLimit: bigint +} & OneOf< + | { value: bigint; data?: Hex } + | { value?: bigint; data: Hex } + | { value: bigint; data: Hex } +> + +/** + * Information about the fee token to be used for the transaction + */ +export type FeeTokenInfo = { + /** Address of the fee token */ + address: Address + /** Chain ID where the fee token is deployed */ + chainId: number +} + +/** + * Information about the instructions to be executed in the transaction + * @internal + */ +export type InstructionResolved = { + /** Array of abstract calls to be executed in the transaction */ + calls: AbstractCall[] + /** Chain ID where the transaction will be executed */ + chainId: number +} + +/** + * Represents an instruction to be executed in the transaction + * @type Instruction + */ +export type Instruction = + | InstructionResolved + | InstructionResolved[] + | ((x?: AnyData) => InstructionResolved) + | ((x?: AnyData) => InstructionResolved[]) + | Promise + | Promise + +/** + * Represents a supertransaction, which is a collection of instructions to be executed in a single transaction + * @type SuperTransaction + */ +export type SuperTransaction = { + /** Array of instructions to be executed in the transaction */ + instructions: Instruction[] + /** Token to be used for paying transaction fees */ + feeToken: FeeTokenInfo +} + +/** + * Parameters required for requesting a quote from the MEE service + * @type GetQuoteParams + */ +export type GetQuoteParams = SuperTransaction & { + /** Optional smart account to execute the transaction. If not provided, uses the client's default account */ + account?: MultichainSmartAccount +} + +/** + * Internal structure for submitting a quote request to the MEE service + * @internal + */ +type QuoteRequest = { + /** Array of user operations to be executed */ + userOps: { + /** Address of the account initiating the operation */ + sender: string + /** Encoded transaction data */ + callData: string + /** Gas limit for the call execution */ + callGasLimit: string + /** Account nonce */ + nonce: string + /** Chain ID where the operation will be executed */ + chainId: string + }[] + /** Payment details for the transaction */ + paymentInfo: PaymentInfo +} + +/** + * Basic payment information required for a quote request + * @interface PaymentInfo + */ +export type PaymentInfo = { + /** Address of the account paying for the transaction */ + sender: Address + /** Optional initialization code for account deployment */ + initCode?: Hex + /** Address of the token used for payment */ + token: Address + /** Current nonce of the sender account */ + nonce: string + /** Chain ID where the payment will be processed */ + chainId: string +} + +/** + * Extended payment information including calculated token amounts + * @interface FilledPaymentInfo + * @extends {Required} + */ +export type FilledPaymentInfo = Required & { + /** Human-readable token amount */ + tokenAmount: string + /** Token amount in wei */ + tokenWeiAmount: string + /** Token value in the transaction */ + tokenValue: string +} + +/** + * Detailed user operation structure with all required fields + * @interface MeeFilledUserOp + */ +export interface MeeFilledUserOp { + /** Address of the account initiating the operation */ + sender: Address + /** Account nonce */ + nonce: string + /** Account initialization code */ + initCode: Hex + /** Encoded transaction data */ + callData: Hex + /** Gas limit for the call execution */ + callGasLimit: string + /** Gas limit for verification */ + verificationGasLimit: string + /** Maximum fee per gas unit */ + maxFeePerGas: string + /** Maximum priority fee per gas unit */ + maxPriorityFeePerGas: string + /** Encoded paymaster data */ + paymasterAndData: Hex + /** Gas required before operation verification */ + preVerificationGas: string +} + +/** + * Extended user operation details including timing and gas parameters + * @interface MeeFilledUserOpDetails + */ +export interface MeeFilledUserOpDetails { + /** Complete user operation data */ + userOp: MeeFilledUserOp + /** Hash of the user operation */ + userOpHash: Hex + /** MEE-specific hash of the user operation */ + meeUserOpHash: Hex + /** Lower bound timestamp for operation validity */ + lowerBoundTimestamp: string + /** Upper bound timestamp for operation validity */ + upperBoundTimestamp: string + /** Maximum gas limit for the operation */ + maxGasLimit: string + /** Maximum fee per gas unit */ + maxFeePerGas: string + /** Chain ID where the operation will be executed */ + chainId: string +} + +/** + * Complete quote response from the MEE service + * @type GetQuotePayload + */ +export type GetQuotePayload = { + /** Hash of the supertransaction */ + hash: Hex + /** Address of the MEE node */ + node: Address + /** Commitment hash */ + commitment: Hex + /** Complete payment information with token amounts */ + paymentInfo: FilledPaymentInfo + /** Array of user operations with their details */ + userOps: MeeFilledUserOpDetails[] +} + +/** + * Requests a quote from the MEE service for executing a set of instructions + * @async + * @param client - MEE client instance used to make the request + * @param params - Parameters for the quote request + * @returns Promise resolving to a committed supertransaction quote + * @throws Error if the account is not deployed on any required chain + * @example + * ```typescript + * const quote = await getQuote(meeClient, { + * instructions: [...], + * feeToken: { address: '0x...', chainId: 1 }, + * account: smartAccount + * }); + * ``` + */ +export const getQuote = async ( + client: BaseMeeClient, + params: GetQuoteParams +): Promise => { + const { account: account_ = client.account, instructions, feeToken } = params + + const resolvedInstructions = (await Promise.all( + instructions.flatMap((userOp) => + typeof userOp === "function" ? userOp(client) : userOp + ) + )) as InstructionResolved[] + + const validUserOps = resolvedInstructions.every((userOp) => + account_.deploymentOn(userOp.chainId) + ) + const validPaymentAccount = account_.deploymentOn(feeToken.chainId) + if (!validPaymentAccount || !validUserOps) { + throw Error("Account is not deployed on necessary chain(s)") + } + + const userOpResults = await Promise.all( + resolvedInstructions.map((userOp) => { + const deployment = account_.deploymentOn(userOp.chainId) + return Promise.all([ + deployment.encodeExecuteBatch(userOp.calls), + deployment.getNonce(), + deployment.isDeployed(), + deployment.getInitCode(), + deployment.address, + userOp.calls + .map((tx) => tx.gasLimit) + .reduce((curr, acc) => curr + acc) + .toString(), + userOp.chainId.toString() + ]) + }) + ) + + const userOps = userOpResults.map( + ([ + callData, + nonce_, + isAccountDeployed, + initCode, + sender, + callGasLimit, + chainId + ]) => ({ + sender, + callData, + callGasLimit, + nonce: nonce_.toString(), + chainId, + ...(!isAccountDeployed && initCode ? { initCode } : {}) + }) + ) + + const [nonce, isAccountDeployed, initCode] = await Promise.all([ + validPaymentAccount.getNonce(), + validPaymentAccount.isDeployed(), + validPaymentAccount.getInitCode() + ]) + + const paymentInfo: PaymentInfo = { + sender: validPaymentAccount.address, + token: feeToken.address, + nonce: nonce.toString(), + chainId: feeToken.chainId.toString(), + ...(!isAccountDeployed && initCode ? { initCode } : {}) + } + + const quoteRequest: QuoteRequest = { + userOps, + paymentInfo + } + + return await client.request({ + path: "v1/quote", + body: quoteRequest + }) +} + +export default getQuote diff --git a/src/sdk/clients/decorators/mee/index.ts b/src/sdk/clients/decorators/mee/index.ts new file mode 100644 index 00000000..0b5071fb --- /dev/null +++ b/src/sdk/clients/decorators/mee/index.ts @@ -0,0 +1,181 @@ +import type { BaseMeeClient } from "../../createMeeClient" +import execute from "./execute" +import executeQuote from "./executeQuote" +import executeSignedFusionQuote, { + type ExecuteSignedFusionQuoteParams, + type ExecuteSignedFusionQuotePayload +} from "./executeSignedFusionQuote" +import executeSignedQuote, { + type ExecuteSignedQuoteParams, + type ExecuteSignedQuotePayload +} from "./executeSignedQuote" +import { type GetQuoteParams, type GetQuotePayload, getQuote } from "./getQuote" +import signFusionQuote, { + type SignFusionQuoteParams, + type SignFusionQuotePayload +} from "./signFusionQuote" +import signQuote, { + type SignQuotePayload, + type SignQuoteParams +} from "./signQuote" +import waitForSupertransactionReceipt, { + type WaitForSupertransactionReceiptParams, + type WaitForSupertransactionReceiptPayload +} from "./waitForSupertransactionReceipt" + +export type MeeActions = { + /** + * Get a quote for executing a set of instructions + * @param params - {@link GetQuoteParams} + * @returns: {@link GetQuotePayload} + * @throws Error if the account is not deployed on any required chain + * @example + * ```typescript + * const quote = await meeClient.getQuote({ + * instructions: [...], + * feeToken: { + * address: '0x...', + * chainId: 1 + * } + * }) + * ``` + */ + getQuote: (params: GetQuoteParams) => Promise + + /** + * Sign a quote for executing a set of instructions + * @param: {@link SignQuoteParams} + * @returns: {@link SignQuotePayload} + * @example + * ```typescript + * const SignQuotePayload = await meeClient.signQuote({ + * quote: quote, + * executionMode: "direct-to-mee" + * }) + * ``` + */ + signQuote: (params: SignQuoteParams) => Promise + + /** + * Execute a signed quote + * @param: {@link ExecuteSignedQuoteParams} + * @returns: {@link ExecuteSignedQuotePayload} + * @example + * ```typescript + * const hash = await meeClient.executeSignedQuote({ + * signedQuote: { + * ... + * } + * }) + * ``` + */ + executeSignedQuote: ( + params: ExecuteSignedQuoteParams + ) => Promise + /** + * Execute a quote by fetching it, signing it, and then executing the signed quote. + * @param: {@link GetQuoteParams} + * @returns: {@link ExecuteSignedQuotePayload} + * @example + * ```typescript + * const hash = await meeClient.execute({ + * instructions: [...], + * feeToken: { + * address: '0x...', + * chainId: 1 + * } + * }) + * ``` + */ + execute: (params: GetQuoteParams) => Promise + + /** + * Execute a quote by fetching it, signing it, and then executing the signed quote. + * @param: {@link GetQuoteParams} + * @returns: {@link ExecuteSignedQuotePayload} + * @example + * ```typescript + * const hash = await meeClient.executeQuote({ + * instructions: [...], + * feeToken: { + * address: '0x...', + * chainId: 1 + * } + * }) + * ``` + */ + executeQuote: (params: SignQuoteParams) => Promise + + /** + * Wait for a super transaction receipt to be available + * @param: {@link WaitForSupertransactionReceiptParams} + * @returns: {@link WaitForSupertransactionReceiptPayload} + * @example + * ```typescript + * const receipt = await meeClient.waitForSupertransactionReceipt({ + * hash: "0x..." + * }) + * ``` + */ + waitForSupertransactionReceipt: ( + params: WaitForSupertransactionReceiptParams + ) => Promise + /** + * Sign a fusion quote + * @param: {@link SignFusionQuoteParams} + * @returns: {@link SignFusionQuotePayload} + * @example + * ```typescript + * const signedQuote = await meeClient.signFusionQuote({ + * quote: quote, + * executionMode: "direct-to-mee" + * }) + * ``` + */ + signFusionQuote: ( + params: SignFusionQuoteParams + ) => Promise + + /** + * Execute a signed fusion quote + * @param: {@link ExecuteSignedFusionQuoteParams} + * @returns: {@link ExecuteSignedFusionQuotePayload} + * @example + * ```typescript + * const hash = await meeClient.executeSignedFusionQuote({ + * signedFusionQuote: { + * ... + * } + * }) + * ``` + */ + executeSignedFusionQuote: ( + params: ExecuteSignedFusionQuoteParams + ) => Promise +} + +export const meeActions = (meeClient: BaseMeeClient): MeeActions => { + return { + getQuote: (params: GetQuoteParams) => getQuote(meeClient, params), + signQuote: (params: SignQuoteParams) => signQuote(meeClient, params), + executeSignedQuote: (params: ExecuteSignedQuoteParams) => + executeSignedQuote(meeClient, params), + execute: (params: GetQuoteParams) => execute(meeClient, params), + executeQuote: (params: SignQuoteParams) => executeQuote(meeClient, params), + waitForSupertransactionReceipt: ( + params: WaitForSupertransactionReceiptParams + ) => waitForSupertransactionReceipt(meeClient, params), + signFusionQuote: (params: SignFusionQuoteParams) => + signFusionQuote(meeClient, params), + executeSignedFusionQuote: (params: ExecuteSignedFusionQuoteParams) => + executeSignedFusionQuote(meeClient, params) + } +} +export * from "./getQuote" +export * from "./signFusionQuote" +export * from "./executeSignedFusionQuote" +export * from "./signQuote" +export * from "./executeSignedQuote" +export * from "./execute" +export * from "./executeQuote" +export * from "./waitForSupertransactionReceipt" diff --git a/src/sdk/clients/decorators/mee/signFusionQuote.test.ts b/src/sdk/clients/decorators/mee/signFusionQuote.test.ts new file mode 100644 index 00000000..e74dc016 --- /dev/null +++ b/src/sdk/clients/decorators/mee/signFusionQuote.test.ts @@ -0,0 +1,114 @@ +import type { Address, Chain, Hex, LocalAccount } from "viem" +import { base } from "viem/chains" +import { beforeAll, describe, expect, inject, test, vi } from "vitest" +import { toNetwork } from "../../../../test/testSetup" +import type { NetworkConfig } from "../../../../test/testUtils" +import { + type MultichainSmartAccount, + toMultichainNexusAccount +} from "../../../account/utils/toMultiChainNexusAccount" +import { type MeeClient, createMeeClient } from "../../createMeeClient" +import executeSignedFusionQuote, { + type ExecuteSignedFusionQuotePayload +} from "./executeSignedFusionQuote" +import { type Instruction, getQuote } from "./getQuote" +import { signFusionQuote } from "./signFusionQuote" + +const { runPaidTests } = inject("settings") + +describe.runIf(runPaidTests).skip("mee:signFusionQuote", () => { + let network: NetworkConfig + let eoaAccount: LocalAccount + let paymentChain: Chain + let paymentToken: Address + let mcNexusMainnet: MultichainSmartAccount + let meeClient: MeeClient + + beforeAll(async () => { + network = await toNetwork("MAINNET_FROM_ENV_VARS") + + paymentChain = network.chain + paymentToken = network.paymentToken! + eoaAccount = network.account! + + mcNexusMainnet = await toMultichainNexusAccount({ + chains: [base, paymentChain], + signer: eoaAccount + }) + + meeClient = createMeeClient({ account: mcNexusMainnet }) + }) + + test("should execute a quote using executeSignedFusionQuote", async () => { + const instructions: Instruction[] = [ + { + calls: [ + { + to: "0x0000000000000000000000000000000000000000", + gasLimit: 50000n, + value: 0n + } + ], + chainId: 8453 + } + ] + + expect(instructions).toBeDefined() + + // Mock the execute function + const mockExecuteQuoteResponse: ExecuteSignedFusionQuotePayload = { + hash: "0x123" as Hex, + receipt: { + blobGasPrice: undefined, + blobGasUsed: undefined, + blockHash: "0x", + blockNumber: 0n, + contractAddress: undefined, + cumulativeGasUsed: 0n, + effectiveGasPrice: 0n, + from: "0x", + gasUsed: 0n, + logs: [], + logsBloom: "0x", + root: undefined, + status: "success", + to: null, + transactionHash: "0x", + transactionIndex: 0, + type: "legacy" + } + } + // Mock implementation for this specific test + vi.mocked(executeSignedFusionQuote).mockResolvedValue( + mockExecuteQuoteResponse + ) + + const quote = await getQuote(meeClient, { + instructions: instructions, + feeToken: { + address: paymentToken, + chainId: paymentChain.id + } + }) + + const signedFusionQuote = await signFusionQuote(meeClient, { + quote, + trigger: { + call: { + to: "0x0000000000000000000000000000000000000000", + value: 0n + }, + chain: paymentChain + } + }) + + const executeSignedFusionQuoteResponse = await executeSignedFusionQuote( + meeClient, + { + signedFusionQuote + } + ) + + expect(executeSignedFusionQuoteResponse).toEqual(mockExecuteQuoteResponse) + }) +}) diff --git a/src/sdk/clients/decorators/mee/signFusionQuote.ts b/src/sdk/clients/decorators/mee/signFusionQuote.ts new file mode 100644 index 00000000..117338dd --- /dev/null +++ b/src/sdk/clients/decorators/mee/signFusionQuote.ts @@ -0,0 +1,105 @@ +import { + http, + type Chain, + type Hex, + type TransactionReceipt, + concatHex, + createWalletClient, + encodeAbiParameters, + publicActions +} from "viem" +import type { Call } from "../../../account/utils/Types" +import type { MultichainSmartAccount } from "../../../account/utils/toMultiChainNexusAccount" +import type { BaseMeeClient } from "../../createMeeClient" +import type { GetQuotePayload } from "./getQuote" +import { type ExecutionMode, PREFIX } from "./signQuote" + +export const FUSION_NATIVE_TRANSFER_PREFIX = "0x150b7a02" + +/** + * Parameters required for requesting a quote from the MEE service + * @interface SignFusionQuoteParams + */ +export type SignFusionQuoteParams = { + /** The quote to sign */ + quote: GetQuotePayload + /** Optional smart account to execute the transaction. If not provided, uses the client's default account */ + account?: MultichainSmartAccount + /** The execution mode to use. Defaults to "direct-to-mee" */ + executionMode?: ExecutionMode + /** The on-chain transaction to use as the trigger */ + trigger: { + /** The on-chain transaction to use as the trigger */ + call: Call + /** The chain to use */ + chain: Chain + } +} + +export type SignFusionQuotePayload = GetQuotePayload & { + /** The signature of the quote */ + signature: Hex + /** The transaction receipt */ + receipt: TransactionReceipt +} + +/** + * Signs a fusion quote + * @param client - The Mee client to use + * @param params - The parameters for the fusion quote + * @returns The signed quote + * @example + * const signedQuote = await signFusionQuote(meeClient, { + * quote: quotePayload, + * account: smartAccount + * }) + */ +export const signFusionQuote = async ( + client: BaseMeeClient, + params: SignFusionQuoteParams +): Promise => { + const { + account: account_ = client.account, + quote, + executionMode = "fusion-with-onchain-tx", + trigger: { call: call_, chain } + } = params + + // If the data field is empty, a prefix must be added in order for the + // chain not to reject the transaction. This is done in cases when the + // user is using the transfer of native gas to the SCA as the trigger + // transaction + const call = { + ...call_, + data: concatHex([ + call_.data ?? FUSION_NATIVE_TRANSFER_PREFIX, + PREFIX[executionMode], + quote.hash + ]) + } + + const signer = account_.signer + const masterClient = createWalletClient({ + account: signer, + chain, + transport: http() + }).extend(publicActions) + + const hash = await masterClient.sendTransaction(call) + const receipt = await masterClient.waitForTransactionReceipt({ hash }) + const signature = concatHex([ + PREFIX[executionMode], + encodeAbiParameters( + [{ type: "bytes32" }, { type: "uint256" }], + [hash, BigInt(chain.id)] + ) + ]) + + return { + receipt, + ...quote, + signature + } +} + +export default signFusionQuote diff --git a/src/sdk/clients/decorators/mee/signQuote.test.ts b/src/sdk/clients/decorators/mee/signQuote.test.ts new file mode 100644 index 00000000..fb1e0803 --- /dev/null +++ b/src/sdk/clients/decorators/mee/signQuote.test.ts @@ -0,0 +1,67 @@ +import { type Address, type Chain, type LocalAccount, isHex } from "viem" +import { base } from "viem/chains" +import { beforeAll, describe, expect, test } from "vitest" +import { toNetwork } from "../../../../test/testSetup" +import type { NetworkConfig } from "../../../../test/testUtils" +import { + type MultichainSmartAccount, + toMultichainNexusAccount +} from "../../../account/utils/toMultiChainNexusAccount" +import { type MeeClient, createMeeClient } from "../../createMeeClient" +import type { Instruction } from "./getQuote" +import { signQuote } from "./signQuote" + +describe("mee:signQuote", () => { + let network: NetworkConfig + let eoaAccount: LocalAccount + let paymentChain: Chain + let paymentToken: Address + let mcNexusMainnet: MultichainSmartAccount + let meeClient: MeeClient + + beforeAll(async () => { + network = await toNetwork("MAINNET_FROM_ENV_VARS") + + paymentChain = network.chain + paymentToken = network.paymentToken! + eoaAccount = network.account! + + mcNexusMainnet = await toMultichainNexusAccount({ + chains: [base, paymentChain], + signer: eoaAccount + }) + + meeClient = createMeeClient({ account: mcNexusMainnet }) + }) + + test("should sign a quote", async () => { + const instructions: Instruction[] = [ + { + calls: [ + { + to: "0x0000000000000000000000000000000000000000", + gasLimit: 50000n, + value: 0n + } + ], + chainId: 8453 + } + ] + + expect(instructions).toBeDefined() + + const quote = await meeClient.getQuote({ + instructions: instructions, + feeToken: { + address: paymentToken, + chainId: paymentChain.id + } + }) + + const signedQuote = await signQuote(meeClient, { quote }) + + expect(signedQuote).toBeDefined() + expect(signedQuote.signature).toBeDefined() + expect(isHex(signedQuote.signature)).toEqual(true) + }) +}) diff --git a/src/sdk/clients/decorators/mee/signQuote.ts b/src/sdk/clients/decorators/mee/signQuote.ts new file mode 100644 index 00000000..f41b1223 --- /dev/null +++ b/src/sdk/clients/decorators/mee/signQuote.ts @@ -0,0 +1,69 @@ +import { type Hex, concatHex } from "viem" +import type { MultichainSmartAccount } from "../../../account/utils/toMultiChainNexusAccount" +import type { BaseMeeClient } from "../../createMeeClient" + +import type { GetQuotePayload } from "./getQuote" + +export type ExecutionMode = + | "direct-to-mee" + | "fusion-with-onchain-tx" + | "fusion-with-erc20permit" + +/** + * Parameters required for requesting a quote from the MEE service + * @interface SignQuoteParams + */ +export type SignQuoteParams = { + /** The quote to sign */ + quote: GetQuotePayload + /** Optional smart account to execute the transaction. If not provided, uses the client's default account */ + account?: MultichainSmartAccount + /** The execution mode to use. Defaults to "direct-to-mee" */ + executionMode?: ExecutionMode +} + +export type SignQuotePayload = GetQuotePayload & { + /** The signature of the quote */ + signature: Hex +} + +export const PREFIX: Record = { + "direct-to-mee": "0x00", + "fusion-with-onchain-tx": "0x01", + "fusion-with-erc20permit": "0x02" +} + +/** + * Signs a quote + * @param client - The Mee client to use + * @param params - The parameters for the quote + * @returns The signed quote + * @example + * const signedQuote = await signQuote(meeClient, { + * quote: quotePayload, + * account: smartAccount + * }) + */ +export const signQuote = async ( + client: BaseMeeClient, + params: SignQuoteParams +): Promise => { + const { + account: account_ = client.account, + quote, + executionMode = "direct-to-mee" + } = params + + const signer = account_.signer + + const signedMessage = await signer.signMessage({ + message: { raw: quote.hash } + }) + + return { + ...quote, + signature: concatHex([PREFIX[executionMode], signedMessage]) + } +} + +export default signQuote diff --git a/src/sdk/clients/decorators/mee/waitForSupertransactionReceipt.ts b/src/sdk/clients/decorators/mee/waitForSupertransactionReceipt.ts new file mode 100644 index 00000000..8d29880f --- /dev/null +++ b/src/sdk/clients/decorators/mee/waitForSupertransactionReceipt.ts @@ -0,0 +1,113 @@ +import type { Hex } from "viem" +import { + getExplorerTxLink, + getJiffyScanLink, + getMeeScanLink +} from "../../../account/utils/explorer/explorer" +import type { Url } from "../../createHttpClient" +import type { BaseMeeClient } from "../../createMeeClient" +import type { GetQuotePayload, MeeFilledUserOpDetails } from "./getQuote" + +/** + * Parameters required for requesting a quote from the MEE service + * @type WaitForSupertransactionReceiptParams + */ +export type WaitForSupertransactionReceiptParams = { + /** The hash of the super transaction */ + hash: Hex +} + +/** + * Explorer links for each chain + * @type ExplorerLinks + */ +type ExplorerLinks = { meeScan: Url } & { + [chainId: string]: { + txHash: Url + jiffyScan: Url + } +} + +/** + * The status of a user operation + * @type UserOpStatus + */ +type UserOpStatus = { + executionStatus: "SUCCESS" | "PENDING" + executionData: Hex + executionError: string +} +/** + * The payload returned by the waitForSupertransactionReceipt function + * @type WaitForSupertransactionReceiptPayload + */ +export type WaitForSupertransactionReceiptPayload = Omit< + GetQuotePayload, + "userOps" +> & { + userOps: (MeeFilledUserOpDetails & UserOpStatus)[] + explorerLinks: ExplorerLinks +} + +/** + * Waits for a super transaction receipt to be available + * @param client - The Mee client to use + * @param params - The parameters for the super transaction + * @returns The receipt of the super transaction + * @example + * const receipt = await waitForSupertransactionReceipt(client, { + * hash: "0x..." + * }) + */ +export const waitForSupertransactionReceipt = async ( + client: BaseMeeClient, + params: WaitForSupertransactionReceiptParams +): Promise => { + const fireRequest = async () => + await client.request({ + path: `v1/explorer/${params.hash}`, + method: "GET" + }) + + const waitForSupertransactionReceipt = async () => { + const explorerResponse = await fireRequest() + + const userOpError = explorerResponse.userOps.find( + (userOp) => userOp.executionError + ) + if (userOpError) { + throw new Error(userOpError.executionError) + } + + const statuses = explorerResponse.userOps.map( + (userOp) => userOp.executionStatus + ) + + const statusPending = statuses.some((status) => status === "PENDING") + if (statusPending) { + await new Promise((resolve) => + setTimeout(resolve, client.pollingInterval) + ) + return await waitForSupertransactionReceipt() + } + + const explorerLinks = explorerResponse.userOps.reduce( + (acc, userOp) => { + acc[userOp.chainId] = { + txHash: getExplorerTxLink(userOp.executionData, userOp.chainId), + jiffyScan: getJiffyScanLink(userOp.userOpHash) + } + return acc + }, + { + meeScan: getMeeScanLink(params.hash) + } as ExplorerLinks + ) + + return { ...explorerResponse, explorerLinks } + } + + return await waitForSupertransactionReceipt() +} + +export default waitForSupertransactionReceipt diff --git a/src/sdk/clients/decorators/smartAccount/account.decorators.test.ts b/src/sdk/clients/decorators/smartAccount/account.decorators.test.ts index bb62bc1b..851eae27 100644 --- a/src/sdk/clients/decorators/smartAccount/account.decorators.test.ts +++ b/src/sdk/clients/decorators/smartAccount/account.decorators.test.ts @@ -49,7 +49,7 @@ describe("account.decorators", async () => { chain, transport: http(), bundlerTransport: http(bundlerUrl), - k1ValidatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, + validatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS }) nexusAccountAddress = await nexusClient.account.getCounterFactualAddress() diff --git a/src/sdk/clients/decorators/smartAccount/debugUserOperation.ts b/src/sdk/clients/decorators/smartAccount/debugUserOperation.ts index eaffb92a..2d0635f1 100644 --- a/src/sdk/clients/decorators/smartAccount/debugUserOperation.ts +++ b/src/sdk/clients/decorators/smartAccount/debugUserOperation.ts @@ -35,6 +35,7 @@ import { parseRequestArguments } from "../../../account/utils/Utils" import { deepHexlify } from "../../../account/utils/deepHexlify" import { getAAError } from "../../../account/utils/getAAError" import { tenderlySimulation } from "../../../account/utils/tenderlySimulation" +import type { AnyData } from "../../../modules/utils/Types" export type DebugUserOperationParameters< account extends SmartAccount | undefined = SmartAccount | undefined, accountOverride extends SmartAccount | undefined = SmartAccount | undefined, @@ -137,7 +138,6 @@ export async function debugUserOperation< )(parameters as unknown as PrepareUserOperationParameters) : parameters - // biome-ignore lint/style/noNonNullAssertion: const signature = (parameters.signature || (await account?.signUserOperation(request as UserOperation)))! @@ -163,7 +163,6 @@ export async function debugUserOperation< method: "eth_sendUserOperation", params: [ rpcParameters, - // biome-ignore lint/style/noNonNullAssertion: (entryPointAddress ?? account?.entryPoint.address)! ] }, @@ -178,7 +177,7 @@ export async function debugUserOperation< console.log({ aaError }) } - const calls = (parameters as any).calls + const calls = (parameters as AnyData).calls throw getUserOperationError(error as BaseError, { ...(request as UserOperation), ...(calls ? { calls } : {}), diff --git a/src/sdk/clients/decorators/tokenPaymaster/getTokenPaymasterQuotes.ts b/src/sdk/clients/decorators/tokenPaymaster/getTokenPaymasterQuotes.ts index 94062b96..17f3f5ea 100644 --- a/src/sdk/clients/decorators/tokenPaymaster/getTokenPaymasterQuotes.ts +++ b/src/sdk/clients/decorators/tokenPaymaster/getTokenPaymasterQuotes.ts @@ -1,5 +1,6 @@ import type { Account, Address, Chain, Client, Transport } from "viem" import type { UserOperation } from "viem/account-abstraction" +import type { AnyData } from "../../../modules" export type BicoTokenPaymasterRpcSchema = [ { @@ -27,7 +28,7 @@ export type TokenPaymasterQuotesResponse = { mode: PaymasterMode paymasterAddress: Address feeQuotes: FeeQuote[] - unsupportedTokens: any[] + unsupportedTokens: AnyData[] } export type TokenPaymasterUserOpParams = { diff --git a/src/sdk/constants/abi/AccountFactory.ts b/src/sdk/constants/abi/AccountFactory.ts new file mode 100644 index 00000000..b0abc196 --- /dev/null +++ b/src/sdk/constants/abi/AccountFactory.ts @@ -0,0 +1,323 @@ +export const AccountFactoryAbi = [ + { + inputs: [ + { + internalType: "address", + name: "implementation_", + type: "address" + }, + { + internalType: "address", + name: "owner_", + type: "address" + } + ], + stateMutability: "nonpayable", + type: "constructor" + }, + { + inputs: [ + { + internalType: "address", + name: "account", + type: "address" + } + ], + name: "AccountAlreadyDeployed", + type: "error" + }, + { + inputs: [], + name: "AlreadyInitialized", + type: "error" + }, + { + inputs: [], + name: "ImplementationAddressCanNotBeZero", + type: "error" + }, + { + inputs: [], + name: "InvalidEntryPointAddress", + type: "error" + }, + { + inputs: [], + name: "NewOwnerIsZeroAddress", + type: "error" + }, + { + inputs: [], + name: "NoHandoverRequest", + type: "error" + }, + { + inputs: [], + name: "Unauthorized", + type: "error" + }, + { + inputs: [], + name: "ZeroAddressNotAllowed", + type: "error" + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "account", + type: "address" + }, + { + indexed: true, + internalType: "bytes", + name: "initData", + type: "bytes" + }, + { + indexed: true, + internalType: "bytes32", + name: "salt", + type: "bytes32" + } + ], + name: "AccountCreated", + type: "event" + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "pendingOwner", + type: "address" + } + ], + name: "OwnershipHandoverCanceled", + type: "event" + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "pendingOwner", + type: "address" + } + ], + name: "OwnershipHandoverRequested", + type: "event" + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "oldOwner", + type: "address" + }, + { + indexed: true, + internalType: "address", + name: "newOwner", + type: "address" + } + ], + name: "OwnershipTransferred", + type: "event" + }, + { + inputs: [], + name: "ACCOUNT_IMPLEMENTATION", + outputs: [ + { + internalType: "address", + name: "", + type: "address" + } + ], + stateMutability: "view", + type: "function" + }, + { + inputs: [ + { + internalType: "address", + name: "epAddress", + type: "address" + }, + { + internalType: "uint32", + name: "unstakeDelaySec", + type: "uint32" + } + ], + name: "addStake", + outputs: [], + stateMutability: "payable", + type: "function" + }, + { + inputs: [], + name: "cancelOwnershipHandover", + outputs: [], + stateMutability: "payable", + type: "function" + }, + { + inputs: [ + { + internalType: "address", + name: "pendingOwner", + type: "address" + } + ], + name: "completeOwnershipHandover", + outputs: [], + stateMutability: "payable", + type: "function" + }, + { + inputs: [ + { + internalType: "bytes", + name: "initData", + type: "bytes" + }, + { + internalType: "bytes32", + name: "salt", + type: "bytes32" + } + ], + name: "computeAccountAddress", + outputs: [ + { + internalType: "address payable", + name: "expectedAddress", + type: "address" + } + ], + stateMutability: "view", + type: "function" + }, + { + inputs: [ + { + internalType: "bytes", + name: "initData", + type: "bytes" + }, + { + internalType: "bytes32", + name: "salt", + type: "bytes32" + } + ], + name: "createAccount", + outputs: [ + { + internalType: "address payable", + name: "", + type: "address" + } + ], + stateMutability: "payable", + type: "function" + }, + { + inputs: [], + name: "owner", + outputs: [ + { + internalType: "address", + name: "result", + type: "address" + } + ], + stateMutability: "view", + type: "function" + }, + { + inputs: [ + { + internalType: "address", + name: "pendingOwner", + type: "address" + } + ], + name: "ownershipHandoverExpiresAt", + outputs: [ + { + internalType: "uint256", + name: "result", + type: "uint256" + } + ], + stateMutability: "view", + type: "function" + }, + { + inputs: [], + name: "renounceOwnership", + outputs: [], + stateMutability: "payable", + type: "function" + }, + { + inputs: [], + name: "requestOwnershipHandover", + outputs: [], + stateMutability: "payable", + type: "function" + }, + { + inputs: [ + { + internalType: "address", + name: "newOwner", + type: "address" + } + ], + name: "transferOwnership", + outputs: [], + stateMutability: "payable", + type: "function" + }, + { + inputs: [ + { + internalType: "address", + name: "epAddress", + type: "address" + } + ], + name: "unlockStake", + outputs: [], + stateMutability: "nonpayable", + type: "function" + }, + { + inputs: [ + { + internalType: "address", + name: "epAddress", + type: "address" + }, + { + internalType: "address payable", + name: "withdrawAddress", + type: "address" + } + ], + name: "withdrawStake", + outputs: [], + stateMutability: "nonpayable", + type: "function" + } +] as const diff --git a/src/sdk/constants/abi/K1ValidatorFactory.ts b/src/sdk/constants/abi/K1ValidatorFactory.ts new file mode 100644 index 00000000..409674c4 --- /dev/null +++ b/src/sdk/constants/abi/K1ValidatorFactory.ts @@ -0,0 +1,36 @@ +export const K1ValidatorFactoryAbi = [ + { + inputs: [ + { + internalType: "address", + name: "eoaOwner", + type: "address" + }, + { + internalType: "uint256", + name: "index", + type: "uint256" + }, + { + internalType: "address[]", + name: "attesters", + type: "address[]" + }, + { + internalType: "uint8", + name: "threshold", + type: "uint8" + } + ], + name: "computeAccountAddress", + outputs: [ + { + internalType: "address payable", + name: "expectedAddress", + type: "address" + } + ], + stateMutability: "view", + type: "function" + } +] as const diff --git a/src/test/__contracts/abi/NexusBootstrapAbi.ts b/src/sdk/constants/abi/NexusBootstrapAbi.ts similarity index 100% rename from src/test/__contracts/abi/NexusBootstrapAbi.ts rename to src/sdk/constants/abi/NexusBootstrapAbi.ts diff --git a/src/sdk/constants/index.ts b/src/sdk/constants/index.ts index c561918e..4604b106 100644 --- a/src/sdk/constants/index.ts +++ b/src/sdk/constants/index.ts @@ -15,7 +15,7 @@ export const ENTRY_POINT_ADDRESS: Hex = export const ENTRYPOINT_SIMULATIONS_ADDRESS: Hex = "0x74Cb5e4eE81b86e70f9045036a1C5477de69eE87" export const NEXUS_BOOTSTRAP_ADDRESS: Hex = - "0x00000008c901d8871b6F6942De0B5D9cCf3873d3" + "0x000000F5b753Fdd20C5CA2D7c1210b3Ab1EA5903" export const TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS: Hex = "0x704C800D313c6B184228B5b733bBd6BC3EA9832c" @@ -28,6 +28,13 @@ export const MAINNET_ADDRESS_K1_VALIDATOR_ADDRESS: Hex = export const BICONOMY_ATTESTER_ADDRESS: Hex = "0xDE8FD2dBcC0CA847d11599AF5964fe2AEa153699" +export const NEXUS_ACCOUNT_FACTORY = + "0x000000226cada0d8b36034F5D5c06855F59F6F3A" +export const MEE_VALIDATOR_ADDRESS = + "0x068EA3E30788ABaFDC6fD0b38d20BD38a40a2B3D" +export const TEMP_MEE_ATTESTER_ADDR = + "0x000000333034E9f539ce08819E12c1b8Cb29084d" + // Rhinestone constants export { SMART_SESSIONS_ADDRESS, diff --git a/src/sdk/constants/tokens/__AUTO_GENERATED__.ts b/src/sdk/constants/tokens/__AUTO_GENERATED__.ts new file mode 100644 index 00000000..be82e6da --- /dev/null +++ b/src/sdk/constants/tokens/__AUTO_GENERATED__.ts @@ -0,0 +1,1254 @@ +// N.B. This file is auto-generated by the fetch:tokenMap.ts script. Do not edit it manually. +// Instead, edit the script and run it again, or hardcode your new tokens in the index file that imports this file + +import { erc20Abi } from "viem" +import { getMultichainContract } from "../../account/utils/getMultichainContract" + +export const mcUSDT = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xdAC17F958D2ee523a2206206994597C13D831ec7", 1], + ["0x919C1c267BC06a7039e03fcc2eF738525769109c", 2222], + ["0x48065fbBE25f71C9282ddf5e1cD6D6A887483D5e", 42220], + ["0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7", 43114] + ] +}) + +export const mcUSDC = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", 1], + ["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", 10], + ["0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", 137], + ["0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4", 324], + ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 8453], + ["0xaf88d065e77c8cC2239327C5EDb3A432268e5831", 42161], + ["0xcebA9300f2b948710d2653dD7B07f33A8B32118C", 42220], + ["0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", 43114], + ["0x036CbD53842c5426634e7929541eC2318f3dCF7e", 84532] + ] +}) + +export const mcTON = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x582d872A1B094FC48F5DE31D3B73F2D9bE47def1", 1], + ["0x76A797A59Ba2C17726896976B7B3747BfD1d220f", 56] + ] +}) + +export const mcLINK = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x514910771AF9Ca656af840dff83E8264EcF986CA", 1], + ["0x350a791Bfc2C21F9Ed5d10980Dad2e2638ffa7f6", 10], + ["0xF8A0BF9cF54Bb92F17374d9e9A321E6a111a51bD", 56], + ["0xE2e73A1c69ecF83F464EFCE6A5be353a37cA09b2", 100], + ["0x9e004545c59D359F6B7BFB06a26390b087717b42", 128], + ["0x53E0bca35eC356BD5ddDFebbD1Fc0fD03FaBad39", 137], + ["0xb3654dc3D10Ea7645f8319668E8F54d2574FBdC8", 250], + ["0xf390830DF829cf22c53c8840554B98eafC5dCBc2", 2001], + ["0x68Ca48cA2626c415A89756471D4ADe2CC9034008", 39797], + ["0xf97f4df75117a78c1A5a0DBb814Af92458539FB4", 42161], + ["0x5947BB275c521040051D82396192181b413227A3", 43114], + ["0x218532a12a389a4a92fC0C5Fb22901D1c19198aA", 1666600000] + ] +}) + +export const mcUNI = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984", 1], + ["0x6fd9d7AD17242c41f7131d257212c54A0e816691", 10], + ["0xBf5140A22578168FD562DCcF235E5D43A02ce9B1", 56], + ["0x4537e328Bf7e4eFA29D05CAeA260D7fE26af9D74", 100], + ["0x22C54cE8321A4015740eE1109D9cBc25815C46E6", 128], + ["0xb33EaAd8d922B1083446DC23f610c2567fB5180f", 137], + ["0x665B3A802979eC24e076c80025bFF33c18eB6007", 39797], + ["0xFa7F8980b0f1E64A2062791cc3b0871572f1F7f0", 42161], + ["0x8eBAf22B6F053dFFeaf46f4Dd9eFA95D89ba8580", 43114], + ["0x90D81749da8867962c760414C1C25ec926E889b6", 1666600000] + ] +}) + +export const mcBGB = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x54D2252757e1672EEaD234D27B1270728fF90581", 1], + ["0x55d1f1879969bdbB9960d269974564C58DBc3238", 2818] + ] +}) + +export const mcPEPE = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x6982508145454Ce325dDbE47a25d4ec3d2311933", 1], + ["0x25d887Ce7a35172C62FeBFD67a1856F20FaEbB00", 56], + ["0x25d887Ce7a35172C62FeBFD67a1856F20FaEbB00", 42161] + ] +}) + +export const mcWEETH = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee", 1], + ["0x35751007a407ca6FEFfE80b3cB397736D2cf4dbe", 42161] + ] +}) + +export const mcUSDS = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xdC035D45d973E3EC169d2276DDab16f1e407384F", 1], + ["0x820C137fa70C8691f0e44Dc420a5e53c168921Dc", 8453] + ] +}) + +export const mcAAVE = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9", 1], + ["0x76FB31fb4af56892A25e32cFC43De717950c9278", 10], + ["0xfb6115445Bff7b52FeB98650C87f44907E58f802", 56], + ["0x202b4936fE1a82A4965220860aE46d7d3939Bb25", 128], + ["0xD6DF932A45C0f255f85145f286eA0b292B21C90B", 137], + ["0x6a07A792ab2965C72a5B8088d3a069A7aC3a993B", 250], + ["0xA7F2f790355E0C32CAb03f92F6EB7f488E6F049a", 39797], + ["0xba5DdD1f9d7F570dc94a51479a000E3BCE967196", 42161], + ["0x63a72806098Bd3D9520cC43356dD78afe5D386D9", 43114], + ["0xcF323Aad9E522B93F11c352CaA519Ad0E14eB40F", 1666600000] + ] +}) + +export const mcMNT = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x3c3a81e81dc49A522A592e7622A7E711c06bf354", 1], + ["0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000", 5000] + ] +}) + +export const mcRENDER = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x6De037ef9aD2725EB40118Bb1702EBb27e4Aeb24", 1], + ["0x61299774020dA444Af134c82fa83E3810b309991", 137] + ] +}) + +export const mcPOL = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x455e53CBB86018Ac2B8092FdCd39d8444aFFC3F6", 1], + ["0x0000000000000000000000000000000000001010", 137] + ] +}) + +export const mcOM = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x3593D125a4f7849a1B059E64F4517A86Dd60c95d", 1], + ["0xF78D2e7936F5Fe18308A3B2951A93b6c4a41F5e2", 56], + ["0xC3Ec80343D2bae2F8E680FDADDe7C17E71E114ea", 137], + ["0x3992B27dA26848C2b19CeA6Fd25ad5568B68AB98", 8453] + ] +}) + +export const mcFET = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xaea46A60368A7bD060eec7DF8CBa43b7EF41Ad85", 1], + ["0x031b41e504677879370e9DBcF937283A8691Fa7f", 56] + ] +}) + +export const mcVIRTUAL = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x44ff8620b8cA30902395A7bD3F2407e1A091BF73", 1], + ["0x0b3e328455c4059EEb9e3f84b5543F74E24e7E1b", 8453] + ] +}) + +export const mcARB = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xB50721BCf8d664c30412Cfbc6cf7a15145234ad1", 1], + ["0x912CE59144191C1204E64559FE8253a0e49E6548", 42161], + ["0xf823C3cD3CeBE0a1fA952ba88Dc9EEf8e0Bf46AD", 42170] + ] +}) + +export const mcOKB = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x75231F58b43240C9718Dd58B4967c5114342a86c", 1], + ["0xdF54B6c6195EA4d948D03bfD818D365cf175cFC2", 66] + ] +}) + +export const mcBONK = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x1151CB3d861920e07a38e03eEAd12C32178567F6", 1], + ["0xA697e272a73744b343528C3Bc4702F2565b2F422", 56], + ["0xe5B49820e5A1063F6F4DdF851327b5E8B2301048", 137], + ["0x09199d9A5F4448D0848e4395D065e1ad9c4a1F74", 42161], + ["0xD4B6520f7Fb78E1EE75639F3376c581a71bcdb0E", 245022934] + ] +}) + +export const mcINJ = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xe28b3B32B6c345A34Ff64674606124Dd5Aceca30", 1], + ["0xa2B726B1145A4773F68593CF171187d8EBe4d495", 56] + ] +}) + +export const mcCBBTC = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf", 1], + ["0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf", 8453], + ["0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf", 42161] + ] +}) + +export const mcGRT = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xc944E90C64B2c07662A292be6244BDf05Cda44a7", 1], + ["0x5fe2B58c013d7601147DcdD68C143A77499f5531", 137], + ["0x771513bA693D457Df3678c951c448701f2eAAad5", 39797], + ["0x9623063377AD1B27544C965cCd7342f7EA7e88C7", 42161], + ["0x8a0cAc13c7da965a312f08ea4229c37869e85cB9", 43114], + ["0x002FA662F2E09de7C306d2BaB0085EE9509488Ff", 1666600000] + ] +}) + +export const mcWLD = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x163f8C2467924be0ae7B5347228CABF260318753", 1], + ["0xdC6fF44d5d932Cbd77B52E5612Ba0529DC6226F1", 10], + ["0x2cFc85d8E48F8EAB294be644d9E25C3030863003", 480] + ] +}) + +export const mcUSD0 = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x73A15FeD60Bf67631dC6cd7Bc5B6e8da8190aCF5", 1], + ["0x35f1C5cB7Fb977E669fD244C567Da99d8a3a6850", 42161] + ] +}) + +export const mcFDUSD = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xc5f0f7b66764F6ec8C8Dff7BA683102295E16409", 1], + ["0xc5f0f7b66764F6ec8C8Dff7BA683102295E16409", 56] + ] +}) + +export const mcRETH = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xae78736Cd615f374D3085123A210448E74Fc6393", 1], + ["0x9Bcef72be871e61ED4fBbc7630889beE758eb81D", 10], + ["0x0266F4F08D82372CF0FcbCCc0Ff74309089c74d1", 137], + ["0xB6fe221Fe9EeF5aBa221c348bA20A1Bf5e73624c", 8453], + ["0xEC70Dcb4A1EFa46b8F2D97C310C9c4790ba5ffA8", 42161] + ] +}) + +export const mcFLOKI = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xcf0C122c6b73ff809C693DB761e7BaeBe62b6a2E", 1], + ["0xfb5B838b6cfEEdC2873aB27866079AC55363D37E", 56] + ] +}) + +export const mcLBTC = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x8236a87084f8B84306f72007F36F2618A5634494", 1], + ["0xecAc9C5F704e954931349Da37F60E39f515c11c1", 56], + ["0xecAc9C5F704e954931349Da37F60E39f515c11c1", 8453] + ] +}) + +export const mcLDO = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32", 1], + ["0xFdb794692724153d1488CcdBE0C56c252596735F", 10], + ["0xC3C7d422809852031b44ab29EEC9F1EfF2A58756", 137], + ["0x13Ad51ed4F1B7e9Dc168d8a00cB3f4dDD85EfA60", 42161] + ] +}) + +export const mcMETH = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xd5F7838F5C461fefF7FE49ea5ebaF7728bB0ADfa", 1], + ["0xcDA86A272531e8640cD7F1a92c01839911B90bb0", 5000] + ] +}) + +export const mcQNT = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x4a220E6096B25EADb88358cb44068A3248254675", 1], + ["0x462B35452E552A66B519EcF70aEdb1835d434965", 39797] + ] +}) + +export const mcSAND = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x3845badAde8e6dFF049820680d1F14bD3903a5d0", 1], + ["0xBbba073C31bF03b8ACf7c28EF0738DeCF3695683", 137], + ["0x73a4AC88c12D66AD08c1cfC891bF47883919ba74", 39797], + ["0x35de8649e1e4Fd1A7Bd3B14F7e24e5e7887174Ed", 1666600000] + ] +}) + +export const mcSPX = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xE0f63A424a4439cBE457D80E4f4b51aD25b2c56C", 1], + ["0x50dA645f148798F68EF2d7dB7C1CB22A6819bb2C", 8453] + ] +}) + +export const mcNEXO = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xB62132e35a6c13ee1EE0f84dC5d40bad8d815206", 1], + ["0x41b3966B4FF7b427969ddf5da3627d6AEAE9a48E", 137], + ["0x7C598c96D02398d89FbCb9d41Eab3DF0C16F227D", 250], + ["0x04640Dc771eDd73cbeB934FB5461674830BAea11", 39797] + ] +}) + +export const mcSOLVBTC = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x7A56E1C57C7475CCf742a1832B028F0456652F97", 1], + ["0x4aae823a6a0b376De6A78e74eCC5b079d38cBCf7", 56], + ["0x41D9036454BE47d3745A823C4aaCD0e29cFB0f71", 4200], + ["0xa68d25fC2AF7278db4BcdcAabce31814252642a9", 5000], + ["0x3B86Ad95859b6AB773f55f8d94B4b9d443EE931f", 8453], + ["0x3647c54c4c2C65bC7a2D63c0Da2809B399DBBDC0", 42161], + ["0xbc78D84Ba0c46dFe32cf2895a19939c86b81a777", 43114], + ["0xbEAf16cFD8eFe0FC97C2a07E349B9411F5dC272C", 810180] + ] +}) + +export const mcMKR = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2", 1], + ["0x6f7C932e7684666C9fd1d44527765433e01fF61d", 137], + ["0x050317d93f29D1bA5FF3EaC3b8157fD4E345588D", 39797], + ["0x88128fd4b259552A9A1D457f435a6527AAb72d42", 43114] + ] +}) + +export const mcBEAM = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x62D0A8458eD7719FDAF978fe5929C6D342B0bFcE", 1], + ["0x62D0A8458eD7719FDAF978fe5929C6D342B0bFcE", 56], + ["0x62D0A8458eD7719FDAF978fe5929C6D342B0bFcE", 43114] + ] +}) + +export const mcAIOZ = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x626E8036dEB333b408Be468F951bdB42433cBF18", 1], + ["0x33d08D8C7a168333a85285a68C0042b39fC3741D", 56] + ] +}) + +export const mcBTT = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xC669928185DbCE49d2230CC9B0979BE6DC797957", 1], + ["0x352Cb5E19b12FC216548a2677bD0fce83BaE434B", 56], + ["0x0000000000000000000000000000000000001010", 199], + ["0xF1BdCF2D4163adF9554111439dAbdD6f18fF9BA7", 39797] + ] +}) + +export const mcCRV = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xD533a949740bb3306d119CC777fa900bA034cd52", 1], + ["0x0994206dfE8De6Ec6920FF4D779B0d950605Fb53", 10], + ["0x172370d5Cd63279eFa6d502DAB29171933a610AF", 137], + ["0x1E4F97b9f9F913c46F1632781732927B9019C68b", 250], + ["0xd3319EAF3c4743ac75AaCE77befCFA445Ed6E69E", 39797], + ["0x11cDb42B0EB46D95f990BeDD4695A6e3fA034978", 42161] + ] +}) + +export const mcSUSDS = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xa3931d71877C0E7a3148CB7Eb4463524FEc27fbD", 1], + ["0x5875eEE11Cf8398102FdAd704C9E96607675467a", 8453] + ] +}) + +export const mcAXS = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xBB0E17EF65F82Ab018d8EDd776e8DD940327B28b", 1], + ["0x715D400F88C167884bbCc41C5FeA407ed4D2f8A0", 56], + ["0x97a9107C1793BC407d6F527b77e7fff4D812bece", 2020], + ["0x7CD3D51beE45434Dd80822c5D58b999333b69FfB", 39797], + ["0x14A7B318fED66FfDcc80C1517C172c13852865De", 1666600000] + ] +}) + +export const mcSOLVBTC_BBN = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xd9D920AA40f578ab794426F5C90F6C731D159DEf", 1], + ["0x1346b618dC92810EC74163e4c27004c921D446a5", 56], + ["0x1760900aCA15B90Fa2ECa70CE4b4EC441c2CF6c5", 4200], + ["0x1d40baFC49c37CdA49F2a5427E2FB95E1e3FCf20", 5000], + ["0xC26C9099BD3789107888c35bb41178079B282561", 8453], + ["0x346c574C56e1A4aAa8dc88Cda8F7EB12b39947aB", 42161], + ["0xCC0966D8418d412c599A6421b760a847eB169A8c", 43114] + ] +}) + +export const mcMANA = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x0F5D2fB29fb7d3CFeE444a200298f468908cC942", 1], + ["0xA1c57f48F0Deb89f569dFbE6E2B7f46D33606fD4", 137] + ] +}) + +export const mcDEXE = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xde4EE8057785A7e8e800Db58F9784845A5C2Cbd6", 1], + ["0x6E88056E8376Ae7709496Ba64d37fa2f8015ce3e", 56] + ] +}) + +export const mcMATIC = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0", 1], + ["0xCC42724C6683B7E57334c4E856f4c9965ED682bD", 56], + ["0x3405A1bd46B85c5C029483FbECf2F3E611026e45", 1284], + ["0x682F81e57EAa716504090C3ECBa8595fB54561D8", 1285], + ["0x98997E1651919fAeacEe7B96aFbB3DfD96cb6036", 39797], + ["0x301259f392B551CA8c592C9f676FCD2f9A0A84C5", 1666600000] + ] +}) + +export const mcMOG = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xaaeE1A9723aaDB7afA2810263653A34bA2C21C7a", 1], + ["0x2Da56AcB9Ea78330f947bD57C54119Debda7AF71", 8453] + ] +}) + +export const mcAPE = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x4d224452801ACEd8B2F0aebE155379bb5D594381", 1], + ["0xB7b31a6BC18e48888545CE79e83E06003bE70930", 137] + ] +}) + +export const mcRSR = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x320623b8E4fF03373931769A31Fc52A4E78B5d70", 1], + ["0xaB36452DbAC151bE02b16Ca17d8919826072f64a", 8453], + ["0xfcE13BB63B60f6e20ed846ae73ed242D29129800", 39797], + ["0xCa5Ca9083702c56b481D1eec86F1776FDbd2e594", 42161] + ] +}) + +export const mcUSDD = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x0C10bF8FcB7Bf5412187A595ab97a3609160b5c6", 1], + ["0xd17479997F34dd9156Deef8F95A52D81D265be9c", 56], + ["0x17F235FD5974318E4E2a5e37919a209f7c37A6d1", 199], + ["0x680447595e8b7b3Aa1B43beB9f6098C79ac2Ab3f", 42161], + ["0xB514CABD09eF5B169eD3fe0FA8DBd590741E81C2", 43114] + ] +}) + +export const mcPRIME = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xb23d80f5FefcDDaa212212F028021B41DEd428CF", 1], + ["0xfA980cEd6895AC314E7dE34Ef1bFAE90a5AdD21b", 8453] + ] +}) + +export const mcW = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xB0fFa8000886e57F86dd5264b9582b2Ad87b2b91", 1], + ["0xB0fFa8000886e57F86dd5264b9582b2Ad87b2b91", 8453], + ["0xB0fFa8000886e57F86dd5264b9582b2Ad87b2b91", 42161] + ] +}) + +export const mcPENDLE = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x808507121B80c02388fAd14726482e061B8da827", 1], + ["0xBC7B1Ff1c6989f006a1185318eD4E7b5796e66E1", 10], + ["0xb3Ed0A426155B79B898849803E3B36552f7ED507", 56], + ["0xA99F6e6785Da0F5d6fB42495Fe424BCE029Eeb3E", 8453], + ["0x0c880f6761F1af8d9Aa9C466984b80DAb9a8c9e8", 42161] + ] +}) + +export const mcCAKE = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x152649eA73beAb28c5b49B26eb48f7EAD6d4c898", 1], + ["0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82", 56], + ["0x2779106e4F4A8A28d77A24c18283651a2AE22D1C", 204], + ["0x3A287a06c66f9E95a56327185cA2BDF5f031cEcD", 324], + ["0x0D1E753a25eBda689453309112904807625bEFBe", 1101], + ["0x3055913c90Fcc1A6CE9a358911721eEb942013A1", 8453], + ["0x1b896893dfc86bb67Cf57767298b9073D2c1bA2c", 42161], + ["0x0D1E753a25eBda689453309112904807625bEFBe", 59144] + ] +}) + +export const mcGNO = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x6810e776880C02933D47DB1b9fc05908e5386b96", 1], + ["0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb", 100], + ["0xF452bff8e958C6F335F06fC3aAc427Ee195366fE", 39797], + ["0xa0b862F60edEf4452F25B4160F177db44DeB6Cf1", 42161] + ] +}) + +export const mcCMETH = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xE6829d9a7eE3040e1276Fa75293Bde931859e8fA", 1], + ["0xE6829d9a7eE3040e1276Fa75293Bde931859e8fA", 5000] + ] +}) + +export const mcFRAX = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x853d955aCEf822Db058eb8505911ED77F175b99e", 1], + ["0x2E3D870790dC77A83DD1d18184Acc7439A53f475", 10], + ["0x90C97F71E18723b0Cf0dfa30ee176Ab653E89F40", 56], + ["0x45c32fA6DF82ead1e2EF74d17b76547EDdFaFF89", 137], + ["0xdc301622e621166BD8E82f2cA0A26c13Ad0BE355", 250], + ["0x7562F525106F5d54E891e005867Bf489B5988CD9", 288], + ["0xFf8544feD5379D9ffa8D47a74cE6b91e632AC44D", 1101], + ["0x322E86852e492a7Ee17f28a78c663da38FB33bfb", 1284], + ["0x1A93B23281CC1CDE4C4741353F3064709A16197d", 1285], + ["0x80Eede496655FB9047dd39d9f418d5483ED600df", 1329], + ["0xE03494D0033687543a80c9B1ca7D6237F2EA8BD8", 9001], + ["0x17FC002b466eEc40DaE837Fc4bE5c67993ddBd6F", 42161], + ["0xD24C2Ad096400B6FBcd2ad8B24E7acBc21A1da64", 43114], + ["0xE4B9e004389d91e4134a28F19BD833cBA1d994B6", 1313161554], + ["0xFa7191D292d5633f702B0bd7E3E3BcCC0e633200", 1666600000] + ] +}) + +export const mcCOMP = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xc00e94Cb662C3520282E6f5717214004A7f26888", 1], + ["0x52CE071Bd9b1C4B00A0b92D298c512478CaD67e8", 56], + ["0x8505b9d2254A7Ae468c0E9dd10Ccea3A837aef5c", 137], + ["0x9e1028F5F1D5eDE59748FFceE5532509976840E0", 8453], + ["0x66bC411714e16B6F0C68be12bD9c666cc4576063", 39797], + ["0x354A6dA3fcde098F8389cad84b0182725c6C91dE", 42161], + ["0xc3048E19E76CB9a3Aa9d77D8C03c29Fc906e2437", 43114], + ["0x32137b9275EA35162812883582623cd6f6950958", 1666600000] + ] +}) + +export const mcSNX = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F", 1], + ["0x8700dAec35aF8Ff88c16BdF0418774CB3D7599B4", 10], + ["0x777850281719d5a96C29812ab72f822E0e09F3Da", 128], + ["0x50B728D8D964fd00C2d0AAD81718b71311feF68a", 137], + ["0x56ee926bD8c72B2d5fa1aF4d9E4Cbb515a1E3Adc", 250], + ["0x22e6966B799c4D5B13BE962E1D117b56327FDa66", 8453], + ["0xa255461fF545d6ecE153283f421D67D2DE5D0E29", 39797], + ["0xBeC243C995409E6520D7C41E404da5dEba4b209B", 43114], + ["0x7b9c523d59AeFd362247Bd5601A89722e3774dD2", 1666600000] + ] +}) + +export const mcAMP = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xfF20817765cB7f73d4bde2e66e067E58D11095C2", 1], + ["0xAD7ABE6f12F1059bDf48aE67bfF92B00438ceD95", 39797] + ] +}) + +export const mcUSDX = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xf3527ef8dE265eAa3716FB312c12847bFBA66Cef", 1], + ["0xf3527ef8dE265eAa3716FB312c12847bFBA66Cef", 56], + ["0xf3527ef8dE265eAa3716FB312c12847bFBA66Cef", 42161] + ] +}) + +export const mcSUPER = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xe53EC727dbDEB9E2d5456c3be40cFF031AB40A55", 1], + ["0xa1428174F516F527fafdD146b883bB4428682737", 137] + ] +}) + +export const mcAXL = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x467719aD09025FcC6cF6F8311755809d45a5E5f3", 1], + ["0x23ee2343B892b1BB63503a4FAbc840E0e2C6810f", 10], + ["0x8b1f4432F943c465A973FeDC6d7aa50Fc96f1f65", 56], + ["0x6e4E624106Cb12E168E6533F8ec7c82263358940", 137], + ["0x8b1f4432F943c465A973FeDC6d7aa50Fc96f1f65", 250], + ["0x467719aD09025FcC6cF6F8311755809d45a5E5f3", 1284], + ["0x23ee2343B892b1BB63503a4FAbc840E0e2C6810f", 8453], + ["0x23ee2343B892b1BB63503a4FAbc840E0e2C6810f", 42161], + ["0x44c784266cf024a60e8acF2427b9857Ace194C5d", 43114] + ] +}) + +export const mcCBETH = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xBe9895146f7AF43049ca1c1AE358B0541Ea49704", 1], + ["0xadDb6A0412DE1BA0F936DCaeb8Aaa24578dcF3B2", 10], + ["0x4b4327dB1600B8B1440163F667e199CEf35385f5", 137], + ["0x2Ae3F1Ec7F1F5012CFEab0185bfc7aa3cf0DEc22", 8453], + ["0x1DEBd73E752bEaF79865Fd6446b0c970EaE7732f", 42161] + ] +}) + +export const mcZRO = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x6985884C4392D348587B19cb9eAAf157F13271cd", 1], + ["0x6985884C4392D348587B19cb9eAAf157F13271cd", 10], + ["0x6985884C4392D348587B19cb9eAAf157F13271cd", 56], + ["0x6985884C4392D348587B19cb9eAAf157F13271cd", 137], + ["0x6985884C4392D348587B19cb9eAAf157F13271cd", 8453], + ["0x6985884C4392D348587B19cb9eAAf157F13271cd", 42161], + ["0x6985884C4392D348587B19cb9eAAf157F13271cd", 43114] + ] +}) + +export const mc_1INCH = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x111111111117dC0aa78b770fA6A738034120C302", 1], + ["0x111111111117dC0aa78b770fA6A738034120C302", 56], + ["0x9c2C5fd7b07E95EE044DDeba0E97a665F142394f", 137], + ["0xDDa6205Dc3f47e5280Eb726613B27374Eee9D130", 39797], + ["0xd501281565bf7789224523144Fe5D98e8B28f267", 43114], + ["0x58f1b044d8308812881a1433d9Bbeff99975e70C", 1666600000] + ] +}) + +export const mcMORPHO = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x58D97B57BB95320F9a05dC918Aef65434969c2B2", 1], + ["0xBAa5CC21fd487B8Fcc2F632f3F4E8D37262a0842", 8453] + ] +}) + +export const mcLPT = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x58b6A8A3302369DAEc383334672404Ee733aB239", 1], + ["0x289ba1701C2F088cf0faf8B3705246331cB8A839", 42161], + ["0xBD3E698b51D340Cc53B0CC549b598c13e0172B7c", 1666600000] + ] +}) + +export const mcNFT = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x198d14F2Ad9CE69E76ea330B374DE4957C3F850a", 1], + ["0x20eE7B720f4E4c4FFcB00C4065cdae55271aECCa", 56] + ] +}) + +export const mcSAFE = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x5aFE3855358E112B5647B952709E6165e1c1eEEe", 1], + ["0x4d18815D14fe5c3304e87B3FA18318baa5c23820", 100] + ] +}) + +export const mcPUMPBTC = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xF469fBD2abcd6B9de8E169d128226C0Fc90a012e", 1], + ["0xf9C4FF105803A77eCB5DAE300871Ad76c2794fa4", 56] + ] +}) + +export const mcTUSD = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x0000000000085d4780B73119b644AE5ecd22b376", 1], + ["0x40af3827F39D0EAcBF4A168f8D4ee67c121D11c9", 56], + ["0x1C20E891Bab6b1727d14Da358FAe2984Ed9B59EB", 43114] + ] +}) + +export const mcFRXETH = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x5E8422345238F34275888049021821E8E08CAa1f", 1], + ["0x6806411765Af15Bddd26f8f544A34cC40cb9838B", 10], + ["0x64048A7eEcF3a2F1BA9e144aAc3D7dB6e58F555e", 56], + ["0xEe327F889d5947c1dc1934Bb208a1E792F953E96", 137], + ["0x9E73F99EE061C8807F69f9c6CCc44ea3d8c373ee", 250], + ["0xCf7eceE185f19e2E970a301eE37F93536ed66179", 1101], + ["0x82bbd1b6f6De2B7bb63D3e1546e6b1553508BE99", 1284], + ["0x178412e79c25968a32e89b11f63B33F733770c2A", 42161] + ] +}) + +export const mcBABYDOGE = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xAC57De9C1A09FeC648E93EB98875B212DB0d460B", 1], + ["0xc748673057861a797275CD8A068AbB95A902e8de", 56] + ] +}) + +export const mcUSDY = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x96F6eF951840721AdBF46Ac996b59E0235CB985C", 1], + ["0x5bE26527e817998A7206475496fDE1E68957c5A6", 5000], + ["0x35e050d3C0eC2d29D269a8EcEa763a183bDF9A9D", 42161] + ] +}) + +export const mcSWETH = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xf951E335afb289353dc249e82926178EaC7DEd78", 1], + ["0xbc011A12Da28e8F0f528d9eE5E7039E22F91cf18", 42161] + ] +}) + +export const mcETHFI = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xFe0c30065B384F05761f15d0CC899D4F9F9Cc0eB", 1], + ["0x7189fb5B6504bbfF6a852B13B7B82a3c118fDc27", 42161] + ] +}) + +export const mcTBTC = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x18084fbA666a33d37592fA2633fD49a74DD93a88", 1], + ["0x6c84a8f1c29108F47a79964b5Fe888D4f4D0dE40", 10], + ["0x236aa50979D5f3De3Bd1Eeb40E81137F22ab794b", 137], + ["0x236aa50979D5f3De3Bd1Eeb40E81137F22ab794b", 8453], + ["0x6c84a8f1c29108F47a79964b5Fe888D4f4D0dE40", 42161] + ] +}) + +export const mcTEL = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x467Bccd9d29f223BcE8043b84E8C8B282827790F", 1], + ["0xdF7837DE1F2Fa4631D716CF2502f8b230F1dcc32", 137] + ] +}) + +export const mcZRX = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xE41d2489571d322189246DaFA5ebDe1F4699F498", 1], + ["0x591C19DC0821704BEDAA5Bbc6A66fee277d9437e", 39797], + ["0x596fA47043f99A4e0F122243B841E55375cdE0d2", 43114], + ["0x8143E2A1085939cAA9cEf6665c2Ff32f7bc08435", 1666600000] + ] +}) + +export const mcCHEX = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x9Ce84F6A69986a83d92C324df10bC8E64771030f", 1], + ["0x9Ce84F6A69986a83d92C324df10bC8E64771030f", 56], + ["0xc43F3Ae305a92043bd9b62eBd2FE14F7547ee485", 8453] + ] +}) + +export const mcHOT = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x6c6EE5e31d828De241282B9606C8e98Ea48526E2", 1], + ["0x34b97EEaB6FD9BBe95A5eAF4645307c5a6f3D4d0", 39797], + ["0x5dfEaDCDD2d4eB29aC5Ae876dAA07FfD07Bf6483", 1666600000] + ] +}) + +export const mcANKR = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x8290333ceF9e6D528dD5618Fb97a76f268f3EDD4", 1], + ["0xAeAeeD23478c3a4b798e4ed40D8B7F41366Ae861", 10], + ["0xf307910A4c7bbc79691fD374889b36d8531B08e3", 56], + ["0x101A023270368c0D50BFfb62780F4aFd4ea79C35", 137], + ["0xDF474B7109b73b7D57926d43598D5934131136b2", 250], + ["0xDF474B7109b73b7D57926d43598D5934131136b2", 1101], + ["0xDF474B7109b73b7D57926d43598D5934131136b2", 34443], + ["0xAeAeeD23478c3a4b798e4ed40D8B7F41366Ae861", 42161], + ["0xDF474B7109b73b7D57926d43598D5934131136b2", 43114], + ["0xa8Ae6365383eb907e6b4B1B7E82A35752cC5Ef8C", 59144], + ["0x3580ac35BED2981d6bDD671a5982c2467d301241", 81457], + ["0xDF474B7109b73b7D57926d43598D5934131136b2", 534352] + ] +}) + +export const mcZETA = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xf091867EC603A6628eD83D274E835539D82e9cc8", 1], + ["0x0000028a2eB8346cd5c0267856aB7594B7a55308", 56] + ] +}) + +export const mcELF = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xbf2179859fc6D5BEE9Bf9158632Dc51678a4100e", 1], + ["0xa3f020a5C92e15be13CAF0Ee5C95cF79585EeCC9", 56] + ] +}) + +export const mcWOO = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x4691937a7508860F876c9c0a2a617E7d9E945D4B", 1], + ["0x4691937a7508860F876c9c0a2a617E7d9E945D4B", 56], + ["0x1B815d120B3eF02039Ee11dC2d33DE7aA4a8C603", 137], + ["0x6626c47c00F1D87902fc13EECfaC3ed06D5E8D8a", 250], + ["0x9E22D758629761FC5708c171d06c2faBB60B5159", 324], + ["0xF3df0A31ec5EA438150987805e841F960b9471b6", 5000], + ["0xF3df0A31ec5EA438150987805e841F960b9471b6", 8453], + ["0xcAFcD85D8ca7Ad1e1C6F82F651fA15E33AEfD07b", 42161], + ["0xaBC9547B534519fF73921b1FBA6E672b5f58D083", 43114], + ["0xF3df0A31ec5EA438150987805e841F960b9471b6", 59144] + ] +}) + +export const mcENJ = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xF629cBd94d3791C9250152BD8dfBDF380E2a3B9c", 1], + ["0x204a90B57d15417864080df1Cd6e907831c206A6", 39797], + ["0xadbd41bFb4389dE499535C14A8a3A12Fead8F66A", 1666600000] + ] +}) + +export const mcATH = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xbe0Ed4138121EcFC5c0E56B40517da27E6c5226B", 1], + ["0xc87B37a581ec3257B734886d9d3a581F5A9d056c", 42161] + ] +}) + +export const mcGLM = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x7DD9c5Cba05E151C895FDe1CF355C9A1D5DA6429", 1], + ["0xf3ff3bF1d1afCbeBD98A304482c4099Dc953E9a8", 39797] + ] +}) + +export const mcVANRY = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x8DE5B80a0C1B02Fe4976851D030B36122dbb8624", 1], + ["0x8DE5B80a0C1B02Fe4976851D030B36122dbb8624", 137] + ] +}) + +export const mcGMT = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xe3c408BD53c31C085a1746AF401A4042954ff740", 1], + ["0x3019BF2a2eF8040C242C9a4c5c4BD4C81678b2A1", 56], + ["0x714DB550b574b3E927af3D93E26127D15721D4C2", 137] + ] +}) + +export const mcBAT = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x0D8775F648430679A709E98d2b0Cb6250d2887EF", 1], + ["0x3Cef98bb43d732E2F285eE605a8158cDE967D219", 137], + ["0xe8Ba8D7765bD33BA7Ff3B19b9020C15BF14123B6", 39797], + ["0x98443B96EA4b0858FDF3219Cd13e98C7A4690588", 43114], + ["0x2875B4CfAb0A4cc4bdc7fBDf94b6E376826A4332", 1666600000] + ] +}) + +export const mcMX = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x11eeF04c884E24d9B7B4760e7476D06ddF797f36", 1], + ["0x0BEeF4B01281D85492713a015d51fEc5b6D14687", 2818] + ] +}) + +export const mcSFRXETH = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xac3E018457B222d93114458476f3E3416Abbe38F", 1], + ["0x484c2D6e3cDd945a8B2DF735e079178C1036578c", 10], + ["0x3Cd55356433C89E50DC51aB07EE0fa0A95623D53", 56], + ["0x6d1FdBB266fCc09A16a22016369210A15bb95761", 137], + ["0xb90CCD563918fF900928dc529aA01046795ccb4A", 250], + ["0xecf91116348aF1cfFe335e9807f0051332BE128D", 1284], + ["0x95aB45875cFFdba1E5f451B950bC2E42c0053f39", 42161] + ] +}) + +export const mcIOTX = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x6fB3e0A217407EFFf7Ca062D46c26E5d60a14d69", 1], + ["0xBCBAf311ceC8a4EAC0430193A528d9FF27ae38C1", 8453] + ] +}) + +export const mcSFP = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x12e2b8033420270db2F3b328E32370Cb5B2Ca134", 1], + ["0xD41FDb03Ba84762dD66a0af1a6C8540FF1ba5dfb", 56], + ["0x12490d720747E312bE64029Dfd475837Ed285cFe", 39797] + ] +}) + +export const mcOHM = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x64aa3364F17a4D01c6f1751Fd97C2BD3D7e7f1D5", 1], + ["0x060cb087a9730E13aa191f31A6d86bFF8DfcdCC0", 8453], + ["0xf0cb2dc0db5e6c66B9a70Ac27B06b878da017028", 42161] + ] +}) + +export const mcBORG = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x64d0f55Cd8C7133a9D7102b13987235F486F2224", 1], + ["0x5666444647f4fD66DECF411D69f994B8244EbeE3", 39797] + ] +}) + +export const mcSUSHI = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x6B3595068778DD592e39A122f4f5a5cF09C90fE2", 1], + ["0x947950BcC74888a40Ffa2593C5798F11Fc9124C4", 56], + ["0x0b3F868E0BE5597D5DB7fEB59E1CADBb0fdDa50a", 137], + ["0xae75A438b2E0cB8Bb01Ec1E1e376De11D44477CC", 250], + ["0x7D49a065D17d6d4a55dc13649901fdBB98B2AFBA", 8453], + ["0x32Aff6ADC46331dAc93E608A9CD4b0332d93a23a", 39797], + ["0xd4d42F0b6DEF4CE0383636770eF773390d85c61A", 42161], + ["0xD15EC721C2A896512Ad29C671997DD68f9593226", 42220], + ["0x37B608519F91f70F2EeB0e5Ed9AF4061722e4F76", 43114], + ["0xBEC775Cb42AbFa4288dE81F387a9b1A3c4Bc552A", 1666600000] + ] +}) + +export const mcFXS = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x3432B6A60D23Ca0dFCa7761B7ab56459D9C964D0", 1], + ["0xe48A3d7d0Bc88d552f730B62c006bC925eadB9eE", 56], + ["0x1a3acf6D19267E2d3e7f898f42803e90C9219062", 137], + ["0x7d016eec9c25232b01F23EF992D98ca97fc2AF5a", 250], + ["0x6b856a14CeA1d7dCfaF80fA6936c0b75972cCacE", 1101], + ["0x6f1D1Ee50846Fcbc3de91723E61cb68CFa6D0E98", 1285], + ["0xd8176865DD0D672c6Ab4A427572f80A72b4B4A9C", 9001], + ["0x9d2F299715D94d8A7E6F5eaa8E654E8c74a988A7", 42161], + ["0x214DB107654fF987AD859F34125307783fC8e387", 43114], + ["0x0767D8E1b05eFA8d6A301a65b324B6b66A1CC14c", 1666600000] + ] +}) + +export const mcMASK = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x69af81e73A73B40adF4f3d4223Cd9b1ECE623074", 1], + ["0x2eD9a5C8C13b93955103B9a7C167B67Ef4d568a3", 56], + ["0x2B9E7ccDF0F4e5B24757c1E1a80e311E34Cb10c7", 137], + ["0x746514E2c7D91E1e84C20c54d1F6F537b28A7d8e", 39797] + ] +}) + +export const mcYFI = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e", 1], + ["0x9046D36440290FfDE54FE0DD84Db8b1CfEE9107B", 10], + ["0xbf65bfcb5da067446CeE6A706ba3Fe2fB1a9fdFd", 100], + ["0xB4F019bEAc758AbBEe2F906033AAa2f0F6Dacb35", 128], + ["0xDA537104D6A5edd53c6fBba9A898708E465260b6", 137], + ["0x29b0Da86e484E1C0029B56e817912d778aC0EC69", 250], + ["0x9EaF8C1E34F05a589EDa6BAfdF391Cf6Ad3CB239", 8453], + ["0x2726Dd5efb3A209a54C512e9562A2045B8F45DBc", 39797], + ["0x82e3A8F066a6989666b031d916c43672085b1582", 42161], + ["0x9eAaC1B23d935365bD7b542Fe22cEEe2922f52dc", 43114], + ["0xa0dc05F84A27FcCBD341305839019aB86576bc07", 1666600000] + ] +}) + +export const mcILV = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x767FE9EDC9E0dF98E07454847909b5E959D7ca0E", 1], + ["0xA4ECF6D10B8D61D4A022821A6FF8b9536a47c70D", 39797] + ] +}) + +export const mcBICO = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xF17e65822b568B3903685a7c9F496CF7656Cc6C2", 1], + ["0xa68Ec98D7ca870cF1Dd0b00EBbb7c4bF60A8e74d", 42161] + ] +}) + +export const mcGAL = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x5fAa989Af96Af85384b8a938c2EdE4A7378D9875", 1], + ["0xe4Cc45Bb5DBDA06dB6183E8bf016569f40497Aa5", 56] + ] +}) + +export const mcWBETH = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xa2E3356610840701BDf5611a53974510Ae27E2e1", 1], + ["0xa2E3356610840701BDf5611a53974510Ae27E2e1", 56] + ] +}) + +export const mcALT = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x8457CA5040ad67fdebbCC8EdCE889A335Bc0fbFB", 1], + ["0x8457CA5040ad67fdebbCC8EdCE889A335Bc0fbFB", 56] + ] +}) + +export const mcMETIS = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x9E32b13ce7f2E80A01932B42553652E053D6ed8e", 1], + ["0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000", 1088] + ] +}) + +export const mcLRC = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xBBbbCA6A901c926F240b89EacB641d8Aec7AEafD", 1], + ["0x193Da10f8A969D4C081b9097B15337b1488CBbEC", 39797], + ["0x46d0cE7de6247b0A95f67b43B589b4041BaE7fbE", 42161] + ] +}) + +export const mcSKL = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x00c83aeCC790e8a4453e5dD3B0B4b3680501a7A7", 1], + ["0xE0595a049d02b7674572b0d59cd4880Db60EDC50", 2046399126] + ] +}) + +export const mcCORGIAI = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x6b431B8a964BFcf28191b07c91189fF4403957D0", 1], + ["0x6b431B8a964BFcf28191b07c91189fF4403957D0", 25] + ] +}) + +export const mcG = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x9C7BEBa8F6eF6643aBd725e45a4E8387eF260649", 1], + ["0x9C7BEBa8F6eF6643aBd725e45a4E8387eF260649", 56], + ["0x9C7BEBa8F6eF6643aBd725e45a4E8387eF260649", 8453] + ] +}) + +export const mcCOW = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB", 1], + ["0x177127622c4A00F3d409B75571e12cB3c8973d3c", 100], + ["0xcb8b5CD20BdCaea9a010aC1F8d835824F5C87A04", 42161] + ] +}) + +export const mcRPL = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xD33526068D116cE69F19A9ee46F0bd304F21A51f", 1], + ["0x7205705771547cF79201111B4bd8aaF29467b9eC", 137], + ["0xB766039cc6DB368759C1E56B79AFfE831d0Cc507", 42161] + ] +}) + +export const mcUSDA = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x8A60E489004Ca22d775C5F2c657598278d17D9c2", 1], + ["0x9356086146be5158E98aD827E21b5cF944699894", 56], + ["0x075df695b8E7f4361FA7F8c1426C63f11B06e326", 5000] + ] +}) + +export const mcAVAIL = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xEeB4d8400AEefafC1B2953e0094134A887C76Bd8", 1], + ["0xd89d90d26B48940FA8F58385Fe84625d468E057a", 8453] + ] +}) + +export const mcFLUID = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x6f40d4A6237C257fff2dB00FA0510DeEECd303eb", 1], + ["0xf50D05A1402d0adAfA880D36050736f9f6ee7dee", 137] + ] +}) + +export const mcUMA = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828", 1], + ["0x3Bd2B1c7ED8D396dbb98DED3aEbb41350a5b2339", 43114] + ] +}) + +export const mcACX = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x44108f0223A3C3028F5Fe7AEC7f9bb2E66beF82F", 1], + ["0xFf733b2A3557a7ed6697007ab5D11B79FdD1b76B", 10], + ["0xF328b73B6c685831F238c30a23Fc19140CB4D8FC", 137], + ["0x96821b258955587069F680729cD77369C0892B40", 288], + ["0x53691596d1BCe8CEa565b84d4915e69e03d9C99d", 42161], + ["0x7E63A5f1a8F0B4d0934B2f2327DAED3F6bb2ee75", 59144] + ] +}) + +export const mcBAND = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0xBA11D00c5f74255f56a5E366F4F77f5A186d7f55", 1], + ["0x46E7628E8b4350b2716ab470eE0bA1fa9e76c6C5", 250], + ["0xb2Ef65460BF71a05d59FDf5e8F114A32d445D164", 39797] + ] +}) + +export const mcGOMINING = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x7Ddc52c4De30e94Be3A6A0A2b259b2850f421989", 1], + ["0x7Ddc52c4De30e94Be3A6A0A2b259b2850f421989", 56] + ] +}) + +export const mcSXP = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x8CE9137d39326AD0cD6491fb5CC0CbA0e089b6A9", 1], + ["0x47BEAd2563dCBf3bF2c9407fEa4dC236fAbA485A", 56], + ["0x77d046614710fdDf5CA3E3cE85F4f09f7ABC283c", 1666600000] + ] +}) + +export const mcMMX = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x614Da3b37B6F66F7Ce69B4Bbbcf9a55CE6168707", 1], + ["0x95A62521c655e7A24A3919AA1f99764C05B7ec4E", 137] + ] +}) + +export const mcHT = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x6f259637dcD74C767781E37Bc6133cd6A68aa161", 1], + ["0xeceefC50f9aAcF0795586Ed90a8b9E24f55Ce3F3", 20], + ["0xBAA0974354680B0e8146d64bB27Fb92C03C4A2f2", 1666600000] + ] +}) + +export const mcAGI = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x7dA2641000Cbb407C329310C461b2cB9c70C3046", 1], + ["0x818835503F55283cd51A4399f595e295A9338753", 56] + ] +}) + +export const mcDOGE = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x1121AcC14c63f3C872BFcA497d10926A6098AAc5", 1], + ["0x67f0870BB897F5E1c369976b4A2962d527B9562c", 8453] + ] +}) + +export const mcBITCOIN = getMultichainContract({ + abi: erc20Abi, + deployments: [ + ["0x72e4f9F808C49A2a61dE9C5896298920Dc4EEEa9", 1], + ["0x2a06A17CBC6d0032Cac2c6696DA90f29D39a1a29", 8453] + ] +}) diff --git a/src/sdk/constants/tokens/index.ts b/src/sdk/constants/tokens/index.ts new file mode 100644 index 00000000..96748bf2 --- /dev/null +++ b/src/sdk/constants/tokens/index.ts @@ -0,0 +1 @@ +export * from "./__AUTO_GENERATED__" diff --git a/src/sdk/constants/tokens/tokens.test.ts b/src/sdk/constants/tokens/tokens.test.ts new file mode 100644 index 00000000..a6b19fd4 --- /dev/null +++ b/src/sdk/constants/tokens/tokens.test.ts @@ -0,0 +1,64 @@ +import type { Address, Chain, LocalAccount } from "viem" +import { base, optimism } from "viem/chains" +import { beforeAll, describe, expect, test } from "vitest" + +import * as tokens from "." +import { toNetwork } from "../../../test/testSetup" +import type { NetworkConfig } from "../../../test/testUtils" +import { addressEquals } from "../../account/utils/Utils" +import { + type MultichainSmartAccount, + toMultichainNexusAccount +} from "../../account/utils/toMultiChainNexusAccount" + +describe("mee:tokens", async () => { + let network: NetworkConfig + let eoaAccount: LocalAccount + let paymentChain: Chain + let paymentToken: Address + let mcNexusMainnet: MultichainSmartAccount + + beforeAll(async () => { + network = await toNetwork("MAINNET_FROM_ENV_VARS") + + paymentChain = network.chain + paymentToken = network.paymentToken! + eoaAccount = network.account! + + mcNexusMainnet = await toMultichainNexusAccount({ + chains: [base, paymentChain], + signer: eoaAccount + }) + }) + + test("should have relevant properties", async () => { + for (const token of Object.values(tokens)) { + expect(token).toHaveProperty("addressOn") + expect(token).toHaveProperty("deployments") + expect(token).toHaveProperty("on") + expect(token).toHaveProperty("read") + } + }) + + test("should instantiate a client", async () => { + const token = tokens.mcUSDC + const tokenWithChain = token.addressOn(10) + const mcNexusAddress = mcNexusMainnet.deploymentOn(base.id).address + + const balances = await token.read({ + onChains: [base, optimism], + functionName: "balanceOf", + args: [mcNexusAddress], + account: mcNexusMainnet + }) + + expect(balances.length).toBe(2) + + expect( + addressEquals( + tokenWithChain, + "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85" + ) + ).toBe(true) + }) +}) diff --git a/src/sdk/modules/k1Validator/toK1Validator.test.ts b/src/sdk/modules/k1Validator/toK1Validator.test.ts index d9168ab6..6fb44343 100644 --- a/src/sdk/modules/k1Validator/toK1Validator.test.ts +++ b/src/sdk/modules/k1Validator/toK1Validator.test.ts @@ -55,7 +55,7 @@ describe("modules.k1Validator", async () => { chain, transport: http(), bundlerTransport: http(bundlerUrl), - k1ValidatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, + validatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS }) diff --git a/src/sdk/modules/ownableValidator/decorators/ownables.decorators.test.ts b/src/sdk/modules/ownableValidator/decorators/ownables.decorators.test.ts index 28b87de5..2b8717a9 100644 --- a/src/sdk/modules/ownableValidator/decorators/ownables.decorators.test.ts +++ b/src/sdk/modules/ownableValidator/decorators/ownables.decorators.test.ts @@ -59,7 +59,7 @@ describe("modules.ownables.decorators", async () => { chain, transport: http(), bundlerTransport: http(bundlerUrl), - k1ValidatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, + validatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS }) diff --git a/src/sdk/modules/ownableValidator/toOwnableValidator.dx.test.ts b/src/sdk/modules/ownableValidator/toOwnableValidator.dx.test.ts index b1ba594e..b5aee66d 100644 --- a/src/sdk/modules/ownableValidator/toOwnableValidator.dx.test.ts +++ b/src/sdk/modules/ownableValidator/toOwnableValidator.dx.test.ts @@ -66,7 +66,7 @@ describe("modules.ownableValidator.dx", async () => { chain, transport: http(), bundlerTransport: http(bundlerUrl), - k1ValidatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, + validatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS }) diff --git a/src/sdk/modules/ownableValidator/toOwnableValidator.executor.test.ts b/src/sdk/modules/ownableValidator/toOwnableValidator.executor.test.ts index bd994b03..160d3714 100644 --- a/src/sdk/modules/ownableValidator/toOwnableValidator.executor.test.ts +++ b/src/sdk/modules/ownableValidator/toOwnableValidator.executor.test.ts @@ -65,7 +65,7 @@ describe("modules.ownableExecutor", async () => { chain, transport: http(), bundlerTransport: http(bundlerUrl), - k1ValidatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, + validatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS }) diff --git a/src/sdk/modules/ownableValidator/toOwnableValidator.test.ts b/src/sdk/modules/ownableValidator/toOwnableValidator.test.ts index c040135c..a8085433 100644 --- a/src/sdk/modules/ownableValidator/toOwnableValidator.test.ts +++ b/src/sdk/modules/ownableValidator/toOwnableValidator.test.ts @@ -69,7 +69,7 @@ describe("modules.ownables", async () => { chain, transport: http(), bundlerTransport: http(bundlerUrl), - k1ValidatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, + validatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS }) diff --git a/src/sdk/modules/smartSessionsValidator/decorators/smartSessions.decorators.test.ts b/src/sdk/modules/smartSessionsValidator/decorators/smartSessions.decorators.test.ts index 76edd8f1..136d3e40 100644 --- a/src/sdk/modules/smartSessionsValidator/decorators/smartSessions.decorators.test.ts +++ b/src/sdk/modules/smartSessionsValidator/decorators/smartSessions.decorators.test.ts @@ -57,7 +57,7 @@ describe("modules.smartSessions.decorators", async () => { chain, transport: http(), bundlerTransport: http(bundlerUrl), - k1ValidatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, + validatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS }) diff --git a/src/sdk/modules/smartSessionsValidator/toSmartSessionValidator.enable.mode.test.ts b/src/sdk/modules/smartSessionsValidator/toSmartSessionValidator.enable.mode.test.ts index ffd6108d..d13b4eb5 100644 --- a/src/sdk/modules/smartSessionsValidator/toSmartSessionValidator.enable.mode.test.ts +++ b/src/sdk/modules/smartSessionsValidator/toSmartSessionValidator.enable.mode.test.ts @@ -4,7 +4,6 @@ import { type Address, type Chain, type LocalAccount, - type PrivateKeyAccount, type PublicClient, type WalletClient, createPublicClient, @@ -175,7 +174,11 @@ describe("modules.smartSessions.enable.mode.dx", async () => { const { permissionEnableHash, ...sessionDetails } = sessionDetailsWithPermissionEnableHash + if (!sessionDetails.enableSessionData?.enableSession.permissionEnableSig) { + throw new Error("enableSessionData is undefined") + } sessionDetails.enableSessionData.enableSession.permissionEnableSig = + // @ts-ignore await eoaAccount.signMessage({ message: { raw: permissionEnableHash diff --git a/src/sdk/modules/utils/Types.ts b/src/sdk/modules/utils/Types.ts index d90b721e..2327c44b 100644 --- a/src/sdk/modules/utils/Types.ts +++ b/src/sdk/modules/utils/Types.ts @@ -119,6 +119,18 @@ export type Modularity = { export type ModularSmartAccount = SmartAccount & Modularity +export type MinimalMEESmartAccount = Pick< + SmartAccount, + | "address" + | "getCounterFactualAddress" + | "isDeployed" + | "client" + | "getInitCode" + | "getNonce" + | "encodeExecuteBatch" + | "isDeployed" +> + export type ModuleMeta = { address: Hex type: ModuleType diff --git a/src/test/README.md b/src/test/README.md index d2698a56..c914d0ab 100644 --- a/src/test/README.md +++ b/src/test/README.md @@ -21,7 +21,7 @@ ### Testnet Scope - Use by setting `const NETWORK_TYPE: TestFileNetworkType = "TESTNET_FROM_ENV_VARS"` for test files that rely on the network, private key and bundler url specified in your env vars. -- `"TESTNET_FROM_ALT_ENV_VARS"` is also available, which uses alternative env vars which you've specified. +- `"MAINNET_FROM_ENV_VARS"` is also available, which uses alternative env vars which you've specified. - Networks scoped to a testnet are not isolated to the file in which they are used, they require tesnet tokens, can often fail for gas reasons, and they will add additional latency to tests. - Avoid overusing this option diff --git a/src/test/__contracts/abi/index.ts b/src/test/__contracts/abi/index.ts index 85d93e93..545f9673 100644 --- a/src/test/__contracts/abi/index.ts +++ b/src/test/__contracts/abi/index.ts @@ -2,7 +2,6 @@ export * from "./MockHookAbi" export * from "./StakeableAbi" export * from "./NexusAccountFactoryAbi" export * from "./BiconomyMetaFactoryAbi" -export * from "./NexusBootstrapAbi" export * from "./CounterAbi" export * from "./MockValidatorAbi" export * from "./MockTokenAbi" diff --git a/src/test/globalSetup.ts b/src/test/globalSetup.ts index ac462081..94b7143a 100644 --- a/src/test/globalSetup.ts +++ b/src/test/globalSetup.ts @@ -1,15 +1,19 @@ +import { config } from "dotenv" import { type NetworkConfig, type NetworkConfigWithBundler, - initLocalhostNetwork + initAnvilNetwork } from "./testUtils" +config() + let globalConfig: NetworkConfigWithBundler // @ts-ignore export const setup = async ({ provide }) => { - globalConfig = await initLocalhostNetwork() + globalConfig = await initAnvilNetwork() + const runPaidTests = process.env.RUN_PAID_TESTS?.toString() === "true" const { bundlerInstance, instance, ...serializeableConfig } = globalConfig - provide("globalNetwork", serializeableConfig) + provide("settings", { ...serializeableConfig, runPaidTests }) } export const teardown = async () => { @@ -21,6 +25,6 @@ export const teardown = async () => { declare module "vitest" { export interface ProvidedContext { - globalNetwork: NetworkConfig + settings: NetworkConfig & { runPaidTests: boolean } } } diff --git a/src/test/testSetup.ts b/src/test/testSetup.ts index 7bc74a8e..8b11b9c6 100644 --- a/src/test/testSetup.ts +++ b/src/test/testSetup.ts @@ -3,8 +3,8 @@ import { type FundedTestClients, type NetworkConfig, type NetworkConfigWithBundler, - initLocalhostNetwork, - initTestnetNetwork, + initAnvilNetwork, + initNetwork, toFundedTestClients } from "./testUtils" @@ -17,7 +17,7 @@ export const localhostTest = test.extend<{ }>({ // biome-ignore lint/correctness/noEmptyPattern: Needed in vitest :/ config: async ({}, use) => { - const testNetwork = await initLocalhostNetwork() + const testNetwork = await initAnvilNetwork() const fundedTestClients = await toFundedTestClients({ chain: testNetwork.chain, bundlerUrl: testNetwork.bundlerUrl @@ -44,7 +44,7 @@ export type TestFileNetworkType = | "BESPOKE_ANVIL_NETWORK" | "BESPOKE_ANVIL_NETWORK_FORKING_BASE_SEPOLIA" | "TESTNET_FROM_ENV_VARS" - | "TESTNET_FROM_ALT_ENV_VARS" + | "MAINNET_FROM_ENV_VARS" | "COMMUNAL_ANVIL_NETWORK" export const toNetworks = async ( @@ -65,17 +65,16 @@ export const toNetwork = async ( const forkBaseSepolia = networkType === "BESPOKE_ANVIL_NETWORK_FORKING_BASE_SEPOLIA" const communalAnvil = networkType === "COMMUNAL_ANVIL_NETWORK" - const testNet = [ - "TESTNET_FROM_ENV_VARS", - "TESTNET_FROM_ALT_ENV_VARS" - ].includes(networkType) + const network = ["TESTNET_FROM_ENV_VARS", "MAINNET_FROM_ENV_VARS"].includes( + networkType + ) return await (communalAnvil ? // @ts-ignore - inject("globalNetwork") - : testNet - ? initTestnetNetwork(networkType) - : initLocalhostNetwork(forkBaseSepolia)) + inject("settings") + : network + ? initNetwork(networkType) + : initAnvilNetwork(forkBaseSepolia)) } export const paymasterTruthy = () => { diff --git a/src/test/testUtils.ts b/src/test/testUtils.ts index 3fef3257..5034c9d9 100644 --- a/src/test/testUtils.ts +++ b/src/test/testUtils.ts @@ -37,6 +37,7 @@ import { type NexusClient, createSmartAccountClient } from "../sdk/clients/createSmartAccountClient" +import { mcUSDC } from "../sdk/constants/tokens" import * as hardhatExec from "./executables" import type { TestFileNetworkType } from "./testSetup" @@ -64,6 +65,7 @@ export type NetworkConfig = Omit< "instance" | "bundlerInstance" > & { account?: PrivateKeyAccount + paymentToken?: Address paymasterUrl?: string meeNodeUrl?: string } @@ -97,17 +99,17 @@ export const killNetwork = (ids: number[]) => }) ) -export const initTestnetNetwork = async ( +export const initNetwork = async ( type: TestFileNetworkType = "TESTNET_FROM_ENV_VARS" ): Promise => { const privateKey = process.env.PRIVATE_KEY const chainId_ = process.env.CHAIN_ID - const altChainId = process.env.ALT_CHAIN_ID + const mainnetChainId = process.env.MAINNET_CHAIN_ID const rpcUrl = process.env.RPC_URL //Optional, taken from chain (using chainId) if not provided const _bundlerUrl = process.env.BUNDLER_URL // Optional, taken from chain (using chainId) if not provided const paymasterUrl = process.env.PAYMASTER_URL // Optional - - const chainId = type === "TESTNET_FROM_ALT_ENV_VARS" ? altChainId : chainId_ + const chainId = type === "MAINNET_FROM_ENV_VARS" ? mainnetChainId : chainId_ + const paymentToken = mcUSDC.addressOn(Number(chainId)) let chain: Chain @@ -134,11 +136,12 @@ export const initTestnetNetwork = async ( bundlerUrl, paymasterUrl, bundlerPort: 0, - account: holder + account: holder, + paymentToken } } -export const initLocalhostNetwork = async ( +export const initAnvilNetwork = async ( shouldForkBaseSepolia = false ): Promise => { const configuredNetwork = await initAnvilPayload(shouldForkBaseSepolia) @@ -505,7 +508,7 @@ export const setByteCodeDynamic = async ( export type TestnetParams = ReturnType export const getTestParamsForTestnet = (publicClient: PublicClient) => ({ - k1ValidatorAddress: MAINNET_ADDRESS_K1_VALIDATOR_ADDRESS, + validatorAddress: MAINNET_ADDRESS_K1_VALIDATOR_ADDRESS, factoryAddress: MAINNET_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS, userOperation: { estimateFeesPerGas: async (_) => { From d743ef555a09b461b6c10e16bbce847cc174d2f6 Mon Sep 17 00:00:00 2001 From: Joe Pegler Date: Mon, 13 Jan 2025 17:28:05 +0000 Subject: [PATCH 2/6] chore: version bump --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7409be5a..d3a8b67b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # @biconomy/sdk +## 0.0.25 + +### Patch Changes + +- Added Mee Client + ## 0.0.24 ### Patch Changes diff --git a/package.json b/package.json index ab924b7b..b49c4425 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@biconomy/sdk", - "version": "0.0.24", + "version": "0.0.25", "author": "Biconomy", "repository": "github:bcnmy/sdk", "main": "./dist/_cjs/index.js", From 1fa44a67ef979f6368502dad1bdeb9e7bb397bb3 Mon Sep 17 00:00:00 2001 From: Joe Pegler Date: Mon, 13 Jan 2025 17:30:32 +0000 Subject: [PATCH 3/6] chore: tsdoc comments --- src/sdk/account/utils/getFactoryData.ts | 30 +++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/sdk/account/utils/getFactoryData.ts b/src/sdk/account/utils/getFactoryData.ts index b81e8e8d..50b653f8 100644 --- a/src/sdk/account/utils/getFactoryData.ts +++ b/src/sdk/account/utils/getFactoryData.ts @@ -11,12 +11,26 @@ import { } from "viem" import { NexusBootstrapAbi } from "../../constants/abi/NexusBootstrapAbi" +/** + * Parameters for generating K1 factory initialization data + * @interface GetK1FactoryDataParams + * @property {Address} signerAddress - The address of the EOA signer + * @property {bigint} index - The account index + * @property {Address[]} attesters - Array of attester addresses + * @property {number} attesterThreshold - Minimum number of attesters required + */ export type GetK1FactoryDataParams = { signerAddress: Address index: bigint attesters: Address[] attesterThreshold: number } + +/** + * Generates encoded factory data for K1 account creation + * @param {GetK1FactoryDataParams} params - Parameters for K1 account creation + * @returns {Promise} Encoded function data for account creation + */ export const getK1FactoryData = async ({ signerAddress, index, @@ -31,6 +45,16 @@ export const getK1FactoryData = async ({ args: [signerAddress, index, attesters, attesterThreshold] }) +/** + * Parameters for generating MEE factory initialization data + * @interface GetMeeFactoryDataParams + * @extends {GetK1FactoryDataParams} + * @property {Address} validatorAddress - The address of the validator + * @property {Address} registryAddress - The address of the registry contract + * @property {PublicClient} publicClient - Viem public client instance + * @property {WalletClient} walletClient - Viem wallet client instance + * @property {Address} bootStrapAddress - The address of the bootstrap contract + */ export type GetMeeFactoryDataParams = GetK1FactoryDataParams & { validatorAddress: Address registryAddress: Address @@ -38,6 +62,12 @@ export type GetMeeFactoryDataParams = GetK1FactoryDataParams & { walletClient: WalletClient bootStrapAddress: Address } + +/** + * Generates encoded factory data for MEE account creation + * @param {GetMeeFactoryDataParams} params - Parameters for MEE account creation + * @returns {Promise} Encoded function data for account creation + */ export const getMeeFactoryData = async ({ validatorAddress, attesters, From 0c52c86a0c4c283d7db85e9c8bf18c7108cc39f0 Mon Sep 17 00:00:00 2001 From: Joe Pegler Date: Mon, 13 Jan 2025 17:32:41 +0000 Subject: [PATCH 4/6] chore: fix workflows --- .github/workflows/funded-tests.yml | 8 ++++++-- .github/workflows/unit-tests.yml | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/funded-tests.yml b/.github/workflows/funded-tests.yml index bc44ad32..8c687645 100644 --- a/.github/workflows/funded-tests.yml +++ b/.github/workflows/funded-tests.yml @@ -25,7 +25,11 @@ jobs: run: bun run test env: PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} - CHAIN_ID: ${{ secrets.CHAIN_ID }} - CI: true + PIMLICO_API_KEY: ${{ secrets.PIMLICO_API_KEY }} + PAYMASTER_URL: ${{ secrets.PAYMASTER_URL }} + BUNDLER_URL: ${{ secrets.BUNDLER_URL }} + CHAIN_ID: 84532 + MAINNET_CHAIN_ID: 10 RUN_PAID_TESTS: true + CI: true diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 25bfd1b7..15ef332c 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -29,5 +29,5 @@ jobs: PAYMASTER_URL: ${{ secrets.PAYMASTER_URL }} BUNDLER_URL: ${{ secrets.BUNDLER_URL }} CHAIN_ID: 84532 - MAINNET_CHAIN_ID: 11155420 + MAINNET_CHAIN_ID: 10 CI: true From ecef0566b672c4161915c5ba856d6ca6fdd7ce20 Mon Sep 17 00:00:00 2001 From: Joe Pegler Date: Tue, 14 Jan 2025 15:54:00 +0000 Subject: [PATCH 5/6] chore: restore helper functions --- .env.example | 2 +- .../buildBalanceInstructions.test.ts | 49 +++ .../decorators/buildBalanceInstructions.ts | 52 ++++ .../buildBridgeInstructions.test.ts | 59 ++++ .../decorators/buildBridgeInstructions.ts | 282 ++++++++++++++++++ .../account/decorators/getFactoryData.test.ts | 82 +++++ .../{utils => decorators}/getFactoryData.ts | 17 +- .../decorators/getNexusAddress.test.ts | 73 +++++ .../getNexusAddress.ts} | 81 +++-- .../decorators/getUnifiedERC20Balance.test.ts | 51 ++++ .../decorators/getUnifiedERC20Balance.ts | 110 +++++++ .../account/decorators/queryBridge.test.ts | 63 ++++ src/sdk/account/decorators/queryBridge.ts | 94 ++++++ .../account/toMultiChainNexusAccount.test.ts | 137 +++++++++ src/sdk/account/toMultiChainNexusAccount.ts | 171 +++++++++++ .../account/toNexusAccount.addresses.test.ts | 8 +- src/sdk/account/toNexusAccount.ts | 22 +- src/sdk/account/utils/acrossPlugin.ts | 150 ++++++++++ .../utils/{explorer => }/explorer.test.ts | 19 +- .../account/utils/{explorer => }/explorer.ts | 4 +- .../utils/getMultichainContract.test.ts | 89 ++++++ .../account/utils/getMultichainContract.ts | 5 +- src/sdk/account/utils/index.ts | 2 +- .../utils/toMultiChainNexusAccount.test.ts | 117 -------- .../account/utils/toMultiChainNexusAccount.ts | 76 ----- src/sdk/clients/createHttpClient.test.ts | 10 +- src/sdk/clients/createHttpClient.ts | 9 +- src/sdk/clients/createMeeClient.test.ts | 86 +----- src/sdk/clients/createMeeClient.ts | 2 +- .../clients/decorators/mee/execute.test.ts | 10 +- .../decorators/mee/executeQuote.test.ts | 10 +- .../decorators/mee/executeSignedQuote.test.ts | 12 +- .../clients/decorators/mee/getQuote.test.ts | 28 +- src/sdk/clients/decorators/mee/getQuote.ts | 66 ++-- .../decorators/mee/signFusionQuote.test.ts | 10 +- .../clients/decorators/mee/signFusionQuote.ts | 2 +- .../clients/decorators/mee/signQuote.test.ts | 10 +- src/sdk/clients/decorators/mee/signQuote.ts | 2 +- .../mee/waitForSupertransactionReceipt.ts | 2 +- src/sdk/constants/tokens/tokens.test.ts | 14 +- src/sdk/modules/utils/Types.ts | 2 +- 41 files changed, 1638 insertions(+), 452 deletions(-) create mode 100644 src/sdk/account/decorators/buildBalanceInstructions.test.ts create mode 100644 src/sdk/account/decorators/buildBalanceInstructions.ts create mode 100644 src/sdk/account/decorators/buildBridgeInstructions.test.ts create mode 100644 src/sdk/account/decorators/buildBridgeInstructions.ts create mode 100644 src/sdk/account/decorators/getFactoryData.test.ts rename src/sdk/account/{utils => decorators}/getFactoryData.ts (90%) create mode 100644 src/sdk/account/decorators/getNexusAddress.test.ts rename src/sdk/account/{utils/getCounterFactualAddress.ts => decorators/getNexusAddress.ts} (50%) create mode 100644 src/sdk/account/decorators/getUnifiedERC20Balance.test.ts create mode 100644 src/sdk/account/decorators/getUnifiedERC20Balance.ts create mode 100644 src/sdk/account/decorators/queryBridge.test.ts create mode 100644 src/sdk/account/decorators/queryBridge.ts create mode 100644 src/sdk/account/toMultiChainNexusAccount.test.ts create mode 100644 src/sdk/account/toMultiChainNexusAccount.ts create mode 100644 src/sdk/account/utils/acrossPlugin.ts rename src/sdk/account/utils/{explorer => }/explorer.test.ts (79%) rename src/sdk/account/utils/{explorer => }/explorer.ts (93%) create mode 100644 src/sdk/account/utils/getMultichainContract.test.ts delete mode 100644 src/sdk/account/utils/toMultiChainNexusAccount.test.ts delete mode 100644 src/sdk/account/utils/toMultiChainNexusAccount.ts diff --git a/.env.example b/.env.example index be9ffdef..d4a63896 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,6 @@ PRIVATE_KEY= CHAIN_ID=84532 -MAINNET_CHAIN_ID=11155111 +MAINNET_CHAIN_ID=10 RPC_URL= BUNDLER_URL= BICONOMY_SDK_DEBUG=false diff --git a/src/sdk/account/decorators/buildBalanceInstructions.test.ts b/src/sdk/account/decorators/buildBalanceInstructions.test.ts new file mode 100644 index 00000000..21263e43 --- /dev/null +++ b/src/sdk/account/decorators/buildBalanceInstructions.test.ts @@ -0,0 +1,49 @@ +import type { Address, Chain, LocalAccount } from "viem" +import { base } from "viem/chains" +import { beforeAll, describe, expect, it } from "vitest" +import { toNetwork } from "../../../test/testSetup" +import type { NetworkConfig } from "../../../test/testUtils" +import { type MeeClient, createMeeClient } from "../../clients/createMeeClient" +import { mcUSDC } from "../../constants/tokens" +import { + type MultichainSmartAccount, + toMultichainNexusAccount +} from "../toMultiChainNexusAccount" +import { buildBalanceInstructions } from "./buildBalanceInstructions" + +describe("mee:buildBalanceInstruction", () => { + let network: NetworkConfig + let eoaAccount: LocalAccount + let paymentChain: Chain + let paymentToken: Address + let mcNexus: MultichainSmartAccount + let meeClient: MeeClient + + beforeAll(async () => { + network = await toNetwork("MAINNET_FROM_ENV_VARS") + + paymentChain = network.chain + paymentToken = network.paymentToken! + eoaAccount = network.account! + + mcNexus = await toMultichainNexusAccount({ + chains: [base, paymentChain], + signer: eoaAccount + }) + + meeClient = createMeeClient({ account: mcNexus }) + }) + + it("should adjust the account balance", async () => { + const instructions = await buildBalanceInstructions({ + account: mcNexus, + amount: BigInt(1000), + token: mcUSDC, + chain: base + }) + + expect(instructions.length).toBeGreaterThan(0) + expect(instructions[0]).toHaveProperty("calls") + expect(instructions[0].calls.length).toBeGreaterThan(0) + }) +}) diff --git a/src/sdk/account/decorators/buildBalanceInstructions.ts b/src/sdk/account/decorators/buildBalanceInstructions.ts new file mode 100644 index 00000000..ddf80b15 --- /dev/null +++ b/src/sdk/account/decorators/buildBalanceInstructions.ts @@ -0,0 +1,52 @@ +import type { Chain, erc20Abi } from "viem" +import type { Instruction } from "../../clients/decorators/mee/getQuote" +import type { BaseMultichainSmartAccount } from "../toMultiChainNexusAccount" +import type { MultichainContract } from "../utils/getMultichainContract" +import buildBridgeInstructions from "./buildBridgeInstructions" +import { getUnifiedERC20Balance } from "./getUnifiedERC20Balance" + +export type BuildBalanceInstructionParams = { + /** Optional smart account to execute the transaction. If not provided, uses the client's default account */ + account: BaseMultichainSmartAccount + /** The amount of tokens to require */ + amount: bigint + /** The token to require */ + token: MultichainContract + /** The chain to require the token on */ + chain: Chain +} + +/** + * Makes sure that the user has enough funds on the selected chain before filling the + * supertransaction. Bridges funds from other chains if needed. + * + * @param client - The Mee client to use + * @param params - The parameters for the balance requirement + * @returns Instructions for any required bridging operations + * @example + * const instructions = await buildBalanceInstruction(client, { + * amount: BigInt(1000), + * token: mcUSDC, + * chain: base + * }) + */ + +export const buildBalanceInstructions = async ( + params: BuildBalanceInstructionParams +): Promise => { + const { amount, token, chain, account } = params + const unifiedBalance = await getUnifiedERC20Balance({ + mcToken: token, + account + }) + const { instructions } = await buildBridgeInstructions({ + account, + amount: amount, + toChain: chain, + unifiedBalance + }) + + return instructions +} + +export default buildBalanceInstructions diff --git a/src/sdk/account/decorators/buildBridgeInstructions.test.ts b/src/sdk/account/decorators/buildBridgeInstructions.test.ts new file mode 100644 index 00000000..09ad3ee1 --- /dev/null +++ b/src/sdk/account/decorators/buildBridgeInstructions.test.ts @@ -0,0 +1,59 @@ +import type { Address, Chain, LocalAccount } from "viem" +import { base } from "viem/chains" +import { beforeAll, describe, expect, it } from "vitest" +import { toNetwork } from "../../../test/testSetup" +import type { NetworkConfig } from "../../../test/testUtils" +import { type MeeClient, createMeeClient } from "../../clients/createMeeClient" +import { mcUSDC } from "../../constants/tokens" +import { + type MultichainSmartAccount, + toMultichainNexusAccount +} from "../toMultiChainNexusAccount" +import { AcrossPlugin } from "../utils/acrossPlugin" +import buildBridgeInstructions from "./buildBridgeInstructions" +import { getUnifiedERC20Balance } from "./getUnifiedERC20Balance" + +describe("mee:buildBridgeInstructions", () => { + let network: NetworkConfig + let eoaAccount: LocalAccount + let paymentChain: Chain + let paymentToken: Address + let mcNexus: MultichainSmartAccount + let meeClient: MeeClient + + beforeAll(async () => { + network = await toNetwork("MAINNET_FROM_ENV_VARS") + + paymentChain = network.chain + paymentToken = network.paymentToken! + eoaAccount = network.account! + + mcNexus = await toMultichainNexusAccount({ + chains: [base, paymentChain], + signer: eoaAccount + }) + + meeClient = createMeeClient({ account: mcNexus }) + }) + + it("should call the bridge with a unified balance", async () => { + const unifiedBalance = await mcNexus.getUnifiedERC20Balance(mcUSDC) + const payload = await buildBridgeInstructions({ + account: mcNexus, + amount: 1n, + bridgingPlugins: [AcrossPlugin], + toChain: base, + unifiedBalance + }) + + expect(payload).toHaveProperty("meta") + expect(payload).toHaveProperty("instructions") + expect(payload.instructions.length).toBeGreaterThan(0) + expect(payload.meta.bridgingInstructions.length).toBeGreaterThan(0) + expect(payload.meta.bridgingInstructions[0]).toHaveProperty("userOp") + expect(payload.meta.bridgingInstructions[0].userOp).toHaveProperty("calls") + expect( + payload.meta.bridgingInstructions[0].userOp.calls.length + ).toBeGreaterThan(0) + }) +}) diff --git a/src/sdk/account/decorators/buildBridgeInstructions.ts b/src/sdk/account/decorators/buildBridgeInstructions.ts new file mode 100644 index 00000000..be74e3f8 --- /dev/null +++ b/src/sdk/account/decorators/buildBridgeInstructions.ts @@ -0,0 +1,282 @@ +import type { Address, Chain } from "viem" +import type { Instruction } from "../../clients/decorators/mee/getQuote" +import type { BaseMultichainSmartAccount } from "../toMultiChainNexusAccount" +import { AcrossPlugin } from "../utils/acrossPlugin" +import type { UnifiedERC20Balance } from "./getUnifiedERC20Balance" +import type { BridgeQueryResult } from "./queryBridge" +import { queryBridge } from "./queryBridge" + +/** + * Mapping of a token address to a specific chain + */ +export type AddressMapping = { + chainId: number + address: Address +} + +/** + * Cross-chain token address mapping with helper functions + */ +export type MultichainAddressMapping = { + deployments: AddressMapping[] + /** Returns the token address for a given chain ID */ + on: (chainId: number) => Address +} + +/** + * Parameters for multichain token bridging operations + */ +export type MultichainBridgingParams = { + /** Destination chain for the bridge operation */ + toChain: Chain + /** Unified token balance across all chains */ + unifiedBalance: UnifiedERC20Balance + /** Amount to bridge */ + amount: bigint + /** Plugins to use for bridging */ + bridgingPlugins?: BridgingPlugin[] + /** FeeData for the tx fee */ + feeData?: { + /** Chain ID where the tx fee is paid */ + txFeeChainId: number + /** Amount of tx fee to pay */ + txFeeAmount: bigint + } +} + +/** + * Result of a bridging plugin operation + */ +export type BridgingPluginResult = { + /** User operation to execute the bridge */ + userOp: Instruction + /** Expected amount to be received at destination */ + receivedAtDestination?: bigint + /** Expected duration of the bridging operation in milliseconds */ + bridgingDurationExpectedMs?: number +} + +/** + * Parameters for generating a bridge user operation + */ +export type BridgingUserOpParams = { + /** Source chain for the bridge */ + fromChain: Chain + /** Destination chain for the bridge */ + toChain: Chain + /** Smart account to execute the bridging */ + account: BaseMultichainSmartAccount + /** Token addresses across chains */ + tokenMapping: MultichainAddressMapping + /** Amount to bridge */ + bridgingAmount: bigint +} + +/** + * Interface for a bridging plugin implementation + */ +export type BridgingPlugin = { + /** Generates a user operation for bridging tokens */ + encodeBridgeUserOp: ( + params: BridgingUserOpParams + ) => Promise +} + +export type BuildBridgeInstructionParams = MultichainBridgingParams & { + /** Smart account to execute the bridging */ + account: BaseMultichainSmartAccount +} + +/** + * Single bridge operation result + */ +export type BridgingInstruction = { + /** User operation to execute */ + userOp: Instruction + /** Expected amount to be received at destination */ + receivedAtDestination?: bigint + /** Expected duration of the bridging operation */ + bridgingDurationExpectedMs?: number +} + +/** + * Complete set of bridging instructions and final outcome + */ +export type BridgingInstructions = { + /** Array of bridging operations to execute */ + instructions: Instruction[] + /** Meta information about the bridging process */ + meta: { + /** Total amount that will be available on destination chain */ + totalAvailableOnDestination: bigint + /** Array of bridging operations to execute */ + bridgingInstructions: BridgingInstruction[] + } +} + +/** + * Makes sure that the user has enough funds on the selected chain before filling the + * supertransaction. Bridges funds from other chains if needed. + * + * @param client - The Mee client to use + * @param params - The parameters for the Bridge requirement + * @returns Instructions for any required bridging operations + * @example + * const instructions = await buildBridgeInstruction(client, { + * amount: BigInt(1000), + * token: mcUSDC, + * chain: base + * }) + */ + +export const buildBridgeInstructions = async ( + params: BuildBridgeInstructionParams +): Promise => { + const { + account, + amount: targetAmount, + toChain, + unifiedBalance, + bridgingPlugins = [AcrossPlugin], + feeData + } = params + + // Create token address mapping + const tokenMapping: MultichainAddressMapping = { + on: (chainId: number) => + unifiedBalance.token.deployments.get(chainId) || "0x", + deployments: Array.from( + unifiedBalance.token.deployments.entries(), + ([chainId, address]) => ({ + chainId, + address + }) + ) + } + + // Get current balance on destination chain + const destinationBalance = + unifiedBalance.breakdown.find((b) => b.chainId === toChain.id)?.balance || + 0n + + // If we have enough on destination, no bridging needed + if (destinationBalance >= targetAmount) { + return { + instructions: [], + meta: { + bridgingInstructions: [], + totalAvailableOnDestination: destinationBalance + } + } + } + + // Calculate how much we need to bridge + const amountToBridge = targetAmount - destinationBalance + + // Get available balances from source chains + const sourceBalances = unifiedBalance.breakdown + .filter((balance) => balance.chainId !== toChain.id) + .map((balance) => { + // If this is the fee payment chain, adjust available balance + const isFeeChain = feeData && feeData.txFeeChainId === balance.chainId + + const availableBalance = + isFeeChain && "txFeeAmount" in feeData + ? balance.balance > feeData.txFeeAmount + ? balance.balance - feeData.txFeeAmount + : 0n + : balance.balance + + return { + chainId: balance.chainId, + balance: availableBalance + } + }) + .filter((balance) => balance.balance > 0n) + + // Get chain configurations + const chains = Object.fromEntries( + account.deployments.map((deployment) => { + const chain = deployment.client.chain + if (!chain) { + throw new Error( + `Client not configured with chain for deployment at ${deployment.address}` + ) + } + return [chain.id, chain] as const + }) + ) + + // Query all possible routes + const bridgeQueries = sourceBalances.flatMap((source) => { + const fromChain = chains[source.chainId] + if (!fromChain) return [] + + return bridgingPlugins.map((plugin) => + queryBridge({ + fromChain, + toChain, + plugin, + amount: source.balance, + account, + tokenMapping + }) + ) + }) + + const bridgeResults = (await Promise.all(bridgeQueries)) + .filter((result): result is BridgeQueryResult => result !== null) + // Sort by received amount relative to sent amount + .sort( + (a, b) => + Number((b.receivedAtDestination * 10000n) / b.amount) - + Number((a.receivedAtDestination * 10000n) / a.amount) + ) + + // Build instructions by taking from best routes until we have enough + const bridgingInstructions: BridgingInstruction[] = [] + const instructions: Instruction[] = [] + let totalBridged = 0n + let remainingNeeded = amountToBridge + + for (const result of bridgeResults) { + if (remainingNeeded <= 0n) break + + const amountToTake = + result.amount >= remainingNeeded ? remainingNeeded : result.amount + + // Recalculate received amount based on portion taken + const receivedFromRoute = + (result.receivedAtDestination * amountToTake) / result.amount + + instructions.push(result.userOp) + bridgingInstructions.push({ + userOp: result.userOp, + receivedAtDestination: receivedFromRoute, + bridgingDurationExpectedMs: result.bridgingDurationExpectedMs + }) + + totalBridged += receivedFromRoute + remainingNeeded -= amountToTake + } + + // Check if we got enough + if (remainingNeeded > 0n) { + throw new Error( + `Insufficient balance for bridging: + Required: ${targetAmount.toString()} + Available to bridge: ${totalBridged.toString()} + Shortfall: ${remainingNeeded.toString()}` + ) + } + + return { + instructions, + meta: { + bridgingInstructions, + totalAvailableOnDestination: destinationBalance + totalBridged + } + } +} + +export default buildBridgeInstructions diff --git a/src/sdk/account/decorators/getFactoryData.test.ts b/src/sdk/account/decorators/getFactoryData.test.ts new file mode 100644 index 00000000..51361bbe --- /dev/null +++ b/src/sdk/account/decorators/getFactoryData.test.ts @@ -0,0 +1,82 @@ +import { + http, + type Address, + type Chain, + type LocalAccount, + type PublicClient, + type WalletClient, + createWalletClient +} from "viem" +import { afterAll, beforeAll, describe, expect, test } from "vitest" +import { toNetwork } from "../../../test/testSetup" +import { + getTestAccount, + killNetwork, + toTestClient +} from "../../../test/testUtils" +import type { MasterClient, NetworkConfig } from "../../../test/testUtils" +import { + type NexusClient, + createSmartAccountClient +} from "../../clients/createSmartAccountClient" +import { + MEE_VALIDATOR_ADDRESS, + RHINESTONE_ATTESTER_ADDRESS, + TEST_ADDRESS_K1_VALIDATOR_ADDRESS, + TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS +} from "../../constants" +import type { NexusAccount } from "../toNexusAccount" +import { getK1FactoryData, getMeeFactoryData } from "./getFactoryData" + +describe("nexus.account.getFactoryData", async () => { + let network: NetworkConfig + let chain: Chain + let bundlerUrl: string + + // Test utils + let testClient: MasterClient + let eoaAccount: LocalAccount + let nexusAccount: NexusAccount + let walletClient: WalletClient + + beforeAll(async () => { + network = await toNetwork("MAINNET_FROM_ENV_VARS") + + chain = network.chain + bundlerUrl = network.bundlerUrl + eoaAccount = network.account! + testClient = toTestClient(chain, getTestAccount(5)) + }) + afterAll(async () => { + await killNetwork([network?.rpcPort, network?.bundlerPort]) + }) + + test("should check factory data", async () => { + const factoryData = await getK1FactoryData({ + signerAddress: eoaAccount.address, + index: 0n, + attesters: [RHINESTONE_ATTESTER_ADDRESS], + attesterThreshold: 1 + }) + + expect(factoryData).toMatchInlineSnapshot( + `"0x0d51f0b70000000000000000000000003079b249dfde4692d7844aa261f8cf7d927a0da50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000333034e9f539ce08819e12c1b8cb29084d"` + ) + }) + + test("should check factory data with mee", async () => { + const factoryData = await getMeeFactoryData({ + signerAddress: eoaAccount.address, + index: 0n, + attesters: [MEE_VALIDATOR_ADDRESS], + attesterThreshold: 1, + validatorAddress: MEE_VALIDATOR_ADDRESS, + publicClient: testClient as unknown as PublicClient, + walletClient + }) + + expect(factoryData).toMatchInlineSnapshot( + `"0xea6d13ac0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000000000f5b753fdd20c5ca2d7c1210b3ab1ea59030000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000012401fe9ff2000000000000000000000000068ea3e30788abafdc6fd0b38d20bd38a40a2b3d00000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000069e2a187aeffb852bf3ccdc95151b200000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000143079b249dfde4692d7844aa261f8cf7d927a0da50000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000068ea3e30788abafdc6fd0b38d20bd38a40a2b3d00000000000000000000000000000000000000000000000000000000"` + ) + }) +}) diff --git a/src/sdk/account/utils/getFactoryData.ts b/src/sdk/account/decorators/getFactoryData.ts similarity index 90% rename from src/sdk/account/utils/getFactoryData.ts rename to src/sdk/account/decorators/getFactoryData.ts index 50b653f8..e27c3a0c 100644 --- a/src/sdk/account/utils/getFactoryData.ts +++ b/src/sdk/account/decorators/getFactoryData.ts @@ -9,6 +9,11 @@ import { parseAbi, toHex } from "viem" +import { + MEE_VALIDATOR_ADDRESS, + NEXUS_BOOTSTRAP_ADDRESS, + REGISTRY_ADDRESS +} from "../../constants" import { NexusBootstrapAbi } from "../../constants/abi/NexusBootstrapAbi" /** @@ -56,11 +61,11 @@ export const getK1FactoryData = async ({ * @property {Address} bootStrapAddress - The address of the bootstrap contract */ export type GetMeeFactoryDataParams = GetK1FactoryDataParams & { - validatorAddress: Address - registryAddress: Address + validatorAddress?: Address + registryAddress?: Address publicClient: PublicClient walletClient: WalletClient - bootStrapAddress: Address + bootStrapAddress?: Address } /** @@ -69,13 +74,13 @@ export type GetMeeFactoryDataParams = GetK1FactoryDataParams & { * @returns {Promise} Encoded function data for account creation */ export const getMeeFactoryData = async ({ - validatorAddress, + validatorAddress = MEE_VALIDATOR_ADDRESS, attesters, - registryAddress, + registryAddress = REGISTRY_ADDRESS, attesterThreshold, publicClient, walletClient, - bootStrapAddress, + bootStrapAddress = NEXUS_BOOTSTRAP_ADDRESS, signerAddress, index }: GetMeeFactoryDataParams): Promise => { diff --git a/src/sdk/account/decorators/getNexusAddress.test.ts b/src/sdk/account/decorators/getNexusAddress.test.ts new file mode 100644 index 00000000..0274ebc3 --- /dev/null +++ b/src/sdk/account/decorators/getNexusAddress.test.ts @@ -0,0 +1,73 @@ +import { + http, + type Address, + type Chain, + type LocalAccount, + type PublicClient, + type WalletClient, + createPublicClient +} from "viem" +import { afterAll, beforeAll, describe, expect, test } from "vitest" +import { toNetwork } from "../../../test/testSetup" +import type { NetworkConfig } from "../../../test/testUtils" +import { + MAINNET_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS, + NEXUS_ACCOUNT_FACTORY +} from "../../constants" +import { getK1NexusAddress, getMeeNexusAddress } from "./getNexusAddress" + +describe("account.getNexusAddress", () => { + let network: NetworkConfig + let chain: Chain + let bundlerUrl: string + + // Test utils + let publicClient: PublicClient + let eoaAccount: LocalAccount + + beforeAll(async () => { + network = await toNetwork("TESTNET_FROM_ENV_VARS") + + chain = network.chain + bundlerUrl = network.bundlerUrl + eoaAccount = network.account! + publicClient = createPublicClient({ + chain, + transport: http(network.rpcUrl) + }) + }) + + test("should check k1 nexus address", async () => { + const customAttesters = [ + "0x1111111111111111111111111111111111111111" as Address, + "0x2222222222222222222222222222222222222222" as Address + ] + const customThreshold = 2 + const customIndex = 5n + + const k1AddressWithParams = await getK1NexusAddress({ + publicClient: publicClient as unknown as PublicClient, + signerAddress: eoaAccount.address, + attesters: customAttesters, + threshold: customThreshold, + index: customIndex + }) + + expect(k1AddressWithParams).toMatchInlineSnapshot( + `"0x93828A8f4405F112a65bf1732a4BE8f5B4C99322"` + ) + }) + + test("should check mee nexus address", async () => { + const index = 1n + + const meeAddress = await getMeeNexusAddress({ + publicClient: publicClient as unknown as PublicClient, + signerAddress: eoaAccount.address + }) + + expect(meeAddress).toMatchInlineSnapshot( + `"0x1968a6Ab4a542EB22e7452AC25381AE6c0f07826"` + ) + }) +}) diff --git a/src/sdk/account/utils/getCounterFactualAddress.ts b/src/sdk/account/decorators/getNexusAddress.ts similarity index 50% rename from src/sdk/account/utils/getCounterFactualAddress.ts rename to src/sdk/account/decorators/getNexusAddress.ts index f6a03b7a..e5ff80ef 100644 --- a/src/sdk/account/utils/getCounterFactualAddress.ts +++ b/src/sdk/account/decorators/getNexusAddress.ts @@ -2,8 +2,7 @@ import { type Address, pad, toHex } from "viem" import type { PublicClient } from "viem" import { MAINNET_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS, - MOCK_ATTESTER_ADDRESS, - NEXUS_BOOTSTRAP_ADDRESS, + NEXUS_ACCOUNT_FACTORY, RHINESTONE_ATTESTER_ADDRESS } from "../../constants" import { AccountFactoryAbi } from "../../constants/abi/AccountFactory" @@ -15,7 +14,6 @@ import { K1ValidatorFactoryAbi } from "../../constants/abi/K1ValidatorFactory" * @param publicClient - The public client to use for the read contract * @param signerAddress - The address of the signer * @param index - The index of the account - * @param isTestnet - Whether the network is testnet * @param attesters - The attesters to use * @param threshold - The threshold of the attesters * @param factoryAddress - The factory address to use @@ -27,38 +25,35 @@ import { K1ValidatorFactoryAbi } from "../../constants/abi/K1ValidatorFactory" * ``` */ -type K1CounterFactualAddressParams = { - /** The public client to use for the read contract */ - publicClient: PublicClient - /** The address of the signer */ - signerAddress: Address - /** Whether the network is testnet */ - isTestnet?: boolean - /** The index of the account */ - index?: bigint - /** The attesters to use */ - attesters?: Address[] - /** The threshold of the attesters */ - threshold?: number - /** The factory address to use. Defaults to the mainnet factory address */ - factoryAddress?: Address -} -export const getK1CounterFactualAddress = async ( - params: K1CounterFactualAddressParams +type K1CounterFactualAddressParams = + { + /** The public client to use for the read contract */ + publicClient: ExtendedPublicClient + /** The address of the signer */ + signerAddress: Address + /** The index of the account */ + index?: bigint + /** The attesters to use */ + attesters?: Address[] + /** The threshold of the attesters */ + threshold?: number + /** The factory address to use. Defaults to the mainnet factory address */ + factoryAddress?: Address + } +export const getK1NexusAddress = async < + ExtendedPublicClient extends PublicClient +>( + params: K1CounterFactualAddressParams ): Promise
=> { const { publicClient, signerAddress, - isTestnet = false, index = 0n, attesters = [RHINESTONE_ATTESTER_ADDRESS], threshold = 1, factoryAddress = MAINNET_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS } = params - if (isTestnet) { - attesters.push(MOCK_ATTESTER_ADDRESS) - } return await publicClient.readContract({ address: factoryAddress, abi: K1ValidatorFactoryAbi, @@ -67,30 +62,24 @@ export const getK1CounterFactualAddress = async ( }) } -type MeeCounterFactualAddressParams = { - /** The public client to use for the read contract */ - publicClient: PublicClient - /** The address of the signer */ - signerAddress: Address - /** The salt for the account */ - index: bigint - /** The factory address to use. Defaults to the mainnet factory address */ - factoryAddress?: Address -} -export const getMeeCounterFactualAddress = async ( - params: MeeCounterFactualAddressParams -) => { - console.log("getMeeCounterFactualAddress", params) +type MeeCounterFactualAddressParams = + { + /** The public client to use for the read contract */ + publicClient: ExtendedPublicClient + /** The address of the signer */ + signerAddress: Address + /** The salt for the account */ + index?: bigint + } - const salt = pad(toHex(params.index), { size: 32 }) - const { - publicClient, - signerAddress, - factoryAddress = NEXUS_BOOTSTRAP_ADDRESS - } = params +export const getMeeNexusAddress = async ( + params: MeeCounterFactualAddressParams +) => { + const salt = pad(toHex(params.index ?? 0n), { size: 32 }) + const { publicClient, signerAddress } = params return await publicClient.readContract({ - address: factoryAddress, + address: NEXUS_ACCOUNT_FACTORY, abi: AccountFactoryAbi, functionName: "computeAccountAddress", args: [signerAddress, salt] diff --git a/src/sdk/account/decorators/getUnifiedERC20Balance.test.ts b/src/sdk/account/decorators/getUnifiedERC20Balance.test.ts new file mode 100644 index 00000000..23f35d7e --- /dev/null +++ b/src/sdk/account/decorators/getUnifiedERC20Balance.test.ts @@ -0,0 +1,51 @@ +import type { Address, Chain, LocalAccount } from "viem" +import { base } from "viem/chains" +import { beforeAll, describe, expect, it } from "vitest" +import { toNetwork } from "../../../test/testSetup" +import type { NetworkConfig } from "../../../test/testUtils" +import { + type MultichainSmartAccount, + toMultichainNexusAccount +} from "../../account/toMultiChainNexusAccount" +import { type MeeClient, createMeeClient } from "../../clients/createMeeClient" +import { mcUSDC } from "../../constants/tokens" +import { getUnifiedERC20Balance } from "./getUnifiedERC20Balance" + +describe("mee:getUnifiedERC20Balance", () => { + let network: NetworkConfig + let eoaAccount: LocalAccount + let paymentChain: Chain + let paymentToken: Address + let mcNexus: MultichainSmartAccount + let meeClient: MeeClient + + beforeAll(async () => { + network = await toNetwork("MAINNET_FROM_ENV_VARS") + + paymentChain = network.chain + paymentToken = network.paymentToken! + eoaAccount = network.account! + + mcNexus = await toMultichainNexusAccount({ + chains: [base, paymentChain], + signer: eoaAccount + }) + + meeClient = createMeeClient({ account: mcNexus }) + }) + + it("should aggregate balances across chains correctly", async () => { + const unifiedBalance = await getUnifiedERC20Balance({ + account: mcNexus, + mcToken: mcUSDC + }) + + expect(unifiedBalance.balance).toBeGreaterThan(0n) + expect(unifiedBalance.breakdown).toHaveLength(2) + expect(unifiedBalance.decimals).toBe(6) + + expect(unifiedBalance.breakdown[0]).toHaveProperty("balance") + expect(unifiedBalance.breakdown[0]).toHaveProperty("decimals") + expect(unifiedBalance.breakdown[0]).toHaveProperty("chainId") + }) +}) diff --git a/src/sdk/account/decorators/getUnifiedERC20Balance.ts b/src/sdk/account/decorators/getUnifiedERC20Balance.ts new file mode 100644 index 00000000..1e41e97a --- /dev/null +++ b/src/sdk/account/decorators/getUnifiedERC20Balance.ts @@ -0,0 +1,110 @@ +import { erc20Abi, getContract } from "viem" +import type { BaseMultichainSmartAccount } from "../toMultiChainNexusAccount" +import type { MultichainContract } from "../utils/getMultichainContract" + +/** + * Represents a balance item with its decimal precision + */ +export type UnifiedBalanceItem = { + /** The token balance as a bigint */ + balance: bigint + /** Number of decimal places for the token */ + decimals: number +} + +export type RelevantBalance = UnifiedBalanceItem & { chainId: number } + +/** + * Represents a unified balance across multiple chains for an ERC20 token + */ +export type UnifiedERC20Balance = { + /** The multichain ERC20 token contract */ + token: MultichainContract + /** Individual balance breakdown per chain */ + breakdown: RelevantBalance[] +} & UnifiedBalanceItem + +export type GetUnifiedERC20BalanceParameters = { + /** The multichain ERC20 token contract */ + mcToken: MultichainContract + /** The multichain smart account to check balances for */ + account: BaseMultichainSmartAccount +} + +/** + * Fetches and aggregates ERC20 token balances across multiple chains for a given account + * + * @param parameters - The input parameters + * @param parameters.mcToken - The multichain ERC20 token contract + * @param parameters.deployments - The multichain smart account deployments to check balances for + * @returns A unified balance object containing the total balance and per-chain breakdown + * @throws Error if the account is not initialized on a chain or if token decimals mismatch across chains + * + * @example + * const balance = await getUnifiedERC20Balance(client, { + * mcToken: mcUSDC, + * deployments: mcNexus.deployments + * }) + */ +export async function getUnifiedERC20Balance( + parameters: GetUnifiedERC20BalanceParameters +): Promise { + const { mcToken, account: account_ } = parameters + + const relevantTokensByChain = Array.from(mcToken.deployments).filter( + ([chainId]) => { + return account_.deployments.some( + (account) => account.client.chain?.id === chainId + ) + } + ) + + const balances = await Promise.all( + relevantTokensByChain.map(async ([chainId, address]) => { + const account = account_.deployments.filter( + (account) => account.client.chain?.id === chainId + )[0] + const tokenContract = getContract({ + abi: erc20Abi, + address, + client: account.client + }) + const [balance, decimals] = await Promise.all([ + tokenContract.read.balanceOf([account.address]), + tokenContract.read.decimals() + ]) + + return { + balance, + decimals, + chainId + } + }) + ) + + return { + ...balances + .map((balance) => { + return { + balance: balance.balance, + decimals: balance.decimals + } + }) + .reduce((curr, acc) => { + if (curr.decimals !== acc.decimals) { + throw Error(` + Error while trying to fetch a unified ERC20 balance. The addresses provided + in the mapping don't have the same number of decimals across all chains. + The function can't fetch a unified balance for token mappings with differing + decimals. + `) + } + return { + balance: curr.balance + acc.balance, + decimals: curr.decimals + } + }), + breakdown: balances, + token: mcToken + } +} diff --git a/src/sdk/account/decorators/queryBridge.test.ts b/src/sdk/account/decorators/queryBridge.test.ts new file mode 100644 index 00000000..518c3c01 --- /dev/null +++ b/src/sdk/account/decorators/queryBridge.test.ts @@ -0,0 +1,63 @@ +import type { Address, Chain, LocalAccount } from "viem" +import { base } from "viem/chains" +import { beforeAll, describe, expect, it } from "vitest" +import { toNetwork } from "../../../test/testSetup" +import type { NetworkConfig } from "../../../test/testUtils" +import { type MeeClient, createMeeClient } from "../../clients/createMeeClient" +import { mcUSDC } from "../../constants/tokens" +import { + type MultichainSmartAccount, + toMultichainNexusAccount +} from "../toMultiChainNexusAccount" +import { AcrossPlugin } from "../utils/acrossPlugin" +import type { MultichainAddressMapping } from "./buildBridgeInstructions" +import { queryBridge } from "./queryBridge" + +describe("mee:queryBridge", () => { + let network: NetworkConfig + let eoaAccount: LocalAccount + let paymentChain: Chain + let paymentToken: Address + let mcNexus: MultichainSmartAccount + let meeClient: MeeClient + + beforeAll(async () => { + network = await toNetwork("MAINNET_FROM_ENV_VARS") + + paymentChain = network.chain + paymentToken = network.paymentToken! + eoaAccount = network.account! + + mcNexus = await toMultichainNexusAccount({ + chains: [base, paymentChain], + signer: eoaAccount + }) + + meeClient = createMeeClient({ account: mcNexus }) + }) + + it("should query the bridge", async () => { + const unifiedBalance = await mcNexus.getUnifiedERC20Balance(mcUSDC) + + const tokenMapping: MultichainAddressMapping = { + on: (chainId: number) => + unifiedBalance.token.deployments.get(chainId) || "0x", + deployments: Array.from( + unifiedBalance.token.deployments.entries(), + ([chainId, address]) => ({ chainId, address }) + ) + } + + const payload = await queryBridge({ + account: mcNexus, + amount: 18600927n, + toChain: base, + fromChain: paymentChain, + tokenMapping + }) + + expect(payload?.amount).toBeGreaterThan(0n) + expect(payload?.receivedAtDestination).toBeGreaterThan(0n) + expect(payload?.plugin).toBe(AcrossPlugin) + }) +}) diff --git a/src/sdk/account/decorators/queryBridge.ts b/src/sdk/account/decorators/queryBridge.ts new file mode 100644 index 00000000..1d5e14e1 --- /dev/null +++ b/src/sdk/account/decorators/queryBridge.ts @@ -0,0 +1,94 @@ +import type { Chain } from "viem" +import type { Instruction } from "../../clients/decorators/mee/getQuote" +import type { BaseMultichainSmartAccount } from "../toMultiChainNexusAccount" +import { AcrossPlugin } from "../utils/acrossPlugin" +import type { + BridgingPlugin, + MultichainAddressMapping +} from "./buildBridgeInstructions" + +/** + * Parameters for querying bridge operations + */ +export type QueryBridgeParams = { + /** Source chain for the bridge operation */ + fromChain: Chain + /** Destination chain for the bridge operation */ + toChain: Chain + /** OptionalPlugin implementation for the bridging operation */ + plugin?: BridgingPlugin + /** Amount to bridge in base units (wei) */ + amount: bigint + /** Multi-chain smart account configuration */ + account: BaseMultichainSmartAccount + /** Mapping of token addresses across chains */ + tokenMapping: MultichainAddressMapping +} + +/** + * Result of a bridge query including chain info + */ +export type BridgeQueryResult = { + /** ID of the source chain */ + fromChainId: number + /** Amount to bridge in base units (wei) */ + amount: bigint + /** Expected amount to receive at destination after fees */ + receivedAtDestination: bigint + /** Plugin implementation used for the bridging operation */ + plugin: BridgingPlugin + /** Resolved user operation for the bridge */ + userOp: Instruction + /** Expected duration of the bridging operation in milliseconds */ + bridgingDurationExpectedMs?: number +} + +/** + * Queries a bridge operation to determine expected outcomes and fees + * @param client - MEE client instance + * @param params - Bridge query parameters + * @returns Bridge query result or null if received amount cannot be determined + * @throws Error if bridge plugin does not return a received amount + * + * @example + * const result = await queryBridge({ + * fromChain, + * toChain, + * plugin, + * amount, + * account, + * tokenMapping + * }) + */ +export const queryBridge = async ( + params: QueryBridgeParams +): Promise => { + const { + account, + fromChain, + toChain, + plugin = AcrossPlugin, + amount, + tokenMapping + } = params + + const result = await plugin.encodeBridgeUserOp({ + fromChain, + toChain, + account, + tokenMapping, + bridgingAmount: amount + }) + + // Skip if bridge doesn't provide received amount + if (!result.receivedAtDestination) return null + + return { + fromChainId: fromChain.id, + amount, + receivedAtDestination: result.receivedAtDestination, + plugin, + userOp: result.userOp, + bridgingDurationExpectedMs: result.bridgingDurationExpectedMs + } +} diff --git a/src/sdk/account/toMultiChainNexusAccount.test.ts b/src/sdk/account/toMultiChainNexusAccount.test.ts new file mode 100644 index 00000000..7c181f25 --- /dev/null +++ b/src/sdk/account/toMultiChainNexusAccount.test.ts @@ -0,0 +1,137 @@ +import { + http, + type Address, + type Chain, + type LocalAccount, + isAddress, + isHex +} from "viem" +import { base, optimism } from "viem/chains" +import { baseSepolia } from "viem/chains" +import { beforeAll, describe, expect, test } from "vitest" +import { toNetwork } from "../../test/testSetup" +import type { NetworkConfig } from "../../test/testUtils" +import { MEE_VALIDATOR_ADDRESS, TEMP_MEE_ATTESTER_ADDR } from "../constants" +import { NEXUS_ACCOUNT_FACTORY } from "../constants" +import { mcUSDC } from "../constants/tokens" +import { MeeSmartAccount } from "../modules" +import { + type MultichainSmartAccount, + toMultichainNexusAccount +} from "./toMultiChainNexusAccount" +import { toNexusAccount } from "./toNexusAccount" + +describe("mee.toMultiChainNexusAccount", async () => { + let network: NetworkConfig + let eoaAccount: LocalAccount + let paymentChain: Chain + let paymentToken: Address + let mcNexus: MultichainSmartAccount + + beforeAll(async () => { + network = await toNetwork("MAINNET_FROM_ENV_VARS") + + paymentChain = network.chain + paymentToken = network.paymentToken! + eoaAccount = network.account! + + mcNexus = await toMultichainNexusAccount({ + chains: [base, paymentChain], + signer: eoaAccount + }) + }) + + test("should create multichain account with correct parameters", async () => { + mcNexus = await toMultichainNexusAccount({ + signer: eoaAccount, + chains: [base, optimism] + }) + + // Verify the structure of the returned object + expect(mcNexus).toHaveProperty("deployments") + expect(mcNexus).toHaveProperty("signer") + expect(mcNexus).toHaveProperty("deploymentOn") + expect(mcNexus.signer).toBe(eoaAccount) + expect(mcNexus.deployments).toHaveLength(2) + }) + + test("should return correct deployment for specific chain", async () => { + const deployment = mcNexus.deploymentOn(base.id) + expect(deployment).toBeDefined() + expect(deployment?.client?.chain?.id).toBe(base.id) + }) + + test("should handle empty chains array", async () => { + const multiChainAccount = await toMultichainNexusAccount({ + signer: eoaAccount, + chains: [] + }) + expect(multiChainAccount.deployments).toHaveLength(0) + }) + + test("should have configured accounts correctly", async () => { + expect(mcNexus.deployments.length).toEqual(2) + }) + + test("should sign message using MEE Compliant Nexus Account", async () => { + const nexus = await toNexusAccount({ + chain: baseSepolia, + signer: eoaAccount, + transport: http(), + validatorAddress: MEE_VALIDATOR_ADDRESS, + factoryAddress: NEXUS_ACCOUNT_FACTORY, + attesters: [TEMP_MEE_ATTESTER_ADDR] + }) + + expect(isAddress(nexus.address)).toBeTruthy() + + const signed = await nexus.signMessage({ message: { raw: "0xABC" } }) + expect(isHex(signed)).toBeTruthy() + }) + + test("should read usdc balance on mainnet", async () => { + const readAddress = mcNexus.deploymentOn(optimism.id)?.address + if (!readAddress) { + throw new Error("No address found for optimism") + } + const usdcBalanceOnChains = await mcUSDC.read({ + account: mcNexus, + functionName: "balanceOf", + args: [readAddress], + onChains: [base, optimism] + }) + + expect(usdcBalanceOnChains.length).toEqual(2) + }) + + test("mcNexus to have decorators successfully applied", async () => { + expect(mcNexus.getUnifiedERC20Balance).toBeInstanceOf(Function) + expect(mcNexus.buildBalanceInstructions).toBeInstanceOf(Function) + expect(mcNexus.buildBridgeInstructions).toBeInstanceOf(Function) + expect(mcNexus.queryBridge).toBeDefined() + }) + + test("should query bridge", async () => { + const unifiedBalance = await mcNexus.getUnifiedERC20Balance(mcUSDC) + + const tokenMapping = { + on: (chainId: number) => + unifiedBalance.token.deployments.get(chainId) || "0x", + deployments: Array.from( + unifiedBalance.token.deployments.entries(), + ([chainId, address]) => ({ chainId, address }) + ) + } + + const payload = await mcNexus.queryBridge({ + amount: 18600927n, + toChain: base, + fromChain: paymentChain, + tokenMapping, + account: mcNexus + }) + + expect(payload?.amount).toBeGreaterThan(0n) + expect(payload?.receivedAtDestination).toBeGreaterThan(0n) + }) +}) diff --git a/src/sdk/account/toMultiChainNexusAccount.ts b/src/sdk/account/toMultiChainNexusAccount.ts new file mode 100644 index 00000000..f617cced --- /dev/null +++ b/src/sdk/account/toMultiChainNexusAccount.ts @@ -0,0 +1,171 @@ +import { http, type Chain, type erc20Abi } from "viem" +import type { Instruction } from "../clients/decorators/mee/getQuote" +import { + MEE_VALIDATOR_ADDRESS, + NEXUS_ACCOUNT_FACTORY, + TEMP_MEE_ATTESTER_ADDR +} from "../constants" +import type { MeeSmartAccount } from "../modules/utils/Types" +import { toNexusAccount } from "./toNexusAccount" +import type { MultichainContract } from "./utils/getMultichainContract" +import type { Signer } from "./utils/toSigner" + +import { + type BuildBalanceInstructionParams, + buildBalanceInstructions as buildBalanceInstructionsDecorator +} from "./decorators/buildBalanceInstructions" +import { + type BridgingInstructions, + type BuildBridgeInstructionParams, + buildBridgeInstructions as buildBridgeInstructionsDecorator +} from "./decorators/buildBridgeInstructions" +import { + type UnifiedERC20Balance, + getUnifiedERC20Balance as getUnifiedERC20BalanceDecorator +} from "./decorators/getUnifiedERC20Balance" +import { + type BridgeQueryResult, + type QueryBridgeParams, + queryBridge as queryBridgeDecorator +} from "./decorators/queryBridge" +/** + * Parameters required to create a multichain Nexus account + */ +export type MultichainNexusParams = { + /** The signer instance used for account creation */ + signer: Signer + /** Array of chains where the account will be deployed */ + chains: Chain[] +} + +/** + * Represents a smart account deployed across multiple chains + */ +export type BaseMultichainSmartAccount = { + /** Array of minimal MEE smart account instances across different chains */ + deployments: MeeSmartAccount[] + /** The signer associated with this multichain account */ + signer: Signer + /** + * Function to retrieve deployment information for a specific chain + * @param chainId - The ID of the chain to query + * @returns The smart account deployment for the specified chain + * @throws Error if no deployment exists for the specified chain + */ + deploymentOn: (chainId: number) => MeeSmartAccount | undefined +} + +export type MultichainSmartAccount = BaseMultichainSmartAccount & { + /** + * Function to retrieve the unified ERC20 balance across all deployments + * @param mcToken - The multichain token to query + * @returns The unified ERC20 balance across all deployments + * @example + * const balance = await mcAccount.getUnifiedERC20Balance(mcUSDC) + */ + getUnifiedERC20Balance: ( + mcToken: MultichainContract + ) => Promise + /** + * Function to build instructions for bridging a token across all deployments + * @param params - The parameters for the balance requirement + * @returns Instructions for any required bridging operations + * @example + * const instructions = await mcAccount.buildBalanceInstructions({ + * amount: BigInt(1000), + * token: mcUSDC, + * chain: base + * }) + */ + buildBalanceInstructions: ( + params: Omit + ) => Promise + /** + * Function to build instructions for bridging a token across all deployments + * @param params - The parameters for the balance requirement + * @returns Instructions for any required bridging operations + * @example + * const instructions = await mcAccount.buildBridgeInstructions({ + * amount: BigInt(1000), + * token: mcUSDC, + * chain: base + * }) + */ + buildBridgeInstructions: ( + params: Omit + ) => Promise + /** + * Function to query the bridge + * @param params - The parameters for the bridge query + * @returns The bridge query result + * @example + * const result = await mcAccount.queryBridge({ + * amount: BigInt(1000), + * token: mcUSDC, + * chain: base + * }) + */ + queryBridge: (params: QueryBridgeParams) => Promise +} + +/** + * Creates a multichain Nexus account across specified chains + * @param parameters - Configuration parameters for multichain account creation + * @returns Promise resolving to a MultichainSmartAccount instance + */ +export async function toMultichainNexusAccount( + parameters: MultichainNexusParams +): Promise { + const { signer, chains } = parameters + + const deployments = await Promise.all( + chains.map((chain) => + toNexusAccount({ + chain, + signer, + transport: http(), + validatorAddress: MEE_VALIDATOR_ADDRESS, + factoryAddress: NEXUS_ACCOUNT_FACTORY, + attesters: [TEMP_MEE_ATTESTER_ADDR] + }) + ) + ) + + const deploymentOn = (chainId: number) => { + const deployment = deployments.find( + (dep) => dep.client.chain?.id === chainId + ) + return deployment + } + + const baseAccount = { + deployments, + signer, + deploymentOn + } + + const getUnifiedERC20Balance = ( + mcToken: MultichainContract + ) => { + return getUnifiedERC20BalanceDecorator({ mcToken, account: baseAccount }) + } + + const buildBalanceInstructions = ( + params: Omit + ) => buildBalanceInstructionsDecorator({ ...params, account: baseAccount }) + + const buildBridgeInstructions = ( + params: Omit + ) => buildBridgeInstructionsDecorator({ ...params, account: baseAccount }) + + const queryBridge = (params: QueryBridgeParams) => + queryBridgeDecorator({ ...params, account: baseAccount }) + + return { + ...baseAccount, + getUnifiedERC20Balance, + buildBalanceInstructions, + buildBridgeInstructions, + queryBridge + } +} diff --git a/src/sdk/account/toNexusAccount.addresses.test.ts b/src/sdk/account/toNexusAccount.addresses.test.ts index 0b3d3ab4..71d9ed82 100644 --- a/src/sdk/account/toNexusAccount.addresses.test.ts +++ b/src/sdk/account/toNexusAccount.addresses.test.ts @@ -35,7 +35,7 @@ import { TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS } from "../constants" import { type NexusAccount, toNexusAccount } from "./toNexusAccount" -import { getK1CounterFactualAddress } from "./utils" +import { getK1NexusAddress } from "./utils" describe("nexus.account.addresses", async () => { let network: NetworkConfig @@ -83,10 +83,9 @@ describe("nexus.account.addresses", async () => { test("should check account address", async () => { nexusAccountAddress = await nexusClient.account.getCounterFactualAddress() - const counterfactualAddressFromHelper = await getK1CounterFactualAddress({ + const counterfactualAddressFromHelper = await getK1NexusAddress({ publicClient: testClient as unknown as PublicClient, signerAddress: eoaAccount.address, - isTestnet: true, index: 0n, attesters: [RHINESTONE_ATTESTER_ADDRESS], threshold: 1, @@ -101,10 +100,9 @@ describe("nexus.account.addresses", async () => { test("should check addresses after fund and deploy", async () => { await fundAndDeployClients(testClient, [nexusClient]) - const counterfactualAddressFromHelper = await getK1CounterFactualAddress({ + const counterfactualAddressFromHelper = await getK1NexusAddress({ publicClient: testClient as unknown as PublicClient, signerAddress: eoaAccount.address, - isTestnet: true, index: 0n, attesters: [RHINESTONE_ATTESTER_ADDRESS], threshold: 1, diff --git a/src/sdk/account/toNexusAccount.ts b/src/sdk/account/toNexusAccount.ts index 70f90574..c4dd5800 100644 --- a/src/sdk/account/toNexusAccount.ts +++ b/src/sdk/account/toNexusAccount.ts @@ -55,14 +55,16 @@ import { // Constants import { EntrypointAbi } from "../constants/abi" import { - getK1CounterFactualAddress, - getMeeCounterFactualAddress -} from "./utils/getCounterFactualAddress" + getK1NexusAddress, + getMeeNexusAddress +} from "./decorators/getNexusAddress" // Modules import { toK1Validator } from "../modules/k1Validator/toK1Validator" import type { Module } from "../modules/utils/Types" +import { getK1FactoryData } from "./decorators/getFactoryData" +import { getMeeFactoryData } from "./decorators/getFactoryData" import { EXECUTE_BATCH, EXECUTE_SINGLE, @@ -81,8 +83,6 @@ import { isNullOrUndefined, typeToString } from "./utils/Utils" -import { getK1FactoryData } from "./utils/getFactoryData" -import { getMeeFactoryData } from "./utils/getFactoryData" import { type EthereumProvider, type Signer, toSigner } from "./utils/toSigner" /** @@ -227,10 +227,6 @@ export const toNexusAccount = async ( } }) - // Review: - // Todo: attesters can be added here to do one time setup upon deployment. - // chain?.testnet && attesters_.push(MOCK_ATTESTER_ADDRESS) - const factoryData = useMeeAccount ? await getMeeFactoryData({ signerAddress, @@ -278,16 +274,14 @@ export const toNexusAccount = async ( } const addressFromFactory = useMeeAccount - ? await getMeeCounterFactualAddress({ + ? await getMeeNexusAddress({ publicClient, signerAddress, - index, - factoryAddress + index }) - : await getK1CounterFactualAddress({ + : await getK1NexusAddress({ publicClient, signerAddress, - isTestnet: chain.testnet, index, attesters: attesters_, threshold: attesterThreshold, diff --git a/src/sdk/account/utils/acrossPlugin.ts b/src/sdk/account/utils/acrossPlugin.ts new file mode 100644 index 00000000..940f8bf7 --- /dev/null +++ b/src/sdk/account/utils/acrossPlugin.ts @@ -0,0 +1,150 @@ +import { type Address, parseAbi } from "abitype" + +import { encodeFunctionData, erc20Abi } from "viem" +import { createHttpClient } from "../../clients/createHttpClient" +import type { + AbstractCall, + Instruction +} from "../../clients/decorators/mee/getQuote" +import type { + BridgingPlugin, + BridgingPluginResult, + BridgingUserOpParams +} from "../decorators/buildBridgeInstructions" + +export interface AcrossRelayFeeResponse { + totalRelayFee: { + pct: string + total: string + } + relayerCapitalFee: { + pct: string + total: string + } + relayerGasFee: { + pct: string + total: string + } + lpFee: { + pct: string + total: string + } + timestamp: string + isAmountTooLow: boolean + quoteBlock: string + spokePoolAddress: Address + exclusiveRelayer: Address + exclusivityDeadline: string +} + +type AcrossSuggestedFeesParams = { + inputToken: Address + outputToken: Address + originChainId: number + destinationChainId: number + amount: bigint +} + +// Create HTTP client instance +const acrossClient = createHttpClient("https://app.across.to/api") + +const acrossGetSuggestedFees = async ({ + inputToken, + outputToken, + originChainId, + destinationChainId, + amount +}: AcrossSuggestedFeesParams): Promise => + acrossClient.request({ + path: "suggested-fees", + method: "GET", + params: { + inputToken, + outputToken, + originChainId: originChainId.toString(), + destinationChainId: destinationChainId.toString(), + amount: amount.toString() + } + }) + +export const acrossEncodeBridgingUserOp = async ( + params: BridgingUserOpParams +): Promise => { + const { bridgingAmount, fromChain, account, toChain, tokenMapping } = params + + const inputToken = tokenMapping.on(fromChain.id) + const outputToken = tokenMapping.on(toChain.id) + const depositor = account.deploymentOn(fromChain.id)?.address + const recipient = account.deploymentOn(toChain.id)?.address + + if (!depositor || !recipient) { + throw new Error("No depositor or recipient found") + } + + const suggestedFees = await acrossGetSuggestedFees({ + amount: bridgingAmount, + destinationChainId: toChain.id, + inputToken: inputToken, + outputToken: outputToken, + originChainId: fromChain.id + }) + + const depositV3abi = parseAbi([ + "function depositV3(address depositor, address recipient, address inputToken, address outputToken, uint256 inputAmount, uint256 outputAmount, uint256 destinationChainId, address exclusiveRelayer, uint32 quoteTimestamp, uint32 fillDeadline, uint32 exclusivityDeadline, bytes message) external" + ]) + + const outputAmount = + BigInt(bridgingAmount) - BigInt(suggestedFees.totalRelayFee.total) + + const fillDeadlineBuffer = 18000 + const fillDeadline = Math.round(Date.now() / 1000) + fillDeadlineBuffer + + const approveCall: AbstractCall = { + to: inputToken, + gasLimit: 100000n, + data: encodeFunctionData({ + abi: erc20Abi, + functionName: "approve", + args: [suggestedFees.spokePoolAddress, bridgingAmount] + }) + } + + const depositCall: AbstractCall = { + to: suggestedFees.spokePoolAddress, + gasLimit: 150000n, + data: encodeFunctionData({ + abi: depositV3abi, + args: [ + depositor, + recipient, + inputToken, + outputToken, + bridgingAmount, + outputAmount, + BigInt(toChain.id), + suggestedFees.exclusiveRelayer, + Number.parseInt(suggestedFees.timestamp), + fillDeadline, + Number.parseInt(suggestedFees.exclusivityDeadline), + "0x" // message + ] + }) + } + + const userOp: Instruction = { + calls: [approveCall, depositCall], + chainId: fromChain.id + } + + return { + userOp: userOp, + receivedAtDestination: outputAmount, + bridgingDurationExpectedMs: undefined + } +} + +export const AcrossPlugin: BridgingPlugin = { + encodeBridgeUserOp: async (params) => { + return await acrossEncodeBridgingUserOp(params) + } +} diff --git a/src/sdk/account/utils/explorer/explorer.test.ts b/src/sdk/account/utils/explorer.test.ts similarity index 79% rename from src/sdk/account/utils/explorer/explorer.test.ts rename to src/sdk/account/utils/explorer.test.ts index fb140e3a..a0feb19f 100644 --- a/src/sdk/account/utils/explorer/explorer.test.ts +++ b/src/sdk/account/utils/explorer.test.ts @@ -1,24 +1,21 @@ import type { Address, Chain, LocalAccount } from "viem" import { base, baseSepolia } from "viem/chains" -import { afterAll, beforeAll, describe, expect, test } from "vitest" -import { toNetwork } from "../../../../test/testSetup" -import type { NetworkConfig } from "../../../../test/testUtils" -import { - type MeeClient, - createMeeClient -} from "../../../clients/createMeeClient" +import { beforeAll, describe, expect, test } from "vitest" +import { toNetwork } from "../../../test/testSetup" +import type { NetworkConfig } from "../../../test/testUtils" +import { type MeeClient, createMeeClient } from "../../clients/createMeeClient" import { type MultichainSmartAccount, toMultichainNexusAccount } from "../toMultiChainNexusAccount" import { getExplorerTxLink, getJiffyScanLink, getMeeScanLink } from "./explorer" -describe("explorer", () => { +describe("mee.explorer", () => { let network: NetworkConfig let eoaAccount: LocalAccount let paymentChain: Chain let paymentToken: Address - let mcNexusMainnet: MultichainSmartAccount + let mcNexus: MultichainSmartAccount let meeClient: MeeClient beforeAll(async () => { @@ -28,12 +25,12 @@ describe("explorer", () => { paymentToken = network.paymentToken! eoaAccount = network.account! - mcNexusMainnet = await toMultichainNexusAccount({ + mcNexus = await toMultichainNexusAccount({ chains: [base, paymentChain], signer: eoaAccount }) - meeClient = createMeeClient({ account: mcNexusMainnet }) + meeClient = createMeeClient({ account: mcNexus }) }) test("should get a meescan url", () => { diff --git a/src/sdk/account/utils/explorer/explorer.ts b/src/sdk/account/utils/explorer.ts similarity index 93% rename from src/sdk/account/utils/explorer/explorer.ts rename to src/sdk/account/utils/explorer.ts index 3f450389..9d17596c 100644 --- a/src/sdk/account/utils/explorer/explorer.ts +++ b/src/sdk/account/utils/explorer.ts @@ -1,6 +1,6 @@ import type { Chain, Hex } from "viem" -import type { Url } from "../../../clients/createHttpClient" -import { getChain } from "../getChain" +import type { Url } from "../../clients/createHttpClient" +import { getChain } from "./getChain" /** * Get the explorer tx link diff --git a/src/sdk/account/utils/getMultichainContract.test.ts b/src/sdk/account/utils/getMultichainContract.test.ts new file mode 100644 index 00000000..e4577837 --- /dev/null +++ b/src/sdk/account/utils/getMultichainContract.test.ts @@ -0,0 +1,89 @@ +import { type Address, parseEther } from "viem" +import { base, optimism } from "viem/chains" +import { describe, expect, it } from "vitest" +import { getMultichainContract } from "./getMultichainContract" + +// Sample ERC20 ABI (minimal version for testing) +const erc20ABI = [ + { + type: "function", + name: "transfer", + inputs: [ + { name: "recipient", type: "address" }, + { name: "amount", type: "uint256" } + ], + outputs: [{ type: "bool" }], + stateMutability: "nonpayable" + }, + { + type: "function", + name: "balanceOf", + inputs: [{ name: "account", type: "address" }], + outputs: [{ type: "uint256" }], + stateMutability: "view" + } +] as const + +describe("mee:getMultichainContract", () => { + const mockDeployments: [Address, number][] = [ + ["0x1234567890123456789012345678901234567890", optimism.id], + ["0x0987654321098765432109876543210987654321", base.id] + ] + + const mockContract = getMultichainContract({ + abi: erc20ABI, + deployments: mockDeployments + }) + + it("should create a contract instance with correct deployments", () => { + expect(mockContract.deployments.get(optimism.id)).toBe( + mockDeployments[0][0] + ) + expect(mockContract.deployments.get(base.id)).toBe(mockDeployments[1][0]) + }) + + it("should return correct address for a chain", () => { + expect(mockContract.addressOn(optimism.id)).toBe(mockDeployments[0][0]) + expect(mockContract.addressOn(base.id)).toBe(mockDeployments[1][0]) + }) + + it("should throw error for non-existent chain deployment", () => { + expect(() => mockContract.addressOn(1)).toThrow( + "No deployment found for chain 1" + ) + expect(() => mockContract.on(1)).toThrow("No deployment found for chain 1") + }) + + it("should create valid transfer instructions", () => { + const recipient = "0x1111111111111111111111111111111111111111" + const amount = parseEther("1.0") + const gasLimit = 100000n + + const instruction = mockContract.on(optimism.id).transfer({ + args: [recipient, amount], + gasLimit + }) + + expect(instruction).toMatchObject({ + chainId: optimism.id, + calls: [ + { + to: mockDeployments[0][0], + gasLimit, + value: 0n, + data: expect.any(String) // We could decode this to verify if needed + } + ] + }) + }) + + it("should throw error for non-existent function", () => { + expect(() => { + // @ts-expect-error - Testing invalid function call + mockContract.on(optimism.id).nonExistentFunction({ + args: [], + gasLimit: 100000n + }) + }).toThrow("Function nonExistentFunction not found in ABI") + }) +}) diff --git a/src/sdk/account/utils/getMultichainContract.ts b/src/sdk/account/utils/getMultichainContract.ts index f1df0a3b..18fdda12 100644 --- a/src/sdk/account/utils/getMultichainContract.ts +++ b/src/sdk/account/utils/getMultichainContract.ts @@ -19,7 +19,7 @@ import type { AbstractCall, Instruction } from "../../clients/decorators/mee/getQuote" -import type { MultichainSmartAccount } from "./toMultiChainNexusAccount" +import type { MultichainSmartAccount } from "../toMultiChainNexusAccount" /** * Contract instance capable of encoding transactions across multiple chains * @template TAbi - The contract ABI type @@ -183,6 +183,9 @@ export function getMultichainContract(config: { } const deployment = params.account.deploymentOn(chain.id) + if (!deployment) { + throw new Error(`No deployment found for chain ${chain.id}`) + } const client = deployment.client as PublicClient const result = await client.readContract({ diff --git a/src/sdk/account/utils/index.ts b/src/sdk/account/utils/index.ts index 7ba34a0a..72f09178 100644 --- a/src/sdk/account/utils/index.ts +++ b/src/sdk/account/utils/index.ts @@ -4,4 +4,4 @@ export * from "./Constants.js" export * from "./getChain.js" export * from "./Logger.js" export * from "./toSigner.js" -export * from "./getCounterFactualAddress.js" +export * from "../decorators/getNexusAddress.js" diff --git a/src/sdk/account/utils/toMultiChainNexusAccount.test.ts b/src/sdk/account/utils/toMultiChainNexusAccount.test.ts deleted file mode 100644 index 0f90aa5c..00000000 --- a/src/sdk/account/utils/toMultiChainNexusAccount.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { - http, - type Chain, - type PrivateKeyAccount, - isAddress, - isHex -} from "viem" -import { base, optimism, optimismSepolia } from "viem/chains" -import { baseSepolia } from "viem/chains" -import { beforeAll, describe, expect, test } from "vitest" -import { toNetwork } from "../../../test/testSetup" -import type { NetworkConfig } from "../../../test/testUtils" -import { MEE_VALIDATOR_ADDRESS, TEMP_MEE_ATTESTER_ADDR } from "../../constants" -import { NEXUS_ACCOUNT_FACTORY } from "../../constants" -import { mcUSDC } from "../../constants/tokens" -import { toNexusAccount } from "../toNexusAccount" -import { - type MultichainSmartAccount, - toMultichainNexusAccount -} from "./toMultiChainNexusAccount" - -describe("mee.toMultiChainNexusAccount", async () => { - let network: NetworkConfig - let chain: Chain - let bundlerUrl: string - - let eoaAccount: PrivateKeyAccount - let mcNexusTestnet: MultichainSmartAccount - let mcNexusMainnet: MultichainSmartAccount - - beforeAll(async () => { - network = await toNetwork("TESTNET_FROM_ENV_VARS") - - chain = network.chain - bundlerUrl = network.bundlerUrl - eoaAccount = network.account! - }) - - test("should create multichain account with correct parameters", async () => { - mcNexusTestnet = await toMultichainNexusAccount({ - signer: eoaAccount, - chains: [baseSepolia, optimismSepolia] - }) - - mcNexusMainnet = await toMultichainNexusAccount({ - signer: eoaAccount, - chains: [base, optimism] - }) - - // Verify the structure of the returned object - expect(mcNexusMainnet).toHaveProperty("deployments") - expect(mcNexusMainnet).toHaveProperty("signer") - expect(mcNexusMainnet).toHaveProperty("deploymentOn") - expect(mcNexusMainnet.signer).toBe(eoaAccount) - expect(mcNexusMainnet.deployments).toHaveLength(2) - - expect(mcNexusTestnet.deployments).toHaveLength(2) - expect(mcNexusTestnet.signer).toBe(eoaAccount) - expect(mcNexusTestnet.deployments).toHaveLength(2) - }) - - test("should return correct deployment for specific chain", async () => { - const deployment = mcNexusTestnet.deploymentOn(baseSepolia.id) - expect(deployment).toBeDefined() - expect(deployment.client.chain?.id).toBe(baseSepolia.id) - }) - - test("should throw error for non-existent chain deployment", async () => { - expect(() => mcNexusTestnet.deploymentOn(999)).toThrow( - "No account deployment for chainId: 999" - ) - }) - - test("should handle empty chains array", async () => { - const multiChainAccount = await toMultichainNexusAccount({ - signer: eoaAccount, - chains: [] - }) - expect(multiChainAccount.deployments).toHaveLength(0) - }) - - test("should have configured accounts correctly", async () => { - expect(mcNexusMainnet.deployments.length).toEqual(2) - expect(mcNexusTestnet.deployments.length).toEqual(2) - expect(mcNexusTestnet.deploymentOn(baseSepolia.id).address).toEqual( - mcNexusTestnet.deploymentOn(baseSepolia.id).address - ) - }) - - test("should sign message using MEE Compliant Nexus Account", async () => { - const nexus = await toNexusAccount({ - chain: baseSepolia, - signer: eoaAccount, - transport: http(), - validatorAddress: MEE_VALIDATOR_ADDRESS, - factoryAddress: NEXUS_ACCOUNT_FACTORY, - attesters: [TEMP_MEE_ATTESTER_ADDR] - }) - - expect(isAddress(nexus.address)).toBeTruthy() - - const signed = await nexus.signMessage({ message: { raw: "0xABC" } }) - expect(isHex(signed)).toBeTruthy() - }) - - test("should read usdc balance on mainnet", async () => { - const readAddress = mcNexusMainnet.deploymentOn(optimism.id).address - const usdcBalanceOnChains = await mcUSDC.read({ - account: mcNexusMainnet, - functionName: "balanceOf", - args: [readAddress], - onChains: [base, optimism] - }) - - expect(usdcBalanceOnChains.length).toEqual(2) - }) -}) diff --git a/src/sdk/account/utils/toMultiChainNexusAccount.ts b/src/sdk/account/utils/toMultiChainNexusAccount.ts deleted file mode 100644 index 7afc5d62..00000000 --- a/src/sdk/account/utils/toMultiChainNexusAccount.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { http, type Chain } from "viem" -import { - MEE_VALIDATOR_ADDRESS, - NEXUS_ACCOUNT_FACTORY, - TEMP_MEE_ATTESTER_ADDR -} from "../../constants" -import type { MinimalMEESmartAccount } from "../../modules/utils/Types" -import { toNexusAccount } from "../toNexusAccount" -import type { Signer } from "./toSigner" - -/** - * Parameters required to create a multichain Nexus account - */ -export type MultichainNexusParams = { - /** The signer instance used for account creation */ - signer: Signer - /** Array of chains where the account will be deployed */ - chains: Chain[] -} - -/** - * Represents a smart account deployed across multiple chains - */ -export type MultichainSmartAccount = { - /** Array of minimal MEE smart account instances across different chains */ - deployments: MinimalMEESmartAccount[] - /** The signer associated with this multichain account */ - signer: Signer - /** - * Function to retrieve deployment information for a specific chain - * @param chainId - The ID of the chain to query - * @returns The smart account deployment for the specified chain - * @throws Error if no deployment exists for the specified chain - */ - deploymentOn: (chainId: number) => MinimalMEESmartAccount -} - -/** - * Creates a multichain Nexus account across specified chains - * @param parameters - Configuration parameters for multichain account creation - * @returns Promise resolving to a MultichainSmartAccount instance - */ -export async function toMultichainNexusAccount( - parameters: MultichainNexusParams -): Promise { - const { signer, chains } = parameters - - const deployments = await Promise.all( - chains.map((chain) => - toNexusAccount({ - chain, - signer, - transport: http(), - validatorAddress: MEE_VALIDATOR_ADDRESS, - factoryAddress: NEXUS_ACCOUNT_FACTORY, - attesters: [TEMP_MEE_ATTESTER_ADDR] - }) - ) - ) - - const deploymentOn = (chainId: number) => { - const deployment = deployments.find( - (dep) => dep.client.chain?.id === chainId - ) - if (!deployment) { - throw Error(`No account deployment for chainId: ${chainId}`) - } - return deployment - } - - return { - deployments, - signer, - deploymentOn - } -} diff --git a/src/sdk/clients/createHttpClient.test.ts b/src/sdk/clients/createHttpClient.test.ts index a427bac1..395be01d 100644 --- a/src/sdk/clients/createHttpClient.test.ts +++ b/src/sdk/clients/createHttpClient.test.ts @@ -6,16 +6,16 @@ import type { NetworkConfig } from "../../test/testUtils" import { type MultichainSmartAccount, toMultichainNexusAccount -} from "../account/utils/toMultiChainNexusAccount" +} from "../account/toMultiChainNexusAccount" import createHttpClient from "./createHttpClient" import { type MeeClient, createMeeClient } from "./createMeeClient" -describe("mee:createHttp Client", async () => { +describe("mee.createHttp Client", async () => { let network: NetworkConfig let eoaAccount: LocalAccount let paymentChain: Chain let paymentToken: Address - let mcNexusMainnet: MultichainSmartAccount + let mcNexus: MultichainSmartAccount let meeClient: MeeClient beforeAll(async () => { @@ -25,12 +25,12 @@ describe("mee:createHttp Client", async () => { paymentToken = network.paymentToken! eoaAccount = network.account! - mcNexusMainnet = await toMultichainNexusAccount({ + mcNexus = await toMultichainNexusAccount({ chains: [base, paymentChain], signer: eoaAccount }) - meeClient = createMeeClient({ account: mcNexusMainnet }) + meeClient = createMeeClient({ account: mcNexus }) }) test("should instantiate a client", async () => { diff --git a/src/sdk/clients/createHttpClient.ts b/src/sdk/clients/createHttpClient.ts index eb11daa9..989ee22a 100644 --- a/src/sdk/clients/createHttpClient.ts +++ b/src/sdk/clients/createHttpClient.ts @@ -16,6 +16,8 @@ type RequestParams = { method?: "GET" | "POST" /** Optional request body */ body?: object + /** Optional request params */ + params?: Record } /** @@ -50,9 +52,10 @@ type Extended = Prettify< * @returns A base Http client instance that can be extended with additional functionality */ export const createHttpClient = (url: Url): HttpClient => { - const request = async (params: RequestParams) => { - const { path, method = "POST", body } = params - const result = await fetch(`${url}/${path}`, { + const request = async (requesParams: RequestParams) => { + const { path, method = "POST", body, params } = requesParams + const urlParams = params ? `?${new URLSearchParams(params)}` : "" + const result = await fetch(`${url}/${path}${urlParams}`, { method, headers: { "Content-Type": "application/json" diff --git a/src/sdk/clients/createMeeClient.test.ts b/src/sdk/clients/createMeeClient.test.ts index 178e3e62..6749aa10 100644 --- a/src/sdk/clients/createMeeClient.test.ts +++ b/src/sdk/clients/createMeeClient.test.ts @@ -1,10 +1,4 @@ -import { - type Address, - type Chain, - type LocalAccount, - erc20Abi, - isHex -} from "viem" +import { type Address, type Chain, type LocalAccount, isHex } from "viem" import { base } from "viem/chains" import { beforeAll, describe, expect, inject, test } from "vitest" import { toNetwork } from "../../test/testSetup" @@ -12,18 +6,19 @@ import type { NetworkConfig } from "../../test/testUtils" import { type MultichainSmartAccount, toMultichainNexusAccount -} from "../account/utils/toMultiChainNexusAccount" +} from "../account/toMultiChainNexusAccount" import { type MeeClient, createMeeClient } from "./createMeeClient" import type { Instruction } from "./decorators/mee" +// @ts-ignore const { runPaidTests } = inject("settings") -describe("mee:createMeeClient", async () => { +describe("mee.createMeeClient", async () => { let network: NetworkConfig let eoaAccount: LocalAccount let paymentChain: Chain let paymentToken: Address - let mcNexusMainnet: MultichainSmartAccount + let mcNexus: MultichainSmartAccount let meeClient: MeeClient beforeAll(async () => { @@ -33,35 +28,16 @@ describe("mee:createMeeClient", async () => { paymentToken = network.paymentToken! eoaAccount = network.account! - mcNexusMainnet = await toMultichainNexusAccount({ + mcNexus = await toMultichainNexusAccount({ chains: [base, paymentChain], signer: eoaAccount }) - meeClient = createMeeClient({ account: mcNexusMainnet }) - }) - - test("should instantiate a client", async () => { - const meeClient = createMeeClient({ account: mcNexusMainnet }) - expect(meeClient).toBeDefined() - expect(meeClient.request).toBeDefined() - expect(Object.keys(meeClient)).toContain("request") - expect(Object.keys(meeClient)).toContain("account") - expect(Object.keys(meeClient)).toContain("getQuote") - }) - - test("should extend meeClient with decorators", () => { - expect(meeClient).toBeDefined() - expect(meeClient.getQuote).toBeDefined() - expect(meeClient.request).toBeDefined() - expect(meeClient.account).toBeDefined() - expect(meeClient.getQuote).toBeDefined() - expect(meeClient.signQuote).toBeDefined() - expect(meeClient.executeSignedQuote).toBeDefined() + meeClient = createMeeClient({ account: mcNexus }) }) test("should get a quote", async () => { - const meeClient = createMeeClient({ account: mcNexusMainnet }) + const meeClient = createMeeClient({ account: mcNexus }) const quote = await meeClient.getQuote({ instructions: [], @@ -73,7 +49,7 @@ describe("mee:createMeeClient", async () => { expect(quote).toBeDefined() expect(quote.paymentInfo.sender).toEqual( - mcNexusMainnet.deploymentOn(paymentChain.id).address + mcNexus.deploymentOn(paymentChain.id)?.address ) expect(quote.paymentInfo.token).toEqual(paymentToken) expect(+quote.paymentInfo.chainId).toEqual(paymentChain.id) @@ -136,8 +112,8 @@ describe("mee:createMeeClient", async () => { }) test("should demo the devEx of preparing instructions", async () => { - // These can be any 'Instruction', or any helper method that resolves to a 'ResolvedInstruction', - // including 'requireErc20Balance'. They all are resolved in the 'getQuote' method under the hood. + // These can be any 'Instruction', or any helper method that resolves to a 'Instruction', + // including 'buildBalanceInstruction'. They all are resolved in the 'getQuote' method under the hood. const preparedInstructions: Instruction[] = [ { calls: [ @@ -149,17 +125,7 @@ describe("mee:createMeeClient", async () => { ], chainId: 8453 }, - () => ({ - calls: [ - { - to: "0x0000000000000000000000000000000000000000", - gasLimit: 50000n, - value: 0n - } - ], - chainId: 8453 - }), - Promise.resolve({ + { calls: [ { to: "0x0000000000000000000000000000000000000000", @@ -168,29 +134,7 @@ describe("mee:createMeeClient", async () => { } ], chainId: 8453 - }), - () => [ - { - calls: [ - { - to: "0x0000000000000000000000000000000000000000", - gasLimit: 50000n, - value: 0n - } - ], - chainId: 8453 - }, - { - calls: [ - { - to: "0x0000000000000000000000000000000000000000", - gasLimit: 50000n, - value: 0n - } - ], - chainId: 8453 - } - ] + } ] expect(preparedInstructions).toBeDefined() @@ -203,10 +147,10 @@ describe("mee:createMeeClient", async () => { } }) - expect(quote.userOps.length).toEqual(6) + expect(quote.userOps.length).toEqual(3) expect(quote).toBeDefined() expect(quote.paymentInfo.sender).toEqual( - mcNexusMainnet.deploymentOn(paymentChain.id).address + mcNexus.deploymentOn(paymentChain.id)?.address ) expect(quote.paymentInfo.token).toEqual(paymentToken) expect(+quote.paymentInfo.chainId).toEqual(paymentChain.id) diff --git a/src/sdk/clients/createMeeClient.ts b/src/sdk/clients/createMeeClient.ts index 35ae284d..76d2d722 100644 --- a/src/sdk/clients/createMeeClient.ts +++ b/src/sdk/clients/createMeeClient.ts @@ -1,6 +1,6 @@ import type { Prettify } from "viem" +import type { MultichainSmartAccount } from "../account/toMultiChainNexusAccount" import { inProduction } from "../account/utils/Utils" -import type { MultichainSmartAccount } from "../account/utils/toMultiChainNexusAccount" import createHttpClient, { type HttpClient, type Url } from "./createHttpClient" import { meeActions } from "./decorators/mee" diff --git a/src/sdk/clients/decorators/mee/execute.test.ts b/src/sdk/clients/decorators/mee/execute.test.ts index 76d6d651..4856df63 100644 --- a/src/sdk/clients/decorators/mee/execute.test.ts +++ b/src/sdk/clients/decorators/mee/execute.test.ts @@ -6,19 +6,19 @@ import type { NetworkConfig } from "../../../../test/testUtils" import { type MultichainSmartAccount, toMultichainNexusAccount -} from "../../../account/utils/toMultiChainNexusAccount" +} from "../../../account/toMultiChainNexusAccount" import { type MeeClient, createMeeClient } from "../../createMeeClient" import { execute } from "./execute" import type { Instruction } from "./getQuote" vi.mock("./execute") -describe("mee:execute", () => { +describe("mee.execute", () => { let network: NetworkConfig let eoaAccount: LocalAccount let paymentChain: Chain let paymentToken: Address - let mcNexusMainnet: MultichainSmartAccount + let mcNexus: MultichainSmartAccount let meeClient: MeeClient beforeAll(async () => { @@ -28,12 +28,12 @@ describe("mee:execute", () => { paymentToken = network.paymentToken! eoaAccount = network.account! - mcNexusMainnet = await toMultichainNexusAccount({ + mcNexus = await toMultichainNexusAccount({ chains: [base, paymentChain], signer: eoaAccount }) - meeClient = createMeeClient({ account: mcNexusMainnet }) + meeClient = createMeeClient({ account: mcNexus }) }) test("should execute a quote using execute", async () => { diff --git a/src/sdk/clients/decorators/mee/executeQuote.test.ts b/src/sdk/clients/decorators/mee/executeQuote.test.ts index 47a3db1e..b5e6605d 100644 --- a/src/sdk/clients/decorators/mee/executeQuote.test.ts +++ b/src/sdk/clients/decorators/mee/executeQuote.test.ts @@ -6,7 +6,7 @@ import type { NetworkConfig } from "../../../../test/testUtils" import { type MultichainSmartAccount, toMultichainNexusAccount -} from "../../../account/utils/toMultiChainNexusAccount" +} from "../../../account/toMultiChainNexusAccount" import { type MeeClient, createMeeClient } from "../../createMeeClient" import executeQuote from "./executeQuote" import type { ExecuteSignedQuotePayload } from "./executeSignedQuote" @@ -14,12 +14,12 @@ import { type Instruction, getQuote } from "./getQuote" vi.mock("./executeQuote") -describe("mee:executeQuote", () => { +describe("mee.executeQuote", () => { let network: NetworkConfig let eoaAccount: LocalAccount let paymentChain: Chain let paymentToken: Address - let mcNexusMainnet: MultichainSmartAccount + let mcNexus: MultichainSmartAccount let meeClient: MeeClient beforeAll(async () => { @@ -29,12 +29,12 @@ describe("mee:executeQuote", () => { paymentToken = network.paymentToken! eoaAccount = network.account! - mcNexusMainnet = await toMultichainNexusAccount({ + mcNexus = await toMultichainNexusAccount({ chains: [base, paymentChain], signer: eoaAccount }) - meeClient = createMeeClient({ account: mcNexusMainnet }) + meeClient = createMeeClient({ account: mcNexus }) }) test("should execute a quote using", async () => { diff --git a/src/sdk/clients/decorators/mee/executeSignedQuote.test.ts b/src/sdk/clients/decorators/mee/executeSignedQuote.test.ts index 06458656..bc8a1dfd 100644 --- a/src/sdk/clients/decorators/mee/executeSignedQuote.test.ts +++ b/src/sdk/clients/decorators/mee/executeSignedQuote.test.ts @@ -3,20 +3,20 @@ import { base } from "viem/chains" import { afterAll, beforeAll, describe, expect, test, vi } from "vitest" import { toNetwork } from "../../../../test/testSetup" import type { NetworkConfig } from "../../../../test/testUtils" -import type { MultichainSmartAccount } from "../../../account/utils/toMultiChainNexusAccount" -import { toMultichainNexusAccount } from "../../../account/utils/toMultiChainNexusAccount" +import type { MultichainSmartAccount } from "../../../account/toMultiChainNexusAccount" +import { toMultichainNexusAccount } from "../../../account/toMultiChainNexusAccount" import { type MeeClient, createMeeClient } from "../../createMeeClient" import { executeSignedQuote } from "./executeSignedQuote" import type { Instruction } from "./getQuote" import { signQuote } from "./signQuote" vi.mock("./executeSignedQuote") -describe("mee:executeSignedQuote", () => { +describe("mee.executeSignedQuote", () => { let network: NetworkConfig let eoaAccount: LocalAccount let paymentChain: Chain let paymentToken: Address - let mcNexusMainnet: MultichainSmartAccount + let mcNexus: MultichainSmartAccount let meeClient: MeeClient beforeAll(async () => { @@ -26,12 +26,12 @@ describe("mee:executeSignedQuote", () => { paymentToken = network.paymentToken! eoaAccount = network.account! - mcNexusMainnet = await toMultichainNexusAccount({ + mcNexus = await toMultichainNexusAccount({ chains: [base, paymentChain], signer: eoaAccount }) - meeClient = createMeeClient({ account: mcNexusMainnet }) + meeClient = createMeeClient({ account: mcNexus }) }) test("should execute a quote using executeSignedQuote", async () => { diff --git a/src/sdk/clients/decorators/mee/getQuote.test.ts b/src/sdk/clients/decorators/mee/getQuote.test.ts index 3c13b6f4..5f14531c 100644 --- a/src/sdk/clients/decorators/mee/getQuote.test.ts +++ b/src/sdk/clients/decorators/mee/getQuote.test.ts @@ -3,17 +3,17 @@ import { base } from "viem/chains" import { beforeAll, describe, expect, test } from "vitest" import { toNetwork } from "../../../../test/testSetup" import type { NetworkConfig } from "../../../../test/testUtils" -import type { MultichainSmartAccount } from "../../../account/utils/toMultiChainNexusAccount" -import { toMultichainNexusAccount } from "../../../account/utils/toMultiChainNexusAccount" +import type { MultichainSmartAccount } from "../../../account/toMultiChainNexusAccount" +import { toMultichainNexusAccount } from "../../../account/toMultiChainNexusAccount" import { type MeeClient, createMeeClient } from "../../createMeeClient" import { type Instruction, getQuote } from "./getQuote" -describe("mee:getQuote", () => { +describe("mee.getQuote", () => { let network: NetworkConfig let eoaAccount: LocalAccount let paymentChain: Chain let paymentToken: Address - let mcNexusMainnet: MultichainSmartAccount + let mcNexus: MultichainSmartAccount let meeClient: MeeClient beforeAll(async () => { @@ -23,12 +23,12 @@ describe("mee:getQuote", () => { paymentToken = network.paymentToken! eoaAccount = network.account! - mcNexusMainnet = await toMultichainNexusAccount({ + mcNexus = await toMultichainNexusAccount({ chains: [base, paymentChain], signer: eoaAccount }) - meeClient = createMeeClient({ account: mcNexusMainnet }) + meeClient = createMeeClient({ account: mcNexus }) }) test("should resolve instructions", async () => { @@ -43,17 +43,7 @@ describe("mee:getQuote", () => { ], chainId: 8453 }, - () => ({ - calls: [ - { - to: "0x0000000000000000000000000000000000000000", - gasLimit: 50000n, - value: 0n - } - ], - chainId: 8453 - }), - Promise.resolve({ + { calls: [ { to: "0x0000000000000000000000000000000000000000", @@ -62,11 +52,11 @@ describe("mee:getQuote", () => { } ], chainId: 8453 - }) + } ] expect(instructions).toBeDefined() - expect(instructions.length).toEqual(3) + expect(instructions.length).toEqual(2) const quote = await getQuote(meeClient, { instructions: instructions, diff --git a/src/sdk/clients/decorators/mee/getQuote.ts b/src/sdk/clients/decorators/mee/getQuote.ts index 38dae91f..43f77541 100644 --- a/src/sdk/clients/decorators/mee/getQuote.ts +++ b/src/sdk/clients/decorators/mee/getQuote.ts @@ -1,6 +1,5 @@ import type { Address, Hex, OneOf } from "viem" -import type { MultichainSmartAccount } from "../../../account/utils/toMultiChainNexusAccount" -import type { AnyData } from "../../../modules/utils/Types" +import type { MultichainSmartAccount } from "../../../account/toMultiChainNexusAccount" import type { BaseMeeClient } from "../../createMeeClient" /** @@ -31,25 +30,13 @@ export type FeeTokenInfo = { * Information about the instructions to be executed in the transaction * @internal */ -export type InstructionResolved = { +export type Instruction = { /** Array of abstract calls to be executed in the transaction */ calls: AbstractCall[] /** Chain ID where the transaction will be executed */ chainId: number } -/** - * Represents an instruction to be executed in the transaction - * @type Instruction - */ -export type Instruction = - | InstructionResolved - | InstructionResolved[] - | ((x?: AnyData) => InstructionResolved) - | ((x?: AnyData) => InstructionResolved[]) - | Promise - | Promise - /** * Represents a supertransaction, which is a collection of instructions to be executed in a single transaction * @type SuperTransaction @@ -212,13 +199,7 @@ export const getQuote = async ( ): Promise => { const { account: account_ = client.account, instructions, feeToken } = params - const resolvedInstructions = (await Promise.all( - instructions.flatMap((userOp) => - typeof userOp === "function" ? userOp(client) : userOp - ) - )) as InstructionResolved[] - - const validUserOps = resolvedInstructions.every((userOp) => + const validUserOps = instructions.every((userOp) => account_.deploymentOn(userOp.chainId) ) const validPaymentAccount = account_.deploymentOn(feeToken.chainId) @@ -227,24 +208,37 @@ export const getQuote = async ( } const userOpResults = await Promise.all( - resolvedInstructions.map((userOp) => { + instructions.map((userOp) => { const deployment = account_.deploymentOn(userOp.chainId) - return Promise.all([ - deployment.encodeExecuteBatch(userOp.calls), - deployment.getNonce(), - deployment.isDeployed(), - deployment.getInitCode(), - deployment.address, - userOp.calls - .map((tx) => tx.gasLimit) - .reduce((curr, acc) => curr + acc) - .toString(), - userOp.chainId.toString() - ]) + if (deployment) { + return Promise.all([ + deployment.encodeExecuteBatch(userOp.calls), + deployment.getNonce(), + deployment.isDeployed(), + deployment.getInitCode(), + deployment.address, + userOp.calls + .map((tx) => tx.gasLimit) + .reduce((curr, acc) => curr + acc) + .toString(), + userOp.chainId.toString() + ]) + } + return null }) ) - const userOps = userOpResults.map( + const validUserOpResults = userOpResults.filter(Boolean) as [ + Hex, + bigint, + boolean, + Hex, + Address, + string, + string + ][] + + const userOps = validUserOpResults.map( ([ callData, nonce_, diff --git a/src/sdk/clients/decorators/mee/signFusionQuote.test.ts b/src/sdk/clients/decorators/mee/signFusionQuote.test.ts index e74dc016..dbf46d9b 100644 --- a/src/sdk/clients/decorators/mee/signFusionQuote.test.ts +++ b/src/sdk/clients/decorators/mee/signFusionQuote.test.ts @@ -6,7 +6,7 @@ import type { NetworkConfig } from "../../../../test/testUtils" import { type MultichainSmartAccount, toMultichainNexusAccount -} from "../../../account/utils/toMultiChainNexusAccount" +} from "../../../account/toMultiChainNexusAccount" import { type MeeClient, createMeeClient } from "../../createMeeClient" import executeSignedFusionQuote, { type ExecuteSignedFusionQuotePayload @@ -16,12 +16,12 @@ import { signFusionQuote } from "./signFusionQuote" const { runPaidTests } = inject("settings") -describe.runIf(runPaidTests).skip("mee:signFusionQuote", () => { +describe.runIf(runPaidTests).skip("mee.signFusionQuote", () => { let network: NetworkConfig let eoaAccount: LocalAccount let paymentChain: Chain let paymentToken: Address - let mcNexusMainnet: MultichainSmartAccount + let mcNexus: MultichainSmartAccount let meeClient: MeeClient beforeAll(async () => { @@ -31,12 +31,12 @@ describe.runIf(runPaidTests).skip("mee:signFusionQuote", () => { paymentToken = network.paymentToken! eoaAccount = network.account! - mcNexusMainnet = await toMultichainNexusAccount({ + mcNexus = await toMultichainNexusAccount({ chains: [base, paymentChain], signer: eoaAccount }) - meeClient = createMeeClient({ account: mcNexusMainnet }) + meeClient = createMeeClient({ account: mcNexus }) }) test("should execute a quote using executeSignedFusionQuote", async () => { diff --git a/src/sdk/clients/decorators/mee/signFusionQuote.ts b/src/sdk/clients/decorators/mee/signFusionQuote.ts index 117338dd..8e35b478 100644 --- a/src/sdk/clients/decorators/mee/signFusionQuote.ts +++ b/src/sdk/clients/decorators/mee/signFusionQuote.ts @@ -8,8 +8,8 @@ import { encodeAbiParameters, publicActions } from "viem" +import type { MultichainSmartAccount } from "../../../account/toMultiChainNexusAccount" import type { Call } from "../../../account/utils/Types" -import type { MultichainSmartAccount } from "../../../account/utils/toMultiChainNexusAccount" import type { BaseMeeClient } from "../../createMeeClient" import type { GetQuotePayload } from "./getQuote" import { type ExecutionMode, PREFIX } from "./signQuote" diff --git a/src/sdk/clients/decorators/mee/signQuote.test.ts b/src/sdk/clients/decorators/mee/signQuote.test.ts index fb1e0803..9249e4e6 100644 --- a/src/sdk/clients/decorators/mee/signQuote.test.ts +++ b/src/sdk/clients/decorators/mee/signQuote.test.ts @@ -6,17 +6,17 @@ import type { NetworkConfig } from "../../../../test/testUtils" import { type MultichainSmartAccount, toMultichainNexusAccount -} from "../../../account/utils/toMultiChainNexusAccount" +} from "../../../account/toMultiChainNexusAccount" import { type MeeClient, createMeeClient } from "../../createMeeClient" import type { Instruction } from "./getQuote" import { signQuote } from "./signQuote" -describe("mee:signQuote", () => { +describe("mee.signQuote", () => { let network: NetworkConfig let eoaAccount: LocalAccount let paymentChain: Chain let paymentToken: Address - let mcNexusMainnet: MultichainSmartAccount + let mcNexus: MultichainSmartAccount let meeClient: MeeClient beforeAll(async () => { @@ -26,12 +26,12 @@ describe("mee:signQuote", () => { paymentToken = network.paymentToken! eoaAccount = network.account! - mcNexusMainnet = await toMultichainNexusAccount({ + mcNexus = await toMultichainNexusAccount({ chains: [base, paymentChain], signer: eoaAccount }) - meeClient = createMeeClient({ account: mcNexusMainnet }) + meeClient = createMeeClient({ account: mcNexus }) }) test("should sign a quote", async () => { diff --git a/src/sdk/clients/decorators/mee/signQuote.ts b/src/sdk/clients/decorators/mee/signQuote.ts index f41b1223..1bdd6a23 100644 --- a/src/sdk/clients/decorators/mee/signQuote.ts +++ b/src/sdk/clients/decorators/mee/signQuote.ts @@ -1,5 +1,5 @@ import { type Hex, concatHex } from "viem" -import type { MultichainSmartAccount } from "../../../account/utils/toMultiChainNexusAccount" +import type { MultichainSmartAccount } from "../../../account/toMultiChainNexusAccount" import type { BaseMeeClient } from "../../createMeeClient" import type { GetQuotePayload } from "./getQuote" diff --git a/src/sdk/clients/decorators/mee/waitForSupertransactionReceipt.ts b/src/sdk/clients/decorators/mee/waitForSupertransactionReceipt.ts index 8d29880f..d8991e19 100644 --- a/src/sdk/clients/decorators/mee/waitForSupertransactionReceipt.ts +++ b/src/sdk/clients/decorators/mee/waitForSupertransactionReceipt.ts @@ -3,7 +3,7 @@ import { getExplorerTxLink, getJiffyScanLink, getMeeScanLink -} from "../../../account/utils/explorer/explorer" +} from "../../../account/utils/explorer" import type { Url } from "../../createHttpClient" import type { BaseMeeClient } from "../../createMeeClient" import type { GetQuotePayload, MeeFilledUserOpDetails } from "./getQuote" diff --git a/src/sdk/constants/tokens/tokens.test.ts b/src/sdk/constants/tokens/tokens.test.ts index a6b19fd4..44faba39 100644 --- a/src/sdk/constants/tokens/tokens.test.ts +++ b/src/sdk/constants/tokens/tokens.test.ts @@ -5,18 +5,18 @@ import { beforeAll, describe, expect, test } from "vitest" import * as tokens from "." import { toNetwork } from "../../../test/testSetup" import type { NetworkConfig } from "../../../test/testUtils" -import { addressEquals } from "../../account/utils/Utils" import { type MultichainSmartAccount, toMultichainNexusAccount -} from "../../account/utils/toMultiChainNexusAccount" +} from "../../account/toMultiChainNexusAccount" +import { addressEquals } from "../../account/utils/Utils" -describe("mee:tokens", async () => { +describe("mee.tokens", async () => { let network: NetworkConfig let eoaAccount: LocalAccount let paymentChain: Chain let paymentToken: Address - let mcNexusMainnet: MultichainSmartAccount + let mcNexus: MultichainSmartAccount beforeAll(async () => { network = await toNetwork("MAINNET_FROM_ENV_VARS") @@ -25,7 +25,7 @@ describe("mee:tokens", async () => { paymentToken = network.paymentToken! eoaAccount = network.account! - mcNexusMainnet = await toMultichainNexusAccount({ + mcNexus = await toMultichainNexusAccount({ chains: [base, paymentChain], signer: eoaAccount }) @@ -43,13 +43,13 @@ describe("mee:tokens", async () => { test("should instantiate a client", async () => { const token = tokens.mcUSDC const tokenWithChain = token.addressOn(10) - const mcNexusAddress = mcNexusMainnet.deploymentOn(base.id).address + const mcNexusAddress = mcNexus.deploymentOn(base.id).address const balances = await token.read({ onChains: [base, optimism], functionName: "balanceOf", args: [mcNexusAddress], - account: mcNexusMainnet + account: mcNexus }) expect(balances.length).toBe(2) diff --git a/src/sdk/modules/utils/Types.ts b/src/sdk/modules/utils/Types.ts index 2327c44b..d2f65c91 100644 --- a/src/sdk/modules/utils/Types.ts +++ b/src/sdk/modules/utils/Types.ts @@ -119,7 +119,7 @@ export type Modularity = { export type ModularSmartAccount = SmartAccount & Modularity -export type MinimalMEESmartAccount = Pick< +export type MeeSmartAccount = Pick< SmartAccount, | "address" | "getCounterFactualAddress" From 061709ca7c8eab3d9f2f8ac8f7cfc42eb5112dc8 Mon Sep 17 00:00:00 2001 From: Joe Pegler Date: Tue, 14 Jan 2025 21:51:13 +0000 Subject: [PATCH 6/6] feat: buildInstructions devEx --- .../buildBalanceInstructions.test.ts | 49 -------- .../decorators/buildBalanceInstructions.ts | 52 -------- .../buildBridgeInstructions.test.ts | 4 +- .../decorators/buildBridgeInstructions.ts | 10 +- .../decorators/buildInstructions.test.ts | 115 ++++++++++++++++++ .../account/decorators/buildInstructions.ts | 78 ++++++++++++ .../decorators/getUnifiedERC20Balance.ts | 4 +- .../account/decorators/queryBridge.test.ts | 8 +- src/sdk/account/decorators/queryBridge.ts | 4 +- .../account/toMultiChainNexusAccount.test.ts | 13 +- src/sdk/account/toMultiChainNexusAccount.ts | 28 ++--- .../{acrossPlugin.ts => toAcrossPlugin.ts} | 4 +- src/sdk/clients/createHttpClient.test.ts | 2 +- src/sdk/clients/createMeeClient.test.ts | 88 +++++++++----- .../clients/decorators/mee/getQuote.test.ts | 41 +++++++ src/sdk/clients/decorators/mee/getQuote.ts | 26 +++- 16 files changed, 355 insertions(+), 171 deletions(-) delete mode 100644 src/sdk/account/decorators/buildBalanceInstructions.test.ts delete mode 100644 src/sdk/account/decorators/buildBalanceInstructions.ts create mode 100644 src/sdk/account/decorators/buildInstructions.test.ts create mode 100644 src/sdk/account/decorators/buildInstructions.ts rename src/sdk/account/utils/{acrossPlugin.ts => toAcrossPlugin.ts} (98%) diff --git a/src/sdk/account/decorators/buildBalanceInstructions.test.ts b/src/sdk/account/decorators/buildBalanceInstructions.test.ts deleted file mode 100644 index 21263e43..00000000 --- a/src/sdk/account/decorators/buildBalanceInstructions.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { Address, Chain, LocalAccount } from "viem" -import { base } from "viem/chains" -import { beforeAll, describe, expect, it } from "vitest" -import { toNetwork } from "../../../test/testSetup" -import type { NetworkConfig } from "../../../test/testUtils" -import { type MeeClient, createMeeClient } from "../../clients/createMeeClient" -import { mcUSDC } from "../../constants/tokens" -import { - type MultichainSmartAccount, - toMultichainNexusAccount -} from "../toMultiChainNexusAccount" -import { buildBalanceInstructions } from "./buildBalanceInstructions" - -describe("mee:buildBalanceInstruction", () => { - let network: NetworkConfig - let eoaAccount: LocalAccount - let paymentChain: Chain - let paymentToken: Address - let mcNexus: MultichainSmartAccount - let meeClient: MeeClient - - beforeAll(async () => { - network = await toNetwork("MAINNET_FROM_ENV_VARS") - - paymentChain = network.chain - paymentToken = network.paymentToken! - eoaAccount = network.account! - - mcNexus = await toMultichainNexusAccount({ - chains: [base, paymentChain], - signer: eoaAccount - }) - - meeClient = createMeeClient({ account: mcNexus }) - }) - - it("should adjust the account balance", async () => { - const instructions = await buildBalanceInstructions({ - account: mcNexus, - amount: BigInt(1000), - token: mcUSDC, - chain: base - }) - - expect(instructions.length).toBeGreaterThan(0) - expect(instructions[0]).toHaveProperty("calls") - expect(instructions[0].calls.length).toBeGreaterThan(0) - }) -}) diff --git a/src/sdk/account/decorators/buildBalanceInstructions.ts b/src/sdk/account/decorators/buildBalanceInstructions.ts deleted file mode 100644 index ddf80b15..00000000 --- a/src/sdk/account/decorators/buildBalanceInstructions.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { Chain, erc20Abi } from "viem" -import type { Instruction } from "../../clients/decorators/mee/getQuote" -import type { BaseMultichainSmartAccount } from "../toMultiChainNexusAccount" -import type { MultichainContract } from "../utils/getMultichainContract" -import buildBridgeInstructions from "./buildBridgeInstructions" -import { getUnifiedERC20Balance } from "./getUnifiedERC20Balance" - -export type BuildBalanceInstructionParams = { - /** Optional smart account to execute the transaction. If not provided, uses the client's default account */ - account: BaseMultichainSmartAccount - /** The amount of tokens to require */ - amount: bigint - /** The token to require */ - token: MultichainContract - /** The chain to require the token on */ - chain: Chain -} - -/** - * Makes sure that the user has enough funds on the selected chain before filling the - * supertransaction. Bridges funds from other chains if needed. - * - * @param client - The Mee client to use - * @param params - The parameters for the balance requirement - * @returns Instructions for any required bridging operations - * @example - * const instructions = await buildBalanceInstruction(client, { - * amount: BigInt(1000), - * token: mcUSDC, - * chain: base - * }) - */ - -export const buildBalanceInstructions = async ( - params: BuildBalanceInstructionParams -): Promise => { - const { amount, token, chain, account } = params - const unifiedBalance = await getUnifiedERC20Balance({ - mcToken: token, - account - }) - const { instructions } = await buildBridgeInstructions({ - account, - amount: amount, - toChain: chain, - unifiedBalance - }) - - return instructions -} - -export default buildBalanceInstructions diff --git a/src/sdk/account/decorators/buildBridgeInstructions.test.ts b/src/sdk/account/decorators/buildBridgeInstructions.test.ts index 09ad3ee1..200f8f15 100644 --- a/src/sdk/account/decorators/buildBridgeInstructions.test.ts +++ b/src/sdk/account/decorators/buildBridgeInstructions.test.ts @@ -9,7 +9,7 @@ import { type MultichainSmartAccount, toMultichainNexusAccount } from "../toMultiChainNexusAccount" -import { AcrossPlugin } from "../utils/acrossPlugin" +import { toAcrossPlugin } from "../utils/toAcrossPlugin" import buildBridgeInstructions from "./buildBridgeInstructions" import { getUnifiedERC20Balance } from "./getUnifiedERC20Balance" @@ -41,7 +41,7 @@ describe("mee:buildBridgeInstructions", () => { const payload = await buildBridgeInstructions({ account: mcNexus, amount: 1n, - bridgingPlugins: [AcrossPlugin], + bridgingPlugins: [toAcrossPlugin()], toChain: base, unifiedBalance }) diff --git a/src/sdk/account/decorators/buildBridgeInstructions.ts b/src/sdk/account/decorators/buildBridgeInstructions.ts index be74e3f8..a600218d 100644 --- a/src/sdk/account/decorators/buildBridgeInstructions.ts +++ b/src/sdk/account/decorators/buildBridgeInstructions.ts @@ -1,7 +1,7 @@ import type { Address, Chain } from "viem" import type { Instruction } from "../../clients/decorators/mee/getQuote" import type { BaseMultichainSmartAccount } from "../toMultiChainNexusAccount" -import { AcrossPlugin } from "../utils/acrossPlugin" +import { toAcrossPlugin } from "../utils/toAcrossPlugin" import type { UnifiedERC20Balance } from "./getUnifiedERC20Balance" import type { BridgeQueryResult } from "./queryBridge" import { queryBridge } from "./queryBridge" @@ -124,7 +124,7 @@ export type BridgingInstructions = { * @example * const instructions = await buildBridgeInstruction(client, { * amount: BigInt(1000), - * token: mcUSDC, + * mcToken: mcUSDC, * chain: base * }) */ @@ -137,16 +137,16 @@ export const buildBridgeInstructions = async ( amount: targetAmount, toChain, unifiedBalance, - bridgingPlugins = [AcrossPlugin], + bridgingPlugins = [toAcrossPlugin()], feeData } = params // Create token address mapping const tokenMapping: MultichainAddressMapping = { on: (chainId: number) => - unifiedBalance.token.deployments.get(chainId) || "0x", + unifiedBalance.mcToken.deployments.get(chainId) || "0x", deployments: Array.from( - unifiedBalance.token.deployments.entries(), + unifiedBalance.mcToken.deployments.entries(), ([chainId, address]) => ({ chainId, address diff --git a/src/sdk/account/decorators/buildInstructions.test.ts b/src/sdk/account/decorators/buildInstructions.test.ts new file mode 100644 index 00000000..fbf6e4b1 --- /dev/null +++ b/src/sdk/account/decorators/buildInstructions.test.ts @@ -0,0 +1,115 @@ +import type { Address, Chain, LocalAccount } from "viem" +import { base } from "viem/chains" +import { beforeAll, describe, expect, it } from "vitest" +import { toNetwork } from "../../../test/testSetup" +import type { NetworkConfig } from "../../../test/testUtils" +import { type MeeClient, createMeeClient } from "../../clients/createMeeClient" +import { + Instruction, + SupertransactionLike +} from "../../clients/decorators/mee/getQuote" +import { mcUSDC } from "../../constants/tokens" +import { + type MultichainSmartAccount, + toMultichainNexusAccount +} from "../toMultiChainNexusAccount" +import { buildInstructions } from "./buildInstructions" + +describe("mee:buildInstructions", () => { + let network: NetworkConfig + let eoaAccount: LocalAccount + let paymentChain: Chain + let paymentToken: Address + let mcNexus: MultichainSmartAccount + let meeClient: MeeClient + + beforeAll(async () => { + network = await toNetwork("MAINNET_FROM_ENV_VARS") + + paymentChain = network.chain + paymentToken = network.paymentToken! + eoaAccount = network.account! + + mcNexus = await toMultichainNexusAccount({ + chains: [base, paymentChain], + signer: eoaAccount + }) + + meeClient = createMeeClient({ account: mcNexus }) + }) + + it("should use the default action while building instructions", async () => { + const instructions = await buildInstructions({ + account: mcNexus, + action: { + type: "DEFAULT", + parameters: [ + { + calls: [ + { + to: "0x0000000000000000000000000000000000000000", + gasLimit: 50000n, + value: 0n + } + ], + chainId: 8453 + } + ] + } + }) + + expect(instructions).toMatchInlineSnapshot(` + [ + { + "calls": [ + { + "gasLimit": 50000n, + "to": "0x0000000000000000000000000000000000000000", + "value": 0n, + }, + ], + "chainId": 8453, + }, + ] + `) + expect(instructions.length).toBeGreaterThan(0) + }) + + it("should use the bridge action while building instructions", async () => { + const initialInstructions = await buildInstructions({ + account: mcNexus, + action: { + type: "BRIDGE", + parameters: { + amount: BigInt(1000), + mcToken: mcUSDC, + chain: base + } + } + }) + + const instructions = await buildInstructions({ + currentInstructions: initialInstructions, + account: mcNexus, + action: { + type: "DEFAULT", + parameters: [ + { + calls: [ + { + to: "0x0000000000000000000000000000000000000000", + gasLimit: 50000n, + value: 0n + } + ], + chainId: 8453 + } + ] + } + }) + + expect(instructions.length).toBe(2) + expect(instructions[0].calls.length).toBe(2) // Bridge instructions generates two calls + expect(instructions[1].calls.length).toBe(1) // Default instruction in this case generates one call + }) +}) diff --git a/src/sdk/account/decorators/buildInstructions.ts b/src/sdk/account/decorators/buildInstructions.ts new file mode 100644 index 00000000..c52a36fd --- /dev/null +++ b/src/sdk/account/decorators/buildInstructions.ts @@ -0,0 +1,78 @@ +import type { Chain, erc20Abi } from "viem" +import type { Instruction } from "../../clients/decorators/mee/getQuote" +import type { BaseMultichainSmartAccount } from "../toMultiChainNexusAccount" +import type { MultichainContract } from "../utils/getMultichainContract" +import buildBridgeInstructions from "./buildBridgeInstructions" +import { getUnifiedERC20Balance } from "./getUnifiedERC20Balance" + +export type BridgeInstructionsForBridgeAction = { + /** The amount of tokens to require */ + amount: bigint + /** The token to require */ + mcToken: MultichainContract + /** The chain to require the token on */ + chain: Chain +} + +/** + * Parameters for querying bridge operations + */ +export type BuildInstructionsParams = { + /** The multichain smart account to check balances for */ + account: BaseMultichainSmartAccount + /** The chain to build instructions for */ + action: DefaultBuildAction | BridgeBuildAction + /** The current instructions */ + currentInstructions?: Instruction[] +} + +type DefaultBuildAction = { + type: "DEFAULT" + parameters: Instruction[] | Instruction +} + +type BridgeBuildAction = { + type: "BRIDGE" + parameters: BridgeInstructionsForBridgeAction +} + +/** + * Makes sure that the user has enough funds on the selected chain before filling the + * supertransaction. Bridges funds from other chains if needed. + * + * @param client - The Mee client to use + * @param params - The parameters for the balance requirement + * @returns Instructions for any required bridging operations + * @example + * const instructions = await buildInstructions(client, { + * amount: BigInt(1000), + * mcToken: mcUSDC, + * chain: base + * }) + */ + +export const buildInstructions = async ( + params: BuildInstructionsParams +): Promise => { + const { account, action, currentInstructions = [] } = params + + switch (action.type) { + case "BRIDGE": { + const { amount, mcToken, chain } = action.parameters + const unifiedBalance = await getUnifiedERC20Balance({ mcToken, account }) + const { instructions } = await buildBridgeInstructions({ + account, + amount: amount, + toChain: chain, + unifiedBalance + }) + return [...currentInstructions, ...instructions] + } + default: + return Array.isArray(action.parameters) + ? [...currentInstructions, ...action.parameters] + : [...currentInstructions, action.parameters] + } +} + +export default buildInstructions diff --git a/src/sdk/account/decorators/getUnifiedERC20Balance.ts b/src/sdk/account/decorators/getUnifiedERC20Balance.ts index 1e41e97a..3434373a 100644 --- a/src/sdk/account/decorators/getUnifiedERC20Balance.ts +++ b/src/sdk/account/decorators/getUnifiedERC20Balance.ts @@ -19,7 +19,7 @@ export type RelevantBalance = UnifiedBalanceItem & { chainId: number } */ export type UnifiedERC20Balance = { /** The multichain ERC20 token contract */ - token: MultichainContract + mcToken: MultichainContract /** Individual balance breakdown per chain */ breakdown: RelevantBalance[] } & UnifiedBalanceItem @@ -105,6 +105,6 @@ export async function getUnifiedERC20Balance( } }), breakdown: balances, - token: mcToken + mcToken } } diff --git a/src/sdk/account/decorators/queryBridge.test.ts b/src/sdk/account/decorators/queryBridge.test.ts index 518c3c01..b37941d9 100644 --- a/src/sdk/account/decorators/queryBridge.test.ts +++ b/src/sdk/account/decorators/queryBridge.test.ts @@ -9,7 +9,7 @@ import { type MultichainSmartAccount, toMultichainNexusAccount } from "../toMultiChainNexusAccount" -import { AcrossPlugin } from "../utils/acrossPlugin" +import { toAcrossPlugin } from "../utils/toAcrossPlugin" import type { MultichainAddressMapping } from "./buildBridgeInstructions" import { queryBridge } from "./queryBridge" @@ -41,9 +41,9 @@ describe("mee:queryBridge", () => { const tokenMapping: MultichainAddressMapping = { on: (chainId: number) => - unifiedBalance.token.deployments.get(chainId) || "0x", + unifiedBalance.mcToken.deployments.get(chainId) || "0x", deployments: Array.from( - unifiedBalance.token.deployments.entries(), + unifiedBalance.mcToken.deployments.entries(), ([chainId, address]) => ({ chainId, address }) ) } @@ -58,6 +58,6 @@ describe("mee:queryBridge", () => { expect(payload?.amount).toBeGreaterThan(0n) expect(payload?.receivedAtDestination).toBeGreaterThan(0n) - expect(payload?.plugin).toBe(AcrossPlugin) + expect(payload?.plugin).toBe(toAcrossPlugin()) }) }) diff --git a/src/sdk/account/decorators/queryBridge.ts b/src/sdk/account/decorators/queryBridge.ts index 1d5e14e1..4ba42a7c 100644 --- a/src/sdk/account/decorators/queryBridge.ts +++ b/src/sdk/account/decorators/queryBridge.ts @@ -1,7 +1,7 @@ import type { Chain } from "viem" import type { Instruction } from "../../clients/decorators/mee/getQuote" import type { BaseMultichainSmartAccount } from "../toMultiChainNexusAccount" -import { AcrossPlugin } from "../utils/acrossPlugin" +import { toAcrossPlugin } from "../utils/toAcrossPlugin" import type { BridgingPlugin, MultichainAddressMapping @@ -67,7 +67,7 @@ export const queryBridge = async ( account, fromChain, toChain, - plugin = AcrossPlugin, + plugin = toAcrossPlugin(), amount, tokenMapping } = params diff --git a/src/sdk/account/toMultiChainNexusAccount.test.ts b/src/sdk/account/toMultiChainNexusAccount.test.ts index 7c181f25..310a5d95 100644 --- a/src/sdk/account/toMultiChainNexusAccount.test.ts +++ b/src/sdk/account/toMultiChainNexusAccount.test.ts @@ -106,19 +106,26 @@ describe("mee.toMultiChainNexusAccount", async () => { test("mcNexus to have decorators successfully applied", async () => { expect(mcNexus.getUnifiedERC20Balance).toBeInstanceOf(Function) - expect(mcNexus.buildBalanceInstructions).toBeInstanceOf(Function) + expect(mcNexus.buildInstructions).toBeInstanceOf(Function) expect(mcNexus.buildBridgeInstructions).toBeInstanceOf(Function) expect(mcNexus.queryBridge).toBeDefined() }) + test("should check unified balance", async () => { + const unifiedBalance = await mcNexus.getUnifiedERC20Balance(mcUSDC) + expect(unifiedBalance).toHaveProperty("mcToken") + expect(unifiedBalance).toHaveProperty("breakdown") + expect(unifiedBalance.mcToken).toHaveProperty("deployments") + }) + test("should query bridge", async () => { const unifiedBalance = await mcNexus.getUnifiedERC20Balance(mcUSDC) const tokenMapping = { on: (chainId: number) => - unifiedBalance.token.deployments.get(chainId) || "0x", + unifiedBalance.mcToken.deployments.get(chainId) || "0x", deployments: Array.from( - unifiedBalance.token.deployments.entries(), + unifiedBalance.mcToken.deployments.entries(), ([chainId, address]) => ({ chainId, address }) ) } diff --git a/src/sdk/account/toMultiChainNexusAccount.ts b/src/sdk/account/toMultiChainNexusAccount.ts index f617cced..ba7f9567 100644 --- a/src/sdk/account/toMultiChainNexusAccount.ts +++ b/src/sdk/account/toMultiChainNexusAccount.ts @@ -10,15 +10,15 @@ import { toNexusAccount } from "./toNexusAccount" import type { MultichainContract } from "./utils/getMultichainContract" import type { Signer } from "./utils/toSigner" -import { - type BuildBalanceInstructionParams, - buildBalanceInstructions as buildBalanceInstructionsDecorator -} from "./decorators/buildBalanceInstructions" import { type BridgingInstructions, type BuildBridgeInstructionParams, buildBridgeInstructions as buildBridgeInstructionsDecorator } from "./decorators/buildBridgeInstructions" +import { + type BuildInstructionsParams, + buildInstructions as buildInstructionsDecorator +} from "./decorators/buildInstructions" import { type UnifiedERC20Balance, getUnifiedERC20Balance as getUnifiedERC20BalanceDecorator @@ -71,14 +71,14 @@ export type MultichainSmartAccount = BaseMultichainSmartAccount & { * @param params - The parameters for the balance requirement * @returns Instructions for any required bridging operations * @example - * const instructions = await mcAccount.buildBalanceInstructions({ + * const instructions = await mcAccount.buildInstructions({ * amount: BigInt(1000), - * token: mcUSDC, + * mcToken: mcUSDC, * chain: base * }) */ - buildBalanceInstructions: ( - params: Omit + buildInstructions: ( + params: Omit ) => Promise /** * Function to build instructions for bridging a token across all deployments @@ -87,7 +87,7 @@ export type MultichainSmartAccount = BaseMultichainSmartAccount & { * @example * const instructions = await mcAccount.buildBridgeInstructions({ * amount: BigInt(1000), - * token: mcUSDC, + * mcToken: mcUSDC, * chain: base * }) */ @@ -101,7 +101,7 @@ export type MultichainSmartAccount = BaseMultichainSmartAccount & { * @example * const result = await mcAccount.queryBridge({ * amount: BigInt(1000), - * token: mcUSDC, + * mcToken: mcUSDC, * chain: base * }) */ @@ -150,9 +150,9 @@ export async function toMultichainNexusAccount( return getUnifiedERC20BalanceDecorator({ mcToken, account: baseAccount }) } - const buildBalanceInstructions = ( - params: Omit - ) => buildBalanceInstructionsDecorator({ ...params, account: baseAccount }) + const buildInstructions = ( + params: Omit + ) => buildInstructionsDecorator({ ...params, account: baseAccount }) const buildBridgeInstructions = ( params: Omit @@ -164,7 +164,7 @@ export async function toMultichainNexusAccount( return { ...baseAccount, getUnifiedERC20Balance, - buildBalanceInstructions, + buildInstructions, buildBridgeInstructions, queryBridge } diff --git a/src/sdk/account/utils/acrossPlugin.ts b/src/sdk/account/utils/toAcrossPlugin.ts similarity index 98% rename from src/sdk/account/utils/acrossPlugin.ts rename to src/sdk/account/utils/toAcrossPlugin.ts index 940f8bf7..59122a64 100644 --- a/src/sdk/account/utils/acrossPlugin.ts +++ b/src/sdk/account/utils/toAcrossPlugin.ts @@ -143,8 +143,8 @@ export const acrossEncodeBridgingUserOp = async ( } } -export const AcrossPlugin: BridgingPlugin = { +export const toAcrossPlugin = (): BridgingPlugin => ({ encodeBridgeUserOp: async (params) => { return await acrossEncodeBridgingUserOp(params) } -} +}) diff --git a/src/sdk/clients/createHttpClient.test.ts b/src/sdk/clients/createHttpClient.test.ts index 395be01d..e442cead 100644 --- a/src/sdk/clients/createHttpClient.test.ts +++ b/src/sdk/clients/createHttpClient.test.ts @@ -10,7 +10,7 @@ import { import createHttpClient from "./createHttpClient" import { type MeeClient, createMeeClient } from "./createMeeClient" -describe("mee.createHttp Client", async () => { +describe("mee.createHttpClient", async () => { let network: NetworkConfig let eoaAccount: LocalAccount let paymentChain: Chain diff --git a/src/sdk/clients/createMeeClient.test.ts b/src/sdk/clients/createMeeClient.test.ts index 6749aa10..6c17c246 100644 --- a/src/sdk/clients/createMeeClient.test.ts +++ b/src/sdk/clients/createMeeClient.test.ts @@ -7,6 +7,7 @@ import { type MultichainSmartAccount, toMultichainNexusAccount } from "../account/toMultiChainNexusAccount" +import { mcUSDC } from "../constants/tokens" import { type MeeClient, createMeeClient } from "./createMeeClient" import type { Instruction } from "./decorators/mee" @@ -113,29 +114,36 @@ describe("mee.createMeeClient", async () => { test("should demo the devEx of preparing instructions", async () => { // These can be any 'Instruction', or any helper method that resolves to a 'Instruction', - // including 'buildBalanceInstruction'. They all are resolved in the 'getQuote' method under the hood. - const preparedInstructions: Instruction[] = [ - { - calls: [ + // including 'buildInstructions'. They all are resolved in the 'getQuote' method under the hood. + const currentInstructions = await meeClient.account.buildInstructions({ + action: { + type: "DEFAULT", + parameters: [ { - to: "0x0000000000000000000000000000000000000000", - gasLimit: 50000n, - value: 0n - } - ], - chainId: 8453 - }, - { - calls: [ - { - to: "0x0000000000000000000000000000000000000000", - gasLimit: 50000n, - value: 0n + calls: [ + { + to: "0x0000000000000000000000000000000000000000", + gasLimit: 50000n, + value: 0n + } + ], + chainId: 8453 } - ], - chainId: 8453 + ] + } + }) + + const preparedInstructions = await meeClient.account.buildInstructions({ + currentInstructions, + action: { + type: "BRIDGE", + parameters: { + amount: BigInt(1000), + mcToken: mcUSDC, + chain: base + } } - ] + }) expect(preparedInstructions).toBeDefined() @@ -161,19 +169,39 @@ describe("mee.createMeeClient", async () => { async () => { console.time("execute:hashTimer") console.time("execute:receiptTimer") - const { hash } = await meeClient.execute({ - instructions: [ - { - calls: [ + + const instructions = [ + mcNexus.buildInstructions({ + action: { + type: "BRIDGE", + parameters: { + amount: BigInt(1000), + mcToken: mcUSDC, + chain: base + } + } + }), + mcNexus.buildInstructions({ + action: { + type: "DEFAULT", + parameters: [ { - to: "0x0000000000000000000000000000000000000000", - gasLimit: 50000n, - value: 0n + calls: [ + { + to: "0x0000000000000000000000000000000000000000", + gasLimit: 50000n, + value: 0n + } + ], + chainId: base.id } - ], - chainId: 8453 + ] } - ], + }) + ] + + const { hash } = await meeClient.execute({ + instructions, feeToken: { address: paymentToken, chainId: paymentChain.id diff --git a/src/sdk/clients/decorators/mee/getQuote.test.ts b/src/sdk/clients/decorators/mee/getQuote.test.ts index 5f14531c..976bb275 100644 --- a/src/sdk/clients/decorators/mee/getQuote.test.ts +++ b/src/sdk/clients/decorators/mee/getQuote.test.ts @@ -5,6 +5,7 @@ import { toNetwork } from "../../../../test/testSetup" import type { NetworkConfig } from "../../../../test/testUtils" import type { MultichainSmartAccount } from "../../../account/toMultiChainNexusAccount" import { toMultichainNexusAccount } from "../../../account/toMultiChainNexusAccount" +import { mcUSDC } from "../../../constants/tokens" import { type MeeClient, createMeeClient } from "../../createMeeClient" import { type Instruction, getQuote } from "./getQuote" @@ -68,4 +69,44 @@ describe("mee.getQuote", () => { expect(quote).toBeDefined() }) + + test("should resolve unresolved instructions", async () => { + const quote = await getQuote(meeClient, { + instructions: [ + mcNexus.buildInstructions({ + action: { + type: "BRIDGE", + parameters: { + amount: BigInt(1000), + mcToken: mcUSDC, + chain: base + } + } + }), + mcNexus.buildInstructions({ + action: { + type: "DEFAULT", + parameters: [ + { + calls: [ + { + to: "0x0000000000000000000000000000000000000000", + gasLimit: 50000n, + value: 0n + } + ], + chainId: base.id + } + ] + } + }) + ], + feeToken: { + address: paymentToken, + chainId: paymentChain.id + } + }) + + expect(quote.userOps.length).toEqual(3) + }) }) diff --git a/src/sdk/clients/decorators/mee/getQuote.ts b/src/sdk/clients/decorators/mee/getQuote.ts index 43f77541..8c148669 100644 --- a/src/sdk/clients/decorators/mee/getQuote.ts +++ b/src/sdk/clients/decorators/mee/getQuote.ts @@ -39,20 +39,25 @@ export type Instruction = { /** * Represents a supertransaction, which is a collection of instructions to be executed in a single transaction - * @type SuperTransaction + * @type Supertransaction */ -export type SuperTransaction = { +export type Supertransaction = { /** Array of instructions to be executed in the transaction */ instructions: Instruction[] /** Token to be used for paying transaction fees */ feeToken: FeeTokenInfo } +export type SupertransactionLike = { + instructions: (Promise | Instruction[])[] | Instruction[] + feeToken: FeeTokenInfo +} + /** * Parameters required for requesting a quote from the MEE service * @type GetQuoteParams */ -export type GetQuoteParams = SuperTransaction & { +export type GetQuoteParams = SupertransactionLike & { /** Optional smart account to execute the transaction. If not provided, uses the client's default account */ account?: MultichainSmartAccount } @@ -199,16 +204,27 @@ export const getQuote = async ( ): Promise => { const { account: account_ = client.account, instructions, feeToken } = params - const validUserOps = instructions.every((userOp) => + const resolvedInstructions: Instruction[] = ( + await Promise.all( + instructions.flatMap((instructions_) => + typeof instructions_ === "function" + ? (instructions_ as () => Promise)() + : instructions_ + ) + ) + ).flat() + + const validUserOps = resolvedInstructions.every((userOp) => account_.deploymentOn(userOp.chainId) ) const validPaymentAccount = account_.deploymentOn(feeToken.chainId) if (!validPaymentAccount || !validUserOps) { + console.log(resolvedInstructions.map((x) => x.chainId)) throw Error("Account is not deployed on necessary chain(s)") } const userOpResults = await Promise.all( - instructions.map((userOp) => { + resolvedInstructions.map((userOp) => { const deployment = account_.deploymentOn(userOp.chainId) if (deployment) { return Promise.all([