From 271db594709f45fafd88ac2941490fc612ae9c33 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:54:05 +1200 Subject: [PATCH 1/4] Implement caveat building logic for erc20tokenstream permission type --- packages/7715-permission-types/src/index.ts | 3 + .../permissions/caveats/erc20TokenStream.ts | 65 ++++++++++++++++-- .../src/permissions/index.ts | 6 ++ .../src/permissions/types.ts | 15 +++++ .../caveats/erc20TokenStream.test.ts | 66 ++++++++++++++++++- 5 files changed, 148 insertions(+), 7 deletions(-) diff --git a/packages/7715-permission-types/src/index.ts b/packages/7715-permission-types/src/index.ts index fa595af4..1f1929c3 100644 --- a/packages/7715-permission-types/src/index.ts +++ b/packages/7715-permission-types/src/index.ts @@ -22,6 +22,9 @@ export type { export type { PayeeRule, RedeemerRule, ExpiryRule } from './permissions'; export { makePermissionDecoderConfigs, + createErc20TokenStreamCaveats, + type Erc20TokenStreamEnforcers, + type Erc20TokenStreamDecoderEnforcers, type DeployedContractsByName, type PermissionDecoderConfig, } from './permissions'; diff --git a/packages/7715-permission-types/src/permissions/caveats/erc20TokenStream.ts b/packages/7715-permission-types/src/permissions/caveats/erc20TokenStream.ts index ef61021a..a7a85b7b 100644 --- a/packages/7715-permission-types/src/permissions/caveats/erc20TokenStream.ts +++ b/packages/7715-permission-types/src/permissions/caveats/erc20TokenStream.ts @@ -1,4 +1,9 @@ -import { decodeERC20StreamingTerms } from '@metamask/delegation-core'; +import type { Caveat } from '@metamask/delegation-core'; +import { + createERC20StreamingTerms, + createValueLteTerms, + decodeERC20StreamingTerms, +} from '@metamask/delegation-core'; import { bigIntToHex } from '@metamask/utils'; import type { Erc20TokenStreamPermission } from '../../types'; @@ -7,9 +12,10 @@ import { erc20PayeeRuleDecoder } from '../rules/payee'; import { redeemerRuleDecoder } from '../rules/redeemer'; import type { ChecksumCaveat, - ChecksumEnforcersByChainId, DecodedPermissionData, - MakePermissionDecoderConfig, + DeepRequired, + ChecksumEnforcersByChainId, + PermissionDecoderConfig, } from '../types'; import { getTermsByEnforcer, ZERO_32_BYTES } from '../utils'; @@ -21,7 +27,7 @@ import { getTermsByEnforcer, ZERO_32_BYTES } from '../utils'; */ export function makeErc20TokenStreamDecoderConfig( contractAddresses: ChecksumEnforcersByChainId, -): MakePermissionDecoderConfig { +): PermissionDecoderConfig { const { timestampEnforcer, erc20StreamingEnforcer, @@ -104,3 +110,54 @@ function validateAndDecodeData( startTime, }; } + +/** + * Enforcers required to build ERC-20 token stream caveats. + */ +export type Erc20TokenStreamEnforcers = Pick< + ChecksumEnforcersByChainId, + 'erc20StreamingEnforcer' | 'valueLteEnforcer' +>; + +/** + * Builds the ERC-20 stream caveats required for this permission type. + * + * @param options0 - Caveat builder arguments. + * @param options0.permission - Fully populated ERC-20 stream permission data. + * @param options0.contracts - Enforcer addresses used to construct caveats. + * @returns The ERC20 streaming and zero-value caveats. + */ +export async function createErc20TokenStreamCaveats({ + permission, + contracts, +}: { + permission: DeepRequired; + contracts: Erc20TokenStreamEnforcers; +}): Promise { + const { initialAmount, maxAmount, amountPerSecond, startTime } = + permission.data; + + // ERC20StreamingEnforcer: enforce token address, initial/max amount, amount per second, and start time. + const erc20StreamingCaveat: Caveat = { + enforcer: contracts.erc20StreamingEnforcer, + terms: createERC20StreamingTerms({ + tokenAddress: permission.data.tokenAddress, + initialAmount: BigInt(initialAmount), + maxAmount: BigInt(maxAmount), + amountPerSecond: BigInt(amountPerSecond), + startTime, + }), + args: '0x', + }; + + // ValueLteEnforcer: allow no native value (e.g. msg.value must be 0). + const valueLteCaveat: Caveat = { + enforcer: contracts.valueLteEnforcer, + terms: createValueLteTerms({ + maxValue: 0n, + }), + args: '0x', + }; + + return [erc20StreamingCaveat, valueLteCaveat]; +} diff --git a/packages/7715-permission-types/src/permissions/index.ts b/packages/7715-permission-types/src/permissions/index.ts index 12aeec06..6471ce9a 100644 --- a/packages/7715-permission-types/src/permissions/index.ts +++ b/packages/7715-permission-types/src/permissions/index.ts @@ -8,6 +8,12 @@ import { makeTokenApprovalRevocationDecoderConfig } from './caveats/tokenApprova import type { DeployedContractsByName, PermissionDecoderConfig } from './types'; import { getChecksumEnforcersByChainId } from './utils'; +export { + createErc20TokenStreamCaveats, + type Erc20TokenStreamEnforcers, + type Erc20TokenStreamDecoderEnforcers, +} from './caveats/erc20TokenStream'; + export type { ExpiryRule } from './rules/expiry'; export type { PayeeRule } from './rules/payee'; export type { RedeemerRule } from './rules/redeemer'; diff --git a/packages/7715-permission-types/src/permissions/types.ts b/packages/7715-permission-types/src/permissions/types.ts index 93b17360..d5f0a4ed 100644 --- a/packages/7715-permission-types/src/permissions/types.ts +++ b/packages/7715-permission-types/src/permissions/types.ts @@ -95,3 +95,18 @@ export type PermissionDecoder = { * A map of deployed contract names to addresses for one chain. */ export type DeployedContractsByName = Record; + +/** + * Makes all properties in an object type required recursively. + * This includes nested objects and arrays. + * Also removes undefined from union types. + */ +export type DeepRequired = TParent extends (infer U)[] + ? DeepRequired[] + : TParent extends object + ? { + [P in keyof TParent]-?: DeepRequired< + Exclude + >; + } + : Exclude; diff --git a/packages/7715-permission-types/test/permissions/caveats/erc20TokenStream.test.ts b/packages/7715-permission-types/test/permissions/caveats/erc20TokenStream.test.ts index db4fd7a9..3343dffe 100644 --- a/packages/7715-permission-types/test/permissions/caveats/erc20TokenStream.test.ts +++ b/packages/7715-permission-types/test/permissions/caveats/erc20TokenStream.test.ts @@ -5,16 +5,24 @@ import { import { bigIntToHex, type Hex } from '@metamask/utils'; import { describe, it, expect } from 'vitest'; -import { makePermissionDecoderConfigs } from '../../../src/permissions'; -import { makeErc20TokenStreamDecoderConfig } from '../../../src/permissions/caveats/erc20TokenStream'; +import { makePermissionDecoderConfigs} from '../../../src/permissions'; +import { + createErc20TokenStreamCaveats, + type Erc20TokenStreamEnforcers, + makeErc20TokenStreamDecoderConfig, +} from '../../../src/permissions/caveats/erc20TokenStream'; import { expiryRuleDecoder } from '../../../src/permissions/rules/expiry'; import { erc20PayeeRuleDecoder } from '../../../src/permissions/rules/payee'; import { redeemerRuleDecoder } from '../../../src/permissions/rules/redeemer'; -import type { ChecksumCaveat } from '../../../src/permissions/types'; +import type { + ChecksumCaveat, + DeepRequired, +} from '../../../src/permissions/types'; import { getChecksumEnforcersByChainId, ZERO_32_BYTES, } from '../../../src/permissions/utils'; +import type { Erc20TokenStreamPermission } from '../../../src/types'; import { toWord } from '../../test-utils'; describe('erc20-token-stream decoder config', () => { @@ -173,3 +181,55 @@ describe('erc20-token-stream decoder config', () => { }); }); }); + +describe('createErc20TokenStreamCaveats()', () => { + const initialAmount = '0x0de0b6b3a7640000' as const; + const maxAmount = '0x8ac7230489e80000' as const; + const amountPerSecond = '0x06f05b59d3b20000' as const; + const startTime = 1729900800; // 10/26/2024 00:00:00 UTC + const tokenAddress = '0x1234567890123456789012345678901234567890' as const; + + const contracts: Erc20TokenStreamEnforcers = { + erc20StreamingEnforcer: '0x7356Ed4321Ff9e7DAE246461829cDC170ff660Ab', + valueLteEnforcer: '0x5e12Ca712176E7557e4fAa1c8cc27382B60B5e39', + }; + + const mockPermission: DeepRequired = { + type: 'erc20-token-stream', + data: { + initialAmount, + maxAmount, + amountPerSecond, + startTime, + tokenAddress, + justification: 'test', + }, + isAdjustmentAllowed: true, + }; + + it('creates erc20Streaming and valueLte caveats', async () => { + const caveats = await createErc20TokenStreamCaveats({ + permission: mockPermission, + contracts, + }); + const initialAmountHex = initialAmount.slice(2).padStart(64, '0'); + const maxAmountHex = maxAmount.slice(2).padStart(64, '0'); + const amountPerSecondHex = amountPerSecond.slice(2).padStart(64, '0'); + const startTimeHex = startTime.toString(16).padStart(64, '0'); + const erc20StreamingExpectedTerms = `0x${tokenAddress.slice(2)}${initialAmountHex}${maxAmountHex}${amountPerSecondHex}${startTimeHex}`; + + expect(caveats).toStrictEqual([ + { + enforcer: contracts.erc20StreamingEnforcer, + terms: erc20StreamingExpectedTerms, + args: '0x', + }, + { + enforcer: contracts.valueLteEnforcer, + terms: + '0x0000000000000000000000000000000000000000000000000000000000000000', + args: '0x', + }, + ]); + }); +}); From 05387b538870e81d4090adc09009dfd209f71347 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Thu, 18 Jun 2026 16:11:05 +1200 Subject: [PATCH 2/4] Add caveat building logic for remaining permission types --- packages/7715-permission-types/src/index.ts | 13 +++- .../caveats/erc20TokenAllowance.ts | 52 ++++++++++++++++ .../permissions/caveats/erc20TokenPeriodic.ts | 54 +++++++++++++++- .../caveats/nativeTokenAllowance.ts | 51 +++++++++++++++ .../caveats/nativeTokenPeriodic.ts | 52 +++++++++++++++- .../permissions/caveats/nativeTokenStream.ts | 54 +++++++++++++++- .../caveats/tokenApprovalRevocation.ts | 39 +++++++++++- .../src/permissions/index.ts | 25 +++++++- .../caveats/erc20TokenAllowance.test.ts | 55 +++++++++++++++- .../caveats/erc20TokenPeriodic.test.ts | 57 ++++++++++++++++- .../caveats/erc20TokenStream.test.ts | 2 +- .../caveats/nativeTokenAllowance.test.ts | 53 +++++++++++++++- .../caveats/nativeTokenPeriodic.test.ts | 56 ++++++++++++++++- .../caveats/nativeTokenStream.test.ts | 62 ++++++++++++++++++- .../caveats/tokenApprovalRevocation.test.ts | 47 +++++++++++++- 15 files changed, 653 insertions(+), 19 deletions(-) diff --git a/packages/7715-permission-types/src/index.ts b/packages/7715-permission-types/src/index.ts index 1f1929c3..402e6c95 100644 --- a/packages/7715-permission-types/src/index.ts +++ b/packages/7715-permission-types/src/index.ts @@ -21,10 +21,21 @@ export type { export type { PayeeRule, RedeemerRule, ExpiryRule } from './permissions'; export { + createErc20TokenAllowanceCaveats, + type Erc20TokenAllowanceEnforcers, + createErc20TokenPeriodicCaveats, + type Erc20TokenPeriodicEnforcers, makePermissionDecoderConfigs, createErc20TokenStreamCaveats, type Erc20TokenStreamEnforcers, - type Erc20TokenStreamDecoderEnforcers, + createNativeTokenAllowanceCaveats, + type NativeTokenAllowanceEnforcers, + createNativeTokenPeriodicCaveats, + type NativeTokenPeriodicEnforcers, + createNativeTokenStreamCaveats, + type NativeTokenStreamEnforcers, + createTokenApprovalRevocationCaveats, + type TokenApprovalRevocationEnforcers, type DeployedContractsByName, type PermissionDecoderConfig, } from './permissions'; diff --git a/packages/7715-permission-types/src/permissions/caveats/erc20TokenAllowance.ts b/packages/7715-permission-types/src/permissions/caveats/erc20TokenAllowance.ts index 4fd9add8..d6707ec6 100644 --- a/packages/7715-permission-types/src/permissions/caveats/erc20TokenAllowance.ts +++ b/packages/7715-permission-types/src/permissions/caveats/erc20TokenAllowance.ts @@ -1,3 +1,8 @@ +import type { Caveat } from '@metamask/delegation-core'; +import { + createERC20TokenPeriodTransferTerms, + createValueLteTerms, +} from '@metamask/delegation-core'; import { hexToNumber } from '@metamask/utils'; import type { Erc20TokenAllowancePermission } from '../../types'; @@ -8,6 +13,7 @@ import type { ChecksumCaveat, ChecksumEnforcersByChainId, DecodedPermissionData, + DeepRequired, MakePermissionDecoderConfig, } from '../types'; import { @@ -111,3 +117,49 @@ function validateAndDecodeData( return { tokenAddress, allowanceAmount, startTime }; } + +/** + * Enforcers required to build ERC-20 token allowance caveats. + */ +export type Erc20TokenAllowanceEnforcers = Pick< + ChecksumEnforcersByChainId, + 'erc20PeriodicEnforcer' | 'valueLteEnforcer' +>; + +/** + * Builds the erc20-token-allowance caveats required for this permission type. + * + * @param options0 - Caveat builder arguments. + * @param options0.permission - Fully populated erc20-token-allowance permission data. + * @param options0.contracts - Enforcer addresses used to construct caveats. + * @returns The ERC-20 allowance and zero-value caveats. + */ +export async function createErc20TokenAllowanceCaveats({ + permission, + contracts, +}: { + permission: DeepRequired; + contracts: Erc20TokenAllowanceEnforcers; +}): Promise { + const { tokenAddress, allowanceAmount, startTime } = permission.data; + + const erc20PeriodCaveat: Caveat = { + enforcer: contracts.erc20PeriodicEnforcer, + terms: createERC20TokenPeriodTransferTerms({ + tokenAddress, + periodAmount: BigInt(allowanceAmount), + // delegation-core accepts bigint for encoding although the type is `number`. + periodDuration: BigInt(UINT256_MAX) as unknown as number, + startDate: startTime, + }), + args: '0x', + }; + + const valueLteCaveat: Caveat = { + enforcer: contracts.valueLteEnforcer, + terms: createValueLteTerms({ maxValue: 0n }), + args: '0x', + }; + + return [erc20PeriodCaveat, valueLteCaveat]; +} diff --git a/packages/7715-permission-types/src/permissions/caveats/erc20TokenPeriodic.ts b/packages/7715-permission-types/src/permissions/caveats/erc20TokenPeriodic.ts index 406ad93d..fdaaaf50 100644 --- a/packages/7715-permission-types/src/permissions/caveats/erc20TokenPeriodic.ts +++ b/packages/7715-permission-types/src/permissions/caveats/erc20TokenPeriodic.ts @@ -1,4 +1,9 @@ -import { decodeERC20TokenPeriodTransferTerms } from '@metamask/delegation-core'; +import type { Caveat } from '@metamask/delegation-core'; +import { + createERC20TokenPeriodTransferTerms, + createValueLteTerms, + decodeERC20TokenPeriodTransferTerms, +} from '@metamask/delegation-core'; import { bigIntToHex } from '@metamask/utils'; import type { Erc20TokenPeriodicPermission } from '../../types'; @@ -9,6 +14,7 @@ import type { ChecksumCaveat, ChecksumEnforcersByChainId, DecodedPermissionData, + DeepRequired, MakePermissionDecoderConfig, } from '../types'; import { @@ -117,3 +123,49 @@ function validateAndDecodeData( startTime, }; } + +/** + * Enforcers required to build ERC-20 token periodic caveats. + */ +export type Erc20TokenPeriodicEnforcers = Pick< + ChecksumEnforcersByChainId, + 'erc20PeriodicEnforcer' | 'valueLteEnforcer' +>; + +/** + * Builds the erc20-token-periodic caveats required for this permission type. + * + * @param options0 - Caveat builder arguments. + * @param options0.permission - Fully populated erc20-token-periodic permission data. + * @param options0.contracts - Enforcer addresses used to construct caveats. + * @returns The ERC-20 periodic and zero-value caveats. + */ +export async function createErc20TokenPeriodicCaveats({ + permission, + contracts, +}: { + permission: DeepRequired; + contracts: Erc20TokenPeriodicEnforcers; +}): Promise { + const { tokenAddress, periodAmount, periodDuration, startTime } = + permission.data; + + const erc20PeriodCaveat: Caveat = { + enforcer: contracts.erc20PeriodicEnforcer, + terms: createERC20TokenPeriodTransferTerms({ + tokenAddress, + periodAmount: BigInt(periodAmount), + periodDuration, + startDate: startTime, + }), + args: '0x', + }; + + const valueLteCaveat: Caveat = { + enforcer: contracts.valueLteEnforcer, + terms: createValueLteTerms({ maxValue: 0n }), + args: '0x', + }; + + return [erc20PeriodCaveat, valueLteCaveat]; +} diff --git a/packages/7715-permission-types/src/permissions/caveats/nativeTokenAllowance.ts b/packages/7715-permission-types/src/permissions/caveats/nativeTokenAllowance.ts index c3b141ec..192fb13e 100644 --- a/packages/7715-permission-types/src/permissions/caveats/nativeTokenAllowance.ts +++ b/packages/7715-permission-types/src/permissions/caveats/nativeTokenAllowance.ts @@ -1,3 +1,8 @@ +import type { Caveat } from '@metamask/delegation-core'; +import { + createExactCalldataTerms, + createNativeTokenPeriodTransferTerms, +} from '@metamask/delegation-core'; import { hexToNumber } from '@metamask/utils'; import type { NativeTokenAllowancePermission } from '../../types'; @@ -8,6 +13,7 @@ import type { ChecksumCaveat, ChecksumEnforcersByChainId, DecodedPermissionData, + DeepRequired, MakePermissionDecoderConfig, } from '../types'; import { @@ -115,3 +121,48 @@ function validateAndDecodeData( return { allowanceAmount, startTime }; } + +/** + * Enforcers required to build native token allowance caveats. + */ +export type NativeTokenAllowanceEnforcers = Pick< + ChecksumEnforcersByChainId, + 'nativeTokenPeriodicEnforcer' | 'exactCalldataEnforcer' +>; + +/** + * Builds the native-token-allowance caveats required for this permission type. + * + * @param options0 - Caveat builder arguments. + * @param options0.permission - Fully populated native-token-allowance permission data. + * @param options0.contracts - Enforcer addresses used to construct caveats. + * @returns The native token allowance and exact-calldata caveats. + */ +export async function createNativeTokenAllowanceCaveats({ + permission, + contracts, +}: { + permission: DeepRequired; + contracts: NativeTokenAllowanceEnforcers; +}): Promise { + const { allowanceAmount, startTime } = permission.data; + + const nativeTokenPeriodTransferCaveat: Caveat = { + enforcer: contracts.nativeTokenPeriodicEnforcer, + terms: createNativeTokenPeriodTransferTerms({ + periodAmount: BigInt(allowanceAmount), + // delegation-core accepts bigint for encoding although the type is `number`. + periodDuration: BigInt(UINT256_MAX) as unknown as number, + startDate: startTime, + }), + args: '0x', + }; + + const exactCalldataCaveat: Caveat = { + enforcer: contracts.exactCalldataEnforcer, + terms: createExactCalldataTerms({ calldata: '0x' }), + args: '0x', + }; + + return [nativeTokenPeriodTransferCaveat, exactCalldataCaveat]; +} diff --git a/packages/7715-permission-types/src/permissions/caveats/nativeTokenPeriodic.ts b/packages/7715-permission-types/src/permissions/caveats/nativeTokenPeriodic.ts index 98b2cccf..08841a9e 100644 --- a/packages/7715-permission-types/src/permissions/caveats/nativeTokenPeriodic.ts +++ b/packages/7715-permission-types/src/permissions/caveats/nativeTokenPeriodic.ts @@ -1,4 +1,9 @@ -import { decodeNativeTokenPeriodTransferTerms } from '@metamask/delegation-core'; +import type { Caveat } from '@metamask/delegation-core'; +import { + createExactCalldataTerms, + createNativeTokenPeriodTransferTerms, + decodeNativeTokenPeriodTransferTerms, +} from '@metamask/delegation-core'; import { bigIntToHex } from '@metamask/utils'; import type { NativeTokenPeriodicPermission } from '../../types'; @@ -9,6 +14,7 @@ import type { ChecksumCaveat, ChecksumEnforcersByChainId, DecodedPermissionData, + DeepRequired, MakePermissionDecoderConfig, } from '../types'; import { getTermsByEnforcer, MAX_PERIOD_DURATION } from '../utils'; @@ -112,3 +118,47 @@ function validateAndDecodeData( startTime, }; } + +/** + * Enforcers required to build native token periodic caveats. + */ +export type NativeTokenPeriodicEnforcers = Pick< + ChecksumEnforcersByChainId, + 'nativeTokenPeriodicEnforcer' | 'exactCalldataEnforcer' +>; + +/** + * Builds the native-token-periodic caveats required for this permission type. + * + * @param options0 - Caveat builder arguments. + * @param options0.permission - Fully populated native-token-periodic permission data. + * @param options0.contracts - Enforcer addresses used to construct caveats. + * @returns The native token periodic and exact-calldata caveats. + */ +export async function createNativeTokenPeriodicCaveats({ + permission, + contracts, +}: { + permission: DeepRequired; + contracts: NativeTokenPeriodicEnforcers; +}): Promise { + const { periodAmount, periodDuration, startTime } = permission.data; + + const nativeTokenPeriodTransferCaveat: Caveat = { + enforcer: contracts.nativeTokenPeriodicEnforcer, + terms: createNativeTokenPeriodTransferTerms({ + periodAmount: BigInt(periodAmount), + periodDuration, + startDate: startTime, + }), + args: '0x', + }; + + const exactCalldataCaveat: Caveat = { + enforcer: contracts.exactCalldataEnforcer, + terms: createExactCalldataTerms({ calldata: '0x' }), + args: '0x', + }; + + return [nativeTokenPeriodTransferCaveat, exactCalldataCaveat]; +} diff --git a/packages/7715-permission-types/src/permissions/caveats/nativeTokenStream.ts b/packages/7715-permission-types/src/permissions/caveats/nativeTokenStream.ts index 1c7a4ee7..0b6a4b11 100644 --- a/packages/7715-permission-types/src/permissions/caveats/nativeTokenStream.ts +++ b/packages/7715-permission-types/src/permissions/caveats/nativeTokenStream.ts @@ -1,4 +1,9 @@ -import { decodeNativeTokenStreamingTerms } from '@metamask/delegation-core'; +import type { Caveat } from '@metamask/delegation-core'; +import { + createExactCalldataTerms, + createNativeTokenStreamingTerms, + decodeNativeTokenStreamingTerms, +} from '@metamask/delegation-core'; import { bigIntToHex } from '@metamask/utils'; import type { NativeTokenStreamPermission } from '../../types'; @@ -9,6 +14,7 @@ import type { ChecksumCaveat, ChecksumEnforcersByChainId, DecodedPermissionData, + DeepRequired, MakePermissionDecoderConfig, } from '../types'; import { getTermsByEnforcer } from '../utils'; @@ -104,3 +110,49 @@ function validateAndDecodeData( startTime, }; } + +/** + * Enforcers required to build native token stream caveats. + */ +export type NativeTokenStreamEnforcers = Pick< + ChecksumEnforcersByChainId, + 'nativeTokenStreamingEnforcer' | 'exactCalldataEnforcer' +>; + +/** + * Builds the native-token-stream caveats required for this permission type. + * + * @param options0 - Caveat builder arguments. + * @param options0.permission - Fully populated native-token-stream permission data. + * @param options0.contracts - Enforcer addresses used to construct caveats. + * @returns The native token streaming and exact-calldata caveats. + */ +export async function createNativeTokenStreamCaveats({ + permission, + contracts, +}: { + permission: DeepRequired; + contracts: NativeTokenStreamEnforcers; +}): Promise { + const { initialAmount, maxAmount, amountPerSecond, startTime } = + permission.data; + + const nativeTokenStreamingCaveat: Caveat = { + enforcer: contracts.nativeTokenStreamingEnforcer, + terms: createNativeTokenStreamingTerms({ + initialAmount: BigInt(initialAmount), + maxAmount: BigInt(maxAmount), + amountPerSecond: BigInt(amountPerSecond), + startTime, + }), + args: '0x', + }; + + const exactCalldataCaveat: Caveat = { + enforcer: contracts.exactCalldataEnforcer, + terms: createExactCalldataTerms({ calldata: '0x' }), + args: '0x', + }; + + return [nativeTokenStreamingCaveat, exactCalldataCaveat]; +} diff --git a/packages/7715-permission-types/src/permissions/caveats/tokenApprovalRevocation.ts b/packages/7715-permission-types/src/permissions/caveats/tokenApprovalRevocation.ts index 54e74ca0..cc628fcc 100644 --- a/packages/7715-permission-types/src/permissions/caveats/tokenApprovalRevocation.ts +++ b/packages/7715-permission-types/src/permissions/caveats/tokenApprovalRevocation.ts @@ -1,4 +1,8 @@ -import { decodeApprovalRevocationTerms } from '@metamask/delegation-core'; +import type { Caveat } from '@metamask/delegation-core'; +import { + createApprovalRevocationTerms, + decodeApprovalRevocationTerms, +} from '@metamask/delegation-core'; import type { TokenApprovalRevocationPermission } from '../../types'; import { expiryRuleDecoder } from '../rules/expiry'; @@ -6,6 +10,7 @@ import type { ChecksumCaveat, ChecksumEnforcersByChainId, DecodedPermissionData, + DeepRequired, MakePermissionDecoderConfig, } from '../types'; import { getTermsByEnforcer } from '../utils'; @@ -73,3 +78,35 @@ function validateAndDecodeData( permit2InvalidateNonces, }; } + +/** + * Enforcers required to build token approval revocation caveats. + */ +export type TokenApprovalRevocationEnforcers = Pick< + ChecksumEnforcersByChainId, + 'approvalRevocationEnforcer' +>; + +/** + * Builds the token-approval-revocation caveat required for this permission type. + * + * @param options0 - Caveat builder arguments. + * @param options0.permission - Fully populated token-approval-revocation permission data. + * @param options0.contracts - Enforcer addresses used to construct caveats. + * @returns A single approval-revocation caveat. + */ +export async function createTokenApprovalRevocationCaveats({ + permission, + contracts, +}: { + permission: DeepRequired; + contracts: TokenApprovalRevocationEnforcers; +}): Promise { + const approvalRevocationCaveat: Caveat = { + enforcer: contracts.approvalRevocationEnforcer, + terms: createApprovalRevocationTerms(permission.data), + args: '0x', + }; + + return [approvalRevocationCaveat]; +} diff --git a/packages/7715-permission-types/src/permissions/index.ts b/packages/7715-permission-types/src/permissions/index.ts index 6471ce9a..676eb064 100644 --- a/packages/7715-permission-types/src/permissions/index.ts +++ b/packages/7715-permission-types/src/permissions/index.ts @@ -11,8 +11,31 @@ import { getChecksumEnforcersByChainId } from './utils'; export { createErc20TokenStreamCaveats, type Erc20TokenStreamEnforcers, - type Erc20TokenStreamDecoderEnforcers, } from './caveats/erc20TokenStream'; +export { + createErc20TokenPeriodicCaveats, + type Erc20TokenPeriodicEnforcers, +} from './caveats/erc20TokenPeriodic'; +export { + createErc20TokenAllowanceCaveats, + type Erc20TokenAllowanceEnforcers, +} from './caveats/erc20TokenAllowance'; +export { + createNativeTokenStreamCaveats, + type NativeTokenStreamEnforcers, +} from './caveats/nativeTokenStream'; +export { + createNativeTokenPeriodicCaveats, + type NativeTokenPeriodicEnforcers, +} from './caveats/nativeTokenPeriodic'; +export { + createNativeTokenAllowanceCaveats, + type NativeTokenAllowanceEnforcers, +} from './caveats/nativeTokenAllowance'; +export { + createTokenApprovalRevocationCaveats, + type TokenApprovalRevocationEnforcers, +} from './caveats/tokenApprovalRevocation'; export type { ExpiryRule } from './rules/expiry'; export type { PayeeRule } from './rules/payee'; diff --git a/packages/7715-permission-types/test/permissions/caveats/erc20TokenAllowance.test.ts b/packages/7715-permission-types/test/permissions/caveats/erc20TokenAllowance.test.ts index c8dc3e61..8a9612cd 100644 --- a/packages/7715-permission-types/test/permissions/caveats/erc20TokenAllowance.test.ts +++ b/packages/7715-permission-types/test/permissions/caveats/erc20TokenAllowance.test.ts @@ -6,16 +6,24 @@ import type { Hex } from '@metamask/utils'; import { describe, it, expect } from 'vitest'; import { makePermissionDecoderConfigs } from '../../../src/permissions'; -import { makeErc20TokenAllowanceDecoderConfig } from '../../../src/permissions/caveats/erc20TokenAllowance'; +import { + createErc20TokenAllowanceCaveats, + makeErc20TokenAllowanceDecoderConfig, + type Erc20TokenAllowanceEnforcers, +} from '../../../src/permissions/caveats/erc20TokenAllowance'; import { expiryRuleDecoder } from '../../../src/permissions/rules/expiry'; import { erc20PayeeRuleDecoder } from '../../../src/permissions/rules/payee'; import { redeemerRuleDecoder } from '../../../src/permissions/rules/redeemer'; -import type { ChecksumCaveat } from '../../../src/permissions/types'; +import type { + ChecksumCaveat, + DeepRequired, +} from '../../../src/permissions/types'; import { getChecksumEnforcersByChainId, UINT256_MAX, ZERO_32_BYTES, } from '../../../src/permissions/utils'; +import type { Erc20TokenAllowancePermission } from '../../../src/types'; import { toWord } from '../../test-utils'; describe('erc20-token-allowance decoder config', () => { @@ -162,3 +170,46 @@ describe('erc20-token-allowance decoder config', () => { }); }); }); + +describe('createErc20TokenAllowanceCaveats()', () => { + const tokenAddress = '0x1234567890123456789012345678901234567890' as const; + const allowanceAmount = '0x64' as const; + const startTime = 1729900800; + + const contracts: Erc20TokenAllowanceEnforcers = { + erc20PeriodicEnforcer: '0x7356Ed4321Ff9e7DAE246461829cDC170ff660Ab', + valueLteEnforcer: '0x5e12Ca712176E7557e4fAa1c8cc27382B60B5e39', + }; + + const permission: DeepRequired = { + type: 'erc20-token-allowance', + data: { + tokenAddress, + allowanceAmount, + startTime, + justification: 'test', + }, + isAdjustmentAllowed: true, + }; + + it('creates erc20Periodic and valueLte caveats', async () => { + const caveats = await createErc20TokenAllowanceCaveats({ + permission, + contracts, + }); + const expectedTerms = `0x${tokenAddress.slice(2)}${toWord(BigInt(allowanceAmount))}${UINT256_MAX.slice(2)}${toWord(startTime)}`; + + expect(caveats).toStrictEqual([ + { + enforcer: contracts.erc20PeriodicEnforcer, + terms: expectedTerms, + args: '0x', + }, + { + enforcer: contracts.valueLteEnforcer, + terms: ZERO_32_BYTES, + args: '0x', + }, + ]); + }); +}); diff --git a/packages/7715-permission-types/test/permissions/caveats/erc20TokenPeriodic.test.ts b/packages/7715-permission-types/test/permissions/caveats/erc20TokenPeriodic.test.ts index bc01703f..ad89dec2 100644 --- a/packages/7715-permission-types/test/permissions/caveats/erc20TokenPeriodic.test.ts +++ b/packages/7715-permission-types/test/permissions/caveats/erc20TokenPeriodic.test.ts @@ -6,16 +6,24 @@ import { bigIntToHex, type Hex } from '@metamask/utils'; import { describe, it, expect } from 'vitest'; import { makePermissionDecoderConfigs } from '../../../src/permissions'; -import { makeErc20TokenPeriodicDecoderConfig } from '../../../src/permissions/caveats/erc20TokenPeriodic'; +import { + createErc20TokenPeriodicCaveats, + makeErc20TokenPeriodicDecoderConfig, + type Erc20TokenPeriodicEnforcers, +} from '../../../src/permissions/caveats/erc20TokenPeriodic'; import { expiryRuleDecoder } from '../../../src/permissions/rules/expiry'; import { erc20PayeeRuleDecoder } from '../../../src/permissions/rules/payee'; import { redeemerRuleDecoder } from '../../../src/permissions/rules/redeemer'; -import type { ChecksumCaveat } from '../../../src/permissions/types'; +import type { + ChecksumCaveat, + DeepRequired, +} from '../../../src/permissions/types'; import { getChecksumEnforcersByChainId, MAX_PERIOD_DURATION, ZERO_32_BYTES, } from '../../../src/permissions/utils'; +import type { Erc20TokenPeriodicPermission } from '../../../src/types'; import { toWord } from '../../test-utils'; describe('erc20-token-periodic decoder config', () => { @@ -192,3 +200,48 @@ describe('erc20-token-periodic decoder config', () => { }); }); }); + +describe('createErc20TokenPeriodicCaveats()', () => { + const tokenAddress = '0x1234567890123456789012345678901234567890' as const; + const periodAmount = '0x64' as const; + const periodDuration = 86400; + const startTime = 1729900800; + + const contracts: Erc20TokenPeriodicEnforcers = { + erc20PeriodicEnforcer: '0x7356Ed4321Ff9e7DAE246461829cDC170ff660Ab', + valueLteEnforcer: '0x5e12Ca712176E7557e4fAa1c8cc27382B60B5e39', + }; + + const permission: DeepRequired = { + type: 'erc20-token-periodic', + data: { + tokenAddress, + periodAmount, + periodDuration, + startTime, + justification: 'test', + }, + isAdjustmentAllowed: true, + }; + + it('creates erc20Periodic and valueLte caveats', async () => { + const caveats = await createErc20TokenPeriodicCaveats({ + permission, + contracts, + }); + const expectedTerms = `0x${tokenAddress.slice(2)}${toWord(BigInt(periodAmount))}${toWord(periodDuration)}${toWord(startTime)}`; + + expect(caveats).toStrictEqual([ + { + enforcer: contracts.erc20PeriodicEnforcer, + terms: expectedTerms, + args: '0x', + }, + { + enforcer: contracts.valueLteEnforcer, + terms: ZERO_32_BYTES, + args: '0x', + }, + ]); + }); +}); diff --git a/packages/7715-permission-types/test/permissions/caveats/erc20TokenStream.test.ts b/packages/7715-permission-types/test/permissions/caveats/erc20TokenStream.test.ts index 3343dffe..6f838f8d 100644 --- a/packages/7715-permission-types/test/permissions/caveats/erc20TokenStream.test.ts +++ b/packages/7715-permission-types/test/permissions/caveats/erc20TokenStream.test.ts @@ -5,7 +5,7 @@ import { import { bigIntToHex, type Hex } from '@metamask/utils'; import { describe, it, expect } from 'vitest'; -import { makePermissionDecoderConfigs} from '../../../src/permissions'; +import { makePermissionDecoderConfigs } from '../../../src/permissions'; import { createErc20TokenStreamCaveats, type Erc20TokenStreamEnforcers, diff --git a/packages/7715-permission-types/test/permissions/caveats/nativeTokenAllowance.test.ts b/packages/7715-permission-types/test/permissions/caveats/nativeTokenAllowance.test.ts index 190580f7..b090492a 100644 --- a/packages/7715-permission-types/test/permissions/caveats/nativeTokenAllowance.test.ts +++ b/packages/7715-permission-types/test/permissions/caveats/nativeTokenAllowance.test.ts @@ -6,15 +6,23 @@ import type { Hex } from '@metamask/utils'; import { describe, it, expect } from 'vitest'; import { makePermissionDecoderConfigs } from '../../../src/permissions'; -import { makeNativeTokenAllowanceDecoderConfig } from '../../../src/permissions/caveats/nativeTokenAllowance'; +import { + createNativeTokenAllowanceCaveats, + makeNativeTokenAllowanceDecoderConfig, + type NativeTokenAllowanceEnforcers, +} from '../../../src/permissions/caveats/nativeTokenAllowance'; import { expiryRuleDecoder } from '../../../src/permissions/rules/expiry'; import { nativePayeeRuleDecoder } from '../../../src/permissions/rules/payee'; import { redeemerRuleDecoder } from '../../../src/permissions/rules/redeemer'; -import type { ChecksumCaveat } from '../../../src/permissions/types'; +import type { + ChecksumCaveat, + DeepRequired, +} from '../../../src/permissions/types'; import { getChecksumEnforcersByChainId, UINT256_MAX, } from '../../../src/permissions/utils'; +import type { NativeTokenAllowancePermission } from '../../../src/types'; import { toWord } from '../../test-utils'; describe('native-token-allowance decoder config', () => { @@ -160,3 +168,44 @@ describe('native-token-allowance decoder config', () => { }); }); }); + +describe('createNativeTokenAllowanceCaveats()', () => { + const allowanceAmount = '0x64' as const; + const startTime = 1729900800; + + const contracts: NativeTokenAllowanceEnforcers = { + nativeTokenPeriodicEnforcer: '0x7356Ed4321Ff9e7DAE246461829cDC170ff660Ab', + exactCalldataEnforcer: '0x5e12Ca712176E7557e4fAa1c8cc27382B60B5e39', + }; + + const permission: DeepRequired = { + type: 'native-token-allowance', + data: { + allowanceAmount, + startTime, + justification: 'test', + }, + isAdjustmentAllowed: true, + }; + + it('creates nativeTokenPeriodic and exactCalldata caveats', async () => { + const caveats = await createNativeTokenAllowanceCaveats({ + permission, + contracts, + }); + const expectedTerms = `0x${toWord(BigInt(allowanceAmount))}${UINT256_MAX.slice(2)}${toWord(startTime)}`; + + expect(caveats).toStrictEqual([ + { + enforcer: contracts.nativeTokenPeriodicEnforcer, + terms: expectedTerms, + args: '0x', + }, + { + enforcer: contracts.exactCalldataEnforcer, + terms: '0x', + args: '0x', + }, + ]); + }); +}); diff --git a/packages/7715-permission-types/test/permissions/caveats/nativeTokenPeriodic.test.ts b/packages/7715-permission-types/test/permissions/caveats/nativeTokenPeriodic.test.ts index 2d2cb904..3dac15cf 100644 --- a/packages/7715-permission-types/test/permissions/caveats/nativeTokenPeriodic.test.ts +++ b/packages/7715-permission-types/test/permissions/caveats/nativeTokenPeriodic.test.ts @@ -6,15 +6,23 @@ import { bigIntToHex, type Hex } from '@metamask/utils'; import { describe, it, expect } from 'vitest'; import { makePermissionDecoderConfigs } from '../../../src/permissions'; -import { makeNativeTokenPeriodicDecoderConfig } from '../../../src/permissions/caveats/nativeTokenPeriodic'; +import { + createNativeTokenPeriodicCaveats, + makeNativeTokenPeriodicDecoderConfig, + type NativeTokenPeriodicEnforcers, +} from '../../../src/permissions/caveats/nativeTokenPeriodic'; import { expiryRuleDecoder } from '../../../src/permissions/rules/expiry'; import { nativePayeeRuleDecoder } from '../../../src/permissions/rules/payee'; import { redeemerRuleDecoder } from '../../../src/permissions/rules/redeemer'; -import type { ChecksumCaveat } from '../../../src/permissions/types'; +import type { + ChecksumCaveat, + DeepRequired, +} from '../../../src/permissions/types'; import { getChecksumEnforcersByChainId, MAX_PERIOD_DURATION, } from '../../../src/permissions/utils'; +import type { NativeTokenPeriodicPermission } from '../../../src/types'; import { toWord } from '../../test-utils'; describe('native-token-periodic decoder config', () => { @@ -180,3 +188,47 @@ describe('native-token-periodic decoder config', () => { }); }); }); + +describe('createNativeTokenPeriodicCaveats()', () => { + const periodAmount = '0x64' as const; + const periodDuration = 86400; + const startTime = 1729900800; + + const contracts: NativeTokenPeriodicEnforcers = { + nativeTokenPeriodicEnforcer: '0x7356Ed4321Ff9e7DAE246461829cDC170ff660Ab', + exactCalldataEnforcer: '0x5e12Ca712176E7557e4fAa1c8cc27382B60B5e39', + }; + + const permission: DeepRequired = { + type: 'native-token-periodic', + data: { + periodAmount, + periodDuration, + startTime, + justification: 'test', + }, + isAdjustmentAllowed: true, + }; + + it('creates nativeTokenPeriodic and exactCalldata caveats', async () => { + const caveats = await createNativeTokenPeriodicCaveats({ + permission, + contracts, + }); + + const nativeTokenPeriodicExpectedTerms = `0x${toWord(BigInt(periodAmount))}${toWord(periodDuration)}${toWord(startTime)}`; + + expect(caveats).toStrictEqual([ + { + enforcer: contracts.nativeTokenPeriodicEnforcer, + terms: nativeTokenPeriodicExpectedTerms, + args: '0x', + }, + { + enforcer: contracts.exactCalldataEnforcer, + terms: '0x', + args: '0x', + }, + ]); + }); +}); diff --git a/packages/7715-permission-types/test/permissions/caveats/nativeTokenStream.test.ts b/packages/7715-permission-types/test/permissions/caveats/nativeTokenStream.test.ts index 5b198b41..0b79625c 100644 --- a/packages/7715-permission-types/test/permissions/caveats/nativeTokenStream.test.ts +++ b/packages/7715-permission-types/test/permissions/caveats/nativeTokenStream.test.ts @@ -6,12 +6,20 @@ import { bigIntToHex, type Hex } from '@metamask/utils'; import { describe, it, expect } from 'vitest'; import { makePermissionDecoderConfigs } from '../../../src/permissions'; -import { makeNativeTokenStreamDecoderConfig } from '../../../src/permissions/caveats/nativeTokenStream'; +import { + createNativeTokenStreamCaveats, + makeNativeTokenStreamDecoderConfig, + type NativeTokenStreamEnforcers, +} from '../../../src/permissions/caveats/nativeTokenStream'; import { expiryRuleDecoder } from '../../../src/permissions/rules/expiry'; import { nativePayeeRuleDecoder } from '../../../src/permissions/rules/payee'; import { redeemerRuleDecoder } from '../../../src/permissions/rules/redeemer'; -import type { ChecksumCaveat } from '../../../src/permissions/types'; +import type { + ChecksumCaveat, + DeepRequired, +} from '../../../src/permissions/types'; import { getChecksumEnforcersByChainId } from '../../../src/permissions/utils'; +import type { NativeTokenStreamPermission } from '../../../src/types'; import { toWord } from '../../test-utils'; describe('native-token-stream decoder config', () => { @@ -156,3 +164,53 @@ describe('native-token-stream decoder config', () => { }); }); }); + +describe('createNativeTokenStreamCaveats()', () => { + const initialAmount = '0x0de0b6b3a7640000' as const; + const maxAmount = '0x8ac7230489e80000' as const; + const amountPerSecond = '0x06f05b59d3b20000' as const; + const startTime = 1729900800; + + const contracts: NativeTokenStreamEnforcers = { + nativeTokenStreamingEnforcer: '0x7356Ed4321Ff9e7DAE246461829cDC170ff660Ab', + exactCalldataEnforcer: '0x5e12Ca712176E7557e4fAa1c8cc27382B60B5e39', + }; + + const permission: DeepRequired = { + type: 'native-token-stream', + data: { + initialAmount, + maxAmount, + amountPerSecond, + startTime, + justification: 'test', + }, + isAdjustmentAllowed: true, + }; + + it('creates nativeTokenStreaming and exactCalldata caveats', async () => { + const caveats = await createNativeTokenStreamCaveats({ + permission, + contracts, + }); + + const initialAmountHex = initialAmount.slice(2).padStart(64, '0'); + const maxAmountHex = maxAmount.slice(2).padStart(64, '0'); + const amountPerSecondHex = amountPerSecond.slice(2).padStart(64, '0'); + const startTimeHex = toWord(startTime); + const nativeTokenStreamingExpectedTerms = `0x${initialAmountHex}${maxAmountHex}${amountPerSecondHex}${startTimeHex}`; + + expect(caveats).toStrictEqual([ + { + enforcer: contracts.nativeTokenStreamingEnforcer, + terms: nativeTokenStreamingExpectedTerms, + args: '0x', + }, + { + enforcer: contracts.exactCalldataEnforcer, + terms: '0x', + args: '0x', + }, + ]); + }); +}); diff --git a/packages/7715-permission-types/test/permissions/caveats/tokenApprovalRevocation.test.ts b/packages/7715-permission-types/test/permissions/caveats/tokenApprovalRevocation.test.ts index cdffdd04..5c645ffe 100644 --- a/packages/7715-permission-types/test/permissions/caveats/tokenApprovalRevocation.test.ts +++ b/packages/7715-permission-types/test/permissions/caveats/tokenApprovalRevocation.test.ts @@ -6,10 +6,18 @@ import type { Hex } from '@metamask/utils'; import { describe, it, expect } from 'vitest'; import { makePermissionDecoderConfigs } from '../../../src/permissions'; -import { makeTokenApprovalRevocationDecoderConfig } from '../../../src/permissions/caveats/tokenApprovalRevocation'; +import { + createTokenApprovalRevocationCaveats, + makeTokenApprovalRevocationDecoderConfig, + type TokenApprovalRevocationEnforcers, +} from '../../../src/permissions/caveats/tokenApprovalRevocation'; import { expiryRuleDecoder } from '../../../src/permissions/rules/expiry'; -import type { ChecksumCaveat } from '../../../src/permissions/types'; +import type { + ChecksumCaveat, + DeepRequired, +} from '../../../src/permissions/types'; import { getChecksumEnforcersByChainId } from '../../../src/permissions/utils'; +import type { TokenApprovalRevocationPermission } from '../../../src/types'; describe('token-approval-revocation decoder config', () => { const chainId = CHAIN_ID.sepolia; @@ -123,3 +131,38 @@ describe('token-approval-revocation decoder config', () => { }); }); }); + +describe('createTokenApprovalRevocationCaveats()', () => { + const contracts: TokenApprovalRevocationEnforcers = { + approvalRevocationEnforcer: '0x7356Ed4321Ff9e7DAE246461829cDC170ff660Ab', + }; + + const permission: DeepRequired = { + type: 'token-approval-revocation', + data: { + erc20Approve: true, + erc721Approve: true, + erc721SetApprovalForAll: true, + permit2Approve: true, + permit2Lockdown: true, + permit2InvalidateNonces: true, + justification: 'test', + }, + isAdjustmentAllowed: true, + }; + + it('creates approvalRevocation caveat', async () => { + const caveats = await createTokenApprovalRevocationCaveats({ + permission, + contracts, + }); + + expect(caveats).toStrictEqual([ + { + enforcer: contracts.approvalRevocationEnforcer, + terms: '0x3f', + args: '0x', + }, + ]); + }); +}); From 4487ce953e0b5e05ce3c1461b0a8efc610d5ea76 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:30:27 +1200 Subject: [PATCH 3/4] Add additional test coverage --- .../caveats/erc20TokenAllowance.test.ts | 59 ++++++++++++++++++ .../caveats/erc20TokenPeriodic.test.ts | 60 ++++++++++++++++++ .../caveats/erc20TokenStream.test.ts | 61 +++++++++++++++++++ .../caveats/nativeTokenAllowance.test.ts | 37 +++++++++++ .../caveats/nativeTokenPeriodic.test.ts | 38 ++++++++++++ .../caveats/nativeTokenStream.test.ts | 39 ++++++++++++ .../caveats/tokenApprovalRevocation.test.ts | 53 ++++++++++++++++ 7 files changed, 347 insertions(+) diff --git a/packages/7715-permission-types/test/permissions/caveats/erc20TokenAllowance.test.ts b/packages/7715-permission-types/test/permissions/caveats/erc20TokenAllowance.test.ts index 8a9612cd..da12de12 100644 --- a/packages/7715-permission-types/test/permissions/caveats/erc20TokenAllowance.test.ts +++ b/packages/7715-permission-types/test/permissions/caveats/erc20TokenAllowance.test.ts @@ -212,4 +212,63 @@ describe('createErc20TokenAllowanceCaveats()', () => { }, ]); }); + + it('rejects malformed numeric hex input', async () => { + const invalidPermission = { + ...permission, + data: { + ...permission.data, + allowanceAmount: 'not-hex' as Hex, + }, + }; + + await expect( + createErc20TokenAllowanceCaveats({ + permission: invalidPermission, + contracts, + }), + ).rejects.toThrow(); + }); + + it('keeps valueLte caveat fixed at zero across varied inputs', async () => { + const variedPermission: DeepRequired = { + ...permission, + data: { + ...permission.data, + tokenAddress: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + allowanceAmount: + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', + startTime: 1, + }, + }; + + const caveats = await createErc20TokenAllowanceCaveats({ + permission: variedPermission, + contracts, + }); + + expect(caveats[1]?.enforcer).toBe(contracts.valueLteEnforcer); + expect(caveats[1]?.terms).toBe(ZERO_32_BYTES); + }); + + it('encodes provided token address in allowance terms', async () => { + const alternateTokenAddress = '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'; + const permissionWithAltToken = { + ...permission, + data: { + ...permission.data, + tokenAddress: alternateTokenAddress, + }, + }; + + const caveats = await createErc20TokenAllowanceCaveats({ + permission: permissionWithAltToken, + contracts, + }); + + expect(caveats[0]?.enforcer).toBe(contracts.erc20PeriodicEnforcer); + expect( + caveats[0]?.terms.startsWith(`0x${alternateTokenAddress.slice(2)}`), + ).toBe(true); + }); }); diff --git a/packages/7715-permission-types/test/permissions/caveats/erc20TokenPeriodic.test.ts b/packages/7715-permission-types/test/permissions/caveats/erc20TokenPeriodic.test.ts index ad89dec2..adf0be6e 100644 --- a/packages/7715-permission-types/test/permissions/caveats/erc20TokenPeriodic.test.ts +++ b/packages/7715-permission-types/test/permissions/caveats/erc20TokenPeriodic.test.ts @@ -244,4 +244,64 @@ describe('createErc20TokenPeriodicCaveats()', () => { }, ]); }); + + it('rejects malformed numeric hex input', async () => { + const invalidPermission = { + ...permission, + data: { + ...permission.data, + periodAmount: 'not-hex' as Hex, + }, + }; + + await expect( + createErc20TokenPeriodicCaveats({ + permission: invalidPermission, + contracts, + }), + ).rejects.toThrow(); + }); + + it('keeps valueLte caveat fixed at zero across varied inputs', async () => { + const variedPermission: DeepRequired = { + ...permission, + data: { + ...permission.data, + tokenAddress: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + periodAmount: + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', + periodDuration: 1, + startTime: 1, + }, + }; + + const caveats = await createErc20TokenPeriodicCaveats({ + permission: variedPermission, + contracts, + }); + + expect(caveats[1]?.enforcer).toBe(contracts.valueLteEnforcer); + expect(caveats[1]?.terms).toBe(ZERO_32_BYTES); + }); + + it('encodes provided token address in periodic terms', async () => { + const alternateTokenAddress = '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'; + const permissionWithAltToken = { + ...permission, + data: { + ...permission.data, + tokenAddress: alternateTokenAddress, + }, + }; + + const caveats = await createErc20TokenPeriodicCaveats({ + permission: permissionWithAltToken, + contracts, + }); + + expect(caveats[0]?.enforcer).toBe(contracts.erc20PeriodicEnforcer); + expect( + caveats[0]?.terms.startsWith(`0x${alternateTokenAddress.slice(2)}`), + ).toBe(true); + }); }); diff --git a/packages/7715-permission-types/test/permissions/caveats/erc20TokenStream.test.ts b/packages/7715-permission-types/test/permissions/caveats/erc20TokenStream.test.ts index 6f838f8d..a5a59103 100644 --- a/packages/7715-permission-types/test/permissions/caveats/erc20TokenStream.test.ts +++ b/packages/7715-permission-types/test/permissions/caveats/erc20TokenStream.test.ts @@ -232,4 +232,65 @@ describe('createErc20TokenStreamCaveats()', () => { }, ]); }); + + it('rejects malformed numeric hex input', async () => { + const invalidPermission = { + ...mockPermission, + data: { + ...mockPermission.data, + initialAmount: 'not-hex' as Hex, + }, + }; + + await expect( + createErc20TokenStreamCaveats({ + permission: invalidPermission, + contracts, + }), + ).rejects.toThrow(); + }); + + it('keeps valueLte caveat fixed at zero across varied inputs', async () => { + const variedPermission: DeepRequired = { + ...mockPermission, + data: { + ...mockPermission.data, + tokenAddress: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + initialAmount: '0x1', + maxAmount: + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', + amountPerSecond: '0x2', + startTime: 1, + }, + }; + + const caveats = await createErc20TokenStreamCaveats({ + permission: variedPermission, + contracts, + }); + + expect(caveats[1]?.enforcer).toBe(contracts.valueLteEnforcer); + expect(caveats[1]?.terms).toBe(ZERO_32_BYTES); + }); + + it('encodes provided token address in stream terms', async () => { + const alternateTokenAddress = '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'; + const permission = { + ...mockPermission, + data: { + ...mockPermission.data, + tokenAddress: alternateTokenAddress, + }, + }; + + const caveats = await createErc20TokenStreamCaveats({ + permission, + contracts, + }); + + expect(caveats[0]?.enforcer).toBe(contracts.erc20StreamingEnforcer); + expect( + caveats[0]?.terms.startsWith(`0x${alternateTokenAddress.slice(2)}`), + ).toBe(true); + }); }); diff --git a/packages/7715-permission-types/test/permissions/caveats/nativeTokenAllowance.test.ts b/packages/7715-permission-types/test/permissions/caveats/nativeTokenAllowance.test.ts index b090492a..df3848a7 100644 --- a/packages/7715-permission-types/test/permissions/caveats/nativeTokenAllowance.test.ts +++ b/packages/7715-permission-types/test/permissions/caveats/nativeTokenAllowance.test.ts @@ -208,4 +208,41 @@ describe('createNativeTokenAllowanceCaveats()', () => { }, ]); }); + + it('rejects malformed numeric hex input', async () => { + const invalidPermission = { + ...permission, + data: { + ...permission.data, + allowanceAmount: 'not-hex' as Hex, + }, + }; + + await expect( + createNativeTokenAllowanceCaveats({ + permission: invalidPermission, + contracts, + }), + ).rejects.toThrow(); + }); + + it('keeps exactCalldata caveat fixed across varied inputs', async () => { + const variedPermission: DeepRequired = { + ...permission, + data: { + ...permission.data, + allowanceAmount: + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', + startTime: 1, + }, + }; + + const caveats = await createNativeTokenAllowanceCaveats({ + permission: variedPermission, + contracts, + }); + + expect(caveats[1]?.enforcer).toBe(contracts.exactCalldataEnforcer); + expect(caveats[1]?.terms).toBe('0x'); + }); }); diff --git a/packages/7715-permission-types/test/permissions/caveats/nativeTokenPeriodic.test.ts b/packages/7715-permission-types/test/permissions/caveats/nativeTokenPeriodic.test.ts index 3dac15cf..b2a68658 100644 --- a/packages/7715-permission-types/test/permissions/caveats/nativeTokenPeriodic.test.ts +++ b/packages/7715-permission-types/test/permissions/caveats/nativeTokenPeriodic.test.ts @@ -231,4 +231,42 @@ describe('createNativeTokenPeriodicCaveats()', () => { }, ]); }); + + it('rejects malformed numeric hex input', async () => { + const invalidPermission = { + ...permission, + data: { + ...permission.data, + periodAmount: 'not-hex' as Hex, + }, + }; + + await expect( + createNativeTokenPeriodicCaveats({ + permission: invalidPermission, + contracts, + }), + ).rejects.toThrow(); + }); + + it('keeps exactCalldata caveat fixed across varied inputs', async () => { + const variedPermission: DeepRequired = { + ...permission, + data: { + ...permission.data, + periodAmount: + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', + periodDuration: 1, + startTime: 1, + }, + }; + + const caveats = await createNativeTokenPeriodicCaveats({ + permission: variedPermission, + contracts, + }); + + expect(caveats[1]?.enforcer).toBe(contracts.exactCalldataEnforcer); + expect(caveats[1]?.terms).toBe('0x'); + }); }); diff --git a/packages/7715-permission-types/test/permissions/caveats/nativeTokenStream.test.ts b/packages/7715-permission-types/test/permissions/caveats/nativeTokenStream.test.ts index 0b79625c..174424b1 100644 --- a/packages/7715-permission-types/test/permissions/caveats/nativeTokenStream.test.ts +++ b/packages/7715-permission-types/test/permissions/caveats/nativeTokenStream.test.ts @@ -213,4 +213,43 @@ describe('createNativeTokenStreamCaveats()', () => { }, ]); }); + + it('rejects malformed numeric hex input', async () => { + const invalidPermission = { + ...permission, + data: { + ...permission.data, + initialAmount: 'not-hex' as Hex, + }, + }; + + await expect( + createNativeTokenStreamCaveats({ + permission: invalidPermission, + contracts, + }), + ).rejects.toThrow(); + }); + + it('keeps exactCalldata caveat fixed across varied inputs', async () => { + const variedPermission: DeepRequired = { + ...permission, + data: { + ...permission.data, + initialAmount: '0x1', + maxAmount: + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', + amountPerSecond: '0x2', + startTime: 1, + }, + }; + + const caveats = await createNativeTokenStreamCaveats({ + permission: variedPermission, + contracts, + }); + + expect(caveats[1]?.enforcer).toBe(contracts.exactCalldataEnforcer); + expect(caveats[1]?.terms).toBe('0x'); + }); }); diff --git a/packages/7715-permission-types/test/permissions/caveats/tokenApprovalRevocation.test.ts b/packages/7715-permission-types/test/permissions/caveats/tokenApprovalRevocation.test.ts index 5c645ffe..25fc300c 100644 --- a/packages/7715-permission-types/test/permissions/caveats/tokenApprovalRevocation.test.ts +++ b/packages/7715-permission-types/test/permissions/caveats/tokenApprovalRevocation.test.ts @@ -165,4 +165,57 @@ describe('createTokenApprovalRevocationCaveats()', () => { }, ]); }); + + it('creates single-flag approvalRevocation caveat', async () => { + const singleFlagPermission: DeepRequired = + { + ...permission, + data: { + ...permission.data, + erc20Approve: true, + erc721Approve: false, + erc721SetApprovalForAll: false, + permit2Approve: false, + permit2Lockdown: false, + permit2InvalidateNonces: false, + }, + }; + + const caveats = await createTokenApprovalRevocationCaveats({ + permission: singleFlagPermission, + contracts, + }); + + expect(caveats).toStrictEqual([ + { + enforcer: contracts.approvalRevocationEnforcer, + terms: '0x01', + args: '0x', + }, + ]); + }); + + it('rejects empty-mask approvalRevocation when all flags are false', async () => { + const noFlagPermission: DeepRequired = { + ...permission, + data: { + ...permission.data, + erc20Approve: false, + erc721Approve: false, + erc721SetApprovalForAll: false, + permit2Approve: false, + permit2Lockdown: false, + permit2InvalidateNonces: false, + }, + }; + + await expect( + createTokenApprovalRevocationCaveats({ + permission: noFlagPermission, + contracts, + }), + ).rejects.toThrow( + 'Invalid ApprovalRevocation terms: at least one revocation primitive must be enabled', + ); + }); }); From 488e6ce5f64c854f88d43a43f03b31b59aa8768f Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:36:01 +1200 Subject: [PATCH 4/4] Add CHANGELOG entry --- packages/7715-permission-types/CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/7715-permission-types/CHANGELOG.md b/packages/7715-permission-types/CHANGELOG.md index c7993153..b4b3f870 100644 --- a/packages/7715-permission-types/CHANGELOG.md +++ b/packages/7715-permission-types/CHANGELOG.md @@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Utility functions to create caveats array for each permission type ([#265](https://github.com/MetaMask/smart-accounts-kit/pull/265)) + - `createErc20TokenStreamCaveats()` + - `createErc20TokenPeriodicCaveats()` + - `createErc20TokenAllowanceCaveats()` + - `createNativeTokenStreamCaveats()` + - `createNativeTokenPeriodicCaveats()` + - `createNativeTokenAllowanceCaveats()` + - `createTokenApprovalRevocationCaveats()` - Add `makePermissionDecoderConfigs` to resolve the `PermissionDecoderConfig`s to be used with @metamask/gator-permissions-controller ([#259](https://github.com/MetaMask/smart-accounts-kit/pull/259)) ## [0.7.1]