-
Notifications
You must be signed in to change notification settings - Fork 2
feat: implement wallet_sendCalls request handler
#526
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ultraviolet10
wants to merge
17
commits into
aritra/5792_get_capabilities
Choose a base branch
from
aritra/5792_wallet_sendCalls
base: aritra/5792_get_capabilities
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
e10d0ec
feat: app demo scaffold
ultraviolet10 2c668cb
update viem package, add new component to demo
ultraviolet10 8113f57
clean up demo scaffold
ultraviolet10 b95844d
feat: app demo scaffold
ultraviolet10 1b6727a
clean up demo scaffold
ultraviolet10 574dbf6
feat(iframe): implement wallet_getCapabilities method
ultraviolet10 0a16d82
fix: import reuse + format
ultraviolet10 9d458c5
restack
ultraviolet10 15c1d56
feat(iframe): implement wallet_getCapabilities method
ultraviolet10 cfed326
fix: import reuse + format
ultraviolet10 9467f75
feat: implement wallet_sendCalls request handler
ultraviolet10 f4acede
fix: package updates params type for send calls
ultraviolet10 c499ba8
boop refresh
ultraviolet10 13aa934
add WalletSendCalls component and enhance wallet_sendCalls request ha…
ultraviolet10 1af7203
minimal refactor of request popup
ultraviolet10 72bef8d
refactor: remove bun.lockb and update WalletSendCalls parameter type …
ultraviolet10 9ed7446
minor refactors for restack
ultraviolet10 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| import { formatEther } from "viem" | ||
| import { getAppURL } from "#src/utils/appURL" | ||
| import DisclosureSection from "./common/DisclosureSection" | ||
| import { | ||
| FormattedDetailsLine, | ||
| Layout, | ||
| LinkToAddress, | ||
| SectionBlock, | ||
| SubsectionBlock, | ||
| SubsectionContent, | ||
| SubsectionTitle, | ||
| } from "./common/Layout" | ||
| import type { RequestConfirmationProps } from "./props" | ||
|
|
||
| export function getFirstParam<T>(params: [T, ...unknown[]] | undefined): T | undefined { | ||
| return Array.isArray(params) ? params[0] : undefined | ||
| } | ||
|
|
||
| export const WalletSendCalls = ({ method, params, reject, accept }: RequestConfirmationProps<"wallet_sendCalls">) => { | ||
| const appURL = getAppURL() | ||
| const request = getFirstParam(params) | ||
|
|
||
| if (!request) return null | ||
| const call = request.calls[0] | ||
|
|
||
| const rawValue = call.value ? BigInt(call.value) : 0n | ||
| const formattedValue = rawValue > 0n ? `${formatEther(rawValue)} HAPPY` : null | ||
|
|
||
| return ( | ||
| <Layout | ||
| headline={ | ||
| <> | ||
| <span className="text-primary">{appURL}</span> | ||
| <br /> is requesting to send a transaction | ||
| </> | ||
| } | ||
| description={<>This app wants to execute a transaction on your behalf.</>} | ||
| actions={{ | ||
| accept: { | ||
| children: "Approve Transaction", | ||
| // biome-ignore lint/suspicious/noExplicitAny: we know the params match the method | ||
| onClick: () => accept({ method, params } as any), | ||
| }, | ||
| reject: { | ||
| children: "Go back", | ||
| onClick: reject, | ||
| }, | ||
| }} | ||
| > | ||
| <SectionBlock> | ||
| <SubsectionBlock> | ||
| {call.to && ( | ||
| <SubsectionContent> | ||
| <SubsectionTitle>Receiver address</SubsectionTitle> | ||
| <FormattedDetailsLine> | ||
| <LinkToAddress address={call.to}>{call.to}</LinkToAddress> | ||
| </FormattedDetailsLine> | ||
| </SubsectionContent> | ||
| )} | ||
|
|
||
| {rawValue > 0n && ( | ||
| <SubsectionContent> | ||
| <SubsectionTitle>Sending amount</SubsectionTitle> | ||
| <FormattedDetailsLine>{formattedValue}</FormattedDetailsLine> | ||
| </SubsectionContent> | ||
| )} | ||
| </SubsectionBlock> | ||
| </SectionBlock> | ||
|
|
||
| <DisclosureSection title="Raw Request"> | ||
| <div className="grid gap-4 p-2"> | ||
| <FormattedDetailsLine isCode>{JSON.stringify(params, null, 2)}</FormattedDetailsLine> | ||
| </div> | ||
| </DisclosureSection> | ||
| </Layout> | ||
| ) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,14 +9,29 @@ import { | |
| EIP1193UnauthorizedError, | ||
| EIP1193UnsupportedMethodError, | ||
| EIP1474InvalidInput, | ||
| HappyWalletCapability, | ||
| permissionsLists, | ||
| } from "@happy.tech/wallet-common" | ||
| import { checksum } from "ox/Address" | ||
| import { type RpcTransactionRequest, type WatchAssetParameters, isAddress, isHex } from "viem" | ||
| import { | ||
| AtomicityNotSupportedError, | ||
| BundleTooLargeError, | ||
| type Hex, | ||
| type RpcTransactionRequest, | ||
| UnsupportedChainIdError, | ||
| UnsupportedNonOptionalCapabilityError, | ||
| type WalletSendCallsParameters, | ||
| type WatchAssetParameters, | ||
| isAddress, | ||
| isHex, | ||
| toHex, | ||
| } from "viem" | ||
| import { getAuthState } from "#src/state/authState" | ||
| import { getCurrentChain } from "#src/state/chains.ts" | ||
| import { getUser } from "#src/state/user.ts" | ||
| import type { AppURL } from "#src/utils/appURL" | ||
| import { checkIfRequestRequiresConfirmation } from "#src/utils/checkIfRequestRequiresConfirmation" | ||
| import { parseSendCallParams } from "#src/utils/isSendCallsParams" | ||
|
|
||
| /** | ||
| * Check if the user is authenticated with the social login provider, otherwise throws an error. | ||
|
|
@@ -131,3 +146,92 @@ export function ensureIsNotHappyMethod( | |
| `happy method unsupported by this handle: ${requestParams.method} — IMPLEMENTATION BUG`, | ||
| ) | ||
| } | ||
|
|
||
| // ================================= EIP-5792 ================================= | ||
| const SUPPORTED_CAPABILITIES = [HappyWalletCapability.BoopPaymaster] as const | ||
|
|
||
| export type ValidWalletSendCallsRequest = { | ||
| id: string | ||
| from: Address | ||
| chainId: Hex | ||
| atomicRequired: false | ||
| version: "2.0.0" | ||
| calls: [ | ||
| { | ||
| to: Address | ||
| data?: Hex | ||
| value?: Hex | bigint | ||
| capabilities?: { | ||
| [HappyWalletCapability.BoopPaymaster]?: { | ||
| address: Address | ||
| optional?: boolean | ||
| } | ||
| } | ||
| }, | ||
| ] | ||
| capabilities?: { | ||
| [HappyWalletCapability.BoopPaymaster]?: { | ||
| address: Address | ||
| optional?: boolean | ||
| } | ||
| } | ||
| } | ||
|
|
||
| type BoopPaymasterCapability = { | ||
| [HappyWalletCapability.BoopPaymaster]: { | ||
| address: Address | ||
| optional?: boolean | ||
| } | ||
| } | ||
|
|
||
| type WalletSendCallsParams = WalletSendCallsParameters<BoopPaymasterCapability, Hex, Hex | bigint> | ||
|
|
||
| export function checkedWalletSendCallsParams(params: WalletSendCallsParams | undefined): ValidWalletSendCallsRequest { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| // 1474 - invalid params | ||
| const parsed = parseSendCallParams(params) | ||
| if (!parsed.success && parsed.error) | ||
| throw new EIP1474InvalidInput("Invalid wallet_sendCalls request body:", parsed.error) | ||
| // 4100 - Unauthorised | ||
| checkAuthenticated() | ||
| const [reqBody] = parsed.data | ||
| // 5710 - unsupported chain ID | ||
| if (reqBody.chainId !== getCurrentChain().chainId) throw new UnsupportedChainIdError(new Error("Not HappyChain")) | ||
| // // 5740 - bundle too large, we currently only support only one call per bundle | ||
| if (reqBody.calls.length > 1) | ||
| throw new BundleTooLargeError(new Error("Happy Wallet currently only supports 1 call per bundle")) | ||
| // 5760 - no atomicity | ||
| if (reqBody.atomicRequired) | ||
| throw new AtomicityNotSupportedError(new Error("Happy Wallet does not support atomicity (yet)")) | ||
| // 5700 - unsupported capability (global + at the call level) | ||
| validateCapabilities(reqBody.capabilities ?? {}, SUPPORTED_CAPABILITIES) | ||
| validateCapabilities(reqBody.calls[0].capabilities ?? {}, SUPPORTED_CAPABILITIES) | ||
|
|
||
| return reqBody as ValidWalletSendCallsRequest | ||
| } | ||
|
|
||
| export function extractValidTxFromCall(request: ValidWalletSendCallsRequest): ValidRpcTransactionRequest { | ||
| const call = request.calls[0] | ||
|
|
||
| const tx: ValidRpcTransactionRequest = { | ||
| from: request.from!, | ||
| to: call.to!, | ||
| value: call.value !== undefined ? toHex(call.value) : undefined, | ||
| data: call.data, | ||
| } | ||
|
|
||
| return tx | ||
| } | ||
|
|
||
| function validateCapabilities( | ||
| // biome-ignore lint/suspicious/noExplicitAny: viem does it | ||
| capabilities: Record<string, any>, | ||
| supported: readonly string[], | ||
| ) { | ||
| for (const [key, value] of Object.entries(capabilities)) { | ||
| const isSupported = supported.includes(key) | ||
| const isOptional = typeof value === "object" && value?.optional === true | ||
| if (!isSupported && !isOptional) { | ||
| throw new UnsupportedNonOptionalCapabilityError(new Error(`Unsupported non-optional capability: ${key}`)) | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import { isAddress, isHex } from "viem/utils" | ||
| import { z } from "zod" | ||
|
|
||
| const addressSchema = z.string().refine((val) => isAddress(val), { message: "Invalid address" }) | ||
|
|
||
| const hexSchema = z.string().refine((val) => isHex(val), { message: "Invalid hex string" }) | ||
|
|
||
| const capabilitySchema = z.record(z.any()) | ||
|
|
||
| const callSchema = z.object({ | ||
| to: addressSchema, | ||
| data: hexSchema.optional(), | ||
| value: z.union([z.string(), z.bigint()]).optional(), | ||
| capabilities: capabilitySchema.optional(), | ||
| }) | ||
|
|
||
| const mainSchema = z.tuple([ | ||
| z.object({ | ||
| version: z.literal("2.0.0"), | ||
| id: z.string().max(4096).optional(), | ||
| from: addressSchema.optional(), | ||
| chainId: z | ||
| .string() | ||
| .refine((val) => /^0x[0-9a-fA-F]+$/.test(val), { message: "chainId must be 0x-prefixed hex" }), | ||
|
|
||
| atomicRequired: z.boolean(), // atomicity not supported yet | ||
| capabilities: capabilitySchema.optional(), | ||
| calls: z.array(callSchema).nonempty({ message: "At least one call is required" }).readonly(), | ||
| }), | ||
| ]) | ||
|
|
||
| export function parseSendCallParams(param: unknown) { | ||
| return mainSchema.safeParse(param) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this need to be in injected as well?