From 923fa6413a5d104eb4548082d2d59922937fab0c 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 + .../lib/interfaces/permissions.ts | 5 +++ 7 files changed, 95 insertions(+) create mode 100644 apps/iframe/src/components/requests/WalletSendCalls.tsx 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 de5c25d4f5..26480ed2e8 100644 --- a/apps/iframe/src/constants/requestLabels.ts +++ b/apps/iframe/src/constants/requestLabels.ts @@ -11,6 +11,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 d85172886d..c2180286f5 100644 --- a/apps/iframe/src/state/walletClient.ts +++ b/apps/iframe/src/state/walletClient.ts @@ -7,6 +7,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/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 e614229d5148b2e7b4fc9ce6b97f9bcbc16eff18 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 +- 2 files changed, 37 insertions(+), 8 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 From ae181b189b693f40536c0a6cd92938b9ac63b337 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/walletClient.ts | 1 - 4 files changed, 49 insertions(+), 7 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/walletClient.ts b/apps/iframe/src/state/walletClient.ts index c2180286f5..d85172886d 100644 --- a/apps/iframe/src/state/walletClient.ts +++ b/apps/iframe/src/state/walletClient.ts @@ -7,7 +7,6 @@ import { providerAtom } from "./provider" import { transportAtom } from "./transport" import { userAtom } from "./user" -// TODO extend with eip5792Actions() ? export type AccountWalletClient = WalletClient< CustomTransport, undefined, From 4f7315ff2f407ba4941784f84607505431404bf9 Mon Sep 17 00:00:00 2001 From: ultraviolet10 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 +-- .../lib/interfaces/permissions.ts | 1 + 3 files changed, 18 insertions(+), 15 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/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 4371e86810996fd36e951e890d286d4d53f9aa41 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 4b176e9d81b19df85303eaff24da21d9adb7f4b4 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 d52926fc72ecc9117342dca491af7f8fbdb0ed3d 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 | 1 - apps/iframe/src/requests/approved.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/apps/iframe/src/constants/requestLabels.ts b/apps/iframe/src/constants/requestLabels.ts index 26480ed2e8..de5c25d4f5 100644 --- a/apps/iframe/src/constants/requestLabels.ts +++ b/apps/iframe/src/constants/requestLabels.ts @@ -11,7 +11,6 @@ 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 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 } From e94ae672ee3b9c3db75da3320c095052ce2375c7 Mon Sep 17 00:00:00 2001 From: ultraviolet10 Date: Thu, 6 Mar 2025 20:08:39 +0530 Subject: [PATCH 8/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", ]) /** From 0f8d881ea899e415bba5c65488dfd7de7f90152a Mon Sep 17 00:00:00 2001 From: ultraviolet10 Date: Thu, 6 Mar 2025 20:17:37 +0530 Subject: [PATCH 9/9] feat: add `wallet_getCapabilities` request handler --- apps/iframe/src/requests/permissionless.ts | 24 +++++++++++++++++++ .../lib/interfaces/permissions.ts | 1 + 2 files changed, 25 insertions(+) diff --git a/apps/iframe/src/requests/permissionless.ts b/apps/iframe/src/requests/permissionless.ts index fd5b3da380..6251ba04cd 100644 --- a/apps/iframe/src/requests/permissionless.ts +++ b/apps/iframe/src/requests/permissionless.ts @@ -5,6 +5,7 @@ import { EIP1193UnauthorizedError, EIP1193UnsupportedMethodError, EIP1193UserRejectedRequestError, + HappyWalletCapability, type Msgs, type ProviderMsgsFromApp, requestPayloadIsHappyMethod, @@ -17,6 +18,7 @@ import { InvalidAddressError, type Transaction, type TransactionReceipt, + type WalletCapabilities, hexToBigInt, isAddress, parseSignature, @@ -269,6 +271,28 @@ export async function dispatchHandlers(request: ProviderMsgsFromApp[Msgs.Request // The app may have bypassed the permission check, but this doesn't do anything. return null + 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 + } + case HappyMethodNames.REQUEST_SESSION_KEY: { const user = getUser() const targetContractAddress = request.payload.params[0] as Address diff --git a/support/wallet-common/lib/interfaces/permissions.ts b/support/wallet-common/lib/interfaces/permissions.ts index 0047327fd8..1e210e86db 100644 --- a/support/wallet-common/lib/interfaces/permissions.ts +++ b/support/wallet-common/lib/interfaces/permissions.ts @@ -66,6 +66,7 @@ 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", ]) /**