diff --git a/.changeset/proud-rockets-play.md b/.changeset/proud-rockets-play.md new file mode 100644 index 00000000000..657bd7b112d --- /dev/null +++ b/.changeset/proud-rockets-play.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +Expose getUserOpHash utility function diff --git a/packages/thirdweb/src/exports/wallets/smart.ts b/packages/thirdweb/src/exports/wallets/smart.ts index ffacbb0fe7d..4f02bab27e1 100644 --- a/packages/thirdweb/src/exports/wallets/smart.ts +++ b/packages/thirdweb/src/exports/wallets/smart.ts @@ -5,6 +5,7 @@ export { createUnsignedUserOp, signUserOp, createAndSignUserOp, + getUserOpHash, } from "../../wallets/smart/lib/userop.js"; export { diff --git a/packages/thirdweb/src/wallets/in-app/core/actions/sign-message.enclave.ts b/packages/thirdweb/src/wallets/in-app/core/actions/sign-message.enclave.ts index d2d1b88d590..58a7ac1c9e7 100644 --- a/packages/thirdweb/src/wallets/in-app/core/actions/sign-message.enclave.ts +++ b/packages/thirdweb/src/wallets/in-app/core/actions/sign-message.enclave.ts @@ -6,13 +6,15 @@ import type { ClientScopedStorage } from "../authentication/client-scoped-storag export async function signMessage({ client, - payload: { message, isRaw }, + payload: { message, isRaw, originalMessage, chainId }, storage, }: { client: ThirdwebClient; payload: { message: string; isRaw: boolean; + originalMessage?: string; + chainId?: number; }; storage: ClientScopedStorage; }) { @@ -37,6 +39,8 @@ export async function signMessage({ messagePayload: { message, isRaw, + originalMessage, + chainId, }, }), }, diff --git a/packages/thirdweb/src/wallets/in-app/core/wallet/enclave-wallet.ts b/packages/thirdweb/src/wallets/in-app/core/wallet/enclave-wallet.ts index 02ecdc62f4b..8d52dad76d4 100644 --- a/packages/thirdweb/src/wallets/in-app/core/wallet/enclave-wallet.ts +++ b/packages/thirdweb/src/wallets/in-app/core/wallet/enclave-wallet.ts @@ -217,10 +217,10 @@ export class EnclaveWallet implements IWebWallet { return { transactionHash }; }, - async signMessage({ message }) { + async signMessage({ message, originalMessage, chainId }) { const messagePayload = (() => { if (typeof message === "string") { - return { message, isRaw: false }; + return { message, isRaw: false, originalMessage, chainId }; } return { message: @@ -228,6 +228,8 @@ export class EnclaveWallet implements IWebWallet { ? message.raw : bytesToHex(message.raw), isRaw: true, + originalMessage, + chainId, }; })(); diff --git a/packages/thirdweb/src/wallets/interfaces/wallet.ts b/packages/thirdweb/src/wallets/interfaces/wallet.ts index 09d57d76e9b..a43b2841d48 100644 --- a/packages/thirdweb/src/wallets/interfaces/wallet.ts +++ b/packages/thirdweb/src/wallets/interfaces/wallet.ts @@ -182,7 +182,15 @@ export type Account = { * const signature = await account.signMessage({ message: 'hello!' }); * ``` */ - signMessage: ({ message }: { message: SignableMessage }) => Promise; + signMessage: ({ + message, + originalMessage, + chainId, + }: { + message: SignableMessage; + originalMessage?: string; + chainId?: number; + }) => Promise; /** * Sign the given typed data and return the signature * @example diff --git a/packages/thirdweb/src/wallets/smart/lib/userop.ts b/packages/thirdweb/src/wallets/smart/lib/userop.ts index 289402eefed..7146215100a 100644 --- a/packages/thirdweb/src/wallets/smart/lib/userop.ts +++ b/packages/thirdweb/src/wallets/smart/lib/userop.ts @@ -1,5 +1,5 @@ import { maxUint96 } from "ox/Solidity"; -import { concat, keccak256, toHex } from "viem"; +import { concat } from "viem"; import type { Chain } from "../../../chains/types.js"; import type { ThirdwebClient } from "../../../client/client.js"; import { @@ -16,9 +16,11 @@ import type { PreparedTransaction } from "../../../transaction/prepare-transacti import type { TransactionReceipt } from "../../../transaction/types.js"; import { encodeAbiParameters } from "../../../utils/abi/encodeAbiParameters.js"; import { isContractDeployed } from "../../../utils/bytecode/is-contract-deployed.js"; -import type { Hex } from "../../../utils/encoding/hex.js"; +import { type Hex, toHex } from "../../../utils/encoding/hex.js"; import { hexToBytes } from "../../../utils/encoding/to-bytes.js"; import { isThirdwebUrl } from "../../../utils/fetch.js"; +import { keccak256 } from "../../../utils/hashing/keccak256.js"; +import { stringify } from "../../../utils/json.js"; import { resolvePromisedValue } from "../../../utils/promise/resolve-promised-value.js"; import type { Account } from "../../interfaces/wallet.js"; import type { @@ -597,6 +599,54 @@ export async function signUserOp(args: { }): Promise { const { userOp, chain, entrypointAddress, adminAccount } = args; + const userOpHash = await getUserOpHash({ + client: args.client, + userOp, + chain, + entrypointAddress, + }); + + if (adminAccount.signMessage) { + const signature = await adminAccount.signMessage({ + message: { + raw: hexToBytes(userOpHash), + }, + originalMessage: stringify(userOp), + chainId: chain.id, + }); + return { + ...userOp, + signature, + }; + } + throw new Error("signMessage not implemented in signingAccount"); +} + +/** + * Get the hash of a user operation. + * @param args - The options for getting the user operation hash + * @returns - The user operation hash + * @example + * ```ts + * import { getUserOpHash } from "thirdweb/wallets/smart"; + * + * const userOp = await createUnsignedUserOp(...); + * const userOpHash = await getUserOpHash({ + * client, + * userOp, + * chain, + * }); + * ``` + * @walletUtils + */ +export async function getUserOpHash(args: { + client: ThirdwebClient; + userOp: UserOperationV06 | UserOperationV07; + chain: Chain; + entrypointAddress?: string; +}): Promise { + const { userOp, chain, entrypointAddress } = args; + const entrypointVersion = getEntryPointVersion( entrypointAddress || ENTRYPOINT_ADDRESS_v0_6, ); @@ -623,19 +673,7 @@ export async function signUserOp(args: { userOp: userOp as UserOperationV06, }); } - - if (adminAccount.signMessage) { - const signature = await adminAccount.signMessage({ - message: { - raw: hexToBytes(userOpHash), - }, - }); - return { - ...userOp, - signature, - }; - } - throw new Error("signMessage not implemented in signingAccount"); + return userOpHash; } async function getAccountInitCode(options: {