From 545e0e56788057a02e294f2830d3dcbbc7bffef2 Mon Sep 17 00:00:00 2001 From: ultraviolet10 Date: Sat, 15 Feb 2025 01:36:59 +0530 Subject: [PATCH 1/9] wip: eip5792 rpc call implementations [skip ci] --- .../components/requests/WalletSendCalls.tsx | 37 +++++++++++++++++++ apps/iframe/src/constants/requestLabels.ts | 1 + apps/iframe/src/requests/approved.ts | 19 ++++++++++ apps/iframe/src/requests/permissionless.ts | 29 +++++++++++++++ apps/iframe/src/routes/request.lazy.tsx | 3 ++ apps/iframe/src/state/walletClient.ts | 1 + support/wallet-common/lib/index.ts | 2 + .../wallet-common/lib/interfaces/eip5792.ts | 6 +++ .../lib/interfaces/permissions.ts | 5 +++ 9 files changed, 103 insertions(+) create mode 100644 apps/iframe/src/components/requests/WalletSendCalls.tsx create mode 100644 support/wallet-common/lib/interfaces/eip5792.ts diff --git a/apps/iframe/src/components/requests/WalletSendCalls.tsx b/apps/iframe/src/components/requests/WalletSendCalls.tsx new file mode 100644 index 0000000000..2a01f2e319 --- /dev/null +++ b/apps/iframe/src/components/requests/WalletSendCalls.tsx @@ -0,0 +1,37 @@ +import { Button } from "../primitives/button/Button" +import RawRequestDetails from "./common/RawRequestDetails" +import RequestContent from "./common/RequestContent" +import RequestLayout from "./common/RequestLayout" +import type { RequestConfirmationProps } from "./props" + +export const WalletSendCalls = ({ method, params, reject, accept }: RequestConfirmationProps<"wallet_sendCalls">) => { + console.log("param", { method, params }) + // TODO only for testiog + return ( + + +
+
+ Calls + {/*
{formattedSignPayload}
*/} +
+ + +
+
+ +
+ + +
+
+ ) +} diff --git a/apps/iframe/src/constants/requestLabels.ts b/apps/iframe/src/constants/requestLabels.ts index 03a7a5f2f8..b258b866b3 100644 --- a/apps/iframe/src/constants/requestLabels.ts +++ b/apps/iframe/src/constants/requestLabels.ts @@ -10,6 +10,7 @@ export const requestLabels = { wallet_watchAsset: "Watch Asset", [HappyMethodNames.USE_ABI]: "Record ABI", [HappyMethodNames.REQUEST_SESSION_KEY]: "Approve Session Key", + wallet_sendCalls: "Send Calls", } as const export const permissionDescriptions = { diff --git a/apps/iframe/src/requests/approved.ts b/apps/iframe/src/requests/approved.ts index f1090cdb22..146b9b5259 100644 --- a/apps/iframe/src/requests/approved.ts +++ b/apps/iframe/src/requests/approved.ts @@ -177,6 +177,25 @@ export async function dispatchHandlers(request: PopupMsgs[Msgs.PopupApprove]) { return accountSessionKey.address } + // EIP-5792 + case "wallet_sendCalls": { + // TODO implementation pending + const callsData = request.payload.params?.[0] + + if (callsData) { + } + + return 1 + } + + case "wallet_showCallsStatus": { + const _bundleIdentifier = request.payload.params?.[0] + // TODO pretty popup + + // c.f. https://github.com/wevm/viem/blob/66e5f6ab7b683a90775dcb8fae340e3154d74b38/src/experimental/eip5792/actions/showCallsStatus.ts#L10 + return undefined + } + default: return await sendToWalletClient(request) } diff --git a/apps/iframe/src/requests/permissionless.ts b/apps/iframe/src/requests/permissionless.ts index fd5b3da380..396ed0c6a7 100644 --- a/apps/iframe/src/requests/permissionless.ts +++ b/apps/iframe/src/requests/permissionless.ts @@ -7,6 +7,7 @@ import { EIP1193UserRejectedRequestError, type Msgs, type ProviderMsgsFromApp, + WalletCapability, requestPayloadIsHappyMethod, } from "@happy.tech/wallet-common" import { decodeNonce } from "permissionless" @@ -17,6 +18,7 @@ import { InvalidAddressError, type Transaction, type TransactionReceipt, + type WalletCapabilities, hexToBigInt, isAddress, parseSignature, @@ -269,6 +271,33 @@ export async function dispatchHandlers(request: ProviderMsgsFromApp[Msgs.Request // The app may have bypassed the permission check, but this doesn't do anything. return null + // EIP5792 methdos + case "wallet_getCapabilities": { + // This method SHOULD return an error if the user has not + // already authorized a connection between the application and + // the requested address. + checkAuthenticated() + const queryAddress = request.payload.params?.[0] + if (!queryAddress) { + throw new Error("Missing address parameter") + } + + const currentChainId = getCurrentChain().chainId + + const capabilities: WalletCapabilities = { + [currentChainId]: Object.fromEntries( + Object.values(WalletCapability).map((capability) => [capability, { supported: true }]), + ), + } + + // c.f. https://www.eip5792.xyz/reference/getCapabilities#returns + return capabilities + } + + case "wallet_getCallsStatus": { + return true + } + case HappyMethodNames.REQUEST_SESSION_KEY: { const user = getUser() const targetContractAddress = request.payload.params[0] as Address diff --git a/apps/iframe/src/routes/request.lazy.tsx b/apps/iframe/src/routes/request.lazy.tsx index a1556023a6..7aecf5c9df 100644 --- a/apps/iframe/src/routes/request.lazy.tsx +++ b/apps/iframe/src/routes/request.lazy.tsx @@ -4,6 +4,7 @@ import { createLazyFileRoute } from "@tanstack/react-router" import { useCallback, useEffect, useState } from "react" import { HappyRequestSessionKey } from "#src/components/requests/HappyRequestSessionKey.js" import { HappyUseAbi } from "#src/components/requests/HappyUseAbi" +import { WalletSendCalls } from "#src/components/requests/WalletSendCalls" import { DotLinearWaveLoader } from "../components/loaders/DotLinearWaveLoader" import { EthRequestAccounts } from "../components/requests/EthRequestAccounts" import { EthSendTransaction } from "../components/requests/EthSendTransaction" @@ -136,6 +137,8 @@ function Request() { return case HappyMethodNames.REQUEST_SESSION_KEY: return + case "wallet_sendCalls": + return default: return (
diff --git a/apps/iframe/src/state/walletClient.ts b/apps/iframe/src/state/walletClient.ts index 4a5117bc3f..3998d31861 100644 --- a/apps/iframe/src/state/walletClient.ts +++ b/apps/iframe/src/state/walletClient.ts @@ -6,6 +6,7 @@ import { providerAtom } from "./provider" import { transportAtom } from "./transport" import { userAtom } from "./user" +// TODO extend with eip5792Actions() ? export type AccountWalletClient = WalletClient< CustomTransport, undefined, diff --git a/support/wallet-common/lib/index.ts b/support/wallet-common/lib/index.ts index 7954801407..7cd05201aa 100644 --- a/support/wallet-common/lib/index.ts +++ b/support/wallet-common/lib/index.ts @@ -42,6 +42,8 @@ export type { } from "./interfaces/eip1193" export type { EIP6963ProviderInfo, EIP6963ProviderDetail, EIP6963AnnounceProviderEvent } from "./interfaces/eip6963" +export { WalletCapability } from "./interfaces/eip5792" + export type { MsgsFromApp, MsgsFromIframe, diff --git a/support/wallet-common/lib/interfaces/eip5792.ts b/support/wallet-common/lib/interfaces/eip5792.ts new file mode 100644 index 0000000000..39686cf136 --- /dev/null +++ b/support/wallet-common/lib/interfaces/eip5792.ts @@ -0,0 +1,6 @@ +export enum WalletCapability { + AtomicBatch = "atomicBatch", + PaymasterService = "paymasterService", + AuxiliaryFunds = "auxiliaryFunds", + OnchainPaymaster = "onchainPaymaster", +} diff --git a/support/wallet-common/lib/interfaces/permissions.ts b/support/wallet-common/lib/interfaces/permissions.ts index 0047327fd8..8455074d87 100644 --- a/support/wallet-common/lib/interfaces/permissions.ts +++ b/support/wallet-common/lib/interfaces/permissions.ts @@ -66,6 +66,8 @@ const safeList = new Set([ "wallet_revokePermissions", // https://github.com/MetaMask/metamask-improvement-proposals/blob/main/MIPs/mip-2.md "web3_clientVersion", "web3_sha3", + // eip-5792 + "wallet_getCapabilities", ]) /** @@ -116,6 +118,9 @@ const unsafeList = new Set([ // cryptography "eth_decrypt", "eth_getEncryptionPublicKey", + // eip-5792: tx batching + "wallet_sendCalls", + "wallet_showCallsStatus", // shows pretty info popup ]) /** From 180a6157c139d999c90ebcee5c0e943bacf9724b Mon Sep 17 00:00:00 2001 From: ultraviolet10 Date: Sun, 16 Feb 2025 14:14:41 +0530 Subject: [PATCH 2/9] feat: sendCalls request handler --- apps/iframe/src/requests/approved.ts | 43 ++++++++++++++++--- apps/iframe/src/requests/permissionless.ts | 2 +- .../wallet-common/lib/interfaces/eip5792.ts | 7 ++- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/apps/iframe/src/requests/approved.ts b/apps/iframe/src/requests/approved.ts index 146b9b5259..26fd21cbc5 100644 --- a/apps/iframe/src/requests/approved.ts +++ b/apps/iframe/src/requests/approved.ts @@ -8,10 +8,11 @@ import { EIP1193UnsupportedMethodError, type Msgs, type PopupMsgs, + WalletCapability, getEIP1193ErrorObjectFromCode, requestPayloadIsHappyMethod, } from "@happy.tech/wallet-common" -import { type Client, InvalidAddressError, isAddress } from "viem" +import { type Client, type Hex, InvalidAddressError, isAddress } from "viem" import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" import { checkIsSessionKeyModuleInstalled, @@ -179,20 +180,48 @@ export async function dispatchHandlers(request: PopupMsgs[Msgs.PopupApprove]) { // EIP-5792 case "wallet_sendCalls": { - // TODO implementation pending + if (!user) throw new EIP1193UnauthorizedError() const callsData = request.payload.params?.[0] + if (!callsData) throw new Error() - if (callsData) { + if (user.controllingAddress !== callsData.from) { + // MAY reject the request if the from address does not match the enabled account + throw new Error() + } + // validate that no unsupported capability is sent through - this should go into the docs + const allowedCapabilities = new Set([WalletCapability.BoopPaymaster]) + if (callsData.capabilities) { + for (const capability of Object.keys(callsData.capabilities)) { + if (!allowedCapabilities.has(capability as WalletCapability)) { + throw new EIP1193UnsupportedMethodError() + } + } + } + + let lastUserOpHash: Hex | null = null + + for (const call of callsData.calls) { + const { to, value, data, chainId } = call // Remove chainId + + if (chainId !== getCurrentChain().chainId) throw new Error("Invalid chainId detected.") + + if (!to) throw new Error("Missing 'to' address in transaction call") + + lastUserOpHash = await sendUserOp({ + user, + tx: { to, value, data }, + validator: contractAddresses.ECDSAValidator, + signer: async (userOp, smartAccountClient) => + await smartAccountClient.account.signUserOperation(userOp), + }) } - return 1 + return lastUserOpHash } case "wallet_showCallsStatus": { - const _bundleIdentifier = request.payload.params?.[0] + const _boopBundleId = request.payload.params?.[0] // TODO pretty popup - - // c.f. https://github.com/wevm/viem/blob/66e5f6ab7b683a90775dcb8fae340e3154d74b38/src/experimental/eip5792/actions/showCallsStatus.ts#L10 return undefined } diff --git a/apps/iframe/src/requests/permissionless.ts b/apps/iframe/src/requests/permissionless.ts index 396ed0c6a7..7f6e7461bb 100644 --- a/apps/iframe/src/requests/permissionless.ts +++ b/apps/iframe/src/requests/permissionless.ts @@ -271,7 +271,7 @@ export async function dispatchHandlers(request: ProviderMsgsFromApp[Msgs.Request // The app may have bypassed the permission check, but this doesn't do anything. return null - // EIP5792 methdos + // EIP-5792 case "wallet_getCapabilities": { // This method SHOULD return an error if the user has not // already authorized a connection between the application and diff --git a/support/wallet-common/lib/interfaces/eip5792.ts b/support/wallet-common/lib/interfaces/eip5792.ts index 39686cf136..9a279b25ba 100644 --- a/support/wallet-common/lib/interfaces/eip5792.ts +++ b/support/wallet-common/lib/interfaces/eip5792.ts @@ -1,6 +1,5 @@ export enum WalletCapability { - AtomicBatch = "atomicBatch", - PaymasterService = "paymasterService", - AuxiliaryFunds = "auxiliaryFunds", - OnchainPaymaster = "onchainPaymaster", + // TODO rename to HappyWalletCapability + // AtomicBatch = "atomicBatch", // coming soon! + BoopPaymaster = "boopPaymaster", } From c0eb36eb8ea1169179414c01308419b5159ffa95 Mon Sep 17 00:00:00 2001 From: ultraviolet10 Date: Sun, 16 Feb 2025 16:00:22 +0530 Subject: [PATCH 3/9] feat: getCallsStatus request handler [skip ci] --- apps/iframe/src/requests/approved.ts | 13 ++++++---- .../requests/modules/boop-batcher/helpers.ts | 25 +++++++++++++++++++ apps/iframe/src/requests/permissionless.ts | 17 ++++++++++++- apps/iframe/src/state/injectedClient.ts | 3 ++- apps/iframe/src/state/walletClient.ts | 7 +++--- 5 files changed, 55 insertions(+), 10 deletions(-) create mode 100644 apps/iframe/src/requests/modules/boop-batcher/helpers.ts diff --git a/apps/iframe/src/requests/approved.ts b/apps/iframe/src/requests/approved.ts index 26fd21cbc5..1c5a978e3e 100644 --- a/apps/iframe/src/requests/approved.ts +++ b/apps/iframe/src/requests/approved.ts @@ -12,7 +12,7 @@ import { getEIP1193ErrorObjectFromCode, requestPayloadIsHappyMethod, } from "@happy.tech/wallet-common" -import { type Client, type Hex, InvalidAddressError, isAddress } from "viem" +import { type Client, type Hex, InternalRpcError, InvalidAddressError, isAddress } from "viem" import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" import { checkIsSessionKeyModuleInstalled, @@ -182,18 +182,18 @@ export async function dispatchHandlers(request: PopupMsgs[Msgs.PopupApprove]) { case "wallet_sendCalls": { if (!user) throw new EIP1193UnauthorizedError() const callsData = request.payload.params?.[0] - if (!callsData) throw new Error() + if (!callsData) throw new InternalRpcError(new Error("Invalid request payload")) if (user.controllingAddress !== callsData.from) { // MAY reject the request if the from address does not match the enabled account - throw new Error() + throw new InternalRpcError(new Error("Sender address does not match enabled account")) } // validate that no unsupported capability is sent through - this should go into the docs const allowedCapabilities = new Set([WalletCapability.BoopPaymaster]) if (callsData.capabilities) { for (const capability of Object.keys(callsData.capabilities)) { if (!allowedCapabilities.has(capability as WalletCapability)) { - throw new EIP1193UnsupportedMethodError() + throw new InternalRpcError(new Error("Invalid capability")) } } } @@ -203,7 +203,8 @@ export async function dispatchHandlers(request: PopupMsgs[Msgs.PopupApprove]) { for (const call of callsData.calls) { const { to, value, data, chainId } = call // Remove chainId - if (chainId !== getCurrentChain().chainId) throw new Error("Invalid chainId detected.") + if (chainId !== getCurrentChain().chainId) + throw new InternalRpcError(new Error("Invalid chainId detected")) if (!to) throw new Error("Missing 'to' address in transaction call") @@ -220,8 +221,10 @@ export async function dispatchHandlers(request: PopupMsgs[Msgs.PopupApprove]) { } case "wallet_showCallsStatus": { + // this will come from the popup const _boopBundleId = request.payload.params?.[0] // TODO pretty popup + // call wallet_getCallsStatus return undefined } diff --git a/apps/iframe/src/requests/modules/boop-batcher/helpers.ts b/apps/iframe/src/requests/modules/boop-batcher/helpers.ts new file mode 100644 index 0000000000..6114c2c787 --- /dev/null +++ b/apps/iframe/src/requests/modules/boop-batcher/helpers.ts @@ -0,0 +1,25 @@ +import type { GetTransactionReceiptReturnType, Hex, WalletGetCallsStatusReturnType } from "viem" + +export function convertUserOpReceiptToCallStatus( + receipts: GetTransactionReceiptReturnType[] | null, +): WalletGetCallsStatusReturnType { + if (!receipts || receipts.length === 0) { + return { status: "PENDING" } + } + + return { + status: "CONFIRMED", + receipts: receipts.map((receipt) => ({ + logs: receipt.logs.map((log) => ({ + address: log.address as Hex, + data: log.data as Hex, + topics: log.topics as Hex[], + })), + status: (receipt.status === "success" ? "0x1" : "0x0") as Hex, + blockHash: receipt.blockHash as Hex, + blockNumber: `0x${receipt.blockNumber.toString(16)}` as Hex, + gasUsed: `0x${receipt.gasUsed.toString(16)}` as Hex, + transactionHash: receipt.transactionHash as Hex, + })), + } +} diff --git a/apps/iframe/src/requests/permissionless.ts b/apps/iframe/src/requests/permissionless.ts index 7f6e7461bb..d367de02a8 100644 --- a/apps/iframe/src/requests/permissionless.ts +++ b/apps/iframe/src/requests/permissionless.ts @@ -15,6 +15,7 @@ import { type Address, type Client, type Hash, + InternalRpcError, InvalidAddressError, type Transaction, type TransactionReceipt, @@ -41,6 +42,7 @@ import { getUser } from "#src/state/user" import { getWalletClient } from "#src/state/walletClient" import type { AppURL } from "#src/utils/appURL" import { checkIfRequestRequiresConfirmation } from "#src/utils/checkIfRequestRequiresConfirmation" +import { convertUserOpReceiptToCallStatus } from "./modules/boop-batcher/helpers" import { sendResponse } from "./sendResponse" import { appForSourceID, checkAuthenticated } from "./utils" @@ -294,8 +296,21 @@ export async function dispatchHandlers(request: ProviderMsgsFromApp[Msgs.Request return capabilities } + // this method only returns a subset of the fields that eth_getTransactionReceipt returns case "wallet_getCallsStatus": { - return true + // TODO if the batch was atomic, this handler MUST return only a single receipt + try { + const [hash] = request.payload.params as [Hash] + if (!hash) { + throw new InternalRpcError(new Error("Transaction hash is missing.")) + } + const transactionReceipt = await getPublicClient()!.getTransactionReceipt({ hash }) + + return convertUserOpReceiptToCallStatus(transactionReceipt ? [transactionReceipt] : null) + } catch (error) { + console.error(error) + throw error + } } case HappyMethodNames.REQUEST_SESSION_KEY: { diff --git a/apps/iframe/src/state/injectedClient.ts b/apps/iframe/src/state/injectedClient.ts index d1048fb79d..12d57f4ef2 100644 --- a/apps/iframe/src/state/injectedClient.ts +++ b/apps/iframe/src/state/injectedClient.ts @@ -1,6 +1,7 @@ import { accessorsFromAtom } from "@happy.tech/common" import { type Atom, atom } from "jotai" import { createWalletClient, custom } from "viem" +import { eip5792Actions } from "viem/experimental" import { InjectedProviderProxy } from "#src/connections/InjectedProviderProxy.ts" import { userAtom } from "./user" import type { AccountWalletClient } from "./walletClient" @@ -10,7 +11,7 @@ export const injectedClientAtom: Atom = atom, [...WalletRpcSchema, ...PublicRpcSchema] -> +> & + Eip5792Actions export const walletClientAtom: Atom = atom((get) => { const user = get(userAtom) @@ -20,7 +21,7 @@ export const walletClientAtom: Atom = atom Date: Wed, 19 Feb 2025 16:08:38 +0530 Subject: [PATCH 4/9] demo: add client calls to demo for requests --- apps/iframe/src/requests/approved.ts | 28 ++++++++++--------- apps/iframe/src/requests/permissionless.ts | 4 +-- apps/iframe/src/requests/userOps.ts | 15 +++------- support/wallet-common/lib/index.ts | 2 +- .../wallet-common/lib/interfaces/eip5792.ts | 3 +- .../lib/interfaces/permissions.ts | 1 + 6 files changed, 24 insertions(+), 29 deletions(-) diff --git a/apps/iframe/src/requests/approved.ts b/apps/iframe/src/requests/approved.ts index 1c5a978e3e..e1806d2900 100644 --- a/apps/iframe/src/requests/approved.ts +++ b/apps/iframe/src/requests/approved.ts @@ -6,13 +6,13 @@ import { type EIP1193RequestResult, EIP1193UnauthorizedError, EIP1193UnsupportedMethodError, + HappyWalletCapability, type Msgs, type PopupMsgs, - WalletCapability, getEIP1193ErrorObjectFromCode, requestPayloadIsHappyMethod, } from "@happy.tech/wallet-common" -import { type Client, type Hex, InternalRpcError, InvalidAddressError, isAddress } from "viem" +import { type Address, type Client, type Hex, InternalRpcError, InvalidAddressError, isAddress } from "viem" import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" import { checkIsSessionKeyModuleInstalled, @@ -181,50 +181,52 @@ export async function dispatchHandlers(request: PopupMsgs[Msgs.PopupApprove]) { // EIP-5792 case "wallet_sendCalls": { if (!user) throw new EIP1193UnauthorizedError() + const callsData = request.payload.params?.[0] if (!callsData) throw new InternalRpcError(new Error("Invalid request payload")) - if (user.controllingAddress !== callsData.from) { + if (user.address !== callsData.from) { // MAY reject the request if the from address does not match the enabled account throw new InternalRpcError(new Error("Sender address does not match enabled account")) } - // validate that no unsupported capability is sent through - this should go into the docs - const allowedCapabilities = new Set([WalletCapability.BoopPaymaster]) + + // validate that no unsupported capability is sent through + const allowedCapabilities = new Set([HappyWalletCapability.BoopPaymaster]) if (callsData.capabilities) { for (const capability of Object.keys(callsData.capabilities)) { - if (!allowedCapabilities.has(capability as WalletCapability)) { + if (!allowedCapabilities.has(capability as HappyWalletCapability)) { throw new InternalRpcError(new Error("Invalid capability")) } } } - let lastUserOpHash: Hex | null = null + // extract specified paymaster address from capabilities + const boopPaymasterAddress: Address | undefined = callsData.capabilities?.boopPaymaster?.address + let userOpHash: Hex | null = null for (const call of callsData.calls) { - const { to, value, data, chainId } = call // Remove chainId + const { to, value, data, chainId } = call if (chainId !== getCurrentChain().chainId) throw new InternalRpcError(new Error("Invalid chainId detected")) if (!to) throw new Error("Missing 'to' address in transaction call") - lastUserOpHash = await sendUserOp({ + userOpHash = await sendUserOp({ user, tx: { to, value, data }, validator: contractAddresses.ECDSAValidator, + paymaster: boopPaymasterAddress, signer: async (userOp, smartAccountClient) => await smartAccountClient.account.signUserOperation(userOp), }) } - return lastUserOpHash + return userOpHash } case "wallet_showCallsStatus": { - // this will come from the popup const _boopBundleId = request.payload.params?.[0] - // TODO pretty popup - // call wallet_getCallsStatus return undefined } diff --git a/apps/iframe/src/requests/permissionless.ts b/apps/iframe/src/requests/permissionless.ts index d367de02a8..ca3657fd84 100644 --- a/apps/iframe/src/requests/permissionless.ts +++ b/apps/iframe/src/requests/permissionless.ts @@ -5,9 +5,9 @@ import { EIP1193UnauthorizedError, EIP1193UnsupportedMethodError, EIP1193UserRejectedRequestError, + HappyWalletCapability, type Msgs, type ProviderMsgsFromApp, - WalletCapability, requestPayloadIsHappyMethod, } from "@happy.tech/wallet-common" import { decodeNonce } from "permissionless" @@ -288,7 +288,7 @@ export async function dispatchHandlers(request: ProviderMsgsFromApp[Msgs.Request const capabilities: WalletCapabilities = { [currentChainId]: Object.fromEntries( - Object.values(WalletCapability).map((capability) => [capability, { supported: true }]), + Object.values(HappyWalletCapability).map((capability) => [capability, { supported: true }]), ), } diff --git a/apps/iframe/src/requests/userOps.ts b/apps/iframe/src/requests/userOps.ts index 31bc150e07..e5e7761e60 100644 --- a/apps/iframe/src/requests/userOps.ts +++ b/apps/iframe/src/requests/userOps.ts @@ -38,6 +38,7 @@ export type SendUserOpArgs = { tx: RpcTransactionRequest validator: Address signer: UserOpSigner + paymaster?: Address // developers may specify a particular deployed paymaster when they use `wallet_sendCalls` } export type UserOpWrappedCall = { @@ -57,11 +58,10 @@ export enum VALIDATOR_TYPE { PERMISSION = "0x02", } -export async function sendUserOp({ user, tx, validator, signer }: SendUserOpArgs, retry = 2) { +export async function sendUserOp({ user, tx, validator, signer, paymaster }: SendUserOpArgs, retry = 2) { const smartAccountClient = (await getSmartAccountClient())! const account = smartAccountClient.account.address - // [DEBUGLOG] // let debugNonce = 0n try { // We need the separate nonce lookup because: // - we do local nonce management to be able to have multiple userOps in flight @@ -70,7 +70,7 @@ export async function sendUserOp({ user, tx, validator, signer }: SendUserOpArgs getNextNonce(account, validator), smartAccountClient.prepareUserOperation({ account: smartAccountClient.account, - paymaster: contractAddresses.HappyPaymaster, + paymaster: paymaster ?? contractAddresses.HappyPaymaster, // Specify this array to avoid fetching the nonce from here too. // We don't really need the dummy signature, but it does not incur an extra network // call and it makes the type system happy. @@ -85,8 +85,6 @@ export async function sendUserOp({ user, tx, validator, signer }: SendUserOpArgs } satisfies PrepareUserOperationParameters), // TS too dumb without this ]) - // [DEBUGLOG] // debugNonce = nonce - // sendUserOperationNow does not want account included const { account: _, ...preparedUserOp } = { ..._preparedUserOp, nonce } preparedUserOp.signature = await signer(preparedUserOp, smartAccountClient) @@ -105,7 +103,6 @@ export async function sendUserOp({ user, tx, validator, signer }: SendUserOpArgs addPendingUserOp(user.address, pendingUserOpDetails) - // [DEBUGLOG] // console.log("sending", userOpHash, retry) const userOpReceipt = await submitUserOp(smartAccountClient, validator, preparedUserOp) receiptCache.put(userOpHash, [ @@ -119,8 +116,6 @@ export async function sendUserOp({ user, tx, validator, signer }: SendUserOpArgs } as GetUserOperationReturnType, ]) - // [DEBUGLOG] // console.log("receipt", userOpHash, retry) - markUserOpAsConfirmed(user.address, pendingUserOpDetails, userOpReceipt) return userOpReceipt.userOpHash @@ -128,9 +123,7 @@ export async function sendUserOp({ user, tx, validator, signer }: SendUserOpArgs // https://docs.stackup.sh/docs/entrypoint-errors // https://docs.pimlico.io/infra/bundler/entrypoint-errors - // [DEBUGLOG] // console.log("error", nonceB, error.details || error, retry) - - // Most likely the transaction didn't land, so need to resynchronize the nonce. + // (most likely) the transaction didn't land, so need to resynchronize the nonce. deleteNonce(account, validator) if (retry > 0) return sendUserOp({ user, tx, validator, signer }, retry - 1) diff --git a/support/wallet-common/lib/index.ts b/support/wallet-common/lib/index.ts index 7cd05201aa..64dd597383 100644 --- a/support/wallet-common/lib/index.ts +++ b/support/wallet-common/lib/index.ts @@ -42,7 +42,7 @@ export type { } from "./interfaces/eip1193" export type { EIP6963ProviderInfo, EIP6963ProviderDetail, EIP6963AnnounceProviderEvent } from "./interfaces/eip6963" -export { WalletCapability } from "./interfaces/eip5792" +export { HappyWalletCapability } from "./interfaces/eip5792" export type { MsgsFromApp, diff --git a/support/wallet-common/lib/interfaces/eip5792.ts b/support/wallet-common/lib/interfaces/eip5792.ts index 9a279b25ba..452ca89cd4 100644 --- a/support/wallet-common/lib/interfaces/eip5792.ts +++ b/support/wallet-common/lib/interfaces/eip5792.ts @@ -1,5 +1,4 @@ -export enum WalletCapability { - // TODO rename to HappyWalletCapability +export enum HappyWalletCapability { // AtomicBatch = "atomicBatch", // coming soon! BoopPaymaster = "boopPaymaster", } diff --git a/support/wallet-common/lib/interfaces/permissions.ts b/support/wallet-common/lib/interfaces/permissions.ts index 8455074d87..3b4c279111 100644 --- a/support/wallet-common/lib/interfaces/permissions.ts +++ b/support/wallet-common/lib/interfaces/permissions.ts @@ -68,6 +68,7 @@ const safeList = new Set([ "web3_sha3", // eip-5792 "wallet_getCapabilities", + "wallet_getCallsStatus", ]) /** From fa36a91f9cc6b95b8015fb03aa53787d0bb4deb5 Mon Sep 17 00:00:00 2001 From: ultraviolet10 Date: Wed, 19 Feb 2025 17:22:42 +0530 Subject: [PATCH 5/9] fix: improve wallet_getCallsStatus handler for user operations [skip ci] --- .../requests/modules/boop-batcher/helpers.ts | 18 +++++++++++++----- apps/iframe/src/requests/permissionless.ts | 10 +++++++--- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/apps/iframe/src/requests/modules/boop-batcher/helpers.ts b/apps/iframe/src/requests/modules/boop-batcher/helpers.ts index 6114c2c787..8ad5a98f8e 100644 --- a/apps/iframe/src/requests/modules/boop-batcher/helpers.ts +++ b/apps/iframe/src/requests/modules/boop-batcher/helpers.ts @@ -1,7 +1,15 @@ -import type { GetTransactionReceiptReturnType, Hex, WalletGetCallsStatusReturnType } from "viem" +import type { Hex, Log, WalletGetCallsStatusReturnType } from "viem" +import type { UserOperationReceipt } from "viem/account-abstraction" +/** + * Converts an array of user operation receipts into the {@link WalletGetCallsStatusReturnType} format. + * If no receipts are provided, returns a `"PENDING"` status. + * + * @param receipts - Array of UserOperationReceipt objects or null. + * @returns WalletGetCallsStatusReturnType with formatted receipt data. + */ export function convertUserOpReceiptToCallStatus( - receipts: GetTransactionReceiptReturnType[] | null, + receipts: UserOperationReceipt[] | null, ): WalletGetCallsStatusReturnType { if (!receipts || receipts.length === 0) { return { status: "PENDING" } @@ -9,13 +17,13 @@ export function convertUserOpReceiptToCallStatus( return { status: "CONFIRMED", - receipts: receipts.map((receipt) => ({ - logs: receipt.logs.map((log) => ({ + receipts: receipts.map(({ receipt, logs, success }) => ({ + logs: logs.map((log: Log) => ({ address: log.address as Hex, data: log.data as Hex, topics: log.topics as Hex[], })), - status: (receipt.status === "success" ? "0x1" : "0x0") as Hex, + status: (success ? "0x1" : "0x0") as Hex, blockHash: receipt.blockHash as Hex, blockNumber: `0x${receipt.blockNumber.toString(16)}` as Hex, gasUsed: `0x${receipt.gasUsed.toString(16)}` as Hex, diff --git a/apps/iframe/src/requests/permissionless.ts b/apps/iframe/src/requests/permissionless.ts index ca3657fd84..fb468c9220 100644 --- a/apps/iframe/src/requests/permissionless.ts +++ b/apps/iframe/src/requests/permissionless.ts @@ -300,13 +300,17 @@ export async function dispatchHandlers(request: ProviderMsgsFromApp[Msgs.Request case "wallet_getCallsStatus": { // TODO if the batch was atomic, this handler MUST return only a single receipt try { - const [hash] = request.payload.params as [Hash] + const [hash] = request.payload.params as Hash[] if (!hash) { throw new InternalRpcError(new Error("Transaction hash is missing.")) } - const transactionReceipt = await getPublicClient()!.getTransactionReceipt({ hash }) + const smartAccountClient = (await getSmartAccountClient()) as ExtendedSmartAccountClient - return convertUserOpReceiptToCallStatus(transactionReceipt ? [transactionReceipt] : null) + const userOpReceipt = await smartAccountClient.waitForUserOperationReceipt({ + hash: hash, + }) + + return convertUserOpReceiptToCallStatus(userOpReceipt ? [userOpReceipt] : null) } catch (error) { console.error(error) throw error From a62cda6e5520657c8c06c05fd6912df843e55526 Mon Sep 17 00:00:00 2001 From: ultraviolet10 Date: Wed, 19 Feb 2025 17:50:02 +0530 Subject: [PATCH 6/9] refactor: misc [skip ci] --- support/wallet-common/lib/interfaces/permissions.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/support/wallet-common/lib/interfaces/permissions.ts b/support/wallet-common/lib/interfaces/permissions.ts index 3b4c279111..f9d3a96252 100644 --- a/support/wallet-common/lib/interfaces/permissions.ts +++ b/support/wallet-common/lib/interfaces/permissions.ts @@ -66,7 +66,6 @@ const safeList = new Set([ "wallet_revokePermissions", // https://github.com/MetaMask/metamask-improvement-proposals/blob/main/MIPs/mip-2.md "web3_clientVersion", "web3_sha3", - // eip-5792 "wallet_getCapabilities", "wallet_getCallsStatus", ]) @@ -119,9 +118,8 @@ const unsafeList = new Set([ // cryptography "eth_decrypt", "eth_getEncryptionPublicKey", - // eip-5792: tx batching "wallet_sendCalls", - "wallet_showCallsStatus", // shows pretty info popup + "wallet_showCallsStatus", ]) /** From 253c9a64b1d618a89b4dc0d1fc2f288a39366756 Mon Sep 17 00:00:00 2001 From: ultraviolet10 Date: Mon, 3 Mar 2025 19:15:01 +0530 Subject: [PATCH 7/9] misc fixes --- apps/iframe/src/constants/requestLabels.ts | 2 +- apps/iframe/src/requests/approved.ts | 1 - demos/react/src/useClients.ts | 5 +++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/iframe/src/constants/requestLabels.ts b/apps/iframe/src/constants/requestLabels.ts index b258b866b3..de5c25d4f5 100644 --- a/apps/iframe/src/constants/requestLabels.ts +++ b/apps/iframe/src/constants/requestLabels.ts @@ -6,11 +6,11 @@ export const requestLabels = { personal_sign: "Signature Request", wallet_addEthereumChain: "Add Network", wallet_requestPermissions: "Permission Request", + wallet_sendCalls: "Send Calls", wallet_switchEthereumChain: "Switch Network", wallet_watchAsset: "Watch Asset", [HappyMethodNames.USE_ABI]: "Record ABI", [HappyMethodNames.REQUEST_SESSION_KEY]: "Approve Session Key", - wallet_sendCalls: "Send Calls", } as const export const permissionDescriptions = { diff --git a/apps/iframe/src/requests/approved.ts b/apps/iframe/src/requests/approved.ts index e1806d2900..bda0bd71f1 100644 --- a/apps/iframe/src/requests/approved.ts +++ b/apps/iframe/src/requests/approved.ts @@ -226,7 +226,6 @@ export async function dispatchHandlers(request: PopupMsgs[Msgs.PopupApprove]) { } case "wallet_showCallsStatus": { - const _boopBundleId = request.payload.params?.[0] return undefined } diff --git a/demos/react/src/useClients.ts b/demos/react/src/useClients.ts index 02f3876ce0..ce57b9ec12 100644 --- a/demos/react/src/useClients.ts +++ b/demos/react/src/useClients.ts @@ -9,13 +9,14 @@ import { createWalletClient, custom, } from "viem" +import { eip5792Actions, type Eip5792Actions } from "viem/experimental" /** * Creates custom public + wallet clients using the HappyProvider. */ export default function useClients(): { publicClient: PublicClient - walletClient: WalletClient | null + walletClient: WalletClient & Eip5792Actions | null } { const { provider, user } = useHappyChain() @@ -27,7 +28,7 @@ export default function useClients(): { ? createWalletClient({ account: user.address, transport: custom(provider), - }) + }).extend(eip5792Actions()) : null, [user, provider], ) From 6050284eef9e9ed073d23aadb0973a5fc05c4dee Mon Sep 17 00:00:00 2001 From: ultraviolet10 Date: Thu, 6 Mar 2025 19:57:29 +0530 Subject: [PATCH 8/9] fix: extend type for wallet client --- demos/react/src/useClients.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demos/react/src/useClients.ts b/demos/react/src/useClients.ts index ce57b9ec12..2376ec20ed 100644 --- a/demos/react/src/useClients.ts +++ b/demos/react/src/useClients.ts @@ -9,14 +9,14 @@ import { createWalletClient, custom, } from "viem" -import { eip5792Actions, type Eip5792Actions } from "viem/experimental" +import { type Eip5792Actions, eip5792Actions } from "viem/experimental" /** * Creates custom public + wallet clients using the HappyProvider. */ export default function useClients(): { publicClient: PublicClient - walletClient: WalletClient & Eip5792Actions | null + walletClient: (WalletClient & Eip5792Actions) | null } { const { provider, user } = useHappyChain() From 9b310f16f5bc407fc2b90eee97ad452427ac7793 Mon Sep 17 00:00:00 2001 From: ultraviolet10 Date: Thu, 6 Mar 2025 20:08:39 +0530 Subject: [PATCH 9/9] pr cleanup for chunks --- .../components/requests/WalletSendCalls.tsx | 37 ------------- apps/iframe/src/requests/approved.ts | 54 +------------------ .../requests/modules/boop-batcher/helpers.ts | 33 ------------ apps/iframe/src/requests/permissionless.ts | 48 ----------------- apps/iframe/src/routes/request.lazy.tsx | 3 -- .../lib/interfaces/permissions.ts | 4 -- 6 files changed, 1 insertion(+), 178 deletions(-) delete mode 100644 apps/iframe/src/components/requests/WalletSendCalls.tsx delete mode 100644 apps/iframe/src/requests/modules/boop-batcher/helpers.ts diff --git a/apps/iframe/src/components/requests/WalletSendCalls.tsx b/apps/iframe/src/components/requests/WalletSendCalls.tsx deleted file mode 100644 index 2a01f2e319..0000000000 --- a/apps/iframe/src/components/requests/WalletSendCalls.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Button } from "../primitives/button/Button" -import RawRequestDetails from "./common/RawRequestDetails" -import RequestContent from "./common/RequestContent" -import RequestLayout from "./common/RequestLayout" -import type { RequestConfirmationProps } from "./props" - -export const WalletSendCalls = ({ method, params, reject, accept }: RequestConfirmationProps<"wallet_sendCalls">) => { - console.log("param", { method, params }) - // TODO only for testiog - return ( - - -
-
- Calls - {/*
{formattedSignPayload}
*/} -
- - -
-
- -
- - -
-
- ) -} diff --git a/apps/iframe/src/requests/approved.ts b/apps/iframe/src/requests/approved.ts index bda0bd71f1..f1090cdb22 100644 --- a/apps/iframe/src/requests/approved.ts +++ b/apps/iframe/src/requests/approved.ts @@ -6,13 +6,12 @@ import { type EIP1193RequestResult, EIP1193UnauthorizedError, EIP1193UnsupportedMethodError, - HappyWalletCapability, type Msgs, type PopupMsgs, getEIP1193ErrorObjectFromCode, requestPayloadIsHappyMethod, } from "@happy.tech/wallet-common" -import { type Address, type Client, type Hex, InternalRpcError, InvalidAddressError, isAddress } from "viem" +import { type Client, InvalidAddressError, isAddress } from "viem" import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" import { checkIsSessionKeyModuleInstalled, @@ -178,57 +177,6 @@ export async function dispatchHandlers(request: PopupMsgs[Msgs.PopupApprove]) { return accountSessionKey.address } - // EIP-5792 - case "wallet_sendCalls": { - if (!user) throw new EIP1193UnauthorizedError() - - const callsData = request.payload.params?.[0] - if (!callsData) throw new InternalRpcError(new Error("Invalid request payload")) - - if (user.address !== callsData.from) { - // MAY reject the request if the from address does not match the enabled account - throw new InternalRpcError(new Error("Sender address does not match enabled account")) - } - - // validate that no unsupported capability is sent through - const allowedCapabilities = new Set([HappyWalletCapability.BoopPaymaster]) - if (callsData.capabilities) { - for (const capability of Object.keys(callsData.capabilities)) { - if (!allowedCapabilities.has(capability as HappyWalletCapability)) { - throw new InternalRpcError(new Error("Invalid capability")) - } - } - } - - // extract specified paymaster address from capabilities - const boopPaymasterAddress: Address | undefined = callsData.capabilities?.boopPaymaster?.address - let userOpHash: Hex | null = null - - for (const call of callsData.calls) { - const { to, value, data, chainId } = call - - if (chainId !== getCurrentChain().chainId) - throw new InternalRpcError(new Error("Invalid chainId detected")) - - if (!to) throw new Error("Missing 'to' address in transaction call") - - userOpHash = await sendUserOp({ - user, - tx: { to, value, data }, - validator: contractAddresses.ECDSAValidator, - paymaster: boopPaymasterAddress, - signer: async (userOp, smartAccountClient) => - await smartAccountClient.account.signUserOperation(userOp), - }) - } - - return userOpHash - } - - case "wallet_showCallsStatus": { - return undefined - } - default: return await sendToWalletClient(request) } diff --git a/apps/iframe/src/requests/modules/boop-batcher/helpers.ts b/apps/iframe/src/requests/modules/boop-batcher/helpers.ts deleted file mode 100644 index 8ad5a98f8e..0000000000 --- a/apps/iframe/src/requests/modules/boop-batcher/helpers.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { Hex, Log, WalletGetCallsStatusReturnType } from "viem" -import type { UserOperationReceipt } from "viem/account-abstraction" - -/** - * Converts an array of user operation receipts into the {@link WalletGetCallsStatusReturnType} format. - * If no receipts are provided, returns a `"PENDING"` status. - * - * @param receipts - Array of UserOperationReceipt objects or null. - * @returns WalletGetCallsStatusReturnType with formatted receipt data. - */ -export function convertUserOpReceiptToCallStatus( - receipts: UserOperationReceipt[] | null, -): WalletGetCallsStatusReturnType { - if (!receipts || receipts.length === 0) { - return { status: "PENDING" } - } - - return { - status: "CONFIRMED", - receipts: receipts.map(({ receipt, logs, success }) => ({ - logs: logs.map((log: Log) => ({ - address: log.address as Hex, - data: log.data as Hex, - topics: log.topics as Hex[], - })), - status: (success ? "0x1" : "0x0") as Hex, - blockHash: receipt.blockHash as Hex, - blockNumber: `0x${receipt.blockNumber.toString(16)}` as Hex, - gasUsed: `0x${receipt.gasUsed.toString(16)}` as Hex, - transactionHash: receipt.transactionHash as Hex, - })), - } -} diff --git a/apps/iframe/src/requests/permissionless.ts b/apps/iframe/src/requests/permissionless.ts index fb468c9220..fd5b3da380 100644 --- a/apps/iframe/src/requests/permissionless.ts +++ b/apps/iframe/src/requests/permissionless.ts @@ -5,7 +5,6 @@ import { EIP1193UnauthorizedError, EIP1193UnsupportedMethodError, EIP1193UserRejectedRequestError, - HappyWalletCapability, type Msgs, type ProviderMsgsFromApp, requestPayloadIsHappyMethod, @@ -15,11 +14,9 @@ import { type Address, type Client, type Hash, - InternalRpcError, InvalidAddressError, type Transaction, type TransactionReceipt, - type WalletCapabilities, hexToBigInt, isAddress, parseSignature, @@ -42,7 +39,6 @@ import { getUser } from "#src/state/user" import { getWalletClient } from "#src/state/walletClient" import type { AppURL } from "#src/utils/appURL" import { checkIfRequestRequiresConfirmation } from "#src/utils/checkIfRequestRequiresConfirmation" -import { convertUserOpReceiptToCallStatus } from "./modules/boop-batcher/helpers" import { sendResponse } from "./sendResponse" import { appForSourceID, checkAuthenticated } from "./utils" @@ -273,50 +269,6 @@ export async function dispatchHandlers(request: ProviderMsgsFromApp[Msgs.Request // The app may have bypassed the permission check, but this doesn't do anything. return null - // EIP-5792 - case "wallet_getCapabilities": { - // This method SHOULD return an error if the user has not - // already authorized a connection between the application and - // the requested address. - checkAuthenticated() - const queryAddress = request.payload.params?.[0] - if (!queryAddress) { - throw new Error("Missing address parameter") - } - - const currentChainId = getCurrentChain().chainId - - const capabilities: WalletCapabilities = { - [currentChainId]: Object.fromEntries( - Object.values(HappyWalletCapability).map((capability) => [capability, { supported: true }]), - ), - } - - // c.f. https://www.eip5792.xyz/reference/getCapabilities#returns - return capabilities - } - - // this method only returns a subset of the fields that eth_getTransactionReceipt returns - case "wallet_getCallsStatus": { - // TODO if the batch was atomic, this handler MUST return only a single receipt - try { - const [hash] = request.payload.params as Hash[] - if (!hash) { - throw new InternalRpcError(new Error("Transaction hash is missing.")) - } - const smartAccountClient = (await getSmartAccountClient()) as ExtendedSmartAccountClient - - const userOpReceipt = await smartAccountClient.waitForUserOperationReceipt({ - hash: hash, - }) - - return convertUserOpReceiptToCallStatus(userOpReceipt ? [userOpReceipt] : null) - } catch (error) { - console.error(error) - throw error - } - } - case HappyMethodNames.REQUEST_SESSION_KEY: { const user = getUser() const targetContractAddress = request.payload.params[0] as Address diff --git a/apps/iframe/src/routes/request.lazy.tsx b/apps/iframe/src/routes/request.lazy.tsx index 7aecf5c9df..a1556023a6 100644 --- a/apps/iframe/src/routes/request.lazy.tsx +++ b/apps/iframe/src/routes/request.lazy.tsx @@ -4,7 +4,6 @@ import { createLazyFileRoute } from "@tanstack/react-router" import { useCallback, useEffect, useState } from "react" import { HappyRequestSessionKey } from "#src/components/requests/HappyRequestSessionKey.js" import { HappyUseAbi } from "#src/components/requests/HappyUseAbi" -import { WalletSendCalls } from "#src/components/requests/WalletSendCalls" import { DotLinearWaveLoader } from "../components/loaders/DotLinearWaveLoader" import { EthRequestAccounts } from "../components/requests/EthRequestAccounts" import { EthSendTransaction } from "../components/requests/EthSendTransaction" @@ -137,8 +136,6 @@ function Request() { return case HappyMethodNames.REQUEST_SESSION_KEY: return - case "wallet_sendCalls": - return default: return (
diff --git a/support/wallet-common/lib/interfaces/permissions.ts b/support/wallet-common/lib/interfaces/permissions.ts index f9d3a96252..0047327fd8 100644 --- a/support/wallet-common/lib/interfaces/permissions.ts +++ b/support/wallet-common/lib/interfaces/permissions.ts @@ -66,8 +66,6 @@ const safeList = new Set([ "wallet_revokePermissions", // https://github.com/MetaMask/metamask-improvement-proposals/blob/main/MIPs/mip-2.md "web3_clientVersion", "web3_sha3", - "wallet_getCapabilities", - "wallet_getCallsStatus", ]) /** @@ -118,8 +116,6 @@ const unsafeList = new Set([ // cryptography "eth_decrypt", "eth_getEncryptionPublicKey", - "wallet_sendCalls", - "wallet_showCallsStatus", ]) /**