From 0ab943228b6761b753663ea4e017e2f8afae7bed Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Fri, 30 Jan 2026 11:00:24 +1300 Subject: [PATCH 1/4] Add caveat terms builders to delegation core for all existing caveat builders - rename src/utils.ts to src/internalUtils.ts to ensure these aren't accidentally added to external API --- .../src/caveats/allowedCalldata.ts | 2 +- .../src/caveats/allowedMethods.ts | 78 ++++++++++++ .../src/caveats/allowedTargets.ts | 62 ++++++++++ .../src/caveats/argsEqualityCheck.ts | 60 +++++++++ .../src/caveats/blockNumber.ts | 67 ++++++++++ .../delegation-core/src/caveats/deployed.ts | 76 ++++++++++++ .../src/caveats/erc1155BalanceChange.ts | 107 ++++++++++++++++ .../src/caveats/erc20BalanceChange.ts | 98 +++++++++++++++ .../src/caveats/erc20Streaming.ts | 2 +- .../src/caveats/erc20TokenPeriodTransfer.ts | 2 +- .../src/caveats/erc20TransferAmount.ts | 65 ++++++++++ .../src/caveats/erc721BalanceChange.ts | 98 +++++++++++++++ .../src/caveats/erc721Transfer.ts | 65 ++++++++++ .../src/caveats/exactCalldataBatch.ts | 88 ++++++++++++++ .../src/caveats/exactExecution.ts | 79 ++++++++++++ .../src/caveats/exactExecutionBatch.ts | 88 ++++++++++++++ packages/delegation-core/src/caveats/id.ts | 73 +++++++++++ packages/delegation-core/src/caveats/index.ts | 22 ++++ .../src/caveats/limitedCalls.ts | 58 +++++++++ .../src/caveats/multiTokenPeriod.ts | 91 ++++++++++++++ .../src/caveats/nativeBalanceChange.ts | 82 +++++++++++++ .../src/caveats/nativeTokenPayment.ts | 69 +++++++++++ .../src/caveats/nativeTokenPeriodTransfer.ts | 2 +- .../src/caveats/nativeTokenStreaming.ts | 2 +- .../src/caveats/nativeTokenTransferAmount.ts | 54 +++++++++ .../src/caveats/ownershipTransfer.ts | 56 +++++++++ .../delegation-core/src/caveats/redeemer.ts | 62 ++++++++++ .../specificActionERC20TransferBatch.ts | 90 ++++++++++++++ .../delegation-core/src/caveats/timestamp.ts | 2 +- packages/delegation-core/src/caveats/types.ts | 4 + .../delegation-core/src/caveats/valueLte.ts | 2 +- packages/delegation-core/src/index.ts | 22 ++++ packages/delegation-core/src/internalUtils.ts | 114 ++++++++++++++++++ packages/delegation-core/src/utils.ts | 22 ---- .../test/caveats/allowedCalldata.test.ts | 2 +- .../test/caveats/allowedMethods.test.ts | 52 ++++++++ .../test/caveats/allowedTargets.test.ts | 40 ++++++ .../test/caveats/argsEqualityCheck.test.ts | 32 +++++ .../test/caveats/blockNumber.test.ts | 42 +++++++ .../test/caveats/deployed.test.ts | 59 +++++++++ .../test/caveats/erc1155BalanceChange.test.ts | 67 ++++++++++ .../test/caveats/erc20BalanceChange.test.ts | 62 ++++++++++ .../test/caveats/erc20TransferAmount.test.ts | 47 ++++++++ .../test/caveats/erc721BalanceChange.test.ts | 62 ++++++++++ .../test/caveats/erc721Transfer.test.ts | 47 ++++++++ .../test/caveats/exactCalldataBatch.test.ts | 47 ++++++++ .../test/caveats/exactExecution.test.ts | 75 ++++++++++++ .../test/caveats/exactExecutionBatch.test.ts | 51 ++++++++ .../delegation-core/test/caveats/id.test.ts | 46 +++++++ .../test/caveats/limitedCalls.test.ts | 32 +++++ .../test/caveats/multiTokenPeriod.test.ts | 82 +++++++++++++ .../test/caveats/nativeBalanceChange.test.ts | 66 ++++++++++ .../test/caveats/nativeTokenPayment.test.ts | 47 ++++++++ .../caveats/nativeTokenTransferAmount.test.ts | 37 ++++++ .../test/caveats/ownershipTransfer.test.ts | 29 +++++ .../test/caveats/redeemer.test.ts | 38 ++++++ .../specificActionERC20TransferBatch.test.ts | 67 ++++++++++ .../test/internalUtils.test.ts | 87 +++++++++++++ 58 files changed, 3050 insertions(+), 30 deletions(-) create mode 100644 packages/delegation-core/src/caveats/allowedMethods.ts create mode 100644 packages/delegation-core/src/caveats/allowedTargets.ts create mode 100644 packages/delegation-core/src/caveats/argsEqualityCheck.ts create mode 100644 packages/delegation-core/src/caveats/blockNumber.ts create mode 100644 packages/delegation-core/src/caveats/deployed.ts create mode 100644 packages/delegation-core/src/caveats/erc1155BalanceChange.ts create mode 100644 packages/delegation-core/src/caveats/erc20BalanceChange.ts create mode 100644 packages/delegation-core/src/caveats/erc20TransferAmount.ts create mode 100644 packages/delegation-core/src/caveats/erc721BalanceChange.ts create mode 100644 packages/delegation-core/src/caveats/erc721Transfer.ts create mode 100644 packages/delegation-core/src/caveats/exactCalldataBatch.ts create mode 100644 packages/delegation-core/src/caveats/exactExecution.ts create mode 100644 packages/delegation-core/src/caveats/exactExecutionBatch.ts create mode 100644 packages/delegation-core/src/caveats/id.ts create mode 100644 packages/delegation-core/src/caveats/limitedCalls.ts create mode 100644 packages/delegation-core/src/caveats/multiTokenPeriod.ts create mode 100644 packages/delegation-core/src/caveats/nativeBalanceChange.ts create mode 100644 packages/delegation-core/src/caveats/nativeTokenPayment.ts create mode 100644 packages/delegation-core/src/caveats/nativeTokenTransferAmount.ts create mode 100644 packages/delegation-core/src/caveats/ownershipTransfer.ts create mode 100644 packages/delegation-core/src/caveats/redeemer.ts create mode 100644 packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts create mode 100644 packages/delegation-core/src/caveats/types.ts create mode 100644 packages/delegation-core/src/internalUtils.ts delete mode 100644 packages/delegation-core/src/utils.ts create mode 100644 packages/delegation-core/test/caveats/allowedMethods.test.ts create mode 100644 packages/delegation-core/test/caveats/allowedTargets.test.ts create mode 100644 packages/delegation-core/test/caveats/argsEqualityCheck.test.ts create mode 100644 packages/delegation-core/test/caveats/blockNumber.test.ts create mode 100644 packages/delegation-core/test/caveats/deployed.test.ts create mode 100644 packages/delegation-core/test/caveats/erc1155BalanceChange.test.ts create mode 100644 packages/delegation-core/test/caveats/erc20BalanceChange.test.ts create mode 100644 packages/delegation-core/test/caveats/erc20TransferAmount.test.ts create mode 100644 packages/delegation-core/test/caveats/erc721BalanceChange.test.ts create mode 100644 packages/delegation-core/test/caveats/erc721Transfer.test.ts create mode 100644 packages/delegation-core/test/caveats/exactCalldataBatch.test.ts create mode 100644 packages/delegation-core/test/caveats/exactExecution.test.ts create mode 100644 packages/delegation-core/test/caveats/exactExecutionBatch.test.ts create mode 100644 packages/delegation-core/test/caveats/id.test.ts create mode 100644 packages/delegation-core/test/caveats/limitedCalls.test.ts create mode 100644 packages/delegation-core/test/caveats/multiTokenPeriod.test.ts create mode 100644 packages/delegation-core/test/caveats/nativeBalanceChange.test.ts create mode 100644 packages/delegation-core/test/caveats/nativeTokenPayment.test.ts create mode 100644 packages/delegation-core/test/caveats/nativeTokenTransferAmount.test.ts create mode 100644 packages/delegation-core/test/caveats/ownershipTransfer.test.ts create mode 100644 packages/delegation-core/test/caveats/redeemer.test.ts create mode 100644 packages/delegation-core/test/caveats/specificActionERC20TransferBatch.test.ts create mode 100644 packages/delegation-core/test/internalUtils.test.ts diff --git a/packages/delegation-core/src/caveats/allowedCalldata.ts b/packages/delegation-core/src/caveats/allowedCalldata.ts index b02e2252..c2a5a180 100644 --- a/packages/delegation-core/src/caveats/allowedCalldata.ts +++ b/packages/delegation-core/src/caveats/allowedCalldata.ts @@ -1,5 +1,6 @@ import { bytesToHex, remove0x, type BytesLike } from '@metamask/utils'; +import { toHexString } from '../internalUtils'; import { defaultOptions, prepareResult, @@ -7,7 +8,6 @@ import { type ResultValue, } from '../returns'; import type { Hex } from '../types'; -import { toHexString } from '../utils'; /** * Terms for configuring an AllowedCalldata caveat. diff --git a/packages/delegation-core/src/caveats/allowedMethods.ts b/packages/delegation-core/src/caveats/allowedMethods.ts new file mode 100644 index 00000000..6257cd17 --- /dev/null +++ b/packages/delegation-core/src/caveats/allowedMethods.ts @@ -0,0 +1,78 @@ +import { bytesToHex, isHexString, type BytesLike } from '@metamask/utils'; + +import { concatHex } from '../internalUtils'; +import { + defaultOptions, + prepareResult, + type EncodingOptions, + type ResultValue, +} from '../returns'; +import type { Hex } from '../types'; + +/** + * Terms for configuring an AllowedMethods caveat. + */ +export type AllowedMethodsTerms = { + /** An array of 4-byte method selectors that the delegate is allowed to call. */ + selectors: BytesLike[]; +}; + +const FUNCTION_SELECTOR_STRING_LENGTH = 10; // 0x + 8 hex chars +const INVALID_SELECTOR_ERROR = + 'Invalid selector: must be a 4 byte hex string, abi function signature, or AbiFunction'; + +/** + * Creates terms for an AllowedMethods caveat that restricts calls to a set of method selectors. + * + * @param terms - The terms for the AllowedMethods caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as concatenated method selectors. + * @throws Error if the selectors array is empty or contains invalid selectors. + */ +export function createAllowedMethodsTerms( + terms: AllowedMethodsTerms, + encodingOptions?: EncodingOptions<'hex'>, +): Hex; +export function createAllowedMethodsTerms( + terms: AllowedMethodsTerms, + encodingOptions: EncodingOptions<'bytes'>, +): Uint8Array; +/** + * Creates terms for an AllowedMethods caveat that restricts calls to a set of method selectors. + * + * @param terms - The terms for the AllowedMethods caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as concatenated method selectors. + * @throws Error if the selectors array is empty or contains invalid selectors. + */ +export function createAllowedMethodsTerms( + terms: AllowedMethodsTerms, + encodingOptions: EncodingOptions = defaultOptions, +): Hex | Uint8Array { + const { selectors } = terms; + + if (selectors.length === 0) { + throw new Error('Invalid selectors: must provide at least one selector'); + } + + const normalizedSelectors = selectors.map((selector) => { + if (typeof selector === 'string') { + if ( + isHexString(selector) && + selector.length === FUNCTION_SELECTOR_STRING_LENGTH + ) { + return selector; + } + throw new Error(INVALID_SELECTOR_ERROR); + } + + if (selector.length !== 4) { + throw new Error(INVALID_SELECTOR_ERROR); + } + + return bytesToHex(selector); + }); + + const hexValue = concatHex(normalizedSelectors); + return prepareResult(hexValue, encodingOptions); +} diff --git a/packages/delegation-core/src/caveats/allowedTargets.ts b/packages/delegation-core/src/caveats/allowedTargets.ts new file mode 100644 index 00000000..3921b503 --- /dev/null +++ b/packages/delegation-core/src/caveats/allowedTargets.ts @@ -0,0 +1,62 @@ +import type { BytesLike } from '@metamask/utils'; + +import { concatHex, normalizeAddress } from '../internalUtils'; +import { + defaultOptions, + prepareResult, + type EncodingOptions, + type ResultValue, +} from '../returns'; +import type { Hex } from '../types'; + +/** + * Terms for configuring an AllowedTargets caveat. + */ +export type AllowedTargetsTerms = { + /** An array of target addresses that the delegate is allowed to call. */ + targets: BytesLike[]; +}; + +/** + * Creates terms for an AllowedTargets caveat that restricts calls to a set of target addresses. + * + * @param terms - The terms for the AllowedTargets caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as concatenated target addresses. + * @throws Error if the targets array is empty or contains invalid addresses. + */ +export function createAllowedTargetsTerms( + terms: AllowedTargetsTerms, + encodingOptions?: EncodingOptions<'hex'>, +): Hex; +export function createAllowedTargetsTerms( + terms: AllowedTargetsTerms, + encodingOptions: EncodingOptions<'bytes'>, +): Uint8Array; +/** + * Creates terms for an AllowedTargets caveat that restricts calls to a set of target addresses. + * + * @param terms - The terms for the AllowedTargets caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as concatenated target addresses. + * @throws Error if the targets array is empty or contains invalid addresses. + */ +export function createAllowedTargetsTerms( + terms: AllowedTargetsTerms, + encodingOptions: EncodingOptions = defaultOptions, +): Hex | Uint8Array { + const { targets } = terms; + + if (targets.length === 0) { + throw new Error( + 'Invalid targets: must provide at least one target address', + ); + } + + const normalizedTargets = targets.map((target) => + normalizeAddress(target, 'Invalid targets: must be valid addresses'), + ); + + const hexValue = concatHex(normalizedTargets); + return prepareResult(hexValue, encodingOptions); +} diff --git a/packages/delegation-core/src/caveats/argsEqualityCheck.ts b/packages/delegation-core/src/caveats/argsEqualityCheck.ts new file mode 100644 index 00000000..4be828cb --- /dev/null +++ b/packages/delegation-core/src/caveats/argsEqualityCheck.ts @@ -0,0 +1,60 @@ +import type { BytesLike } from '@metamask/utils'; + +import { normalizeHex } from '../internalUtils'; +import { + defaultOptions, + prepareResult, + type EncodingOptions, + type ResultValue, +} from '../returns'; +import type { Hex } from '../types'; + +/** + * Terms for configuring an ArgsEqualityCheck caveat. + */ +export type ArgsEqualityCheckTerms = { + /** The expected args that must match exactly when redeeming the delegation. */ + args: BytesLike; +}; + +/** + * Creates terms for an ArgsEqualityCheck caveat that requires exact args matching. + * + * @param terms - The terms for the ArgsEqualityCheck caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as the args themselves. + * @throws Error if args is not a valid hex string. + */ +export function createArgsEqualityCheckTerms( + terms: ArgsEqualityCheckTerms, + encodingOptions?: EncodingOptions<'hex'>, +): Hex; +export function createArgsEqualityCheckTerms( + terms: ArgsEqualityCheckTerms, + encodingOptions: EncodingOptions<'bytes'>, +): Uint8Array; +/** + * Creates terms for an ArgsEqualityCheck caveat that requires exact args matching. + * + * @param terms - The terms for the ArgsEqualityCheck caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as the args themselves. + * @throws Error if args is not a valid hex string. + */ +export function createArgsEqualityCheckTerms( + terms: ArgsEqualityCheckTerms, + encodingOptions: EncodingOptions = defaultOptions, +): Hex | Uint8Array { + const { args } = terms; + + if (typeof args === 'string' && args === '0x') { + return prepareResult(args, encodingOptions); + } + + const hexValue = normalizeHex( + args, + 'Invalid config: args must be a valid hex string', + ); + + return prepareResult(hexValue, encodingOptions); +} diff --git a/packages/delegation-core/src/caveats/blockNumber.ts b/packages/delegation-core/src/caveats/blockNumber.ts new file mode 100644 index 00000000..8b8ff930 --- /dev/null +++ b/packages/delegation-core/src/caveats/blockNumber.ts @@ -0,0 +1,67 @@ +import { toHexString } from '../internalUtils'; +import { + defaultOptions, + prepareResult, + type EncodingOptions, + type ResultValue, +} from '../returns'; +import type { Hex } from '../types'; + +/** + * Terms for configuring a BlockNumber caveat. + */ +export type BlockNumberTerms = { + /** The block number after which the delegation is valid. Set to 0n to disable. */ + afterThreshold: bigint; + /** The block number before which the delegation is valid. Set to 0n to disable. */ + beforeThreshold: bigint; +}; + +/** + * Creates terms for a BlockNumber caveat that constrains delegation validity by block range. + * + * @param terms - The terms for the BlockNumber caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as a 32-byte hex string (16 bytes for each threshold). + * @throws Error if both thresholds are zero or if afterThreshold >= beforeThreshold when both are set. + */ +export function createBlockNumberTerms( + terms: BlockNumberTerms, + encodingOptions?: EncodingOptions<'hex'>, +): Hex; +export function createBlockNumberTerms( + terms: BlockNumberTerms, + encodingOptions: EncodingOptions<'bytes'>, +): Uint8Array; +/** + * Creates terms for a BlockNumber caveat that constrains delegation validity by block range. + * + * @param terms - The terms for the BlockNumber caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as a 32-byte hex string (16 bytes for each threshold). + * @throws Error if both thresholds are zero or if afterThreshold >= beforeThreshold when both are set. + */ +export function createBlockNumberTerms( + terms: BlockNumberTerms, + encodingOptions: EncodingOptions = defaultOptions, +): Hex | Uint8Array { + const { afterThreshold, beforeThreshold } = terms; + + if (afterThreshold === 0n && beforeThreshold === 0n) { + throw new Error( + 'Invalid thresholds: At least one of afterThreshold or beforeThreshold must be specified', + ); + } + + if (beforeThreshold !== 0n && afterThreshold >= beforeThreshold) { + throw new Error( + 'Invalid thresholds: afterThreshold must be less than beforeThreshold if both are specified', + ); + } + + const afterThresholdHex = toHexString({ value: afterThreshold, size: 16 }); + const beforeThresholdHex = toHexString({ value: beforeThreshold, size: 16 }); + const hexValue = `0x${afterThresholdHex}${beforeThresholdHex}`; + + return prepareResult(hexValue, encodingOptions); +} diff --git a/packages/delegation-core/src/caveats/deployed.ts b/packages/delegation-core/src/caveats/deployed.ts new file mode 100644 index 00000000..5e573489 --- /dev/null +++ b/packages/delegation-core/src/caveats/deployed.ts @@ -0,0 +1,76 @@ +import type { BytesLike } from '@metamask/utils'; +import { remove0x } from '@metamask/utils'; + +import { concatHex, normalizeAddress, normalizeHex } from '../internalUtils'; +import { + defaultOptions, + prepareResult, + type EncodingOptions, + type ResultValue, +} from '../returns'; +import type { Hex } from '../types'; + +/** + * Terms for configuring a Deployed caveat. + */ +export type DeployedTerms = { + /** The contract address. */ + contractAddress: BytesLike; + /** The deployment salt. */ + salt: BytesLike; + /** The contract bytecode. */ + bytecode: BytesLike; +}; + +/** + * Creates terms for a Deployed caveat that constrains deployments by address, salt, and bytecode. + * + * @param terms - The terms for the Deployed caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as concatenated contractAddress + salt (32 bytes) + bytecode. + * @throws Error if the contract address, salt, or bytecode is invalid. + */ +export function createDeployedTerms( + terms: DeployedTerms, + encodingOptions?: EncodingOptions<'hex'>, +): Hex; +export function createDeployedTerms( + terms: DeployedTerms, + encodingOptions: EncodingOptions<'bytes'>, +): Uint8Array; +/** + * Creates terms for a Deployed caveat that constrains deployments by address, salt, and bytecode. + * + * @param terms - The terms for the Deployed caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as concatenated contractAddress + salt (32 bytes) + bytecode. + * @throws Error if the contract address, salt, or bytecode is invalid. + */ +export function createDeployedTerms( + terms: DeployedTerms, + encodingOptions: EncodingOptions = defaultOptions, +): Hex | Uint8Array { + const { contractAddress, salt, bytecode } = terms; + + const contractAddressHex = normalizeAddress( + contractAddress, + 'Invalid contractAddress: must be a valid Ethereum address', + ); + const saltHex = normalizeHex( + salt, + 'Invalid salt: must be a valid hexadecimal string', + ); + const bytecodeHex = normalizeHex( + bytecode, + 'Invalid bytecode: must be a valid hexadecimal string', + ); + + const unprefixedSalt = remove0x(saltHex); + if (unprefixedSalt.length > 64) { + throw new Error('Invalid salt: must be a valid hexadecimal string'); + } + const paddedSalt = `0x${unprefixedSalt.padStart(64, '0')}`; + + const hexValue = concatHex([contractAddressHex, paddedSalt, bytecodeHex]); + return prepareResult(hexValue, encodingOptions); +} diff --git a/packages/delegation-core/src/caveats/erc1155BalanceChange.ts b/packages/delegation-core/src/caveats/erc1155BalanceChange.ts new file mode 100644 index 00000000..b54561da --- /dev/null +++ b/packages/delegation-core/src/caveats/erc1155BalanceChange.ts @@ -0,0 +1,107 @@ +import type { BytesLike } from '@metamask/utils'; + +import { + concatHex, + normalizeAddressLowercase, + toHexString, +} from '../internalUtils'; +import { + defaultOptions, + prepareResult, + type EncodingOptions, + type ResultValue, +} from '../returns'; +import type { Hex } from '../types'; +import { BalanceChangeType } from './types'; + +/** + * Terms for configuring an ERC1155BalanceChange caveat. + */ +export type ERC1155BalanceChangeTerms = { + /** The ERC-1155 token address. */ + tokenAddress: BytesLike; + /** The recipient address. */ + recipient: BytesLike; + /** The token id. */ + tokenId: bigint; + /** The balance change amount. */ + balance: bigint; + /** The balance change type. */ + changeType: number; +}; + +/** + * Creates terms for an ERC1155BalanceChange caveat that checks token balance changes. + * + * @param terms - The terms for the ERC1155BalanceChange caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as changeType + tokenAddress + recipient + tokenId + balance. + * @throws Error if any parameter is invalid. + */ +export function createERC1155BalanceChangeTerms( + terms: ERC1155BalanceChangeTerms, + encodingOptions?: EncodingOptions<'hex'>, +): Hex; +export function createERC1155BalanceChangeTerms( + terms: ERC1155BalanceChangeTerms, + encodingOptions: EncodingOptions<'bytes'>, +): Uint8Array; +/** + * Creates terms for an ERC1155BalanceChange caveat that checks token balance changes. + * + * @param terms - The terms for the ERC1155BalanceChange caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as changeType + tokenAddress + recipient + tokenId + balance. + * @throws Error if any parameter is invalid. + */ +export function createERC1155BalanceChangeTerms( + terms: ERC1155BalanceChangeTerms, + encodingOptions: EncodingOptions = defaultOptions, +): Hex | Uint8Array { + const { + tokenAddress, + recipient, + tokenId, + balance, + changeType: changeTypeNumber, + } = terms; + + const tokenAddressHex = normalizeAddressLowercase( + tokenAddress, + 'Invalid tokenAddress: must be a valid address', + ); + const recipientHex = normalizeAddressLowercase( + recipient, + 'Invalid recipient: must be a valid address', + ); + + if (balance <= 0n) { + throw new Error('Invalid balance: must be a positive number'); + } + + if (tokenId < 0n) { + throw new Error('Invalid tokenId: must be a non-negative number'); + } + + const changeType = changeTypeNumber as BalanceChangeType; + + if ( + changeType !== BalanceChangeType.Increase && + changeType !== BalanceChangeType.Decrease + ) { + throw new Error('Invalid changeType: must be either Increase or Decrease'); + } + + const changeTypeHex = `0x${toHexString({ value: changeType, size: 1 })}`; + const tokenIdHex = `0x${toHexString({ value: tokenId, size: 32 })}`; + const balanceHex = `0x${toHexString({ value: balance, size: 32 })}`; + const hexValue = concatHex([ + changeTypeHex, + tokenAddressHex, + recipientHex, + tokenIdHex, + balanceHex, + ]); + + return prepareResult(hexValue, encodingOptions); +} diff --git a/packages/delegation-core/src/caveats/erc20BalanceChange.ts b/packages/delegation-core/src/caveats/erc20BalanceChange.ts new file mode 100644 index 00000000..5f9bffa1 --- /dev/null +++ b/packages/delegation-core/src/caveats/erc20BalanceChange.ts @@ -0,0 +1,98 @@ +import type { BytesLike } from '@metamask/utils'; + +import { + concatHex, + normalizeAddressLowercase, + toHexString, +} from '../internalUtils'; +import { + defaultOptions, + prepareResult, + type EncodingOptions, + type ResultValue, +} from '../returns'; +import type { Hex } from '../types'; +import { BalanceChangeType } from './types'; + +/** + * Terms for configuring an ERC20BalanceChange caveat. + */ +export type ERC20BalanceChangeTerms = { + /** The ERC-20 token address. */ + tokenAddress: BytesLike; + /** The recipient address. */ + recipient: BytesLike; + /** The balance change amount. */ + balance: bigint; + /** The balance change type. */ + changeType: number; +}; + +/** + * Creates terms for an ERC20BalanceChange caveat that checks token balance changes. + * + * @param terms - The terms for the ERC20BalanceChange caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as changeType + tokenAddress + recipient + balance. + * @throws Error if any parameter is invalid. + */ +export function createERC20BalanceChangeTerms( + terms: ERC20BalanceChangeTerms, + encodingOptions?: EncodingOptions<'hex'>, +): Hex; +export function createERC20BalanceChangeTerms( + terms: ERC20BalanceChangeTerms, + encodingOptions: EncodingOptions<'bytes'>, +): Uint8Array; +/** + * Creates terms for an ERC20BalanceChange caveat that checks token balance changes. + * + * @param terms - The terms for the ERC20BalanceChange caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as changeType + tokenAddress + recipient + balance. + * @throws Error if any parameter is invalid. + */ +export function createERC20BalanceChangeTerms( + terms: ERC20BalanceChangeTerms, + encodingOptions: EncodingOptions = defaultOptions, +): Hex | Uint8Array { + const { + tokenAddress, + recipient, + balance, + changeType: changeTypeNumber, + } = terms; + + const tokenAddressHex = normalizeAddressLowercase( + tokenAddress, + 'Invalid tokenAddress: must be a valid address', + ); + const recipientHex = normalizeAddressLowercase( + recipient, + 'Invalid recipient: must be a valid address', + ); + + if (balance <= 0n) { + throw new Error('Invalid balance: must be a positive number'); + } + + const changeType = changeTypeNumber as BalanceChangeType; + + if ( + changeType !== BalanceChangeType.Increase && + changeType !== BalanceChangeType.Decrease + ) { + throw new Error('Invalid changeType: must be either Increase or Decrease'); + } + + const changeTypeHex = `0x${toHexString({ value: changeType, size: 1 })}`; + const balanceHex = `0x${toHexString({ value: balance, size: 32 })}`; + const hexValue = concatHex([ + changeTypeHex, + tokenAddressHex, + recipientHex, + balanceHex, + ]); + + return prepareResult(hexValue, encodingOptions); +} diff --git a/packages/delegation-core/src/caveats/erc20Streaming.ts b/packages/delegation-core/src/caveats/erc20Streaming.ts index 1fedae06..bf424674 100644 --- a/packages/delegation-core/src/caveats/erc20Streaming.ts +++ b/packages/delegation-core/src/caveats/erc20Streaming.ts @@ -1,5 +1,6 @@ import { type BytesLike, bytesToHex, isHexString } from '@metamask/utils'; +import { toHexString } from '../internalUtils'; import { defaultOptions, prepareResult, @@ -7,7 +8,6 @@ import { type ResultValue, } from '../returns'; import type { Hex } from '../types'; -import { toHexString } from '../utils'; // Upper bound for timestamps (January 1, 10000 CE) const TIMESTAMP_UPPER_BOUND_SECONDS = 253402300799; diff --git a/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts b/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts index 1505de10..954d8f07 100644 --- a/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts +++ b/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts @@ -1,5 +1,6 @@ import { type BytesLike, isHexString, bytesToHex } from '@metamask/utils'; +import { toHexString } from '../internalUtils'; import { defaultOptions, prepareResult, @@ -7,7 +8,6 @@ import { type ResultValue, } from '../returns'; import type { Hex } from '../types'; -import { toHexString } from '../utils'; /** * Terms for configuring a periodic transfer allowance of ERC20 tokens. diff --git a/packages/delegation-core/src/caveats/erc20TransferAmount.ts b/packages/delegation-core/src/caveats/erc20TransferAmount.ts new file mode 100644 index 00000000..50f189ee --- /dev/null +++ b/packages/delegation-core/src/caveats/erc20TransferAmount.ts @@ -0,0 +1,65 @@ +import type { BytesLike } from '@metamask/utils'; + +import { concatHex, normalizeAddress, toHexString } from '../internalUtils'; +import { + defaultOptions, + prepareResult, + type EncodingOptions, + type ResultValue, +} from '../returns'; +import type { Hex } from '../types'; + +/** + * Terms for configuring an ERC20TransferAmount caveat. + */ +export type ERC20TransferAmountTerms = { + /** The ERC-20 token address. */ + tokenAddress: BytesLike; + /** The maximum amount of tokens that can be transferred. */ + maxAmount: bigint; +}; + +/** + * Creates terms for an ERC20TransferAmount caveat that caps transfer amount. + * + * @param terms - The terms for the ERC20TransferAmount caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as tokenAddress + maxAmount. + * @throws Error if the token address is invalid or maxAmount is not positive. + */ +export function createERC20TransferAmountTerms( + terms: ERC20TransferAmountTerms, + encodingOptions?: EncodingOptions<'hex'>, +): Hex; +export function createERC20TransferAmountTerms( + terms: ERC20TransferAmountTerms, + encodingOptions: EncodingOptions<'bytes'>, +): Uint8Array; +/** + * Creates terms for an ERC20TransferAmount caveat that caps transfer amount. + * + * @param terms - The terms for the ERC20TransferAmount caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as tokenAddress + maxAmount. + * @throws Error if the token address is invalid or maxAmount is not positive. + */ +export function createERC20TransferAmountTerms( + terms: ERC20TransferAmountTerms, + encodingOptions: EncodingOptions = defaultOptions, +): Hex | Uint8Array { + const { tokenAddress, maxAmount } = terms; + + const tokenAddressHex = normalizeAddress( + tokenAddress, + 'Invalid tokenAddress: must be a valid address', + ); + + if (maxAmount <= 0n) { + throw new Error('Invalid maxAmount: must be a positive number'); + } + + const maxAmountHex = `0x${toHexString({ value: maxAmount, size: 32 })}`; + const hexValue = concatHex([tokenAddressHex, maxAmountHex]); + + return prepareResult(hexValue, encodingOptions); +} diff --git a/packages/delegation-core/src/caveats/erc721BalanceChange.ts b/packages/delegation-core/src/caveats/erc721BalanceChange.ts new file mode 100644 index 00000000..912f0454 --- /dev/null +++ b/packages/delegation-core/src/caveats/erc721BalanceChange.ts @@ -0,0 +1,98 @@ +import type { BytesLike } from '@metamask/utils'; + +import { + concatHex, + normalizeAddressLowercase, + toHexString, +} from '../internalUtils'; +import { + defaultOptions, + prepareResult, + type EncodingOptions, + type ResultValue, +} from '../returns'; +import type { Hex } from '../types'; +import { BalanceChangeType } from './types'; + +/** + * Terms for configuring an ERC721BalanceChange caveat. + */ +export type ERC721BalanceChangeTerms = { + /** The ERC-721 token address. */ + tokenAddress: BytesLike; + /** The recipient address. */ + recipient: BytesLike; + /** The balance change amount. */ + amount: bigint; + /** The balance change type. */ + changeType: number; +}; + +/** + * Creates terms for an ERC721BalanceChange caveat that checks token balance changes. + * + * @param terms - The terms for the ERC721BalanceChange caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as changeType + tokenAddress + recipient + amount. + * @throws Error if any parameter is invalid. + */ +export function createERC721BalanceChangeTerms( + terms: ERC721BalanceChangeTerms, + encodingOptions?: EncodingOptions<'hex'>, +): Hex; +export function createERC721BalanceChangeTerms( + terms: ERC721BalanceChangeTerms, + encodingOptions: EncodingOptions<'bytes'>, +): Uint8Array; +/** + * Creates terms for an ERC721BalanceChange caveat that checks token balance changes. + * + * @param terms - The terms for the ERC721BalanceChange caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as changeType + tokenAddress + recipient + amount. + * @throws Error if any parameter is invalid. + */ +export function createERC721BalanceChangeTerms( + terms: ERC721BalanceChangeTerms, + encodingOptions: EncodingOptions = defaultOptions, +): Hex | Uint8Array { + const { + tokenAddress, + recipient, + amount, + changeType: changeTypeNumber, + } = terms; + + const tokenAddressHex = normalizeAddressLowercase( + tokenAddress, + 'Invalid tokenAddress: must be a valid address', + ); + const recipientHex = normalizeAddressLowercase( + recipient, + 'Invalid recipient: must be a valid address', + ); + + if (amount <= 0n) { + throw new Error('Invalid balance: must be a positive number'); + } + + const changeType = changeTypeNumber as BalanceChangeType; + + if ( + changeType !== BalanceChangeType.Increase && + changeType !== BalanceChangeType.Decrease + ) { + throw new Error('Invalid changeType: must be either Increase or Decrease'); + } + + const changeTypeHex = `0x${toHexString({ value: changeType, size: 1 })}`; + const amountHex = `0x${toHexString({ value: amount, size: 32 })}`; + const hexValue = concatHex([ + changeTypeHex, + tokenAddressHex, + recipientHex, + amountHex, + ]); + + return prepareResult(hexValue, encodingOptions); +} diff --git a/packages/delegation-core/src/caveats/erc721Transfer.ts b/packages/delegation-core/src/caveats/erc721Transfer.ts new file mode 100644 index 00000000..8270dbad --- /dev/null +++ b/packages/delegation-core/src/caveats/erc721Transfer.ts @@ -0,0 +1,65 @@ +import type { BytesLike } from '@metamask/utils'; + +import { concatHex, normalizeAddress, toHexString } from '../internalUtils'; +import { + defaultOptions, + prepareResult, + type EncodingOptions, + type ResultValue, +} from '../returns'; +import type { Hex } from '../types'; + +/** + * Terms for configuring an ERC721Transfer caveat. + */ +export type ERC721TransferTerms = { + /** The ERC-721 token address. */ + tokenAddress: BytesLike; + /** The token id. */ + tokenId: bigint; +}; + +/** + * Creates terms for an ERC721Transfer caveat that restricts transfers to a token and id. + * + * @param terms - The terms for the ERC721Transfer caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as tokenAddress + tokenId. + * @throws Error if the token address is invalid or tokenId is negative. + */ +export function createERC721TransferTerms( + terms: ERC721TransferTerms, + encodingOptions?: EncodingOptions<'hex'>, +): Hex; +export function createERC721TransferTerms( + terms: ERC721TransferTerms, + encodingOptions: EncodingOptions<'bytes'>, +): Uint8Array; +/** + * Creates terms for an ERC721Transfer caveat that restricts transfers to a token and id. + * + * @param terms - The terms for the ERC721Transfer caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as tokenAddress + tokenId. + * @throws Error if the token address is invalid or tokenId is negative. + */ +export function createERC721TransferTerms( + terms: ERC721TransferTerms, + encodingOptions: EncodingOptions = defaultOptions, +): Hex | Uint8Array { + const { tokenAddress, tokenId } = terms; + + const tokenAddressHex = normalizeAddress( + tokenAddress, + 'Invalid tokenAddress: must be a valid address', + ); + + if (tokenId < 0n) { + throw new Error('Invalid tokenId: must be a non-negative number'); + } + + const tokenIdHex = `0x${toHexString({ value: tokenId, size: 32 })}`; + const hexValue = concatHex([tokenAddressHex, tokenIdHex]); + + return prepareResult(hexValue, encodingOptions); +} diff --git a/packages/delegation-core/src/caveats/exactCalldataBatch.ts b/packages/delegation-core/src/caveats/exactCalldataBatch.ts new file mode 100644 index 00000000..cfe4ec33 --- /dev/null +++ b/packages/delegation-core/src/caveats/exactCalldataBatch.ts @@ -0,0 +1,88 @@ +import { encodeSingle } from '@metamask/abi-utils'; +import { bytesToHex, type BytesLike } from '@metamask/utils'; + +import { normalizeAddress } from '../internalUtils'; +import { + defaultOptions, + prepareResult, + type EncodingOptions, + type ResultValue, +} from '../returns'; +import type { Hex } from '../types'; + +/** + * Terms for configuring an ExactCalldataBatch caveat. + */ +export type ExactCalldataBatchTerms = { + /** The executions that must be matched exactly in the batch. */ + executions: { + target: BytesLike; + value: bigint; + callData: BytesLike; + }[]; +}; + +const EXECUTION_ARRAY_ABI = '(address,uint256,bytes)[]'; + +/** + * Creates terms for an ExactCalldataBatch caveat that matches a batch of executions. + * + * @param terms - The terms for the ExactCalldataBatch caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as ABI-encoded execution array. + * @throws Error if any execution parameters are invalid. + */ +export function createExactCalldataBatchTerms( + terms: ExactCalldataBatchTerms, + encodingOptions?: EncodingOptions<'hex'>, +): Hex; +export function createExactCalldataBatchTerms( + terms: ExactCalldataBatchTerms, + encodingOptions: EncodingOptions<'bytes'>, +): Uint8Array; +/** + * Creates terms for an ExactCalldataBatch caveat that matches a batch of executions. + * + * @param terms - The terms for the ExactCalldataBatch caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as ABI-encoded execution array. + * @throws Error if any execution parameters are invalid. + */ +export function createExactCalldataBatchTerms( + terms: ExactCalldataBatchTerms, + encodingOptions: EncodingOptions = defaultOptions, +): Hex | Uint8Array { + const { executions } = terms; + + if (executions.length === 0) { + throw new Error('Invalid executions: array cannot be empty'); + } + + const encodableExecutions = executions.map((execution) => { + const targetHex = normalizeAddress( + execution.target, + 'Invalid target: must be a valid address', + ); + + if (execution.value < 0n) { + throw new Error('Invalid value: must be a non-negative number'); + } + + let callDataHex: string; + if (typeof execution.callData === 'string') { + if (!execution.callData.startsWith('0x')) { + throw new Error( + 'Invalid calldata: must be a hex string starting with 0x', + ); + } + callDataHex = execution.callData; + } else { + callDataHex = bytesToHex(execution.callData); + } + + return [targetHex, execution.value, callDataHex]; + }); + + const hexValue = encodeSingle(EXECUTION_ARRAY_ABI, encodableExecutions); + return prepareResult(hexValue, encodingOptions); +} diff --git a/packages/delegation-core/src/caveats/exactExecution.ts b/packages/delegation-core/src/caveats/exactExecution.ts new file mode 100644 index 00000000..f8d3a56c --- /dev/null +++ b/packages/delegation-core/src/caveats/exactExecution.ts @@ -0,0 +1,79 @@ +import { bytesToHex, type BytesLike } from '@metamask/utils'; + +import { concatHex, normalizeAddress, toHexString } from '../internalUtils'; +import { + defaultOptions, + prepareResult, + type EncodingOptions, + type ResultValue, +} from '../returns'; +import type { Hex } from '../types'; + +/** + * Terms for configuring an ExactExecution caveat. + */ +export type ExactExecutionTerms = { + /** The execution that must be matched exactly. */ + execution: { + target: BytesLike; + value: bigint; + callData: BytesLike; + }; +}; + +/** + * Creates terms for an ExactExecution caveat that matches a single execution. + * + * @param terms - The terms for the ExactExecution caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as concatenated target + value + calldata. + * @throws Error if any execution parameters are invalid. + */ +export function createExactExecutionTerms( + terms: ExactExecutionTerms, + encodingOptions?: EncodingOptions<'hex'>, +): Hex; +export function createExactExecutionTerms( + terms: ExactExecutionTerms, + encodingOptions: EncodingOptions<'bytes'>, +): Uint8Array; +/** + * Creates terms for an ExactExecution caveat that matches a single execution. + * + * @param terms - The terms for the ExactExecution caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as concatenated target + value + calldata. + * @throws Error if any execution parameters are invalid. + */ +export function createExactExecutionTerms( + terms: ExactExecutionTerms, + encodingOptions: EncodingOptions = defaultOptions, +): Hex | Uint8Array { + const { execution } = terms; + + const targetHex = normalizeAddress( + execution.target, + 'Invalid target: must be a valid address', + ); + + if (execution.value < 0n) { + throw new Error('Invalid value: must be a non-negative number'); + } + + let callDataHex: string; + if (typeof execution.callData === 'string') { + if (!execution.callData.startsWith('0x')) { + throw new Error( + 'Invalid calldata: must be a hex string starting with 0x', + ); + } + callDataHex = execution.callData; + } else { + callDataHex = bytesToHex(execution.callData); + } + + const valueHex = `0x${toHexString({ value: execution.value, size: 32 })}`; + const hexValue = concatHex([targetHex, valueHex, callDataHex]); + + return prepareResult(hexValue, encodingOptions); +} diff --git a/packages/delegation-core/src/caveats/exactExecutionBatch.ts b/packages/delegation-core/src/caveats/exactExecutionBatch.ts new file mode 100644 index 00000000..a8a72e25 --- /dev/null +++ b/packages/delegation-core/src/caveats/exactExecutionBatch.ts @@ -0,0 +1,88 @@ +import { encodeSingle } from '@metamask/abi-utils'; +import { bytesToHex, type BytesLike } from '@metamask/utils'; + +import { normalizeAddress } from '../internalUtils'; +import { + defaultOptions, + prepareResult, + type EncodingOptions, + type ResultValue, +} from '../returns'; +import type { Hex } from '../types'; + +/** + * Terms for configuring an ExactExecutionBatch caveat. + */ +export type ExactExecutionBatchTerms = { + /** The executions that must be matched exactly in the batch. */ + executions: { + target: BytesLike; + value: bigint; + callData: BytesLike; + }[]; +}; + +const EXECUTION_ARRAY_ABI = '(address,uint256,bytes)[]'; + +/** + * Creates terms for an ExactExecutionBatch caveat that matches a batch of executions. + * + * @param terms - The terms for the ExactExecutionBatch caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as ABI-encoded execution array. + * @throws Error if any execution parameters are invalid. + */ +export function createExactExecutionBatchTerms( + terms: ExactExecutionBatchTerms, + encodingOptions?: EncodingOptions<'hex'>, +): Hex; +export function createExactExecutionBatchTerms( + terms: ExactExecutionBatchTerms, + encodingOptions: EncodingOptions<'bytes'>, +): Uint8Array; +/** + * Creates terms for an ExactExecutionBatch caveat that matches a batch of executions. + * + * @param terms - The terms for the ExactExecutionBatch caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as ABI-encoded execution array. + * @throws Error if any execution parameters are invalid. + */ +export function createExactExecutionBatchTerms( + terms: ExactExecutionBatchTerms, + encodingOptions: EncodingOptions = defaultOptions, +): Hex | Uint8Array { + const { executions } = terms; + + if (executions.length === 0) { + throw new Error('Invalid executions: array cannot be empty'); + } + + const encodableExecutions = executions.map((execution) => { + const targetHex = normalizeAddress( + execution.target, + 'Invalid target: must be a valid address', + ); + + if (execution.value < 0n) { + throw new Error('Invalid value: must be a non-negative number'); + } + + let callDataHex: string; + if (typeof execution.callData === 'string') { + if (!execution.callData.startsWith('0x')) { + throw new Error( + 'Invalid calldata: must be a hex string starting with 0x', + ); + } + callDataHex = execution.callData; + } else { + callDataHex = bytesToHex(execution.callData); + } + + return [targetHex, execution.value, callDataHex]; + }); + + const hexValue = encodeSingle(EXECUTION_ARRAY_ABI, encodableExecutions); + return prepareResult(hexValue, encodingOptions); +} diff --git a/packages/delegation-core/src/caveats/id.ts b/packages/delegation-core/src/caveats/id.ts new file mode 100644 index 00000000..de6f1ea9 --- /dev/null +++ b/packages/delegation-core/src/caveats/id.ts @@ -0,0 +1,73 @@ +import { toHexString } from '../internalUtils'; +import { + defaultOptions, + prepareResult, + type EncodingOptions, + type ResultValue, +} from '../returns'; +import type { Hex } from '../types'; + +const MAX_UINT256 = BigInt(`0x${'f'.repeat(64)}`); + +/** + * Terms for configuring an Id caveat. + */ +export type IdTerms = { + /** An id for the delegation. Only one delegation may be redeemed with any given id. */ + id: bigint | number; +}; + +/** + * Creates terms for an Id caveat that restricts delegations by unique identifier. + * + * @param terms - The terms for the Id caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as a 32-byte hex string. + * @throws Error if the id is invalid or out of range. + */ +export function createIdTerms( + terms: IdTerms, + encodingOptions?: EncodingOptions<'hex'>, +): Hex; +export function createIdTerms( + terms: IdTerms, + encodingOptions: EncodingOptions<'bytes'>, +): Uint8Array; +/** + * Creates terms for an Id caveat that restricts delegations by unique identifier. + * + * @param terms - The terms for the Id caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as a 32-byte hex string. + * @throws Error if the id is invalid or out of range. + */ +export function createIdTerms( + terms: IdTerms, + encodingOptions: EncodingOptions = defaultOptions, +): Hex | Uint8Array { + const { id } = terms; + + let idBigInt: bigint; + + if (typeof id === 'number') { + if (!Number.isInteger(id)) { + throw new Error('Invalid id: must be an integer'); + } + idBigInt = BigInt(id); + } else if (typeof id === 'bigint') { + idBigInt = id; + } else { + throw new Error('Invalid id: must be a bigint or number'); + } + + if (idBigInt < 0n) { + throw new Error('Invalid id: must be a non-negative number'); + } + + if (idBigInt > MAX_UINT256) { + throw new Error('Invalid id: must be less than 2^256'); + } + + const hexValue = `0x${toHexString({ value: idBigInt, size: 32 })}`; + return prepareResult(hexValue, encodingOptions); +} diff --git a/packages/delegation-core/src/caveats/index.ts b/packages/delegation-core/src/caveats/index.ts index dea44f71..9d30a363 100644 --- a/packages/delegation-core/src/caveats/index.ts +++ b/packages/delegation-core/src/caveats/index.ts @@ -2,8 +2,30 @@ export { createValueLteTerms } from './valueLte'; export { createTimestampTerms } from './timestamp'; export { createNativeTokenPeriodTransferTerms } from './nativeTokenPeriodTransfer'; export { createExactCalldataTerms } from './exactCalldata'; +export { createExactCalldataBatchTerms } from './exactCalldataBatch'; +export { createExactExecutionTerms } from './exactExecution'; +export { createExactExecutionBatchTerms } from './exactExecutionBatch'; export { createNativeTokenStreamingTerms } from './nativeTokenStreaming'; +export { createNativeTokenTransferAmountTerms } from './nativeTokenTransferAmount'; +export { createNativeTokenPaymentTerms } from './nativeTokenPayment'; +export { createNativeBalanceChangeTerms } from './nativeBalanceChange'; export { createERC20StreamingTerms } from './erc20Streaming'; export { createERC20TokenPeriodTransferTerms } from './erc20TokenPeriodTransfer'; +export { createERC20TransferAmountTerms } from './erc20TransferAmount'; +export { createERC20BalanceChangeTerms } from './erc20BalanceChange'; +export { createERC721BalanceChangeTerms } from './erc721BalanceChange'; +export { createERC721TransferTerms } from './erc721Transfer'; +export { createERC1155BalanceChangeTerms } from './erc1155BalanceChange'; export { createNonceTerms } from './nonce'; export { createAllowedCalldataTerms } from './allowedCalldata'; +export { createAllowedMethodsTerms } from './allowedMethods'; +export { createAllowedTargetsTerms } from './allowedTargets'; +export { createArgsEqualityCheckTerms } from './argsEqualityCheck'; +export { createBlockNumberTerms } from './blockNumber'; +export { createDeployedTerms } from './deployed'; +export { createIdTerms } from './id'; +export { createLimitedCallsTerms } from './limitedCalls'; +export { createMultiTokenPeriodTerms } from './multiTokenPeriod'; +export { createOwnershipTransferTerms } from './ownershipTransfer'; +export { createRedeemerTerms } from './redeemer'; +export { createSpecificActionERC20TransferBatchTerms } from './specificActionERC20TransferBatch'; diff --git a/packages/delegation-core/src/caveats/limitedCalls.ts b/packages/delegation-core/src/caveats/limitedCalls.ts new file mode 100644 index 00000000..624a8269 --- /dev/null +++ b/packages/delegation-core/src/caveats/limitedCalls.ts @@ -0,0 +1,58 @@ +import { toHexString } from '../internalUtils'; +import { + defaultOptions, + prepareResult, + type EncodingOptions, + type ResultValue, +} from '../returns'; +import type { Hex } from '../types'; + +/** + * Terms for configuring a LimitedCalls caveat. + */ +export type LimitedCallsTerms = { + /** The maximum number of times this delegation may be redeemed. */ + limit: number; +}; + +/** + * Creates terms for a LimitedCalls caveat that restricts the number of redeems. + * + * @param terms - The terms for the LimitedCalls caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as a 32-byte hex string. + * @throws Error if the limit is not a positive integer. + */ +export function createLimitedCallsTerms( + terms: LimitedCallsTerms, + encodingOptions?: EncodingOptions<'hex'>, +): Hex; +export function createLimitedCallsTerms( + terms: LimitedCallsTerms, + encodingOptions: EncodingOptions<'bytes'>, +): Uint8Array; +/** + * Creates terms for a LimitedCalls caveat that restricts the number of redeems. + * + * @param terms - The terms for the LimitedCalls caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as a 32-byte hex string. + * @throws Error if the limit is not a positive integer. + */ +export function createLimitedCallsTerms( + terms: LimitedCallsTerms, + encodingOptions: EncodingOptions = defaultOptions, +): Hex | Uint8Array { + const { limit } = terms; + + if (!Number.isInteger(limit)) { + throw new Error('Invalid limit: must be an integer'); + } + + if (limit <= 0) { + throw new Error('Invalid limit: must be a positive integer'); + } + + const hexValue = `0x${toHexString({ value: limit, size: 32 })}`; + return prepareResult(hexValue, encodingOptions); +} diff --git a/packages/delegation-core/src/caveats/multiTokenPeriod.ts b/packages/delegation-core/src/caveats/multiTokenPeriod.ts new file mode 100644 index 00000000..d6eadac3 --- /dev/null +++ b/packages/delegation-core/src/caveats/multiTokenPeriod.ts @@ -0,0 +1,91 @@ +import type { BytesLike } from '@metamask/utils'; + +import { concatHex, normalizeAddress, toHexString } from '../internalUtils'; +import { + defaultOptions, + prepareResult, + type EncodingOptions, + type ResultValue, +} from '../returns'; +import type { Hex } from '../types'; + +/** + * Configuration for a single token in MultiTokenPeriod terms. + */ +export type TokenPeriodConfig = { + token: BytesLike; + periodAmount: bigint; + periodDuration: number; + startDate: number; +}; + +/** + * Terms for configuring a MultiTokenPeriod caveat. + */ +export type MultiTokenPeriodTerms = { + tokenConfigs: TokenPeriodConfig[]; +}; + +/** + * Creates terms for a MultiTokenPeriod caveat that configures multiple token periods. + * + * @param terms - The terms for the MultiTokenPeriod caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as concatenated token period configs. + * @throws Error if the tokenConfigs array is empty or contains invalid parameters. + */ +export function createMultiTokenPeriodTerms( + terms: MultiTokenPeriodTerms, + encodingOptions?: EncodingOptions<'hex'>, +): Hex; +export function createMultiTokenPeriodTerms( + terms: MultiTokenPeriodTerms, + encodingOptions: EncodingOptions<'bytes'>, +): Uint8Array; +/** + * Creates terms for a MultiTokenPeriod caveat that configures multiple token periods. + * + * @param terms - The terms for the MultiTokenPeriod caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as concatenated token period configs. + * @throws Error if the tokenConfigs array is empty or contains invalid parameters. + */ +export function createMultiTokenPeriodTerms( + terms: MultiTokenPeriodTerms, + encodingOptions: EncodingOptions = defaultOptions, +): Hex | Uint8Array { + const { tokenConfigs } = terms; + + if (!tokenConfigs || tokenConfigs.length === 0) { + throw new Error( + 'MultiTokenPeriodBuilder: tokenConfigs array cannot be empty', + ); + } + + const hexParts: string[] = []; + + for (const tokenConfig of tokenConfigs) { + const tokenHex = normalizeAddress( + tokenConfig.token, + `Invalid token address: ${String(tokenConfig.token)}`, + ); + + if (tokenConfig.periodAmount <= 0n) { + throw new Error('Invalid period amount: must be greater than 0'); + } + + if (tokenConfig.periodDuration <= 0) { + throw new Error('Invalid period duration: must be greater than 0'); + } + + hexParts.push( + tokenHex, + `0x${toHexString({ value: tokenConfig.periodAmount, size: 32 })}`, + `0x${toHexString({ value: tokenConfig.periodDuration, size: 32 })}`, + `0x${toHexString({ value: tokenConfig.startDate, size: 32 })}`, + ); + } + + const hexValue = concatHex(hexParts); + return prepareResult(hexValue, encodingOptions); +} diff --git a/packages/delegation-core/src/caveats/nativeBalanceChange.ts b/packages/delegation-core/src/caveats/nativeBalanceChange.ts new file mode 100644 index 00000000..a52e2ecb --- /dev/null +++ b/packages/delegation-core/src/caveats/nativeBalanceChange.ts @@ -0,0 +1,82 @@ +import type { BytesLike } from '@metamask/utils'; + +import { + concatHex, + normalizeAddressLowercase, + toHexString, +} from '../internalUtils'; +import { + defaultOptions, + prepareResult, + type EncodingOptions, + type ResultValue, +} from '../returns'; +import type { Hex } from '../types'; +import { BalanceChangeType } from './types'; + +/** + * Terms for configuring a NativeBalanceChange caveat. + */ +export type NativeBalanceChangeTerms = { + /** The recipient address. */ + recipient: BytesLike; + /** The balance change amount. */ + balance: bigint; + /** The balance change type. */ + changeType: number; +}; + +/** + * Creates terms for a NativeBalanceChange caveat that checks recipient balance changes. + * + * @param terms - The terms for the NativeBalanceChange caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as changeType + recipient + balance. + * @throws Error if the recipient address is invalid or balance/changeType are invalid. + */ +export function createNativeBalanceChangeTerms( + terms: NativeBalanceChangeTerms, + encodingOptions?: EncodingOptions<'hex'>, +): Hex; +export function createNativeBalanceChangeTerms( + terms: NativeBalanceChangeTerms, + encodingOptions: EncodingOptions<'bytes'>, +): Uint8Array; +/** + * Creates terms for a NativeBalanceChange caveat that checks recipient balance changes. + * + * @param terms - The terms for the NativeBalanceChange caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as changeType + recipient + balance. + * @throws Error if the recipient address is invalid or balance/changeType are invalid. + */ +export function createNativeBalanceChangeTerms( + terms: NativeBalanceChangeTerms, + encodingOptions: EncodingOptions = defaultOptions, +): Hex | Uint8Array { + const { recipient, balance, changeType: changeTypeNumber } = terms; + + const recipientHex = normalizeAddressLowercase( + recipient, + 'Invalid recipient: must be a valid Address', + ); + + if (balance <= 0n) { + throw new Error('Invalid balance: must be a positive number'); + } + + const changeType = changeTypeNumber as BalanceChangeType; + + if ( + changeType !== BalanceChangeType.Increase && + changeType !== BalanceChangeType.Decrease + ) { + throw new Error('Invalid changeType: must be either Increase or Decrease'); + } + + const changeTypeHex = `0x${toHexString({ value: changeType, size: 1 })}`; + const balanceHex = `0x${toHexString({ value: balance, size: 32 })}`; + const hexValue = concatHex([changeTypeHex, recipientHex, balanceHex]); + + return prepareResult(hexValue, encodingOptions); +} diff --git a/packages/delegation-core/src/caveats/nativeTokenPayment.ts b/packages/delegation-core/src/caveats/nativeTokenPayment.ts new file mode 100644 index 00000000..3074320e --- /dev/null +++ b/packages/delegation-core/src/caveats/nativeTokenPayment.ts @@ -0,0 +1,69 @@ +import type { BytesLike } from '@metamask/utils'; + +import { + concatHex, + normalizeAddressLowercase, + toHexString, +} from '../internalUtils'; +import { + defaultOptions, + prepareResult, + type EncodingOptions, + type ResultValue, +} from '../returns'; +import type { Hex } from '../types'; + +/** + * Terms for configuring a NativeTokenPayment caveat. + */ +export type NativeTokenPaymentTerms = { + /** The recipient address. */ + recipient: BytesLike; + /** The amount that must be paid. */ + amount: bigint; +}; + +/** + * Creates terms for a NativeTokenPayment caveat that requires a payment to a recipient. + * + * @param terms - The terms for the NativeTokenPayment caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as recipient + amount. + * @throws Error if the recipient address is invalid or amount is not positive. + */ +export function createNativeTokenPaymentTerms( + terms: NativeTokenPaymentTerms, + encodingOptions?: EncodingOptions<'hex'>, +): Hex; +export function createNativeTokenPaymentTerms( + terms: NativeTokenPaymentTerms, + encodingOptions: EncodingOptions<'bytes'>, +): Uint8Array; +/** + * Creates terms for a NativeTokenPayment caveat that requires a payment to a recipient. + * + * @param terms - The terms for the NativeTokenPayment caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as recipient + amount. + * @throws Error if the recipient address is invalid or amount is not positive. + */ +export function createNativeTokenPaymentTerms( + terms: NativeTokenPaymentTerms, + encodingOptions: EncodingOptions = defaultOptions, +): Hex | Uint8Array { + const { recipient, amount } = terms; + + const recipientHex = normalizeAddressLowercase( + recipient, + 'Invalid recipient: must be a valid address', + ); + + if (amount <= 0n) { + throw new Error('Invalid amount: must be positive'); + } + + const amountHex = `0x${toHexString({ value: amount, size: 32 })}`; + const hexValue = concatHex([recipientHex, amountHex]); + + return prepareResult(hexValue, encodingOptions); +} diff --git a/packages/delegation-core/src/caveats/nativeTokenPeriodTransfer.ts b/packages/delegation-core/src/caveats/nativeTokenPeriodTransfer.ts index cd3e5582..80015d46 100644 --- a/packages/delegation-core/src/caveats/nativeTokenPeriodTransfer.ts +++ b/packages/delegation-core/src/caveats/nativeTokenPeriodTransfer.ts @@ -1,3 +1,4 @@ +import { toHexString } from '../internalUtils'; import { defaultOptions, prepareResult, @@ -5,7 +6,6 @@ import { type ResultValue, } from '../returns'; import type { Hex } from '../types'; -import { toHexString } from '../utils'; /** * Terms for configuring a periodic transfer allowance of native tokens. diff --git a/packages/delegation-core/src/caveats/nativeTokenStreaming.ts b/packages/delegation-core/src/caveats/nativeTokenStreaming.ts index f0d96442..414c83a8 100644 --- a/packages/delegation-core/src/caveats/nativeTokenStreaming.ts +++ b/packages/delegation-core/src/caveats/nativeTokenStreaming.ts @@ -1,3 +1,4 @@ +import { toHexString } from '../internalUtils'; import { defaultOptions, prepareResult, @@ -5,7 +6,6 @@ import { type ResultValue, } from '../returns'; import type { Hex } from '../types'; -import { toHexString } from '../utils'; // Upper bound for timestamps (January 1, 10000 CE) const TIMESTAMP_UPPER_BOUND_SECONDS = 253402300799; diff --git a/packages/delegation-core/src/caveats/nativeTokenTransferAmount.ts b/packages/delegation-core/src/caveats/nativeTokenTransferAmount.ts new file mode 100644 index 00000000..c468ded8 --- /dev/null +++ b/packages/delegation-core/src/caveats/nativeTokenTransferAmount.ts @@ -0,0 +1,54 @@ +import { toHexString } from '../internalUtils'; +import { + defaultOptions, + prepareResult, + type EncodingOptions, + type ResultValue, +} from '../returns'; +import type { Hex } from '../types'; + +/** + * Terms for configuring a NativeTokenTransferAmount caveat. + */ +export type NativeTokenTransferAmountTerms = { + /** The maximum amount of native tokens that can be transferred. */ + maxAmount: bigint; +}; + +/** + * Creates terms for a NativeTokenTransferAmount caveat that caps native transfers. + * + * @param terms - The terms for the NativeTokenTransferAmount caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as a 32-byte hex string. + * @throws Error if maxAmount is negative. + */ +export function createNativeTokenTransferAmountTerms( + terms: NativeTokenTransferAmountTerms, + encodingOptions?: EncodingOptions<'hex'>, +): Hex; +export function createNativeTokenTransferAmountTerms( + terms: NativeTokenTransferAmountTerms, + encodingOptions: EncodingOptions<'bytes'>, +): Uint8Array; +/** + * Creates terms for a NativeTokenTransferAmount caveat that caps native transfers. + * + * @param terms - The terms for the NativeTokenTransferAmount caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as a 32-byte hex string. + * @throws Error if maxAmount is negative. + */ +export function createNativeTokenTransferAmountTerms( + terms: NativeTokenTransferAmountTerms, + encodingOptions: EncodingOptions = defaultOptions, +): Hex | Uint8Array { + const { maxAmount } = terms; + + if (maxAmount < 0n) { + throw new Error('Invalid maxAmount: must be zero or positive'); + } + + const hexValue = `0x${toHexString({ value: maxAmount, size: 32 })}`; + return prepareResult(hexValue, encodingOptions); +} diff --git a/packages/delegation-core/src/caveats/ownershipTransfer.ts b/packages/delegation-core/src/caveats/ownershipTransfer.ts new file mode 100644 index 00000000..5b0675eb --- /dev/null +++ b/packages/delegation-core/src/caveats/ownershipTransfer.ts @@ -0,0 +1,56 @@ +import type { BytesLike } from '@metamask/utils'; + +import { normalizeAddress } from '../internalUtils'; +import { + defaultOptions, + prepareResult, + type EncodingOptions, + type ResultValue, +} from '../returns'; +import type { Hex } from '../types'; + +/** + * Terms for configuring an OwnershipTransfer caveat. + */ +export type OwnershipTransferTerms = { + /** The contract address for which ownership transfers are allowed. */ + contractAddress: BytesLike; +}; + +/** + * Creates terms for an OwnershipTransfer caveat that constrains ownership transfers to a contract. + * + * @param terms - The terms for the OwnershipTransfer caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as the contract address. + * @throws Error if the contract address is invalid. + */ +export function createOwnershipTransferTerms( + terms: OwnershipTransferTerms, + encodingOptions?: EncodingOptions<'hex'>, +): Hex; +export function createOwnershipTransferTerms( + terms: OwnershipTransferTerms, + encodingOptions: EncodingOptions<'bytes'>, +): Uint8Array; +/** + * Creates terms for an OwnershipTransfer caveat that constrains ownership transfers to a contract. + * + * @param terms - The terms for the OwnershipTransfer caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as the contract address. + * @throws Error if the contract address is invalid. + */ +export function createOwnershipTransferTerms( + terms: OwnershipTransferTerms, + encodingOptions: EncodingOptions = defaultOptions, +): Hex | Uint8Array { + const { contractAddress } = terms; + + const contractAddressHex = normalizeAddress( + contractAddress, + 'Invalid contractAddress: must be a valid address', + ); + + return prepareResult(contractAddressHex, encodingOptions); +} diff --git a/packages/delegation-core/src/caveats/redeemer.ts b/packages/delegation-core/src/caveats/redeemer.ts new file mode 100644 index 00000000..b9bf42a8 --- /dev/null +++ b/packages/delegation-core/src/caveats/redeemer.ts @@ -0,0 +1,62 @@ +import type { BytesLike } from '@metamask/utils'; + +import { concatHex, normalizeAddress } from '../internalUtils'; +import { + defaultOptions, + prepareResult, + type EncodingOptions, + type ResultValue, +} from '../returns'; +import type { Hex } from '../types'; + +/** + * Terms for configuring a Redeemer caveat. + */ +export type RedeemerTerms = { + /** An array of addresses allowed to redeem the delegation. */ + redeemers: BytesLike[]; +}; + +/** + * Creates terms for a Redeemer caveat that restricts who may redeem. + * + * @param terms - The terms for the Redeemer caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as concatenated redeemer addresses. + * @throws Error if the redeemers array is empty or contains invalid addresses. + */ +export function createRedeemerTerms( + terms: RedeemerTerms, + encodingOptions?: EncodingOptions<'hex'>, +): Hex; +export function createRedeemerTerms( + terms: RedeemerTerms, + encodingOptions: EncodingOptions<'bytes'>, +): Uint8Array; +/** + * Creates terms for a Redeemer caveat that restricts who may redeem. + * + * @param terms - The terms for the Redeemer caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as concatenated redeemer addresses. + * @throws Error if the redeemers array is empty or contains invalid addresses. + */ +export function createRedeemerTerms( + terms: RedeemerTerms, + encodingOptions: EncodingOptions = defaultOptions, +): Hex | Uint8Array { + const { redeemers } = terms; + + if (redeemers.length === 0) { + throw new Error( + 'Invalid redeemers: must specify at least one redeemer address', + ); + } + + const normalizedRedeemers = redeemers.map((redeemer) => + normalizeAddress(redeemer, 'Invalid redeemers: must be a valid address'), + ); + + const hexValue = concatHex(normalizedRedeemers); + return prepareResult(hexValue, encodingOptions); +} diff --git a/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts b/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts new file mode 100644 index 00000000..f73da887 --- /dev/null +++ b/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts @@ -0,0 +1,90 @@ +import { bytesToHex, type BytesLike } from '@metamask/utils'; + +import { concatHex, normalizeAddress, toHexString } from '../internalUtils'; +import { + defaultOptions, + prepareResult, + type EncodingOptions, + type ResultValue, +} from '../returns'; +import type { Hex } from '../types'; + +/** + * Terms for configuring a SpecificActionERC20TransferBatch caveat. + */ +export type SpecificActionERC20TransferBatchTerms = { + /** The address of the ERC-20 token contract. */ + tokenAddress: BytesLike; + /** The recipient of the ERC-20 transfer. */ + recipient: BytesLike; + /** The amount of tokens to transfer. */ + amount: bigint; + /** The target address for the first transaction. */ + target: BytesLike; + /** The calldata for the first transaction. */ + calldata: BytesLike; +}; + +/** + * Creates terms for a SpecificActionERC20TransferBatch caveat that enforces a + * specific action followed by an ERC20 transfer. + * + * @param terms - The terms for the SpecificActionERC20TransferBatch caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as concatenated tokenAddress + recipient + amount + target + calldata. + * @throws Error if any address is invalid or amount is not positive. + */ +export function createSpecificActionERC20TransferBatchTerms( + terms: SpecificActionERC20TransferBatchTerms, + encodingOptions?: EncodingOptions<'hex'>, +): Hex; +export function createSpecificActionERC20TransferBatchTerms( + terms: SpecificActionERC20TransferBatchTerms, + encodingOptions: EncodingOptions<'bytes'>, +): Uint8Array; +/** + * Creates terms for a SpecificActionERC20TransferBatch caveat that enforces a + * specific action followed by an ERC20 transfer. + * + * @param terms - The terms for the SpecificActionERC20TransferBatch caveat. + * @param encodingOptions - The encoding options for the result. + * @returns The terms as concatenated tokenAddress + recipient + amount + target + calldata. + * @throws Error if any address is invalid or amount is not positive. + */ +export function createSpecificActionERC20TransferBatchTerms( + terms: SpecificActionERC20TransferBatchTerms, + encodingOptions: EncodingOptions = defaultOptions, +): Hex | Uint8Array { + const { tokenAddress, recipient, amount, target, calldata } = terms; + + const tokenAddressHex = normalizeAddress( + tokenAddress, + 'Invalid tokenAddress: must be a valid address', + ); + const recipientHex = normalizeAddress( + recipient, + 'Invalid recipient: must be a valid address', + ); + const targetHex = normalizeAddress( + target, + 'Invalid target: must be a valid address', + ); + + if (amount <= 0n) { + throw new Error('Invalid amount: must be a positive number'); + } + + const amountHex = `0x${toHexString({ value: amount, size: 32 })}`; + const calldataHex = + typeof calldata === 'string' ? calldata : bytesToHex(calldata); + + const hexValue = concatHex([ + tokenAddressHex, + recipientHex, + amountHex, + targetHex, + calldataHex, + ]); + + return prepareResult(hexValue, encodingOptions); +} diff --git a/packages/delegation-core/src/caveats/timestamp.ts b/packages/delegation-core/src/caveats/timestamp.ts index 3d88550b..e6f45998 100644 --- a/packages/delegation-core/src/caveats/timestamp.ts +++ b/packages/delegation-core/src/caveats/timestamp.ts @@ -1,3 +1,4 @@ +import { toHexString } from '../internalUtils'; import { defaultOptions, prepareResult, @@ -5,7 +6,6 @@ import { type ResultValue, } from '../returns'; import type { Hex } from '../types'; -import { toHexString } from '../utils'; // Upper bound for timestamps (equivalent to January 1, 10000 CE) const TIMESTAMP_UPPER_BOUND_SECONDS = 253402300799; diff --git a/packages/delegation-core/src/caveats/types.ts b/packages/delegation-core/src/caveats/types.ts new file mode 100644 index 00000000..ec48f4c1 --- /dev/null +++ b/packages/delegation-core/src/caveats/types.ts @@ -0,0 +1,4 @@ +export enum BalanceChangeType { + Increase = 0x0, + Decrease = 0x1, +} diff --git a/packages/delegation-core/src/caveats/valueLte.ts b/packages/delegation-core/src/caveats/valueLte.ts index 5dea3c8a..af5d65b3 100644 --- a/packages/delegation-core/src/caveats/valueLte.ts +++ b/packages/delegation-core/src/caveats/valueLte.ts @@ -1,3 +1,4 @@ +import { toHexString } from '../internalUtils'; import { defaultOptions, prepareResult, @@ -5,7 +6,6 @@ import { type ResultValue, } from '../returns'; import type { Hex } from '../types'; -import { toHexString } from '../utils'; /** * Terms for configuring a ValueLte caveat. diff --git a/packages/delegation-core/src/index.ts b/packages/delegation-core/src/index.ts index 72559583..eab94a6f 100644 --- a/packages/delegation-core/src/index.ts +++ b/packages/delegation-core/src/index.ts @@ -9,11 +9,33 @@ export { createTimestampTerms, createNativeTokenPeriodTransferTerms, createExactCalldataTerms, + createExactCalldataBatchTerms, + createExactExecutionTerms, + createExactExecutionBatchTerms, createNativeTokenStreamingTerms, + createNativeTokenTransferAmountTerms, + createNativeTokenPaymentTerms, + createNativeBalanceChangeTerms, createERC20StreamingTerms, createERC20TokenPeriodTransferTerms, + createERC20TransferAmountTerms, + createERC20BalanceChangeTerms, + createERC721BalanceChangeTerms, + createERC721TransferTerms, + createERC1155BalanceChangeTerms, createNonceTerms, createAllowedCalldataTerms, + createAllowedMethodsTerms, + createAllowedTargetsTerms, + createArgsEqualityCheckTerms, + createBlockNumberTerms, + createDeployedTerms, + createIdTerms, + createLimitedCallsTerms, + createMultiTokenPeriodTerms, + createOwnershipTransferTerms, + createRedeemerTerms, + createSpecificActionERC20TransferBatchTerms, } from './caveats'; export { diff --git a/packages/delegation-core/src/internalUtils.ts b/packages/delegation-core/src/internalUtils.ts new file mode 100644 index 00000000..61906ba2 --- /dev/null +++ b/packages/delegation-core/src/internalUtils.ts @@ -0,0 +1,114 @@ +import { + bytesToHex, + hexToBytes, + isHexString, + remove0x, + type BytesLike, +} from '@metamask/utils'; + +/** + * Converts a numeric value to a hexadecimal string with zero-padding, without 0x prefix. + * + * @param options - The options for the conversion. + * @param options.value - The numeric value to convert to hex (bigint or number). + * @param options.size - The size in bytes for the resulting hex string (each byte = 2 hex characters). + * @returns A hexadecimal string prefixed with zeros to match the specified size. + * @example + * ```typescript + * toHexString({ value: 255, size: 2 }) // Returns "00ff" + * toHexString({ value: 16n, size: 1 }) // Returns "10" + * ``` + */ +export const toHexString = ({ + value, + size, +}: { + value: bigint | number; + size: number; +}): string => { + return value.toString(16).padStart(size * 2, '0'); +}; + +/** + * Normalizes a bytes-like value into a hex string. + * + * @param value - The value to normalize. + * @param errorMessage - Error message used for invalid input. + * @returns The normalized hex string (0x-prefixed). + * @throws Error if the input is an invalid hex string. + */ +export const normalizeHex = ( + value: BytesLike, + errorMessage: string, +): string => { + if (typeof value === 'string') { + if (!isHexString(value)) { + throw new Error(errorMessage); + } + return value; + } + + return bytesToHex(value); +}; + +/** + * Normalizes an address into a hex string without changing casing. + * + * @param value - The address as a hex string or bytes. + * @param errorMessage - Error message used for invalid input. + * @returns The address as a 0x-prefixed hex string. + * @throws Error if the input is not a 20-byte address. + */ +export const normalizeAddress = ( + value: BytesLike, + errorMessage: string, +): string => { + if (typeof value === 'string') { + if (!isHexString(value) || value.length !== 42) { + throw new Error(errorMessage); + } + return value; + } + + if (value.length !== 20) { + throw new Error(errorMessage); + } + + return bytesToHex(value); +}; + +/** + * Normalizes an address into a lowercased hex string. + * + * @param value - The address as a hex string or bytes. + * @param errorMessage - Error message used for invalid input. + * @returns The address as a lowercased 0x-prefixed hex string. + * @throws Error if the input is not a 20-byte address. + */ +export const normalizeAddressLowercase = ( + value: BytesLike, + errorMessage: string, +): string => { + if (typeof value === 'string') { + if (!isHexString(value) || value.length !== 42) { + throw new Error(errorMessage); + } + return bytesToHex(hexToBytes(value)); + } + + if (value.length !== 20) { + throw new Error(errorMessage); + } + + return bytesToHex(value); +}; + +/** + * Concatenates 0x-prefixed hex strings into a single 0x-prefixed hex string. + * + * @param parts - The hex string parts to concatenate. + * @returns The concatenated hex string. + */ +export const concatHex = (parts: string[]): string => { + return `0x${parts.map(remove0x).join('')}`; +}; diff --git a/packages/delegation-core/src/utils.ts b/packages/delegation-core/src/utils.ts deleted file mode 100644 index 3b3f6f16..00000000 --- a/packages/delegation-core/src/utils.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Converts a numeric value to a hexadecimal string with zero-padding, without 0x prefix. - * - * @param options - The options for the conversion. - * @param options.value - The numeric value to convert to hex (bigint or number). - * @param options.size - The size in bytes for the resulting hex string (each byte = 2 hex characters). - * @returns A hexadecimal string prefixed with zeros to match the specified size. - * @example - * ```typescript - * toHexString({ value: 255, size: 2 }) // Returns "00ff" - * toHexString({ value: 16n, size: 1 }) // Returns "10" - * ``` - */ -export const toHexString = ({ - value, - size, -}: { - value: bigint | number; - size: number; -}): string => { - return value.toString(16).padStart(size * 2, '0'); -}; diff --git a/packages/delegation-core/test/caveats/allowedCalldata.test.ts b/packages/delegation-core/test/caveats/allowedCalldata.test.ts index c417a1d3..8709e653 100644 --- a/packages/delegation-core/test/caveats/allowedCalldata.test.ts +++ b/packages/delegation-core/test/caveats/allowedCalldata.test.ts @@ -2,8 +2,8 @@ import { hexToBytes } from '@metamask/utils'; import { describe, it, expect } from 'vitest'; import { createAllowedCalldataTerms } from '../../src/caveats/allowedCalldata'; +import { toHexString } from '../../src/internalUtils'; import type { Hex } from '../../src/types'; -import { toHexString } from '../../src/utils'; describe('createAllowedCalldataTerms', function () { const prefixWithIndex = (startIndex: number, value: Hex): Hex => { diff --git a/packages/delegation-core/test/caveats/allowedMethods.test.ts b/packages/delegation-core/test/caveats/allowedMethods.test.ts new file mode 100644 index 00000000..1a96011a --- /dev/null +++ b/packages/delegation-core/test/caveats/allowedMethods.test.ts @@ -0,0 +1,52 @@ +import { describe, it, expect } from 'vitest'; + +import { createAllowedMethodsTerms } from '../../src/caveats/allowedMethods'; + +describe('createAllowedMethodsTerms', () => { + const selectorA = '0xa9059cbb'; + const selectorB = '0x70a08231'; + + it('creates valid terms for selectors', () => { + const result = createAllowedMethodsTerms({ + selectors: [selectorA, selectorB], + }); + + expect(result).toStrictEqual('0xa9059cbb70a08231'); + }); + + it('throws for empty selectors array', () => { + expect(() => createAllowedMethodsTerms({ selectors: [] })).toThrow( + 'Invalid selectors: must provide at least one selector', + ); + }); + + it('throws for invalid selector length', () => { + expect(() => + createAllowedMethodsTerms({ + selectors: ['0x123456'], + }), + ).toThrow( + 'Invalid selector: must be a 4 byte hex string, abi function signature, or AbiFunction', + ); + }); + + it('throws for invalid selector bytes length', () => { + expect(() => + createAllowedMethodsTerms({ + selectors: [new Uint8Array([0x12, 0x34, 0x56])], + }), + ).toThrow( + 'Invalid selector: must be a 4 byte hex string, abi function signature, or AbiFunction', + ); + }); + + it('returns Uint8Array when bytes encoding is specified', () => { + const result = createAllowedMethodsTerms( + { selectors: [selectorA, selectorB] }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(8); + }); +}); diff --git a/packages/delegation-core/test/caveats/allowedTargets.test.ts b/packages/delegation-core/test/caveats/allowedTargets.test.ts new file mode 100644 index 00000000..5952ef78 --- /dev/null +++ b/packages/delegation-core/test/caveats/allowedTargets.test.ts @@ -0,0 +1,40 @@ +import { describe, it, expect } from 'vitest'; + +import { createAllowedTargetsTerms } from '../../src/caveats/allowedTargets'; + +describe('createAllowedTargetsTerms', () => { + const addressA = '0x0000000000000000000000000000000000000001'; + const addressB = '0x0000000000000000000000000000000000000002'; + + it('creates valid terms for multiple addresses', () => { + const result = createAllowedTargetsTerms({ targets: [addressA, addressB] }); + + expect(result).toStrictEqual( + '0x00000000000000000000000000000000000000010000000000000000000000000000000000000002', + ); + }); + + it('throws for empty targets array', () => { + expect(() => createAllowedTargetsTerms({ targets: [] })).toThrow( + 'Invalid targets: must provide at least one target address', + ); + }); + + it('throws for invalid address', () => { + expect(() => + createAllowedTargetsTerms({ + targets: ['0x1234'], + }), + ).toThrow('Invalid targets: must be valid addresses'); + }); + + it('returns Uint8Array when bytes encoding is specified', () => { + const result = createAllowedTargetsTerms( + { targets: [addressA, addressB] }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(40); + }); +}); diff --git a/packages/delegation-core/test/caveats/argsEqualityCheck.test.ts b/packages/delegation-core/test/caveats/argsEqualityCheck.test.ts new file mode 100644 index 00000000..e67903d2 --- /dev/null +++ b/packages/delegation-core/test/caveats/argsEqualityCheck.test.ts @@ -0,0 +1,32 @@ +import { describe, it, expect } from 'vitest'; + +import { createArgsEqualityCheckTerms } from '../../src/caveats/argsEqualityCheck'; + +describe('createArgsEqualityCheckTerms', () => { + it('creates valid terms for args', () => { + const args = '0x1234abcd'; + const result = createArgsEqualityCheckTerms({ args }); + + expect(result).toStrictEqual(args); + }); + + it('creates valid terms for empty args', () => { + const result = createArgsEqualityCheckTerms({ args: '0x' }); + + expect(result).toStrictEqual('0x'); + }); + + it('throws for invalid args', () => { + expect(() => + createArgsEqualityCheckTerms({ args: 'not-hex' as any }), + ).toThrow('Invalid config: args must be a valid hex string'); + }); + + it('returns Uint8Array when bytes encoding is specified', () => { + const args = '0x1234abcd'; + const result = createArgsEqualityCheckTerms({ args }, { out: 'bytes' }); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(4); + }); +}); diff --git a/packages/delegation-core/test/caveats/blockNumber.test.ts b/packages/delegation-core/test/caveats/blockNumber.test.ts new file mode 100644 index 00000000..0f10eb74 --- /dev/null +++ b/packages/delegation-core/test/caveats/blockNumber.test.ts @@ -0,0 +1,42 @@ +import { describe, it, expect } from 'vitest'; + +import { createBlockNumberTerms } from '../../src/caveats/blockNumber'; + +describe('createBlockNumberTerms', () => { + it('creates valid terms for thresholds', () => { + const result = createBlockNumberTerms({ + afterThreshold: 5n, + beforeThreshold: 10n, + }); + + expect(result).toStrictEqual( + '0x000000000000000000000000000000050000000000000000000000000000000a', + ); + }); + + it('throws when both thresholds are zero', () => { + expect(() => + createBlockNumberTerms({ afterThreshold: 0n, beforeThreshold: 0n }), + ).toThrow( + 'Invalid thresholds: At least one of afterThreshold or beforeThreshold must be specified', + ); + }); + + it('throws when afterThreshold is greater than or equal to beforeThreshold', () => { + expect(() => + createBlockNumberTerms({ afterThreshold: 10n, beforeThreshold: 5n }), + ).toThrow( + 'Invalid thresholds: afterThreshold must be less than beforeThreshold if both are specified', + ); + }); + + it('returns Uint8Array when bytes encoding is specified', () => { + const result = createBlockNumberTerms( + { afterThreshold: 1n, beforeThreshold: 2n }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(32); + }); +}); diff --git a/packages/delegation-core/test/caveats/deployed.test.ts b/packages/delegation-core/test/caveats/deployed.test.ts new file mode 100644 index 00000000..fd59fde8 --- /dev/null +++ b/packages/delegation-core/test/caveats/deployed.test.ts @@ -0,0 +1,59 @@ +import { describe, it, expect } from 'vitest'; + +import { createDeployedTerms } from '../../src/caveats/deployed'; + +describe('createDeployedTerms', () => { + const contractAddress = '0x00000000000000000000000000000000000000aa'; + const salt = '0x01'; + const bytecode = '0x1234'; + + it('creates valid terms for deployment parameters', () => { + const result = createDeployedTerms({ contractAddress, salt, bytecode }); + + expect(result).toStrictEqual( + '0x00000000000000000000000000000000000000aa' + + '0000000000000000000000000000000000000000000000000000000000000001' + + '1234', + ); + }); + + it('throws for invalid contract address', () => { + expect(() => + createDeployedTerms({ + contractAddress: '0x1234', + salt, + bytecode, + }), + ).toThrow('Invalid contractAddress: must be a valid Ethereum address'); + }); + + it('throws for invalid salt', () => { + expect(() => + createDeployedTerms({ + contractAddress, + salt: 'invalid' as any, + bytecode, + }), + ).toThrow('Invalid salt: must be a valid hexadecimal string'); + }); + + it('throws for invalid bytecode', () => { + expect(() => + createDeployedTerms({ + contractAddress, + salt, + bytecode: 'invalid' as any, + }), + ).toThrow('Invalid bytecode: must be a valid hexadecimal string'); + }); + + it('returns Uint8Array when bytes encoding is specified', () => { + const result = createDeployedTerms( + { contractAddress, salt, bytecode }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(20 + 32 + 2); + }); +}); diff --git a/packages/delegation-core/test/caveats/erc1155BalanceChange.test.ts b/packages/delegation-core/test/caveats/erc1155BalanceChange.test.ts new file mode 100644 index 00000000..f1868b86 --- /dev/null +++ b/packages/delegation-core/test/caveats/erc1155BalanceChange.test.ts @@ -0,0 +1,67 @@ +import { describe, it, expect } from 'vitest'; + +import { createERC1155BalanceChangeTerms } from '../../src/caveats/erc1155BalanceChange'; +import { BalanceChangeType } from '../../src/caveats/types'; + +describe('createERC1155BalanceChangeTerms', () => { + const tokenAddress = '0x00000000000000000000000000000000000000cc'; + const recipient = '0x00000000000000000000000000000000000000dd'; + + it('creates valid terms for balance change', () => { + const result = createERC1155BalanceChangeTerms({ + tokenAddress, + recipient, + tokenId: 7n, + balance: 3n, + changeType: BalanceChangeType.Decrease, + }); + + expect(result).toStrictEqual( + '0x01' + + '00000000000000000000000000000000000000cc' + + '00000000000000000000000000000000000000dd' + + '0000000000000000000000000000000000000000000000000000000000000007' + + '0000000000000000000000000000000000000000000000000000000000000003', + ); + }); + + it('throws for invalid tokenId', () => { + expect(() => + createERC1155BalanceChangeTerms({ + tokenAddress, + recipient, + tokenId: -1n, + balance: 1n, + changeType: BalanceChangeType.Increase, + }), + ).toThrow('Invalid tokenId: must be a non-negative number'); + }); + + it('throws for invalid balance', () => { + expect(() => + createERC1155BalanceChangeTerms({ + tokenAddress, + recipient, + tokenId: 1n, + balance: 0n, + changeType: BalanceChangeType.Increase, + }), + ).toThrow('Invalid balance: must be a positive number'); + }); + + it('returns Uint8Array when bytes encoding is specified', () => { + const result = createERC1155BalanceChangeTerms( + { + tokenAddress, + recipient, + tokenId: 1n, + balance: 1n, + changeType: BalanceChangeType.Increase, + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(105); + }); +}); diff --git a/packages/delegation-core/test/caveats/erc20BalanceChange.test.ts b/packages/delegation-core/test/caveats/erc20BalanceChange.test.ts new file mode 100644 index 00000000..88115deb --- /dev/null +++ b/packages/delegation-core/test/caveats/erc20BalanceChange.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect } from 'vitest'; + +import { createERC20BalanceChangeTerms } from '../../src/caveats/erc20BalanceChange'; +import { BalanceChangeType } from '../../src/caveats/types'; + +describe('createERC20BalanceChangeTerms', () => { + const tokenAddress = '0x00000000000000000000000000000000000000dd'; + const recipient = '0x00000000000000000000000000000000000000ee'; + + it('creates valid terms for balance decrease', () => { + const result = createERC20BalanceChangeTerms({ + tokenAddress, + recipient, + balance: 5n, + changeType: BalanceChangeType.Decrease, + }); + + expect(result).toStrictEqual( + '0x01' + + '00000000000000000000000000000000000000dd' + + '00000000000000000000000000000000000000ee' + + '0000000000000000000000000000000000000000000000000000000000000005', + ); + }); + + it('throws for invalid token address', () => { + expect(() => + createERC20BalanceChangeTerms({ + tokenAddress: '0x1234', + recipient, + balance: 1n, + changeType: BalanceChangeType.Increase, + }), + ).toThrow('Invalid tokenAddress: must be a valid address'); + }); + + it('throws for invalid balance', () => { + expect(() => + createERC20BalanceChangeTerms({ + tokenAddress, + recipient, + balance: 0n, + changeType: BalanceChangeType.Increase, + }), + ).toThrow('Invalid balance: must be a positive number'); + }); + + it('returns Uint8Array when bytes encoding is specified', () => { + const result = createERC20BalanceChangeTerms( + { + tokenAddress, + recipient, + balance: 1n, + changeType: BalanceChangeType.Increase, + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(73); + }); +}); diff --git a/packages/delegation-core/test/caveats/erc20TransferAmount.test.ts b/packages/delegation-core/test/caveats/erc20TransferAmount.test.ts new file mode 100644 index 00000000..10d4bccf --- /dev/null +++ b/packages/delegation-core/test/caveats/erc20TransferAmount.test.ts @@ -0,0 +1,47 @@ +import { describe, it, expect } from 'vitest'; + +import { createERC20TransferAmountTerms } from '../../src/caveats/erc20TransferAmount'; + +describe('createERC20TransferAmountTerms', () => { + const tokenAddress = '0x0000000000000000000000000000000000000011'; + + it('creates valid terms for token and amount', () => { + const result = createERC20TransferAmountTerms({ + tokenAddress, + maxAmount: 10n, + }); + + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000011' + + '000000000000000000000000000000000000000000000000000000000000000a', + ); + }); + + it('throws for invalid token address', () => { + expect(() => + createERC20TransferAmountTerms({ + tokenAddress: '0x1234', + maxAmount: 10n, + }), + ).toThrow('Invalid tokenAddress: must be a valid address'); + }); + + it('throws for invalid maxAmount', () => { + expect(() => + createERC20TransferAmountTerms({ + tokenAddress, + maxAmount: 0n, + }), + ).toThrow('Invalid maxAmount: must be a positive number'); + }); + + it('returns Uint8Array when bytes encoding is specified', () => { + const result = createERC20TransferAmountTerms( + { tokenAddress, maxAmount: 1n }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(52); + }); +}); diff --git a/packages/delegation-core/test/caveats/erc721BalanceChange.test.ts b/packages/delegation-core/test/caveats/erc721BalanceChange.test.ts new file mode 100644 index 00000000..52b4b305 --- /dev/null +++ b/packages/delegation-core/test/caveats/erc721BalanceChange.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect } from 'vitest'; + +import { createERC721BalanceChangeTerms } from '../../src/caveats/erc721BalanceChange'; +import { BalanceChangeType } from '../../src/caveats/types'; + +describe('createERC721BalanceChangeTerms', () => { + const tokenAddress = '0x00000000000000000000000000000000000000aa'; + const recipient = '0x00000000000000000000000000000000000000bb'; + + it('creates valid terms for balance increase', () => { + const result = createERC721BalanceChangeTerms({ + tokenAddress, + recipient, + amount: 1n, + changeType: BalanceChangeType.Increase, + }); + + expect(result).toStrictEqual( + '0x00' + + '00000000000000000000000000000000000000aa' + + '00000000000000000000000000000000000000bb' + + '0000000000000000000000000000000000000000000000000000000000000001', + ); + }); + + it('throws for invalid recipient', () => { + expect(() => + createERC721BalanceChangeTerms({ + tokenAddress, + recipient: '0x1234', + amount: 1n, + changeType: BalanceChangeType.Increase, + }), + ).toThrow('Invalid recipient: must be a valid address'); + }); + + it('throws for invalid amount', () => { + expect(() => + createERC721BalanceChangeTerms({ + tokenAddress, + recipient, + amount: 0n, + changeType: BalanceChangeType.Decrease, + }), + ).toThrow('Invalid balance: must be a positive number'); + }); + + it('returns Uint8Array when bytes encoding is specified', () => { + const result = createERC721BalanceChangeTerms( + { + tokenAddress, + recipient, + amount: 2n, + changeType: BalanceChangeType.Decrease, + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(73); + }); +}); diff --git a/packages/delegation-core/test/caveats/erc721Transfer.test.ts b/packages/delegation-core/test/caveats/erc721Transfer.test.ts new file mode 100644 index 00000000..30b61766 --- /dev/null +++ b/packages/delegation-core/test/caveats/erc721Transfer.test.ts @@ -0,0 +1,47 @@ +import { describe, it, expect } from 'vitest'; + +import { createERC721TransferTerms } from '../../src/caveats/erc721Transfer'; + +describe('createERC721TransferTerms', () => { + const tokenAddress = '0x00000000000000000000000000000000000000aa'; + + it('creates valid terms for token and tokenId', () => { + const result = createERC721TransferTerms({ + tokenAddress, + tokenId: 42n, + }); + + expect(result).toStrictEqual( + '0x00000000000000000000000000000000000000aa' + + '000000000000000000000000000000000000000000000000000000000000002a', + ); + }); + + it('throws for invalid token address', () => { + expect(() => + createERC721TransferTerms({ + tokenAddress: '0x1234', + tokenId: 1n, + }), + ).toThrow('Invalid tokenAddress: must be a valid address'); + }); + + it('throws for negative tokenId', () => { + expect(() => + createERC721TransferTerms({ + tokenAddress, + tokenId: -1n, + }), + ).toThrow('Invalid tokenId: must be a non-negative number'); + }); + + it('returns Uint8Array when bytes encoding is specified', () => { + const result = createERC721TransferTerms( + { tokenAddress, tokenId: 1n }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(52); + }); +}); diff --git a/packages/delegation-core/test/caveats/exactCalldataBatch.test.ts b/packages/delegation-core/test/caveats/exactCalldataBatch.test.ts new file mode 100644 index 00000000..89544c95 --- /dev/null +++ b/packages/delegation-core/test/caveats/exactCalldataBatch.test.ts @@ -0,0 +1,47 @@ +import { encodeSingle } from '@metamask/abi-utils'; +import { bytesToHex } from '@metamask/utils'; +import { describe, it, expect } from 'vitest'; + +import { createExactCalldataBatchTerms } from '../../src/caveats/exactCalldataBatch'; +import type { Hex } from '../../src/types'; + +describe('createExactCalldataBatchTerms', () => { + const targetA: Hex = '0x0000000000000000000000000000000000000003'; + const targetB: Hex = '0x0000000000000000000000000000000000000004'; + + const executions = [ + { target: targetA, value: 0n, callData: '0xdeadbeef' as Hex }, + { target: targetB, value: 5n, callData: '0x' as Hex }, + ]; + + it('creates valid terms for calldata batch', () => { + const result = createExactCalldataBatchTerms({ executions }); + const expected = bytesToHex( + encodeSingle('(address,uint256,bytes)[]', [ + [targetA, 0n, '0xdeadbeef'], + [targetB, 5n, '0x'], + ]), + ); + + expect(result).toStrictEqual(expected); + }); + + it('throws for invalid calldata', () => { + expect(() => + createExactCalldataBatchTerms({ + executions: [ + { target: targetA, value: 0n, callData: 'deadbeef' as any }, + ], + }), + ).toThrow('Invalid calldata: must be a hex string starting with 0x'); + }); + + it('returns Uint8Array when bytes encoding is specified', () => { + const result = createExactCalldataBatchTerms( + { executions }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + }); +}); diff --git a/packages/delegation-core/test/caveats/exactExecution.test.ts b/packages/delegation-core/test/caveats/exactExecution.test.ts new file mode 100644 index 00000000..90a3692c --- /dev/null +++ b/packages/delegation-core/test/caveats/exactExecution.test.ts @@ -0,0 +1,75 @@ +import { describe, it, expect } from 'vitest'; + +import { createExactExecutionTerms } from '../../src/caveats/exactExecution'; + +describe('createExactExecutionTerms', () => { + const target = '0x00000000000000000000000000000000000000ab'; + + it('creates valid terms for execution', () => { + const result = createExactExecutionTerms({ + execution: { + target, + value: 1n, + callData: '0x1234', + }, + }); + + expect(result).toStrictEqual( + '0x00000000000000000000000000000000000000ab' + + '0000000000000000000000000000000000000000000000000000000000000001' + + '1234', + ); + }); + + it('throws for invalid target', () => { + expect(() => + createExactExecutionTerms({ + execution: { + target: '0x1234', + value: 1n, + callData: '0x', + }, + }), + ).toThrow('Invalid target: must be a valid address'); + }); + + it('throws for negative value', () => { + expect(() => + createExactExecutionTerms({ + execution: { + target, + value: -1n, + callData: '0x', + }, + }), + ).toThrow('Invalid value: must be a non-negative number'); + }); + + it('throws for invalid calldata', () => { + expect(() => + createExactExecutionTerms({ + execution: { + target, + value: 0n, + callData: '1234' as any, + }, + }), + ).toThrow('Invalid calldata: must be a hex string starting with 0x'); + }); + + it('returns Uint8Array when bytes encoding is specified', () => { + const result = createExactExecutionTerms( + { + execution: { + target, + value: 1n, + callData: '0x1234', + }, + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(20 + 32 + 2); + }); +}); diff --git a/packages/delegation-core/test/caveats/exactExecutionBatch.test.ts b/packages/delegation-core/test/caveats/exactExecutionBatch.test.ts new file mode 100644 index 00000000..a68e4b45 --- /dev/null +++ b/packages/delegation-core/test/caveats/exactExecutionBatch.test.ts @@ -0,0 +1,51 @@ +import { encodeSingle } from '@metamask/abi-utils'; +import { bytesToHex } from '@metamask/utils'; +import { describe, it, expect } from 'vitest'; + +import { createExactExecutionBatchTerms } from '../../src/caveats/exactExecutionBatch'; +import type { Hex } from '../../src/types'; + +describe('createExactExecutionBatchTerms', () => { + const targetA: Hex = '0x0000000000000000000000000000000000000001'; + const targetB: Hex = '0x0000000000000000000000000000000000000002'; + + const executions = [ + { target: targetA, value: 1n, callData: '0x1234' as Hex }, + { target: targetB, value: 2n, callData: '0x' as Hex }, + ]; + + it('creates valid terms for execution batch', () => { + const result = createExactExecutionBatchTerms({ executions }); + const expected = bytesToHex( + encodeSingle('(address,uint256,bytes)[]', [ + [targetA, 1n, '0x1234'], + [targetB, 2n, '0x'], + ]), + ); + + expect(result).toStrictEqual(expected); + }); + + it('throws for empty executions array', () => { + expect(() => createExactExecutionBatchTerms({ executions: [] })).toThrow( + 'Invalid executions: array cannot be empty', + ); + }); + + it('throws for invalid target', () => { + expect(() => + createExactExecutionBatchTerms({ + executions: [{ target: '0x1234', value: 1n, callData: '0x' }], + }), + ).toThrow('Invalid target: must be a valid address'); + }); + + it('returns Uint8Array when bytes encoding is specified', () => { + const result = createExactExecutionBatchTerms( + { executions }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + }); +}); diff --git a/packages/delegation-core/test/caveats/id.test.ts b/packages/delegation-core/test/caveats/id.test.ts new file mode 100644 index 00000000..382a344d --- /dev/null +++ b/packages/delegation-core/test/caveats/id.test.ts @@ -0,0 +1,46 @@ +import { describe, it, expect } from 'vitest'; + +import { createIdTerms } from '../../src/caveats/id'; + +describe('createIdTerms', () => { + it('creates valid terms for number id', () => { + const result = createIdTerms({ id: 1 }); + + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000000000000000000000000000001', + ); + }); + + it('creates valid terms for bigint id', () => { + const result = createIdTerms({ id: 255n }); + + expect(result).toStrictEqual( + '0x00000000000000000000000000000000000000000000000000000000000000ff', + ); + }); + + it('throws for non-integer number', () => { + expect(() => createIdTerms({ id: 1.5 })).toThrow( + 'Invalid id: must be an integer', + ); + }); + + it('throws for negative id', () => { + expect(() => createIdTerms({ id: -1 })).toThrow( + 'Invalid id: must be a non-negative number', + ); + }); + + it('throws for invalid id type', () => { + expect(() => createIdTerms({ id: '1' as any })).toThrow( + 'Invalid id: must be a bigint or number', + ); + }); + + it('returns Uint8Array when bytes encoding is specified', () => { + const result = createIdTerms({ id: 2 }, { out: 'bytes' }); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(32); + }); +}); diff --git a/packages/delegation-core/test/caveats/limitedCalls.test.ts b/packages/delegation-core/test/caveats/limitedCalls.test.ts new file mode 100644 index 00000000..4cad5e60 --- /dev/null +++ b/packages/delegation-core/test/caveats/limitedCalls.test.ts @@ -0,0 +1,32 @@ +import { describe, it, expect } from 'vitest'; + +import { createLimitedCallsTerms } from '../../src/caveats/limitedCalls'; + +describe('createLimitedCallsTerms', () => { + it('creates valid terms for a positive limit', () => { + const result = createLimitedCallsTerms({ limit: 5 }); + + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000000000000000000000000000005', + ); + }); + + it('throws for non-integer limit', () => { + expect(() => createLimitedCallsTerms({ limit: 1.5 })).toThrow( + 'Invalid limit: must be an integer', + ); + }); + + it('throws for non-positive limit', () => { + expect(() => createLimitedCallsTerms({ limit: 0 })).toThrow( + 'Invalid limit: must be a positive integer', + ); + }); + + it('returns Uint8Array when bytes encoding is specified', () => { + const result = createLimitedCallsTerms({ limit: 3 }, { out: 'bytes' }); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(32); + }); +}); diff --git a/packages/delegation-core/test/caveats/multiTokenPeriod.test.ts b/packages/delegation-core/test/caveats/multiTokenPeriod.test.ts new file mode 100644 index 00000000..c5f35fa0 --- /dev/null +++ b/packages/delegation-core/test/caveats/multiTokenPeriod.test.ts @@ -0,0 +1,82 @@ +import { describe, it, expect } from 'vitest'; + +import { createMultiTokenPeriodTerms } from '../../src/caveats/multiTokenPeriod'; + +describe('createMultiTokenPeriodTerms', () => { + const token = '0x0000000000000000000000000000000000000011'; + + it('creates valid terms for a single token config', () => { + const result = createMultiTokenPeriodTerms({ + tokenConfigs: [ + { + token, + periodAmount: 100n, + periodDuration: 60, + startDate: 10, + }, + ], + }); + + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000011' + + '0000000000000000000000000000000000000000000000000000000000000064' + + '000000000000000000000000000000000000000000000000000000000000003c' + + '000000000000000000000000000000000000000000000000000000000000000a', + ); + }); + + it('throws for empty token configs', () => { + expect(() => createMultiTokenPeriodTerms({ tokenConfigs: [] })).toThrow( + 'MultiTokenPeriodBuilder: tokenConfigs array cannot be empty', + ); + }); + + it('throws for invalid token address', () => { + expect(() => + createMultiTokenPeriodTerms({ + tokenConfigs: [ + { + token: '0x1234', + periodAmount: 1n, + periodDuration: 1, + startDate: 1, + }, + ], + }), + ).toThrow('Invalid token address: 0x1234'); + }); + + it('throws for invalid period amount', () => { + expect(() => + createMultiTokenPeriodTerms({ + tokenConfigs: [ + { + token, + periodAmount: 0n, + periodDuration: 1, + startDate: 1, + }, + ], + }), + ).toThrow('Invalid period amount: must be greater than 0'); + }); + + it('returns Uint8Array when bytes encoding is specified', () => { + const result = createMultiTokenPeriodTerms( + { + tokenConfigs: [ + { + token, + periodAmount: 1n, + periodDuration: 1, + startDate: 1, + }, + ], + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(116); + }); +}); diff --git a/packages/delegation-core/test/caveats/nativeBalanceChange.test.ts b/packages/delegation-core/test/caveats/nativeBalanceChange.test.ts new file mode 100644 index 00000000..877eb06b --- /dev/null +++ b/packages/delegation-core/test/caveats/nativeBalanceChange.test.ts @@ -0,0 +1,66 @@ +import { describe, it, expect } from 'vitest'; + +import { createNativeBalanceChangeTerms } from '../../src/caveats/nativeBalanceChange'; +import { BalanceChangeType } from '../../src/caveats/types'; + +describe('createNativeBalanceChangeTerms', () => { + const recipient = '0x00000000000000000000000000000000000000cc'; + + it('creates valid terms for balance increase', () => { + const result = createNativeBalanceChangeTerms({ + recipient, + balance: 1n, + changeType: BalanceChangeType.Increase, + }); + + expect(result).toStrictEqual( + '0x00' + + '00000000000000000000000000000000000000cc' + + '0000000000000000000000000000000000000000000000000000000000000001', + ); + }); + + it('throws for invalid recipient', () => { + expect(() => + createNativeBalanceChangeTerms({ + recipient: '0x1234', + balance: 1n, + changeType: BalanceChangeType.Increase, + }), + ).toThrow('Invalid recipient: must be a valid Address'); + }); + + it('throws for invalid balance', () => { + expect(() => + createNativeBalanceChangeTerms({ + recipient, + balance: 0n, + changeType: BalanceChangeType.Increase, + }), + ).toThrow('Invalid balance: must be a positive number'); + }); + + it('throws for invalid changeType', () => { + expect(() => + createNativeBalanceChangeTerms({ + recipient, + balance: 1n, + changeType: 2 as any, + }), + ).toThrow('Invalid changeType: must be either Increase or Decrease'); + }); + + it('returns Uint8Array when bytes encoding is specified', () => { + const result = createNativeBalanceChangeTerms( + { + recipient, + balance: 2n, + changeType: BalanceChangeType.Decrease, + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(53); + }); +}); diff --git a/packages/delegation-core/test/caveats/nativeTokenPayment.test.ts b/packages/delegation-core/test/caveats/nativeTokenPayment.test.ts new file mode 100644 index 00000000..c52d6e7d --- /dev/null +++ b/packages/delegation-core/test/caveats/nativeTokenPayment.test.ts @@ -0,0 +1,47 @@ +import { describe, it, expect } from 'vitest'; + +import { createNativeTokenPaymentTerms } from '../../src/caveats/nativeTokenPayment'; + +describe('createNativeTokenPaymentTerms', () => { + const recipient = '0x00000000000000000000000000000000000000bb'; + + it('creates valid terms for recipient and amount', () => { + const result = createNativeTokenPaymentTerms({ + recipient, + amount: 10n, + }); + + expect(result).toStrictEqual( + '0x00000000000000000000000000000000000000bb' + + '000000000000000000000000000000000000000000000000000000000000000a', + ); + }); + + it('throws for invalid recipient', () => { + expect(() => + createNativeTokenPaymentTerms({ + recipient: '0x1234', + amount: 1n, + }), + ).toThrow('Invalid recipient: must be a valid address'); + }); + + it('throws for non-positive amount', () => { + expect(() => + createNativeTokenPaymentTerms({ + recipient, + amount: 0n, + }), + ).toThrow('Invalid amount: must be positive'); + }); + + it('returns Uint8Array when bytes encoding is specified', () => { + const result = createNativeTokenPaymentTerms( + { recipient, amount: 5n }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(52); + }); +}); diff --git a/packages/delegation-core/test/caveats/nativeTokenTransferAmount.test.ts b/packages/delegation-core/test/caveats/nativeTokenTransferAmount.test.ts new file mode 100644 index 00000000..ff8b9039 --- /dev/null +++ b/packages/delegation-core/test/caveats/nativeTokenTransferAmount.test.ts @@ -0,0 +1,37 @@ +import { describe, it, expect } from 'vitest'; + +import { createNativeTokenTransferAmountTerms } from '../../src/caveats/nativeTokenTransferAmount'; + +describe('createNativeTokenTransferAmountTerms', () => { + it('creates valid terms for zero amount', () => { + const result = createNativeTokenTransferAmountTerms({ maxAmount: 0n }); + + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000000000000000000000000000000', + ); + }); + + it('creates valid terms for positive amount', () => { + const result = createNativeTokenTransferAmountTerms({ maxAmount: 100n }); + + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000000000000000000000000000064', + ); + }); + + it('throws for negative amount', () => { + expect(() => + createNativeTokenTransferAmountTerms({ maxAmount: -1n }), + ).toThrow('Invalid maxAmount: must be zero or positive'); + }); + + it('returns Uint8Array when bytes encoding is specified', () => { + const result = createNativeTokenTransferAmountTerms( + { maxAmount: 1n }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(32); + }); +}); diff --git a/packages/delegation-core/test/caveats/ownershipTransfer.test.ts b/packages/delegation-core/test/caveats/ownershipTransfer.test.ts new file mode 100644 index 00000000..61305b75 --- /dev/null +++ b/packages/delegation-core/test/caveats/ownershipTransfer.test.ts @@ -0,0 +1,29 @@ +import { describe, it, expect } from 'vitest'; + +import { createOwnershipTransferTerms } from '../../src/caveats/ownershipTransfer'; + +describe('createOwnershipTransferTerms', () => { + const contractAddress = '0x00000000000000000000000000000000000000ff'; + + it('creates valid terms for contract address', () => { + const result = createOwnershipTransferTerms({ contractAddress }); + + expect(result).toStrictEqual(contractAddress); + }); + + it('throws for invalid contract address', () => { + expect(() => + createOwnershipTransferTerms({ contractAddress: '0x1234' }), + ).toThrow('Invalid contractAddress: must be a valid address'); + }); + + it('returns Uint8Array when bytes encoding is specified', () => { + const result = createOwnershipTransferTerms( + { contractAddress }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(20); + }); +}); diff --git a/packages/delegation-core/test/caveats/redeemer.test.ts b/packages/delegation-core/test/caveats/redeemer.test.ts new file mode 100644 index 00000000..1264a09c --- /dev/null +++ b/packages/delegation-core/test/caveats/redeemer.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect } from 'vitest'; + +import { createRedeemerTerms } from '../../src/caveats/redeemer'; + +describe('createRedeemerTerms', () => { + const redeemerA = '0x0000000000000000000000000000000000000001'; + const redeemerB = '0x0000000000000000000000000000000000000002'; + + it('creates valid terms for redeemers', () => { + const result = createRedeemerTerms({ redeemers: [redeemerA, redeemerB] }); + + expect(result).toStrictEqual( + '0x00000000000000000000000000000000000000010000000000000000000000000000000000000002', + ); + }); + + it('throws for empty redeemers', () => { + expect(() => createRedeemerTerms({ redeemers: [] })).toThrow( + 'Invalid redeemers: must specify at least one redeemer address', + ); + }); + + it('throws for invalid redeemer address', () => { + expect(() => createRedeemerTerms({ redeemers: ['0x1234'] })).toThrow( + 'Invalid redeemers: must be a valid address', + ); + }); + + it('returns Uint8Array when bytes encoding is specified', () => { + const result = createRedeemerTerms( + { redeemers: [redeemerA, redeemerB] }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(40); + }); +}); diff --git a/packages/delegation-core/test/caveats/specificActionERC20TransferBatch.test.ts b/packages/delegation-core/test/caveats/specificActionERC20TransferBatch.test.ts new file mode 100644 index 00000000..af5838bc --- /dev/null +++ b/packages/delegation-core/test/caveats/specificActionERC20TransferBatch.test.ts @@ -0,0 +1,67 @@ +import { describe, it, expect } from 'vitest'; + +import { createSpecificActionERC20TransferBatchTerms } from '../../src/caveats/specificActionERC20TransferBatch'; + +describe('createSpecificActionERC20TransferBatchTerms', () => { + const tokenAddress = '0x0000000000000000000000000000000000000011'; + const recipient = '0x0000000000000000000000000000000000000022'; + const target = '0x0000000000000000000000000000000000000033'; + + it('creates valid terms for specific action batch', () => { + const result = createSpecificActionERC20TransferBatchTerms({ + tokenAddress, + recipient, + amount: 1n, + target, + calldata: '0x1234', + }); + + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000011' + + '0000000000000000000000000000000000000022' + + '0000000000000000000000000000000000000000000000000000000000000001' + + '0000000000000000000000000000000000000033' + + '1234', + ); + }); + + it('throws for invalid token address', () => { + expect(() => + createSpecificActionERC20TransferBatchTerms({ + tokenAddress: '0x1234', + recipient, + amount: 1n, + target, + calldata: '0x', + }), + ).toThrow('Invalid tokenAddress: must be a valid address'); + }); + + it('throws for invalid amount', () => { + expect(() => + createSpecificActionERC20TransferBatchTerms({ + tokenAddress, + recipient, + amount: 0n, + target, + calldata: '0x', + }), + ).toThrow('Invalid amount: must be a positive number'); + }); + + it('returns Uint8Array when bytes encoding is specified', () => { + const result = createSpecificActionERC20TransferBatchTerms( + { + tokenAddress, + recipient, + amount: 2n, + target, + calldata: '0x1234', + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(94); + }); +}); diff --git a/packages/delegation-core/test/internalUtils.test.ts b/packages/delegation-core/test/internalUtils.test.ts new file mode 100644 index 00000000..c46989c1 --- /dev/null +++ b/packages/delegation-core/test/internalUtils.test.ts @@ -0,0 +1,87 @@ +import { describe, it, expect } from 'vitest'; + +import { + concatHex, + normalizeAddress, + normalizeAddressLowercase, + normalizeHex, +} from '../src/internalUtils'; + +describe('internal utils', () => { + describe('normalizeHex', () => { + it('returns a valid hex string as-is', () => { + const value = '0x1234'; + expect(normalizeHex(value, 'invalid')).toStrictEqual(value); + }); + + it('converts Uint8Array to hex', () => { + const value = new Uint8Array([0x12, 0x34, 0xab]); + expect(normalizeHex(value, 'invalid')).toStrictEqual('0x1234ab'); + }); + + it('throws for invalid hex string', () => { + expect(() => normalizeHex('not-hex' as any, 'invalid')).toThrow( + 'invalid', + ); + }); + }); + + describe('normalizeAddress', () => { + it('accepts a valid address string without changing casing', () => { + const value = '0x1234567890abcdefABCDEF1234567890abcdef12'; + expect(normalizeAddress(value, 'invalid')).toStrictEqual(value); + }); + + it('accepts a 20-byte Uint8Array address', () => { + const value = new Uint8Array(20).fill(0x11); + expect(normalizeAddress(value, 'invalid')).toStrictEqual( + '0x1111111111111111111111111111111111111111', + ); + }); + + it('throws for invalid address length', () => { + expect(() => normalizeAddress('0x1234' as any, 'invalid')).toThrow( + 'invalid', + ); + }); + + it('throws for invalid byte length', () => { + expect(() => normalizeAddress(new Uint8Array(19), 'invalid')).toThrow( + 'invalid', + ); + }); + }); + + describe('normalizeAddressLowercase', () => { + it('lowercases a valid address string', () => { + const value = '0x1234567890abcdefABCDEF1234567890abcdef12'; + expect(normalizeAddressLowercase(value, 'invalid')).toStrictEqual( + '0x1234567890abcdefabcdef1234567890abcdef12', + ); + }); + + it('accepts a 20-byte Uint8Array address', () => { + const value = new Uint8Array(20).fill(0xaa); + expect(normalizeAddressLowercase(value, 'invalid')).toStrictEqual( + '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + ); + }); + + it('throws for invalid address length', () => { + expect(() => + normalizeAddressLowercase('0x1234' as any, 'invalid'), + ).toThrow('invalid'); + }); + }); + + describe('concatHex', () => { + it('concatenates hex strings with or without 0x prefix', () => { + const result = concatHex(['0x12', '34', '0x56']); + expect(result).toStrictEqual('0x123456'); + }); + + it('returns 0x for empty parts', () => { + expect(concatHex([])).toStrictEqual('0x'); + }); + }); +}); From d99c514073b6ed12e34ac61a1d0a3aa46d95c949 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Fri, 30 Jan 2026 11:41:46 +1300 Subject: [PATCH 2/4] Update CaveatBuilders to use underlying terms builders from delegation-core --- .../caveatBuilder/allowedMethodsBuilder.ts | 5 ++-- .../caveatBuilder/allowedTargetsBuilder.ts | 5 ++-- .../caveatBuilder/argsEqualityCheckBuilder.ts | 3 ++- .../src/caveatBuilder/blockNumberBuilder.ts | 11 ++------- .../src/caveatBuilder/deployedBuilder.ts | 5 ++-- .../erc1155BalanceChangeBuilder.ts | 14 +++++++---- .../erc20BalanceChangeBuilder.ts | 13 +++++++---- .../erc20TransferAmountBuilder.ts | 5 ++-- .../erc721BalanceChangeBuilder.ts | 13 +++++++---- .../caveatBuilder/erc721TransferBuilder.ts | 5 ++-- .../exactCalldataBatchBuilder.ts | 18 +++------------ .../exactExecutionBatchBuilder.ts | 18 +++------------ .../caveatBuilder/exactExecutionBuilder.ts | 9 +++----- .../src/caveatBuilder/idBuilder.ts | 5 ++-- .../src/caveatBuilder/limitedCallsBuilder.ts | 4 ++-- .../caveatBuilder/multiTokenPeriodBuilder.ts | 23 ++++--------------- .../nativeBalanceChangeBuilder.ts | 12 ++++++---- .../nativeTokenPaymentBuilder.ts | 5 ++-- .../nativeTokenTransferAmountBuilder.ts | 4 ++-- .../caveatBuilder/ownershipTransferBuilder.ts | 3 ++- .../src/caveatBuilder/redeemerBuilder.ts | 5 ++-- ...specificActionERC20TransferBatchBuilder.ts | 9 ++++---- 22 files changed, 85 insertions(+), 109 deletions(-) diff --git a/packages/smart-accounts-kit/src/caveatBuilder/allowedMethodsBuilder.ts b/packages/smart-accounts-kit/src/caveatBuilder/allowedMethodsBuilder.ts index 68e92534..d9a1cfc8 100644 --- a/packages/smart-accounts-kit/src/caveatBuilder/allowedMethodsBuilder.ts +++ b/packages/smart-accounts-kit/src/caveatBuilder/allowedMethodsBuilder.ts @@ -1,4 +1,5 @@ -import { isHex, concat, toFunctionSelector } from 'viem'; +import { createAllowedMethodsTerms } from '@metamask/delegation-core'; +import { isHex, toFunctionSelector } from 'viem'; import type { AbiFunction, Hex } from 'viem'; import type { Caveat, SmartAccountsEnvironment } from '../types'; @@ -38,7 +39,7 @@ export const allowedMethodsBuilder = ( const parsedSelectors = selectors.map(parseSelector); - const terms = concat(parsedSelectors); + const terms = createAllowedMethodsTerms({ selectors: parsedSelectors }); const { caveatEnforcers: { AllowedMethodsEnforcer }, diff --git a/packages/smart-accounts-kit/src/caveatBuilder/allowedTargetsBuilder.ts b/packages/smart-accounts-kit/src/caveatBuilder/allowedTargetsBuilder.ts index c0a2ad88..fcd1e15c 100644 --- a/packages/smart-accounts-kit/src/caveatBuilder/allowedTargetsBuilder.ts +++ b/packages/smart-accounts-kit/src/caveatBuilder/allowedTargetsBuilder.ts @@ -1,4 +1,5 @@ -import { concat, isAddress, type Address } from 'viem'; +import { createAllowedTargetsTerms } from '@metamask/delegation-core'; +import { isAddress, type Address } from 'viem'; import type { Caveat, SmartAccountsEnvironment } from '../types'; @@ -41,7 +42,7 @@ export const allowedTargetsBuilder = ( throw new Error('Invalid targets: must be valid addresses'); } - const terms = concat(targets); + const terms = createAllowedTargetsTerms({ targets }); const { caveatEnforcers: { AllowedTargetsEnforcer }, diff --git a/packages/smart-accounts-kit/src/caveatBuilder/argsEqualityCheckBuilder.ts b/packages/smart-accounts-kit/src/caveatBuilder/argsEqualityCheckBuilder.ts index 83064e10..0234e408 100644 --- a/packages/smart-accounts-kit/src/caveatBuilder/argsEqualityCheckBuilder.ts +++ b/packages/smart-accounts-kit/src/caveatBuilder/argsEqualityCheckBuilder.ts @@ -1,3 +1,4 @@ +import { createArgsEqualityCheckTerms } from '@metamask/delegation-core'; import { type Hex, isHex } from 'viem'; import type { SmartAccountsEnvironment, Caveat } from '../types'; @@ -38,7 +39,7 @@ export const argsEqualityCheckBuilder = ( return { enforcer: ArgsEqualityCheckEnforcer, - terms: args, + terms: createArgsEqualityCheckTerms({ args }), args: '0x00', }; }; diff --git a/packages/smart-accounts-kit/src/caveatBuilder/blockNumberBuilder.ts b/packages/smart-accounts-kit/src/caveatBuilder/blockNumberBuilder.ts index 1a9770c4..99d3e033 100644 --- a/packages/smart-accounts-kit/src/caveatBuilder/blockNumberBuilder.ts +++ b/packages/smart-accounts-kit/src/caveatBuilder/blockNumberBuilder.ts @@ -1,4 +1,4 @@ -import { concat, toHex } from 'viem'; +import { createBlockNumberTerms } from '@metamask/delegation-core'; import type { SmartAccountsEnvironment, Caveat } from '../types'; @@ -43,14 +43,7 @@ export const blockNumberBuilder = ( ); } - const terms = concat([ - toHex(afterThreshold, { - size: 16, - }), - toHex(beforeThreshold, { - size: 16, - }), - ]); + const terms = createBlockNumberTerms({ afterThreshold, beforeThreshold }); const { caveatEnforcers: { BlockNumberEnforcer }, diff --git a/packages/smart-accounts-kit/src/caveatBuilder/deployedBuilder.ts b/packages/smart-accounts-kit/src/caveatBuilder/deployedBuilder.ts index e6538f2a..e1a2d358 100644 --- a/packages/smart-accounts-kit/src/caveatBuilder/deployedBuilder.ts +++ b/packages/smart-accounts-kit/src/caveatBuilder/deployedBuilder.ts @@ -1,4 +1,5 @@ -import { concat, isAddress, isHex, pad, type Address, type Hex } from 'viem'; +import { createDeployedTerms } from '@metamask/delegation-core'; +import { isAddress, isHex, type Address, type Hex } from 'viem'; import type { Caveat, SmartAccountsEnvironment } from '../types'; @@ -48,7 +49,7 @@ export const deployedBuilder = ( throw new Error('Invalid bytecode: must be a valid hexadecimal string'); } - const terms = concat([contractAddress, pad(salt, { size: 32 }), bytecode]); + const terms = createDeployedTerms({ contractAddress, salt, bytecode }); const { caveatEnforcers: { DeployedEnforcer }, diff --git a/packages/smart-accounts-kit/src/caveatBuilder/erc1155BalanceChangeBuilder.ts b/packages/smart-accounts-kit/src/caveatBuilder/erc1155BalanceChangeBuilder.ts index 07dfcde7..c3746a4e 100644 --- a/packages/smart-accounts-kit/src/caveatBuilder/erc1155BalanceChangeBuilder.ts +++ b/packages/smart-accounts-kit/src/caveatBuilder/erc1155BalanceChangeBuilder.ts @@ -1,4 +1,5 @@ -import { type Address, isAddress, encodePacked } from 'viem'; +import { createERC1155BalanceChangeTerms } from '@metamask/delegation-core'; +import { type Address, isAddress } from 'viem'; import type { SmartAccountsEnvironment, Caveat } from '../types'; import { BalanceChangeType } from './types'; @@ -67,10 +68,13 @@ export const erc1155BalanceChangeBuilder = ( throw new Error('Invalid changeType: must be either Increase or Decrease'); } - const terms = encodePacked( - ['uint8', 'address', 'address', 'uint256', 'uint256'], - [changeType, tokenAddress, recipient, tokenId, balance], - ); + const terms = createERC1155BalanceChangeTerms({ + tokenAddress, + recipient, + tokenId, + balance, + changeType, + }); const { caveatEnforcers: { ERC1155BalanceChangeEnforcer }, diff --git a/packages/smart-accounts-kit/src/caveatBuilder/erc20BalanceChangeBuilder.ts b/packages/smart-accounts-kit/src/caveatBuilder/erc20BalanceChangeBuilder.ts index db2263fa..5dcc982c 100644 --- a/packages/smart-accounts-kit/src/caveatBuilder/erc20BalanceChangeBuilder.ts +++ b/packages/smart-accounts-kit/src/caveatBuilder/erc20BalanceChangeBuilder.ts @@ -1,4 +1,5 @@ -import { type Address, isAddress, encodePacked } from 'viem'; +import { createERC20BalanceChangeTerms } from '@metamask/delegation-core'; +import { type Address, isAddress } from 'viem'; import type { SmartAccountsEnvironment, Caveat } from '../types'; import { BalanceChangeType } from './types'; @@ -55,10 +56,12 @@ export const erc20BalanceChangeBuilder = ( throw new Error('Invalid changeType: must be either Increase or Decrease'); } - const terms = encodePacked( - ['uint8', 'address', 'address', 'uint256'], - [changeType, tokenAddress, recipient, balance], - ); + const terms = createERC20BalanceChangeTerms({ + tokenAddress, + recipient, + balance, + changeType, + }); const { caveatEnforcers: { ERC20BalanceChangeEnforcer }, diff --git a/packages/smart-accounts-kit/src/caveatBuilder/erc20TransferAmountBuilder.ts b/packages/smart-accounts-kit/src/caveatBuilder/erc20TransferAmountBuilder.ts index 63eb99f9..ca2f2563 100644 --- a/packages/smart-accounts-kit/src/caveatBuilder/erc20TransferAmountBuilder.ts +++ b/packages/smart-accounts-kit/src/caveatBuilder/erc20TransferAmountBuilder.ts @@ -1,5 +1,6 @@ +import { createERC20TransferAmountTerms } from '@metamask/delegation-core'; import type { Address } from 'viem'; -import { concat, isAddress, toHex } from 'viem'; +import { isAddress } from 'viem'; import type { Caveat, SmartAccountsEnvironment } from '../types'; @@ -38,7 +39,7 @@ export const erc20TransferAmountBuilder = ( throw new Error('Invalid maxAmount: must be a positive number'); } - const terms = concat([tokenAddress, toHex(maxAmount, { size: 32 })]); + const terms = createERC20TransferAmountTerms({ tokenAddress, maxAmount }); const { caveatEnforcers: { ERC20TransferAmountEnforcer }, diff --git a/packages/smart-accounts-kit/src/caveatBuilder/erc721BalanceChangeBuilder.ts b/packages/smart-accounts-kit/src/caveatBuilder/erc721BalanceChangeBuilder.ts index 7be2a55a..1509707d 100644 --- a/packages/smart-accounts-kit/src/caveatBuilder/erc721BalanceChangeBuilder.ts +++ b/packages/smart-accounts-kit/src/caveatBuilder/erc721BalanceChangeBuilder.ts @@ -1,4 +1,5 @@ -import { type Address, isAddress, encodePacked } from 'viem'; +import { createERC721BalanceChangeTerms } from '@metamask/delegation-core'; +import { type Address, isAddress } from 'viem'; import type { SmartAccountsEnvironment, Caveat } from '../types'; import { BalanceChangeType } from './types'; @@ -59,10 +60,12 @@ export const erc721BalanceChangeBuilder = ( throw new Error('Invalid changeType: must be either Increase or Decrease'); } - const terms = encodePacked( - ['uint8', 'address', 'address', 'uint256'], - [changeType, tokenAddress, recipient, amount], - ); + const terms = createERC721BalanceChangeTerms({ + tokenAddress, + recipient, + amount, + changeType, + }); const { caveatEnforcers: { ERC721BalanceChangeEnforcer }, diff --git a/packages/smart-accounts-kit/src/caveatBuilder/erc721TransferBuilder.ts b/packages/smart-accounts-kit/src/caveatBuilder/erc721TransferBuilder.ts index 9eee6d39..96ad9184 100644 --- a/packages/smart-accounts-kit/src/caveatBuilder/erc721TransferBuilder.ts +++ b/packages/smart-accounts-kit/src/caveatBuilder/erc721TransferBuilder.ts @@ -1,4 +1,5 @@ -import { type Address, isAddress, toHex, concat } from 'viem'; +import { createERC721TransferTerms } from '@metamask/delegation-core'; +import { type Address, isAddress } from 'viem'; import type { SmartAccountsEnvironment, Caveat } from '../types'; @@ -37,7 +38,7 @@ export const erc721TransferBuilder = ( throw new Error('Invalid tokenId: must be a non-negative number'); } - const terms = concat([tokenAddress, toHex(tokenId, { size: 32 })]); + const terms = createERC721TransferTerms({ tokenAddress, tokenId }); const { caveatEnforcers: { ERC721TransferEnforcer }, diff --git a/packages/smart-accounts-kit/src/caveatBuilder/exactCalldataBatchBuilder.ts b/packages/smart-accounts-kit/src/caveatBuilder/exactCalldataBatchBuilder.ts index 9003213d..0b294f32 100644 --- a/packages/smart-accounts-kit/src/caveatBuilder/exactCalldataBatchBuilder.ts +++ b/packages/smart-accounts-kit/src/caveatBuilder/exactCalldataBatchBuilder.ts @@ -1,4 +1,5 @@ -import { encodeAbiParameters, isAddress } from 'viem'; +import { createExactCalldataBatchTerms } from '@metamask/delegation-core'; +import { isAddress } from 'viem'; import type { ExecutionStruct } from '../executions'; import type { Caveat, SmartAccountsEnvironment } from '../types'; @@ -50,20 +51,7 @@ export const exactCalldataBatchBuilder = ( } } - // Encode the executions using the approach implemented in ExecutionLib.sol encodeBatch() - const terms = encodeAbiParameters( - [ - { - type: 'tuple[]', - components: [ - { type: 'address', name: 'target' }, - { type: 'uint256', name: 'value' }, - { type: 'bytes', name: 'callData' }, - ], - }, - ], - [executions], - ); + const terms = createExactCalldataBatchTerms({ executions }); const { caveatEnforcers: { ExactCalldataBatchEnforcer }, diff --git a/packages/smart-accounts-kit/src/caveatBuilder/exactExecutionBatchBuilder.ts b/packages/smart-accounts-kit/src/caveatBuilder/exactExecutionBatchBuilder.ts index 18a790c5..cf15aead 100644 --- a/packages/smart-accounts-kit/src/caveatBuilder/exactExecutionBatchBuilder.ts +++ b/packages/smart-accounts-kit/src/caveatBuilder/exactExecutionBatchBuilder.ts @@ -1,4 +1,5 @@ -import { encodeAbiParameters, isAddress } from 'viem'; +import { createExactExecutionBatchTerms } from '@metamask/delegation-core'; +import { isAddress } from 'viem'; import type { ExecutionStruct } from '../executions'; import type { Caveat, SmartAccountsEnvironment } from '../types'; @@ -50,20 +51,7 @@ export const exactExecutionBatchBuilder = ( } } - // Encode the executions using the approach implemented in ExecutionLib.sol encodeBatch() - const terms = encodeAbiParameters( - [ - { - type: 'tuple[]', - components: [ - { type: 'address', name: 'target' }, - { type: 'uint256', name: 'value' }, - { type: 'bytes', name: 'callData' }, - ], - }, - ], - [executions], - ); + const terms = createExactExecutionBatchTerms({ executions }); const { caveatEnforcers: { ExactExecutionBatchEnforcer }, diff --git a/packages/smart-accounts-kit/src/caveatBuilder/exactExecutionBuilder.ts b/packages/smart-accounts-kit/src/caveatBuilder/exactExecutionBuilder.ts index 8affb6a0..4a879661 100644 --- a/packages/smart-accounts-kit/src/caveatBuilder/exactExecutionBuilder.ts +++ b/packages/smart-accounts-kit/src/caveatBuilder/exactExecutionBuilder.ts @@ -1,4 +1,5 @@ -import { isAddress, concat, toHex } from 'viem'; +import { createExactExecutionTerms } from '@metamask/delegation-core'; +import { isAddress } from 'viem'; import type { ExecutionStruct } from '../executions'; import type { Caveat, SmartAccountsEnvironment } from '../types'; @@ -41,11 +42,7 @@ export const exactExecutionBuilder = ( throw new Error('Invalid calldata: must be a hex string starting with 0x'); } - const terms = concat([ - execution.target, - toHex(execution.value, { size: 32 }), - execution.callData, - ]); + const terms = createExactExecutionTerms({ execution }); const { caveatEnforcers: { ExactExecutionEnforcer }, diff --git a/packages/smart-accounts-kit/src/caveatBuilder/idBuilder.ts b/packages/smart-accounts-kit/src/caveatBuilder/idBuilder.ts index 972caa10..c1b769a3 100644 --- a/packages/smart-accounts-kit/src/caveatBuilder/idBuilder.ts +++ b/packages/smart-accounts-kit/src/caveatBuilder/idBuilder.ts @@ -1,4 +1,5 @@ -import { maxUint256, toHex } from 'viem'; +import { createIdTerms } from '@metamask/delegation-core'; +import { maxUint256 } from 'viem'; import type { SmartAccountsEnvironment, Caveat } from '../types'; @@ -47,7 +48,7 @@ export const idBuilder = ( throw new Error('Invalid id: must be less than 2^256'); } - const terms = toHex(idBigInt, { size: 32 }); + const terms = createIdTerms({ id: idBigInt }); const { caveatEnforcers: { IdEnforcer }, diff --git a/packages/smart-accounts-kit/src/caveatBuilder/limitedCallsBuilder.ts b/packages/smart-accounts-kit/src/caveatBuilder/limitedCallsBuilder.ts index fbe03fca..eb71fa5c 100644 --- a/packages/smart-accounts-kit/src/caveatBuilder/limitedCallsBuilder.ts +++ b/packages/smart-accounts-kit/src/caveatBuilder/limitedCallsBuilder.ts @@ -1,4 +1,4 @@ -import { type Hex, toHex, pad } from 'viem'; +import { createLimitedCallsTerms } from '@metamask/delegation-core'; import type { SmartAccountsEnvironment, Caveat } from '../types'; @@ -33,7 +33,7 @@ export const limitedCallsBuilder = ( throw new Error('Invalid limit: must be a positive integer'); } - const terms: Hex = pad(toHex(limit), { size: 32 }); + const terms = createLimitedCallsTerms({ limit }); const { caveatEnforcers: { LimitedCallsEnforcer }, diff --git a/packages/smart-accounts-kit/src/caveatBuilder/multiTokenPeriodBuilder.ts b/packages/smart-accounts-kit/src/caveatBuilder/multiTokenPeriodBuilder.ts index 1ac5a1ca..32b90447 100644 --- a/packages/smart-accounts-kit/src/caveatBuilder/multiTokenPeriodBuilder.ts +++ b/packages/smart-accounts-kit/src/caveatBuilder/multiTokenPeriodBuilder.ts @@ -1,5 +1,6 @@ +import { createMultiTokenPeriodTerms } from '@metamask/delegation-core'; import type { Hex } from 'viem'; -import { concat, isAddress, pad, toHex } from 'viem'; +import { isAddress } from 'viem'; import type { SmartAccountsEnvironment, Caveat } from '../types'; @@ -62,23 +63,9 @@ export const multiTokenPeriodBuilder = ( } }); - // Each config requires 116 bytes: - // - 20 bytes for token address - // - 32 bytes for periodAmount - // - 32 bytes for periodDuration - // - 32 bytes for startDate - const termsArray = config.tokenConfigs.reduce( - (acc, { token, periodAmount, periodDuration, startDate }) => [ - ...acc, - pad(token, { size: 20 }), - toHex(periodAmount, { size: 32 }), - toHex(periodDuration, { size: 32 }), - toHex(startDate, { size: 32 }), - ], - [], - ); - - const terms = concat(termsArray); + const terms = createMultiTokenPeriodTerms({ + tokenConfigs: config.tokenConfigs, + }); const { caveatEnforcers: { MultiTokenPeriodEnforcer }, diff --git a/packages/smart-accounts-kit/src/caveatBuilder/nativeBalanceChangeBuilder.ts b/packages/smart-accounts-kit/src/caveatBuilder/nativeBalanceChangeBuilder.ts index 45252d2d..66fc8897 100644 --- a/packages/smart-accounts-kit/src/caveatBuilder/nativeBalanceChangeBuilder.ts +++ b/packages/smart-accounts-kit/src/caveatBuilder/nativeBalanceChangeBuilder.ts @@ -1,4 +1,5 @@ -import { type Address, isAddress, encodePacked } from 'viem'; +import { createNativeBalanceChangeTerms } from '@metamask/delegation-core'; +import { type Address, isAddress } from 'viem'; import type { SmartAccountsEnvironment, Caveat } from '../types'; import { BalanceChangeType } from './types'; @@ -51,10 +52,11 @@ export const nativeBalanceChangeBuilder = ( throw new Error('Invalid changeType: must be either Increase or Decrease'); } - const terms = encodePacked( - ['uint8', 'address', 'uint256'], - [changeType, recipient, balance], - ); + const terms = createNativeBalanceChangeTerms({ + recipient, + balance, + changeType, + }); const { caveatEnforcers: { NativeBalanceChangeEnforcer }, diff --git a/packages/smart-accounts-kit/src/caveatBuilder/nativeTokenPaymentBuilder.ts b/packages/smart-accounts-kit/src/caveatBuilder/nativeTokenPaymentBuilder.ts index 58bb0643..53e81b53 100644 --- a/packages/smart-accounts-kit/src/caveatBuilder/nativeTokenPaymentBuilder.ts +++ b/packages/smart-accounts-kit/src/caveatBuilder/nativeTokenPaymentBuilder.ts @@ -1,4 +1,5 @@ -import { type Address, encodePacked, isAddress } from 'viem'; +import { createNativeTokenPaymentTerms } from '@metamask/delegation-core'; +import { type Address, isAddress } from 'viem'; import type { Caveat, SmartAccountsEnvironment } from '../types'; @@ -37,7 +38,7 @@ export const nativeTokenPaymentBuilder = ( throw new Error('Invalid recipient: must be a valid address'); } - const terms = encodePacked(['address', 'uint256'], [recipient, amount]); + const terms = createNativeTokenPaymentTerms({ recipient, amount }); const { caveatEnforcers: { NativeTokenPaymentEnforcer }, diff --git a/packages/smart-accounts-kit/src/caveatBuilder/nativeTokenTransferAmountBuilder.ts b/packages/smart-accounts-kit/src/caveatBuilder/nativeTokenTransferAmountBuilder.ts index ee8ee229..373e0229 100644 --- a/packages/smart-accounts-kit/src/caveatBuilder/nativeTokenTransferAmountBuilder.ts +++ b/packages/smart-accounts-kit/src/caveatBuilder/nativeTokenTransferAmountBuilder.ts @@ -1,4 +1,4 @@ -import { encodePacked } from 'viem'; +import { createNativeTokenTransferAmountTerms } from '@metamask/delegation-core'; import type { Caveat, SmartAccountsEnvironment } from '../types'; @@ -29,7 +29,7 @@ export const nativeTokenTransferAmountBuilder = ( throw new Error('Invalid maxAmount: must be zero or positive'); } - const terms = encodePacked(['uint256'], [maxAmount]); + const terms = createNativeTokenTransferAmountTerms({ maxAmount }); const { caveatEnforcers: { NativeTokenTransferAmountEnforcer }, diff --git a/packages/smart-accounts-kit/src/caveatBuilder/ownershipTransferBuilder.ts b/packages/smart-accounts-kit/src/caveatBuilder/ownershipTransferBuilder.ts index a51e76b8..7a5de773 100644 --- a/packages/smart-accounts-kit/src/caveatBuilder/ownershipTransferBuilder.ts +++ b/packages/smart-accounts-kit/src/caveatBuilder/ownershipTransferBuilder.ts @@ -1,3 +1,4 @@ +import { createOwnershipTransferTerms } from '@metamask/delegation-core'; import { type Address, isAddress } from 'viem'; import type { SmartAccountsEnvironment, Caveat } from '../types'; @@ -29,7 +30,7 @@ export const ownershipTransferBuilder = ( throw new Error('Invalid contractAddress: must be a valid address'); } - const terms = contractAddress; + const terms = createOwnershipTransferTerms({ contractAddress }); const { caveatEnforcers: { OwnershipTransferEnforcer }, diff --git a/packages/smart-accounts-kit/src/caveatBuilder/redeemerBuilder.ts b/packages/smart-accounts-kit/src/caveatBuilder/redeemerBuilder.ts index dd0da95d..23995549 100644 --- a/packages/smart-accounts-kit/src/caveatBuilder/redeemerBuilder.ts +++ b/packages/smart-accounts-kit/src/caveatBuilder/redeemerBuilder.ts @@ -1,4 +1,5 @@ -import { type Address, concat, isAddress } from 'viem'; +import { createRedeemerTerms } from '@metamask/delegation-core'; +import { type Address, isAddress } from 'viem'; import type { Caveat, SmartAccountsEnvironment } from '../types'; @@ -38,7 +39,7 @@ export const redeemerBuilder = ( } } - const terms = concat(redeemers); + const terms = createRedeemerTerms({ redeemers }); const { caveatEnforcers: { RedeemerEnforcer }, diff --git a/packages/smart-accounts-kit/src/caveatBuilder/specificActionERC20TransferBatchBuilder.ts b/packages/smart-accounts-kit/src/caveatBuilder/specificActionERC20TransferBatchBuilder.ts index 97e32769..e32f7bcd 100644 --- a/packages/smart-accounts-kit/src/caveatBuilder/specificActionERC20TransferBatchBuilder.ts +++ b/packages/smart-accounts-kit/src/caveatBuilder/specificActionERC20TransferBatchBuilder.ts @@ -1,4 +1,5 @@ -import { concat, isAddress, toHex, type Address, type Hex } from 'viem'; +import { createSpecificActionERC20TransferBatchTerms } from '@metamask/delegation-core'; +import { isAddress, type Address, type Hex } from 'viem'; import type { Caveat, SmartAccountsEnvironment } from '../types'; @@ -59,13 +60,13 @@ export const specificActionERC20TransferBatchBuilder = ( throw new Error('Invalid amount: must be a positive number'); } - const terms = concat([ + const terms = createSpecificActionERC20TransferBatchTerms({ tokenAddress, recipient, - toHex(amount, { size: 32 }), + amount, target, calldata, - ]); + }); const { caveatEnforcers: { SpecificActionERC20TransferBatchEnforcer }, From f816d0b73fe92cb653bf505911d2b92e84b1c03e Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Fri, 30 Jan 2026 12:36:53 +1300 Subject: [PATCH 3/4] Add line to CHANGELOG.md --- packages/delegation-core/CHANGELOG.md | 28 ++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/delegation-core/CHANGELOG.md b/packages/delegation-core/CHANGELOG.md index 1819325e..3c29a1b2 100644 --- a/packages/delegation-core/CHANGELOG.md +++ b/packages/delegation-core/CHANGELOG.md @@ -7,7 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Uncategorized +### Added + +- Add terms builders for all enforcers implemented in @metamask/smart-accounts-kit ([#139](https://github.com/metamask/smart-accounts-kit/pull/139)) + - `createAllowedMethodsTerms` + - `createAllowedTargetsTerms` + - `createArgsEqualityCheckTerms` + - `createBlockNumberTerms` + - `createDeployedTerms` + - `createERC1155BalanceChangeTerms` + - `createERC20BalanceChangeTerms` + - `createERC20TransferAmountTerms` + - `createERC721BalanceChangeTerms` + - `createERC721TransferTerms` + - `createExactCalldataBatchTerms` + - `createExactExecutionTerms` + - `createExactExecutionBatchTerms` + - `createIdTerms` + - `createLimitedCallsTerms` + - `createMultiTokenPeriodTerms` + - `createNativeBalanceChangeTerms` + - `createNativeTokenPaymentTerms` + - `createNativeTokenTransferAmountTerms` + - `createOwnershipTransferTerms` + - `createRedeemerTerms` + - `createSpecificActionERC20TransferBatchTerms` + +### Fixed - Resolve yarn peer dependency warnings ([#123](https://github.com/metamask/smart-accounts-kit/pull/123)) From eac9eb75adc8e10c4df7495cfe1e0022f1a272b6 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Fri, 30 Jan 2026 17:27:48 +1300 Subject: [PATCH 4/4] Add additional validation to terms builders --- .../src/caveats/allowedMethods.ts | 2 +- .../src/caveats/allowedTargets.ts | 2 +- .../src/caveats/blockNumber.ts | 4 +++ .../src/caveats/exactCalldata.ts | 4 +++ .../src/caveats/multiTokenPeriod.ts | 4 +++ .../delegation-core/src/caveats/redeemer.ts | 2 +- .../specificActionERC20TransferBatch.ts | 14 +++++++-- .../test/caveats/allowedMethods.test.ts | 8 +++++ .../test/caveats/allowedTargets.test.ts | 8 +++++ .../test/caveats/blockNumber.test.ts | 12 ++++++++ .../test/caveats/exactCalldata.test.ts | 6 ++-- .../test/caveats/multiTokenPeriod.test.ts | 30 +++++++++++++++++++ .../test/caveats/redeemer.test.ts | 6 ++++ .../specificActionERC20TransferBatch.test.ts | 12 ++++++++ 14 files changed, 107 insertions(+), 7 deletions(-) diff --git a/packages/delegation-core/src/caveats/allowedMethods.ts b/packages/delegation-core/src/caveats/allowedMethods.ts index 6257cd17..65af5189 100644 --- a/packages/delegation-core/src/caveats/allowedMethods.ts +++ b/packages/delegation-core/src/caveats/allowedMethods.ts @@ -51,7 +51,7 @@ export function createAllowedMethodsTerms( ): Hex | Uint8Array { const { selectors } = terms; - if (selectors.length === 0) { + if (!selectors || selectors.length === 0) { throw new Error('Invalid selectors: must provide at least one selector'); } diff --git a/packages/delegation-core/src/caveats/allowedTargets.ts b/packages/delegation-core/src/caveats/allowedTargets.ts index 3921b503..eae6056b 100644 --- a/packages/delegation-core/src/caveats/allowedTargets.ts +++ b/packages/delegation-core/src/caveats/allowedTargets.ts @@ -47,7 +47,7 @@ export function createAllowedTargetsTerms( ): Hex | Uint8Array { const { targets } = terms; - if (targets.length === 0) { + if (!targets || targets.length === 0) { throw new Error( 'Invalid targets: must provide at least one target address', ); diff --git a/packages/delegation-core/src/caveats/blockNumber.ts b/packages/delegation-core/src/caveats/blockNumber.ts index 8b8ff930..89c6f6db 100644 --- a/packages/delegation-core/src/caveats/blockNumber.ts +++ b/packages/delegation-core/src/caveats/blockNumber.ts @@ -47,6 +47,10 @@ export function createBlockNumberTerms( ): Hex | Uint8Array { const { afterThreshold, beforeThreshold } = terms; + if (afterThreshold < 0n || beforeThreshold < 0n) { + throw new Error('Invalid thresholds: block numbers must be non-negative'); + } + if (afterThreshold === 0n && beforeThreshold === 0n) { throw new Error( 'Invalid thresholds: At least one of afterThreshold or beforeThreshold must be specified', diff --git a/packages/delegation-core/src/caveats/exactCalldata.ts b/packages/delegation-core/src/caveats/exactCalldata.ts index 950f6125..43b25fd9 100644 --- a/packages/delegation-core/src/caveats/exactCalldata.ts +++ b/packages/delegation-core/src/caveats/exactCalldata.ts @@ -48,6 +48,10 @@ export function createExactCalldataTerms( ): Hex | Uint8Array { const { calldata } = terms; + if (calldata === undefined || calldata === null) { + throw new Error('Invalid calldata: calldata is required'); + } + if (typeof calldata === 'string' && !calldata.startsWith('0x')) { throw new Error('Invalid calldata: must be a hex string starting with 0x'); } diff --git a/packages/delegation-core/src/caveats/multiTokenPeriod.ts b/packages/delegation-core/src/caveats/multiTokenPeriod.ts index d6eadac3..15c85f85 100644 --- a/packages/delegation-core/src/caveats/multiTokenPeriod.ts +++ b/packages/delegation-core/src/caveats/multiTokenPeriod.ts @@ -78,6 +78,10 @@ export function createMultiTokenPeriodTerms( throw new Error('Invalid period duration: must be greater than 0'); } + if (tokenConfig.startDate <= 0) { + throw new Error('Invalid start date: must be greater than 0'); + } + hexParts.push( tokenHex, `0x${toHexString({ value: tokenConfig.periodAmount, size: 32 })}`, diff --git a/packages/delegation-core/src/caveats/redeemer.ts b/packages/delegation-core/src/caveats/redeemer.ts index b9bf42a8..d88d29f2 100644 --- a/packages/delegation-core/src/caveats/redeemer.ts +++ b/packages/delegation-core/src/caveats/redeemer.ts @@ -47,7 +47,7 @@ export function createRedeemerTerms( ): Hex | Uint8Array { const { redeemers } = terms; - if (redeemers.length === 0) { + if (!redeemers || redeemers.length === 0) { throw new Error( 'Invalid redeemers: must specify at least one redeemer address', ); diff --git a/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts b/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts index f73da887..bfc33e13 100644 --- a/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts +++ b/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts @@ -70,13 +70,23 @@ export function createSpecificActionERC20TransferBatchTerms( 'Invalid target: must be a valid address', ); + let calldataHex: string; + if (typeof calldata === 'string') { + if (!calldata.startsWith('0x')) { + throw new Error( + 'Invalid calldata: must be a hex string starting with 0x', + ); + } + calldataHex = calldata; + } else { + calldataHex = bytesToHex(calldata); + } + if (amount <= 0n) { throw new Error('Invalid amount: must be a positive number'); } const amountHex = `0x${toHexString({ value: amount, size: 32 })}`; - const calldataHex = - typeof calldata === 'string' ? calldata : bytesToHex(calldata); const hexValue = concatHex([ tokenAddressHex, diff --git a/packages/delegation-core/test/caveats/allowedMethods.test.ts b/packages/delegation-core/test/caveats/allowedMethods.test.ts index 1a96011a..e6707c50 100644 --- a/packages/delegation-core/test/caveats/allowedMethods.test.ts +++ b/packages/delegation-core/test/caveats/allowedMethods.test.ts @@ -14,6 +14,14 @@ describe('createAllowedMethodsTerms', () => { expect(result).toStrictEqual('0xa9059cbb70a08231'); }); + it('throws when selectors is undefined', () => { + expect(() => + createAllowedMethodsTerms( + {} as Parameters[0], + ), + ).toThrow('Invalid selectors: must provide at least one selector'); + }); + it('throws for empty selectors array', () => { expect(() => createAllowedMethodsTerms({ selectors: [] })).toThrow( 'Invalid selectors: must provide at least one selector', diff --git a/packages/delegation-core/test/caveats/allowedTargets.test.ts b/packages/delegation-core/test/caveats/allowedTargets.test.ts index 5952ef78..e50c6c77 100644 --- a/packages/delegation-core/test/caveats/allowedTargets.test.ts +++ b/packages/delegation-core/test/caveats/allowedTargets.test.ts @@ -14,6 +14,14 @@ describe('createAllowedTargetsTerms', () => { ); }); + it('throws when targets is undefined', () => { + expect(() => + createAllowedTargetsTerms( + {} as Parameters[0], + ), + ).toThrow('Invalid targets: must provide at least one target address'); + }); + it('throws for empty targets array', () => { expect(() => createAllowedTargetsTerms({ targets: [] })).toThrow( 'Invalid targets: must provide at least one target address', diff --git a/packages/delegation-core/test/caveats/blockNumber.test.ts b/packages/delegation-core/test/caveats/blockNumber.test.ts index 0f10eb74..0ac8a05d 100644 --- a/packages/delegation-core/test/caveats/blockNumber.test.ts +++ b/packages/delegation-core/test/caveats/blockNumber.test.ts @@ -14,6 +14,18 @@ describe('createBlockNumberTerms', () => { ); }); + it('throws when afterThreshold is negative', () => { + expect(() => + createBlockNumberTerms({ afterThreshold: -1n, beforeThreshold: 10n }), + ).toThrow('Invalid thresholds: block numbers must be non-negative'); + }); + + it('throws when beforeThreshold is negative', () => { + expect(() => + createBlockNumberTerms({ afterThreshold: 5n, beforeThreshold: -1n }), + ).toThrow('Invalid thresholds: block numbers must be non-negative'); + }); + it('throws when both thresholds are zero', () => { expect(() => createBlockNumberTerms({ afterThreshold: 0n, beforeThreshold: 0n }), diff --git a/packages/delegation-core/test/caveats/exactCalldata.test.ts b/packages/delegation-core/test/caveats/exactCalldata.test.ts index 719de866..0a3c8677 100644 --- a/packages/delegation-core/test/caveats/exactCalldata.test.ts +++ b/packages/delegation-core/test/caveats/exactCalldata.test.ts @@ -84,11 +84,13 @@ describe('createExactCalldataTerms', () => { it('throws an error for undefined callData', () => { expect(() => createExactCalldataTerms({ calldata: undefined as any }), - ).toThrow(); + ).toThrow('Invalid calldata: calldata is required'); }); it('throws an error for null callData', () => { - expect(() => createExactCalldataTerms({ calldata: null as any })).toThrow(); + expect(() => createExactCalldataTerms({ calldata: null as any })).toThrow( + 'Invalid calldata: calldata is required', + ); }); it('throws an error for non-string non-Uint8Array callData', () => { diff --git a/packages/delegation-core/test/caveats/multiTokenPeriod.test.ts b/packages/delegation-core/test/caveats/multiTokenPeriod.test.ts index c5f35fa0..bb3d9131 100644 --- a/packages/delegation-core/test/caveats/multiTokenPeriod.test.ts +++ b/packages/delegation-core/test/caveats/multiTokenPeriod.test.ts @@ -61,6 +61,36 @@ describe('createMultiTokenPeriodTerms', () => { ).toThrow('Invalid period amount: must be greater than 0'); }); + it('throws for invalid start date (zero)', () => { + expect(() => + createMultiTokenPeriodTerms({ + tokenConfigs: [ + { + token, + periodAmount: 1n, + periodDuration: 1, + startDate: 0, + }, + ], + }), + ).toThrow('Invalid start date: must be greater than 0'); + }); + + it('throws for invalid start date (negative)', () => { + expect(() => + createMultiTokenPeriodTerms({ + tokenConfigs: [ + { + token, + periodAmount: 1n, + periodDuration: 1, + startDate: -1, + }, + ], + }), + ).toThrow('Invalid start date: must be greater than 0'); + }); + it('returns Uint8Array when bytes encoding is specified', () => { const result = createMultiTokenPeriodTerms( { diff --git a/packages/delegation-core/test/caveats/redeemer.test.ts b/packages/delegation-core/test/caveats/redeemer.test.ts index 1264a09c..2d598c31 100644 --- a/packages/delegation-core/test/caveats/redeemer.test.ts +++ b/packages/delegation-core/test/caveats/redeemer.test.ts @@ -14,6 +14,12 @@ describe('createRedeemerTerms', () => { ); }); + it('throws when redeemers is undefined', () => { + expect(() => + createRedeemerTerms({} as Parameters[0]), + ).toThrow('Invalid redeemers: must specify at least one redeemer address'); + }); + it('throws for empty redeemers', () => { expect(() => createRedeemerTerms({ redeemers: [] })).toThrow( 'Invalid redeemers: must specify at least one redeemer address', diff --git a/packages/delegation-core/test/caveats/specificActionERC20TransferBatch.test.ts b/packages/delegation-core/test/caveats/specificActionERC20TransferBatch.test.ts index af5838bc..5543432b 100644 --- a/packages/delegation-core/test/caveats/specificActionERC20TransferBatch.test.ts +++ b/packages/delegation-core/test/caveats/specificActionERC20TransferBatch.test.ts @@ -49,6 +49,18 @@ describe('createSpecificActionERC20TransferBatchTerms', () => { ).toThrow('Invalid amount: must be a positive number'); }); + it('throws when calldata string does not start with 0x', () => { + expect(() => + createSpecificActionERC20TransferBatchTerms({ + tokenAddress, + recipient, + amount: 1n, + target, + calldata: '1234' as `0x${string}`, + }), + ).toThrow('Invalid calldata: must be a hex string starting with 0x'); + }); + it('returns Uint8Array when bytes encoding is specified', () => { const result = createSpecificActionERC20TransferBatchTerms( {