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"