diff --git a/packages/delegation-toolkit/package.json b/packages/delegation-toolkit/package.json index d79f1aaa..0252e65e 100644 --- a/packages/delegation-toolkit/package.json +++ b/packages/delegation-toolkit/package.json @@ -116,6 +116,7 @@ "@metamask/delegation-abis": "^0.11.0", "@metamask/delegation-core": "^0.2.0-rc.1", "@metamask/delegation-deployments": "^0.12.0", + "@metamask/permission-types": "workspace:*", "buffer": "^6.0.3", "webauthn-p256": "^0.0.10" }, diff --git a/packages/delegation-toolkit/src/experimental/erc7715GrantPermissionsAction.ts b/packages/delegation-toolkit/src/experimental/erc7715GrantPermissionsAction.ts deleted file mode 100644 index cb878829..00000000 --- a/packages/delegation-toolkit/src/experimental/erc7715GrantPermissionsAction.ts +++ /dev/null @@ -1,431 +0,0 @@ -import type { - Account, - Address, - Chain, - Client, - Hex, - RpcSchema, - Transport, -} from 'viem'; -import { isHex, toHex } from 'viem'; - -/** - * Represents a custom permission with arbitrary data. - */ -export type Permission = { - data: Record; - type: string; - rules?: Record; - isRequired?: boolean; -}; - -/** - * Represents a native token stream permission. - * This allows for continuous token streaming with defined parameters. - */ -export type NativeTokenStreamPermission = Permission & { - type: 'native-token-stream'; - data: { - initialAmount?: bigint; - amountPerSecond: bigint; - startTime?: number; - maxAmount?: number; - justification: string; - }; -}; - -export type SignerParam = - | Address - | { type: 'account'; data: { address: Address } } - | { type: string; data: Record }; - -/** - * Parameters for a permission request. - * - * @template Signer - The type of the signer provided, either an Address or Account. - */ -export type PermissionRequest = { - chainId: number; - // address to which the permission request is targetting - address?: Address; - // Timestamp (in seconds) that specifies the time by which this permission MUST expire. - expiry: number; - // The permission to grant to the user. - permission: Permission; - // Account to assign the permission to. - signer: Signer; - // Whether the caller allows the permission to be adjusted. - isAdjustmentAllowed?: boolean; -}; - -/** - * Response from a permission request, including additional metadata. - */ -export type PermissionResponse = PermissionRequest
& { - // the PermissionsContext for the granted permission - context: Hex; - - // ERC-4337 Factory and data to deploy smart contract account. - accountMeta?: { - factory: Hex; - factoryData: Hex; - }[]; - - // This is actually _either_ a userOpBuilder or a delegationManager, but the typings - // become complicated to consume if we encode it correctly. - signerMeta?: { - userOpBuilder?: Hex; - delegationManager?: Hex; - }; -}; - -/** - * Parameters for granting permissions. - * - * @template Signer - The type of the signer, either an Address or Account. - */ -export type GrantPermissionsParameters = PermissionRequest[]; - -/** - * Return type for the grant permissions action. - */ -export type GrantPermissionsReturnType = PermissionResponse[]; -/** - * Represents the authorization status of installed MetaMask Snaps. - * - * @property {string} version - The version of the installed Snap. - * @property {string} id - The unique identifier of the Snap. - * @property {boolean} enabled - Whether the Snap is currently enabled. - * @property {boolean} blocked - Whether the Snap is currently blocked. - */ -export type SnapAuthorizations = Record< - string, - { version: string; id: string; enabled: boolean; blocked: boolean } ->; - -/** - * RPC schema for MetaMask Snap-related methods. - * - * Extends the base RPC schema with methods specific to interacting with Snaps: - * - `wallet_invokeSnap`: Invokes a method on a specific Snap. - * - `wallet_getSnaps`: Retrieves all installed Snaps and their authorization status. - * - `wallet_requestSnaps`: Requests permission to use specific Snaps. - */ -export type SnapRpcSchema = RpcSchema & - [ - { - // eslint-disable-next-line @typescript-eslint/naming-convention - Method: 'wallet_invokeSnap'; - // eslint-disable-next-line @typescript-eslint/naming-convention - Params: { - snapId: string; - request: { - method: string; - params: unknown[]; - }; - }; - // eslint-disable-next-line @typescript-eslint/naming-convention - ReturnType: unknown; - }, - { - // eslint-disable-next-line @typescript-eslint/naming-convention - Method: 'wallet_getSnaps'; - // eslint-disable-next-line @typescript-eslint/naming-convention - Params: Record; - // eslint-disable-next-line @typescript-eslint/naming-convention - ReturnType: SnapAuthorizations; - }, - { - // eslint-disable-next-line @typescript-eslint/naming-convention - Method: 'wallet_requestSnaps'; - // eslint-disable-next-line @typescript-eslint/naming-convention - Params: Record; - // eslint-disable-next-line @typescript-eslint/naming-convention - ReturnType: SnapAuthorizations; - }, - ]; - -/** - * A Viem client extended with MetaMask Snap-specific RPC methods. - * - * This client type allows for interaction with MetaMask Snaps through - * the standard Viem client interface, with added type safety for - * Snap-specific methods. - */ -export type SnapClient = Client< - Transport, - Chain | undefined, - Account | undefined, - SnapRpcSchema ->; - -/** - * Checks if a specific snap is authorized, within the specified authorizations object.. - * - * @param authorizations - The SnapAuthorizations object containing installed Snaps and their authorization status. - * @param snapId - The ID of the snap to check. - * @returns A boolean indicating whether the snap is authorized. - */ -const isSnapAuthorized = ( - authorizations: SnapAuthorizations, - snapId: string, -) => { - const authorization = authorizations[snapId]; - const isAuthorized = - (authorization?.enabled && !authorization?.blocked) || false; - - return isAuthorized; -}; - -/** - * Requests re-authorization of a specific snap. - * - * @param client - The SnapClient instance used to interact with MetaMask Snaps. - * @param snapId - The ID of the snap to re-authorize. - * @returns A promise that resolves to a boolean indicating whether the snap was re-authorized. - */ -const reAuthorize = async (client: SnapClient, snapId: string) => { - const newAuthorizations = await client.request({ - method: 'wallet_requestSnaps', - params: { - [snapId]: {} as Record, - }, - }); - - return isSnapAuthorized(newAuthorizations, snapId); -}; - -/** - * Ensures that the required MetaMask Snaps for ERC-7715 permissions are authorized. - * - * @param client - The SnapClient instance used to interact with MetaMask Snaps. - * @param snapIds - Optional object containing custom snap IDs to use. - * @param snapIds.kernelSnapId - Custom ID for the permissions kernel snap (defaults to 'npm:@metamask/permissions-kernel-snap'). - * @param snapIds.providerSnapId - Custom ID for the permissions provider snap (defaults to 'npm:@metamask/gator-permissions-snap'). - * @returns A promise that resolves to a boolean indicating whether both snaps are authorized. - * @description - * This function attempts to authorize both the kernel and provider snaps required for ERC-7715 permissions. - * It returns true only if both snaps are successfully authorized. - */ -export async function ensureSnapsAuthorized( - client: SnapClient, - snapIds?: { kernelSnapId: string; providerSnapId: string }, -) { - const kernelSnapId = - snapIds?.kernelSnapId ?? 'npm:@metamask/permissions-kernel-snap'; - const providerSnapId = - snapIds?.providerSnapId ?? 'npm:@metamask/gator-permissions-snap'; - - const existingAuthorizations = await client.request({ - method: 'wallet_getSnaps', - params: {} as Record, - }); - - if ( - !isSnapAuthorized(existingAuthorizations, kernelSnapId) && - !(await reAuthorize(client, kernelSnapId)) - ) { - return false; - } - - if ( - !isSnapAuthorized(existingAuthorizations, providerSnapId) && - !(await reAuthorize(client, providerSnapId)) - ) { - return false; - } - - return true; -} - -/** - * Grants permissions according to EIP-7715 specification. - * - * @template Signer - The type of the signer, either an Address or Account. - * @param client - The client to use for the request. - * @param parameters - The permissions requests to grant. - * @param kernelSnapId - The ID of the kernel snap to invoke, defaults to 'npm:@metamask/permissions-kernel-snap'. - * @returns A promise that resolves to the permission responses. - * @description - * This function formats the permissions requests and invokes the wallet snap to grant permissions. - * It will throw an error if the permissions could not be granted. - */ -export async function erc7715GrantPermissionsAction( - client: SnapClient, - parameters: GrantPermissionsParameters, - kernelSnapId = 'npm:@metamask/permissions-kernel-snap', -): Promise { - const formattedParameters = parameters.map(formatPermissionsRequest); - - const result = await client.request( - { - method: 'wallet_invokeSnap', - params: { - snapId: kernelSnapId, - request: { - method: 'wallet_grantPermissions', - params: formattedParameters, - }, - }, - }, - { retryCount: 0 }, - ); - - if (result === null) { - throw new Error('Failed to grant permissions'); - } - - return result as any as GrantPermissionsReturnType; -} - -/** - * Formats a permissions request for submission to the wallet. - * - * @param parameters - The permissions request to format. - * @returns The formatted permissions request. - * @internal - */ -function formatPermissionsRequest(parameters: PermissionRequest) { - const { chainId, address, expiry, isAdjustmentAllowed } = parameters; - - const permissionFormatter = getPermissionFormatter( - parameters.permission.type, - ); - - const signerAddress = - typeof parameters.signer === 'string' - ? parameters.signer - : parameters.signer.data.address; - - const isAdjustmentAllowedSpecified = - isAdjustmentAllowed !== null && isAdjustmentAllowed !== undefined; - - const optionalFields = { - ...(isAdjustmentAllowedSpecified ? { isAdjustmentAllowed } : {}), - ...(address ? { address } : {}), - }; - - return { - ...optionalFields, - chainId: toHex(chainId), - expiry, - permission: permissionFormatter(parameters.permission), - signer: { - // only support account type for now - type: 'account', - data: { - address: signerAddress, - }, - }, - }; -} - -/** - * Asserts that a value is defined (not null or undefined). - * - * @param value - The value to check. - * @param message - Optional custom error message to throw if the value is not defined. - * @throws {Error} If the value is null or undefined. - */ -function assertIsDefined( - value: TValue | null | undefined, - message?: string, -): asserts value is TValue { - if (value === null || value === undefined) { - throw new Error(message ?? 'Invalid parameters: value is required'); - } -} - -/** - * Converts a value to a hex string or throws an error if the value is invalid. - * - * @param value - The value to convert to hex. - * @param message - Optional custom error message. - * @returns The value as a hex string. - */ -function toHexOrThrow( - value: Parameters[0] | undefined, - message?: string, -) { - assertIsDefined(value, message); - - if (typeof value === 'string') { - if (!isHex(value)) { - throw new Error('Invalid parameters: invalid hex value'); - } - return value; - } - - return toHex(value); -} - -/** - * Gets the appropriate formatter function for a specific permission type. - * - * @param permissionType - The type of permission to format. - * @returns A formatter function for the specified permission type. - */ -function getPermissionFormatter( - permissionType: string, -): (permission: Permission) => Permission { - switch (permissionType) { - case 'native-token-stream': - return (permission: Permission) => - formatNativeTokenStreamPermission( - permission as NativeTokenStreamPermission, - ); - default: - return (permission: Permission) => ({ ...permission }); - } -} - -/** - * Formats a native token stream permission for the wallet. - * - * @param permission - The native token stream permission to format. - * @returns The formatted permission object. - */ -function formatNativeTokenStreamPermission( - permission: NativeTokenStreamPermission, -): Permission { - assertIsDefined( - permission.data.justification, - 'Invalid parameters: justification is required', - ); - - const isInitialAmountSpecified = - permission.data.initialAmount !== undefined && - permission.data.initialAmount !== null; - - const isMaxAmountSpecified = - permission.data.maxAmount !== undefined && - permission.data.maxAmount !== null; - - const isStartTimeSpecified = - permission.data.startTime !== undefined && - permission.data.startTime !== null; - - const optionalFields = { - ...(isInitialAmountSpecified && { - initialAmount: toHexOrThrow(permission.data.initialAmount), - }), - ...(isMaxAmountSpecified && { - maxAmount: toHexOrThrow(permission.data.maxAmount), - }), - ...(isStartTimeSpecified && { - startTime: Number(permission.data.startTime), - }), - }; - - return { - ...permission, - data: { - amountPerSecond: toHexOrThrow( - permission.data.amountPerSecond, - 'Invalid parameters: amountPerSecond is required', - ), - justification: permission.data.justification, - ...optionalFields, - }, - }; -} diff --git a/packages/delegation-toolkit/src/experimental/erc7715RequestExecutionPermissionsAction.ts b/packages/delegation-toolkit/src/experimental/erc7715RequestExecutionPermissionsAction.ts new file mode 100644 index 00000000..4465b4b3 --- /dev/null +++ b/packages/delegation-toolkit/src/experimental/erc7715RequestExecutionPermissionsAction.ts @@ -0,0 +1,480 @@ +import type { + Account, + Address, + Chain, + Client, + Hex as ViemHex, + RpcSchema, + Transport, +} from 'viem'; +import { isHex, toHex } from 'viem'; +import type { + Hex, + Signer as PermissionSigner, + PermissionTypes, + PermissionRequest as ExecutionPermissionRequest, + PermissionResponse as ExecutionPermissionResponse, + NativeTokenStreamPermission as PkgNativeTokenStreamPermission, + NativeTokenPeriodicPermission as PkgNativeTokenPeriodicPermission, + Erc20TokenStreamPermission as PkgErc20TokenStreamPermission, + Erc20TokenPeriodicPermission as PkgErc20TokenPeriodicPermission, +} from '@metamask/permission-types'; + +// Developer-friendly helper types and inputs + +type DeveloperHexish = Hex | bigint | number | string | null | undefined; + +export type WalletSignerInput = { type: 'wallet'; data?: Record }; +export type KeySignerInput = { + type: 'key'; + data: { type: 'secp256r1' | 'secp256k1' | 'ed25519' | 'schnorr'; publicKey: Hex | ViemHex }; +}; +export type KeysSignerInput = { + type: 'keys'; + data: { keys: { type: 'secp256r1' | 'secp256k1' | 'ed25519' | 'schnorr'; publicKey: Hex | ViemHex }[] }; +}; +export type AccountSignerInput = { type: 'account'; data: { address: Address | Hex | ViemHex } }; + +export type SignerInput = + | string + | Address + | WalletSignerInput + | KeySignerInput + | KeysSignerInput + | AccountSignerInput; + +export type NativeTokenStreamPermissionInput = { + type: 'native-token-stream'; + isAdjustmentAllowed?: boolean; + data: { + initialAmount?: DeveloperHexish; + maxAmount?: DeveloperHexish; + amountPerSecond: DeveloperHexish; + startTime?: number | string | null; + justification?: string | null; + }; +}; + +export type NativeTokenPeriodicPermissionInput = { + type: 'native-token-periodic'; + isAdjustmentAllowed?: boolean; + data: { + periodAmount: DeveloperHexish; + periodDuration: number; + startTime?: number | string | null; + justification?: string | null; + }; +}; + +export type Erc20TokenStreamPermissionInput = { + type: 'erc20-token-stream'; + isAdjustmentAllowed?: boolean; + data: { + tokenAddress: Address | Hex | ViemHex; + initialAmount?: DeveloperHexish; + maxAmount?: DeveloperHexish; + amountPerSecond: DeveloperHexish; + startTime?: number | string | null; + justification?: string | null; + }; +}; + +export type Erc20TokenPeriodicPermissionInput = { + type: 'erc20-token-periodic'; + isAdjustmentAllowed?: boolean; + data: { + tokenAddress: Address | Hex | ViemHex; + periodAmount: DeveloperHexish; + periodDuration: number; + startTime?: number | string | null; + justification?: string | null; + }; +}; + +export type PermissionInput = + | NativeTokenStreamPermissionInput + | NativeTokenPeriodicPermissionInput + | Erc20TokenStreamPermissionInput + | Erc20TokenPeriodicPermissionInput; + +export type RequestExecutionPermissionParameters = Array<{ + chainId: number | Hex | ViemHex; + address?: Address | Hex | ViemHex; + signer: SignerInput; + permission: PermissionInput; + rules?: { type: string; isAdjustmentAllowed: boolean; data: Record }[] | null; +}>; + +export type RequestExecutionPermissionsReturnType = ExecutionPermissionResponse< + PermissionSigner, + PermissionTypes +>[]; + +// Snap RPC types + +export type SnapAuthorizations = Record< + string, + { version: string; id: string; enabled: boolean; blocked: boolean } +>; + +export type SnapRpcSchema = RpcSchema & + [ + { + // eslint-disable-next-line @typescript-eslint/naming-convention + Method: 'wallet_invokeSnap'; + // eslint-disable-next-line @typescript-eslint/naming-convention + Params: { + snapId: string; + request: { + method: string; + params: unknown[]; + }; + }; + // eslint-disable-next-line @typescript-eslint/naming-convention + ReturnType: unknown; + }, + { + // eslint-disable-next-line @typescript-eslint/naming-convention + Method: 'wallet_getSnaps'; + // eslint-disable-next-line @typescript-eslint/naming-convention + Params: Record; + // eslint-disable-next-line @typescript-eslint/naming-convention + ReturnType: SnapAuthorizations; + }, + { + // eslint-disable-next-line @typescript-eslint/naming-convention + Method: 'wallet_requestSnaps'; + // eslint-disable-next-line @typescript-eslint/naming-convention + Params: Record; + // eslint-disable-next-line @typescript-eslint/naming-convention + ReturnType: SnapAuthorizations; + }, + ]; + +export type SnapClient = Client< + Transport, + Chain | undefined, + Account | undefined, + SnapRpcSchema +>; + +const isSnapAuthorized = ( + authorizations: SnapAuthorizations, + snapId: string, +) => { + const authorization = authorizations[snapId]; + const isAuthorized = + (authorization?.enabled && !authorization?.blocked) || false; + + return isAuthorized; +}; + +const reAuthorize = async (client: SnapClient, snapId: string) => { + const newAuthorizations = await client.request({ + method: 'wallet_requestSnaps', + params: { + [snapId]: {} as Record, + }, + }); + + return isSnapAuthorized(newAuthorizations, snapId); +}; + +export async function ensureSnapsAuthorized( + client: SnapClient, + snapIds?: { kernelSnapId: string; providerSnapId: string }, +) { + const kernelSnapId = + snapIds?.kernelSnapId ?? 'npm:@metamask/permissions-kernel-snap'; + const providerSnapId = + snapIds?.providerSnapId ?? 'npm:@metamask/gator-permissions-snap'; + + const existingAuthorizations = await client.request({ + method: 'wallet_getSnaps', + params: {} as Record, + }); + + if ( + !isSnapAuthorized(existingAuthorizations, kernelSnapId) && + !(await reAuthorize(client, kernelSnapId)) + ) { + return false; + } + + if ( + !isSnapAuthorized(existingAuthorizations, providerSnapId) && + !(await reAuthorize(client, providerSnapId)) + ) { + return false; + } + + return true; +} + +export async function erc7715RequestExecutionPermissionsAction( + client: SnapClient, + parameters: RequestExecutionPermissionParameters, + kernelSnapId = 'npm:@metamask/permissions-kernel-snap', +): Promise { + const formattedParameters = parameters.map(formatPermissionsRequest); + + const result = await client.request( + { + method: 'wallet_invokeSnap', + params: { + snapId: kernelSnapId, + request: { + method: 'wallet_requestExecutionPermissions', + params: formattedParameters, + }, + }, + }, + { retryCount: 0 }, + ); + + if (result === null) { + throw new Error('Failed to request execution permissions'); + } + + return result as any as RequestExecutionPermissionsReturnType; +} + +function formatPermissionsRequest( + parameters: RequestExecutionPermissionParameters[number], +): ExecutionPermissionRequest { + const { rules } = parameters; + + const chainIdHex = toHexOrThrow(parameters.chainId, 'Invalid chainId'); + + const addressHex = parameters.address + ? (toHexOrThrow(parameters.address, 'Invalid address') as Hex) + : undefined; + + const signer = formatSigner(parameters.signer); + + const permission = formatPermission(parameters.permission); + + const optionalFields = { + ...(rules !== undefined ? { rules: rules ?? null } : {}), + ...(addressHex ? { address: addressHex } : {}), + } as Partial>; + + return { + chainId: chainIdHex as Hex, + signer, + permission, + ...optionalFields, + } as ExecutionPermissionRequest; +} + +function formatSigner(signer: SignerInput): PermissionSigner { + if (typeof signer === 'string') { + const address = signer as Address | Hex | ViemHex; + return { type: 'account', data: { address: toHexOrThrow(address) as Hex } }; + } + + if ('type' in signer && signer.type === 'account') { + return { + type: 'account', + data: { address: toHexOrThrow(signer.data.address) as Hex }, + }; + } + + if ('type' in signer && signer.type === 'wallet') { + return { type: 'wallet', data: {} }; + } + + if ('type' in signer && signer.type === 'key') { + return { + type: 'key', + data: { type: signer.data.type, publicKey: toHexOrThrow(signer.data.publicKey) as Hex }, + }; + } + + if ('type' in signer && signer.type === 'keys') { + return { + type: 'keys', + data: { + keys: signer.data.keys.map((k) => ({ + type: k.type, + publicKey: toHexOrThrow(k.publicKey) as Hex, + })), + }, + }; + } + + // Fallback: treat as account address + return { type: 'account', data: { address: toHexOrThrow(signer as Address) as Hex } }; +} + +function assertIsDefined( + value: TValue | null | undefined, + message?: string, +): asserts value is TValue { + if (value === null || value === undefined) { + throw new Error(message ?? 'Invalid parameters: value is required'); + } +} + +function toHexOrThrow( + value: DeveloperHexish, + message?: string, +) { + assertIsDefined(value, message); + + if (typeof value === 'string') { + if (!isHex(value)) { + // Treat as decimal string, coerce via Number -> toHex + const decimal = Number(value); + if (Number.isNaN(decimal)) { + throw new Error('Invalid parameters: invalid hex value'); + } + return toHex(decimal); + } + return value as Hex; + } + + if (typeof value === 'number' || typeof value === 'bigint') { + return toHex(value as Parameters[0]); + } + + return value as Hex; // should be null/undefined handled earlier +} + +function toOptionalHex(value: DeveloperHexish): Hex | null | undefined { + if (value === null || value === undefined) { + return value as null | undefined; + } + return toHexOrThrow(value); +} + +function toOptionalNumber(value: number | string | null | undefined): number | null | undefined { + if (value === null || value === undefined) { + return value; + } + if (typeof value === 'string') { + if (isHex(value)) { + return Number(BigInt(value)); + } + const decimal = Number(value); + if (Number.isNaN(decimal)) { + throw new Error('Invalid parameters: invalid number value'); + } + return decimal; + } + return value; +} + +function formatPermission(permission: PermissionInput): PermissionTypes { + switch (permission.type) { + case 'native-token-stream': + return formatNativeTokenStreamPermission(permission); + case 'native-token-periodic': + return formatNativeTokenPeriodicPermission(permission); + case 'erc20-token-stream': + return formatErc20TokenStreamPermission(permission); + case 'erc20-token-periodic': + return formatErc20TokenPeriodicPermission(permission); + default: + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return permission as any as PermissionTypes; + } +} + +function withIsAdjustmentAllowed( + permission: T, +): { isAdjustmentAllowed: boolean } { + return { isAdjustmentAllowed: permission.isAdjustmentAllowed ?? false }; +} + +function formatNativeTokenStreamPermission( + permission: NativeTokenStreamPermissionInput, +): PkgNativeTokenStreamPermission { + assertIsDefined( + permission.data.amountPerSecond, + 'Invalid parameters: amountPerSecond is required', + ); + + return { + type: 'native-token-stream', + ...withIsAdjustmentAllowed(permission), + data: { + amountPerSecond: toHexOrThrow(permission.data.amountPerSecond) as Hex, + initialAmount: toOptionalHex(permission.data.initialAmount) ?? undefined, + maxAmount: toOptionalHex(permission.data.maxAmount) ?? undefined, + startTime: toOptionalNumber(permission.data.startTime), + justification: permission.data.justification ?? undefined, + }, + } as PkgNativeTokenStreamPermission; +} + +function formatNativeTokenPeriodicPermission( + permission: NativeTokenPeriodicPermissionInput, +): PkgNativeTokenPeriodicPermission { + assertIsDefined( + permission.data.periodAmount, + 'Invalid parameters: periodAmount is required', + ); + assertIsDefined( + permission.data.periodDuration, + 'Invalid parameters: periodDuration is required', + ); + + return { + type: 'native-token-periodic', + ...withIsAdjustmentAllowed(permission), + data: { + periodAmount: toHexOrThrow(permission.data.periodAmount) as Hex, + periodDuration: permission.data.periodDuration, + startTime: toOptionalNumber(permission.data.startTime), + justification: permission.data.justification ?? undefined, + }, + } as PkgNativeTokenPeriodicPermission; +} + +function formatErc20TokenStreamPermission( + permission: Erc20TokenStreamPermissionInput, +): PkgErc20TokenStreamPermission { + assertIsDefined( + permission.data.amountPerSecond, + 'Invalid parameters: amountPerSecond is required', + ); + + return { + type: 'erc20-token-stream', + ...withIsAdjustmentAllowed(permission), + data: { + tokenAddress: toHexOrThrow(permission.data.tokenAddress) as Hex, + amountPerSecond: toHexOrThrow(permission.data.amountPerSecond) as Hex, + initialAmount: toOptionalHex(permission.data.initialAmount) ?? undefined, + maxAmount: toOptionalHex(permission.data.maxAmount) ?? undefined, + startTime: toOptionalNumber(permission.data.startTime), + justification: permission.data.justification ?? undefined, + }, + } as PkgErc20TokenStreamPermission; +} + +function formatErc20TokenPeriodicPermission( + permission: Erc20TokenPeriodicPermissionInput, +): PkgErc20TokenPeriodicPermission { + assertIsDefined( + permission.data.periodAmount, + 'Invalid parameters: periodAmount is required', + ); + assertIsDefined( + permission.data.periodDuration, + 'Invalid parameters: periodDuration is required', + ); + + return { + type: 'erc20-token-periodic', + ...withIsAdjustmentAllowed(permission), + data: { + tokenAddress: toHexOrThrow(permission.data.tokenAddress) as Hex, + periodAmount: toHexOrThrow(permission.data.periodAmount) as Hex, + periodDuration: permission.data.periodDuration, + startTime: toOptionalNumber(permission.data.startTime), + justification: permission.data.justification ?? undefined, + }, + } as PkgErc20TokenPeriodicPermission; +} \ No newline at end of file diff --git a/packages/delegation-toolkit/src/experimental/index.ts b/packages/delegation-toolkit/src/experimental/index.ts index e022d04c..e7d8abfa 100644 --- a/packages/delegation-toolkit/src/experimental/index.ts +++ b/packages/delegation-toolkit/src/experimental/index.ts @@ -11,18 +11,18 @@ import { } from './erc7710RedeemDelegationAction'; import { ensureSnapsAuthorized, - erc7715GrantPermissionsAction, -} from './erc7715GrantPermissionsAction'; + erc7715RequestExecutionPermissionsAction, +} from './erc7715RequestExecutionPermissionsAction'; import type { SnapClient, - GrantPermissionsParameters, -} from './erc7715GrantPermissionsAction'; + RequestExecutionPermissionParameters, +} from './erc7715RequestExecutionPermissionsAction'; export { - erc7715GrantPermissionsAction as grantPermissions, - type GrantPermissionsParameters, - type GrantPermissionsReturnType, -} from './erc7715GrantPermissionsAction'; + erc7715RequestExecutionPermissionsAction as requestExecutionPermissions, + type RequestExecutionPermissionParameters, + type RequestExecutionPermissionsReturnType, +} from './erc7715RequestExecutionPermissionsAction'; export { DelegationStorageClient, @@ -34,12 +34,14 @@ export { export const erc7715ProviderActions = (snapIds?: { kernelSnapId: string; providerSnapId: string }) => (client: Client) => ({ - grantPermissions: async (parameters: GrantPermissionsParameters) => { + requestExecutionPermissions: async ( + parameters: RequestExecutionPermissionParameters, + ) => { if (!(await ensureSnapsAuthorized(client as SnapClient, snapIds))) { throw new Error('Snaps not authorized'); } - return erc7715GrantPermissionsAction( + return erc7715RequestExecutionPermissionsAction( client as SnapClient, parameters, snapIds?.kernelSnapId, diff --git a/packages/delegation-toolkit/test/experimental/erc7715GrantPermissionsAction.test.ts b/packages/delegation-toolkit/test/experimental/erc7715RequestExecutionPermissionsAction.test.ts similarity index 75% rename from packages/delegation-toolkit/test/experimental/erc7715GrantPermissionsAction.test.ts rename to packages/delegation-toolkit/test/experimental/erc7715RequestExecutionPermissionsAction.test.ts index 28d544f0..906a9f23 100644 --- a/packages/delegation-toolkit/test/experimental/erc7715GrantPermissionsAction.test.ts +++ b/packages/delegation-toolkit/test/experimental/erc7715RequestExecutionPermissionsAction.test.ts @@ -7,10 +7,10 @@ import { beforeEach, describe, expect, it } from 'vitest'; import { erc7715ProviderActions } from '../../src/experimental'; import { ensureSnapsAuthorized, - erc7715GrantPermissionsAction, -} from '../../src/experimental/erc7715GrantPermissionsAction'; + erc7715RequestExecutionPermissionsAction, +} from '../../src/experimental/erc7715RequestExecutionPermissionsAction'; -describe('erc7715GrantPermissionsAction', () => { +describe('erc7715RequestExecutionPermissionsAction', () => { let alice: Account; let bob: Account; @@ -26,15 +26,15 @@ describe('erc7715GrantPermissionsAction', () => { stubRequest.reset(); }); - describe('erc7715GrantPermissionsAction()', () => { + describe('erc7715RequestExecutionPermissionsAction()', () => { it('should format permissions request correctly', async () => { const parameters = [ { chainId: 31337, address: bob.address, - expiry: 1234567890, permission: { type: 'native-token-stream', + isAdjustmentAllowed: false, data: { amountPerSecond: '0x1', maxAmount: '0x2', @@ -42,12 +42,11 @@ describe('erc7715GrantPermissionsAction', () => { justification: 'Test justification', }, }, - isAdjustmentAllowed: false, signer: { type: 'account', data: { address: alice.address } }, }, ]; - await erc7715GrantPermissionsAction(mockClient, parameters); + await erc7715RequestExecutionPermissionsAction(mockClient as any, parameters as any); expect(stubRequest.callCount).to.equal(1); expect(stubRequest.firstCall.args[0]).to.deep.equal({ @@ -55,14 +54,14 @@ describe('erc7715GrantPermissionsAction', () => { params: { snapId: 'npm:@metamask/permissions-kernel-snap', request: { - method: 'wallet_grantPermissions', + method: 'wallet_requestExecutionPermissions', params: [ { chainId: '0x7a69', address: bob.address, - expiry: 1234567890, permission: { type: 'native-token-stream', + isAdjustmentAllowed: false, data: { amountPerSecond: '0x1', maxAmount: '0x2', @@ -70,7 +69,6 @@ describe('erc7715GrantPermissionsAction', () => { justification: 'Test justification', }, }, - isAdjustmentAllowed: false, signer: { type: 'account', data: { @@ -89,9 +87,9 @@ describe('erc7715GrantPermissionsAction', () => { { chainId: 31337, address: bob.address, - expiry: 1234567890, permission: { type: 'native-token-stream', + isAdjustmentAllowed: false, data: { amountPerSecond: '0x1', maxAmount: '0x2', @@ -99,12 +97,11 @@ describe('erc7715GrantPermissionsAction', () => { justification: 'Test justification', }, }, - isAdjustmentAllowed: false, signer: { type: 'account', data: { address: alice.address } }, }, ]; - await erc7715GrantPermissionsAction(mockClient, parameters); + await erc7715RequestExecutionPermissionsAction(mockClient as any, parameters as any); expect(stubRequest.callCount).to.equal(1); expect(stubRequest.firstCall.args[1]).to.deep.equal({ @@ -117,57 +114,30 @@ describe('erc7715GrantPermissionsAction', () => { { chainId: 31337, address: bob.address, - expiry: 1234567890, permission: { type: 'native-token-stream', data: { - amountPerSecond: undefined, + // @ts-ignore testing undefined + amountPerSecond: undefined as any, maxAmount: '0x2', startTime: 2, justification: 'Test justification', }, }, - isAdjustmentAllowed: false, signer: { type: 'account', data: { address: alice.address } }, }, ]; await expect( - erc7715GrantPermissionsAction(mockClient, parameters), + erc7715RequestExecutionPermissionsAction(mockClient as any, parameters as any), ).rejects.toThrow('Invalid parameters: amountPerSecond is required'); }); - it('should throw an error when justification is undefined', async () => { - const parameters = [ - { - chainId: 31337, - address: bob.address, - expiry: 1234567890, - permission: { - type: 'native-token-stream', - data: { - amountPerSecond: '0x1', - maxAmount: '0x2', - startTime: 2, - justification: undefined, - }, - }, - isAdjustmentAllowed: false, - signer: { type: 'account', data: { address: alice.address } }, - }, - ]; - - await expect( - erc7715GrantPermissionsAction(mockClient, parameters), - ).rejects.toThrow('Invalid parameters: justification is required'); - }); - it('should format native-token-stream permission request correctly', async () => { const parameters = [ { chainId: 31337, address: bob.address, - expiry: 1234567890, permission: { type: 'native-token-stream', data: { @@ -177,12 +147,11 @@ describe('erc7715GrantPermissionsAction', () => { justification: 'Test justification', }, }, - isAdjustmentAllowed: false, signer: { type: 'account', data: { address: alice.address } }, }, ]; - await erc7715GrantPermissionsAction(mockClient, parameters); + await erc7715RequestExecutionPermissionsAction(mockClient as any, parameters as any); expect(stubRequest.callCount).to.equal(1); expect(stubRequest.firstCall.args[0]).to.deep.equal({ @@ -190,14 +159,14 @@ describe('erc7715GrantPermissionsAction', () => { params: { snapId: 'npm:@metamask/permissions-kernel-snap', request: { - method: 'wallet_grantPermissions', + method: 'wallet_requestExecutionPermissions', params: [ { chainId: '0x7a69', address: bob.address, - expiry: 1234567890, permission: { type: 'native-token-stream', + isAdjustmentAllowed: false, data: { amountPerSecond: '0x1', maxAmount: '0x2', @@ -205,7 +174,6 @@ describe('erc7715GrantPermissionsAction', () => { justification: 'Test justification', }, }, - isAdjustmentAllowed: false, signer: { type: 'account', data: { @@ -225,7 +193,6 @@ describe('erc7715GrantPermissionsAction', () => { const parameters = [ { chainId: 31337, - expiry: Math.floor(Date.now() / 1000) + 3600, permission: { type: 'native-token-stream', data: { @@ -235,43 +202,42 @@ describe('erc7715GrantPermissionsAction', () => { justification: 'Test justification', }, }, - isAdjustmentAllowed: false, signer: { type: 'account', data: { address: alice.address } }, }, ]; await expect( - erc7715GrantPermissionsAction(mockClient, parameters), - ).rejects.toThrow('Failed to grant permissions'); + erc7715RequestExecutionPermissionsAction(mockClient as any, parameters as any), + ).rejects.toThrow('Failed to request execution permissions'); }); it('should use the default snap ID if not provided', async () => { stubRequest.resolves([ { chainId: '0x7b27', - expiry: 1234567890, permission: { - type: 'basic-permission', - data: { foo: 'bar' }, + type: 'native-token-stream', + data: { amountPerSecond: '0x1' }, + isAdjustmentAllowed: false, }, signer: alice.address, context: '0x123456', + dependencyInfo: [], }, ]); const parameters = [ { chainId: 31337, - expiry: 1234567890, permission: { - type: 'basic-permission', - data: { foo: 'bar' }, + type: 'native-token-stream', + data: { amountPerSecond: '0x1' }, }, signer: { type: 'account', data: { address: alice.address } }, }, ]; - await erc7715GrantPermissionsAction(mockClient, parameters); + await erc7715RequestExecutionPermissionsAction(mockClient as any, parameters as any); expect(stubRequest.callCount).to.equal(1); expect(stubRequest.firstCall.args[0].params.snapId).to.equal( @@ -284,31 +250,31 @@ describe('erc7715GrantPermissionsAction', () => { stubRequest.resolves([ { chainId: '0x7b27', - expiry: 1234567890, permission: { - type: 'basic-permission', - data: { foo: 'bar' }, + type: 'native-token-stream', + data: { amountPerSecond: '0x1' }, + isAdjustmentAllowed: false, }, signer: alice.address, context: '0x123456', + dependencyInfo: [], }, ]); const parameters = [ { chainId: 31337, - expiry: 1234567890, permission: { - type: 'basic-permission', - data: { foo: 'bar' }, + type: 'native-token-stream', + data: { amountPerSecond: '0x1' }, }, signer: { type: 'account', data: { address: alice.address } }, }, ]; - await erc7715GrantPermissionsAction( - mockClient, - parameters, + await erc7715RequestExecutionPermissionsAction( + mockClient as any, + parameters as any, customKernelId, ); @@ -322,23 +288,25 @@ describe('erc7715GrantPermissionsAction', () => { const permissionsResponse = [ { chainId: '0x7b27', - expiry: 1234567890, permission: { - type: 'basic-permission', - data: { foo: 'bar' }, + type: 'native-token-stream', + data: { amountPerSecond: '0x1' }, + isAdjustmentAllowed: false, }, signer: alice.address, context: '0x123456', + dependencyInfo: [], }, { chainId: '0x7b27', - expiry: 1234567890, permission: { - type: 'basic-permission', - data: { foo: 'bar' }, + type: 'native-token-stream', + data: { amountPerSecond: '0x1' }, + isAdjustmentAllowed: false, }, signer: bob.address, context: '0x654321', + dependencyInfo: [], }, ]; stubRequest.resolves(permissionsResponse); @@ -346,99 +314,36 @@ describe('erc7715GrantPermissionsAction', () => { const parameters = [ { chainId: 31337, - expiry: 1234567890, permission: { - type: 'basic-permission', - data: { foo: 'bar' }, + type: 'native-token-stream', + data: { amountPerSecond: '0x1' }, }, signer: alice.address, }, { chainId: 31337, - expiry: 1234567890, permission: { - type: 'basic-permission', - data: { foo: 'bar' }, + type: 'native-token-stream', + data: { amountPerSecond: '0x1' }, }, signer: { type: 'account', data: { address: bob.address } }, }, ]; - const result = await erc7715GrantPermissionsAction( - mockClient, - parameters, + const result = await erc7715RequestExecutionPermissionsAction( + mockClient as any, + parameters as any, ); expect(result).to.have.lengthOf(2); expect(result).to.deep.equal(permissionsResponse); }); - it('should not specify isAdjustmentAllowed when not specified in the request', async () => { - const parameters = [ - { - chainId: 31337, - address: bob.address, - expiry: 1234567890, - permission: { - type: 'native-token-stream', - data: { - amountPerSecond: '0x1', - maxAmount: '0x2', - startTime: 2, - justification: 'Test justification', - }, - }, - signer: { - type: 'account', - data: { - address: alice.address, - }, - }, - }, - ]; - - await erc7715GrantPermissionsAction(mockClient, parameters); - - expect(stubRequest.callCount).to.equal(1); - expect(stubRequest.firstCall.args[0]).to.deep.equal({ - method: 'wallet_invokeSnap', - params: { - snapId: 'npm:@metamask/permissions-kernel-snap', - request: { - method: 'wallet_grantPermissions', - params: [ - { - chainId: '0x7a69', - address: bob.address, - expiry: 1234567890, - permission: { - type: 'native-token-stream', - data: { - amountPerSecond: '0x1', - maxAmount: '0x2', - startTime: 2, - justification: 'Test justification', - }, - }, - signer: { - type: 'account', - data: { - address: alice.address, - }, - }, - }, - ], - }, - }, - }); - }); - it('should allow maxAmount to be excluded from the request', async () => { const parameters = [ { chainId: 31337, address: bob.address, - expiry: 1234567890, permission: { type: 'native-token-stream', data: { @@ -451,7 +356,7 @@ describe('erc7715GrantPermissionsAction', () => { }, ]; - await erc7715GrantPermissionsAction(mockClient, parameters); + await erc7715RequestExecutionPermissionsAction(mockClient as any, parameters as any); expect(stubRequest.callCount).to.equal(1); expect(stubRequest.firstCall.args[0]).to.deep.equal({ @@ -459,14 +364,14 @@ describe('erc7715GrantPermissionsAction', () => { params: { snapId: 'npm:@metamask/permissions-kernel-snap', request: { - method: 'wallet_grantPermissions', + method: 'wallet_requestExecutionPermissions', params: [ { chainId: '0x7a69', address: bob.address, - expiry: 1234567890, permission: { type: 'native-token-stream', + isAdjustmentAllowed: false, data: { amountPerSecond: '0x1', startTime: 2, @@ -491,7 +396,6 @@ describe('erc7715GrantPermissionsAction', () => { { chainId: 31337, address: bob.address, - expiry: 1234567890, permission: { type: 'native-token-stream', data: { @@ -505,7 +409,7 @@ describe('erc7715GrantPermissionsAction', () => { }, ]; - await erc7715GrantPermissionsAction(mockClient, parameters); + await erc7715RequestExecutionPermissionsAction(mockClient as any, parameters as any); expect(stubRequest.callCount).to.equal(1); expect(stubRequest.firstCall.args[0]).to.deep.equal({ @@ -513,14 +417,14 @@ describe('erc7715GrantPermissionsAction', () => { params: { snapId: 'npm:@metamask/permissions-kernel-snap', request: { - method: 'wallet_grantPermissions', + method: 'wallet_requestExecutionPermissions', params: [ { chainId: '0x7a69', address: bob.address, - expiry: 1234567890, permission: { type: 'native-token-stream', + isAdjustmentAllowed: false, data: { amountPerSecond: '0x1', startTime: 2, @@ -540,12 +444,11 @@ describe('erc7715GrantPermissionsAction', () => { }); }); - it('should accept (incorrectly) numerical values as hex', async () => { + it('should accept numerical values as hex', async () => { const parameters = [ { chainId: 31337, address: bob.address, - expiry: 1234567890, permission: { type: 'native-token-stream', data: { @@ -559,7 +462,7 @@ describe('erc7715GrantPermissionsAction', () => { }, ]; - await erc7715GrantPermissionsAction(mockClient, parameters); + await erc7715RequestExecutionPermissionsAction(mockClient as any, parameters as any); expect(stubRequest.callCount).to.equal(1); expect(stubRequest.firstCall.args[0]).to.deep.equal({ @@ -567,14 +470,14 @@ describe('erc7715GrantPermissionsAction', () => { params: { snapId: 'npm:@metamask/permissions-kernel-snap', request: { - method: 'wallet_grantPermissions', + method: 'wallet_requestExecutionPermissions', params: [ { chainId: '0x7a69', address: bob.address, - expiry: 1234567890, permission: { type: 'native-token-stream', + isAdjustmentAllowed: false, data: { amountPerSecond: '0x1', maxAmount: '0x2', @@ -597,7 +500,7 @@ describe('erc7715GrantPermissionsAction', () => { }); }); - describe('erc7715GrantPermissionsAction()', () => { + describe('erc7715ProviderActions()', () => { const allSnapsEnabledResponse = { 'npm:@metamask/permissions-kernel-snap': { version: '1.0.0', @@ -624,13 +527,12 @@ describe('erc7715GrantPermissionsAction', () => { }), }).extend(erc7715ProviderActions()); - expect(client).to.have.property('grantPermissions'); + expect(client).to.have.property('requestExecutionPermissions'); const parameters = [ { chainId: 31337, address: bob.address, - expiry: 1234567890, permission: { type: 'native-token-stream', data: { @@ -639,11 +541,10 @@ describe('erc7715GrantPermissionsAction', () => { justification: 'Test justification', }, }, - isAdjustmentAllowed: false, signer: { type: 'account', data: { address: alice.address } }, }, ]; - await client.grantPermissions(parameters); + await (client as any).requestExecutionPermissions(parameters as any); expect(stubRequest.callCount).to.equal(2); @@ -652,21 +553,20 @@ describe('erc7715GrantPermissionsAction', () => { params: { snapId: 'npm:@metamask/permissions-kernel-snap', request: { - method: 'wallet_grantPermissions', + method: 'wallet_requestExecutionPermissions', params: [ { chainId: '0x7a69', address: bob.address, - expiry: 1234567890, permission: { type: 'native-token-stream', + isAdjustmentAllowed: false, data: { amountPerSecond: '0x1', startTime: 2, justification: 'Test justification', }, }, - isAdjustmentAllowed: false, signer: { type: 'account', data: { address: alice.address } }, }, ], @@ -696,7 +596,6 @@ describe('erc7715GrantPermissionsAction', () => { { chainId: 31337, address: bob.address, - expiry: 1234567890, permission: { type: 'native-token-stream', data: { @@ -705,12 +604,11 @@ describe('erc7715GrantPermissionsAction', () => { justification: 'Test justification', }, }, - isAdjustmentAllowed: false, signer: { type: 'account', data: { address: alice.address } }, }, ]; - const grantPromise = client.grantPermissions(parameters); + const grantPromise = (client as any).requestExecutionPermissions(parameters as any); // just let the event loop cycle await new Promise((resolve) => setImmediate(resolve)); @@ -724,31 +622,30 @@ describe('erc7715GrantPermissionsAction', () => { // Resolve the snaps check with authorized snaps resolveSnapsPromise?.(allSnapsEnabledResponse); - // Wait for the grant permissions operation to complete + // Wait for the operation to complete await grantPromise; - // Verify the second call was made to grant permissions + // Verify the second call was made to request execution permissions expect(stubRequest.callCount).to.equal(2); expect(stubRequest.secondCall.args[0]).to.deep.equal({ method: 'wallet_invokeSnap', params: { snapId: 'npm:@metamask/permissions-kernel-snap', request: { - method: 'wallet_grantPermissions', + method: 'wallet_requestExecutionPermissions', params: [ { chainId: '0x7a69', address: bob.address, - expiry: 1234567890, permission: { type: 'native-token-stream', + isAdjustmentAllowed: false, data: { amountPerSecond: '0x1', startTime: 2, justification: 'Test justification', }, }, - isAdjustmentAllowed: false, signer: { type: 'account', data: { address: alice.address } }, }, ], @@ -781,7 +678,7 @@ describe('erc7715GrantPermissionsAction', () => { }, }); - const res = await ensureSnapsAuthorized(client); + const res = await ensureSnapsAuthorized(client as any); expect(res).to.equal(true); @@ -813,7 +710,7 @@ describe('erc7715GrantPermissionsAction', () => { }, }); - const res = await ensureSnapsAuthorized(client, { + const res = await ensureSnapsAuthorized(client as any, { kernelSnapId, providerSnapId, }); @@ -839,7 +736,7 @@ describe('erc7715GrantPermissionsAction', () => { }, }); - const res = await ensureSnapsAuthorized(client); + const res = await ensureSnapsAuthorized(client as any); expect(res).to.equal(false); }); @@ -860,7 +757,7 @@ describe('erc7715GrantPermissionsAction', () => { }, }); - const res = await ensureSnapsAuthorized(client); + const res = await ensureSnapsAuthorized(client as any); expect(res).to.equal(false); }); @@ -881,7 +778,7 @@ describe('erc7715GrantPermissionsAction', () => { }, }); - const res = await ensureSnapsAuthorized(client); + const res = await ensureSnapsAuthorized(client as any); expect(res).to.equal(false); }); @@ -902,9 +799,9 @@ describe('erc7715GrantPermissionsAction', () => { }, }); - const res = await ensureSnapsAuthorized(client); + const res = await ensureSnapsAuthorized(client as any); expect(res).to.equal(false); }); }); -}); +}); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 3ed6046c..600fd70f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -607,6 +607,7 @@ __metadata: "@metamask/eslint-config": "npm:^12.0.0" "@metamask/eslint-config-nodejs": "npm:^12.0.0" "@metamask/eslint-config-typescript": "npm:^12.0.0" + "@metamask/permission-types": "workspace:*" "@types/node": "npm:^20.10.6" "@types/sinon": "npm:^17.0.3" "@typescript-eslint/eslint-plugin": "npm:^5.42.1" @@ -683,7 +684,7 @@ __metadata: languageName: node linkType: hard -"@metamask/permission-types@workspace:packages/permission-types": +"@metamask/permission-types@workspace:*, @metamask/permission-types@workspace:packages/permission-types": version: 0.0.0-use.local resolution: "@metamask/permission-types@workspace:packages/permission-types" dependencies: