From 8629116972dbb3e9166fe9c033bc5f0ebe9540ee Mon Sep 17 00:00:00 2001 From: Joe Pegler Date: Mon, 13 Jan 2025 17:21:35 +0000 Subject: [PATCH 01/11] 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 2c074f1ca..be9ffdefe 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 000000000..bc44ad321 --- /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 83d440bfb..25bfd1b72 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 3a8fedd36..1e02182ef 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 e171d2fba..82ff0abcd 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 262693997..10f647f57 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 e30b042a3..ab924b7bc 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 000000000..923837d18 --- /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 ffc0dd4fa..0b3d3ab4c 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 e50412038..952b475b8 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 a10624c45..70f905743 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 92000d5e0..a8be599c0 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 000000000..fb140e3a7 --- /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 000000000..3f450389a --- /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 7405dc44d..f6a03b7ab 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 000000000..b81e8e8d7 --- /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 000000000..f1df0a3b8 --- /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 000000000..0f90aa5c2 --- /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 000000000..7afc5d62d --- /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 b64cb143e..bed11bef5 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 f35a05992..b64c5f1ae 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 000000000..a427bac18 --- /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 000000000..eb11daa97 --- /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 000000000..178e3e625 --- /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 000000000..35ae284da --- /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 1f3d6dc32..81911acca 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 c02144aae..773078fd6 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 639e72276..dc72b0163 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 5a0123b37..ac2c91aa9 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 000000000..76d6d6513 --- /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 000000000..6c97cb938 --- /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 000000000..47a3db1e8 --- /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 000000000..78eb7e415 --- /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 000000000..6c7552226 --- /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 000000000..064586564 --- /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 000000000..35b722e97 --- /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 000000000..3c13b6f49 --- /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 000000000..38dae91f9 --- /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 000000000..0b5071fbd --- /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 000000000..e74dc0161 --- /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 000000000..117338dd8 --- /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 000000000..fb1e08030 --- /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 000000000..f41b1223b --- /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 000000000..8d29880f2 --- /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 bb62bc1b9..851eae270 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 eaffb92a8..2d0635f17 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 94062b963..17f3f5eaf 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 000000000..b0abc196c --- /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 000000000..409674c4a --- /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 c561918e4..4604b106b 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 000000000..be82e6da1 --- /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 000000000..96748bf2f --- /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 000000000..a6b19fd40 --- /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 d9168ab6e..6fb443439 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 28b87de5e..2b8717a9e 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 b1ba594e4..b5aee66d6 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 bd994b038..160d37149 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 c040135c1..a80854332 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 76edd8f1d..136d3e400 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 ffd6108de..d13b4eb5a 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 d90b721e3..2327c44b3 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 d2698a565..c914d0abd 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 85d93e93c..545f9673a 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 ac4620817..94b7143a6 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 7bc74a8e0..8b11b9c60 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 3fef32577..5034c9d90 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 02/11] 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 7409be5aa..d3a8b67b0 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 ab924b7bc..b49c4425f 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 03/11] 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 b81e8e8d7..50b653f86 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 04/11] 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 bc44ad321..8c6876453 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 25bfd1b72..15ef332c5 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 05/11] 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 be9ffdefe..d4a63896d 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 000000000..21263e436 --- /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 000000000..ddf80b157 --- /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 000000000..09ad3ee13 --- /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 000000000..be74e3f8c --- /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 000000000..51361bbe3 --- /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 50b653f86..e27c3a0c5 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 000000000..0274ebc35 --- /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 f6a03b7ab..e5ff80ef5 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 000000000..23f35d7e8 --- /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 000000000..1e41e97ac --- /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 000000000..518c3c014 --- /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 000000000..1d5e14e19 --- /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 000000000..7c181f257 --- /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 000000000..f617cceda --- /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 0b3d3ab4c..71d9ed825 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 70f905743..c4dd5800e 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 000000000..940f8bf75 --- /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 fb140e3a7..a0feb19f1 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 3f450389a..9d17596c4 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 000000000..e4577837b --- /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 f1df0a3b8..18fdda122 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 7ba34a0ad..72f09178e 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 0f90aa5c2..000000000 --- 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 7afc5d62d..000000000 --- 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 a427bac18..395be01df 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 eb11daa97..989ee22a5 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 178e3e625..6749aa10a 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 35ae284da..76d2d722b 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 76d6d6513..4856df63f 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 47a3db1e8..b5e6605de 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 064586564..bc8a1dfd2 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 3c13b6f49..5f14531cc 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 38dae91f9..43f775415 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 e74dc0161..dbf46d9b5 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 117338dd8..8e35b4786 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 fb1e08030..9249e4e6b 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 f41b1223b..1bdd6a233 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 8d29880f2..d8991e196 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 a6b19fd40..44faba394 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 2327c44b3..d2f65c917 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 06/11] 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 21263e436..000000000 --- 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 ddf80b157..000000000 --- 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 09ad3ee13..200f8f150 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 be74e3f8c..a600218d5 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 000000000..fbf6e4b13 --- /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 000000000..c52a36fdc --- /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 1e41e97ac..3434373ae 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 518c3c014..b37941d90 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 1d5e14e19..4ba42a7c7 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 7c181f257..310a5d95f 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 f617cceda..ba7f9567d 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 940f8bf75..59122a648 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 395be01df..e442cead1 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 6749aa10a..6c17c2462 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 5f14531cc..976bb275b 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 43f775415..8c1486699 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([ From 89ef7041e80a991b1b6a6f0ba4de7187e332ec88 Mon Sep 17 00:00:00 2001 From: Joe Pegler Date: Wed, 15 Jan 2025 21:37:46 +0000 Subject: [PATCH 07/11] chore: continued --- ...uildInstructions.test.ts => build.test.ts} | 93 +++++++------- src/sdk/account/decorators/build.ts | 116 ++++++++++++++++++ .../decorators/buildBridgeInstructions.ts | 98 ++++++++++----- .../account/decorators/buildInstructions.ts | 78 ------------ .../buildBaseInstructions.test.ts | 62 ++++++++++ .../instructions/buildBaseInstructions.ts | 47 +++++++ .../instructions/buildIntent.test.ts | 54 ++++++++ .../decorators/instructions/buildIntent.ts | 65 ++++++++++ .../account/decorators/instructions/index.ts | 2 + .../account/toMultiChainNexusAccount.test.ts | 2 +- src/sdk/account/toMultiChainNexusAccount.ts | 25 ++-- src/sdk/clients/createMeeClient.test.ts | 48 ++++---- .../clients/decorators/mee/getQuote.test.ts | 22 ++-- 13 files changed, 506 insertions(+), 206 deletions(-) rename src/sdk/account/decorators/{buildInstructions.test.ts => build.test.ts} (61%) create mode 100644 src/sdk/account/decorators/build.ts delete mode 100644 src/sdk/account/decorators/buildInstructions.ts create mode 100644 src/sdk/account/decorators/instructions/buildBaseInstructions.test.ts create mode 100644 src/sdk/account/decorators/instructions/buildBaseInstructions.ts create mode 100644 src/sdk/account/decorators/instructions/buildIntent.test.ts create mode 100644 src/sdk/account/decorators/instructions/buildIntent.ts create mode 100644 src/sdk/account/decorators/instructions/index.ts diff --git a/src/sdk/account/decorators/buildInstructions.test.ts b/src/sdk/account/decorators/build.test.ts similarity index 61% rename from src/sdk/account/decorators/buildInstructions.test.ts rename to src/sdk/account/decorators/build.test.ts index fbf6e4b13..58d88c116 100644 --- a/src/sdk/account/decorators/buildInstructions.test.ts +++ b/src/sdk/account/decorators/build.test.ts @@ -4,18 +4,14 @@ 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" +import { build } from "./build" -describe("mee:buildInstructions", () => { +describe("mee:build", () => { let network: NetworkConfig let eoaAccount: LocalAccount let paymentChain: Chain @@ -39,24 +35,26 @@ describe("mee:buildInstructions", () => { }) 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 - } - ] + const instructions = await build( + { account: mcNexus }, + { + type: "default", + data: { + instructions: [ + { + calls: [ + { + to: "0x0000000000000000000000000000000000000000", + gasLimit: 50000n, + value: 0n + } + ], + chainId: 8453 + } + ] + } } - }) + ) expect(instructions).toMatchInlineSnapshot(` [ @@ -76,37 +74,38 @@ describe("mee:buildInstructions", () => { }) it("should use the bridge action while building instructions", async () => { - const initialInstructions = await buildInstructions({ - account: mcNexus, - action: { - type: "BRIDGE", - parameters: { + const currentInstructions = await build( + { account: mcNexus }, + { + type: "intent", + data: { 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 - } - ] + const instructions = await build( + { account: mcNexus, currentInstructions }, + { + type: "default", + data: { + instructions: [ + { + 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 diff --git a/src/sdk/account/decorators/build.ts b/src/sdk/account/decorators/build.ts new file mode 100644 index 000000000..dbf46818a --- /dev/null +++ b/src/sdk/account/decorators/build.ts @@ -0,0 +1,116 @@ +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 { + type BuildBaseInstructionsParams, + buildBaseInstructions +} from "./instructions/buildBaseInstructions" +import { type BuildIntentParams, buildIntent } from "./instructions/buildIntent" + +/** + * Base parameters for building instructions + * @property account - {@link BaseMultichainSmartAccount} The multichain smart account to check balances for + * @property currentInstructions - {@link Instruction[]} Optional array of existing instructions to append to + */ +export type BaseInstructionsParams = { + /** The multichain smart account to check balances for */ + account: BaseMultichainSmartAccount + /** The current instructions */ + currentInstructions?: Instruction[] +} + +/** + * Configuration for bridging tokens between chains + * @property amount - The amount of tokens to bridge + * @property mcToken - The multichain token contract to bridge + * @property chain - The destination chain for the bridge operation + */ +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 +} + +/** + * Default build action which is used to build instructions for a chain + */ +export type BuildBaseInstruction = { + /** The type of action */ + type: "default" + /** The parameters for the action */ + data: BuildBaseInstructionsParams +} + +/** + * Bridge build action which is used to build instructions for bridging funds from other chains + */ +export type BuildIntentInstruction = { + /** The type of action */ + type: "intent" + /** The parameters for the action */ + data: BuildIntentParams +} +/** + * The types of build instructions + */ +export type BuildInstructionTypes = + | BuildBaseInstruction + | BuildIntentInstruction + +/** + * Builds transaction instructions based on the provided action type and parameters + * + * @param params - The build instructions configuration + * @param params.account - {@link BaseMultichainSmartAccount} The multichain smart account to check balances for + * @param params.currentInstructions - {@link Instruction[]} Optional array of existing instructions to append to + * @param params.type - The type of build action ("default" | "intent") + * @param params.parameters - {@link BuildBaseInstruction} | {@link BuildIntentInstruction} + * + * @returns Promise resolving to an array of {@link Instruction} + * + * @example + * // Bridge tokens example + * const bridgeInstructions = await build({ + * type: "intent", + * parameters: { + * amount: BigInt(1000), + * mcToken: mcUSDC, + * chain: base + * }, + * account: myMultichainAccount + * }) + * + * @example + * // Default action example + * const defaultInstructions = await build({ + * type: "default", + * parameters: myExistingInstruction, + * account: myMultichainAccount + * }) + */ +export const build = async ( + baseParams: BaseInstructionsParams, + parameters: BuildInstructionTypes +): Promise => { + console.log({ baseParams, parameters }) + + const { type, data } = parameters + + switch (type) { + case "intent": { + return buildIntent(baseParams, data) + } + case "default": { + return buildBaseInstructions(baseParams, data) + } + default: { + throw new Error(`Unknown build action type: ${type}`) + } + } +} + +export default build diff --git a/src/sdk/account/decorators/buildBridgeInstructions.ts b/src/sdk/account/decorators/buildBridgeInstructions.ts index a600218d5..421e3fd98 100644 --- a/src/sdk/account/decorators/buildBridgeInstructions.ts +++ b/src/sdk/account/decorators/buildBridgeInstructions.ts @@ -1,4 +1,4 @@ -import type { Address, Chain } from "viem" +import type { Address, Chain, Prettify } from "viem" import type { Instruction } from "../../clients/decorators/mee/getQuote" import type { BaseMultichainSmartAccount } from "../toMultiChainNexusAccount" import { toAcrossPlugin } from "../utils/toAcrossPlugin" @@ -8,44 +8,61 @@ import { queryBridge } from "./queryBridge" /** * Mapping of a token address to a specific chain + * @property chainId - The numeric ID of the chain + * @property address - {@link Address} The token's contract address on the chain */ -export type AddressMapping = { +export type AddressMapping = Prettify<{ + /** The numeric ID of the chain */ chainId: number + /** The token's contract address on the chain */ address: Address -} +}> /** * Cross-chain token address mapping with helper functions + * @property deployments - Array of {@link AddressMapping} containing token addresses per chain + * @property on - Function to retrieve token address for a specific chain ID */ -export type MultichainAddressMapping = { +export type MultichainAddressMapping = Prettify<{ + /** Array of {@link AddressMapping} containing token addresses per chain */ deployments: AddressMapping[] /** Returns the token address for a given chain ID */ on: (chainId: number) => Address +}> + +/** + * Fee data for the transaction fee + * @property txFeeChainId - The chain ID where the tx fee is paid + * @property txFeeAmount - The amount of tx fee to pay + */ +export type FeeData = { + /** The chain ID where the tx fee is paid */ + txFeeChainId: number + /** The amount of tx fee to pay */ + txFeeAmount: bigint } /** * Parameters for multichain token bridging operations + * @property toChain - {@link Chain} Destination chain for the bridge operation + * @property unifiedBalance - {@link UnifiedERC20Balance} Token balance information across all chains + * @property amount - Amount of tokens to bridge as BigInt + * @property bridgingPlugins - Optional array of {@link BridgingPlugin} to use for bridging + * @property feeData - Optional fee information for the transaction */ -export type MultichainBridgingParams = { - /** Destination chain for the bridge operation */ +export type MultichainBridgingParams = Prettify<{ 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 - } -} + feeData?: FeeData +}> /** * Result of a bridging plugin operation + * @property userOp - {@link Instruction} User operation to execute the bridge + * @property receivedAtDestination - Expected amount to be received after bridging + * @property bridgingDurationExpectedMs - Expected duration of the bridging operation */ export type BridgingPluginResult = { /** User operation to execute the bridge */ @@ -58,19 +75,19 @@ export type BridgingPluginResult = { /** * Parameters for generating a bridge user operation + * @property fromChain - {@link Chain} Source chain for the bridge + * @property toChain - {@link Chain} Destination chain for the bridge + * @property account - {@link BaseMultichainSmartAccount} Smart account to execute the bridging + * @property tokenMapping - {@link MultichainAddressMapping} Token addresses across chains + * @property bridgingAmount - Amount to bridge as BigInt */ -export type BridgingUserOpParams = { - /** Source chain for the bridge */ +export type BridgingUserOpParams = Prettify<{ 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 @@ -118,17 +135,32 @@ export type BridgingInstructions = { * 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 + * @param params - Configuration for the bridge operation + * @param params.account - {@link BaseMultichainSmartAccount} The smart account to execute the bridging + * @param params.amount - The amount to bridge as BigInt + * @param params.toChain - {@link Chain} The destination chain + * @param params.unifiedBalance - {@link UnifiedERC20Balance} Current token balances across chains + * @param params.bridgingPlugins - Optional array of {@link BridgingPlugin} to use (defaults to Across) + * @param params.feeData - Optional fee configuration for the transaction + * + * @returns Promise resolving to {@link BridgingInstructions} containing all necessary operations + * + * @throws Error if insufficient balance is available for bridging + * @throws Error if chain configuration is missing for any deployment + * * @example - * const instructions = await buildBridgeInstruction(client, { - * amount: BigInt(1000), - * mcToken: mcUSDC, - * chain: base - * }) + * const bridgeInstructions = await buildBridgeInstructions({ + * account: myMultichainAccount, + * amount: BigInt("1000000"), // 1 USDC + * toChain: optimism, + * unifiedBalance: myTokenBalance, + * bridgingPlugins: [acrossPlugin], + * feeData: { + * txFeeChainId: 1, + * txFeeAmount: BigInt("100000") + * } + * }); */ - export const buildBridgeInstructions = async ( params: BuildBridgeInstructionParams ): Promise => { diff --git a/src/sdk/account/decorators/buildInstructions.ts b/src/sdk/account/decorators/buildInstructions.ts deleted file mode 100644 index c52a36fdc..000000000 --- a/src/sdk/account/decorators/buildInstructions.ts +++ /dev/null @@ -1,78 +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 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/instructions/buildBaseInstructions.test.ts b/src/sdk/account/decorators/instructions/buildBaseInstructions.test.ts new file mode 100644 index 000000000..5579373bf --- /dev/null +++ b/src/sdk/account/decorators/instructions/buildBaseInstructions.test.ts @@ -0,0 +1,62 @@ +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 { + type MultichainSmartAccount, + toMultichainNexusAccount +} from "../../toMultiChainNexusAccount" +import buildBaseInstructions from "./buildBaseInstructions" + +describe("mee:buildBaseInstructions", () => { + 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 instructions = await buildBaseInstructions( + { + account: mcNexus + }, + { + instructions: [ + { + chainId: base.id, + calls: [ + { + to: "0x0000000000000000000000000000000000000000", + gasLimit: 50000n, + value: 0n + } + ] + } + ] + } + ) + + expect(instructions.length).toBeGreaterThan(0) + }) +}) diff --git a/src/sdk/account/decorators/instructions/buildBaseInstructions.ts b/src/sdk/account/decorators/instructions/buildBaseInstructions.ts new file mode 100644 index 000000000..577b4b254 --- /dev/null +++ b/src/sdk/account/decorators/instructions/buildBaseInstructions.ts @@ -0,0 +1,47 @@ +import type { Instruction } from "../../../clients/decorators/mee" +import type { BaseInstructionsParams } from "../build" + +/** + * Parameters for building base instructions + * @property currentInstructions - Optional array of {@link Instruction} existing instructions to append to + * @property instruction - Single {@link Instruction} or array of instructions to add + */ +export type BuildBaseInstructionsParams = { + instructions: Instruction[] | Instruction +} + +/** + * Builds a base set of instructions by combining existing instructions with new ones + * + * @param params - {@link BuildBaseInstructionsParams} Configuration for building instructions + * @param params.currentInstructions - Optional array of existing instructions (defaults to empty array) + * @param params.instruction - Single instruction or array of instructions to append + * + * @returns Promise resolving to an array of {@link Instruction} + * + * @example + * const instructions = await buildBaseInstructions({ + * currentInstructions: existingInstructions, + * instruction: newInstruction + * }); + * + * @example + * // With multiple new instructions + * const instructions = await buildBaseInstructions({ + * currentInstructions: [], + * instruction: [instruction1, instruction2] + * }); + */ +export const buildBaseInstructions = async ( + baseParams: BaseInstructionsParams, + params: BuildBaseInstructionsParams +): Promise => { + const { currentInstructions = [] } = baseParams + const { instructions } = params + return [ + ...currentInstructions, + ...(Array.isArray(instructions) ? instructions : [instructions]) + ] +} + +export default buildBaseInstructions diff --git a/src/sdk/account/decorators/instructions/buildIntent.test.ts b/src/sdk/account/decorators/instructions/buildIntent.test.ts new file mode 100644 index 000000000..04b8b41ac --- /dev/null +++ b/src/sdk/account/decorators/instructions/buildIntent.test.ts @@ -0,0 +1,54 @@ +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 type { Instruction } from "../../../clients/decorators/mee/getQuote" +import { mcUSDC } from "../../../constants/tokens" +import { + type MultichainSmartAccount, + toMultichainNexusAccount +} from "../../toMultiChainNexusAccount" +import buildIntent from "./buildIntent" + +describe("mee:buildIntent", () => { + 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 instructions: Instruction[] = await buildIntent( + { account: mcNexus }, + { + amount: 100n, + mcToken: mcUSDC, + chain: base + } + ) + + expect(instructions).toHaveLength(1) + expect(instructions[0].calls).toHaveLength(2) + }) +}) diff --git a/src/sdk/account/decorators/instructions/buildIntent.ts b/src/sdk/account/decorators/instructions/buildIntent.ts new file mode 100644 index 000000000..a66d4e578 --- /dev/null +++ b/src/sdk/account/decorators/instructions/buildIntent.ts @@ -0,0 +1,65 @@ +import type { Chain, erc20Abi } from "viem" +import type { Instruction } from "../../../clients/decorators/mee" +import type { BaseMultichainSmartAccount } from "../../toMultiChainNexusAccount" +import type { MultichainContract } from "../../utils/getMultichainContract" +import type { BaseInstructionsParams } from "../build" +import buildBridgeInstructions from "../buildBridgeInstructions" +import { getUnifiedERC20Balance } from "../getUnifiedERC20Balance" + +/** + * Parameters for building bridge intent instructions + * @property amount - Amount of tokens to bridge as BigInt + * @property mcToken - {@link MultichainContract} The multichain token contract to bridge + * @property chain - {@link Chain} The destination chain for the bridge operation + */ +export type BuildIntentParameters = { + amount: bigint + mcToken: MultichainContract + chain: Chain +} +/** + * Parameters for building bridge intent instructions + * @property account - {@link BaseMultichainSmartAccount} The smart account to execute the bridging + * @property currentInstructions - Array of {@link Instruction} existing instructions to append to + * @property parameters - {@link BuildIntentParameters} The parameters for building the bridge intent + */ +export type BuildIntentParams = BuildIntentParameters + +/** + * Builds bridge intent instructions by checking unified token balance and creating necessary bridge operations + * + * @param params - {@link BuildIntentParams} Configuration for building the bridge intent + * @param params.account - The smart account to execute the bridging + * @param params.currentInstructions - Existing instructions to append to (defaults to empty array) + * @param params.amount - The amount to bridge + * @param params.mcToken - The multichain token contract + * @param params.chain - The destination chain + * + * @returns Promise resolving to an array of {@link Instruction} + * + * @example + * const bridgeIntentInstructions = await buildIntent({ + * account: myMultichainAccount, + * currentInstructions: [], + * amount: BigInt("1000000"), // 1 USDC + * mcToken: mcUSDC, + * chain: optimism + * }); + */ +export const buildIntent = async ( + baseParams: BaseInstructionsParams, + parameters: BuildIntentParams +): Promise => { + const { account, currentInstructions = [] } = baseParams + const { amount, mcToken, chain } = parameters + const unifiedBalance = await getUnifiedERC20Balance({ mcToken, account }) + const { instructions } = await buildBridgeInstructions({ + account, + amount: amount, + toChain: chain, + unifiedBalance + }) + return [...currentInstructions, ...instructions] +} + +export default buildIntent diff --git a/src/sdk/account/decorators/instructions/index.ts b/src/sdk/account/decorators/instructions/index.ts new file mode 100644 index 000000000..20e22c735 --- /dev/null +++ b/src/sdk/account/decorators/instructions/index.ts @@ -0,0 +1,2 @@ +export * from "./buildBaseInstructions" +export * from "./buildIntent" diff --git a/src/sdk/account/toMultiChainNexusAccount.test.ts b/src/sdk/account/toMultiChainNexusAccount.test.ts index 310a5d95f..9074332f9 100644 --- a/src/sdk/account/toMultiChainNexusAccount.test.ts +++ b/src/sdk/account/toMultiChainNexusAccount.test.ts @@ -106,7 +106,7 @@ describe("mee.toMultiChainNexusAccount", async () => { test("mcNexus to have decorators successfully applied", async () => { expect(mcNexus.getUnifiedERC20Balance).toBeInstanceOf(Function) - expect(mcNexus.buildInstructions).toBeInstanceOf(Function) + expect(mcNexus.build).toBeInstanceOf(Function) expect(mcNexus.buildBridgeInstructions).toBeInstanceOf(Function) expect(mcNexus.queryBridge).toBeDefined() }) diff --git a/src/sdk/account/toMultiChainNexusAccount.ts b/src/sdk/account/toMultiChainNexusAccount.ts index ba7f9567d..3d5e34c58 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 BuildInstructionTypes, + build as buildDecorator +} from "./decorators/build" 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,15 @@ export type MultichainSmartAccount = BaseMultichainSmartAccount & { * @param params - The parameters for the balance requirement * @returns Instructions for any required bridging operations * @example - * const instructions = await mcAccount.buildInstructions({ + * const instructions = await mcAccount.build({ * amount: BigInt(1000), * mcToken: mcUSDC, * chain: base * }) */ - buildInstructions: ( - params: Omit + build: ( + params: BuildInstructionTypes, + currentInstructions?: Instruction[] ) => Promise /** * Function to build instructions for bridging a token across all deployments @@ -150,9 +151,11 @@ export async function toMultichainNexusAccount( return getUnifiedERC20BalanceDecorator({ mcToken, account: baseAccount }) } - const buildInstructions = ( - params: Omit - ) => buildInstructionsDecorator({ ...params, account: baseAccount }) + const build = ( + params: BuildInstructionTypes, + currentInstructions?: Instruction[] + ): Promise => + buildDecorator({ currentInstructions, account: baseAccount }, params) const buildBridgeInstructions = ( params: Omit @@ -164,7 +167,7 @@ export async function toMultichainNexusAccount( return { ...baseAccount, getUnifiedERC20Balance, - buildInstructions, + build, buildBridgeInstructions, queryBridge } diff --git a/src/sdk/clients/createMeeClient.test.ts b/src/sdk/clients/createMeeClient.test.ts index 1b82e6191..d0f3db819 100644 --- a/src/sdk/clients/createMeeClient.test.ts +++ b/src/sdk/clients/createMeeClient.test.ts @@ -114,11 +114,11 @@ 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 'buildInstructions'. They all are resolved in the 'getQuote' method under the hood. - const currentInstructions = await meeClient.account.buildInstructions({ - action: { - type: "DEFAULT", - parameters: [ + // including 'build'. They all are resolved in the 'getQuote' method under the hood. + const currentInstructions = await meeClient.account.build({ + type: "default", + data: { + instructions: [ { calls: [ { @@ -133,17 +133,17 @@ describe("mee.createMeeClient", async () => { } }) - const preparedInstructions = await meeClient.account.buildInstructions({ - currentInstructions, - action: { - type: "BRIDGE", - parameters: { + const preparedInstructions = await meeClient.account.build( + { + type: "intent", + data: { amount: BigInt(1000), mcToken: mcUSDC, chain: base } - } - }) + }, + currentInstructions + ) expect(preparedInstructions).toBeDefined() @@ -155,6 +155,8 @@ describe("mee.createMeeClient", async () => { } }) + console.log(quote.userOps) + expect(quote.userOps.length).toEqual(3) expect(quote).toBeDefined() expect(quote.paymentInfo.sender).toEqual( @@ -175,21 +177,19 @@ describe("mee.createMeeClient", async () => { // Create an array of instructions that will be executed as a single transaction const instructions = [ // First instruction: Bridge USDC tokens - mcNexus.buildInstructions({ - action: { - type: "BRIDGE", - parameters: { - amount: BigInt(1000), // Amount of tokens to bridge (in smallest unit, e.g., wei) - mcToken: mcUSDC, // The multichain USDC token being bridged - chain: base // Destination chain (Base network) - } + mcNexus.build({ + type: "intent", + data: { + amount: BigInt(1000), // Amount of tokens to bridge (in smallest unit, e.g., wei) + mcToken: mcUSDC, // The multichain USDC token being bridged + chain: base // Destination chain (Base network) } }), // Second instruction: Execute a simple call on the Base network - mcNexus.buildInstructions({ - action: { - type: "DEFAULT", - parameters: [ + mcNexus.build({ + type: "default", + data: { + instructions: [ { calls: [ { diff --git a/src/sdk/clients/decorators/mee/getQuote.test.ts b/src/sdk/clients/decorators/mee/getQuote.test.ts index 976bb275b..57fc0b43c 100644 --- a/src/sdk/clients/decorators/mee/getQuote.test.ts +++ b/src/sdk/clients/decorators/mee/getQuote.test.ts @@ -73,20 +73,18 @@ describe("mee.getQuote", () => { 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.build({ + type: "intent", + data: { + amount: BigInt(1000), + mcToken: mcUSDC, + chain: base } }), - mcNexus.buildInstructions({ - action: { - type: "DEFAULT", - parameters: [ + mcNexus.build({ + type: "default", + data: { + instructions: [ { calls: [ { From b26c193f905cbd7d82191690ec699cc249641540 Mon Sep 17 00:00:00 2001 From: Joe Pegler Date: Wed, 15 Jan 2025 21:40:27 +0000 Subject: [PATCH 08/11] chore: abstractJS --- README.md | 2 +- src/sdk/account/decorators/build.test.ts | 6 ++-- src/sdk/account/decorators/build.ts | 32 +++++-------------- src/sdk/clients/createMeeClient.test.ts | 4 +-- .../clients/decorators/mee/getQuote.test.ts | 2 +- 5 files changed, 15 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 82ff0abcd..41aaa070d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![Biconomy](https://img.shields.io/badge/Made_with_%F0%9F%8D%8A_by-Biconomy-ff4e17?style=flat)](https://biconomy.io) [![License MIT](https://img.shields.io/badge/License-MIT-blue?&style=flat)](./LICENSE) [![codecov](https://codecov.io/github/bcnmy/sdk/graph/badge.svg?token=DTdIR5aBDA)](https://codecov.io/github/bcnmy/sdk) -# SDK 🚀 +# abstractJS 🚀 [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/bcnmy/sdk) diff --git a/src/sdk/account/decorators/build.test.ts b/src/sdk/account/decorators/build.test.ts index 58d88c116..6e8543318 100644 --- a/src/sdk/account/decorators/build.test.ts +++ b/src/sdk/account/decorators/build.test.ts @@ -34,11 +34,11 @@ describe("mee:build", () => { meeClient = createMeeClient({ account: mcNexus }) }) - it("should use the default action while building instructions", async () => { + it("should use the base option while building instructions", async () => { const instructions = await build( { account: mcNexus }, { - type: "default", + type: "base", data: { instructions: [ { @@ -89,7 +89,7 @@ describe("mee:build", () => { const instructions = await build( { account: mcNexus, currentInstructions }, { - type: "default", + type: "base", data: { instructions: [ { diff --git a/src/sdk/account/decorators/build.ts b/src/sdk/account/decorators/build.ts index dbf46818a..02cb04eb0 100644 --- a/src/sdk/account/decorators/build.ts +++ b/src/sdk/account/decorators/build.ts @@ -4,9 +4,10 @@ import type { BaseMultichainSmartAccount } from "../toMultiChainNexusAccount" import type { MultichainContract } from "../utils/getMultichainContract" import { type BuildBaseInstructionsParams, - buildBaseInstructions -} from "./instructions/buildBaseInstructions" -import { type BuildIntentParams, buildIntent } from "./instructions/buildIntent" + type BuildIntentParams, + buildBaseInstructions, + buildIntent +} from "./instructions" /** * Base parameters for building instructions @@ -20,27 +21,12 @@ export type BaseInstructionsParams = { currentInstructions?: Instruction[] } -/** - * Configuration for bridging tokens between chains - * @property amount - The amount of tokens to bridge - * @property mcToken - The multichain token contract to bridge - * @property chain - The destination chain for the bridge operation - */ -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 -} - /** * Default build action which is used to build instructions for a chain */ export type BuildBaseInstruction = { /** The type of action */ - type: "default" + type: "base" /** The parameters for the action */ data: BuildBaseInstructionsParams } @@ -67,7 +53,7 @@ export type BuildInstructionTypes = * @param params - The build instructions configuration * @param params.account - {@link BaseMultichainSmartAccount} The multichain smart account to check balances for * @param params.currentInstructions - {@link Instruction[]} Optional array of existing instructions to append to - * @param params.type - The type of build action ("default" | "intent") + * @param params.type - The type of build action ("base" | "intent") * @param params.parameters - {@link BuildBaseInstruction} | {@link BuildIntentInstruction} * * @returns Promise resolving to an array of {@link Instruction} @@ -87,7 +73,7 @@ export type BuildInstructionTypes = * @example * // Default action example * const defaultInstructions = await build({ - * type: "default", + * type: "base", * parameters: myExistingInstruction, * account: myMultichainAccount * }) @@ -96,15 +82,13 @@ export const build = async ( baseParams: BaseInstructionsParams, parameters: BuildInstructionTypes ): Promise => { - console.log({ baseParams, parameters }) - const { type, data } = parameters switch (type) { case "intent": { return buildIntent(baseParams, data) } - case "default": { + case "base": { return buildBaseInstructions(baseParams, data) } default: { diff --git a/src/sdk/clients/createMeeClient.test.ts b/src/sdk/clients/createMeeClient.test.ts index d0f3db819..55cf37a2e 100644 --- a/src/sdk/clients/createMeeClient.test.ts +++ b/src/sdk/clients/createMeeClient.test.ts @@ -116,7 +116,7 @@ describe("mee.createMeeClient", async () => { // These can be any 'Instruction', or any helper method that resolves to a 'Instruction', // including 'build'. They all are resolved in the 'getQuote' method under the hood. const currentInstructions = await meeClient.account.build({ - type: "default", + type: "base", data: { instructions: [ { @@ -187,7 +187,7 @@ describe("mee.createMeeClient", async () => { }), // Second instruction: Execute a simple call on the Base network mcNexus.build({ - type: "default", + type: "base", data: { instructions: [ { diff --git a/src/sdk/clients/decorators/mee/getQuote.test.ts b/src/sdk/clients/decorators/mee/getQuote.test.ts index 57fc0b43c..7a55d443e 100644 --- a/src/sdk/clients/decorators/mee/getQuote.test.ts +++ b/src/sdk/clients/decorators/mee/getQuote.test.ts @@ -82,7 +82,7 @@ describe("mee.getQuote", () => { } }), mcNexus.build({ - type: "default", + type: "base", data: { instructions: [ { From 8320f38599bc622f97b549aac7f7348585f94e9b Mon Sep 17 00:00:00 2001 From: Joe Pegler Date: Thu, 16 Jan 2025 15:59:40 +0000 Subject: [PATCH 09/11] chore: tsdocs --- src/sdk/account/decorators/build.ts | 78 +++++++------ .../decorators/buildBridgeInstructions.ts | 64 +++++------ src/sdk/account/decorators/getFactoryData.ts | 73 ++++++++---- src/sdk/account/decorators/getNexusAddress.ts | 105 ++++++++++++------ .../decorators/getUnifiedERC20Balance.ts | 54 ++++++--- src/sdk/account/decorators/index.ts | 7 ++ .../buildBaseInstructions.test.ts | 6 +- .../instructions/buildBaseInstructions.ts | 47 -------- .../instructions/buildDefaultInstructions.ts | 49 ++++++++ .../decorators/instructions/buildIntent.ts | 41 ++++--- .../account/decorators/instructions/index.ts | 2 +- src/sdk/account/decorators/queryBridge.ts | 50 +++++++-- src/sdk/account/toMultiChainNexusAccount.ts | 43 +++++-- src/sdk/account/utils/Types.ts | 5 +- src/sdk/account/utils/contractSimulation.ts | 24 ++++ src/sdk/account/utils/deepHexlify.ts | 38 +++++++ src/sdk/account/utils/explorer.ts | 57 +++++----- .../account/utils/getMultichainContract.ts | 41 ++++++- src/sdk/account/utils/index.ts | 5 + src/sdk/account/utils/toFeeToken.test.ts | 46 ++++++++ src/sdk/account/utils/toFeeToken.ts | 33 ++++++ src/sdk/clients/createMeeClient.test.ts | 2 - 22 files changed, 602 insertions(+), 268 deletions(-) create mode 100644 src/sdk/account/decorators/index.ts delete mode 100644 src/sdk/account/decorators/instructions/buildBaseInstructions.ts create mode 100644 src/sdk/account/decorators/instructions/buildDefaultInstructions.ts create mode 100644 src/sdk/account/utils/toFeeToken.test.ts create mode 100644 src/sdk/account/utils/toFeeToken.ts diff --git a/src/sdk/account/decorators/build.ts b/src/sdk/account/decorators/build.ts index 55f13dc9d..e7b1f4ace 100644 --- a/src/sdk/account/decorators/build.ts +++ b/src/sdk/account/decorators/build.ts @@ -1,11 +1,13 @@ import type { Instruction } from "../../clients/decorators/mee/getQuote" import type { BaseMultichainSmartAccount } from "../toMultiChainNexusAccount" import { - type BuildBaseInstructionsParams, - type BuildIntentParams, - buildBaseInstructions, + type BuildDefaultInstructionsParams, + buildDefaultInstructions +} from "./instructions/buildDefaultInstructions" +import { + type BuildIntentParameters, buildIntent -} from "./instructions" +} from "./instructions/buildIntent" /** * Base parameters for building instructions @@ -13,68 +15,74 @@ import { * @property currentInstructions - {@link Instruction[]} Optional array of existing instructions to append to */ export type BaseInstructionsParams = { - /** The multichain smart account to check balances for */ account: BaseMultichainSmartAccount - /** The current instructions */ currentInstructions?: Instruction[] } /** * Default build action which is used to build instructions for a chain + * @property type - Literal "default" to identify the action type + * @property data - {@link BuildDefaultInstructionsParams} The parameters for the action */ -export type BuildBaseInstruction = { - /** The type of action */ +export type BuildDefaultInstruction = { type: "default" - /** The parameters for the action */ - data: BuildBaseInstructionsParams + data: BuildDefaultInstructionsParams } /** * Bridge build action which is used to build instructions for bridging funds from other chains + * @property type - Literal "intent" to identify the action type + * @property data - {@link BuildIntentParams} The parameters for the bridge action */ export type BuildIntentInstruction = { - /** The type of action */ type: "intent" - /** The parameters for the action */ - data: BuildIntentParams + data: BuildIntentParameters } + /** - * The types of build instructions + * Union type of all possible build instruction types */ export type BuildInstructionTypes = - | BuildBaseInstruction + | BuildDefaultInstruction | BuildIntentInstruction /** * Builds transaction instructions based on the provided action type and parameters * - * @param params - The build instructions configuration - * @param params.account - {@link BaseMultichainSmartAccount} The multichain smart account to check balances for - * @param params.currentInstructions - {@link Instruction[]} Optional array of existing instructions to append to - * @param params.type - The type of build action ("default" | "intent") - * @param params.parameters - {@link BuildBaseInstruction} | {@link BuildIntentInstruction} + * @param baseParams - {@link BaseInstructionsParams} Base configuration for instructions + * @param baseParams.account - The multichain smart account to check balances for + * @param baseParams.currentInstructions - Optional array of existing instructions to append to + * @param parameters - {@link BuildInstructionTypes} The build action configuration + * @param parameters.type - The type of build action ("default" | "intent") + * @param parameters.data - Action-specific parameters based on the type * * @returns Promise resolving to an array of {@link Instruction} * * @example * // Bridge tokens example - * const bridgeInstructions = await build({ - * type: "intent", - * parameters: { - * amount: BigInt(1000), - * mcToken: mcUSDC, - * chain: base - * }, - * account: myMultichainAccount - * }) + * const bridgeInstructions = await build( + * { account: myMultichainAccount }, + * { + * type: "intent", + * data: { + * amount: BigInt(1000000), + * mcToken: mcUSDC, + * chain: optimism + * } + * } + * ); * * @example * // Default action example - * const defaultInstructions = await build({ - * type: "default", - * parameters: myExistingInstruction, - * account: myMultichainAccount - * }) + * const defaultInstructions = await build( + * { account: myMultichainAccount }, + * { + * type: "default", + * data: { + * instructions: myExistingInstruction + * } + * } + * ); */ export const build = async ( baseParams: BaseInstructionsParams, @@ -87,7 +95,7 @@ export const build = async ( return buildIntent(baseParams, data) } case "default": { - return buildBaseInstructions(baseParams, data) + return buildDefaultInstructions(baseParams, data) } default: { throw new Error(`Unknown build action type: ${type}`) diff --git a/src/sdk/account/decorators/buildBridgeInstructions.ts b/src/sdk/account/decorators/buildBridgeInstructions.ts index 421e3fd98..83d2b7d52 100644 --- a/src/sdk/account/decorators/buildBridgeInstructions.ts +++ b/src/sdk/account/decorators/buildBridgeInstructions.ts @@ -1,4 +1,4 @@ -import type { Address, Chain, Prettify } from "viem" +import type { Address, Chain } from "viem" import type { Instruction } from "../../clients/decorators/mee/getQuote" import type { BaseMultichainSmartAccount } from "../toMultiChainNexusAccount" import { toAcrossPlugin } from "../utils/toAcrossPlugin" @@ -11,24 +11,20 @@ import { queryBridge } from "./queryBridge" * @property chainId - The numeric ID of the chain * @property address - {@link Address} The token's contract address on the chain */ -export type AddressMapping = Prettify<{ - /** The numeric ID of the chain */ +export type AddressMapping = { chainId: number - /** The token's contract address on the chain */ address: Address -}> +} /** * Cross-chain token address mapping with helper functions * @property deployments - Array of {@link AddressMapping} containing token addresses per chain * @property on - Function to retrieve token address for a specific chain ID */ -export type MultichainAddressMapping = Prettify<{ - /** Array of {@link AddressMapping} containing token addresses per chain */ +export type MultichainAddressMapping = { deployments: AddressMapping[] - /** Returns the token address for a given chain ID */ on: (chainId: number) => Address -}> +} /** * Fee data for the transaction fee @@ -36,9 +32,7 @@ export type MultichainAddressMapping = Prettify<{ * @property txFeeAmount - The amount of tx fee to pay */ export type FeeData = { - /** The chain ID where the tx fee is paid */ txFeeChainId: number - /** The amount of tx fee to pay */ txFeeAmount: bigint } @@ -48,15 +42,15 @@ export type FeeData = { * @property unifiedBalance - {@link UnifiedERC20Balance} Token balance information across all chains * @property amount - Amount of tokens to bridge as BigInt * @property bridgingPlugins - Optional array of {@link BridgingPlugin} to use for bridging - * @property feeData - Optional fee information for the transaction + * @property feeData - Optional {@link FeeData} for the transaction */ -export type MultichainBridgingParams = Prettify<{ +export type MultichainBridgingParams = { toChain: Chain unifiedBalance: UnifiedERC20Balance amount: bigint bridgingPlugins?: BridgingPlugin[] feeData?: FeeData -}> +} /** * Result of a bridging plugin operation @@ -65,11 +59,8 @@ export type MultichainBridgingParams = Prettify<{ * @property bridgingDurationExpectedMs - Expected duration of the bridging 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 } @@ -81,13 +72,13 @@ export type BridgingPluginResult = { * @property tokenMapping - {@link MultichainAddressMapping} Token addresses across chains * @property bridgingAmount - Amount to bridge as BigInt */ -export type BridgingUserOpParams = Prettify<{ +export type BridgingUserOpParams = { fromChain: Chain toChain: Chain account: BaseMultichainSmartAccount tokenMapping: MultichainAddressMapping bridgingAmount: bigint -}> +} /** * Interface for a bridging plugin implementation @@ -99,34 +90,27 @@ export type BridgingPlugin = { ) => Promise } -export type BuildBridgeInstructionParams = MultichainBridgingParams & { - /** Smart account to execute the bridging */ - account: BaseMultichainSmartAccount -} - /** * Single bridge operation result + * @property userOp - {@link Instruction} User operation to execute + * @property receivedAtDestination - Expected amount to be received at destination + * @property bridgingDurationExpectedMs - Expected duration of the bridging operation */ 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 + * @property instructions - Array of {@link Instruction} to execute + * @property meta - Meta information about the bridging process */ 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[] } } @@ -135,13 +119,13 @@ export type BridgingInstructions = { * Makes sure that the user has enough funds on the selected chain before filling the * supertransaction. Bridges funds from other chains if needed. * - * @param params - Configuration for the bridge operation - * @param params.account - {@link BaseMultichainSmartAccount} The smart account to execute the bridging - * @param params.amount - The amount to bridge as BigInt - * @param params.toChain - {@link Chain} The destination chain - * @param params.unifiedBalance - {@link UnifiedERC20Balance} Current token balances across chains - * @param params.bridgingPlugins - Optional array of {@link BridgingPlugin} to use (defaults to Across) - * @param params.feeData - Optional fee configuration for the transaction + * @param params - {@link MultichainBridgingParams} Configuration for the bridge operation + * @param params.account - The smart account to execute the bridging + * @param params.amount - The amount to bridge + * @param params.toChain - The destination chain + * @param params.unifiedBalance - Current token balances across chains + * @param params.bridgingPlugins - Optional array of bridging plugins (defaults to Across) + * @param params.feeData - Optional fee configuration * * @returns Promise resolving to {@link BridgingInstructions} containing all necessary operations * @@ -162,7 +146,9 @@ export type BridgingInstructions = { * }); */ export const buildBridgeInstructions = async ( - params: BuildBridgeInstructionParams + params: MultichainBridgingParams & { + account: BaseMultichainSmartAccount + } ): Promise => { const { account, diff --git a/src/sdk/account/decorators/getFactoryData.ts b/src/sdk/account/decorators/getFactoryData.ts index e27c3a0c5..8c0325a46 100644 --- a/src/sdk/account/decorators/getFactoryData.ts +++ b/src/sdk/account/decorators/getFactoryData.ts @@ -18,11 +18,10 @@ 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 + * @property signerAddress - {@link Address} The address of the EOA signer + * @property index - Account index as BigInt for deterministic deployment + * @property attesters - Array of {@link Address} attester addresses for account verification + * @property attesterThreshold - Minimum number of attesters required for validation */ export type GetK1FactoryDataParams = { signerAddress: Address @@ -33,8 +32,22 @@ export type GetK1FactoryDataParams = { /** * Generates encoded factory data for K1 account creation - * @param {GetK1FactoryDataParams} params - Parameters for K1 account creation - * @returns {Promise} Encoded function data for account creation + * + * @param params - {@link GetK1FactoryDataParams} Parameters for K1 account creation + * @param params.signerAddress - The address of the EOA signer + * @param params.index - Account index for deterministic deployment + * @param params.attesters - Array of attester addresses + * @param params.attesterThreshold - Minimum number of attesters required + * + * @returns Promise resolving to {@link Hex} encoded function data for account creation + * + * @example + * const factoryData = await getK1FactoryData({ + * signerAddress: "0x123...", + * index: BigInt(0), + * attesters: ["0xabc...", "0xdef..."], + * attesterThreshold: 2 + * }); */ export const getK1FactoryData = async ({ signerAddress, @@ -52,13 +65,15 @@ export const getK1FactoryData = async ({ /** * 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 + * @property signerAddress - {@link Address} The address of the EOA signer + * @property index - Account index as BigInt for deterministic deployment + * @property attesters - Array of {@link Address} attester addresses for account verification + * @property attesterThreshold - Minimum number of attesters required for validation + * @property validatorAddress - Optional {@link Address} of the validator (defaults to MEE_VALIDATOR_ADDRESS) + * @property registryAddress - Optional {@link Address} of the registry contract (defaults to REGISTRY_ADDRESS) + * @property publicClient - {@link PublicClient} Viem public client instance + * @property walletClient - {@link WalletClient} Viem wallet client instance + * @property bootStrapAddress - Optional {@link Address} of the bootstrap contract (defaults to NEXUS_BOOTSTRAP_ADDRESS) */ export type GetMeeFactoryDataParams = GetK1FactoryDataParams & { validatorAddress?: Address @@ -70,8 +85,30 @@ export type GetMeeFactoryDataParams = GetK1FactoryDataParams & { /** * Generates encoded factory data for MEE account creation - * @param {GetMeeFactoryDataParams} params - Parameters for MEE account creation - * @returns {Promise} Encoded function data for account creation + * + * @param params - {@link GetMeeFactoryDataParams} Parameters for MEE account creation + * @param params.validatorAddress - Optional validator address + * @param params.attesters - Array of attester addresses + * @param params.registryAddress - Optional registry contract address + * @param params.attesterThreshold - Minimum number of attesters required + * @param params.publicClient - Viem public client instance + * @param params.walletClient - Viem wallet client instance + * @param params.bootStrapAddress - Optional bootstrap contract address + * @param params.signerAddress - The address of the EOA signer + * @param params.index - Account index for deterministic deployment + * + * @returns Promise resolving to {@link Hex} encoded function data for account creation + * + * @example + * const factoryData = await getMeeFactoryData({ + * signerAddress: "0x123...", + * index: BigInt(0), + * attesters: ["0xabc...", "0xdef..."], + * attesterThreshold: 2, + * publicClient: viemPublicClient, + * walletClient: viemWalletClient, + * validatorAddress: "0x789..." // optional + * }); */ export const getMeeFactoryData = async ({ validatorAddress = MEE_VALIDATOR_ADDRESS, @@ -106,13 +143,11 @@ export const getMeeFactoryData = async ({ const salt = pad(toHex(index), { size: 32 }) - const factoryData = encodeFunctionData({ + return 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/decorators/getNexusAddress.ts b/src/sdk/account/decorators/getNexusAddress.ts index e5ff80ef5..7f7e0227e 100644 --- a/src/sdk/account/decorators/getNexusAddress.ts +++ b/src/sdk/account/decorators/getNexusAddress.ts @@ -9,37 +9,47 @@ import { AccountFactoryAbi } from "../../constants/abi/AccountFactory" import { K1ValidatorFactoryAbi } from "../../constants/abi/K1ValidatorFactory" /** - * Get the counterfactual address of a signer + * Parameters for getting the K1 counterfactual address + * @property publicClient - {@link PublicClient} The public client to use for the read contract + * @property signerAddress - {@link Address} The address of the EOA signer + * @property index - Optional BigInt index for deterministic deployment (defaults to 0) + * @property attesters - Optional array of {@link Address} attester addresses (defaults to [RHINESTONE_ATTESTER_ADDRESS]) + * @property threshold - Optional number of required attesters (defaults to 1) + * @property factoryAddress - Optional {@link Address} of the factory contract (defaults to MAINNET_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS) + */ +export type K1CounterFactualAddressParams< + ExtendedPublicClient extends PublicClient +> = { + publicClient: ExtendedPublicClient + signerAddress: Address + index?: bigint + attesters?: Address[] + threshold?: number + factoryAddress?: Address +} + +/** + * Gets the counterfactual address for a K1 Nexus account * - * @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 attesters - The attesters to use - * @param threshold - The threshold of the attesters - * @param factoryAddress - The factory address to use - * @returns The counterfactual address + * @param params - {@link K1CounterFactualAddressParams} Configuration for address computation + * @param params.publicClient - The public client to use for the read contract + * @param params.signerAddress - The address of the EOA signer + * @param params.index - Optional account index (defaults to 0) + * @param params.attesters - Optional array of attester addresses + * @param params.threshold - Optional attestation threshold + * @param params.factoryAddress - Optional factory contract address + * + * @returns Promise resolving to the {@link Address} of the counterfactual account * * @example - * ```ts - * const counterFactualAddress = await getCounterFactualAddress(publicClient, signerAddress) - * ``` + * const accountAddress = await getK1NexusAddress({ + * publicClient: viemPublicClient, + * signerAddress: "0x123...", + * index: BigInt(0), + * attesters: ["0xabc..."], + * threshold: 1 + * }); */ - -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 >( @@ -62,19 +72,40 @@ export const getK1NexusAddress = async < }) } -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 - } +/** + * Parameters for getting the MEE counterfactual address + * @property publicClient - {@link PublicClient} The public client to use for the read contract + * @property signerAddress - {@link Address} The address of the EOA signer + * @property index - Optional BigInt index for deterministic deployment (defaults to 0) + */ +export type MeeCounterFactualAddressParams< + ExtendedPublicClient extends PublicClient +> = { + publicClient: ExtendedPublicClient + signerAddress: Address + index?: bigint +} +/** + * Gets the counterfactual address for a MEE Nexus account + * + * @param params - {@link MeeCounterFactualAddressParams} Configuration for address computation + * @param params.publicClient - The public client to use for the read contract + * @param params.signerAddress - The address of the EOA signer + * @param params.index - Optional account index (defaults to 0) + * + * @returns Promise resolving to the {@link Address} of the counterfactual account + * + * @example + * const accountAddress = await getMeeNexusAddress({ + * publicClient: viemPublicClient, + * signerAddress: "0x123...", + * index: BigInt(0) + * }); + */ export const getMeeNexusAddress = async ( params: MeeCounterFactualAddressParams -) => { +): Promise
=> { const salt = pad(toHex(params.index ?? 0n), { size: 32 }) const { publicClient, signerAddress } = params diff --git a/src/sdk/account/decorators/getUnifiedERC20Balance.ts b/src/sdk/account/decorators/getUnifiedERC20Balance.ts index 3434373ae..e7798e47c 100644 --- a/src/sdk/account/decorators/getUnifiedERC20Balance.ts +++ b/src/sdk/account/decorators/getUnifiedERC20Balance.ts @@ -1,50 +1,72 @@ import { erc20Abi, getContract } from "viem" import type { BaseMultichainSmartAccount } from "../toMultiChainNexusAccount" +import type { MultichainToken } from "../utils/Types" import type { MultichainContract } from "../utils/getMultichainContract" /** * Represents a balance item with its decimal precision + * @property balance - The token balance as a bigint + * @property decimals - Number of decimal places for the token */ 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 balance item for a specific chain + * @property balance - The token balance as a bigint + * @property decimals - Number of decimal places for the token + * @property chainId - The numeric ID of the chain this balance is on + */ +export type RelevantBalance = UnifiedBalanceItem & { + chainId: number +} /** * Represents a unified balance across multiple chains for an ERC20 token + * @property mcToken - {@link MultichainContract} The multichain ERC20 token contract + * @property breakdown - Array of {@link RelevantBalance} Individual balance breakdown per chain + * @property balance - The total balance across all chains as a bigint + * @property decimals - Number of decimal places for the token */ export type UnifiedERC20Balance = { - /** The multichain ERC20 token contract */ - mcToken: MultichainContract - /** Individual balance breakdown per chain */ + mcToken: MultichainToken breakdown: RelevantBalance[] } & UnifiedBalanceItem +/** + * Parameters for fetching unified ERC20 balance + * @property mcToken - {@link MultichainContract} The multichain ERC20 token contract + * @property account - {@link BaseMultichainSmartAccount} The multichain smart account to check balances for + */ export type GetUnifiedERC20BalanceParameters = { - /** The multichain ERC20 token contract */ - mcToken: MultichainContract - /** The multichain smart account to check balances for */ + mcToken: MultichainToken account: BaseMultichainSmartAccount } /** * Fetches and aggregates ERC20 token balances across multiple chains for a given account * - * @param parameters - The input parameters + * @param parameters - {@link GetUnifiedERC20BalanceParameters} Configuration for balance fetching * @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 + * @param parameters.account - The multichain smart account to check balances for + * + * @returns Promise resolving to {@link UnifiedERC20Balance} containing total balance and per-chain breakdown + * + * @throws Error if token decimals mismatch across chains * * @example - * const balance = await getUnifiedERC20Balance(client, { + * const balance = await getUnifiedERC20Balance({ * mcToken: mcUSDC, - * deployments: mcNexus.deployments - * }) + * account: myMultichainAccount + * }); + * + * console.log(`Total balance: ${balance.balance}`); + * console.log(`Decimals: ${balance.decimals}`); + * balance.breakdown.forEach(chainBalance => { + * console.log(`Chain ${chainBalance.chainId}: ${chainBalance.balance}`); + * }); */ export async function getUnifiedERC20Balance( parameters: GetUnifiedERC20BalanceParameters diff --git a/src/sdk/account/decorators/index.ts b/src/sdk/account/decorators/index.ts new file mode 100644 index 000000000..ed0e9859b --- /dev/null +++ b/src/sdk/account/decorators/index.ts @@ -0,0 +1,7 @@ +export * from "./getNexusAddress.js" +export * from "./build.js" +export * from "./queryBridge.js" +export * from "./getUnifiedERC20Balance.js" +export * from "./getFactoryData.js" +export * from "./buildBridgeInstructions.js" +export * from "./instructions" diff --git a/src/sdk/account/decorators/instructions/buildBaseInstructions.test.ts b/src/sdk/account/decorators/instructions/buildBaseInstructions.test.ts index 5579373bf..ebbc2c2f9 100644 --- a/src/sdk/account/decorators/instructions/buildBaseInstructions.test.ts +++ b/src/sdk/account/decorators/instructions/buildBaseInstructions.test.ts @@ -11,9 +11,9 @@ import { type MultichainSmartAccount, toMultichainNexusAccount } from "../../toMultiChainNexusAccount" -import buildBaseInstructions from "./buildBaseInstructions" +import buildDefaultInstructions from "./buildDefaultInstructions" -describe("mee:buildBaseInstructions", () => { +describe("mee:buildDefaultInstructions", () => { let network: NetworkConfig let eoaAccount: LocalAccount let paymentChain: Chain @@ -37,7 +37,7 @@ describe("mee:buildBaseInstructions", () => { }) it("should call the bridge with a unified balance", async () => { - const instructions = await buildBaseInstructions( + const instructions = await buildDefaultInstructions( { account: mcNexus }, diff --git a/src/sdk/account/decorators/instructions/buildBaseInstructions.ts b/src/sdk/account/decorators/instructions/buildBaseInstructions.ts deleted file mode 100644 index 577b4b254..000000000 --- a/src/sdk/account/decorators/instructions/buildBaseInstructions.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { Instruction } from "../../../clients/decorators/mee" -import type { BaseInstructionsParams } from "../build" - -/** - * Parameters for building base instructions - * @property currentInstructions - Optional array of {@link Instruction} existing instructions to append to - * @property instruction - Single {@link Instruction} or array of instructions to add - */ -export type BuildBaseInstructionsParams = { - instructions: Instruction[] | Instruction -} - -/** - * Builds a base set of instructions by combining existing instructions with new ones - * - * @param params - {@link BuildBaseInstructionsParams} Configuration for building instructions - * @param params.currentInstructions - Optional array of existing instructions (defaults to empty array) - * @param params.instruction - Single instruction or array of instructions to append - * - * @returns Promise resolving to an array of {@link Instruction} - * - * @example - * const instructions = await buildBaseInstructions({ - * currentInstructions: existingInstructions, - * instruction: newInstruction - * }); - * - * @example - * // With multiple new instructions - * const instructions = await buildBaseInstructions({ - * currentInstructions: [], - * instruction: [instruction1, instruction2] - * }); - */ -export const buildBaseInstructions = async ( - baseParams: BaseInstructionsParams, - params: BuildBaseInstructionsParams -): Promise => { - const { currentInstructions = [] } = baseParams - const { instructions } = params - return [ - ...currentInstructions, - ...(Array.isArray(instructions) ? instructions : [instructions]) - ] -} - -export default buildBaseInstructions diff --git a/src/sdk/account/decorators/instructions/buildDefaultInstructions.ts b/src/sdk/account/decorators/instructions/buildDefaultInstructions.ts new file mode 100644 index 000000000..5cfecab33 --- /dev/null +++ b/src/sdk/account/decorators/instructions/buildDefaultInstructions.ts @@ -0,0 +1,49 @@ +import type { Instruction } from "../../../clients/decorators/mee" +import type { BaseInstructionsParams } from "../build" + +/** + * Parameters for building default instructions + * @property instructions - Single {@link Instruction} or array of instructions to add + */ +export type BuildDefaultParams = { + instructions: Instruction[] | Instruction +} + +/** + * Parameters for building base instructions + * @property currentInstructions - Optional array of {@link Instruction} existing instructions to append to + * @property instructions - Single {@link Instruction} or array of instructions to add + */ +export type BuildDefaultInstructionsParams = BaseInstructionsParams & + BuildDefaultParams + +/** + * Builds a base set of instructions by combining existing instructions with new ones + * + * @param baseParams - {@link BaseInstructionsParams} Base configuration + * @param baseParams.currentInstructions - Optional array of existing instructions (defaults to empty array) + * @param params - {@link BuildDefaultInstructionsParams} Instructions configuration + * @param params.instructions - Single instruction or array of instructions to append + * + * @returns Promise resolving to an array of {@link Instruction} + * + * @example + * // Adding a single instruction + * const instructions = await buildDefaultInstructions( + * { currentInstructions: existingInstructions }, + * { instructions: newInstruction } + * ); + */ +export const buildDefaultInstructions = async ( + baseParams: BaseInstructionsParams, + params: BuildDefaultParams +): Promise => { + const { currentInstructions = [] } = baseParams + const { instructions } = params + return [ + ...currentInstructions, + ...(Array.isArray(instructions) ? instructions : [instructions]) + ] +} + +export default buildDefaultInstructions diff --git a/src/sdk/account/decorators/instructions/buildIntent.ts b/src/sdk/account/decorators/instructions/buildIntent.ts index a66d4e578..c144a78f2 100644 --- a/src/sdk/account/decorators/instructions/buildIntent.ts +++ b/src/sdk/account/decorators/instructions/buildIntent.ts @@ -1,6 +1,7 @@ import type { Chain, erc20Abi } from "viem" import type { Instruction } from "../../../clients/decorators/mee" import type { BaseMultichainSmartAccount } from "../../toMultiChainNexusAccount" +import type { MultichainToken } from "../../utils/Types" import type { MultichainContract } from "../../utils/getMultichainContract" import type { BaseInstructionsParams } from "../build" import buildBridgeInstructions from "../buildBridgeInstructions" @@ -14,41 +15,49 @@ import { getUnifiedERC20Balance } from "../getUnifiedERC20Balance" */ export type BuildIntentParameters = { amount: bigint - mcToken: MultichainContract + mcToken: MultichainToken chain: Chain } + /** * Parameters for building bridge intent instructions * @property account - {@link BaseMultichainSmartAccount} The smart account to execute the bridging * @property currentInstructions - Array of {@link Instruction} existing instructions to append to * @property parameters - {@link BuildIntentParameters} The parameters for building the bridge intent */ -export type BuildIntentParams = BuildIntentParameters +export type BuildIntentParams = BaseInstructionsParams & { + parameters: BuildIntentParameters +} /** * Builds bridge intent instructions by checking unified token balance and creating necessary bridge operations * - * @param params - {@link BuildIntentParams} Configuration for building the bridge intent - * @param params.account - The smart account to execute the bridging - * @param params.currentInstructions - Existing instructions to append to (defaults to empty array) - * @param params.amount - The amount to bridge - * @param params.mcToken - The multichain token contract - * @param params.chain - The destination chain + * @param baseParams - {@link BaseInstructionsParams} Base configuration + * @param baseParams.account - {@link BaseMultichainSmartAccount} The smart account to execute the bridging + * @param baseParams.currentInstructions - Array of existing instructions to append to + * @param parameters - {@link BuildIntentParameters} Bridge configuration + * @param parameters.amount - The amount to bridge + * @param parameters.mcToken - The multichain token contract + * @param parameters.chain - The destination chain * * @returns Promise resolving to an array of {@link Instruction} * * @example - * const bridgeIntentInstructions = await buildIntent({ - * account: myMultichainAccount, - * currentInstructions: [], - * amount: BigInt("1000000"), // 1 USDC - * mcToken: mcUSDC, - * chain: optimism - * }); + * const bridgeIntentInstructions = await buildIntent( + * { + * account: myMultichainAccount, + * currentInstructions: [] + * }, + * { + * amount: BigInt("1000000"), // 1 USDC + * mcToken: mcUSDC, + * chain: optimism + * } + * ); */ export const buildIntent = async ( baseParams: BaseInstructionsParams, - parameters: BuildIntentParams + parameters: BuildIntentParameters ): Promise => { const { account, currentInstructions = [] } = baseParams const { amount, mcToken, chain } = parameters diff --git a/src/sdk/account/decorators/instructions/index.ts b/src/sdk/account/decorators/instructions/index.ts index 20e22c735..416b18fe4 100644 --- a/src/sdk/account/decorators/instructions/index.ts +++ b/src/sdk/account/decorators/instructions/index.ts @@ -1,2 +1,2 @@ -export * from "./buildBaseInstructions" +export * from "./buildDefaultInstructions" export * from "./buildIntent" diff --git a/src/sdk/account/decorators/queryBridge.ts b/src/sdk/account/decorators/queryBridge.ts index 4ba42a7c7..834d9a9ae 100644 --- a/src/sdk/account/decorators/queryBridge.ts +++ b/src/sdk/account/decorators/queryBridge.ts @@ -9,6 +9,12 @@ import type { /** * Parameters for querying bridge operations + * @property fromChain - {@link Chain} Source chain for the bridge operation + * @property toChain - {@link Chain} Destination chain for the bridge operation + * @property plugin - Optional {@link BridgingPlugin} implementation (defaults to Across) + * @property amount - Amount to bridge in base units (wei) as BigInt + * @property account - {@link BaseMultichainSmartAccount} Smart account to execute the bridging + * @property tokenMapping - {@link MultichainAddressMapping} Token addresses across chains */ export type QueryBridgeParams = { /** Source chain for the bridge operation */ @@ -27,6 +33,12 @@ export type QueryBridgeParams = { /** * Result of a bridge query including chain info + * @property fromChainId - ID of the source chain + * @property amount - Amount to bridge in base units (wei) as BigInt + * @property receivedAtDestination - Expected amount to receive at destination after fees + * @property plugin - {@link BridgingPlugin} Plugin implementation used for the bridging operation + * @property userOp - {@link Instruction} Resolved user operation for the bridge + * @property bridgingDurationExpectedMs - Optional expected duration of the bridging operation in milliseconds */ export type BridgeQueryResult = { /** ID of the source chain */ @@ -45,20 +57,38 @@ export type BridgeQueryResult = { /** * 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 + * + * @param params - {@link QueryBridgeParams} Configuration for the bridge query + * @param params.fromChain - Source chain for the bridge operation + * @param params.toChain - Destination chain for the bridge operation + * @param params.plugin - Optional bridging plugin (defaults to Across) + * @param params.amount - Amount to bridge in base units (wei) + * @param params.account - Smart account to execute the bridging + * @param params.tokenMapping - Token addresses across chains + * + * @returns Promise resolving to {@link BridgeQueryResult} 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 - * }) + * fromChain: optimism, + * toChain: base, + * amount: BigInt("1000000"), // 1 USDC + * account: myMultichainAccount, + * tokenMapping: { + * deployments: [ + * { chainId: 10, address: "0x123..." }, + * { chainId: 8453, address: "0x456..." } + * ], + * on: (chainId) => deployments.find(d => d.chainId === chainId).address + * } + * }); + * + * if (result) { + * console.log(`Expected to receive: ${result.receivedAtDestination}`); + * console.log(`Expected duration: ${result.bridgingDurationExpectedMs}ms`); + * } */ export const queryBridge = async ( params: QueryBridgeParams diff --git a/src/sdk/account/toMultiChainNexusAccount.ts b/src/sdk/account/toMultiChainNexusAccount.ts index 3d5e34c58..eb63ee6e2 100644 --- a/src/sdk/account/toMultiChainNexusAccount.ts +++ b/src/sdk/account/toMultiChainNexusAccount.ts @@ -7,7 +7,6 @@ import { } 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 { @@ -16,7 +15,7 @@ import { } from "./decorators/build" import { type BridgingInstructions, - type BuildBridgeInstructionParams, + type MultichainBridgingParams, buildBridgeInstructions as buildBridgeInstructionsDecorator } from "./decorators/buildBridgeInstructions" import { @@ -28,6 +27,7 @@ import { type QueryBridgeParams, queryBridge as queryBridgeDecorator } from "./decorators/queryBridge" +import type { MultichainToken } from "./utils/Types" /** * Parameters required to create a multichain Nexus account */ @@ -64,7 +64,7 @@ export type MultichainSmartAccount = BaseMultichainSmartAccount & { * const balance = await mcAccount.getUnifiedERC20Balance(mcUSDC) */ getUnifiedERC20Balance: ( - mcToken: MultichainContract + mcToken: MultichainToken ) => Promise /** * Function to build instructions for bridging a token across all deployments @@ -93,7 +93,7 @@ export type MultichainSmartAccount = BaseMultichainSmartAccount & { * }) */ buildBridgeInstructions: ( - params: Omit + params: Omit ) => Promise /** * Function to query the bridge @@ -111,8 +111,33 @@ export type MultichainSmartAccount = BaseMultichainSmartAccount & { /** * Creates a multichain Nexus account across specified chains - * @param parameters - Configuration parameters for multichain account creation - * @returns Promise resolving to a MultichainSmartAccount instance + * + * @param parameters - {@link MultichainNexusParams} Configuration for multichain account creation + * @param parameters.signer - The signer instance used for account creation + * @param parameters.chains - Array of chains where the account will be deployed + * + * @returns Promise resolving to {@link MultichainSmartAccount} instance + * + * @throws Error if account creation fails on any chain + * + * @example + * const account = await toMultichainNexusAccount({ + * signer: mySigner, + * chains: [optimism, base] + * }); + * + * // Get deployment on specific chain + * const optimismDeployment = account.deploymentOn(10); + * + * // Check token balance across chains + * const balance = await account.getUnifiedERC20Balance(mcUSDC); + * + * // Build bridge transaction + * const bridgeInstructions = await account.buildBridgeInstructions({ + * amount: BigInt("1000000"), // 1 USDC + * mcToken: mcUSDC, + * toChain: base + * }); */ export async function toMultichainNexusAccount( parameters: MultichainNexusParams @@ -145,9 +170,7 @@ export async function toMultichainNexusAccount( deploymentOn } - const getUnifiedERC20Balance = ( - mcToken: MultichainContract - ) => { + const getUnifiedERC20Balance = (mcToken: MultichainToken) => { return getUnifiedERC20BalanceDecorator({ mcToken, account: baseAccount }) } @@ -158,7 +181,7 @@ export async function toMultichainNexusAccount( buildDecorator({ currentInstructions, account: baseAccount }, params) const buildBridgeInstructions = ( - params: Omit + params: Omit ) => buildBridgeInstructionsDecorator({ ...params, account: baseAccount }) const queryBridge = (params: QueryBridgeParams) => diff --git a/src/sdk/account/utils/Types.ts b/src/sdk/account/utils/Types.ts index da108fa73..c44cba089 100644 --- a/src/sdk/account/utils/Types.ts +++ b/src/sdk/account/utils/Types.ts @@ -1,4 +1,5 @@ -import type { Address, Hash, Hex, Log } from "viem" +import type { Address, Hash, Hex, Log, erc20Abi } from "viem" +import type { MultichainContract } from "./getMultichainContract" /** Represents the status of a transaction. */ export type TStatus = "success" | "reverted" @@ -100,3 +101,5 @@ export type Call = { data?: Hex | undefined value?: bigint | undefined } + +export type MultichainToken = MultichainContract diff --git a/src/sdk/account/utils/contractSimulation.ts b/src/sdk/account/utils/contractSimulation.ts index d8779fd74..81bbbde04 100644 --- a/src/sdk/account/utils/contractSimulation.ts +++ b/src/sdk/account/utils/contractSimulation.ts @@ -4,6 +4,30 @@ import { getChain } from "./getChain" import { getSimulationUserOp } from "./tenderlySimulation" import type { AnyUserOperation } from "./tenderlySimulation" +/** + * Simulates a user operation through the EntryPoint contract + * + * @param partialUserOp - {@link AnyUserOperation} The user operation to simulate + * @param chainId - The numeric ID of the chain to simulate on + * + * @returns Promise resolving to the simulation result from the EntryPoint contract + * + * @remarks + * This function creates a temporary public client and simulates the operation with + * a state override that ensures the sender has sufficient balance (1000 ETH). + * + * @example + * const userOp = { + * sender: "0x123...", + * nonce: 0n, + * initCode: "0x", + * callData: "0x...", + * // ... other UserOperation fields + * }; + * + * const simulation = await contractSimulation(userOp, 1); // Simulate on mainnet + * console.log("Simulation successful:", simulation.result); + */ export async function contractSimulation( partialUserOp: AnyUserOperation, chainId: number diff --git a/src/sdk/account/utils/deepHexlify.ts b/src/sdk/account/utils/deepHexlify.ts index 23f9a9bf9..8d04562a3 100644 --- a/src/sdk/account/utils/deepHexlify.ts +++ b/src/sdk/account/utils/deepHexlify.ts @@ -1,6 +1,44 @@ import { toHex } from "viem" import type { AnyData } from "../../modules/utils/Types" +/** + * Recursively converts all BigInt and BigNumber values in an object or array to hexadecimal strings + * + * @param obj - {@link AnyData} The input object or value to convert + * @returns {@link AnyData} The converted object or value with all BigInts and BigNumbers as hex strings + * + * @remarks + * - Functions are converted to undefined + * - Null, strings, and booleans remain unchanged + * - BigInts and BigNumbers are converted to hex strings + * - Arrays are processed recursively + * - Objects are processed recursively with their keys preserved + * + * @example + * // Converting simple values + * deepHexlify(BigInt(123)); // returns "0x7b" + * deepHexlify("hello"); // returns "hello" + * + * @example + * // Converting complex objects + * const result = deepHexlify({ + * amount: BigInt(1000000), + * token: "0x123...", + * values: [BigInt(1), BigInt(2)], + * nested: { + * balance: BigInt(500) + * } + * }); + * // Returns: + * // { + * // amount: "0xf4240", + * // token: "0x123...", + * // values: ["0x1", "0x2"], + * // nested: { + * // balance: "0x1f4" + * // } + * // } + */ export function deepHexlify(obj: AnyData): AnyData { if (typeof obj === "function") { return undefined diff --git a/src/sdk/account/utils/explorer.ts b/src/sdk/account/utils/explorer.ts index 9d17596c4..c3f26898d 100644 --- a/src/sdk/account/utils/explorer.ts +++ b/src/sdk/account/utils/explorer.ts @@ -3,18 +3,27 @@ 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 + * Gets the block explorer transaction link for a given chain + * + * @param hash - {@link Hex} The transaction hash to view + * @param chain_ - {@link Chain} The chain object, chain ID, or chain name + * @returns {@link Url} The complete URL to view the transaction + * + * @throws Error if the chain has no block explorer configured + * + * @example + * // Using chain object + * const url = getExplorerTxLink( + * "0x123...", + * optimism + * ); * * @example - * ```ts - * const hash = "0x123" - * const chain = optimism - * const url = getExplorerTxLink(hash, chain) - * console.log(url) // https://meescan.biconomy.io/details/0x123 - * ``` + * // Using chain ID + * const url = getExplorerTxLink( + * "0x123...", + * 10 // Optimism chain ID + * ); */ export const getExplorerTxLink = ( hash: Hex, @@ -29,32 +38,28 @@ export const getExplorerTxLink = ( } /** - * Get the jiffyscan tx link - * @param hash - The transaction hash - * @returns The jiffyscan tx link + * Gets the JiffyScan transaction link for a user operation + * + * @param userOpHash - {@link Hex} The user operation hash to view + * @returns {@link Url} The complete URL to view the user operation on JiffyScan * * @example - * ```ts - * const hash = "0x123" - * const url = getJiffyScanLink(hash) - * console.log(url) // https://jiffyscan.com/tx/0x123 - * ``` + * const url = getJiffyScanLink("0x123..."); + * console.log(url); // https://v2.jiffyscan.xyz/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 + * Gets the MeeScan transaction link for a user operation + * + * @param hash - {@link Hex} The transaction or user operation hash to view + * @returns {@link Url} The complete URL to view the transaction on MeeScan * * @example - * ```ts - * const hash = "0x123" - * const url = getMeeScanLink(hash) - * console.log(url) // https://meescan.biconomy.io/details/0x123 - * ``` + * const url = getMeeScanLink("0x123..."); + * 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/getMultichainContract.ts b/src/sdk/account/utils/getMultichainContract.ts index 18fdda122..762a4483e 100644 --- a/src/sdk/account/utils/getMultichainContract.ts +++ b/src/sdk/account/utils/getMultichainContract.ts @@ -23,6 +23,11 @@ import type { MultichainSmartAccount } from "../toMultiChainNexusAccount" /** * Contract instance capable of encoding transactions across multiple chains * @template TAbi - The contract ABI type + * @property abi - {@link Abi} The contract's ABI + * @property deployments - Map of chain IDs to {@link Address} contract addresses + * @property on - Function to get chain-specific contract instance + * @property addressOn - Function to get contract address for a specific chain + * @property read - Function to read contract state across multiple chains */ export type MultichainContract = { abi: TAbi @@ -44,6 +49,11 @@ export type MultichainContract = { > } +/** + * Chain-specific contract instance with typed function calls + * @template TAbi - The contract ABI type + * @property [functionName] - Each function from the ABI becomes a property that returns an {@link Instruction} + */ export type ChainSpecificContract = { [TFunctionName in ExtractAbiFunctionNames]: (params: { args: AbiParametersToPrimitiveTypes< @@ -100,21 +110,40 @@ function createChainSpecificContract( /** * Creates a contract instance that can encode function calls across multiple chains - * @template TAbi The contract ABI type + * + * @template TAbi - The contract ABI type + * @param config - Configuration for the multichain contract + * @param config.abi - {@link Abi} The contract's ABI + * @param config.deployments - Array of tuples containing [address, chainId] for each deployment + * + * @returns {@link MultichainContract} A contract instance that works across multiple chains + * + * @throws Error if attempting to access contract on an unsupported chain + * @throws Error if attempting to call a non-existent function + * @throws Error if attempting to read a non-view/pure function + * * @example * const mcUSDC = getMultichainContract({ * abi: erc20ABI, * deployments: [ - * ['0x...', optimism.id], - * ['0x...', base.id], - * // Other chains + * ["0x7F5c764cBc14f9669B88837ca1490cCa17c31607", optimism.id], // Optimism USDC + * ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", base.id] // Base USDC * ] * }); * - * const transferOp = usdc.on(optimism.id).transfer({ - * args: ['0x...', 100n], + * // Encode a transfer on Optimism + * const transferOp = mcUSDC.on(optimism.id).transfer({ + * args: ["0x123...", BigInt("1000000")], // 1 USDC * gasLimit: 100000n * }); + * + * // Read balances across multiple chains + * const balances = await mcUSDC.read({ + * onChains: [optimism, base], + * functionName: "balanceOf", + * args: ["0x123..."], + * account: myMultichainAccount + * }); */ export function getMultichainContract(config: { abi: TAbi diff --git a/src/sdk/account/utils/index.ts b/src/sdk/account/utils/index.ts index 72f09178e..9f7fcb4d8 100644 --- a/src/sdk/account/utils/index.ts +++ b/src/sdk/account/utils/index.ts @@ -5,3 +5,8 @@ export * from "./getChain.js" export * from "./Logger.js" export * from "./toSigner.js" export * from "../decorators/getNexusAddress.js" +export * from "./toFeeToken.js" +export * from "./getMultichainContract.js" +export * from "./explorer.js" +export * from "./toValidator.js" +export * from "./contractSimulation.js" diff --git a/src/sdk/account/utils/toFeeToken.test.ts b/src/sdk/account/utils/toFeeToken.test.ts new file mode 100644 index 000000000..095012848 --- /dev/null +++ b/src/sdk/account/utils/toFeeToken.test.ts @@ -0,0 +1,46 @@ +import type { Address, Chain, LocalAccount } from "viem" +import { base, baseSepolia } from "viem/chains" +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 { mcUSDC } from "../../constants/tokens" +import { + type MultichainSmartAccount, + toMultichainNexusAccount +} from "../toMultiChainNexusAccount" +import { toFeeToken } from "./toFeeToken" + +describe("mee.toFeeToken", () => { + 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 }) + }) + + test("should get a fee token", () => { + const feeToken = toFeeToken({ + token: mcUSDC, + chainId: paymentChain.id + }) + + expect(feeToken.address).toBe("0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85") + expect(feeToken.chainId).toBe(10) + }) +}) diff --git a/src/sdk/account/utils/toFeeToken.ts b/src/sdk/account/utils/toFeeToken.ts new file mode 100644 index 000000000..7457ebdbb --- /dev/null +++ b/src/sdk/account/utils/toFeeToken.ts @@ -0,0 +1,33 @@ +import type { FeeTokenInfo } from "../../clients/decorators/mee/getQuote" +import type { MultichainToken } from "./Types" + +/** + * Converts a multichain token to fee token information for a specific chain + * + * @param params - Configuration for the fee token conversion + * @param params.token - {@link MultichainToken} The multichain token to convert + * @param params.chainId - The numeric ID of the chain to get the token address for + * + * @returns {@link FeeTokenInfo} The fee token information for the specified chain + * + * @example + * const feeToken = toFeeToken({ + * token: mcUSDC, + * chainId: 10 // Optimism + * }); + * + * console.log(feeToken); + * // { + * // address: "0x7F5c764cBc14f9669B88837ca1490cCa17c31607", + * // chainId: 10 + * // } + */ +export function toFeeToken(params: { + token: MultichainToken + chainId: number +}): FeeTokenInfo { + return { + address: params.token.addressOn(params.chainId), + chainId: params.chainId + } +} diff --git a/src/sdk/clients/createMeeClient.test.ts b/src/sdk/clients/createMeeClient.test.ts index d0f3db819..3eac6cd92 100644 --- a/src/sdk/clients/createMeeClient.test.ts +++ b/src/sdk/clients/createMeeClient.test.ts @@ -155,8 +155,6 @@ describe("mee.createMeeClient", async () => { } }) - console.log(quote.userOps) - expect(quote.userOps.length).toEqual(3) expect(quote).toBeDefined() expect(quote.paymentInfo.sender).toEqual( From 07acd5fa73f21611478149d6a10dfbca11720817 Mon Sep 17 00:00:00 2001 From: Joe Pegler Date: Thu, 16 Jan 2025 16:41:49 +0000 Subject: [PATCH 10/11] chore: update readme --- CHANGELOG.md | 8 ++++ README.md | 40 ++++++++++------- package.json | 2 +- src/sdk/account/decorators/build.ts | 4 +- .../instructions/buildDefaultInstructions.ts | 13 +----- .../decorators/instructions/buildIntent.ts | 2 +- src/sdk/account/toMultiChainNexusAccount.ts | 2 +- .../account/toNexusAccount.addresses.test.ts | 12 ++++-- src/sdk/account/toNexusAccount.test.ts | 3 +- src/sdk/account/toNexusAccount.ts | 9 +++- src/sdk/account/utils/index.ts | 2 +- src/sdk/account/utils/toAcrossPlugin.ts | 43 +++++++++++++++++++ .../clients/createBicoBundlerClient.test.ts | 3 +- .../clients/createBicoPaymasterClient.test.ts | 2 - src/sdk/clients/createBundlerClient.test.ts | 11 +++-- src/sdk/clients/createHttpClient.ts | 1 + .../clients/createNexusSessionClient.test.ts | 9 ++-- .../clients/createSmartAccountClient.test.ts | 12 ++++-- src/sdk/clients/createSmartAccountClient.ts | 6 ++- .../decorators/bundler/getGasFeeValues.ts | 11 +++-- .../erc7579/erc7579.decorators.test.ts | 3 +- .../smartAccount/account.decorators.test.ts | 3 +- .../smartAccount/debugUserOperation.ts | 9 ++-- .../modules/k1Validator/toK1Validator.test.ts | 3 +- .../decorators/ownables.decorators.test.ts | 3 +- .../toOwnableValidator.dx.test.ts | 3 +- .../toOwnableValidator.executor.test.ts | 3 +- .../toOwnableValidator.test.ts | 3 +- .../smartSessions.decorators.test.ts | 6 ++- ...oSmartSessionValidator.enable.mode.test.ts | 6 ++- .../toSmartSessionsValidator.advanced.test.ts | 6 ++- .../toSmartSessionsValidator.dx.test.ts | 6 ++- .../toSmartSessionsValidator.policies.test.ts | 6 ++- ...SmartSessionsValidator.sudo.policy.test.ts | 6 ++- .../toSmartSessionsValidator.test.ts | 6 ++- ...oSmartSessionsValidator.uni.policy.test.ts | 6 ++- 36 files changed, 183 insertions(+), 90 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ea9aeb34..6e32e43de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # @biconomy/sdk +## 0.0.29 + +### Patch Changes + +- AbstractJS rebrand +- meeNode support +- useTestBundler + ## 0.0.28 ### Patch Changes diff --git a/README.md b/README.md index 41aaa070d..f918041b4 100644 --- a/README.md +++ b/README.md @@ -32,21 +32,31 @@ bun add @biconomy/sdk viem @rhinestone/module-sdk 2. **Basic Usage:** ```typescript -import { createSmartAccountClient } from "@biconomy/sdk"; -import { http } from "viem"; - -const nexusClient = await createSmartAccountClient({ - signer: account, - chain, - transport: http(), - bundlerTransport: http(bundlerUrl), -}); - -const hash = await nexusClient.sendTransaction({ - calls: [{ to: "0x...", value: 1 }] -}); - -const { status, transactionHash } = await nexusClient.waitForTransactionReceipt({ hash }); +import { toMultichainNexusAccount, mcUSDC } from "@biconomy/sdk"; +import { base, optimism } from "viem/chains"; +import { privateKeyToAccount } from "viem/accounts"; + +const eoaAccount = privateKeyToAccount(`0x${process.env.PRIVATE_KEY}`) +const mcNexus = await toMultichainNexusAccount({ + chains: [base, optimism], + signer: eoaAccount +}) +const meeClient = createMeeClient({ account: mcNexus }) + +const quote = await meeClient.getQuote({ + instructions: [{ + calls: [{ to: "0x...", value: 1 }], + chainId: base.id + }], + feeToken: { + address: mcUSDC.addressOn(base.id), // Token used to pay for the transaction + chainId: base.id // Chain where the payment will be processed + } +}) + +// Execute the quote and get back a transaction hash +// This sends the transaction to the network +const { hash } = await meeClient.executeQuote({ quote }) ``` ### Testing diff --git a/package.json b/package.json index ec470ec0b..d274fd83b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@biconomy/sdk", - "version": "0.0.28", + "version": "0.0.29", "author": "Biconomy", "repository": "github:bcnmy/sdk", "main": "./dist/_cjs/index.js", diff --git a/src/sdk/account/decorators/build.ts b/src/sdk/account/decorators/build.ts index e7b1f4ace..148011e27 100644 --- a/src/sdk/account/decorators/build.ts +++ b/src/sdk/account/decorators/build.ts @@ -1,7 +1,7 @@ import type { Instruction } from "../../clients/decorators/mee/getQuote" import type { BaseMultichainSmartAccount } from "../toMultiChainNexusAccount" import { - type BuildDefaultInstructionsParams, + type BuildDefaultParams, buildDefaultInstructions } from "./instructions/buildDefaultInstructions" import { @@ -26,7 +26,7 @@ export type BaseInstructionsParams = { */ export type BuildDefaultInstruction = { type: "default" - data: BuildDefaultInstructionsParams + data: BuildDefaultParams } /** diff --git a/src/sdk/account/decorators/instructions/buildDefaultInstructions.ts b/src/sdk/account/decorators/instructions/buildDefaultInstructions.ts index 5cfecab33..6963a309b 100644 --- a/src/sdk/account/decorators/instructions/buildDefaultInstructions.ts +++ b/src/sdk/account/decorators/instructions/buildDefaultInstructions.ts @@ -9,22 +9,11 @@ export type BuildDefaultParams = { instructions: Instruction[] | Instruction } -/** - * Parameters for building base instructions - * @property currentInstructions - Optional array of {@link Instruction} existing instructions to append to - * @property instructions - Single {@link Instruction} or array of instructions to add - */ -export type BuildDefaultInstructionsParams = BaseInstructionsParams & - BuildDefaultParams - /** * Builds a base set of instructions by combining existing instructions with new ones * * @param baseParams - {@link BaseInstructionsParams} Base configuration - * @param baseParams.currentInstructions - Optional array of existing instructions (defaults to empty array) - * @param params - {@link BuildDefaultInstructionsParams} Instructions configuration - * @param params.instructions - Single instruction or array of instructions to append - * + * @param params - {@link BuildDefaultParams} Instructions configuration * @returns Promise resolving to an array of {@link Instruction} * * @example diff --git a/src/sdk/account/decorators/instructions/buildIntent.ts b/src/sdk/account/decorators/instructions/buildIntent.ts index c144a78f2..01ea6defa 100644 --- a/src/sdk/account/decorators/instructions/buildIntent.ts +++ b/src/sdk/account/decorators/instructions/buildIntent.ts @@ -1,4 +1,4 @@ -import type { Chain, erc20Abi } from "viem" +import type { Chain } from "viem" import type { Instruction } from "../../../clients/decorators/mee" import type { BaseMultichainSmartAccount } from "../../toMultiChainNexusAccount" import type { MultichainToken } from "../../utils/Types" diff --git a/src/sdk/account/toMultiChainNexusAccount.ts b/src/sdk/account/toMultiChainNexusAccount.ts index eb63ee6e2..9937ff881 100644 --- a/src/sdk/account/toMultiChainNexusAccount.ts +++ b/src/sdk/account/toMultiChainNexusAccount.ts @@ -1,4 +1,4 @@ -import { http, type Chain, type erc20Abi } from "viem" +import { http, type Chain } from "viem" import type { Instruction } from "../clients/decorators/mee/getQuote" import { MEE_VALIDATOR_ADDRESS, diff --git a/src/sdk/account/toNexusAccount.addresses.test.ts b/src/sdk/account/toNexusAccount.addresses.test.ts index 52f8cef8c..68ba02fbe 100644 --- a/src/sdk/account/toNexusAccount.addresses.test.ts +++ b/src/sdk/account/toNexusAccount.addresses.test.ts @@ -72,7 +72,8 @@ describe("nexus.account.addresses", async () => { transport: http(), bundlerTransport: http(bundlerUrl), validatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, - factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS + factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS, + useTestBundler: true }) nexusAccount = nexusClient.account @@ -139,14 +140,16 @@ describe("nexus.account.addresses", async () => { signer: eoaAccount, chain: base, transport: http(), - bundlerTransport: http(bundlerUrl) + bundlerTransport: http(bundlerUrl), + useTestBundler: true }) const testnetClient = await createSmartAccountClient({ signer: eoaAccount, chain: baseSepolia, transport: http(), - bundlerTransport: http(bundlerUrl) + bundlerTransport: http(bundlerUrl), + useTestBundler: true }) const testnetAddress = await testnetClient.account.getAddress() @@ -164,7 +167,8 @@ describe("nexus.account.addresses", async () => { transport: http(), validatorAddress: MEE_VALIDATOR_ADDRESS, factoryAddress: NEXUS_ACCOUNT_FACTORY, - attesters: [TEMP_MEE_ATTESTER_ADDR] + attesters: [TEMP_MEE_ATTESTER_ADDR], + useTestBundler: true }) const meeAddress = await meeAccount.getAddress() diff --git a/src/sdk/account/toNexusAccount.test.ts b/src/sdk/account/toNexusAccount.test.ts index 952b475b8..67e15fb17 100644 --- a/src/sdk/account/toNexusAccount.test.ts +++ b/src/sdk/account/toNexusAccount.test.ts @@ -98,7 +98,8 @@ describe("nexus.account", async () => { transport: http(), bundlerTransport: http(bundlerUrl), validatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, - factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS + factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS, + useTestBundler: true }) nexusAccount = nexusClient.account diff --git a/src/sdk/account/toNexusAccount.ts b/src/sdk/account/toNexusAccount.ts index c4dd5800e..cbe83f3aa 100644 --- a/src/sdk/account/toNexusAccount.ts +++ b/src/sdk/account/toNexusAccount.ts @@ -118,6 +118,8 @@ export type ToNexusSmartAccountParameters = { bootStrapAddress?: Address /** Optional registry address */ registryAddress?: Address + /** Optional use test bundler */ + useTestBundler?: boolean } & Prettify< Pick< ClientConfig, @@ -160,6 +162,7 @@ export type NexusSmartAccountImplementation = SmartAccountImplementation< signer: Signer publicClient: PublicClient walletClient: WalletClient + useTestBundler: boolean } > @@ -196,7 +199,8 @@ export const toNexusAccount = async ( attesters: attesters_ = [RHINESTONE_ATTESTER_ADDRESS], attesterThreshold = 1, bootStrapAddress = NEXUS_BOOTSTRAP_ADDRESS, - registryAddress = REGISTRY_ADDRESS + registryAddress = REGISTRY_ADDRESS, + useTestBundler = false } = parameters const useMeeAccount = addressEquals(validatorAddress, MEE_VALIDATOR_ADDRESS) @@ -600,7 +604,8 @@ export const toNexusAccount = async ( signer, walletClient, publicClient, - attesters: attesters_ + attesters: attesters_, + useTestBundler } }) } diff --git a/src/sdk/account/utils/index.ts b/src/sdk/account/utils/index.ts index 9f7fcb4d8..a63982d9e 100644 --- a/src/sdk/account/utils/index.ts +++ b/src/sdk/account/utils/index.ts @@ -4,7 +4,7 @@ export * from "./Constants.js" export * from "./getChain.js" export * from "./Logger.js" export * from "./toSigner.js" -export * from "../decorators/getNexusAddress.js" +export * from "../decorators" export * from "./toFeeToken.js" export * from "./getMultichainContract.js" export * from "./explorer.js" diff --git a/src/sdk/account/utils/toAcrossPlugin.ts b/src/sdk/account/utils/toAcrossPlugin.ts index 59122a648..080f65090 100644 --- a/src/sdk/account/utils/toAcrossPlugin.ts +++ b/src/sdk/account/utils/toAcrossPlugin.ts @@ -12,6 +12,10 @@ import type { BridgingUserOpParams } from "../decorators/buildBridgeInstructions" +/** + * Response type for Across bridge relay fee information + * @interface AcrossRelayFeeResponse + */ export interface AcrossRelayFeeResponse { totalRelayFee: { pct: string @@ -37,6 +41,10 @@ export interface AcrossRelayFeeResponse { exclusivityDeadline: string } +/** + * Parameters for fetching suggested fees from Across bridge + * @interface AcrossSuggestedFeesParams + */ type AcrossSuggestedFeesParams = { inputToken: Address outputToken: Address @@ -48,6 +56,16 @@ type AcrossSuggestedFeesParams = { // Create HTTP client instance const acrossClient = createHttpClient("https://app.across.to/api") +/** + * Fetches suggested fees from Across bridge API + * @param {AcrossSuggestedFeesParams} params - Parameters for fee calculation + * @param {Address} params.inputToken - Source token address + * @param {Address} params.outputToken - Destination token address + * @param {number} params.originChainId - Source chain ID + * @param {number} params.destinationChainId - Destination chain ID + * @param {bigint} params.amount - Amount to bridge + * @returns {Promise} Suggested fees and related information + */ const acrossGetSuggestedFees = async ({ inputToken, outputToken, @@ -67,6 +85,17 @@ const acrossGetSuggestedFees = async ({ } }) +/** + * Encodes a bridging operation for the Across protocol into a user operation + * @param {BridgingUserOpParams} params - Parameters for the bridge operation + * @param {bigint} params.bridgingAmount - Amount to bridge + * @param {Chain} params.fromChain - Source chain information + * @param {Account} params.account - User's account information + * @param {Chain} params.toChain - Destination chain information + * @param {TokenMapping} params.tokenMapping - Token address mapping across chains + * @returns {Promise} Encoded user operation and bridging details + * @throws {Error} When depositor or recipient address cannot be found + */ export const acrossEncodeBridgingUserOp = async ( params: BridgingUserOpParams ): Promise => { @@ -143,6 +172,20 @@ export const acrossEncodeBridgingUserOp = async ( } } +/** + * Creates an Across bridging plugin instance + * @returns {BridgingPlugin} Plugin instance implementing the Across bridge protocol + * + * @example + * const acrossPlugin = toAcrossPlugin() + * const bridgeResult = await acrossPlugin.encodeBridgeUserOp({ + * bridgingAmount: 1000000n, + * fromChain: sourceChain, + * toChain: destChain, + * account: userAccount, + * tokenMapping: tokens + * }) + */ export const toAcrossPlugin = (): BridgingPlugin => ({ encodeBridgeUserOp: async (params) => { return await acrossEncodeBridgingUserOp(params) diff --git a/src/sdk/clients/createBicoBundlerClient.test.ts b/src/sdk/clients/createBicoBundlerClient.test.ts index bed11bef5..c6f852f9f 100644 --- a/src/sdk/clients/createBicoBundlerClient.test.ts +++ b/src/sdk/clients/createBicoBundlerClient.test.ts @@ -44,7 +44,8 @@ describe("bico.bundler", async () => { chain, transport: http(), validatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, - factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS + factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS, + useTestBundler: true }) bicoBundler = createBicoBundlerClient({ bundlerUrl, account: nexusAccount }) diff --git a/src/sdk/clients/createBicoPaymasterClient.test.ts b/src/sdk/clients/createBicoPaymasterClient.test.ts index 70b82af78..806579b16 100644 --- a/src/sdk/clients/createBicoPaymasterClient.test.ts +++ b/src/sdk/clients/createBicoPaymasterClient.test.ts @@ -145,8 +145,6 @@ describe.skip("bico.paymaster", async () => { bundlerTransport: http(bundlerUrl) }) - console.log(nexusClient.account.address, "nexusClient.account.address") - const initialBalance = await publicClient.getBalance({ address: nexusAccountAddress }) diff --git a/src/sdk/clients/createBundlerClient.test.ts b/src/sdk/clients/createBundlerClient.test.ts index 03fb1ae46..58525b8eb 100644 --- a/src/sdk/clients/createBundlerClient.test.ts +++ b/src/sdk/clients/createBundlerClient.test.ts @@ -15,18 +15,20 @@ const COMPETITORS = [ { name: "Pimlico", chain: baseSepolia, - bundlerUrl: `https://api.pimlico.io/v2/${baseSepolia.id}/rpc?apikey=${process.env.PIMLICO_API_KEY}` + bundlerUrl: `https://api.pimlico.io/v2/${baseSepolia.id}/rpc?apikey=${process.env.PIMLICO_API_KEY}`, + useTestBundler: true }, { name: "Biconomy", bundlerUrl: `https://bundler.biconomy.io/api/v3/${baseSepolia.id}/nJPK7B3ru.dd7f7861-190d-41bd-af80-6877f74b8f44`, - chain: baseSepolia + chain: baseSepolia, + useTestBundler: false } ] describe.each(COMPETITORS)( "nexus.interoperability with $name", - async ({ bundlerUrl, chain }) => { + async ({ bundlerUrl, chain, useTestBundler }) => { const account = privateKeyToAccount(`0x${process.env.PRIVATE_KEY as Hex}`) const publicClient = createPublicClient({ @@ -45,7 +47,8 @@ describe.each(COMPETITORS)( transport: http(), // You can omit this outside of a testing context validatorAddress: MAINNET_ADDRESS_K1_VALIDATOR_ADDRESS, - factoryAddress: MAINNET_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS + factoryAddress: MAINNET_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS, + useTestBundler }) nexusAccountAddress = await nexusAccount.getCounterFactualAddress() diff --git a/src/sdk/clients/createHttpClient.ts b/src/sdk/clients/createHttpClient.ts index 989ee22a5..ed47206be 100644 --- a/src/sdk/clients/createHttpClient.ts +++ b/src/sdk/clients/createHttpClient.ts @@ -64,6 +64,7 @@ export const createHttpClient = (url: Url): HttpClient => { }) if (!result.ok) { + console.log({ result }) throw new Error(result.statusText) } diff --git a/src/sdk/clients/createNexusSessionClient.test.ts b/src/sdk/clients/createNexusSessionClient.test.ts index bdb5ed100..667e37cbf 100644 --- a/src/sdk/clients/createNexusSessionClient.test.ts +++ b/src/sdk/clients/createNexusSessionClient.test.ts @@ -61,7 +61,8 @@ describe("nexus.session.client", async () => { signer: eoaAccount, chain, transport: http(), - bundlerTransport: http(bundlerUrl) + bundlerTransport: http(bundlerUrl), + useTestBundler: true }) nexusAccountAddress = await nexusClient.account.getCounterFactualAddress() @@ -181,7 +182,8 @@ describe("nexus.session.client", async () => { accountAddress: nexusClient.account.address, signer: sessionKeyAccount, transport: http(), - bundlerTransport: http(bundlerUrl) + bundlerTransport: http(bundlerUrl), + useTestBundler: true }) const usePermissionsModule = toSmartSessionsValidator({ @@ -238,7 +240,8 @@ describe("nexus.session.client", async () => { accountAddress: nexusClient.account.address, signer: sessionKeyAccount, transport: http(), - bundlerTransport: http(bundlerUrl) + bundlerTransport: http(bundlerUrl), + useTestBundler: true }) const useSmartSessionNexusClient = smartSessionNexusClient.extend( diff --git a/src/sdk/clients/createSmartAccountClient.test.ts b/src/sdk/clients/createSmartAccountClient.test.ts index 81911acca..46850ba45 100644 --- a/src/sdk/clients/createSmartAccountClient.test.ts +++ b/src/sdk/clients/createSmartAccountClient.test.ts @@ -74,7 +74,8 @@ describe("nexus.client", async () => { transport: http(), bundlerTransport: http(bundlerUrl), validatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, - factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS + factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS, + useTestBundler: true }) nexusAccountAddress = await nexusClient.account.getCounterFactualAddress() }) @@ -277,7 +278,8 @@ describe("nexus.client", async () => { transport: http(), bundlerTransport: http(bundlerUrl), validatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, - factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS + factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS, + useTestBundler: true }) const ethersNexusClient = await createSmartAccountClient({ @@ -286,7 +288,8 @@ describe("nexus.client", async () => { transport: http(), bundlerTransport: http(bundlerUrl), validatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, - factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS + factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS, + useTestBundler: true }) const sig1 = await viemNexusClient.signMessage({ message: "123" }) @@ -303,7 +306,8 @@ describe("nexus.client", async () => { transport: http(), bundlerTransport: http(bundlerUrl), validatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, - factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS + factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS, + useTestBundler: true }) const hash = await ethersNexusClient.sendUserOperation({ diff --git a/src/sdk/clients/createSmartAccountClient.ts b/src/sdk/clients/createSmartAccountClient.ts index 773078fd6..d39f1ddea 100644 --- a/src/sdk/clients/createSmartAccountClient.ts +++ b/src/sdk/clients/createSmartAccountClient.ts @@ -176,6 +176,8 @@ export type SmartAccountClientConfig< bootStrapAddress?: Address /** Registry address */ registryAddress?: Address + /** Use test bundler */ + useTestBundler?: boolean } > @@ -217,6 +219,7 @@ export async function createSmartAccountClient( paymasterContext, attesters, attesterThreshold, + useTestBundler = false, ...bundlerConfig } = parameters @@ -234,7 +237,8 @@ export async function createSmartAccountClient( factoryAddress, validatorAddress, attesters, - attesterThreshold + attesterThreshold, + useTestBundler })) const bundler_ = createBicoBundlerClient({ diff --git a/src/sdk/clients/decorators/bundler/getGasFeeValues.ts b/src/sdk/clients/decorators/bundler/getGasFeeValues.ts index b04dd021f..d175875dd 100644 --- a/src/sdk/clients/decorators/bundler/getGasFeeValues.ts +++ b/src/sdk/clients/decorators/bundler/getGasFeeValues.ts @@ -1,4 +1,5 @@ import type { Account, Chain, Client, Hex, Transport } from "viem" +import type { NexusAccount } from "../../../account/toNexusAccount" export type BicoRpcSchema = [ { @@ -65,14 +66,12 @@ export const getGasFeeValues = async ( BicoRpcSchema > ): Promise => { - const isABiconomyBundler = client.transport.url - .toLowerCase() - .includes("biconomy") + const usePimlico = !!(client?.account as NexusAccount)?.useTestBundler const gasPrice = await client.request({ - method: isABiconomyBundler - ? "biconomy_getGasFeeValues" - : "pimlico_getUserOperationGasPrice", + method: usePimlico + ? "pimlico_getUserOperationGasPrice" + : "biconomy_getGasFeeValues", params: [] }) diff --git a/src/sdk/clients/decorators/erc7579/erc7579.decorators.test.ts b/src/sdk/clients/decorators/erc7579/erc7579.decorators.test.ts index dc72b0163..106756001 100644 --- a/src/sdk/clients/decorators/erc7579/erc7579.decorators.test.ts +++ b/src/sdk/clients/decorators/erc7579/erc7579.decorators.test.ts @@ -55,7 +55,8 @@ describe("erc7579.decorators", async () => { transport: http(), bundlerTransport: http(bundlerUrl), validatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, - factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS + factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS, + useTestBundler: true }) nexusAccountAddress = await nexusClient.account.getCounterFactualAddress() diff --git a/src/sdk/clients/decorators/smartAccount/account.decorators.test.ts b/src/sdk/clients/decorators/smartAccount/account.decorators.test.ts index 851eae270..737e047bc 100644 --- a/src/sdk/clients/decorators/smartAccount/account.decorators.test.ts +++ b/src/sdk/clients/decorators/smartAccount/account.decorators.test.ts @@ -50,7 +50,8 @@ describe("account.decorators", async () => { transport: http(), bundlerTransport: http(bundlerUrl), validatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, - factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS + factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS, + useTestBundler: true }) nexusAccountAddress = await nexusClient.account.getCounterFactualAddress() await fundAndDeployClients(testClient, [nexusClient]) diff --git a/src/sdk/clients/decorators/smartAccount/debugUserOperation.ts b/src/sdk/clients/decorators/smartAccount/debugUserOperation.ts index c0435e834..17cc1778e 100644 --- a/src/sdk/clients/decorators/smartAccount/debugUserOperation.ts +++ b/src/sdk/clients/decorators/smartAccount/debugUserOperation.ts @@ -13,9 +13,9 @@ import { type UserOperationRequest, formatUserOperationRequest, getUserOperationError, + prepareUserOperation, toPackedUserOperation } from "viem/account-abstraction" -import { prepareUserOperationWithoutSignature } from "./prepareUserOperationWithoutSignature" import type { Assign, @@ -133,7 +133,7 @@ export async function debugUserOperation< const request = account ? await getAction( client, - prepareUserOperationWithoutSignature, + prepareUserOperation, "prepareUserOperation" )(parameters as unknown as PrepareUserOperationParameters) : parameters @@ -141,10 +141,7 @@ export async function debugUserOperation< const signature = (parameters.signature || (await account?.signUserOperation(request as UserOperation)))! - const userOpWithSignature = { - ...request, - signature - } as UserOperation + const userOpWithSignature = { ...request, signature } as UserOperation const packed = toPackedUserOperation(userOpWithSignature) console.log( diff --git a/src/sdk/modules/k1Validator/toK1Validator.test.ts b/src/sdk/modules/k1Validator/toK1Validator.test.ts index 6fb443439..bd1b00e05 100644 --- a/src/sdk/modules/k1Validator/toK1Validator.test.ts +++ b/src/sdk/modules/k1Validator/toK1Validator.test.ts @@ -56,7 +56,8 @@ describe("modules.k1Validator", async () => { transport: http(), bundlerTransport: http(bundlerUrl), validatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, - factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS + factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS, + useTestBundler: true }) nexusAccountAddress = await nexusClient.account.getCounterFactualAddress() diff --git a/src/sdk/modules/ownableValidator/decorators/ownables.decorators.test.ts b/src/sdk/modules/ownableValidator/decorators/ownables.decorators.test.ts index 2b8717a9e..96df48bb4 100644 --- a/src/sdk/modules/ownableValidator/decorators/ownables.decorators.test.ts +++ b/src/sdk/modules/ownableValidator/decorators/ownables.decorators.test.ts @@ -60,7 +60,8 @@ describe("modules.ownables.decorators", async () => { transport: http(), bundlerTransport: http(bundlerUrl), validatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, - factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS + factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS, + useTestBundler: true }) nexusAccountAddress = await nexusClient.account.getCounterFactualAddress() diff --git a/src/sdk/modules/ownableValidator/toOwnableValidator.dx.test.ts b/src/sdk/modules/ownableValidator/toOwnableValidator.dx.test.ts index b5aee66d6..33f9b329d 100644 --- a/src/sdk/modules/ownableValidator/toOwnableValidator.dx.test.ts +++ b/src/sdk/modules/ownableValidator/toOwnableValidator.dx.test.ts @@ -67,7 +67,8 @@ describe("modules.ownableValidator.dx", async () => { transport: http(), bundlerTransport: http(bundlerUrl), validatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, - factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS + factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS, + useTestBundler: true }) // Fund the account and deploy the smart contract wallet diff --git a/src/sdk/modules/ownableValidator/toOwnableValidator.executor.test.ts b/src/sdk/modules/ownableValidator/toOwnableValidator.executor.test.ts index 160d37149..df87e7a8e 100644 --- a/src/sdk/modules/ownableValidator/toOwnableValidator.executor.test.ts +++ b/src/sdk/modules/ownableValidator/toOwnableValidator.executor.test.ts @@ -66,7 +66,8 @@ describe("modules.ownableExecutor", async () => { transport: http(), bundlerTransport: http(bundlerUrl), validatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, - factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS + factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS, + useTestBundler: true }) nexusAccountAddress = await nexusClient.account.getCounterFactualAddress() diff --git a/src/sdk/modules/ownableValidator/toOwnableValidator.test.ts b/src/sdk/modules/ownableValidator/toOwnableValidator.test.ts index a80854332..517d44913 100644 --- a/src/sdk/modules/ownableValidator/toOwnableValidator.test.ts +++ b/src/sdk/modules/ownableValidator/toOwnableValidator.test.ts @@ -70,7 +70,8 @@ describe("modules.ownables", async () => { transport: http(), bundlerTransport: http(bundlerUrl), validatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, - factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS + factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS, + useTestBundler: true }) await fundAndDeployClients(testClient, [nexusClient]) diff --git a/src/sdk/modules/smartSessionsValidator/decorators/smartSessions.decorators.test.ts b/src/sdk/modules/smartSessionsValidator/decorators/smartSessions.decorators.test.ts index 074f81561..e08391eec 100644 --- a/src/sdk/modules/smartSessionsValidator/decorators/smartSessions.decorators.test.ts +++ b/src/sdk/modules/smartSessionsValidator/decorators/smartSessions.decorators.test.ts @@ -58,7 +58,8 @@ describe("modules.smartSessions.decorators", async () => { transport: http(), bundlerTransport: http(bundlerUrl), validatorAddress: TEST_ADDRESS_K1_VALIDATOR_ADDRESS, - factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS + factoryAddress: TEST_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS, + useTestBundler: true }) sessionRequestedInfo = [ @@ -121,7 +122,8 @@ describe("modules.smartSessions.decorators", async () => { accountAddress: nexusClient.account.address, signer: sessionKeyAccount, transport: http(), - bundlerTransport: http(bundlerUrl) + bundlerTransport: http(bundlerUrl), + useTestBundler: true }) const nexusSessionClient = smartSessionNexusClient.extend( diff --git a/src/sdk/modules/smartSessionsValidator/toSmartSessionValidator.enable.mode.test.ts b/src/sdk/modules/smartSessionsValidator/toSmartSessionValidator.enable.mode.test.ts index a55e69b44..bf4be7ecd 100644 --- a/src/sdk/modules/smartSessionsValidator/toSmartSessionValidator.enable.mode.test.ts +++ b/src/sdk/modules/smartSessionsValidator/toSmartSessionValidator.enable.mode.test.ts @@ -99,7 +99,8 @@ describe.skip("modules.smartSessions.enable.mode.dx", async () => { index, signer: eoaAccount, chain, - transport: http() + transport: http(), + useTestBundler: true }) nexusAccountAddress = await nexusAccount.getCounterFactualAddress() @@ -110,7 +111,8 @@ describe.skip("modules.smartSessions.enable.mode.dx", async () => { signer: eoaAccount, chain, transport: http(), - bundlerTransport: http(bundlerUrl) + bundlerTransport: http(bundlerUrl), + useTestBundler: true }) await fundAndDeployClients(testClient, [nexusClient]) diff --git a/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.advanced.test.ts b/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.advanced.test.ts index 82024e817..948e41232 100644 --- a/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.advanced.test.ts +++ b/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.advanced.test.ts @@ -84,7 +84,8 @@ describe("modules.smartSessions.dx", async () => { signer: eoaAccount, chain, transport: http(), - bundlerTransport: http(bundlerUrl) + bundlerTransport: http(bundlerUrl), + useTestBundler: true }) // Fund the account and deploy the smart contract wallet @@ -174,7 +175,8 @@ describe("modules.smartSessions.dx", async () => { accountAddress: usersSessionData.granter, signer: sessionKeyAccount, transport: http(), - bundlerTransport: http(bundlerUrl) + bundlerTransport: http(bundlerUrl), + useTestBundler: true }) // Create a new smart sessions module with the session key diff --git a/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.dx.test.ts b/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.dx.test.ts index 12b9fef73..14acd0f18 100644 --- a/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.dx.test.ts +++ b/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.dx.test.ts @@ -84,7 +84,8 @@ describe("modules.smartSessions.dx", async () => { signer: eoaAccount, chain, transport: http(), - bundlerTransport: http(bundlerUrl) + bundlerTransport: http(bundlerUrl), + useTestBundler: true }) // Fund the account and deploy the smart contract wallet @@ -157,7 +158,8 @@ describe("modules.smartSessions.dx", async () => { accountAddress: usersSessionData.granter, signer: sessionKeyAccount, transport: http(), - bundlerTransport: http(bundlerUrl) + bundlerTransport: http(bundlerUrl), + useTestBundler: true }) // Create a new smart sessions module with the session key diff --git a/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.policies.test.ts b/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.policies.test.ts index 031ad57d8..c3de634cc 100644 --- a/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.policies.test.ts +++ b/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.policies.test.ts @@ -54,7 +54,8 @@ describe("modules.smartSessions.policies", async () => { signer: eoaAccount, chain, transport: http(), - bundlerTransport: http(bundlerUrl) + bundlerTransport: http(bundlerUrl), + useTestBundler: true }) nexusAccountAddress = await nexusClient.account.getCounterFactualAddress() @@ -97,7 +98,8 @@ describe("modules.smartSessions.policies", async () => { signer: eoaAccount, chain, transport: http(), - bundlerTransport: http(bundlerUrl) + bundlerTransport: http(bundlerUrl), + useTestBundler: true }) // Create a smart sessions module for the user's account diff --git a/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.sudo.policy.test.ts b/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.sudo.policy.test.ts index c8521ebec..3a72eb099 100644 --- a/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.sudo.policy.test.ts +++ b/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.sudo.policy.test.ts @@ -62,7 +62,8 @@ describe("modules.smartSessions.sudo.policy", async () => { signer: eoaAccount, chain, transport: http(), - bundlerTransport: http(bundlerUrl) + bundlerTransport: http(bundlerUrl), + useTestBundler: true }) nexusAccountAddress = await nexusClient.account.getCounterFactualAddress() @@ -156,7 +157,8 @@ describe("modules.smartSessions.sudo.policy", async () => { accountAddress: usersSessionData.granter, signer: sessionKeyAccount, transport: http(), - bundlerTransport: http(bundlerUrl) + bundlerTransport: http(bundlerUrl), + useTestBundler: true }) // Create a new smart sessions module with the session key diff --git a/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.test.ts b/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.test.ts index 302d122cf..dd78f5b71 100644 --- a/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.test.ts +++ b/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.test.ts @@ -62,7 +62,8 @@ describe("modules.smartSessions", async () => { signer: eoaAccount, chain, transport: http(), - bundlerTransport: http(bundlerUrl) + bundlerTransport: http(bundlerUrl), + useTestBundler: true }) sessionsModule = toSmartSessionsValidator({ @@ -284,7 +285,8 @@ describe("modules.smartSessions", async () => { accountAddress: nexusClient.account.address, signer: sessionKeyAccount, transport: http(), - bundlerTransport: http(bundlerUrl) + bundlerTransport: http(bundlerUrl), + useTestBundler: true }) const usePermissionsModule = toSmartSessionsValidator({ diff --git a/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.uni.policy.test.ts b/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.uni.policy.test.ts index 6ee0f827f..6e777118d 100644 --- a/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.uni.policy.test.ts +++ b/src/sdk/modules/smartSessionsValidator/toSmartSessionsValidator.uni.policy.test.ts @@ -68,7 +68,8 @@ describe("modules.smartSessions.uni.policy", async () => { signer: eoaAccount, chain, transport: http(), - bundlerTransport: http(bundlerUrl) + bundlerTransport: http(bundlerUrl), + useTestBundler: true }) nexusAccountAddress = await nexusClient.account.getCounterFactualAddress() @@ -292,7 +293,8 @@ describe("modules.smartSessions.uni.policy", async () => { accountAddress: nexusClient.account.address, signer: sessionKeyAccount, transport: http(), - bundlerTransport: http(bundlerUrl) + bundlerTransport: http(bundlerUrl), + useTestBundler: true }) const usePermissionsModule = toSmartSessionsValidator({ From 96ff0609d2757a20860e2f6ffa706284cb4c62ef Mon Sep 17 00:00:00 2001 From: Joe Pegler Date: Thu, 16 Jan 2025 16:57:39 +0000 Subject: [PATCH 11/11] chore: add exports --- README.md | 2 +- src/sdk/clients/decorators/index.ts | 1 + src/sdk/clients/decorators/mee/signFusionQuote.ts | 4 ++-- src/sdk/clients/decorators/mee/signQuote.ts | 6 +++--- src/sdk/clients/index.ts | 2 ++ src/sdk/constants/index.ts | 1 + 6 files changed, 10 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f918041b4..077b89bfd 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ const meeClient = createMeeClient({ account: mcNexus }) const quote = await meeClient.getQuote({ instructions: [{ - calls: [{ to: "0x...", value: 1 }], + calls: [{ to: "0x...", value: 1n }], chainId: base.id }], feeToken: { diff --git a/src/sdk/clients/decorators/index.ts b/src/sdk/clients/decorators/index.ts index a651fc255..aa611ad0a 100644 --- a/src/sdk/clients/decorators/index.ts +++ b/src/sdk/clients/decorators/index.ts @@ -1,3 +1,4 @@ export * from "./erc7579" export * from "./smartAccount" export * from "./bundler" +export * from "./mee" diff --git a/src/sdk/clients/decorators/mee/signFusionQuote.ts b/src/sdk/clients/decorators/mee/signFusionQuote.ts index 8e35b4786..cd39c6f6b 100644 --- a/src/sdk/clients/decorators/mee/signFusionQuote.ts +++ b/src/sdk/clients/decorators/mee/signFusionQuote.ts @@ -12,7 +12,7 @@ import type { MultichainSmartAccount } from "../../../account/toMultiChainNexusA import type { Call } from "../../../account/utils/Types" import type { BaseMeeClient } from "../../createMeeClient" import type { GetQuotePayload } from "./getQuote" -import { type ExecutionMode, PREFIX } from "./signQuote" +import { type MeeExecutionMode, PREFIX } from "./signQuote" export const FUSION_NATIVE_TRANSFER_PREFIX = "0x150b7a02" @@ -26,7 +26,7 @@ export type SignFusionQuoteParams = { /** 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 + executionMode?: MeeExecutionMode /** The on-chain transaction to use as the trigger */ trigger: { /** The on-chain transaction to use as the trigger */ diff --git a/src/sdk/clients/decorators/mee/signQuote.ts b/src/sdk/clients/decorators/mee/signQuote.ts index 1bdd6a233..46325ed68 100644 --- a/src/sdk/clients/decorators/mee/signQuote.ts +++ b/src/sdk/clients/decorators/mee/signQuote.ts @@ -4,7 +4,7 @@ import type { BaseMeeClient } from "../../createMeeClient" import type { GetQuotePayload } from "./getQuote" -export type ExecutionMode = +export type MeeExecutionMode = | "direct-to-mee" | "fusion-with-onchain-tx" | "fusion-with-erc20permit" @@ -19,7 +19,7 @@ export type SignQuoteParams = { /** 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 + executionMode?: MeeExecutionMode } export type SignQuotePayload = GetQuotePayload & { @@ -27,7 +27,7 @@ export type SignQuotePayload = GetQuotePayload & { signature: Hex } -export const PREFIX: Record = { +export const PREFIX: Record = { "direct-to-mee": "0x00", "fusion-with-onchain-tx": "0x01", "fusion-with-erc20permit": "0x02" diff --git a/src/sdk/clients/index.ts b/src/sdk/clients/index.ts index 4022c530d..3331aa4e9 100644 --- a/src/sdk/clients/index.ts +++ b/src/sdk/clients/index.ts @@ -1,4 +1,6 @@ export * from "./createBicoBundlerClient" export * from "./createBicoPaymasterClient" export * from "./createSmartAccountClient" +export * from "./createMeeClient" +export * from "./createHttpClient" export * from "./decorators" diff --git a/src/sdk/constants/index.ts b/src/sdk/constants/index.ts index 99ec2f02b..197c1a7e6 100644 --- a/src/sdk/constants/index.ts +++ b/src/sdk/constants/index.ts @@ -9,6 +9,7 @@ import { type Hex, toBytes, toHex } from "viem" import { ParamCondition } from "../modules/smartSessionsValidator/Types" export * from "./abi" +export * from "./tokens" export const ENTRY_POINT_ADDRESS: Hex = "0x0000000071727De22E5E9d8BAf0edAc6f37da032"