diff --git a/.eslintrc b/.eslintrc index 225e62bbdc..222c0d7153 100644 --- a/.eslintrc +++ b/.eslintrc @@ -4,6 +4,7 @@ "es2021": true, "node": true }, + "plugins": ["import"], "parserOptions": { "sourceType": "module" }, diff --git a/packages/accounts/plugindefs/multisig/abi.ts b/packages/accounts/plugindefs/multisig/abi.ts index 16fe01e74e..ba981de06b 100644 --- a/packages/accounts/plugindefs/multisig/abi.ts +++ b/packages/accounts/plugindefs/multisig/abi.ts @@ -50,7 +50,7 @@ export const MultisigPluginAbi = [ ], outputs: [ { - name: "failed", + name: "success", type: "bool", internalType: "bool", }, @@ -932,7 +932,7 @@ export const MultisigPluginAbi = [ }, { type: "error", - name: "InvalidGasValues", + name: "InvalidAddress", inputs: [], }, { @@ -945,6 +945,11 @@ export const MultisigPluginAbi = [ name: "InvalidMaxPriorityFeePerGas", inputs: [], }, + { + type: "error", + name: "InvalidNumSigsOnActualGas", + inputs: [], + }, { type: "error", name: "InvalidOwner", diff --git a/packages/accounts/src/msca/abis/MultisigModularAccountFactory.ts b/packages/accounts/src/msca/abis/MultisigModularAccountFactory.ts index dec33bc3db..7612bfbf4b 100644 --- a/packages/accounts/src/msca/abis/MultisigModularAccountFactory.ts +++ b/packages/accounts/src/msca/abis/MultisigModularAccountFactory.ts @@ -114,8 +114,8 @@ export const MultisigModularAccountFactoryAbi = [ }, { name: "threshold", - type: "uint256", - internalType: "uint256", + type: "uint128", + internalType: "uint128", }, ], outputs: [ diff --git a/packages/accounts/src/msca/account/multisigAccount.ts b/packages/accounts/src/msca/account/multisigAccount.ts index e6c6012934..4343993813 100644 --- a/packages/accounts/src/msca/account/multisigAccount.ts +++ b/packages/accounts/src/msca/account/multisigAccount.ts @@ -27,7 +27,11 @@ export const MULTISIG_ACCOUNT_SOURCE = "MultisigModularAccount"; export type MultisigModularAccount< TSigner extends SmartAccountSigner = SmartAccountSigner -> = SmartContractAccountWithSigner & { +> = SmartContractAccountWithSigner< + typeof MULTISIG_ACCOUNT_SOURCE, + TSigner, + "0.6.0" +> & { getLocalThreshold: () => bigint; }; diff --git a/packages/accounts/src/msca/e2e-tests/multisig-modular-account.e2e.test.ts b/packages/accounts/src/msca/e2e-tests/multisig-modular-account.e2e.test.ts index 817668777b..78e39646db 100644 --- a/packages/accounts/src/msca/e2e-tests/multisig-modular-account.e2e.test.ts +++ b/packages/accounts/src/msca/e2e-tests/multisig-modular-account.e2e.test.ts @@ -59,7 +59,7 @@ describe("Multisig Modular Account Tests", async () => { threshold, }); expect(address).toMatchInlineSnapshot( - '"0x4ff93F25764CefC22aeeE111CEf47CD1B5e05370"' + '"0xB717003B9777B894000B89d60B179FDA96a655D3"' ); }); @@ -75,9 +75,7 @@ describe("Multisig Modular Account Tests", async () => { owners.slice().sort() ); - expect(await provider.getThreshold({ account: provider.account })).toBe( - threshold - ); + expect(await provider.getThreshold({})).toBe(threshold); }); it("should correctly verify 1271 signatures over messages", async () => { @@ -99,20 +97,23 @@ describe("Multisig Modular Account Tests", async () => { const signature2 = await provider2.account.signMessage({ message }); - const combined = formatSignatures([ - { - userOpSigType: "ACTUAL", - signerType: "EOA", - signature: signature1, - signer: await signer1.getAddress(), - }, - { - userOpSigType: "ACTUAL", - signerType: "EOA", - signature: signature2, - signer: await signer2.getAddress(), - }, - ]); + const combined = formatSignatures( + [ + { + userOpSigType: "ACTUAL", + signerType: "EOA", + signature: signature1, + signer: await signer1.getAddress(), + }, + { + userOpSigType: "ACTUAL", + signerType: "EOA", + signature: signature2, + signer: await signer2.getAddress(), + }, + ], + true + ); expect( await provider1.verifyMessage({ @@ -159,20 +160,23 @@ describe("Multisig Modular Account Tests", async () => { message, }); - const combined = formatSignatures([ - { - userOpSigType: "ACTUAL", - signerType: "EOA", - signature: signature1, - signer: await signer1.getAddress(), - }, - { - userOpSigType: "ACTUAL", - signerType: "EOA", - signature: signature2, - signer: await signer2.getAddress(), - }, - ]); + const combined = formatSignatures( + [ + { + userOpSigType: "ACTUAL", + signerType: "EOA", + signature: signature1, + signer: await signer1.getAddress(), + }, + { + userOpSigType: "ACTUAL", + signerType: "EOA", + signature: signature2, + signer: await signer2.getAddress(), + }, + ], + true + ); expect( await provider1.verifyTypedData({ @@ -211,7 +215,9 @@ describe("Multisig Modular Account Tests", async () => { const { account: { address }, } = provider1; - expect(address).toEqual("0xB77423329491BAF4b7B904887627C55Cd53968f8"); + expect(address).toMatchInlineSnapshot( + '"0xD605446440A7d09772C909263823189377A503Da"' + ); const message = "test"; @@ -219,20 +225,23 @@ describe("Multisig Modular Account Tests", async () => { const signature2 = await provider2.account.signMessage({ message }); - const combined = formatSignatures([ - { - userOpSigType: "ACTUAL", - signerType: "EOA", - signature: signature1, - signer: await signer1.getAddress(), - }, - { - userOpSigType: "ACTUAL", - signerType: "EOA", - signature: signature2, - signer: await signer2.getAddress(), - }, - ]); + const combined = formatSignatures( + [ + { + userOpSigType: "ACTUAL", + signerType: "EOA", + signature: signature1, + signer: await signer1.getAddress(), + }, + { + userOpSigType: "ACTUAL", + signerType: "EOA", + signature: signature2, + signer: await signer2.getAddress(), + }, + ], + true + ); const [, factoryCalldata] = parseFactoryAddressFromAccountInitCode( await provider1.account.getInitCode() @@ -283,7 +292,9 @@ describe("Multisig Modular Account Tests", async () => { const { account: { address }, } = provider1; - expect(address).toEqual("0xB77423329491BAF4b7B904887627C55Cd53968f8"); + expect(address).toMatchInlineSnapshot( + '"0xD605446440A7d09772C909263823189377A503Da"' + ); const types = { Request: [{ name: "hello", type: "string" }], @@ -307,20 +318,23 @@ describe("Multisig Modular Account Tests", async () => { message, }); - const combined = formatSignatures([ - { - userOpSigType: "ACTUAL", - signerType: "EOA", - signature: signature1, - signer: await signer1.getAddress(), - }, - { - userOpSigType: "ACTUAL", - signerType: "EOA", - signature: signature2, - signer: await signer2.getAddress(), - }, - ]); + const combined = formatSignatures( + [ + { + userOpSigType: "ACTUAL", + signerType: "EOA", + signature: signature1, + signer: await signer1.getAddress(), + }, + { + userOpSigType: "ACTUAL", + signerType: "EOA", + signature: signature2, + signer: await signer2.getAddress(), + }, + ], + true + ); const [, factoryCalldata] = parseFactoryAddressFromAccountInitCode( await provider1.account.getInitCode() @@ -349,7 +363,7 @@ describe("Multisig Modular Account Tests", async () => { ).toBe(true); }); - it.skip("should execute successfully", async () => { + it("should execute successfully", async () => { const initiator = await givenConnectedProvider({ signer: signer1, chain, @@ -366,12 +380,13 @@ describe("Multisig Modular Account Tests", async () => { expect(initiator.getAddress()).toBe(submitter.getAddress()); - const { aggregatedSignature } = await initiator.proposeUserOperation({ - uo: { - target: initiator.getAddress(), - data: "0x", - }, - }); + const { aggregatedSignature, signatureObj } = + await initiator.proposeUserOperation({ + uo: { + target: initiator.getAddress(), + data: "0x", + }, + }); const result = await submitter.sendUserOperation({ uo: { @@ -379,7 +394,9 @@ describe("Multisig Modular Account Tests", async () => { data: "0x", }, context: { - signature: aggregatedSignature, + aggregatedSignature: aggregatedSignature, + signatures: [signatureObj], + userOpSignatureType: "ACTUAL", }, }); @@ -390,7 +407,7 @@ describe("Multisig Modular Account Tests", async () => { await expect(txnHash).resolves.not.toThrowError(); }, 100000); - it.skip("should execute successfully with actual gas values equal to max gas values", async () => { + it("should execute successfully when using sendTransaction", async () => { const initiator = await givenConnectedProvider({ signer: signer1, chain, @@ -407,26 +424,72 @@ describe("Multisig Modular Account Tests", async () => { expect(initiator.getAddress()).toBe(submitter.getAddress()); - const { aggregatedSignature, request: userOpReq } = + const { aggregatedSignature, signatureObj } = await initiator.proposeUserOperation({ uo: { target: initiator.getAddress(), data: "0x", }, - overrides: { - maxFeePerGas: { multiplier: 2 }, - maxPriorityFeePerGas: { multiplier: 3 }, - preVerificationGas: { multiplier: 1.5 }, - }, }); + const result = submitter.sendTransaction( + { + to: initiator.getAddress(), + data: "0x", + }, + undefined, + { + aggregatedSignature: aggregatedSignature, + signatures: [signatureObj], + userOpSignatureType: "ACTUAL", + } + ); + + await expect(result).resolves.not.toThrowError(); + }, 100000); + + it("should execute successfully with actual gas values equal to max gas values", async () => { + const initiator = await givenConnectedProvider({ + signer: signer1, + chain, + owners, + threshold, + }); + + const submitter = await givenConnectedProvider({ + signer: signer2, + chain, + owners, + threshold, + }); + + expect(initiator.getAddress()).toBe(submitter.getAddress()); + + const { + aggregatedSignature, + request: userOpReq, + signatureObj, + } = await initiator.proposeUserOperation({ + uo: { + target: initiator.getAddress(), + data: "0x", + }, + overrides: { + maxFeePerGas: { multiplier: 2 }, + maxPriorityFeePerGas: { multiplier: 3 }, + preVerificationGas: { multiplier: 1.5 }, + }, + }); + const result = await submitter.sendUserOperation({ uo: { target: initiator.getAddress(), data: "0x", }, context: { - signature: aggregatedSignature, + signatures: [signatureObj], + aggregatedSignature, + userOpSignatureType: "ACTUAL", }, overrides: { callGasLimit: userOpReq.callGasLimit, @@ -469,7 +532,9 @@ describe("Multisig Modular Account Tests", async () => { const { account: { address }, } = provider1; - expect(address).toEqual("0xE2c5429De9133F03f3D36d2Be3695AB315D65ECa"); + expect(address).toMatchInlineSnapshot( + '"0xF2dBBB10E1a7406B17B0056357132B0702e184D5"' + ); const { request, signatureObj: signature1 } = await provider1.proposeUserOperation({ @@ -479,11 +544,12 @@ describe("Multisig Modular Account Tests", async () => { }, }); - const { aggregatedSignature } = await provider2.signMultisigUserOperation({ - account: provider2.account, - signatures: [signature1], - userOperationRequest: request, - }); + const { aggregatedSignature, signatureObj: signature2 } = + await provider2.signMultisigUserOperation({ + account: provider2.account, + signatures: [signature1], + userOperationRequest: request, + }); // parse the UO request fields into the override format to send to sendUserOperation // todo: helper function to go from UserOperationRequest to SendUserOperationParams? @@ -496,7 +562,9 @@ describe("Multisig Modular Account Tests", async () => { nonceKey: fromHex(`0x${pad(request.nonce).slice(2, 26)}`, "bigint"), // Nonce key is the first 24 bytes of the nonce }, context: { - signature: aggregatedSignature, + aggregatedSignature, + signatures: [signature1, signature2], + userOpSignatureType: "ACTUAL", }, }); @@ -524,7 +592,9 @@ const givenConnectedProvider = async ({ threshold: bigint; }) => { return createMultisigModularAccountClient({ - transport: http(`${chain.rpcUrls.alchemy.http[0]}/${API_KEY!}`), + transport: http(`${chain.rpcUrls.alchemy.http[0]}/${API_KEY!}`, { + retryCount: 0, + }), chain: chain, account: { signer, diff --git a/packages/accounts/src/msca/plugins/multisig/actions/proposeUserOperation.ts b/packages/accounts/src/msca/plugins/multisig/actions/proposeUserOperation.ts index a2afd6fc7e..6b0b8bfbcd 100644 --- a/packages/accounts/src/msca/plugins/multisig/actions/proposeUserOperation.ts +++ b/packages/accounts/src/msca/plugins/multisig/actions/proposeUserOperation.ts @@ -10,8 +10,8 @@ import { type UserOperationOverrides, } from "@alchemy/aa-core"; import { type Chain, type Client, type Transport } from "viem"; -import { combineSignatures, getSignerType } from "../index.js"; -import { type ProposeUserOperationResult, type Signature } from "../types.js"; +import { splitAggregatedSignature } from "../index.js"; +import { type ProposeUserOperationResult } from "../types.js"; export async function proposeUserOperation< TTransport extends Transport = Transport, @@ -61,29 +61,22 @@ export async function proposeUserOperation< const request = await client.signUserOperation({ uoStruct: builtUo, account, + context: { + userOpSignatureType: "UPPERLIMIT", + }, }); - const signerType = await getSignerType({ - client, - signature: request.signature, - signer: account.getSigner(), + const splitSignatures = await splitAggregatedSignature({ + request, + aggregatedSignature: request.signature, + account, + // split works on the assumption that we have t - 1 signatures + threshold: 2, }); - const signatureObj: Signature = { - signature: request.signature, - signer: await account.getSigner().getAddress(), - signerType, - userOpSigType: "UPPERLIMIT", - }; - return { request, - signatureObj, - aggregatedSignature: combineSignatures({ - signatures: [signatureObj], - upperLimitMaxFeePerGas: request.maxFeePerGas, - upperLimitMaxPriorityFeePerGas: request.maxPriorityFeePerGas, - upperLimitPvg: request.preVerificationGas, - }), + signatureObj: splitSignatures.signatures[0], + aggregatedSignature: request.signature, }; } diff --git a/packages/accounts/src/msca/plugins/multisig/actions/signMultisigUserOperation.ts b/packages/accounts/src/msca/plugins/multisig/actions/signMultisigUserOperation.ts index e196ff25ed..c710e8a9d2 100644 --- a/packages/accounts/src/msca/plugins/multisig/actions/signMultisigUserOperation.ts +++ b/packages/accounts/src/msca/plugins/multisig/actions/signMultisigUserOperation.ts @@ -8,11 +8,10 @@ import { } from "@alchemy/aa-core"; import { type Chain, type Client, type Transport } from "viem"; import { MultisigMissingSignatureError } from "../../../errors.js"; -import { combineSignatures, getSignerType } from "../index.js"; +import { combineSignatures, splitAggregatedSignature } from "../index.js"; import { type SignMultisigUserOperationParams, type SignMultisigUserOperationResult, - type Signature, } from "../types.js"; export async function signMultisigUserOperation< @@ -47,31 +46,48 @@ export async function signMultisigUserOperation< throw new MultisigMissingSignatureError(); } - const ep = account.getEntryPoint(); - const uoHash = ep.getUserOperationHash(userOperationRequest); - const signature = await account.signUserOperationHash(uoHash); const signerAddress = await account.getSigner().getAddress(); - const signerType = await getSignerType({ - client, - signature, - signer: account.getSigner(), + + const signedRequest = await client.signUserOperation({ + account, + uoStruct: userOperationRequest, + context: { + aggregatedSignature: combineSignatures({ + signatures, + upperLimitMaxFeePerGas: userOperationRequest.maxFeePerGas, + upperLimitMaxPriorityFeePerGas: + userOperationRequest.maxPriorityFeePerGas, + upperLimitPvg: userOperationRequest.preVerificationGas, + usingMaxValues: false, + }), + signatures, + userOpSignatureType: "UPPERLIMIT", + }, }); - const signatureObj: Signature = { - signerType, - signer: signerAddress, - signature, - userOpSigType: "UPPERLIMIT", - }; + const splitSignatures = await splitAggregatedSignature({ + account, + request: signedRequest, + aggregatedSignature: signedRequest.signature, + // split works on the assumption that we have t - 1 signatures + // we have signatures.length + 1 signatures now, so we need sl + 1 + 1 + threshold: signatures.length + 2, + }); + + const signatureObj = splitSignatures.signatures.find( + (x) => x.signer === signerAddress + ); + + if (!signatureObj) { + // TODO: strongly type this + throw new Error( + "INTERNAL ERROR: signature not found in split signatures, this is an internal bug please report" + ); + } return { signatureObj, - signature, - aggregatedSignature: combineSignatures({ - signatures: [...signatures, signatureObj], - upperLimitMaxFeePerGas: userOperationRequest.maxFeePerGas, - upperLimitMaxPriorityFeePerGas: userOperationRequest.maxPriorityFeePerGas, - upperLimitPvg: userOperationRequest.preVerificationGas, - }), + signature: signatureObj.signature, + aggregatedSignature: signedRequest.signature, }; } diff --git a/packages/accounts/src/msca/plugins/multisig/extension.ts b/packages/accounts/src/msca/plugins/multisig/extension.ts index b8b553b724..80b7ec8311 100644 --- a/packages/accounts/src/msca/plugins/multisig/extension.ts +++ b/packages/accounts/src/msca/plugins/multisig/extension.ts @@ -1,5 +1,6 @@ import { type GetAccountParameter, + type GetEntryPointFromAccount, type IsUndefined, type SendUserOperationParameters, type SmartContractAccount, @@ -42,7 +43,9 @@ export type MultisigPluginActions< proposeUserOperation: ( params: SendUserOperationParameters - ) => Promise>; + ) => Promise< + ProposeUserOperationResult> + >; signMultisigUserOperation: ( params: SignMultisigUserOperationParams diff --git a/packages/accounts/src/msca/plugins/multisig/middleware.ts b/packages/accounts/src/msca/plugins/multisig/middleware.ts index 2f8e8dfbb4..83a5018695 100644 --- a/packages/accounts/src/msca/plugins/multisig/middleware.ts +++ b/packages/accounts/src/msca/plugins/multisig/middleware.ts @@ -6,23 +6,47 @@ import { isValidRequest, resolveProperties, type ClientMiddlewareFn, + type UserOperationRequest_v6, + type UserOperationRequest_v7, } from "@alchemy/aa-core"; -import { isHex, type Hex } from "viem"; +import { type Hex } from "viem"; import { isMultisigModularAccount } from "../../account/multisigAccount.js"; import { InvalidContextSignatureError, MultisigAccountExpectedError, } from "../../errors.js"; -import { getThreshold } from "./actions/getThreshold.js"; import { combineSignatures, getSignerType, splitAggregatedSignature, + type MultisigUserOperationContext, } from "./index.js"; -export const multisigSignatureMiddleware: ClientMiddlewareFn<{ - signature: Hex; -}> = async (struct, { account, client, context }) => { +/** + * A signer middleware to be used with Multisig Account Clients. + * This middleware handles correctly aggregating signatures passed through + * as context when sending UserOperations, proposing UserOperations, or adding signatures to a UserOperation. + * + * @param struct the user operation struct to be signed + * @param param the parameters to be passed to the middleware + * @param param.account the account to be used for signing + * @param param.client the smart account client that will be used for RPC requests + * @param param.context the context object containing the signatures to be aggregated {@link MultisigUserOperationContext} + * @returns a Promise containing a UserOperation with an aggregated signature in the `signature` field + */ +export const multisigSignatureMiddleware: ClientMiddlewareFn< + MultisigUserOperationContext +> = async (struct, { account, client, context }) => { + // if the signature is not present, this has to be UPPERLIMIT because it's likely a propose operation + if ( + !context || + (context.userOpSignatureType === "ACTUAL" && + !context.signatures && + !context.aggregatedSignature) + ) { + throw new InvalidContextSignatureError(); + } + if (!isSmartAccountWithSigner(account)) { throw new SmartAccountWithSignerRequiredError(); } @@ -32,6 +56,7 @@ export const multisigSignatureMiddleware: ClientMiddlewareFn<{ } const resolvedStruct = await resolveProperties(struct); + const request = deepHexlify(resolvedStruct); if (!isValidRequest(request)) { throw new InvalidUserOperationError(resolvedStruct); @@ -47,36 +72,50 @@ export const multisigSignatureMiddleware: ClientMiddlewareFn<{ signer: account.getSigner(), }); - // TODO: this needs to actually check the account's installed plugins and fetch the multisig plugin address - const threshold = await getThreshold(client, { account }); - - // if there is no override, then return the dummy signature - if (context?.signature == null) { + // then this is a propose operation + if ( + context.userOpSignatureType === "UPPERLIMIT" && + context?.signatures?.length == null && + context?.aggregatedSignature == null + ) { return { ...resolvedStruct, - signature: await account.getDummySignature(), + signature: combineSignatures({ + signatures: [ + { + signature, + signer: await account.getSigner().getAddress(), + signerType, + userOpSigType: context.userOpSignatureType, + }, + ], + upperLimitMaxFeePerGas: request.maxFeePerGas, + upperLimitMaxPriorityFeePerGas: request.maxPriorityFeePerGas, + upperLimitPvg: request.preVerificationGas, + usingMaxValues: false, + }), }; } - if (!isHex(context.signature)) { + if (context.aggregatedSignature == null || context.signatures == null) { throw new InvalidContextSignatureError(); } + // otherwise this is a sign operation const { upperLimitPvg, upperLimitMaxFeePerGas, upperLimitMaxPriorityFeePerGas, - signatures, } = await splitAggregatedSignature({ - aggregatedSignature: context.signature, - threshold: Number(threshold), + aggregatedSignature: context.aggregatedSignature, + threshold: context.signatures.length + 1, account, request, }); const finalSignature = combineSignatures({ - signatures: signatures.concat({ - userOpSigType: "ACTUAL", + signatures: context.signatures.concat({ + userOpSigType: context.userOpSignatureType, signerType, signature, signer: await account.getSigner().getAddress(), @@ -84,6 +123,11 @@ export const multisigSignatureMiddleware: ClientMiddlewareFn<{ upperLimitPvg, upperLimitMaxFeePerGas, upperLimitMaxPriorityFeePerGas, + usingMaxValues: isUsingMaxValues(request, { + upperLimitPvg, + upperLimitMaxFeePerGas, + upperLimitMaxPriorityFeePerGas, + }), }); return { @@ -91,3 +135,33 @@ export const multisigSignatureMiddleware: ClientMiddlewareFn<{ signature: finalSignature, }; }; + +const isUsingMaxValues = ( + request: UserOperationRequest_v6 | UserOperationRequest_v7, + upperLimits: { + upperLimitPvg: Hex; + upperLimitMaxFeePerGas: Hex; + upperLimitMaxPriorityFeePerGas: Hex; + } +): boolean => { + if ( + BigInt(request.preVerificationGas) !== BigInt(upperLimits.upperLimitPvg) + ) { + return false; + } + + if ( + BigInt(request.maxFeePerGas) !== BigInt(upperLimits.upperLimitMaxFeePerGas) + ) { + return false; + } + + if ( + BigInt(request.maxPriorityFeePerGas) !== + BigInt(upperLimits.upperLimitMaxPriorityFeePerGas) + ) { + return false; + } + + return true; +}; diff --git a/packages/accounts/src/msca/plugins/multisig/plugin.ts b/packages/accounts/src/msca/plugins/multisig/plugin.ts index e4547d3c04..ebad5d8759 100644 --- a/packages/accounts/src/msca/plugins/multisig/plugin.ts +++ b/packages/accounts/src/msca/plugins/multisig/plugin.ts @@ -392,7 +392,7 @@ export const MultisigPluginAbi = [ ], name: "checkNSignatures", outputs: [ - { name: "failed", internalType: "bool", type: "bool" }, + { name: "success", internalType: "bool", type: "bool" }, { name: "firstFailure", internalType: "uint256", type: "uint256" }, ], }, @@ -908,9 +908,10 @@ export const MultisigPluginAbi = [ { type: "error", inputs: [], name: "ECDSARecoverFailure" }, { type: "error", inputs: [], name: "EmptyOwnersNotAllowed" }, { type: "error", inputs: [], name: "InvalidAction" }, - { type: "error", inputs: [], name: "InvalidGasValues" }, + { type: "error", inputs: [], name: "InvalidAddress" }, { type: "error", inputs: [], name: "InvalidMaxFeePerGas" }, { type: "error", inputs: [], name: "InvalidMaxPriorityFeePerGas" }, + { type: "error", inputs: [], name: "InvalidNumSigsOnActualGas" }, { type: "error", inputs: [{ name: "owner", internalType: "address", type: "address" }], diff --git a/packages/accounts/src/msca/plugins/multisig/types.ts b/packages/accounts/src/msca/plugins/multisig/types.ts index d1e05fabb3..fe512e0641 100644 --- a/packages/accounts/src/msca/plugins/multisig/types.ts +++ b/packages/accounts/src/msca/plugins/multisig/types.ts @@ -46,6 +46,14 @@ export type ProposeUserOperationResult< signatureObj: Signature; }; -export type MultisigUserOperationContext = { - signature: Hex; -}; +export type MultisigUserOperationContext = + | { + userOpSignatureType: Extract; + aggregatedSignature?: Hex; + signatures?: Signature[]; + } + | { + aggregatedSignature: Hex; + signatures: Signature[]; + userOpSignatureType: Extract; + }; diff --git a/packages/accounts/src/msca/plugins/multisig/utils/combineSignatures.ts b/packages/accounts/src/msca/plugins/multisig/utils/combineSignatures.ts index 87cbb42675..fb0b1866e7 100644 --- a/packages/accounts/src/msca/plugins/multisig/utils/combineSignatures.ts +++ b/packages/accounts/src/msca/plugins/multisig/utils/combineSignatures.ts @@ -1,22 +1,24 @@ import { type Hex, concat, pad } from "viem"; -import { formatSignatures } from "./formatSignatures.js"; import type { Signature } from "../types.js"; +import { formatSignatures } from "./formatSignatures.js"; export const combineSignatures = ({ signatures, upperLimitMaxFeePerGas, upperLimitMaxPriorityFeePerGas, upperLimitPvg, + usingMaxValues, }: { upperLimitPvg: Hex; upperLimitMaxFeePerGas: Hex; upperLimitMaxPriorityFeePerGas: Hex; signatures: Signature[]; + usingMaxValues: boolean; }) => { return concat([ pad(upperLimitPvg), pad(upperLimitMaxFeePerGas), pad(upperLimitMaxPriorityFeePerGas), - formatSignatures(signatures), + formatSignatures(signatures, usingMaxValues), ]); }; diff --git a/packages/accounts/src/msca/plugins/multisig/utils/formatSignatures.ts b/packages/accounts/src/msca/plugins/multisig/utils/formatSignatures.ts index 5ba7adec37..41afd8b2df 100644 --- a/packages/accounts/src/msca/plugins/multisig/utils/formatSignatures.ts +++ b/packages/accounts/src/msca/plugins/multisig/utils/formatSignatures.ts @@ -1,8 +1,21 @@ import { takeBytes } from "@alchemy/aa-core"; -import { hexToBigInt, concat, toHex, pad } from "viem"; +import { concat, hexToBigInt, pad, toHex } from "viem"; import type { Signature } from "../types"; -export const formatSignatures = (signatures: Signature[]) => { +/** + * Formats a collection of Signature objects into a single aggregated signature. + * The format is in the form of EOA_SIGS | CONTRACT_SIG_DATAS. The signatures are ordered + * by signer address. The EOA SIGS contain the 65 signautre data for EOA signers and 65 bytes containing SIGNER | OFFSET | V for contract signers. + * The OFFSET is used to fetch the signature data from the CONTRACT_SIG_DATAS. + * + * @param signatures the array of {@link Signature} objects to combine into the correct aggregated signature format excluding the upper limits + * @param usingMaxValues a boolean indicating wether or not the UserOperation is using the UPPER_LIMIT for the gas and fee values + * @returns the Hex representation of the signature + */ +export const formatSignatures = ( + signatures: Signature[], + usingMaxValues: boolean = false +) => { let eoaSigs: string = ""; let contractSigs: string = ""; let offset: bigint = BigInt(65 * signatures.length); @@ -15,7 +28,7 @@ export const formatSignatures = (signatures: Signature[]) => { }) .forEach((sig) => { // add 32 to v if the signature covers the actual gas values - const addV = sig.userOpSigType === "ACTUAL" ? 32 : 0; + const addV = sig.userOpSigType === "ACTUAL" && !usingMaxValues ? 32 : 0; if (sig.signerType === "EOA") { let v = diff --git a/packages/alchemy/e2e-tests/multisig-account.e2e.test.ts b/packages/alchemy/e2e-tests/multisig-account.e2e.test.ts index 331ffefa01..f95dca8cde 100644 --- a/packages/alchemy/e2e-tests/multisig-account.e2e.test.ts +++ b/packages/alchemy/e2e-tests/multisig-account.e2e.test.ts @@ -1,4 +1,5 @@ import { LocalAccountSigner, sepolia } from "@alchemy/aa-core"; +import { fromHex, pad } from "viem"; import { createMultisigAccountAlchemyClient, type AlchemyMultisigAccountClientConfig, @@ -8,7 +9,6 @@ import { MODULAR_MULTISIG_ACCOUNT_OWNER_MNEMONIC, PAYMASTER_POLICY_ID, } from "./constants.js"; -import { fromHex, pad } from "viem"; const chain = sepolia; @@ -46,7 +46,7 @@ describe("Multisig Modular Account Alchemy Client Tests", async () => { threshold, }); expect(address).toMatchInlineSnapshot( - '"0x4ff93F25764CefC22aeeE111CEf47CD1B5e05370"' + '"0xea78315aec5Ff47bF320843A1BaA769C99c8Ae32"' ); }); @@ -84,12 +84,13 @@ describe("Multisig Modular Account Alchemy Client Tests", async () => { expect(initiator.getAddress()).toBe(submitter.getAddress()); - const { aggregatedSignature } = await initiator.proposeUserOperation({ - uo: { - target: initiator.getAddress(), - data: "0x", - }, - }); + const { aggregatedSignature, signatureObj } = + await initiator.proposeUserOperation({ + uo: { + target: initiator.getAddress(), + data: "0x", + }, + }); const result = await submitter.sendUserOperation({ uo: { @@ -97,7 +98,9 @@ describe("Multisig Modular Account Alchemy Client Tests", async () => { data: "0x", }, context: { - signature: aggregatedSignature, + aggregatedSignature, + signatures: [signatureObj], + userOpSignatureType: "ACTUAL", }, }); @@ -126,7 +129,7 @@ describe("Multisig Modular Account Alchemy Client Tests", async () => { threshold, }); - const { aggregatedSignature, request } = + const { aggregatedSignature, request, signatureObj } = await provider1.proposeUserOperation({ uo: { target: provider1.getAddress(), @@ -149,10 +152,13 @@ describe("Multisig Modular Account Alchemy Client Tests", async () => { maxFeePerGas: request.maxFeePerGas, maxPriorityFeePerGas: request.maxPriorityFeePerGas, nonceKey: fromHex(`0x${pad(request.nonce).slice(2, 26)}`, "bigint"), // Nonce key is the first 24 bytes of the nonce + // @ts-ignore paymasterAndData: request.paymasterAndData, }, context: { - signature: aggregatedSignature, + aggregatedSignature, + signatures: [signatureObj], + userOpSignatureType: "ACTUAL", }, }); const txnHash = provider2.waitForUserOperationTransaction(result); @@ -185,7 +191,7 @@ describe("Multisig Modular Account Alchemy Client Tests", async () => { const { account: { address }, } = provider1; - expect(address).toEqual("0xE2c5429De9133F03f3D36d2Be3695AB315D65ECa"); + expect(address).toBe("0xDAcFC8de3c63579BA8aF72a0b73262a85c176b3F"); const { request, signatureObj: signature1 } = await provider1.proposeUserOperation({ @@ -201,11 +207,12 @@ describe("Multisig Modular Account Alchemy Client Tests", async () => { }, }); - const { aggregatedSignature } = await provider2.signMultisigUserOperation({ - account: provider2.account, - signatures: [signature1], - userOperationRequest: request, - }); + const { aggregatedSignature, signatureObj } = + await provider2.signMultisigUserOperation({ + account: provider2.account, + signatures: [signature1], + userOperationRequest: request, + }); const result = await provider3.sendUserOperation({ uo: request.callData, @@ -216,10 +223,11 @@ describe("Multisig Modular Account Alchemy Client Tests", async () => { maxFeePerGas: request.maxFeePerGas, maxPriorityFeePerGas: request.maxPriorityFeePerGas, nonceKey: fromHex(`0x${pad(request.nonce).slice(2, 26)}`, "bigint"), // Nonce key is the first 24 bytes of the nonce - paymasterAndData: request.paymasterAndData, }, context: { - signature: aggregatedSignature, + aggregatedSignature, + signatures: [signatureObj], + userOpSignatureType: "ACTUAL", }, }); diff --git a/packages/alchemy/src/react/hooks/useClientActions.ts b/packages/alchemy/src/react/hooks/useClientActions.ts index 21bb0fa3c6..292bb21057 100644 --- a/packages/alchemy/src/react/hooks/useClientActions.ts +++ b/packages/alchemy/src/react/hooks/useClientActions.ts @@ -81,11 +81,6 @@ export type ClientActionParameters< * and await them in your UX. This is particularly useful for using Plugins * with Modular Accounts. * - * @param args the arguments for the hook - * @param args.client the smart account client to use for executing the actions - * @param args.actions the smart account client decorator actions to execute - * @returns a set of functions for executing the actions inlcuding the state of execution see {@link UseClientActionsResult} - * * @example * ```tsx * const Foo = () => { @@ -101,6 +96,11 @@ export type ClientActionParameters< * }); * }; * ``` + * + * @param args the hooks arguments highlighted below + * @param args.client the smart account client returned from {@link useSmartAccountClient} + * @param args.actions the smart account client decorator you want to execute actions from + * @returns an object containing methods to execute the actions as well loading and error states (see: {@link UseClientActionsResult}) */ export function useClientActions< TTransport extends Transport = Transport, diff --git a/packages/core/src/actions/smartAccount/buildUserOperation.ts b/packages/core/src/actions/smartAccount/buildUserOperation.ts index 6cdc673f31..3e633c51bd 100644 --- a/packages/core/src/actions/smartAccount/buildUserOperation.ts +++ b/packages/core/src/actions/smartAccount/buildUserOperation.ts @@ -10,7 +10,7 @@ import type { UserOperationStruct } from "../../types.js"; import { _initUserOperation } from "./internal/initUserOperation.js"; import { _runMiddlewareStack } from "./internal/runMiddlewareStack.js"; import type { - SendUserOperationParameters, + BuildUserOperationParameters, UserOperationContext, } from "./types"; @@ -26,7 +26,7 @@ export async function buildUserOperation< TEntryPointVersion extends GetEntryPointFromAccount = GetEntryPointFromAccount >( client: Client, - args: SendUserOperationParameters + args: BuildUserOperationParameters ): Promise> { const { account = client.account, overrides, context } = args; if (!account) { diff --git a/packages/core/src/actions/smartAccount/buildUserOperationFromTx.ts b/packages/core/src/actions/smartAccount/buildUserOperationFromTx.ts index ce4dcd1186..0712dc4a05 100644 --- a/packages/core/src/actions/smartAccount/buildUserOperationFromTx.ts +++ b/packages/core/src/actions/smartAccount/buildUserOperationFromTx.ts @@ -19,6 +19,45 @@ import type { import { buildUserOperation } from "./buildUserOperation.js"; import type { UserOperationContext } from "./types.js"; +/** + * Performs [`buildUserOperationFromTx`](./buildUserOperationFromTx.md) in batch and builds into a single, yet to be signed `UserOperation` (UO) struct. The output user operation struct will be filled with all gas fields (and paymaster data if a paymaster is used) based on the transactions data (`to`, `data`, `value`, `maxFeePerGas`, `maxPriorityFeePerGas`) computed using the configured [`ClientMiddlewares`](/packages/aa-core/smart-account-client/middleware/index) on the `SmartAccountClient` + * + * @example + * ```ts +import type { RpcTransactionRequest } from "viem"; +import { smartAccountClient } from "./smartAccountClient"; +// [!code focus:99] +// buildUserOperationFromTx converts a traditional Ethereum transaction and returns +// the unsigned user operation struct after constructing the user operation struct +// through the middleware pipeline +const tx: RpcTransactionRequest = { + from, // ignored + to, + data: encodeFunctionData({ + abi: ContractABI.abi, + functionName: "func", + args: [arg1, arg2, ...], + }), +}; +const uoStruct = await smartAccountClient.buildUserOperationFromTx(tx); + +// signUserOperation signs the above unsigned user operation struct built +// using the account connected to the smart account client +const request = await smartAccountClient.signUserOperation({ uoStruct }); + +// You can use the BundlerAction `sendRawUserOperation` (packages/core/src/actions/bundler/sendRawUserOperation.ts) +// to send the signed user operation request to the bundler, requesting the bundler to send the signed uo to the +// EntryPoint contract pointed at by the entryPoint address parameter +const entryPointAddress = client.account.getEntryPoint().address; +const uoHash = await smartAccountClient.sendRawUserOperation({ request, entryPoint: entryPointAddress }); +``` + * + * @param client the smart account client to use for RPC requests + * @param args {@link SendTransactionParameters} + * @param overrides optional {@link UserOperationOverrides} to use for any of the fields + * @param context if the smart account client requires additinoal context for building UOs + * @returns a Promise containing the built user operation + */ export async function buildUserOperationFromTx< TChain extends Chain | undefined = Chain | undefined, TAccount extends SmartContractAccount | undefined = diff --git a/packages/core/src/actions/smartAccount/buildUserOperationFromTxs.ts b/packages/core/src/actions/smartAccount/buildUserOperationFromTxs.ts index 6a4839dae8..1717096d3f 100644 --- a/packages/core/src/actions/smartAccount/buildUserOperationFromTxs.ts +++ b/packages/core/src/actions/smartAccount/buildUserOperationFromTxs.ts @@ -11,20 +11,94 @@ import type { UserOperationOverrides } from "../../types"; import { bigIntMax } from "../../utils/index.js"; import { buildUserOperation } from "./buildUserOperation.js"; import type { + BuildTransactionParameters, BuildUserOperationFromTransactionsResult, - SendTransactionsParameters, + UserOperationContext, } from "./types"; +/** + * Performs {@link buildUserOperationFromTx} in batch and builds into a single, + * yet to be signed `UserOperation` (UO) struct. The output user operation struct + * will be filled with all gas fields (and paymaster data if a paymaster is used) + * based on the transactions data (`to`, `data`, `value`, `maxFeePerGas`, + * `maxPriorityFeePerGas`) computed using the configured + * [`ClientMiddlewares`](/packages/aa-core/smart-account-client/middleware/index) on the `SmartAccountClient` + * + * @example + * ```ts + * import type { RpcTransactionRequest } from "viem"; +import { smartAccountClient } from "./smartAccountClient"; +// [!code focus:99] +// buildUserOperationFromTxs converts traditional Ethereum transactions in batch and returns +// the unsigned user operation struct after constructing the user operation struct +// through the middleware pipeline +const requests: RpcTransactionRequest[] = [ + { + from, // ignored + to, + data: encodeFunctionData({ + abi: ContractABI.abi, + functionName: "func", + args: [arg1, arg2, ...], + }), + }, + { + from, // ignored + to, + data: encodeFunctionData({ + abi: ContractABI.abi, + functionName: "func", + args: [arg1, arg2, ...], + }), + }, + ... + { + from, // ignored + to, + data: encodeFunctionData({ + abi: ContractABI.abi, + functionName: "func", + args: [arg1, arg2, ...], + }), + }, +]; +const uoStruct = await smartAccountClient.buildUserOperationFromTxs({ + requests, +}); + +// signUserOperation signs the above unsigned user operation struct built +// using the account connected to the smart account client +const request = await smartAccountClient.signUserOperation({ uoStruct }); + +// You can use the BundlerAction `sendRawUserOperation` (packages/core/src/actions/bundler/sendRawUserOperation.ts) +// to send the signed user operation request to the bundler, requesting the bundler to send the signed uo to the +// EntryPoint contract pointed at by the entryPoint address parameter +const entryPointAddress = client.account.getEntryPoint().address; +const uoHash = await smartAccountClient.sendRawUserOperation({ + request, + entryPoint: entryPointAddress, +}); + * ``` + * + * @param client the smart account client to use to make RPC calls + * @param args {@link BuildTransactionParameters} an object containing the requests + * to build as well as, the account if not hoisted, the context, the overrides, and + * optionally a flag to enable signing of the UO via the underlying middleware + * @returns a Promise containing the built user operation + */ export async function buildUserOperationFromTxs< TTransport extends Transport = Transport, TChain extends Chain | undefined = Chain | undefined, TAccount extends SmartContractAccount | undefined = | SmartContractAccount | undefined, - TEntryPointVersion extends GetEntryPointFromAccount = GetEntryPointFromAccount + TEntryPointVersion extends GetEntryPointFromAccount = GetEntryPointFromAccount, + TContext extends UserOperationContext | undefined = + | UserOperationContext + | undefined >( client: Client, - args: SendTransactionsParameters + args: BuildTransactionParameters ): Promise> { const { account = client.account, requests, overrides, context } = args; if (!account) { @@ -80,9 +154,9 @@ export async function buildUserOperationFromTxs< const uoStruct = await buildUserOperation(client, { uo: batch, - overrides: _overrides, account, context, + overrides: _overrides, }); return { diff --git a/packages/core/src/actions/smartAccount/checkGasSponsorshipEligibility.ts b/packages/core/src/actions/smartAccount/checkGasSponsorshipEligibility.ts index 8a4ec94dcc..80cb046d07 100644 --- a/packages/core/src/actions/smartAccount/checkGasSponsorshipEligibility.ts +++ b/packages/core/src/actions/smartAccount/checkGasSponsorshipEligibility.ts @@ -10,7 +10,35 @@ import type { UserOperationContext, } from "./types"; -export const checkGasSponsorshipEligibility: < +/** + * This function verifies the eligibility of the connected account for gas sponsorship concerning the upcoming `UserOperation` (UO) that is intended to be sent. + * Internally, this method invokes [`buildUserOperation`](./buildUserOperation.md), which navigates through the middleware pipeline, including the `PaymasterMiddleware`. Its purpose is to construct the UO struct meant for transmission to the bundler. Following the construction of the UO struct, this function verifies if the resulting structure contains a non-empty `paymasterAndData` field. + * You can utilize this method before sending the user operation to confirm its eligibility for gas sponsorship. Depending on the outcome, it allows you to tailor the user experience accordingly, based on eligibility. + * + * @example + * ```ts + * import { smartAccountClient } from "./smartAccountClient"; +// [!code focus:99] +const eligible = await smartAccountClient.checkGasSponsorshipEligibility({ + uo: { + data: "0xCalldata", + target: "0xTarget", + value: 0n, + }, +}); + +console.log( + `User Operation is ${ + eligible ? "eligible" : "ineligible" + } for gas sponsorship.` +); + * ``` + * + * @param client the smart account client to use for making RPC calls + * @param args {@link SendUserOperationParameters} containing the user operation, account, context, and overrides + * @returns a Promise containing a boolean indicating if the account is elgibile for sponsorship + */ +export function checkGasSponsorshipEligibility< TTransport extends Transport = Transport, TChain extends Chain | undefined = Chain | undefined, TAccount extends SmartContractAccount | undefined = @@ -22,8 +50,8 @@ export const checkGasSponsorshipEligibility: < >( client: Client, args: SendUserOperationParameters -) => Promise = async (client, args) => { - const { account = client.account } = args; +): Promise { + const { account = client.account, overrides, context } = args; if (!account) { throw new AccountNotFoundError(); @@ -37,7 +65,12 @@ export const checkGasSponsorshipEligibility: < ); } - return buildUserOperation(client, args) + return buildUserOperation(client, { + uo: args.uo, + account, + overrides, + context, + }) .then((userOperationStruct) => account.getEntryPoint().version === "0.6.0" ? (userOperationStruct as UserOperationStruct<"0.6.0">) @@ -50,4 +83,4 @@ export const checkGasSponsorshipEligibility: < .paymasterData !== null ) .catch(() => false); -}; +} diff --git a/packages/core/src/actions/smartAccount/dropAndReplaceUserOperation.ts b/packages/core/src/actions/smartAccount/dropAndReplaceUserOperation.ts index 70f029fd07..697452bb00 100644 --- a/packages/core/src/actions/smartAccount/dropAndReplaceUserOperation.ts +++ b/packages/core/src/actions/smartAccount/dropAndReplaceUserOperation.ts @@ -106,5 +106,6 @@ export async function dropAndReplaceUserOperation< uoStruct: uoToSend, account, context, + overrides: _overrides, }); } diff --git a/packages/core/src/actions/smartAccount/internal/initUserOperation.ts b/packages/core/src/actions/smartAccount/internal/initUserOperation.ts index 43327d011f..90dc76d65b 100644 --- a/packages/core/src/actions/smartAccount/internal/initUserOperation.ts +++ b/packages/core/src/actions/smartAccount/internal/initUserOperation.ts @@ -9,6 +9,7 @@ import { ChainNotFoundError } from "../../../errors/client.js"; import type { UserOperationStruct } from "../../../types.js"; import { conditionalReturn, type Deferrable } from "../../../utils/index.js"; import type { + BuildUserOperationParameters, SendUserOperationParameters, UserOperationContext, } from "../types.js"; @@ -39,7 +40,9 @@ export async function _initUserOperation< TEntryPointVersion extends GetEntryPointFromAccount = GetEntryPointFromAccount >( client: BaseSmartAccountClient, - args: SendUserOperationParameters + args: + | SendUserOperationParameters + | BuildUserOperationParameters ): Promise>> { const { account = client.account, uo, overrides } = args; if (!account) { diff --git a/packages/core/src/actions/smartAccount/internal/sendUserOperation.ts b/packages/core/src/actions/smartAccount/internal/sendUserOperation.ts index 10145c3359..26647774f5 100644 --- a/packages/core/src/actions/smartAccount/internal/sendUserOperation.ts +++ b/packages/core/src/actions/smartAccount/internal/sendUserOperation.ts @@ -8,10 +8,20 @@ import type { BaseSmartAccountClient } from "../../../client/smartAccountClient" import type { SendUserOperationResult } from "../../../client/types"; import { AccountNotFoundError } from "../../../errors/account.js"; import { ChainNotFoundError } from "../../../errors/client.js"; -import type { UserOperationStruct } from "../../../types"; -import { deepHexlify, resolveProperties } from "../../../utils/index.js"; +import type { + UserOperationOverrides, + UserOperationStruct, +} from "../../../types"; +import { signUserOperation } from "../signUserOperation.js"; import type { GetContextParameter, UserOperationContext } from "../types"; +/** + * Used internally to send a user operation that has **already** been signed + * + * @param client a base smart account client instance with middleware configured + * @param args user operation struct, overrides, account, and context to be used in sending + * @returns A Promise containing the send user operation result {@link SendUserOperationResult} + */ export async function _sendUserOperation< TTransport extends Transport = Transport, TChain extends Chain | undefined = Chain | undefined, @@ -26,10 +36,11 @@ export async function _sendUserOperation< client: BaseSmartAccountClient, args: { uoStruct: UserOperationStruct; + overrides?: UserOperationOverrides; } & GetAccountParameter & GetContextParameter ): Promise> { - const { account = client.account, context } = args; + const { account = client.account, uoStruct, context, overrides } = args; if (!account) { throw new AccountNotFoundError(); } @@ -38,21 +49,16 @@ export async function _sendUserOperation< throw new ChainNotFoundError(); } - const request = await client.middleware - .signUserOperation(args.uoStruct, { - ...args, - account, - client, - context, - }) - .then(resolveProperties) - .then(deepHexlify); + const entryPoint = account.getEntryPoint(); + const request = await signUserOperation(client, { + uoStruct, + account, + context, + overrides, + }); return { - hash: await client.sendRawUserOperation( - request, - account.getEntryPoint().address - ), + hash: await client.sendRawUserOperation(request, entryPoint.address), request, }; } diff --git a/packages/core/src/actions/smartAccount/sendTransaction.ts b/packages/core/src/actions/smartAccount/sendTransaction.ts index 173781fb41..b55bf85095 100644 --- a/packages/core/src/actions/smartAccount/sendTransaction.ts +++ b/packages/core/src/actions/smartAccount/sendTransaction.ts @@ -62,6 +62,7 @@ export async function sendTransaction< account: account as SmartContractAccount, uoStruct, context, + overrides, }); return waitForUserOperationTransaction(client, { hash }); diff --git a/packages/core/src/actions/smartAccount/sendTransactions.ts b/packages/core/src/actions/smartAccount/sendTransactions.ts index 914610fab0..9be5e767a3 100644 --- a/packages/core/src/actions/smartAccount/sendTransactions.ts +++ b/packages/core/src/actions/smartAccount/sendTransactions.ts @@ -36,12 +36,14 @@ export async function sendTransactions< requests, overrides, account, + context, }); const { hash } = await _sendUserOperation(client, { account, uoStruct, context, + overrides, }); return waitForUserOperationTransaction(client, { hash }); diff --git a/packages/core/src/actions/smartAccount/sendUserOperation.ts b/packages/core/src/actions/smartAccount/sendUserOperation.ts index 7c4243055b..434c547001 100644 --- a/packages/core/src/actions/smartAccount/sendUserOperation.ts +++ b/packages/core/src/actions/smartAccount/sendUserOperation.ts @@ -14,7 +14,14 @@ import type { UserOperationContext, } from "./types.js"; -export const sendUserOperation: < +/** + * Sends a user operation or batch of user operations using the connected account. Before executing, sendUserOperation will run the user operation through the middleware pipeline. + * + * @param client the smart account client to use for RPC requests + * @param args {@link SendUserOperationParameters} containg the UO or batch to send, context, overrides, and account if not hoisted on the client + * @returns a Promise containing the {@link SendUserOperationResult} + */ +export async function sendUserOperation< TTransport extends Transport = Transport, TChain extends Chain | undefined = Chain | undefined, TAccount extends SmartContractAccount | undefined = @@ -27,11 +34,8 @@ export const sendUserOperation: < >( client: Client, args: SendUserOperationParameters -) => Promise> = async ( - client, - args -) => { - const { account = client.account, context } = args; +): Promise> { + const { account = client.account, context, overrides } = args; if (!account) { throw new AccountNotFoundError(); @@ -45,10 +49,17 @@ export const sendUserOperation: < ); } - const uoStruct = await buildUserOperation(client, args); + const uoStruct = await buildUserOperation(client, { + uo: args.uo, + account, + context, + overrides, + }); + return _sendUserOperation(client, { account, uoStruct, context, + overrides, }); -}; +} diff --git a/packages/core/src/actions/smartAccount/signUserOperation.ts b/packages/core/src/actions/smartAccount/signUserOperation.ts index df31a9e46f..bb34bd1827 100644 --- a/packages/core/src/actions/smartAccount/signUserOperation.ts +++ b/packages/core/src/actions/smartAccount/signUserOperation.ts @@ -24,7 +24,7 @@ export async function signUserOperation< client: Client, args: SignUserOperationParameters ): Promise> { - const { account = client.account } = args; + const { account = client.account, context } = args; if (!account) { throw new AccountNotFoundError(); @@ -47,6 +47,7 @@ export async function signUserOperation< ...args, account, client, + context, }) .then(resolveProperties) .then(deepHexlify); diff --git a/packages/core/src/actions/smartAccount/types.ts b/packages/core/src/actions/smartAccount/types.ts index 2f1d0216a9..48334940c0 100644 --- a/packages/core/src/actions/smartAccount/types.ts +++ b/packages/core/src/actions/smartAccount/types.ts @@ -44,15 +44,29 @@ export type SendUserOperationParameters< UserOperationOverridesParameter; //#endregion SendUserOperationParameters +//#region BuildUserOperationParameters +export type BuildUserOperationParameters< + TAccount extends SmartContractAccount | undefined, + TContext extends UserOperationContext | undefined = + | UserOperationContext + | undefined, + TEntryPointVersion extends GetEntryPointFromAccount = GetEntryPointFromAccount +> = SendUserOperationParameters; +//#endregion BuildUserOperationParameters + //#region SignUserOperationParameters export type SignUserOperationParameters< TAccount extends SmartContractAccount | undefined = | SmartContractAccount | undefined, - TEntryPointVersion extends GetEntryPointFromAccount = GetEntryPointFromAccount + TEntryPointVersion extends GetEntryPointFromAccount = GetEntryPointFromAccount, + TContext extends UserOperationContext | undefined = + | UserOperationContext + | undefined > = { uoStruct: UserOperationStruct; -} & GetAccountParameter; +} & GetAccountParameter & + GetContextParameter; //#endregion SignUserOperationParameters //#region SendTransactionsParameters @@ -69,6 +83,16 @@ export type SendTransactionsParameters< UserOperationOverridesParameter; //#endregion SendTransactionsParameters +//#region BuildTransactionParameters +export type BuildTransactionParameters< + TAccount extends SmartContractAccount | undefined, + TContext extends UserOperationContext | undefined = + | UserOperationContext + | undefined, + TEntryPointVersion extends GetEntryPointFromAccount = GetEntryPointFromAccount +> = SendTransactionsParameters; +//#endregion BuildTransactionParameters + //#region DropAndReplaceUserOperationParameters export type DropAndReplaceUserOperationParameters< TAccount extends SmartContractAccount | undefined, diff --git a/packages/core/src/client/decorators/smartAccountClient.ts b/packages/core/src/client/decorators/smartAccountClient.ts index cb1ec10c67..94622b848e 100644 --- a/packages/core/src/client/decorators/smartAccountClient.ts +++ b/packages/core/src/client/decorators/smartAccountClient.ts @@ -33,7 +33,9 @@ import { import { signTypedDataWith6492 } from "../../actions/smartAccount/signTypedDataWith6492.js"; import { signUserOperation } from "../../actions/smartAccount/signUserOperation.js"; import type { + BuildTransactionParameters, BuildUserOperationFromTransactionsResult, + BuildUserOperationParameters, DropAndReplaceUserOperationParameters, SendTransactionsParameters, SendUserOperationParameters, @@ -64,7 +66,7 @@ export type BaseSmartAccountClientActions< TEntryPointVersion extends GetEntryPointFromAccount = GetEntryPointFromAccount > = { buildUserOperation: ( - args: SendUserOperationParameters + args: BuildUserOperationParameters ) => Promise>; buildUserOperationFromTx: ( args: SendTransactionParameters, @@ -72,7 +74,7 @@ export type BaseSmartAccountClientActions< context?: TContext ) => Promise>; buildUserOperationFromTxs: ( - args: SendTransactionsParameters + args: BuildTransactionParameters ) => Promise>; checkGasSponsorshipEligibility: < TContext extends UserOperationContext | undefined = @@ -82,7 +84,7 @@ export type BaseSmartAccountClientActions< args: SendUserOperationParameters ) => Promise; signUserOperation: ( - args: SignUserOperationParameters + args: SignUserOperationParameters ) => Promise>; dropAndReplaceUserOperation: ( args: DropAndReplaceUserOperationParameters @@ -98,7 +100,11 @@ export type BaseSmartAccountClientActions< args: SendTransactionsParameters ) => Promise; sendUserOperation: ( - args: SendUserOperationParameters + args: SendUserOperationParameters< + TAccount, + TContext, + GetEntryPointFromAccount + > ) => Promise>; waitForUserOperationTransaction: ( args: WaitForUserOperationTxParameters